refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
+39
-77
@@ -1,95 +1,57 @@
|
||||
# @umt/server
|
||||
|
||||
服务进程(Deno):Hono HTTP API(前缀 `/api/v1`,OpenAPI 见 `/api/openapi.json`、`/api/docs`)、SSE 事件流与 worker 入口。`--role api|worker|worker:agent|all` 决定本进程承担哪部分,默认 `all` 单进程运行(`worker` 只认领 io 组任务,`worker:agent` 只认领 Agent 组;另有内部角色 `--role fsworker`,由 FsProxy 以子进程方式拉起执行单个文件操作,不要手动启动)。业务规则不写在这里,只做编排、鉴权、审计与协议适配。
|
||||
服务进程(Bun 1.4.3):Hono HTTP API(`/api/v1`)、SSE 与 worker 入口。默认 `--role all` 单进程运行;部署按 `api`、`worker`(IO)、`worker:agent` 分进程。OpenAPI 见 `/api/openapi.json`、`/api/docs`。业务编排和写入放在模块 service,确定性规则放在 `@umt/domain`。
|
||||
|
||||
## 模块骨架(W2)
|
||||
## 模块骨架
|
||||
|
||||
```
|
||||
```text
|
||||
src/
|
||||
main.ts 进程入口:按 --role 装配
|
||||
context.ts bootstrap():ServerContext、任务注册表(模块 + 空壳兜底)、outbox 路由合并、连接引擎
|
||||
http/app.ts createApp():系统路由 + 逐个挂 SERVER_MODULES 的 registerRoutes
|
||||
worker/index.ts startWorker():引擎 worker + OutboxRelay + 各模块 start()
|
||||
hooks/ 链式钩子注册表与调用入口(ADR-006)
|
||||
main.ts 按 --role 装配进程
|
||||
context.ts 装配 ServerContext、实际任务注册表与 outbox 路由
|
||||
http/app.ts 系统路由 + 模块 registerRoutes
|
||||
worker/index.ts 任务 worker、OutboxRelay、模块后台循环
|
||||
authorization/grants.ts 共用计划绑定、持续授权范围与预算检查
|
||||
sandbox/ 外部字节解析子进程与 seccomp 策略
|
||||
modules/
|
||||
types.ts ServerModule / ServerContext / OutboxWriter / SecretStore / SettingsReader(共享,不随子任务改)
|
||||
types.ts ServerModule / ServerContext 等装配契约
|
||||
index.ts SERVER_MODULES 清单
|
||||
auth-settings/ W2-A(provided.ts 提供 secrets / settings 实现)
|
||||
sources-identity/ W2-B
|
||||
acquisition-organize/ W2-C
|
||||
file-manager/ W5-A:/files/*(浏览、mkdir/rename、move/copy/organize 两阶段、回收站、identify/probe/scrape、download),
|
||||
见 docs/runbook/files.md
|
||||
agent/ W2-D:capability-registry.ts(authorize / audit / 幂等键 / 操作账本)、会话 API、runAgent、resolveDecision
|
||||
(见 docs/runbook/agent.md;agent 模块可以 import 其它模块的 service 导出,反向不行)
|
||||
decision-hooks.ts 待处理事项的实际业务处理器
|
||||
auth-settings/ 鉴权、密钥与设置
|
||||
sources-identity/ 站点、资源、识别与元数据
|
||||
acquisition-organize/ 订阅、收录、下载、整理与刮削
|
||||
file-manager/ 文件浏览与操作
|
||||
agent/ Agent 会话、交互与能力调用编排
|
||||
… 其余模块见 SERVER_MODULES
|
||||
```
|
||||
|
||||
**分工规则:各 W2 子任务只改自己的 `modules/<area>/` 目录,以及 `packages/contracts`、`packages/db` 中本域的文件**(新迁移按编号顺延,开工前在群里认领编号)。`modules/types.ts`、`modules/index.ts`、`context.ts`、`http/app.ts`、`worker/index.ts`、`main.ts` 是共享装配,确需改动时单独提交并通知其他子任务;`http/auth.ts`、`http/sse.ts` 归 W2-A。跨模块只通过 DB 表 + `outbox_event`(事件类型在 contracts/events.ts)+ contracts 类型耦合,不互相 import 对方模块。
|
||||
跨模块的同步业务操作显式调用 service;可靠的异步后续动作随业务事务写 outbox。路由和能力处理器共用业务入口,service 不依赖 HTTP 路由或 Agent 会话编排。共用语义类型直接复用,有实际取值差异才用 `@umt/mapping`。完整边界、状态归属和抽象约定见 [开发指南](../../docs/runbook/dev.md#工程设计与适度抽象)。
|
||||
|
||||
一个模块长这样(`modules/<area>/index.ts` 导出,目录内自由拆 `routes.ts` / `service.ts` / `handlers/*.ts`):
|
||||
## 模块装配
|
||||
|
||||
```ts
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { capabilitySpec, EventType } from "@umt/contracts";
|
||||
import { defineTask } from "@umt/tasks";
|
||||
import { requireScope } from "../../http/auth.ts";
|
||||
import type { ServerModule } from "../types.ts";
|
||||
`modules/<area>/index.ts` 导出 `ServerModule`,按实际需求实现:
|
||||
|
||||
export const sourcesIdentityModule: ServerModule = {
|
||||
name: "sources-identity",
|
||||
// 路径相对 /api/v1,已过 API Key 鉴权;每个路由自己声明 scope
|
||||
registerRoutes(app, ctx) {
|
||||
app.use("/sites/*", requireScope("read"));
|
||||
app.openapi(createRoute({ method: "get", path: "/sites", /* … */ }), async (c) => c.json(/* … */));
|
||||
},
|
||||
// 同名任务只能由一个模块注册;未注册的 §3.1-8 任务由 @umt/tasks 空壳兜底
|
||||
registerTasks(registry, ctx) {
|
||||
registry.register(defineTask({ name: "resolveObservation", version: 1, /* … */ handler: async (t) => {
|
||||
await ctx.db.transaction().execute(async (trx) => {
|
||||
// …写业务表,同事务发事件(payload 按 contracts 校验)
|
||||
await ctx.outbox.emit(trx, EventType.releaseResolved, payload, { idempotencyKey: `release.resolved:${id}` });
|
||||
});
|
||||
} }));
|
||||
},
|
||||
// 消费别的模块发出的事件:type → 任务;同一 type 只能一个模块认领
|
||||
outboxRoutes: () => ({ "release.resolved": (ev) => ({ task: "evaluateAcquisition", input: { sourceReleaseId: (ev.payload as { sourceReleaseId: string }).sourceReleaseId } }) }),
|
||||
// 能力:规格一律取 contracts,处理器调本模块服务
|
||||
registerCapabilities(registry, ctx) {
|
||||
registry.register(capabilitySpec("release.search"), async (input, c) => ({ items: [], coverage: [] }));
|
||||
},
|
||||
// worker 角色的后台循环(qB 轮询、Feed 调度…);引擎与中继已启动
|
||||
async start(ctx) {},
|
||||
async stop() {},
|
||||
};
|
||||
```
|
||||
| 入口 | 职责 |
|
||||
|---|---|
|
||||
| `registerRoutes(app, ctx)` | 注册相对 `/api/v1` 的路由,声明 scope、校验输入、调用 service |
|
||||
| `registerTasks(registry, ctx)` | 用 `defineTask` 注册真实 handler;任务名和版本不能重复,没有空壳兜底 |
|
||||
| `registerCapabilities(registry, ctx)` | 规格取自 contracts,处理器调用 service |
|
||||
| `outboxRoutes(ctx)` | 事件 → 任务、任务数组或 null;生命周期事件允许多个订阅方,内部流程事件只有一个认领方 |
|
||||
| `start(ctx)` / `stop()` | 后台循环与回收;需要恢复的长任务进入持久队列 |
|
||||
|
||||
`ServerContext` 字段:`db`(Kysely<DB>)、`config`、`logger`(pino,自动脱敏)、`events`(SSE 总线 `EventBusPort`)、`engine`(TaskEngine:enqueue/signal/cancel/get/list;register* 阶段不要调用)、`outbox`(`emit(trx, type, payload, {idempotencyKey})` + worker 的 `relay`)、`secrets`(SecretStore,W2-A 实现前调用会抛错)、`settings`(读 settings 表并合并缺省)、`capabilities`(CapabilityRegistry)。domain ↔ contracts 的取值转换一律用 `@umt/mapping`。
|
||||
`ServerContext` 提供 `db`、`config`、`logger`、`events`、`engine`、`outbox`、`secrets`、`settings`、`capabilities`。注册阶段只做装配,不要使用尚未连接的引擎。`all` 连接时顺带迁移引擎;分进程部署先执行 `--role migrate`,再以 `runMigrations:false` 连接。
|
||||
|
||||
角色:`all` 单进程时连接引擎会顺带跑引擎迁移;`api` / `worker` 分进程部署时由 `--role migrate` 预先迁移(db + openworkflow),两个进程都以 `runMigrations:false` 连接。
|
||||
业务写入与 `ctx.outbox.emit(trx, type, payload, { idempotencyKey })` 同事务提交。payload 在 contracts 定义并校验,订阅任务按 ID 回查业务记录、幂等执行。引擎步骤与租约不替代业务表的状态。
|
||||
|
||||
## 如何写一个功能模块(ADR-006)
|
||||
## 新增功能模块
|
||||
|
||||
外围功能(补字幕这类)按下面的约定接进来,不做插件系统、不做开关。样板是 `modules/subtitles/`。
|
||||
以 `modules/subtitles/` 为样板,只接入实际需要的入口:
|
||||
|
||||
1. **登记 manifest**:在 `packages/contracts/src/features.ts` 的 `FEATURES` 里加一份(名称、说明、怎么用、订阅的事件、能力、页面入口、插槽、钩子)。它会进智能体系统提示「能力地图」末尾的功能清单;`test/modules.test.ts` 核对模块与清单一一对应。
|
||||
2. **建模块**:`modules/<id>/index.ts` 导出 `ServerModule`,`name` 与 manifest id 相同,写 `manifest: featureManifest("<id>")`,加进 `modules/index.ts`。
|
||||
3. **订阅广播事件**(事后通知,异步、至少一次):`outboxRoutes` 里写 `"<事件>": (ev) => 任务 | 任务数组 | null`。生命周期事件(`LIFECYCLE_EVENTS`)可以多个模块同时订阅;payload 用 `parseEventPayload` 取,按 ID 回查业务表;订阅任务要幂等(同一事件可能投递多次)。
|
||||
```ts
|
||||
outboxRoutes: (ctx) => ({
|
||||
"organization.finished": (ev) => {
|
||||
const p = parseEventPayload("organization.finished", ev.payload);
|
||||
return p?.outcome === "succeeded" ? { task: "myFollowUp", input: { recordId: p.recordId } } : null;
|
||||
},
|
||||
}),
|
||||
```
|
||||
4. **注册链式钩子**(同步改写 / 拦截,挂点见 `packages/contracts/src/hooks.ts`):
|
||||
```ts
|
||||
registerHooks(hooks) {
|
||||
hooks.rewrite("organize.rename", (name, c) => (c.mediaType === "movie" ? name.replace(/ - 1080p/, "") : undefined));
|
||||
hooks.intercept("organize.intercept", (c) => (c.files.some((f) => f.dst.includes("/Sample/")) ? "目标在 Sample 目录" : null));
|
||||
},
|
||||
```
|
||||
钩子可能在业务事务里执行:只做内存里的判断,不发外部请求、不写库;缺省 2 秒超时,抛错 / 超时只跳过自己。
|
||||
5. **自己发事件**:业务节点在事务里 `ctx.outbox.emit(trx, "<事件>", payload, { idempotencyKey: lifecycleKey("<事件>", id) })`;新事件先在 `contracts/events.ts` 加类型、payload schema,并登记进 `LIFECYCLE_EVENTS`。
|
||||
6. **前端入口**:在 `apps/web/app/features/<id>.tsx` 用插槽注册(作品页操作区 / 面板区、整理记录行操作、侧栏、设置分区),在 `features/index.ts` 调一次;默认直接显示。
|
||||
7. **智能体能力**(可选):照常在 contracts 定义能力、在 `registerCapabilities` 挂处理器,并登记 Skill 与能力地图(见仓库根 CLAUDE.md)。
|
||||
1. 在 `packages/contracts/src/features.ts` 登记 manifest(名称、用法、事件、能力、页面与插槽)。它进入 Agent 功能清单,由 `test/modules.test.ts` 核对。
|
||||
2. 导出 `ServerModule`,设置 `manifest: featureManifest("<id>")`,加入 `modules/index.ts`。
|
||||
3. 需要事后动作时订阅生命周期事件,用 `parseEventPayload` 读取;自己发新事件时在 contracts 登记类型、schema 与 `LIFECYCLE_EVENTS`。
|
||||
4. 需要前端入口时在 `apps/web/app/features/<id>.tsx` 注册插槽并加入 `features/index.ts`,默认直接显示。
|
||||
5. 新增 Agent 能力时同时登记 Skill 与能力地图,见根目录 `CLAUDE.md`。
|
||||
|
||||
同步规则直接在所属 service 实现。目前没有通用链式钩子;`decision-hooks.ts` 只处理已有待处理事项的业务动作。功能默认启用,无需另建插件系统或开关。
|
||||
|
||||
开发、测试和容器说明见 [开发手册](../../docs/runbook/dev.md) 与 [部署手册](../../docs/runbook/deploy.md)。
|
||||
Reference in new issue
Block a user