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:
rootandClaude Opus 5.5 committed 2026-10-06 19:27:02 +08:00
1 parent 0f656f0f58
commit 1db689d9a7
16 files changed
+1121 -22

No files matched your search

+11 -6
View File
@@ -21,8 +21,11 @@ export interface OutboxRoute {
priority?: number;
}
/** 返回 null 表示该事件无需建任务,直接标记已投递 */
export type OutboxRouteFn = (event: OutboxEvent) => OutboxRoute | null;
/**
* 返回 null 表示该事件无需建任务,直接标记已投递。
* 返回数组 = 广播给多个订阅方(ADR-006):第一个任务用事件的幂等键,其余用 `<键>#<任务名>`,重投时由引擎幂等去重。
*/
export type OutboxRouteFn = (event: OutboxEvent) => OutboxRoute | readonly OutboxRoute[] | null;
export interface OutboxRelayOptions {
db: AnyDb;
@@ -82,11 +85,13 @@ export class OutboxRelay {
for (const ev of rows.rows) {
try {
const routeFn = this.opts.routes[ev.type];
const route = routeFn ? routeFn(ev) : null;
if (route) {
const payloadTrace = (ev.payload as { traceId?: unknown } | null)?.traceId;
const routed = routeFn ? routeFn(ev) : null;
const routes = (routed === null ? [] : Array.isArray(routed) ? routed : [routed]) as readonly OutboxRoute[];
const baseKey = ev.idempotency_key ?? OutboxRelay.idempotencyKey(ev.id);
const payloadTrace = (ev.payload as { traceId?: unknown } | null)?.traceId;
for (const [i, route] of routes.entries()) {
await this.opts.engine.enqueue(route.task, route.input, {
idempotencyKey: ev.idempotency_key ?? OutboxRelay.idempotencyKey(ev.id),
idempotencyKey: i === 0 ? baseKey : `${baseKey}#${route.task}`,
traceId: route.traceId ?? (typeof payloadTrace === "string" ? payloadTrace : randomUUID()),
...(route.priority === undefined ? {} : { priority: route.priority }),
});