feat(server): ServerModule skeleton with shared ServerContext, worker runs engine + outbox relay (B6)
modules/{auth-settings,sources-identity,acquisition-organize,agent} placeholders listed in
SERVER_MODULES; ServerModule registers routes (under /api/v1, authenticated), tasks,
outbox routes, capabilities and background loops. bootstrap() builds ServerContext (db,
config, logger, events, engine, outbox writer, secrets placeholder, settings reader,
capability registry), fills unregistered tasks with stubs and rejects duplicate task or
outbox ownership. worker starts the OpenWorkflow worker, OutboxRelay and module start().
--role migrate also migrates the engine schema; api/worker connect without migrating.
tasks: TaskRegistry.has, migrateEngineSchema.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ef3998767c
commit
9858b8494d
23 files changed
+829
-22
No files matched your search
+26
-11
@@ -8,17 +8,18 @@ import type { Logger } from "pino";
|
||||
import type { Config } from "../config.ts";
|
||||
import { isDev } from "../config.ts";
|
||||
import { redactString } from "../logger.ts";
|
||||
import type { ServerContext, ServerModule } from "../modules/types.ts";
|
||||
import { type ApiKeyStore, type AuthEnv, apiKeyAuth, kyselyApiKeyStore, requireScope } from "./auth.ts";
|
||||
import { ApiException, errorBody } from "./errors.ts";
|
||||
import { healthRoutes } from "./routes/health.ts";
|
||||
import { mountOpenApi } from "./routes/openapi.ts";
|
||||
import { type EventBus, sseRoutes } from "./sse.ts";
|
||||
import { type EventBusPort, sseRoutes } from "./sse.ts";
|
||||
import { mountWebRoot } from "./static.ts";
|
||||
|
||||
export interface AppDeps {
|
||||
config: Pick<Config, "NODE_ENV" | "DEV_ADMIN_KEY">;
|
||||
logger: Logger;
|
||||
bus: EventBus;
|
||||
bus: EventBusPort;
|
||||
version: string;
|
||||
role: "api" | "worker" | "all";
|
||||
startedAt?: number;
|
||||
@@ -27,6 +28,9 @@ export interface AppDeps {
|
||||
apiKeyStore?: ApiKeyStore;
|
||||
/** 单容器模式:托管 web 静态产物的目录 */
|
||||
webRoot?: string;
|
||||
/** 业务模块(modules/index.ts 的 SERVER_MODULES);给了 ctx 才挂载 */
|
||||
modules?: readonly ServerModule[];
|
||||
ctx?: ServerContext;
|
||||
}
|
||||
|
||||
export const API_PREFIX = "/api/v1";
|
||||
@@ -35,15 +39,7 @@ export function createApp(deps: AppDeps) {
|
||||
const { config, logger, bus, db } = deps;
|
||||
const dev = isDev(config);
|
||||
|
||||
const app = new OpenAPIHono<AuthEnv>({
|
||||
// zod 校验失败统一成 validation_failed 信封
|
||||
defaultHook: (result, c) => {
|
||||
if (!result.success) {
|
||||
const msg = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
||||
return c.json(errorBody("validation_failed", msg), 422);
|
||||
}
|
||||
},
|
||||
});
|
||||
const app = newApiApp();
|
||||
|
||||
app.use("*", requestId());
|
||||
// 访问日志:路径中的 passkey/token 参数脱敏
|
||||
@@ -112,8 +108,27 @@ export function createApp(deps: AppDeps) {
|
||||
return c.json({ ok: true, data: { id: p.id, name: p.name, kind: p.kind, scopes: p.scopes } });
|
||||
});
|
||||
|
||||
// ── 业务模块:各自挂在 /api/v1 下(已鉴权),按清单顺序注册 ──
|
||||
if (deps.ctx && deps.modules?.length) {
|
||||
const api = newApiApp();
|
||||
for (const m of deps.modules) m.registerRoutes(api, deps.ctx);
|
||||
app.route(API_PREFIX, api);
|
||||
}
|
||||
|
||||
if (deps.webRoot) mountWebRoot(app, deps.webRoot);
|
||||
|
||||
return app;
|
||||
}
|
||||
export type App = ReturnType<typeof createApp>;
|
||||
|
||||
/** 统一的 OpenAPIHono 实例:zod 校验失败统一成 validation_failed 信封 */
|
||||
function newApiApp() {
|
||||
return new OpenAPIHono<AuthEnv>({
|
||||
defaultHook: (result, c) => {
|
||||
if (!result.success) {
|
||||
const msg = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
||||
return c.json(errorBody("validation_failed", msg), 422);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5,11 +5,22 @@ import { ApiException } from "./errors.ts";
|
||||
|
||||
type Listener = (e: EventEnvelope) => void;
|
||||
|
||||
/**
|
||||
* SSE 总线端口。模块与 SSE 路由只依赖这个接口;W2-A 把实现换成 PG NOTIFY + 持久化 seq 时接口不变。
|
||||
*/
|
||||
export interface EventBusPort {
|
||||
readonly latestSeq: number;
|
||||
publish<P>(type: string, payload: P): EventEnvelope<P>;
|
||||
/** 取 seq > after 的事件;gap=true 表示起点已不可补读,客户端需重新拉快照 */
|
||||
since(after: number): { events: EventEnvelope[]; gap: boolean };
|
||||
subscribe(l: Listener): () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进程内事件总线:序号自增,保留最近 capacity 条供断线补读。
|
||||
* 多进程部署时需换成 PG LISTEN/NOTIFY 或 outbox 投影(W2)。
|
||||
*/
|
||||
export class EventBus {
|
||||
export class EventBus implements EventBusPort {
|
||||
private seq = 0;
|
||||
private readonly buffer: EventEnvelope[] = [];
|
||||
private readonly listeners = new Set<Listener>();
|
||||
@@ -42,7 +53,7 @@ export class EventBus {
|
||||
}
|
||||
|
||||
export interface SseOptions {
|
||||
bus: EventBus;
|
||||
bus: EventBusPort;
|
||||
keepaliveMs?: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user