refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

+80 -25
View File
@@ -2,19 +2,19 @@
## 前置
- Deno 2.9.7(锁定;`curl -fsSL https://deno.land/install.sh | sh -s v2.9.7`)、Docker。不需要 Node.js / pnpm。
- Bun 1.4.3(锁定)、Docker;Linux 解析沙箱构建需要 C 编译器和 Node-API 头文件(Debian: `build-essential libnode-dev`)。不需要单独安装 Node.js / pnpm。
- 端口:web 5180、API 4390、PG 55433。4380 留给本机部署版(Caddy),本机 3000/5175/5176/55432 已被其他项目占用,不要改用。
## 首次启动
```sh
cp .env.example .env # 按需修改;server 与 db 脚本都读仓库根 .env
deno install # 按 deno.lock 装依赖到 node_modules(成员包软链到 node_modules/@umt/*)
deno task dev:up # 起开发库 postgres:18.6(docker-compose.dev.yml)并迁移到最新;拉代码后也跑一次
deno task dev # 同时起 server(4390)与 web(5180)
bun install --frozen-lockfile # 按 bun.lock 装 workspace 依赖
bun run dev:up # 起开发库 postgres:18.6(docker-compose.dev.yml)并迁移到最新;拉代码后也跑一次
bun run dev # 同时起 server(4390)与 web(5180)
```
单独起:`deno task dev:server`(`deno run --watch`,默认 `--role all`,API + worker 同进程)、`deno task dev:web`。
单独起:`bun run dev:server`(`bun --watch`,默认 `--role all`,API + worker 同进程)、`bun run dev:web`。
## 验证
@@ -32,21 +32,76 @@ curl -H "Authorization: Bearer umt-dev-admin-key" "localhost:4390/api/v1/events?
| 命令 | 作用 |
|---|---|
| `deno task ci` | typecheck(tsc 7.0.2 + deno check)→ lint(biome)→ test(vitest)→ check:tokens → build,提交前必须全绿 |
| `deno task typecheck` | `deno types` 生成 `types/deno.d.ts` 后,逐个成员跑 tsc 7.0.2,再跑 `deno check`(Deno 内置 TS 6.0.3) |
| `deno task lint` / `deno task fmt` | biome 检查 / 自动修复(含 import 排序;相对 import 必须带扩展名) |
| `deno task test` | 逐个成员跑 vitest(`deno run -A npm:vitest run`);单包:`cd packages/<x> && deno task test` |
| `deno task db:reset` | 清空 public schema 并重新迁移(生产环境拒绝执行) |
| `deno task gen:types` | 从已迁移的库反射生成 `packages/db/src/types.ts` |
| `deno task gen:tokens` | 改了 app.css 后重新生成 token 登记表 |
| `deno task build` | web 构建到 `apps/web/build/client` + server 编译为单文件 `dist/umt-server` |
| `cd apps/server && deno task start` | 以受限权限(net/env/sys/read)直接跑源码 |
| `bun run ci` | typecheck(tsc 7.0.2)→ lint(biome)→ test(vitest)→ check:tokens / check:copy → build → check:binary,提交前必须全绿 |
| `bun run typecheck` | 逐个成员跑 tsc 7.0.2 |
| `bun run lint` / `bun run fmt` | biome 检查 / 自动修复(含 import 排序;相对 import 必须带扩展名) |
| `bun run test` | 逐个成员跑 vitest(`bun x --bun vitest run`);单包:`cd packages/<x> && bun run test` |
| `bun run db:reset` | 清空 public schema 并重新迁移(生产环境拒绝执行) |
| `bun run gen:types` | 从已迁移的库反射生成 `packages/db/src/types.ts` |
| `bun run gen:tokens` | 改了 app.css 后重新生成 token 登记表 |
| `bun run build` | web 构建到 `apps/web/build/client` + server 编译为单文件 `dist/umt-server` |
| `cd apps/server && bun run start` | 直接跑源码;外部字节解析另走 Linux seccomp 子进程 |
依赖:npm 包版本只在根 `deno.json` 的 `imports` 里登记(web 的前端依赖仍在 `apps/web/package.json`)。改完跑 `deno install` 更新 `deno.lock`。有安装脚本的新依赖需要 `deno approve-scripts` 显式批准。
依赖:各成员包在 `package.json` 登记依赖,根 `package.json` 管理 workspace 与共用工具。修改依赖后运行 `bun install` 更新 `bun.lock`;正常拉取使用 `bun install --frozen-lockfile`。解析沙箱测试前运行 `bun run sandbox:build`(`test:all` 会自动构建)。
## 工程设计与适度抽象
适度抽象用于减少重复修改、统一可靠行为,方便后续维护与开发。判断依据是已有实现的共同语义,不只看代码外形相似。
- 有稳定的重复机制时及时提取,例如协议分帧、消息上限、进程退出与回收;新增业务任务应能复用这些行为,不必重复写管道循环。
- 调用方各自决定的策略保持显式。文件操作按进度停滞判断超时,解析按总时长和 RSS 判断;权限、业务重试、取消后的文件清理不放进通信层。
- 共用机制放在双方可依赖的最小内部模块,按用途导出。不要为了复用创建完整框架,也不要用大量模式开关把不同业务揉成一个类。
- 保留清晰的类型边界。协议载荷在共用层是 unknown,具体输入、进度和结果由业务模块定义与校验;业务代码不直接依赖传输细节。
- 只有第二个实际使用方出现相同的管理逻辑时,再提取排队、常驻 worker 管理或进程池。没有性能证据时,不因为别的项目复用了进程就提前改运行方式。
- 持久业务任务与本地执行任务分别建模。下载、整理等流程的恢复与去重归任务引擎和业务记录;文件/解析 worker 只完成一次执行。通信失败不能自动重做有副作用的操作。
- 定时任务的触发与执行分开考虑:短巡检可直接运行;长耗时、需要恢复或产生重要副作用的任务应入持久队列。新任务不要在调度器里另建一套重试与恢复逻辑。
- 抽象后的测试覆盖真实边界:分块传输、异常退出、取消、大小超限、故障回收;业务模块继续验证各自的超时与副作用。不要只测试一层转发调用。
当前共用入口为 `@umt/runtime/worker`(`packages/runtime/src/worker.ts`):一次性连接、逐行 JSON 协议及有界回收。文件代理和解析代理分别选择启动环境、消息上限和终止策略;仍使用一次性子进程,未引入常驻池或自动重试。
协议为 `{id,task,input}` 请求,以及 `{id,type:"progress",value}` / `{id,type:"result",result}` / `{id,type:"error",error:{code,message,detail?}}` 响应。每条消息必须以换行结束。文件协议单条上限 16 MiB(内容走文件系统),解析协议单条上限 128 MiB(包括 base64);stderr 持续排空且仅保留末尾 2000 字符用于诊断。大小限制不替代任务的解压与资源限制。
服务端 `FS_PROXY_MODE` 缺省为 `subprocess`,启动失败报错。`auto` 的进程内回退仅在明确选择时使用,不能保证回收阻塞在系统调用中的线程。解析隔离失败始终报错。
### 模块与业务入口
- 业务操作由所属模块的 service 提供,负责业务不变量、写入和事务边界。HTTP 路由负责协议、访问权限、参数与响应;能力处理器负责能力输入与授权上下文,然后调用同一业务入口。服务不从路由文件或 Agent 会话编排里取业务实现。
- 跨模块的同步操作可以显式调用已有 service,无需为一次调用再建接口、注册表或消息总线。需要随业务提交可靠触发的异步后续动作,在同一事务写 outbox;接收方按业务 ID 回查并幂等执行。
- 共用计划绑定、持续授权范围和预算检查放在 `src/authorization/grants.ts`;Agent 专有的会话、审批交互策略留在 `modules/agent/authorize.ts`。共用数据检查不依赖 Agent 会话装配。
- 示例:策略预演/发布由 `acquisition-organize/services/policies.ts` 提供,路由与能力共用;整理入口仍是 `services/organize.ts`,内部按计划、执行、收尾拆分。首次完成与恢复重放共用收尾处理,移除 qB 的不同判断留在各自调用处。
- 任务由所属模块用 `defineTask` 注册真实 handler,`@umt/tasks` 只提供执行机制;未注册任务明确报错,不注册空壳兜底。同步业务规则直接实现,出现实际扩展需求后再判断是否需要钩子。
- 同一含义的类型只定义一份,例如 domain 与 contracts 共用 `ContentType`。`@umt/mapping` 只处理真实语义差异,例如下载暂停原因或 `undefined ↔ unknown`,不为同值枚举保留转换表。
### 状态与配置归属
每个阶段维护自己的业务事实,引擎成功只表示本次任务尝试完成。业务仍在等待授权、资源或磁盘空间时,不能因为引擎成功就显示业务已完成。时间线与页面应按这些事实生成只读投影,不要另存一套总流程状态。
| 事实 | 维护方 |
|---|---|
| 发布资源解析、身份绑定与证据 | sources-identity 的资源/身份记录 |
| 订阅目标、单元满足度、候选与收录计划 | acquisition-organize 的 subscription / slot / acquisition_plan |
| 下载提交与下载进度 | download_task 与下载服务 |
| 整理计划、执行结果与文件证据 | organization_record / organization_file_result 与整理服务 |
| 刮削结果 | scrape_record 与刮削服务 |
| 步骤、租约、重试与唤醒 | 任务引擎;不替代上述业务状态 |
部署环境负责连接地址、密钥、基础目录等;settings 负责日常业务策略;单次请求只覆盖明确允许的选项,需要恢复的执行计划冻结本次有效值。读取与校验放在所属模块已有的配置入口,调用方不各写一遍合并规则。存量配置的优先级仍按各模块现有约定执行,有实际重复再统一;对用户有影响的覆盖应说明有效值与来源。
收录策略的适度抽象:`domain/acquisition/policy.ts` 维护分类选择顺序,`domain/slot/season-release.ts` 维护规格身份与排序,候选判断仍在 `evaluateCandidate`。`services/collection-strategy.ts` 统一策略模板到订阅版本的创建入口,分类查询在事务外,版本创建在订阅锁内。追更与洗版通过同一 `SlotSpec` 的阶段规则表达,不创建两套计划执行器。季级目标从资产与计划事实计算,计划冻结在途规格;分集完成、界面和旧文件处置都使用这些事实。
资源属性属于 sources-identity:`service/release-facts.ts` 合成解析输入,Agent 补充只存 `raw._facts` 并保留出处,重解析不会抹掉它,新的明确解析值优先。`release-facts-admin.ts` 提供窄的补全入口,调查编排和持续授权仍由 agent 模块处理;两者不互相承接业务状态。修改规则编辑器时应保留未编辑的 JSON 字段,同时显式清除用户删除的已编辑字段。
后续优先扩展现有纯判断和业务服务;出现共同不变量时抽成有明确输入输出的函数,文件 IO、事务锁、授权和重试继续留在执行阶段。默认策略更新只影响后续模板创建,不隐式改写已建订阅版本;需要调整已有作品时直接编辑其保留版本。新增规则字段必须同时检查候选评估、完成口径、覆盖展示和物理替换,避免各自形成第二套规则。
配置收敛先确定编辑归属:订阅表单维护追踪与下载安排,具体版本维护质量、组合规则和洗版;删掉重复入口时同步删除草稿与序列化,不留下两套普通写入路径。旧导入字段保留在兼容入口,普通保存不得清除或重建它们。总览与执行复用 `resolveRuleChain` 的版本优先级,订阅级自动补全继承主版本的收录链;显式开关仍优先。业务记录跳转复用 `subjectLink`,异常状态复用现有决策状态,不另造处理中心。
手动下载以默认值直接提交,高级选项仅覆盖本次操作。搜索与工作台复用 `ManualDownloadActions` 的提交反馈、未识别确认和高级选项窗口,资源摘要与 API 参数仍由各入口转换;原站页面保留必须先解析详情的准备步骤。默认下载不先加载目录、作品详情或已有版本,不为三个入口引入通用流程引擎。
维护时先验证真实的成功、失败和恢复路径,尤其是授权、文件替换、取消与外部副作用;删除空壳或无使用者的机制时同步删除其专属测试与文档。不要为了目录对称增加转发层,也不要把不同阶段的状态合并成一个枚举。
## 进程角色
`umt-server --role api|worker|worker:agent|all`(源码:`deno run ... apps/server/src/main.ts --role ...`)。
`umt-server --role api|worker|worker:agent|all`(源码:`bun apps/server/src/main.ts --role ...`)。
| 角色 | 引擎 worker | OutboxRelay / 模块后台循环 |
|---|---|---|
@@ -118,7 +173,7 @@ ci 逐行核对这张表与任务注册表(`apps/server/test/worker-lanes.test.t
各保留期裁剪(`system/retention.ts` 的 `RETENTION_JOBS`)、`metadata.backfill_languages`(元数据语言回刷)。
只入队的定时任务(如 `trash.purge`、`cache.threshold`、`library.reconcile`、`pi.token_refresh`)按所入队任务的道执行。
启动时 `bootstrap()` 装配 `apps/server/src/modules/` 下的四个业务模块(见 apps/server/README.md),并连接任务引擎。数据库不可达时进程启动失败并退出。
启动时 `bootstrap()` 装配 `apps/server/src/modules/` 下的业务模块(见 apps/server/README.md),并连接任务引擎。数据库不可达时进程启动失败并退出。
acquisition-organize 的后台循环只做编排:qB 同步(含阈值暂停/恢复)一个定时器;另一个定时器按阈值入队 `cleanupDownloadCache`、
巡检补投 `submitDownload`/`verifyAndImport`、每 6h 入队 `reconcileLibrary`,重活都在任务里跑。
@@ -138,21 +193,21 @@ docker compose up -d --build # 服务:app(4380)+ postgres(数据卷 umt-p
curl localhost:4380/api/v1/health
```
- 镜像:`denoland/deno:2.9.7` 构建 web 与 `deno compile` 出二进制,运行时 `debian:trixie-slim` + gosu + postgresql-client + 静态 ffprobe,root 启动修 `/data` 属主后降权到 PUID(缺省 10001)。
- 镜像:`oven/bun:1.4.3` 构建 web 与 `bun build --compile` 出二进制,并构建 `sandbox-policy.node`,运行时 `debian:trixie-slim` + gosu + postgresql-client + 静态 ffprobe,root 启动修 `/data` 属主后降权到 PUID(缺省 10001)。
- 入口 `docker/entrypoint.sh`:迁移 → 自检 → 同时起 api、worker、worker:agent 三个进程;任一退出则停掉其余并以非 0 退出,由 `restart: unless-stopped` 拉起。
- 二进制编译时固定权限:`--allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,cat`;镜像带静态 ffprobe;运行期写入落在 `DATA_DIR`(容器内 `/data`)。完整部署说明见 [deploy.md](deploy.md)。
- 主进程使用非 root 用户和只读的 `/app` 产物;外部字节解析另起子进程,由原生 seccomp 策略限制文件、网络与进程访问。镜像带静态 ffprobe;运行期写入落在 `DATA_DIR`(容器内 `/data`)。完整部署说明见 [deploy.md](deploy.md)。
- 构建版本号:`APP_VERSION=v0.1.0 docker compose build`(写入 web 的 `version.json`)。
## 测试数据库
需要 PG 的测试(db、tasks)默认连 `DATABASE_URL` 或 55433 开发库,不可达时跳过。tasks 包可用 `TASKS_TEST_DATABASE_URL` 指定独立库;每个测试文件使用带 pid+随机后缀的独立 schema,结束时删除,多份测试并发跑也不冲突。
需要 PG 的测试默认连 `DATABASE_URL` 或 55433 开发库。单包测试在库不可达时会跳过集成用例,`bun run test:all` 会先检查连接并直接失败,不能把跳过当作通过。tasks 包可用 `TASKS_TEST_DATABASE_URL` 指定独立库;每个测试文件使用带 pid+随机后缀的独立 schema,结束时删除,多份测试并发跑也不冲突。
测试被中断(超时、kill、并发跑挂掉)时 schema 不会删,开发库会越积越多。清理脚本:
```sh
deno run -A scripts/dev-db-clean.ts # dry-run:只打印清单
deno run -A scripts/dev-db-clean.ts --verbose # 同上,逐个列出 schema 名
deno run -A scripts/dev-db-clean.ts --yes # 真正删除
bun scripts/dev-db-clean.ts # dry-run:只打印清单
bun scripts/dev-db-clean.ts --verbose # 同上,逐个列出 schema 名
bun scripts/dev-db-clean.ts --yes # 真正删除
```
- 只在 `DATABASE_URL`(缺省 `postgres://umt:umt@localhost:55433/umt`)指向 localhost:55433 时运行,否则直接拒绝。
@@ -163,6 +218,6 @@ deno run -A scripts/dev-db-clean.ts --yes # 真正删除
- `配置无效`:按提示检查 `.env`,`SECRET_MASTER_KEY` 至少 16 字符,`DATABASE_URL` 必须是 postgres://。
- health 返回 `db: "down"`:`docker compose -f docker-compose.dev.yml ps` 看容器健康状态。
- `deno check` 报找不到 `Deno`:检查根目录是否误放了 `tsconfig.json`(tsc 基座在 `config/tsconfig.base.json`)。
- tsc 报 `Deno` 未定义:先跑 `deno task types` 生成 `types/deno.d.ts`(已 gitignore)。
- 解析沙箱提示策略模块不存在:先跑 `bun run sandbox:build`;编译部署需把 `sandbox-policy.node` 放在二进制旁边。
- tsc 使用 `config/tsconfig.base.json`;运行时类型来自 Node / Bun,不再生成 Deno 声明。
- 日志中的 passkey/token/cookie/authorization/password 字段与 URL query 参数会被替换为 `[REDACTED]`(含 `logger.child()` 绑定字段与 `err` 的 message/stack);若发现明文,属于缺陷。