/** * Hub entry — Fastify server + Feishu WebSocket listener. * * Wires the full trigger path: lark ws receives `im.message.receive_v1` → * trigger handler resolves chat->project (ADR-0001), creates AgentRun + acquires * the project lock (ADR-0002), runs the AI SDK-backed agent runner * (ADR-0017), and releases the lock on finish. Feishu context reads inside the * loop are ADR-0003-authorized (chat must match binding). * * Env (see .env.example): * DATABASE_URL, ANTHROPIC_AUTH_TOKEN, FEISHU_APP_ID/SECRET/BOT_OPEN_ID, PORT */ import "dotenv/config"; import Fastify from "fastify"; import { prisma } from "./db.js"; import { createEnvRuntimeSettings } from "./settings/runtime.js"; import { createLarkClient, startFeishuListenerWithClient } from "./feishu/client.js"; import { makeTriggerHandler } from "./feishu/trigger.js"; import { triggerQueue } from "./feishu/triggerQueue.js"; function requireEnv(name: string): string { const v = process.env[name]; if (v === undefined || v === "") { console.error(`[hub] missing required env: ${name}`); process.exit(1); } return v; } function booleanEnv(name: string, fallback: boolean): boolean { const raw = process.env[name]; if (raw === undefined || raw === "") return fallback; return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase()); } async function main(): Promise { const databaseUrl = requireEnv("DATABASE_URL"); void databaseUrl; const runtimeSettings = createEnvRuntimeSettings(); await runtimeSettings.provider("openrouter"); const feishuAppId = requireEnv("FEISHU_APP_ID"); const feishuAppSecret = requireEnv("FEISHU_APP_SECRET"); const feishuBotOpenId = requireEnv("FEISHU_BOT_OPEN_ID"); const projectWorkspaceRoot = requireEnv("HUB_PROJECT_WORKSPACE_ROOT"); const port = Number(process.env["PORT"] ?? "8788"); const app = Fastify({ logger: true }); // Startup reset: clear stale locks + mark dead runs as FAILED. await prisma.projectAgentLock.deleteMany({}); await prisma.agentRun.updateMany({ where: { status: "ACTIVE" }, data: { status: "FAILED", error: "process restart", finishedAt: new Date() }, }); app.log.info("startup: cleared stale locks + dead runs"); app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() })); // --- Feishu listener --- const feishuListenerEnabled = booleanEnv("HUB_FEISHU_LISTENER_ENABLED", true); if (feishuListenerEnabled) { const feishuConfig = { appId: feishuAppId, appSecret: feishuAppSecret, botOpenId: feishuBotOpenId }; const larkClient = createLarkClient(feishuConfig); const triggerQueuePurgeTimer = setInterval(() => { const removed = triggerQueue.purgeExpired(); if (removed > 0) { app.log.info({ removed }, "feishu trigger queue: purged expired triggers"); } }, 60_000); triggerQueuePurgeTimer.unref(); const trigger = makeTriggerHandler({ prisma, settings: runtimeSettings, logger: app.log, projectWorkspaceRoot }); startFeishuListenerWithClient(feishuConfig, larkClient, app.log, trigger, trigger.onCardAction); } else { app.log.info("feishu listener disabled by HUB_FEISHU_LISTENER_ENABLED"); } app.listen({ port, host: "0.0.0.0" }, (err, address) => { if (err !== null) { app.log.error({ err }, "listen failed"); process.exit(1); } app.log.info({ address }, "hub listening"); }); } main().catch((e) => { console.error("[hub] fatal:", e); process.exit(1); });