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:
rootandClaude Opus 5.5 committed 2026-09-27 19:30:55 +08:00
1 parent ef3998767c
commit 9858b8494d
23 files changed
+829 -22

No files matched your search

+26 -1
View File
@@ -1,10 +1,13 @@
import { parseArgs } from "node:util";
import { createDb, migrateToLatest } from "@umt/db";
import { migrateEngineSchema } from "@umt/tasks";
import denoJson from "../deno.json" with { type: "json" };
import { loadConfig } from "./config.ts";
import { bootstrap } from "./context.ts";
import { createApp } from "./http/app.ts";
import { EventBus } from "./http/sse.ts";
import { createLogger } from "./logger.ts";
import { SERVER_MODULES } from "./modules/index.ts";
import { runPreflight } from "./preflight.ts";
import { startWorker } from "./worker/index.ts";
@@ -52,6 +55,9 @@ if (role === "migrate" || role === "preflight") {
const { results } = await migrateToLatest(db);
for (const r of results ?? []) logger.info({ migration: r.migrationName, status: r.status }, "migration");
logger.info({ applied: results?.length ?? 0 }, "迁移完成");
// 任务引擎自己的表(openworkflow schema);api/worker 进程启动时不再迁移
await migrateEngineSchema(config.DATABASE_URL);
logger.info("任务引擎迁移完成");
} else {
const problems = await runPreflight({ config, db });
for (const p of problems) logger.error({ check: p.check }, p.message);
@@ -70,6 +76,17 @@ if (role === "migrate" || role === "preflight") {
const bus = new EventBus();
const stops: Array<() => Promise<void>> = [];
// 装配 ServerContext:模块注册任务/能力,连接任务引擎(api 角色也要:入队、信号、取消)
let booted: Awaited<ReturnType<typeof bootstrap>>;
try {
booted = await bootstrap({ role, config, logger, db, events: bus, modules: SERVER_MODULES });
} catch (err) {
logger.error({ err }, "启动装配失败");
await db.destroy();
Deno.exit(1);
}
const { ctx, outbox, engine } = booted;
if (role === "api" || role === "all") {
const app = createApp({
config,
@@ -79,6 +96,8 @@ if (role === "api" || role === "all") {
role,
version: denoJson.version,
...(config.WEB_ROOT ? { webRoot: config.WEB_ROOT } : {}),
modules: SERVER_MODULES,
ctx,
});
const ac = new AbortController();
const server = Deno.serve(
@@ -101,7 +120,12 @@ if (role === "api" || role === "all") {
});
}
if (role === "worker" || role === "all") {
const worker = startWorker({ logger, heartbeatMs: config.WORKER_HEARTBEAT_MS, bus });
const worker = await startWorker({
logger,
heartbeatMs: config.WORKER_HEARTBEAT_MS,
bus,
runtime: { ctx, modules: SERVER_MODULES, setRelay: outbox.setRelay },
});
stops.push(worker.stop);
}
@@ -113,6 +137,7 @@ async function shutdown(signal: Deno.Signal) {
// 兜底:10 秒内没退干净就强退,避免容器 stop 超时被 SIGKILL
const force = setTimeout(() => Deno.exit(1), 10_000);
await Promise.allSettled(stops.map((s) => s()));
await engine.stop().catch((err) => logger.error({ err }, "任务引擎关闭失败"));
await db.destroy();
clearTimeout(force);
Deno.exit(0);