/** * apps/api 唯一组装层入口(编码规范 §5.2)。 * 只有 app/ 能同时认识多个模块:new adapter、注入 port、注册路由 / worker / Mastra 实例。 */ import { serve } from '@hono/node-server'; import { loadConfig } from './config.ts'; import { createContainer } from './container.ts'; import { checkKeycloakConnection } from './keycloak-check.ts'; import { createServer } from './server.ts'; import { createConsumers } from './workers/consumers.ts'; import { dispatchOnce, startDispatcher } from './workers/dispatcher.ts'; import { advanceOnce, startTaskAdvancer } from './workers/task-advancer.ts'; import { registerPulse } from './workers/pulse.ts'; export async function boot(): Promise { const config = loadConfig(); if (config.keycloak === undefined) { process.stderr.write( '✗ 缺 KEYCLOAK_URL / KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID —— 统一鉴权中心是唯一的登录途径,缺了谁也进不来(ADR-012)\n', ); process.exit(1); } try { const info = await checkKeycloakConnection(config.keycloak); process.stdout.write(`✓ Keycloak 连通 · issuer=${info.issuer} · client=${info.clientId}\n`); } catch (error) { // 配置错了就别启动,免得跑起来每个请求都 401 还查不出原因(手册第 5 步) process.stderr.write(`✗ Keycloak 连不上或配置有误:${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } const container = await createContainer(config); await container.org.ensureSeeded(); // 派发器:投递记录即队列,按目标 ID 取 advisory lock 串行消费(ADR-008) const dispatcherDeps = { container, consumers: createConsumers() }; const dispatcher = startDispatcher(dispatcherDeps, config.dispatchIntervalMs); // 过程在后端:运行中的任务由推进器一拍一拍推,不依赖浏览器在线 const advancer = startTaskAdvancer({ container }, config.dispatchIntervalMs); /* * 人一答完就踢一脚 —— **顺序有意义**:先把事件投递下去(任务才醒),再推它一拍。 * 反过来登记的话,推进那一轮跑在投递之前,任务还没醒,白跑一轮, * 人还是要等下一个 2 秒。 */ registerPulse(() => dispatchOnce(dispatcherDeps)); registerPulse(() => advanceOnce({ container })); const app = createServer(container); serve({ fetch: app.fetch, port: config.port }, (info) => { process.stdout.write(`JCAI api · http://localhost:${info.port} · env=${config.nodeEnv}\n`); }); const shutdown = async (): Promise => { dispatcher.stop(); advancer.stop(); await container.close(); process.exit(0); }; process.on('SIGINT', () => void shutdown()); process.on('SIGTERM', () => void shutdown()); } if (import.meta.url === `file://${process.argv[1]}`) { await boot(); }