feat: 生命周期广播事件与链式钩子设施(ADR-006)
- contracts:补齐生命周期广播事件(订阅、下载、整理结束、刮削、字幕、作品关注 / 元数据 / 季集结构、站点可达性) 的类型与 payload schema,LIFECYCLE_EVENTS 清单与 lifecycleKey 幂等键;HOOK_POINTS 链式钩子挂点; FEATURES 功能清单与 SLOT_POSITIONS 前端插槽位置 - outbox 中继支持一条事件路由给多个订阅方(其余订阅方用 <键>#<任务名>);生命周期事件允许多模块订阅, 没有订阅方时也被中继认领并镜像到 SSE - 服务端 HookRegistry:按注册顺序执行、rewrite / intercept 两类、单钩子超时与异常隔离、无注册零开销; ServerModule 增加 manifest 与 registerHooks,bootstrap 装配 ctx.hooks - ADR-006 与 server README「如何写一个功能模块」 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
0f656f0f58
commit
1db689d9a7
16 files changed
+1121
-22
No files matched your search
@@ -1,16 +1,18 @@
|
||||
import { type EventType, parseEventPayload } from "@umt/contracts";
|
||||
import { type EventType, LIFECYCLE_EVENTS, parseEventPayload } from "@umt/contracts";
|
||||
import { type DB, type Kysely, sql } from "@umt/db";
|
||||
import {
|
||||
createDefaultRegistry,
|
||||
type LogSink,
|
||||
OpenWorkflowEngine,
|
||||
type OutboxRelay,
|
||||
type OutboxRoute,
|
||||
type OutboxRouteFn,
|
||||
type TaskEngine,
|
||||
TaskRegistry,
|
||||
} from "@umt/tasks";
|
||||
import type { Logger } from "pino";
|
||||
import type { Config } from "./config.ts";
|
||||
import { HookRegistry } from "./hooks/registry.ts";
|
||||
import type { EventBusPort } from "./http/sse.ts";
|
||||
import { assertCapabilityCount, createCapabilityRegistry } from "./modules/agent/capability-registry.ts";
|
||||
import { createSecretStore, createSettingsReader } from "./modules/auth-settings/provided.ts";
|
||||
@@ -97,24 +99,66 @@ export function buildTaskRegistry(modules: readonly ServerModule[], ctx: ServerC
|
||||
return registry;
|
||||
}
|
||||
|
||||
/** 合并各模块的 outbox 路由;同一事件 type 只能由一个模块认领 */
|
||||
const BROADCAST_TYPES = new Set<string>(LIFECYCLE_EVENTS.map((e) => e.type));
|
||||
|
||||
/**
|
||||
* 合并各模块的 outbox 路由。生命周期广播事件(LIFECYCLE_EVENTS,ADR-006)可以有多个订阅方,按模块顺序合成一个路由:
|
||||
* 各订阅方各自返回任务(或 null),合起来交给中继逐个入队;某个订阅方的路由函数抛错只记日志,不连累别人。
|
||||
* 其它事件(内部流程,如 release.observed → resolveObservation)仍只能由一个模块认领。
|
||||
*/
|
||||
export function collectOutboxRoutes(
|
||||
modules: readonly ServerModule[],
|
||||
ctx: ServerContext,
|
||||
): Record<string, OutboxRouteFn> {
|
||||
const routes: Record<string, OutboxRouteFn> = {};
|
||||
const owner = new Map<string, string>();
|
||||
const subscribers = new Map<string, { module: string; fn: OutboxRouteFn }[]>();
|
||||
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;
|
||||
const prev = subscribers.get(type) ?? [];
|
||||
if (prev.length && !BROADCAST_TYPES.has(type))
|
||||
throw new Error(`outbox 事件 ${type} 被模块 ${prev[0]!.module} 与 ${m.name} 重复认领`);
|
||||
subscribers.set(type, [...prev, { module: m.name, fn }]);
|
||||
}
|
||||
}
|
||||
const routes: Record<string, OutboxRouteFn> = {};
|
||||
for (const [type, subs] of subscribers) {
|
||||
if (subs.length === 1) {
|
||||
routes[type] = subs[0]!.fn;
|
||||
continue;
|
||||
}
|
||||
routes[type] = (ev) => {
|
||||
const out: OutboxRoute[] = [];
|
||||
for (const s of subs) {
|
||||
let r: ReturnType<OutboxRouteFn>;
|
||||
try {
|
||||
r = s.fn(ev);
|
||||
} catch (err) {
|
||||
ctx.logger.warn(
|
||||
{
|
||||
type,
|
||||
module: s.module,
|
||||
eventId: ev.id,
|
||||
err: { message: err instanceof Error ? err.message : String(err) },
|
||||
},
|
||||
"outbox 订阅方路由出错,已跳过",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (r === null) continue;
|
||||
out.push(...((Array.isArray(r) ? r : [r]) as OutboxRoute[]));
|
||||
}
|
||||
return out.length ? out : null;
|
||||
};
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
/** 链式钩子注册表:按模块顺序调 registerHooks,执行顺序即注册顺序(ADR-006) */
|
||||
export function buildHookRegistry(modules: readonly ServerModule[], ctx: ServerContext): HookRegistry {
|
||||
const hooks = new HookRegistry(ctx.logger?.child({ component: "hooks" }));
|
||||
for (const m of modules) m.registerHooks?.(hooks.forModule(m.name), ctx);
|
||||
return hooks;
|
||||
}
|
||||
|
||||
export interface BootstrapOptions {
|
||||
role: ServerRole;
|
||||
config: Config;
|
||||
@@ -158,6 +202,7 @@ export async function bootstrap(opts: BootstrapOptions): Promise<Bootstrapped> {
|
||||
};
|
||||
const tasks = buildTaskRegistry(opts.modules, ctx);
|
||||
for (const m of opts.modules) m.registerCapabilities?.(ctx.capabilities, ctx);
|
||||
ctx.hooks = buildHookRegistry(opts.modules, ctx);
|
||||
// 装配了 agent 模块(即完整模块清单)时,首批 14 个能力必须全部挂上处理器
|
||||
if (opts.modules.some((m) => m.name === "agent")) assertCapabilityCount(ctx.capabilities);
|
||||
engine.attach(
|
||||
|
||||
Reference in new issue
Block a user