import express from "express"; import type { Server } from "node:http"; import { fileURLToPath } from "node:url"; import { AcpBackendRegistry } from "./acp/backend-registry.js"; import { AcpSessionManager } from "./acp/session-manager.js"; import { defaultStateFile, loadConfig, type AppConfig } from "./config.js"; import type { PlatformAdapter } from "./core/adapter.js"; import { DurableSessionStore } from "./core/durable-session-store.js"; import { Gateway } from "./core/gateway.js"; import { PlatformRegistry } from "./core/platform-registry.js"; import { FeishuAdapter } from "./platforms/feishu/adapter.js"; import { QqAdapter } from "./platforms/qq/adapter.js"; import { QqGatewayClient } from "./platforms/qq/gateway-client.js"; import { WeComAdapter } from "./platforms/wecom/adapter.js"; import { GenericWebhookAdapter } from "./platforms/webhook/adapter.js"; import { WeixinAdapter } from "./platforms/weixin/adapter.js"; import { RoleRegistry } from "./roles/role-registry.js"; export interface GatewayRuntime { app: express.Express; gateway: Gateway; sessionManager: AcpSessionManager; store: DurableSessionStore; qqGatewayClient?: QqGatewayClient; shutdown(): Promise; } export async function createGatewayRuntime(config: AppConfig): Promise { const store = new DurableSessionStore(defaultStateFile(config)); await store.open(); const roles = new RoleRegistry(config); const sessionManager = new AcpSessionManager(config.acp, new AcpBackendRegistry(config.backends), roles, store); const gateway = new Gateway(config.policy, sessionManager, roles); const platforms = new PlatformRegistry(); const qqAdapter = new QqAdapter(config.platforms.qq, gateway); const adapters: PlatformAdapter[] = [ new FeishuAdapter(config.platforms.feishu, gateway), new WeComAdapter(config.platforms.wecom), qqAdapter, new GenericWebhookAdapter(config.platforms.webhook, gateway), new WeixinAdapter(config.platforms.weixin, gateway) ]; for (const adapter of adapters) platforms.register(adapter); const app = express(); app.use(express.json({ limit: "1mb", verify: (req, _res, buf) => { (req as express.Request & { rawBody?: Buffer }).rawBody = Buffer.from(buf); } })); app.get("/health", (_req, res) => res.json({ ok: true, acp: gateway.stats() })); app.get("/platforms", (_req, res) => res.json({ ok: true, platforms: platforms.list(), roles: roles.list(), backends: config.backends.map(({ id }) => id) })); function mountWebhook(routeName: string, adapterName = routeName): void { app.post(`/webhook/${routeName}`, async (req, res) => { try { const response = await platforms.get(adapterName).handleWebhook({ req, body: req.body, headers: req.headers, query: req.query, rawBody: (req as express.Request & { rawBody?: Buffer }).rawBody }); if (response.headers) for (const [key, value] of Object.entries(response.headers)) res.setHeader(key, value); res.status(response.status || 200).json(response.body ?? { ok: true }); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Webhook ${routeName} failed`, error); res.status(500).json({ ok: false, error: message }); } }); } for (const name of ["feishu", "wecom", "qq", "weixin"]) mountWebhook(name); mountWebhook("generic", "webhook"); const qqGatewayClient = config.platforms.qq.enabled && config.platforms.qq.connectionMode === "websocket" ? new QqGatewayClient(config.platforms.qq, qqAdapter) : undefined; let closing: Promise | undefined; const runtime: GatewayRuntime = { app, gateway, sessionManager, store, qqGatewayClient, shutdown: () => closing ||= (async () => { qqGatewayClient?.stop(); await sessionManager.shutdown(); await store.close(); })() }; return runtime; } export async function createApp(config: AppConfig): Promise { return (await createGatewayRuntime(config)).app; } export interface RunningServer { server: Server; runtime: GatewayRuntime; shutdown(): Promise } export async function startServer(config: AppConfig): Promise { const runtime = await createGatewayRuntime(config); const server = runtime.app.listen(config.server.port, config.server.host, () => { console.log(`gori-agent listening on ${config.server.host}:${config.server.port}`); if (runtime.qqGatewayClient) { console.log("QQ websocket gateway enabled; connecting to QQ..."); runtime.qqGatewayClient.start(); } }); let closing: Promise | undefined; return { server, runtime, shutdown: () => closing ||= (async () => { runtime.qqGatewayClient?.stop(); const runtimeShutdown = runtime.shutdown(); await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); await runtimeShutdown; })() }; } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { void startServer(loadConfig()).catch((error) => { console.error(error); process.exitCode = 1; }); }