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,13 @@
|
||||
import type { ServerModule } from "../types.ts";
|
||||
|
||||
/**
|
||||
* W2-C 规则/下载/入库/整理记录:subscription、acquisition_slot、policy_revision、preference_tag;
|
||||
* evaluateAcquisition / submitDownload / verifyAndImport / cleanupDownloadCache / reconcileLibrary;
|
||||
* qB 同步与缓存阈值、Copy/Move、path_binding、organization_record 与公开增量 API、媒体库与首页聚合。
|
||||
* 消费 release.resolved(outboxRoutes);删种带数据一律经 adapters 的 assertSafeToDeleteData。
|
||||
*/
|
||||
export const acquisitionOrganizeModule: ServerModule = {
|
||||
name: "acquisition-organize",
|
||||
registerRoutes() {},
|
||||
registerTasks() {},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CapabilityRegistry } from "@umt/agent";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
/**
|
||||
* 能力注册中心工厂(W2-D 负责接 authorize:automation_grant / approval + 自动化总开关;audit:写 audit_event)。
|
||||
* 当前未注入 authorize:read/preview 可调用,propose/approve/execute 一律拒绝(安全默认)。
|
||||
*/
|
||||
export function createCapabilityRegistry(deps: { logger: Logger }): CapabilityRegistry {
|
||||
const log = deps.logger.child({ component: "capabilities" });
|
||||
return new CapabilityRegistry({
|
||||
audit: (r) => log.info({ capability: r.capabilityId, outcome: r.outcome, actor: r.actorId }, "capability.invoke"),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ServerModule } from "../types.ts";
|
||||
|
||||
/**
|
||||
* W2-D Agent 装配:14 个能力的 authorize/audit 注入(./capability-registry.ts)、Agent 会话 API + SSE、
|
||||
* agent_run_event 落库、resolveDecision 处理器、后台触发的有界 agent 任务。
|
||||
*/
|
||||
export const agentModule: ServerModule = {
|
||||
name: "agent",
|
||||
registerRoutes() {},
|
||||
registerTasks() {},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ServerModule } from "../types.ts";
|
||||
|
||||
/**
|
||||
* W2-A 认证/设置/实例/引擎面板:管理员登录门禁、settings、api_key、secret 加密服务、
|
||||
* downloader/media_server 实例与 library_root/path_mapping、审计查询、SSE 总线改 PG 通知、引擎面板只读 API、自动化总开关。
|
||||
* secrets / settings 的实现放在 ./provided.ts(context 从这里取),SSE 总线在 http/sse.ts,鉴权在 http/auth.ts。
|
||||
*/
|
||||
export const authSettingsModule: ServerModule = {
|
||||
name: "auth-settings",
|
||||
registerRoutes() {},
|
||||
registerTasks() {},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DEFAULT_SETTINGS, GlobalSettings, type SettingsKey } from "@umt/contracts";
|
||||
import type { DB, Kysely } from "@umt/db";
|
||||
import type { Logger } from "pino";
|
||||
import type { SecretStore, SettingsReader } from "../types.ts";
|
||||
|
||||
// auth-settings 模块向 ServerContext 提供的基础服务。W2-A 在这里替换实现,其它模块只用 ctx 上的接口。
|
||||
|
||||
/** 占位:W2-A 实现 AES-GCM 加密存取(secret 表,SECRET_MASTER_KEY) */
|
||||
export function createSecretStore(_deps: { db: Kysely<DB>; masterKey: string }): SecretStore {
|
||||
const notYet = async (): Promise<never> => {
|
||||
throw new Error("SecretStore 尚未实现(W2-A)");
|
||||
};
|
||||
return { resolve: notYet, create: notYet, replace: notYet, remove: notYet };
|
||||
}
|
||||
|
||||
/** settings 表(key → jsonb)按分组合并缺省值;每次读库,不缓存(设置变更立即生效) */
|
||||
export function createSettingsReader(deps: { db: Kysely<DB>; logger: Logger }): SettingsReader {
|
||||
const all = async (): Promise<GlobalSettings> => {
|
||||
const rows = await deps.db.selectFrom("settings").select(["key", "value"]).execute();
|
||||
const stored = new Map(rows.map((r) => [r.key, r.value]));
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(DEFAULT_SETTINGS) as SettingsKey[]) {
|
||||
const def = DEFAULT_SETTINGS[key];
|
||||
const v = stored.get(key);
|
||||
const merged = v && typeof v === "object" && !Array.isArray(v) ? { ...def, ...(v as object) } : def;
|
||||
const parsed = GlobalSettings.shape[key].safeParse(merged);
|
||||
if (!parsed.success) deps.logger.warn({ key }, "settings 值不合法,回退缺省");
|
||||
out[key] = parsed.success ? parsed.data : def;
|
||||
}
|
||||
return out as GlobalSettings;
|
||||
};
|
||||
return { all, get: async (key) => (await all())[key] };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { acquisitionOrganizeModule } from "./acquisition-organize/index.ts";
|
||||
import { agentModule } from "./agent/index.ts";
|
||||
import { authSettingsModule } from "./auth-settings/index.ts";
|
||||
import { sourcesIdentityModule } from "./sources-identity/index.ts";
|
||||
import type { ServerModule } from "./types.ts";
|
||||
|
||||
export type * from "./types.ts";
|
||||
|
||||
/**
|
||||
* 模块清单:http/app.ts(路由)与 worker(任务/中继/后台循环)都从这里取。
|
||||
* 顺序即注册顺序;auth-settings 在前(其它模块的路由依赖它的登录门禁中间件)。
|
||||
*/
|
||||
export const SERVER_MODULES: readonly ServerModule[] = [
|
||||
authSettingsModule,
|
||||
sourcesIdentityModule,
|
||||
acquisitionOrganizeModule,
|
||||
agentModule,
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ServerModule } from "../types.ts";
|
||||
|
||||
/**
|
||||
* W2-B 来源/身份/元数据:site、site_account、feed_source;ingestFeed / resolveObservation / refreshMetadata;
|
||||
* observation/source_release 写入与去重;身份解析、待识别池与 decision;站点工作台 API。
|
||||
* 发出 release.resolved、decision.created(经 ctx.outbox.emit)。
|
||||
*/
|
||||
export const sourcesIdentityModule: ServerModule = {
|
||||
name: "sources-identity",
|
||||
registerRoutes() {},
|
||||
registerTasks() {},
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import type { CapabilityRegistry } from "@umt/agent";
|
||||
import type { EventPayload, EventType, GlobalSettings, SecretPurpose, SettingsKey } from "@umt/contracts";
|
||||
import type { DB, Kysely, Transaction } from "@umt/db";
|
||||
import type { OutboxRelay, OutboxRouteFn, TaskEngine, TaskRegistry } from "@umt/tasks";
|
||||
import type { Logger } from "pino";
|
||||
import type { Config } from "../config.ts";
|
||||
import type { AuthEnv } from "../http/auth.ts";
|
||||
import type { EventBusPort } from "../http/sse.ts";
|
||||
|
||||
// 服务端模块骨架(W1 审查 B6;任务书 §5)。
|
||||
// 每个 W2 子任务是一个 ServerModule,只改 modules/<自己的目录>/ 与 contracts/db 中本域的文件。
|
||||
// 跨模块只通过 DB 表 + outbox_event(事件类型在 contracts/events.ts)+ contracts 类型耦合,不互相 import。
|
||||
|
||||
export type ServerRole = "api" | "worker" | "all";
|
||||
|
||||
/**
|
||||
* 模块路由挂载点:已挂在 /api/v1 下、已经过 API Key 鉴权(c.var.principal 可用)。
|
||||
* 路径写相对 /api/v1 的部分(如 "/sites"),每个路由自己加 requireScope(...);
|
||||
* 用 app.openapi(createRoute(...)) 注册的路由会进 /api/openapi.json。
|
||||
*/
|
||||
export type ModuleApp = OpenAPIHono<AuthEnv>;
|
||||
|
||||
/** 业务事务内写 outbox_event;中继(worker)再按 type 路由成任务 */
|
||||
export interface OutboxWriter {
|
||||
/**
|
||||
* payload 按 contracts EVENT_PAYLOADS 校验,不合法直接抛错。
|
||||
* 传 idempotencyKey 时同键重复写入会被忽略(outbox_event.idempotency_key 唯一),也作为引擎入队幂等键。
|
||||
*/
|
||||
emit<T extends EventType>(
|
||||
executor: Kysely<DB> | Transaction<DB>,
|
||||
type: T,
|
||||
payload: EventPayload<T>,
|
||||
opts?: { idempotencyKey?: string },
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface OutboxPort extends OutboxWriter {
|
||||
/** 仅 worker 角色有中继实例;api 角色为 null */
|
||||
readonly relay: OutboxRelay | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 凭证存取(secret 表 + SECRET_MASTER_KEY 加密)。W2-A 在 modules/auth-settings/provided.ts 实现,
|
||||
* 当前占位实现一律抛"未实现"。其它模块只存/传 secret_ref,需要明文时现场 resolve,不缓存、不打日志。
|
||||
*/
|
||||
export interface SecretStore {
|
||||
resolve(secretRef: string): Promise<string>;
|
||||
create(input: { purpose: SecretPurpose; plaintext: string; label?: string }): Promise<string>;
|
||||
replace(secretRef: string, plaintext: string): Promise<void>;
|
||||
remove(secretRef: string): Promise<void>;
|
||||
}
|
||||
|
||||
/** 全局设置读取:settings 表按分组合并 DEFAULT_SETTINGS 后校验;表里值非法时回退缺省并记警告 */
|
||||
export interface SettingsReader {
|
||||
get<K extends SettingsKey>(key: K): Promise<GlobalSettings[K]>;
|
||||
all(): Promise<GlobalSettings>;
|
||||
}
|
||||
|
||||
export interface ServerContext {
|
||||
role: ServerRole;
|
||||
db: Kysely<DB>;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
/** SSE 事件总线 */
|
||||
events: EventBusPort;
|
||||
/** 任务引擎:api 角色也可 enqueue/signal/cancel;只有 worker 角色执行任务 */
|
||||
engine: TaskEngine;
|
||||
outbox: OutboxPort;
|
||||
secrets: SecretStore;
|
||||
settings: SettingsReader;
|
||||
/** 能力注册中心(Web/Pi/CLI/MCP 共用一条 invoke 路径) */
|
||||
capabilities: CapabilityRegistry;
|
||||
}
|
||||
|
||||
export interface ServerModule {
|
||||
/** 与目录名一致,用于日志 */
|
||||
name: string;
|
||||
/** api / all 角色:挂 HTTP 路由 */
|
||||
registerRoutes(app: ModuleApp, ctx: ServerContext): void;
|
||||
/**
|
||||
* 所有角色:注册持久任务处理器(api 角色也要注册,enqueue 时按定义校验输入)。
|
||||
* 同名任务只能由一个模块注册;没人注册的 §3.1-8 任务由 @umt/tasks 的空壳兜底。
|
||||
*/
|
||||
registerTasks(registry: TaskRegistry, ctx: ServerContext): void;
|
||||
/** 所有角色:outbox 事件 type → 任务路由;同一 type 只能由一个模块认领 */
|
||||
outboxRoutes?(ctx: ServerContext): Record<string, OutboxRouteFn>;
|
||||
/** 所有角色:用 contracts 的 capabilitySpec(id) 注册能力处理器 */
|
||||
registerCapabilities?(registry: CapabilityRegistry, ctx: ServerContext): void;
|
||||
/** worker / all 角色:启动轮询器、定时器等后台循环(引擎与中继已启动) */
|
||||
start?(ctx: ServerContext): Promise<void> | void;
|
||||
/** 退出时按注册逆序调用 */
|
||||
stop?(): Promise<void> | void;
|
||||
}
|
||||
Reference in new issue
Block a user