Archived
上一个任务把统一鉴权中心接上了,但缺配置照常起、issuer 配错也照常起 —— 每个请求都 401 还查不出原因。boot() 开头补两道保险:缺 KEYCLOAK_URL/REALM/CLIENT_ID 直接拒绝启动;配了就拉一次 .well-known/openid-configuration 核对 issuer,不一致同样拒绝启动 (话里带期望/实际/KC_HOSTNAME,照后端接入手册第 5 步)。 另加路由覆盖 it 测试:Hono 里每条注册路由都必须能在 appRouteRegistry() 里查到归属,查不到就是一个没人管的权限口子。真库跑通,96 条路由全部 有归属、0 条查不到——把上一个任务里人工核对的结论固化成测试。
67 lines
2.8 KiB
TypeScript
67 lines
2.8 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> => {
|
|
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();
|
|
}
|