Files
UHD_Media_Tool/docs/plan/04-v0.1-acceptance.md
T
2026-09-27 22:24:59 +08:00

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:smoke 22/22 通过,耗时 49 秒。
  • 五条原则都有 schema 或代码层面的保障。

阻塞发布的原因有四个:

  1. 03 清单里的 P0 还没修完(后台循环绕过任务引擎;HTTP 请求里同步执行重活)。
  2. 版本号还是 0.0.0。
  3. REQ §15.2 要求 M1 必须验证的一项"授权内异常调查与恢复"没有实现,也没有在任务书里写明延期。
  4. 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 条建议

  1. 合并凭证处理通道:只保留一份共享的 redact 和 SecretResolver,qB/TMDB/Emby/RSS 统一走 secret_ref(W1 S8)。浏览器接入之后会有 4 类以上凭证流经日志和模型,在 M2 引入 camofox 之前就要收口,并加一条"全仓凭证泄漏"的故障注入测试(REQ §16.2 要求零容忍)。
  2. 授权模型产品化:审批的决定限定为管理员会话;调查型 Agent 在授权内可以执行恢复(R3 的完整版);对 grant/approval/budget 做基于属性的测试(AT15/16/32/52)。
  3. 按 03 清单把 IO 与 Agent 分成两个 worker 组,给 verifyAndImport、runAgent 分配独立并发度,并加事件循环延迟探针。在性能基线(REQ §16.2)里记录文件数、挂载类型和 ci 耗时,防止 ci 从约 135 秒继续膨胀(可以考虑按包并行跑 -j)。
  4. e2e 矩阵扩到剧集和部分失败:剧集追更(S5)、整季包部分已有(AT54)、部分文件入库后不清理(AT29)、多下载器同一 hash(AT73)。这些是 M2 综艺分段建模的前置样本。
  5. 把恢复和版本升级纳入 CI:用 docker compose 跑"备份 → 清库 → 恢复 → 只读对账 → 映射比对"的夜间任务,再加一条 openworkflow 版本升级的回归测试,避免只靠人工演练记录。