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
@@ -0,0 +1,167 @@
|
||||
import { type EventType, parseEventPayload } from "@umt/contracts";
|
||||
import { type DB, type Kysely, sql } from "@umt/db";
|
||||
import {
|
||||
createDefaultRegistry,
|
||||
type LogSink,
|
||||
OpenWorkflowEngine,
|
||||
type OutboxRelay,
|
||||
type OutboxRouteFn,
|
||||
type TaskEngine,
|
||||
TaskRegistry,
|
||||
} from "@umt/tasks";
|
||||
import type { Logger } from "pino";
|
||||
import type { Config } from "./config.ts";
|
||||
import type { EventBusPort } from "./http/sse.ts";
|
||||
import { createCapabilityRegistry } from "./modules/agent/capability-registry.ts";
|
||||
import { createSecretStore, createSettingsReader } from "./modules/auth-settings/provided.ts";
|
||||
import type { OutboxPort, ServerContext, ServerModule, ServerRole } from "./modules/types.ts";
|
||||
|
||||
// 进程装配:ServerContext、任务注册表、outbox 路由。api / worker / all 三种角色共用。
|
||||
|
||||
/** 引擎在任务注册表建好后才能连接;先给模块一个会转发的句柄 */
|
||||
export class DeferredTaskEngine implements TaskEngine {
|
||||
private inner: TaskEngine | null = null;
|
||||
|
||||
attach(engine: TaskEngine) {
|
||||
this.inner = engine;
|
||||
}
|
||||
|
||||
get ready(): boolean {
|
||||
return this.inner !== null;
|
||||
}
|
||||
|
||||
private get e(): TaskEngine {
|
||||
if (!this.inner) throw new Error("任务引擎尚未就绪(模块不要在 register* 阶段调用引擎)");
|
||||
return this.inner;
|
||||
}
|
||||
|
||||
enqueue: TaskEngine["enqueue"] = (...a) => this.e.enqueue(...a);
|
||||
signal: TaskEngine["signal"] = (...a) => this.e.signal(...a);
|
||||
cancel: TaskEngine["cancel"] = (...a) => this.e.cancel(...a);
|
||||
get: TaskEngine["get"] = (...a) => this.e.get(...a);
|
||||
list: TaskEngine["list"] = (...a) => this.e.list(...a);
|
||||
startWorker: TaskEngine["startWorker"] = (...a) => this.e.startWorker(...a);
|
||||
stop: TaskEngine["stop"] = async () => {
|
||||
await this.inner?.stop();
|
||||
};
|
||||
}
|
||||
|
||||
export function createOutboxPort(): OutboxPort & { setRelay(r: OutboxRelay): void } {
|
||||
let relay: OutboxRelay | null = null;
|
||||
return {
|
||||
get relay() {
|
||||
return relay;
|
||||
},
|
||||
setRelay(r) {
|
||||
relay = r;
|
||||
},
|
||||
async emit(executor, type, payload, opts = {}) {
|
||||
if (parseEventPayload(type as EventType, payload) === null) {
|
||||
throw new Error(`outbox 事件 ${type} 的 payload 不符合 contracts 定义`);
|
||||
}
|
||||
await sql`insert into outbox_event (type, payload, idempotency_key)
|
||||
values (${type}, ${JSON.stringify(payload)}::jsonb, ${opts.idempotencyKey ?? null})
|
||||
on conflict (idempotency_key) where idempotency_key is not null do nothing`.execute(executor);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 把 pino logger 接到 @umt/tasks 的 LogSink(tasks 自己已脱敏,这里再经 pino 的脱敏) */
|
||||
export function tasksLogSink(logger: Logger): LogSink {
|
||||
// r.fields 里已带 component(tasks.engine / tasks.outbox)
|
||||
return (r) => logger[r.level](r.fields, r.msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务注册表:各模块注册自己的任务;没人注册的 §3.1-8 任务用 @umt/tasks 的空壳兜底
|
||||
* (outbox 路由到它们时不至于报"未知任务")。同名任务被两个模块注册会在这里报错。
|
||||
*/
|
||||
export function buildTaskRegistry(modules: readonly ServerModule[], ctx: ServerContext): TaskRegistry {
|
||||
const registry = new TaskRegistry();
|
||||
const owner = new Map<string, string>();
|
||||
for (const m of modules) {
|
||||
const own = new TaskRegistry();
|
||||
m.registerTasks(own, ctx);
|
||||
for (const name of own.names()) {
|
||||
const prev = owner.get(name);
|
||||
if (prev) throw new Error(`任务 ${name} 被模块 ${prev} 与 ${m.name} 重复注册`);
|
||||
owner.set(name, m.name);
|
||||
}
|
||||
registry.register(...own.all());
|
||||
}
|
||||
for (const stub of createDefaultRegistry().all()) if (!registry.has(stub.name)) registry.register(stub);
|
||||
return registry;
|
||||
}
|
||||
|
||||
/** 合并各模块的 outbox 路由;同一事件 type 只能由一个模块认领 */
|
||||
export function collectOutboxRoutes(
|
||||
modules: readonly ServerModule[],
|
||||
ctx: ServerContext,
|
||||
): Record<string, OutboxRouteFn> {
|
||||
const routes: Record<string, OutboxRouteFn> = {};
|
||||
const owner = new Map<string, string>();
|
||||
for (const m of modules) {
|
||||
for (const [type, fn] of Object.entries(m.outboxRoutes?.(ctx) ?? {})) {
|
||||
const prev = owner.get(type);
|
||||
if (prev) throw new Error(`outbox 事件 ${type} 被模块 ${prev} 与 ${m.name} 重复认领`);
|
||||
owner.set(type, m.name);
|
||||
routes[type] = fn;
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
export interface BootstrapOptions {
|
||||
role: ServerRole;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
db: Kysely<DB>;
|
||||
events: EventBusPort;
|
||||
modules: readonly ServerModule[];
|
||||
/** 连接引擎时是否跑引擎迁移;默认只有 all 角色跑(api/worker 分进程时由 --role migrate 预先迁移) */
|
||||
engineMigrations?: boolean;
|
||||
/** 仅测试覆盖:引擎表 schema(默认 openworkflow) */
|
||||
engineSchema?: string;
|
||||
/** 仅测试覆盖:不连接真实引擎 */
|
||||
engine?: TaskEngine;
|
||||
}
|
||||
|
||||
export interface Bootstrapped {
|
||||
ctx: ServerContext;
|
||||
tasks: TaskRegistry;
|
||||
outbox: ReturnType<typeof createOutboxPort>;
|
||||
engine: DeferredTaskEngine;
|
||||
}
|
||||
|
||||
/** 建 ServerContext → 模块注册任务与能力 → 连接引擎。路由与 worker 由调用方按角色启动 */
|
||||
export async function bootstrap(opts: BootstrapOptions): Promise<Bootstrapped> {
|
||||
const { config, logger, db } = opts;
|
||||
const engine = new DeferredTaskEngine();
|
||||
const outbox = createOutboxPort();
|
||||
const ctx: ServerContext = {
|
||||
role: opts.role,
|
||||
db,
|
||||
config,
|
||||
logger,
|
||||
events: opts.events,
|
||||
engine,
|
||||
outbox,
|
||||
secrets: createSecretStore({ db, masterKey: config.SECRET_MASTER_KEY }),
|
||||
settings: createSettingsReader({ db, logger }),
|
||||
capabilities: createCapabilityRegistry({ logger }),
|
||||
};
|
||||
const tasks = buildTaskRegistry(opts.modules, ctx);
|
||||
for (const m of opts.modules) m.registerCapabilities?.(ctx.capabilities, ctx);
|
||||
engine.attach(
|
||||
opts.engine ??
|
||||
(await OpenWorkflowEngine.connect({
|
||||
databaseUrl: config.DATABASE_URL,
|
||||
db,
|
||||
registry: tasks,
|
||||
logSink: tasksLogSink(logger),
|
||||
runMigrations: opts.engineMigrations ?? opts.role === "all",
|
||||
...(opts.engineSchema ? { schema: opts.engineSchema } : {}),
|
||||
})),
|
||||
);
|
||||
return { ctx, tasks, outbox, engine };
|
||||
}
|
||||
Reference in new issue
Block a user