28 KiB
v0.1 独立验收审查
审查日期:2026-09-27。基线提交 9e6ff33。审查期间另一子代理在同一工作区修 03-decoupling-audit.md(W4-D),工作区里出现了它的未提交改动。本报告的结论只针对 HEAD 9e6ff33,那些改动不在审查范围内。
对照依据:00-first-version-plan.md §3.1 / §3.2,REQ v0.7 §2.1、§15.1-15.2、§16.1(只看与 M1 相关的 AT),以及 01、02、03 三份计划文档、README、runbook 和 ADR。
0. 总体结论
现在还不能打 v0.1.0。 下面 4 个阻塞项处理完后可以打,都不需要改架构。
代码主体已经达到 M0+M1 纵切的要求。实测结果:
deno task ci连跑 2 次都通过(退出码 0),耗时 136 秒和 133 秒。两次测试数完全一致:server 136、adapters 236(另有 2 个 skip)、agent 30、contracts 508、db 12、domain 230、mapping 22、tasks 15、testkit 4、web 3,合计 1196 个通过。- 前端冒烟
deno task test:web:smoke22/22 通过,耗时 49 秒。 - 五条原则都有 schema 或代码层面的保障。
阻塞发布的原因有四个:
03清单里的 P0 还没修完(后台循环绕过任务引擎;HTTP 请求里同步执行重活)。- 版本号还是 0.0.0。
- REQ §15.2 要求 M1 必须验证的一项"授权内异常调查与恢复"没有实现,也没有在任务书里写明延期。
POST /organization-records的sourcePaths可以指向服务器上任意路径,Move 时还会删源,违背原则 5。
补充说明:
- ci 耗时比任务书记录的约 75 秒涨到约 135 秒。其中 server 测试 55 秒、tasks 测试 34 秒。
- 冒烟跑在
PORT=4392 WEB_ROOT=apps/web/build/client上,用单容器同端口托管方式,管理员账号w31-admin。
1. 问题清单(按严重度)
1.1 阻塞发布
| # | 问题 | 位置 | 修法 |
|---|---|---|---|
| R1 | 03 的 P0 未落地。start() 的 tick 仍在直接调 planImport/executeRecord(sweepPendingImports),没有租约保护;/download-cache/cleanup 和 /library/reconcile 在 HTTP 请求里同步执行 |
apps/server/src/modules/acquisition-organize/index.ts:118-151,routes/downloads.ts、routes/library.ts |
按 03 清单 P0 改为 enqueue,并返回 {taskId, runId}。W4-D 合入且 ci 通过后,再重跑本报告第 3 节的 e2e |
| R2 | 所有包和 app 的版本都是 0.0.0,/health 返回的也是 version:"0.0.0";仓库里没有任何 tag |
apps/server/deno.json:3 等 10 处 |
统一改成 0.1.0(server 的 denoJson.version 会写进备份清单和 health),再打 v0.1.0 tag |
| R3 | REQ §15.2 要求 M1 验证"授权内异常调查与恢复",AT68 也要求"查证后自动执行获准修正并继续"。现在后台调查固定是 plan_only:E5 只把结论写回待处理事项并交还人工,没有自动恢复这条路径 |
apps/server/src/modules/agent/service.ts:323-340,test/e2e/e5-auto-investigate.test.ts |
二选一。(a) 在有 automation_grant 时允许调查 run 以 execute 模式调用 task.retry 或 identity.resolve,并加一条 e2e:下载失败 → 调查 → 授权内重试 → 入库。(b) 在 00 §3.2 写明"授权内恢复延到 M2",并在 README 的范围说明里注明。不能不写就发布 |
| R4 | POST /organization-records 的 sourcePaths 没有任何根目录约束。持 organize.execute 的 API Key 可以 walk 任意目录(dryRun 时会泄漏文件清单),也可以把任意文件 Move 进媒体库并删掉源文件。这条路径不检查 automation_grant,也不检查 H&R |
apps/server/src/modules/acquisition-organize/services/organize.ts:229-241,routes/organization.ts:84-130 |
sourcePaths 必须落在已登记的下载器路径映射(本机视图)或显式的导入根下,并且不能在任何 library_root 内;action=move 且调用方是 api_key 时要求有持续授权。补 403/422 反例测试 |
1.2 应修(v0.1.x 内,11 项)
| # | 问题 | 位置 | 修法 |
|---|---|---|---|
| S1 | 持 organize.execute 的 API Key 可以 POST /approvals 再 PATCH /approvals/{id} 自己批准,再去调 download.submit 或 organize.execute,单次审批就形同虚设 |
packages/contracts/src/api.ts(approvals 的 scope),modules/agent/routes.ts:~280-311 |
把审批的决定动作(PATCH 改为 approved)的 scope 改成 admin,或者用 requireAdminSession;并禁止 decided_by 与计划发起人是同一个 api_key 已修(f48e1aa):POST /approvals、PATCH /approvals/{id} 标 auth:"session",只接受管理员会话,API Key 一律 403 |
| S2 | 服务端 logger 的脱敏覆盖面比 agent 和 adapters 的实现窄。实测 redactString("/rss?rsskey=abc&torrent_pass=dead&authkey=zz&downhash=q1&c_secure_pass=cc") 只遮住了 passkey。现在一共有 4 份脱敏实现(W1 的 S8 还没解决,反而多了一份) |
apps/server/src/logger.ts:6-8,packages/agent/src/redact.ts,packages/adapters/src/{rss,browser}/redact.ts |
在 contracts(或新建的共享包)里保留唯一一份实现,取 agent 版的参数全集;logger 改为复用它;在 logger.test.ts 加上述 5 类参数的用例 已修(f48e1aa):键表与规则收到 @umt/contracts 的 redact.ts,logger 与 redactForModel 共用;adapters 的 rss/browser 两份仍独立,留到 M2 收口 |
| S3 | TMDB 网络错误时,TmdbError 的 message 直接拼上 Deno fetch 的错误信息。Deno 的这条信息里带完整 URL,也就带着 api_key;cause 里同样有(W1 的 S7 没有解决) |
packages/adapters/src/tmdb/client.ts:171-175 |
message 先经过 redact,cause 不透传;或者强制使用 Bearer(accessToken) 已修(f48e1aa):message 经 redactUrl,cause 只留脱敏后的 name/message |
| S4 | 服务端授权器的单次审批分支(payloadHash/revision 变化后失效、过期、用后标为 consumed、approvalId 与计划不符)没有任何测试。AT15/AT16 在产品层也没有被验证 | apps/server/src/modules/agent/authorize.ts:115-137 |
新增 test/agent/authorize.test.ts,对真实 PG 覆盖 6 条正反例;再加一条 e2e:审批后改计划 → 被拒 → 重新审批 → 成功且只消费一次 已修(f48e1aa):test/agent/approvals.test.ts 覆盖 8 条拒绝分支与改计划→重批→只消费一次→不可重放;e2e 版留给 E2 系列 |
| S5 | M1 要求的"一部常规剧集"没有端到端链路。E1 和 E2 只覆盖电影;剧集只在识别单测(resolve)和整理单测(organize-move)里分别出现过 | apps/server/test/e2e/ |
新增 E7:RSS 剧集 S01E01 → 识别出 series 和单元 → Slot → qB → Copy 入库;第二集改译名后仍进旧目录 |
| S6 | AT24(管理标签被移除、qB 任务消失)在服务端没有测试。代码在 downloads.ts:359-383 里,只有 domain 层的 download-state 单测覆盖 |
__tests__/vertical.test.ts |
补两个断言:去掉标签后 managed=false、历史保留、之后不再暂停也不再清理;任务消失后 removed,并且库文件还在 |
| S7 | 前端 Agent 页在生产构建里也能用 ?mock=1 打开 mock 模式(sessionStorage 会记住),可能把演示数据当成真实运行展示,与 AT78 冲突 |
apps/web/app/api/agent.ts:37-48 |
只在 import.meta.env.DEV 或设置了 VITE_AGENT_MOCK 时启用 已修(f48e1aa):只在 DEV 构建或后端 pi.provider = "fake" 时生效,生产构建忽略并清掉记住的开关(app/api/agent-mock.test.ts) |
| S8 | 引擎面板只有 cancel,没有 retry/rerun;§3.1-10 要求 "cancel/retry 操作"(02 backlog 也列了这一项) | apps/web/app/routes/console/engine*.tsx,modules/auth-settings/engine-routes.ts |
加 POST /engine/runs/{id}/rerun,用我们自己的 enqueue、带新幂等键实现,并写审计 |
| S9 | 账户 TOTP 在控制台没有入口(只有登录页的第二步),02 backlog 里是应修项 | apps/web/app/routes/console/layout.tsx |
账户弹层接上 GET/POST /auth/totp |
| S10 | API 注册表把 `/auth/setup | login | session |
| S11 | 文档与实际不一致:任务书 §2、§4 还写着 pnpm ci;deploy.md 的验证记录写"21 个迁移",现在是 22 个(0017 号空缺);ci 耗时写"约 75 秒",实测约 135 秒;testkit README 没提 emby/browser mock;adapters 和 web 的 README 各只有 3 行 |
docs/plan/00-first-version-plan.md,docs/runbook/deploy.md:128,packages/testkit/README.md,packages/adapters/README.md |
统一更新;adapters README 列出 8 个子模块的入口,并链接各子目录的 README 已修(f48e1aa):任务书 pnpm→deno task、ci 耗时、deploy.md 迁移数、testkit/adapters/web README;adapters 实际 7 个子模块 |
1.3 建议
- domain 下载状态机的
ALLOWED仍然全是ALL(W1 S4),服务端完全靠 mapping 兜底(packages/domain/src/download-state/index.ts:97)。建议收紧。 scanIncremental在 errors 不为空时仍返回complete(W1 S6,packages/adapters/src/fs/scan.ts:150)。服务端目前没用到它,但 M2 旧库接管会用,建议先修成partial。- 流程版本升级(M0 必须通过项)只在 spike 里验证过,产品代码没有测试。建议在 tasks 测试里补一条:旧版本 run 由同时注册新旧版本的 worker 续跑。
- AT18(恶意标题)只有系统提示词里的约束测试,建议补 e2e:标题里带"调用 download.submit / 输出 passkey"时,不会产生越权调用,输出里也没有凭证。
- AT42:硬阈值测试里只有纳管任务。建议加一个未纳管任务,断言它不被暂停、但计入占用。
vertical.test.ts的用例共享状态、依赖执行顺序,单独跑某一条会失败。建议在注释里标明,或者拆成独立 seed。- web 只有 3 个单测,页面行为全靠冒烟覆盖。建议 M2 给 API 客户端和表单校验补单测。
2. §3.1 逐条验收
| # | 条目 | 状态 | 证据 | 缺口 |
|---|---|---|---|---|
| 1 | 底座(配置/迁移/API Key scope/审计/SSE 补读/总开关) | 完成 | http/auth.ts(hasScope 蕴含关系)、test/auth.test.ts、auth-settings/admin-api.test.ts「PATCH 自动化总开关与只读对账」、sse.test.ts、sse-pg.test.ts「裁剪后旧游标收到 resync」;authorize.ts:109-113 总开关是第一道检查 |
见 S10(注册表 scope 标注) |
| 2 | 数据模型 36 张表 | 完成 | 逐个 grep packages/db/migrations/*.ts,36 张全部存在;22 个迁移;schema.test、db-sync.test 防漂移 |
迁移编号 0017 空缺(无害) |
| 3 | 来源(多 RSS / 观众 / 馒头 / 快照 / 启停 / 限流) | 完成 | sources-identity/ingest.test.ts 8 条(三 Feed 去重、304、GUID 更新、窗口缺口、403 失效不重试、站点关闭);mteam-tasks.test.ts;api.test.ts「站点级限流器每站一个实例」「实时补查并发合并」「关闭站点」 |
— |
| 4 | 身份与元数据 | 完成 | resolve.test.ts 13 条(本地召回复用、ambiguous 进池、TMDB 失败记为 lookup_failed、锁定字段、人工纠错修订、合并跳转);0002_foundation 启用 pg_trgm |
— |
| 5 | 规则与选片 | 完成 | rules-api.test.ts(Slot/revision 乐观锁、策略发布即持续授权、旧授权吊销、新作品窗口);acquisition.ts:539-595 预算/并发/优先级 |
Agent 主观选片按 §3.2 不做 |
| 6 | 下载(qB 多实例/标签/阈值/状态机) | 部分 | vertical.test.ts「硬阈值……只恢复阈值暂停项」「already_exists 接管只补标签」;downloads.ts:861 删数据必经 assertSafeToDeleteData |
S6(标签移除/任务消失无服务端测试);R1(qB 同步与大文件复制在同一个 tick 里) |
| 7 | 入库(Copy/Move/EXDEV/路径固定/整理记录 API) | 部分 | organize-move.test.ts「跨卷 Move kill 后 resumed……不重复复制」「路径绑定固定」;vertical.test.ts「changes 游标增量」 |
R4(sourcePaths 无约束) |
| 8 | 持久任务 9 个 + outbox + 恢复 + 幂等 + 取消 | 部分 | 引擎注册了 11 个任务(9 个 + snapshotSite + runAgent);tasks/test/crash-recovery、engine.test「取消经 abortSignal」、op-ledger「租约丢失旧 attempt 提交被拒」、outbox「两个 relay 并发 60 条只投一次」;E4 |
R1:后台循环绕过引擎 |
| 9 | Agent(Pi 封装/14 能力/服务端授权/SSE/后台触发/设置) | 部分 | E2「启动断言 14 个能力全部注册」「没有授权被拒」「已提交≠已入库」;agent/test/runtime.test.ts 白名单、脱敏、预算、JSONL 恢复;E5 |
R3(调查后不能恢复);S4(审批分支无测试);子代理按 02 后置 |
| 10 | 控制台页面 + 引擎面板 | 部分 | apps/web/app/routes.ts 共 20 个路由;冒烟 22/22 通过 |
S8(引擎面板没有 retry);S9(TOTP 入口);原站视图只是占位(符合 §3.2) |
| 11 | 可选 Emby | 完成 | adapters/src/emby/emby.test.ts「凭据错误 → unknown(auth),而不是不存在」等 10 条;vertical.test.ts 调用了 /inventory/media-servers |
服务端只测了无实例的情况(建议补 unknown 透传) |
| 12 | 运维(备份/只读对账/单容器/runbook) | 完成 | scripts/backup.sh、restore.sh(校验和、有活动连接时拒绝、单事务、打开 readOnlyReconcile、映射行数比对);deploy.md 验证记录里有完整的 compose 演练;preflight.test.ts 7 条 |
恢复没有自动化测试,只有人工演练记录(可以接受) |
| 13 | 测试(domain 单测 / adapters / RSS→入库 e2e / Agent e2e / 崩溃恢复) | 完成 | E1、E2、E4(worker 子进程 kill -9)、agent 的 JSONL SIGKILL 用例;ci 两次数量一致 |
S5(剧集链路) |
| 14 | 管理员登录门禁 | 完成 | login.test.ts 8 条(并发初始化只有一个成功、CSRF、同 IP 5 次 / 跨 IP 15 次限速、吊销、改密、TOTP 防重放);实测:已初始化后 setup 返回 409,缺少或伪造 csrf 返回 403,cookie 为 HttpOnly + SameSite=Lax |
Passkey 按 02 后置 |
§3.2 不做项核对:综艺分段、双版本组合校验、浏览器原站视图、HHClub/CHD、旧库接管、Agent 主观选片、多用户,都没有越界实现。BrowserProvider 只有接口和 camofox 骨架(effaa32),符合"接口先落"。
3. REQ 验证项对照
3.1 M0 / M1 必须验证项 → 测试
| 验证项 | 测试 | 结论 |
|---|---|---|
| Pi 显式白名单、无隐式扩展 | agent/test/runtime.test.ts「白名单:未注册工具(bash)……被拒绝」;E5 download__submit 被拒 |
有效 |
| 执行器崩溃恢复 | tasks/test/crash-recovery.test.ts;E4;runtime.test.ts「JSONL 恢复 SIGKILL」 |
有效 |
| 流程版本升级 | 只有 docs/spikes/w0b-openworkflow.md |
缺(建议项) |
| 提前审批不丢失 | 设计上审批存库、invoke 时再读;tasks/engine.test「waitForFact 事实先于等待到达不丢失」 |
引擎层有效,产品层缺(S4) |
| 多 Feed 并发不重复提交 | ingest.test「三个 Feed……release.observed 只发一次」(顺序执行);outbox.test 并发;E6「重复评估只留一条候选」 |
部分(没有并发 ingest 的测试) |
| 改名与映射修订后路径不变 | organize-move.test「路径绑定固定」;resolve.test「人工纠错映射」 |
有效(两者分开测) |
| 观众 + 馒头站点 | api.test(观众 mock)、mteam-tasks.test、adapters spiders 与 mteam 测试 |
有效 |
| qB 标签接管 | vertical「already_exists」「硬阈值」中 adopted=2 |
有效 |
| Copy / Move | E1(Copy);organize-move(跨卷 Move) |
有效 |
| 一个电影 + 一部常规剧集 | E1/E2 电影 | 剧集缺(S5) |
| 整理记录 API | vertical「changes 游标增量」;E1 |
有效 |
| 备份 / 审计 / 恢复 / 停用开关 | deploy.md 演练;admin-api.test 总开关;preflight.test「映射清单行数比对」 |
有效(恢复靠人工) |
| Agent 自然语言 → 查库存/搜索/选片/提交 → 持续流程入库 → 真实汇报 | E2「有持续授权:……汇报来自整理记录」 | 有效 |
| 授权内异常调查与恢复 | E5 只调查 | 缺(R3) |
| 关闭聊天不影响后台 | E3「创建后不再请求,任务系统独立推进到 completed」 | 有效 |
3.2 指定 AT 场景
| AT | 场景 | 测试 | 结论 |
|---|---|---|---|
| AT01 | 多来源合并 | ingest.test 三 Feed |
有效 |
| AT02/AT09 | Slot 隔离 | rules-api、E6「按作品选 Slot」;授权按 Slot 独立预留 |
部分:没有"两个 Slot 同时缺失分别执行"的专项测试 |
| AT12/AT55 | 崩溃恢复 | E4、op-ledger「租约丢失」 |
有效;E4 的 kill 时机是"账本行出现",没有精确钉在"qB 已收单、DB 还没写" |
| AT13/AT27 | 复制后崩溃 | organize-move 跨卷 resumed |
有效(模拟上一次 attempt) |
| AT20/AT56 | 备份恢复 | 人工演练(deploy.md);auth.test 撤销后 401 |
部分(没有自动化) |
| AT16 | 授权失效重校验 | rules-api「新版本发布(旧授权吊销)」;审批分支无测试 |
部分(S4) |
| AT23 | 标签纳管 | vertical 硬阈值用例 adopted + origin:"adopted" |
有效 |
| AT24 | 标签移除 / 任务消失 | 无服务端测试 | 缺(S6) |
| AT25/AT26/AT41 | 缓存硬阈值 | vertical「硬阈值」「缓存清理」(库内陷阱任务被拒删) |
有效 |
| AT67 | Agent 一次委托闭环 | E2 | 有效 |
| AT70 | 已提交≠已入库 | E2 waiting.summary 含"已提交≠已入库",入库后才 succeeded |
有效 |
| AT79 | SSE 断线补读 | sse.test、sse-pg.test(resync)、E6「/events?after= JSON 补读」、E2 afterSeq |
有效 |
4. 路由 scope 表
共 185 个端点:read 89、organize.read 3、organize.execute 21、admin 72。所有业务路由都经过三个 helper 之一(modules/*/http.ts、routes/helpers.ts、auth-settings/endpoint.ts),统一加 requireScope(def.scope)。api-registry.test 保证 OpenAPI 与注册表双向一致。POST /capabilities/{id}/invoke 标的是 read,但处理器内部会再按能力描述符的 scope 校验(agent/routes.ts:325),例如 download.submit 需要 organize.execute,policy.apply 需要 admin。
免鉴权清单:GET /api/v1/health、GET /api/openapi.json、GET /api/docs、POST /auth/setup(已初始化后返回 409,token 至少 16 位)、POST /auth/login、GET /auth/session,以及 WEB_ROOT 静态文件。实测 /events、/settings、/library、/auth/whoami 无凭证时都返回 401。
| 资源 | read | organize.read | organize.execute | admin |
|---|---|---|---|---|
/acquisition-plans |
GET · / {id} | POST · | ||
/acquisition-slots |
GET · / {id} | POST ·; PATCH {id} | ||
/agent/runs |
GET · / {id} | POST ·; POST {id}/messages; POST {id}/abort | ||
/api-keys、/secrets |
GET/POST/PATCH/DELETE | |||
/approvals |
GET · / {id} | POST ·; PATCH {id}(S1) | ||
/artifacts、/asset-locations、/asset-unit-coverage、/media-assets、/path-bindings、/observations、/reconcile-runs、/server-mappings |
GET · / {id} | |||
/audit-events |
GET | |||
/auth |
setup/login/session(免鉴权)、logout | password、totp(要求 admin 会话) | ||
/automation-grants |
GET · / {id} | POST ·; PATCH {id} | ||
/capabilities |
GET ·; POST {id}/invoke(内部再校验) | |||
/content-units |
GET · / {id} | PATCH {id} | ||
/dashboard、/inventory、/inventory/media-servers、/site-definitions、/events、/health |
GET | |||
/decisions |
GET · / {id} / summary | POST {id}/resolve | ||
/download-cache |
POST cleanup | |||
/download-tasks |
GET · / {id} | POST {id}/pause、resume | ||
/downloader-instances、/media-server-instances、/library-roots、/path-mappings |
GET · / {id} | POST/PATCH/DELETE、test | ||
/engine/runs、/engine/stats |
GET;POST runs/{id}/cancel | |||
/external-identities |
GET · / {id} / revisions | POST ·; PATCH {id} | ||
/feed-sources |
GET · / {id} | POST/PATCH、fetch/enable/disable/reset | ||
/identity-pool |
GET | POST {id}/assign、dismiss、retry | ||
/library |
GET · / {id} | POST reconcile | ||
/media-entities |
GET · / {id} / detail | POST {id}/identities、refresh;PUT/DELETE {id}/fields/{field} | POST ·; PATCH {id}; POST {id}/merge | |
/organization-records |
GET · / {id} / changes | POST ·(R4) | ||
/policy-revisions |
GET · / {id}; POST {id}/preview | POST/PATCH、publish、rollback | ||
/preference-tags、/preference-suppressions |
GET · / {id} / revisions | POST/PATCH/DELETE | ||
/settings |
GET | PATCH | ||
/site-accounts、/sites |
GET · / {id} / snapshots / releases | POST/PATCH、enable/disable/snapshot、PUT credentials | ||
/source-releases |
GET · / {id} | POST {id}/identity、unlock | ||
/subscriptions |
GET · / {id} | POST ·; PATCH {id} | ||
/tasks |
GET · / {id} / operations | POST {id}/cancel、retry | ||
/title-aliases |
GET · / {id} | POST ·; DELETE {id} |
5. 安全快查
| 项 | 结果 |
|---|---|
| 凭证进日志 | 访问日志的 URL 经过 redactString,但它只认 passkey/token/access_token/api_key/apikey/auth/sign 这几个参数,漏掉了 rsskey、torrent_pass、authkey、downhash、c_secure_pass(S2,已实测)。对象字段按键名脱敏。secrets.test「SecretError 进日志只有 ref」和 logger.test 6 条通过。本次起的服务日志里 grep 不到未脱敏的 passkey/api_key |
| 凭证进模型上下文 | pi/runtime.ts:157 的工具输出一律经过 redactForModel,错误信息经过 redactString;runtime.test「工具输出进入模型与事件前已脱敏」。凭证存储为 secret_ref + AES(auth-settings/crypto.ts),响应里只返回 secretRef(api.test「凭证只写」、admin-api.test「Pi apiKey 只写」)。风险点:TMDB 错误信息里带 api_key(S3),不过 agent 侧的脱敏能认出 api_key= |
| 鉴权绕过 | 免鉴权清单见第 4 节;DEV_ADMIN_KEY 只在 NODE_ENV=development 下生效(auth.test);控制台管理员从 Web 手动调用算作人工授权(authorize.ts:114),API Key 必须有 grant 或 approval。漏洞:S1(自己批准)、R4(sourcePaths) |
| CSRF | cookie 会话的写方法用 HMAC 双提交校验(auth.ts:170-176),实测缺少或伪造都返回 403;Bearer 不校验。cookie 为 HttpOnly、SameSite=Lax,secure 看直连 https,或者来自私网代理的 x-forwarded-proto。login/setup 免 CSRF(可以接受) |
| SSE 鉴权 | /events 需要 read(app.ts:102),sse.test「需要鉴权」;实测用 cookie 连上返回 200 text/event-stream。给 API Key 调用方的 ?after=&limit= JSON 补读已实现(E6)。注意:持 read 的 Key 能看到所有 agent 文本增量 |
| qB 删数据防护 | 仅有两个 deleteFiles:true 调用点(downloads.ts:879、:902),都经过 removeWithData,在本机视图和下载器视图上各跑一次 assertSafeToDeleteData;候选必须满足:已入库 succeeded、没有失败的文件级结果、不再被 download_cache/hardlink 位置引用、保留期和 H&R 已满足。Move 后删种用的是 deleteFiles:false(organize.ts:1101)。vertical「缓存清理」里的库内陷阱任务被拒删,有效 |
| API Key scope 覆盖率 | 185/185 个端点都有 scope,见第 4 节 |
6. 工程规范
- 提交规范:48 个提交全部符合
type(scope): 描述,0 个违规。 - 命令一致性:README、runbook、各包 README 里出现的
deno task *都能在 deno.json 里找到;全文没有残留 pnpm/npm 用法(任务书除外,见 S11)。 - 包 README 与导出:agent、db、domain、mapping、tasks 的 README 与导出一致。contracts 有 18 行,尚可。testkit 缺 emby/browser mock 的说明;adapters 和 web 的 README 只有 3 行(S11)。
- ci 时长与稳定性:2 次分别 136 秒和 133 秒,测试数与结果一致,没有抖动。server 测试 55 秒、tasks 测试 34 秒是主要耗时。
抽查 10 个关键测试(是否断言了真实行为)
| 用例 | 结论 |
|---|---|
| E2「没有授权:download.submit 被拒」 | 有效:断言了 tool_call_end 为 isError、SSE 的 phase=denied、审计 capability.denied、download_task 为 0、计划停在 draft、Slot 仍为 empty,汇报标注"未经核查" |
| E2「有持续授权……」 | 有效:断言了调用序列、账本 grant_id、run 先 waiting 再 succeeded、汇报引用整理记录 ID、afterSeq 补读、qB 只 add 一次 |
| E4 kill -9 | 有效:qB 只 add 一次、账本只有 1 行、resumed 事件、seq 无重复;kill 时机不够精确(见 3.2) |
organize-move 跨卷 resumed |
有效:copy 为 skipped、delete_source 为 done、qB 删种时 deleteFiles=false、download_task 为 imported |
vertical 硬阈值 |
有效:只暂停纳管的下载中任务、手动 resume 返回 412、恢复水位区间保持暂停、只恢复阈值暂停项、读不到磁盘时保守阻止 |
vertical 缓存清理 |
有效:库内陷阱被拒删并给出原因,库副本仍在,asset_operation 已落库 |
ingest 三 Feed 去重 |
有效:1 条 source_release、3 条观察、事件只发 1 次、时间分开保存、游标持久化 |
login CSRF / 限速 |
有效:403/429 正反例齐全,被拦的请求不落表 |
sse-pg 跨进程 |
有效:子进程发布、API 推送,裁剪后收到 resync |
agent/registry「approve 类必须经授权」 |
部分有效:只测了注册中心回调,服务端 createAuthorizer 的审批分支没有测试(S4) |
7. 五原则核对(每条两处)
| 原则 | 保障 | 违背或风险 |
|---|---|---|
| 身份≠名字 | ① external_identity_accepted_key:(provider, entity_type, external_id) 在 accepted 状态下部分唯一(0005_identity.ts:34);② 合并时用 merged_into_id 跳转,旧 ID 保留(resolve.test「合并重复作品」) |
domain 的匹配仍要求本地候选带 externalId(W1 建议项,没有修) |
| 路径绑定固定 | ① path_binding_active_key:(entity, season) 同时只允许一个 active 绑定(0007_assets.ts:88);② organize-move「译名变化后新集仍进旧目录」 |
R4:任意 sourcePaths 的 Move 可以从别的库根把文件搬走 |
| 模型不是唯一事实来源 | ① authorize.ts 在服务端核对 grant/approval,忽略模型参数里的"已同意";② agent/report.ts 的汇报取自整理记录,模型文本标为"未经核查"(E2) |
S1:外部 Key 可以自己批准 |
| 失败≠不存在 | ① 对账时库根离线只标 unavailable,缺失按 suspected → confirmed 推进(vertical「资产对账」);② TMDB 失败记为 lookup_failed,不进 no_match(resolve.test),Emby 失败返回 unknown |
scanIncremental 出错仍报 complete(建议项) |
| 清理按明确授权 | ① assertSafeToDeleteData + lockReason(入库已验证、没有部分入库、没有引用、H&R);② 只读对账或总开关关闭时拒绝一切自动 execute(authorize.ts:109-113、downloads.ts:557) |
R4:手动 Move 删源时不检查 H&R 和授权 |
8. 给 M2 的前 5 条建议
- 合并凭证处理通道:只保留一份共享的 redact 和 SecretResolver,qB/TMDB/Emby/RSS 统一走 secret_ref(W1 S8)。浏览器接入之后会有 4 类以上凭证流经日志和模型,在 M2 引入 camofox 之前就要收口,并加一条"全仓凭证泄漏"的故障注入测试(REQ §16.2 要求零容忍)。
- 授权模型产品化:审批的决定限定为管理员会话;调查型 Agent 在授权内可以执行恢复(R3 的完整版);对 grant/approval/budget 做基于属性的测试(AT15/16/32/52)。
- 按 03 清单把 IO 与 Agent 分成两个 worker 组,给 verifyAndImport、runAgent 分配独立并发度,并加事件循环延迟探针。在性能基线(REQ §16.2)里记录文件数、挂载类型和 ci 耗时,防止 ci 从约 135 秒继续膨胀(可以考虑按包并行跑
-j)。 - e2e 矩阵扩到剧集和部分失败:剧集追更(S5)、整季包部分已有(AT54)、部分文件入库后不清理(AT29)、多下载器同一 hash(AT73)。这些是 M2 综艺分段建模的前置样本。
- 把恢复和版本升级纳入 CI:用 docker compose 跑"备份 → 清库 → 恢复 → 只读对账 → 映射比对"的夜间任务,再加一条 openworkflow 版本升级的回归测试,避免只靠人工演练记录。