import { Hono } from 'hono'; import { healthRoute, registerHealthCheck } from './health'; import { checkNats } from '../shared/bus/nats'; import { createKeycloakTokenProvider } from '../shared/service-token'; import { proxyRoutes } from '../modules/proxy/routes'; import { uploadRoutes } from '../modules/upload/routes'; import { SseHub } from '../modules/sse/hub'; import { sseRoutes } from '../modules/sse/routes'; import { createHttpTaskAccess } from '../modules/sse/task-access'; import { createHttpWorkflowAccess } from '../modules/sse/workflow-access'; /** * hub 由外面传:server.ts 要拿同一个实例去 attachNats/shutdown。缺省自建一个, * 让只关心 HTTP 面的测试照旧 buildApp() 就能用。 */ export function buildApp(hub: SseHub = new SseHub()): Hono { registerHealthCheck('nats', checkNats); const app = new Hono(); // 统一错误形状:任何未捕获异常都收敛成 {error,message} 的 500,不漏 Hono 默认裸文本/栈 app.onError((err, c) => { console.error(err); return c.json({ error: 'internal_error', message: '服务内部错误——已记录,稍后重试或联系维护' }, 500); }); app.route('/health', healthRoute()); // 公开:不过 requireAuth // 惰性构造:createKeycloakTokenProvider 只在闭包被调用时读 KEYCLOAK_CLIENT_SECRET, // 建 app 本身不要求环境齐备,否则任何 buildApp() 的测试都会当场炸。 // 两口共用同一个 token provider:换来的是同一个服务账号 token,缓存也只此一份 const serviceToken = createKeycloakTokenProvider(); const access = createHttpTaskAccess(serviceToken); const workflowAccess = createHttpWorkflowAccess(serviceToken); // 顺序要紧:/api/stream 与 /api/upload 必须先于 /api 代理挂载,否则被代理的 catch-all 吞掉 app.route('/api/stream', sseRoutes(hub, access, workflowAccess)); app.route('/api/upload', uploadRoutes()); app.route('/api', proxyRoutes()); return app; }