35 KiB
v0.1 第二轮独立验收审查(发布前终审)
审查日期:2026-09-28。基线 HEAD 36aa99b(工作区只有 deno.lock 未提交改动)。只读审查,本文件是唯一写入。
对照:04-v0.1-acceptance.md(第一轮)、00 §3.1 与 §6、02、03、05、06、docs/design/agent-ui-reference.md、docs/porting/{mp-rust-logic-parity,moviepilot-rust-transfer}.md、README、runbook、CHANGELOG。
约束说明:W6-A 正在独占修 e2e 稳定性,本轮没有跑全量 deno task ci,也没有跑依赖 PG 的测试。依赖 PG 的测试(server e2e、file-manager、acquisition-organize __tests__)只读代码核对断言,没有实际运行。
本轮实测(均通过):
| 项 | 结果 |
|---|---|
| 单包 vitest(不依赖 PG) | domain 510、contracts 589、adapters 264(另有 3 个 skip)、mapping 22、agent 31、web 27;server 里不依赖 PG 的 4 个文件(api-registry / logger / invoke-limits / health)共 56 个 |
| 类型检查 | apps/server、apps/web 的 deno task typecheck 退出码 0 |
| biome | 763 个文件,0 个 error;1 个 warning(subscription.test.ts:132 的 dt2 未使用),1 个 info(subscriptions-policy.tsx:420 有多余的 fragment) |
实机 PORT=4396 --role api(测完已关) |
/health 返回 version:"0.1.0";/files/roots、/files、/files/trash、/engine/runs 无凭证时 401;path=../etc 返回 422;用 DEV_ADMIN_KEY 的 Bearer 调 POST /approvals 返回 403 |
| 提交规范 | 82 个提交全部符合 type(scope): 描述;还没有任何 tag |
0. 结论
前提是 W6-A 把 e2e 治稳、deno task ci 连续两次全绿。在这个前提下,修完 1 个阻塞项(只改文档)就可以打 v0.1.0。
- 第一轮的 4 个阻塞项全部复核落地。11 个应修项里 10 个已验证,S9(控制台 TOTP 入口)仍缺。
- §6 追加范围:
- 文件管理:后端 18 个端点齐全,规格 §4.1 的 7 条不变量都有代码和测试对应。
- 订阅增强:05 清单 P0 的 1–7 条全部完成,第 8 条部分完成。
- Agent 页:agentui 映射表 11 行全部落地。
- 转移语义:5 项全部落地,挂死隔离在生产二进制里生效有容器演练记录。
- 差分补迁:missing 和 our_bug 已清零。
- 阻塞的是发布说明:CHANGELOG
[0.1.0]完全没写 W4-E4、W4-F、W5-A~E,"已知限制"里还有一条已经过时的表述。 - 安全面没有发现需要阻塞发布的漏洞。有 1 个多源 Move 的授权范围缺口(应修 S-1),危害比第一轮 R4 小:只能在已登记的根之间搬运,而且资产位置会跟着更新。
1. 问题清单
1.1 阻塞发布(1)
| # | 问题 | 位置 | 修法 |
|---|---|---|---|
| B1 | CHANGELOG [0.1.0] 只写到 W4-E3。缺少 W4-E4(转移语义)、W4-F(Agent 会话页)、W5-A/A2/B(文件管理)、W5-C(订阅增强)、W5-D(识别差分补迁)、W5-E(生产 fsworker、autoInvestigateMode 设置)。"已知限制"第 3 条("没有来源会把集单元确认……Slot 不会进入 satisfied")在 W5-C 引入总集数账本后已经不成立 |
CHANGELOG.md |
按波次补 W4-E4 到 W5-E 的条目;把"已知限制"换成本文 §8 的清单(或直接链接本文),删掉过时的第 3 条 |
1.2 应修(v0.1.x,10 项)
| # | 问题 | 位置 | 修法 |
|---|---|---|---|
| S-1 | /files/move 用 API Key 执行时,只取 sources[0] 对应的作品去核对持续授权。只要持有覆盖作品 A 的 grant,就能在同一请求里把作品 B 的已登记文件一起 Move 走 |
file-manager/transfer.ts:236-244 |
对每个源(目录按其中已登记资产)求出作品集合,要求每个作品都被 grant 覆盖;有无法归属作品的源时只接受一次性审批。补一条"两个作品只授权一个 → 403"的测试 |
| S-2 | 注册表的鉴权标注又和实际行为不一致了(第一轮 S10 的回归)。GET /files/download 和 POST /files/trash/purge 在处理器里只放行管理员会话,但注册表只标了 scope:"admin",没有标 auth:"session",所以 api-registry 的双向测试没有覆盖到它们。另外 isAdmin 放行 dev_admin,而 auth:"session" 的端点会拒绝 DEV key,两处口径不同 |
packages/contracts/src/api.ts:1150,1182;file-manager/common.ts isAdmin |
两个端点标 auth:"session",在 api-registry.test 的会话端点列表里加上;确定 DEV key 的口径,两处统一 |
| S-3 | 第一轮 S9 仍未做:useTotpStatus / useTotpAction 写好了,但界面里没有任何地方调用,账户弹层只有改密和登出 |
apps/web/app/components/console/account-menu.tsx |
账户弹层加"二次验证"开关,接 GET/POST /auth/totp;04 表里 S9 的状态同步更新 |
| S-4 | 脱敏键表还是三份(见 §5.4)。rss 和 browser 两处都漏了 c_secure_pass、pass_key/auth_key 这类带分隔符的写法、refresh_token、password、uid_key;browser 另外漏了 auth。adapters README 写的是"键表与规则取 contracts 的 redact.ts",与实际不符 |
packages/adapters/src/rss/redact.ts:9、browser/redact.ts:13 |
rss/browser 的参数判定改为调用 @umt/contracts 的 isSensitiveKey 和 SENSITIVE_QUERY_PARAMS,各自只保留特有规则(路径式 passkey、cookie 名、a11y 密码框);加一个三处一致性测试 |
| S-5 | 回收站"还原到…"不检查目标根是否可写,可以还原进 register_only 库根。父目录不存在时,会先按 resolve() 递归 mkdir,再做 realpath 复核;如果中间一段目录是指向根外的符号链接,根外可能已经被建出目录 |
file-manager/trash.ts:478-506 |
target 走 isWritableTarget;原位还原时先逐级 realpath 已存在的祖先目录,再 mkdir |
| S-6 | /files/identify 走 capabilities.invoke("catalog.lookup"),绕开了 invoke-limits 的 TMDB 并发限制。每项超时 15 秒;目录最多展开 200 个视频,在 HTTP 请求里串行 |
file-manager/identify.ts:150-160 |
复用 createInvokeLimiter 的 TMDB 信号量;单次请求内去重后的远端查询设上限(如 20),超出的项给 warning |
| S-7 | 文件管理的重活仍在 API 进程的 HTTP 请求内做:跨卷进回收站最多复制 2 GiB;还原时跨卷 copyTree 没有大小上限;永久删除时递归 rm;所有 fs 调用都不经 fsproxy,挂死的挂载会占满 API 进程的 libuv 线程池,而 probeRoot 的 2 秒超时只挡住等待,挡不住卡住的系统调用 |
file-manager/trash.ts:390-399,513-516 |
还原沿用 trashCrossMaxBytes,或者跨卷的回收站/还原改为入队任务、返回 taskId;根探测与 readdir 走 FsProxy 的快操作。暂时做不到就写进已知限制(已列入 §8) |
| S-8 | 完成判定有两条路径。organize.ts 仍保留本地的 slotComplete,并直接把 once 订阅置为 completed/slot_satisfied;services/subscriptions.ts 按总集数账本判定并写 subscription_history。两者结论不同时,once 订阅可能不写历史就完成 |
acquisition-organize/services/organize.ts:1458-1492,1548 |
删掉本地判定,统一调用 services/subscriptions.ts(05 §4 遗留里已经点名) |
| S-9 | 文档与代码不同步 | 见右列 | moviepilot-rust-transfer.md §7 遗留第 1 条("编译二进制回退 in-process")已被 3641ab9 解决,改为已做;contracts README 缺 files.ts、redact.ts、browser.ts、auth.ts;adapters README 的 fs 行缺 FsProxy 和 ./fsworker 导出;apps/server/README.md 的模块清单缺 file-manager/,角色缺 worker:agent 和 fsworker;04 表 S9 行没有标状态 |
| S-10 | 验收开关 UMT_FS_TEST_HANG_AFTER_BYTES 在生产二进制里同样生效。误设以后,所有数据类文件操作都会卡死,直到被 SIGKILL |
packages/adapters/src/fs/fsproxy.ts |
只在 NODE_ENV!=="production",或者同时设置 UMT_FS_TEST_HOOKS=1 时生效;preflight 发现已设置就 warn |
1.3 建议
POST /files/probe标的是organize.read,但会写media_asset.media_info,而且在请求里串行跑最多 20 次 ffprobe,每次 30 秒超时。建议改成organize.execute或入队。- 永久删除只在单项时要求
confirmName,多项时没有服务端确认(trash.ts:217)。 - 测试可以补几处:trash 用例在审批消费后补一条"重放 → 403";download 用例补"admin scope 的 API Key → 403";E6 自动恢复现在只核实到 recheck 后回到
downloading,可以再延伸到入库;engine-retry 用的是 mock 的 enqueue,可以加一条真实引擎的用例。 - e2e 文件编号重复(
e6-auto-recover和e6-w4a-endpoints),建议改成 E10。迁移 0017 空缺维持现状即可(已知、无害)。 - 04 §1.3 的 7 条建议都还没动(下载状态机
ALLOWED=ALL、scanIncremental出错仍返回 complete、流程版本升级没有测试、AT18 e2e、AT42 未纳管、vertical 用例依赖执行顺序、web 单测少),已并入 §8。 - 清掉 biome 的 1 个 warning 和 1 个 info。
2. 第一轮问题复核
| # | 第一轮问题 | 结论 | 证据 |
|---|---|---|---|
| R1 | 后台循环绕过引擎;HTTP 内同步重活 | 已验证 | fe9693d:index.ts planTick 只调 enqueueTracked/enqueueSweep,W5-C 新增的 sweepDueSlots/sweepSubscriptionSearch/sweepMetadataRefresh 也只 enqueue;routes/downloads.ts:100-125 返回 202 {taskId,runId};__tests__/background-loop.test.ts |
| R2 | 版本 0.0.0 | 已验证(tag 待打) | 根与 9 个成员 deno.json、apps/web/package.json 都是 0.1.0;实机 /health 返回 0.1.0;git tag 为空,由协调者打 |
| R3 | 授权内恢复 | 已验证 | 86af723;e6-auto-recover 3 条:无授权降级、已发布策略授权下 task.inspect→download.recheck→系统核实→事项 resolved、换源被拒、重复触发只有一个 run、预算超限。3641ab9 把 autoInvestigateMode 放进了设置页 |
| R4 | sourcePaths 无约束 |
已验证 | ceff9cf;organize-guard.test.ts 6 条:根外、..、符号链接 → 400,dryRun 同样受限;API Key 做 Move 没有授权 → 403;grant 绑定的策略非 published 不算;审批用后 consumed;H&R → 409 |
| S1 | 自己批准审批 | 已验证 | api.ts 中 approvals 两个端点为 auth:"session";approvals.test「admin scope 的 Key 同样 403」;实机 DEV key → 403 |
| S2 | logger 脱敏窄 | 部分:logger 与模型上下文已验证,rss/browser 仍缺 | contracts/redact.ts 由 logger 与 agent 共用,logger.test 通过;rss/browser 见 S-4 |
| S3 | TMDB 错误带 api_key | 已验证 | f48e1aa;tmdb/client.ts 的 message 经过 redactUrl |
| S4 | 审批分支无测试 | 已验证 | test/agent/approvals.test.ts 5 条,断言了"改计划→被拒→重批→只消费一次→不可重放" |
| S5 | 剧集纵切 | 已验证 | e7-tv-series;W5-C 又加了 e9-continuous-series |
| S6 | AT24 | 已验证 | e8-tag-removal 4 条,用 qB 调用日志断言:torrents/delete 计数不变、资产与位置不变 |
| S7 | 生产构建能开 Agent mock | 已验证 | agent-mock.test.ts;文件页 mock 也按同一套规则只在 DEV 生效(resolveFilesMock,model.test.ts:237) |
| S8 | 引擎面板重跑 | 已验证 | engine-retry.test.ts 2 条(幂等键序号、审计、409/404/403) |
| S9 | 控制台 TOTP 入口 | 仍缺 | 见 S-3 |
| S10 | 注册表鉴权标注 | 已验证,但新端点回归 | api-registry.test 4 条通过;新增的 download/purge 没有标注,见 S-2 |
| S11 | 文档不一致 | 已验证,但有新的不同步 | 见 S-9 |
3. §6 追加范围
3.1 订阅增强(对照 05)
| 05 §3 条目 | 状态 | 证据 | 缺口 |
|---|---|---|---|
| 1 总集数来源与账本 | 完成 | 迁移 0025(content_unit.episode_total*);sources-identity/service/metadata-totals.ts;subscription-search.test 断言季总集数写入、pending→active |
只建季单元,不预建集单元(设计如此) |
2 resolveEpisodeTotal |
完成 | domain/slot/episode-total.ts;domain/test/subscription.test.ts 覆盖账本 §13 的样本 1、9 |
— |
| 3 完成判定 | 完成(有双轨) | services/subscriptions.ts slotComplete;__tests__/subscription.test.ts:233「总集数未知不结束、记疑似缺集;就绪后补齐 → once 完成并写历史」 |
S-8 |
4 computeUnitGaps |
完成 | domain/slot/gaps.ts + 单测;services/acquisition.ts |
— |
| 5 Slot×单元预留 | 完成 | 0025 去掉 acquisition_plan_slot_active_key;domain/slot/inflight.ts;subscription.test.ts:112(20 个断言);E9「Slot 忙时 E02/E03 同时在途」 |
电影 Slot 仍然只允许一个在途计划(设计如此) |
| 6/7 忙后重评、主动搜索 | 完成 | slot_deferral、sweepDueSlots;searchSubscription 与 POST /subscriptions/{id}/search(admin,入队) |
— |
| 8 元数据 6h 刷新 | 部分 | sweepMetadataRefresh |
完成前没有强制刷新 |
| P1 9–14 | 完成 | include/exclude 正则白名单、站点范围、每集体积、集范围、季包选文件、slot_unit 分集洗版 |
— |
| P1 15–17、P2 18/21–23/25/26 | 未做或部分 | 05 §4 遗留 | 见 §8 |
REQ 增强点:"总集数未知不结束"和"临时集单元"从 ✗/◐ 变成 ✅;"Slot 多目标"(require_distinct_asset 没有接入)、"偏好标签参与评估"、"Agent 选片理由与独立预算"、"回退等待期"、"队列按优先级出队"仍是 ✗/◐。
3.2 文件管理(对照 06)
端点:规格里的 16 个全部实现,另外新增 POST /files/copy 和 GET /files/records。与规格的偏离:
| 端点 | 与规格的偏离 | 性质 |
|---|---|---|
| download | 规格写的是 organize.read,实现为仅管理员会话 |
更严,可以接受 |
| 永久删除开关 | 规格写的是设置项 files.allowPermanentDelete,实现为环境变量 FILES_ALLOW_PERMANENT_DELETE |
需要重启才能生效,可以接受 |
| scrape | writeNfo/images 返回 not_implemented |
规格允许拆到 W5 之后,已列入 §8 |
| 路径守卫 | 没有下沉到 adapters/fs/guard.ts,模块自带 resolveIn/recheck |
runbook 已写明 |
§4.1 不变量:
| 不变量 | 状态 | 代码 | 测试 |
|---|---|---|---|
1 realpath 在根内;..、符号链接逃逸、根本身写操作拒绝;执行前再 realpath(TOCTOU) |
满足 | common.ts resolveIn/recheck;所有写端点都先过 resolveIn(mkdir/rename/move/copy/organize/delete/restore/probe/identify/download) |
browse.test:90(..、URL 编码、根外、符号链接指向文件和目录、未登记根);write.test:280 的 TOCTOU 用例:预览后源被换成链接 → path_escape,根外文件不动 |
| 2 浏览器不直接写;审计与幂等键 | 满足 | routes.ts idem() + ledger.ts;每个写操作都调 audit() |
write.test:196「幂等重放不重复执行」 |
| 3 路径变更只来自显式操作,同一事务更新位置与绑定 | 满足 | mutate.ts、records.ts relocate |
write.test:74/144 |
| 4 删除不碰 qB;做种中不能改名、删除、Move | 满足 | trash.ts checkItem、transfer.ts download_active |
trash.test:38、write.test:174 |
| 5 失败≠不存在 | 满足 | onlineRoot 返回 503;fsErrorToFiles |
browse.test:120、trash.test:202、w5a2.test:392 |
| 6 只执行 dryRun 产出的 planId,源事实不变 | 满足(move/copy/organize/delete) | loadPlan + 指纹 |
write.test:196、identify-organize.test:72 |
| 7 不覆盖 | 满足 | moveSafe;copy 遇到目标已存在记失败 |
write.test:74、w5a2.test:182 |
前端:路由 /console/files 和 /console/files/trash,组件 DirTree、PathBreadcrumb、ContextMenu、DetailSheet,4 步向导(未决项挡住下一步)。虚拟滚动是自己写的(没有引入 @tanstack/react-virtual,所以不需要登记许可)。键盘支持 role="grid"、F2、Del、Backspace、Shift 区间选、Ctrl+A;拖拽后先打开 dryRun 对话框;SSE files.changed 触发刷新。测试:components/files/model.test.ts 16 条(选择模型、徽标、禁用原因、集号偏移、未决判定、mock 开关),冒烟覆盖两个路由。
缺口:规格 §4.2 要求的前端 e2e("下载根散文件 → 识别 → 向导 Copy → 整理记录 → 状态变为已登记")和移动端快照没有做。
3.3 Agent 会话页(对照 agent-ui-reference §3 映射表)
| 事件/对象 | 落地 | 位置 |
|---|---|---|
run_started / text_delta |
Message + StreamingResponse(增量拼接,加流式光标) |
agent-conversation.tsx |
thinking_delta |
自写可展开的思考块,保留完整文本 | :239 |
tool_call_start/end |
ToolResult,带类别/scope/风险/幂等徽章 |
agentui/tool-result.tsx |
tool_call_rejected 与授权失败(unauthorized/forbidden) |
ToolApproval 的 rejected 醒目态(role=alert) |
:130-139 |
compaction |
MessageMarker |
:451 |
run_completed/failed、超预算 |
MessageBubble 的 warning/danger 变体 + 超限原因 |
— |
| Approval | ApprovalCard,状态集对齐 pending/approved/rejected/revoked/expired/consumed;非管理员会话时按钮禁用 |
:626-670 |
AgentGoalReport 声明与核实分开 |
FinalReport,双栏 + FileDiff + CitationList |
:684-790 |
| Budget | BudgetBar(自写,有超限态) |
:563 |
| RunMode | 头部显示"只要方案 / 授权内执行";plan_only 用 TodoList |
:338,491-514 |
| 滚动跟随 | MessageScroller(阈值状态机 + "回到底部"按钮) |
agentui/message-scroller.tsx |
结论:映射表每一行都落地了,许可文件 LICENSE-agentui.md 也在。缺口:组件层没有单测,只有 agent-helpers.test.ts;幂等键取自审计,最多翻 3 页(文档已写明)。
3.4 转移语义(对照 moviepilot-rust-transfer §7)
| 项 | 状态 | 证据 |
|---|---|---|
| 挂死隔离在生产二进制里生效 | 已验证 | 3641ab9:main.ts 在加载其余模块前分派 --role fsworker;deno.json compile --allow-run=…,/app/umt-server;deploy.md W5-E 容器演练:日志里 workerKind:"self",/proc 里能看到 umt-server --role fsworker(uid 10001),停滞 3 秒 → SIGTERM → SIGKILL,共 3 次,之后账本记 TIMEOUT 并建待处理,只残留 .uhd-partial、没有最终名的半成品;fsproxy.test 的 10 条中有 3 条挂死用例(屏蔽 SIGTERM、FIFO、快操作) |
| partial 清理 | 已验证 | copy.ts sweepPartialsOlderThanMs,executeRecord 对每个目标目录也清一次;organize-fs.test:48「写前清走超过 24h 的 partial」 |
| replace | 已验证 | copy.ts replaceCommit(硬链接留底 → rename 原子替换);3641ab9 起旧文件进 .umt-trash/<day>/<uuid>/ 并登记 trash_item;organize-fs.test:48 |
| EEXIST 误报 | 已验证 | 对账阶段比对失败按 IO/TIMEOUT 处理,不再报 EEXIST;organize-fs.test:118:FIFO 让对账挂死 → 2 次 TIMEOUT 重试 → 第 3 次 failed + fs_retry_exhausted |
| TIMEOUT 错误码 | 已验证 | FsErrorCode.TIMEOUT,ETIMEDOUT 也映射为它;fsErrorClass 归为 retry;fsproxy.test:184 |
| 缺口 | — | planImport 的 stat/哈希、reconcile.ts、文件管理都不经 fsproxy;没有根级熔断(把根标为 suspect);基准 B1–B3 没跑;失效软链、hardlink、蓝光原盘、organize 的空目录上溯清理 |
3.5 差分补迁(对照 mp-rust-logic-parity §10)
| 链路 | 补迁前 | 补迁后 | missing / our_bug |
|---|---|---|---|
| 标题识别(216 条 × 18 字段) | 92.9%;剔除登记项后 97.4% | 94.4%;剔除登记项后 100% | 0 / 0 |
| 显式 ID | 8/11 | 10/11 | 0 / 0 |
| 过滤规则(644 个 case) | 95.7% | 97.8% | 7(每集体积,属 Slot 能力)/ 0 |
| 站点索引解析 | 98.1% | 99.5% | 0 / 0 |
| 繁简转换 | 84.1% | 92.8%;剔除登记项后 100% | 0 / 0 |
| RSS | 95.5% | 未变 | 0 / 0 |
差分测试在 adapters/test/differential/{indexer,rss}.test.ts 和 domain/test/differential/{meta,zhconv}.test.ts,本轮单包测试全部通过。遗留:字幕搜索还没接到站点适配器;RSS 不带时区的时间按站点时区解释;HTML 命名实体。
4. 解耦审计(03)复核
| 项 | 状态 | 复核 |
|---|---|---|
| 后台循环只编排 | 满足 | planTick 只有 enqueue;qB 同步有独立定时器;文件管理的 purgeTrash 也是入队(file-manager/index.ts enqueuePurge) |
| HTTP 内无重活 | 部分 | cleanup/reconcile 返回 202;/subscriptions/{id}/search 入队;files 的 move/copy/organize 执行入队 verifyAndImport,请求内只等待,最多 20 秒。遗留:GET /inventory/media-servers 串行查 Emby;files 的回收站/还原跨卷复制、永久删除、identify、probe 仍在请求内(S-6、S-7) |
| worker 分组 | 满足 | --role worker(io 组)、--role worker:agent、--role all;认领时按任务名过滤(worker-groups.test);purgeTrash 注册在 io 组 |
| 超时与限并发 | 部分 | 4 个外部 IO 能力在 HTTP 路径上有 15 秒超时,站点并发 2、TMDB 并发 4。/files/identify 绕开了限流器(S-6);超时后底层调用仍会跑完 |
| 探针 | 满足 | 事件循环延迟探针,1 秒采样,偏差超过 5 秒告警,随心跳上报;fsproxy 的 stats() 写进 organize.transfer 日志 |
5. 安全复查
5.1 新端点的 scope 与会话要求
| 端点 | 注册表 scope | 处理器内的附加要求 |
|---|---|---|
GET /files/roots、/files、/files/stat、/files/trash、/files/records;POST /files/identify、/files/probe |
organize.read | 无(probe 会写 media_info,见 §1.3) |
POST /files/mkdir、/files/rename、/files/copy、/files/scrape、/files/trash/restore |
organize.execute | 目标或源根必须可写,或者是 managed 库根 |
POST /files/move、/files/organize(action=move) |
organize.execute | API Key 需要 grant 或一次性审批(authorizeMove);move 只核对第一个源的作品(S-1) |
POST /files/delete |
organize.execute | 进回收站:API Key 每次都要审批(绑定 planId 与内容哈希,用后 consumed);永久删除:必须设置 FILES_ALLOW_PERMANENT_DELETE,且只接受管理员会话 |
POST /files/trash/purge |
admin | 只接受管理员会话;注册表没有标 auth:"session"(S-2) |
GET /files/download |
admin | 只接受管理员会话,文件类型白名单,上限 20 MiB;注册表没有标 auth:"session"(S-2) |
POST /subscriptions/{id}/search、/subscriptions/{id}/episode-total |
admin | 无 |
GET /subscriptions/{id}/progress、/subscription-history |
read | 无 |
POST /engine/runs/{id}/retry |
admin | 只允许已结束的 run;幂等键带序号;写审计 |
POST /agent/runs、/agent/runs/{id}/messages、/agent/runs/{id}/abort |
organize.execute | 能力调用在服务端逐次授权 |
POST /approvals、PATCH /approvals/{id} |
admin + auth:"session" |
API Key 和 DEV key 一律 403(已实测) |
注册表共 208 个端点:read 89(其中 3 个 public)、organize.read 10、organize.execute 27、admin 79(其中 5 个 session)。
5.2 其他项
- 路径守卫覆盖:13 个 files 写端点和读端点都先过
resolveIn,执行前再调recheck。只有 restore 的目标可写性,以及原位还原时的递归 mkdir 这两处有缺口(S-5)。 - 回收站与永久删除:
purgeTrash删除前先复核路径在.umt-trash或DATA_DIR/trash下,并且先删行、再删文件。永久删除有双闸(环境变量开关 + 管理员会话)。restore 不需要审批(只把文件放回,风险低,可以接受)。 - 下载上限:打开文件后按文件句柄 stat,超过
FILES_DOWNLOAD_MAX_BYTES返回 413;SVG 一律作为附件下载;响应带nosniff和CSP sandbox。w5a2.test:350覆盖了上述行为。 - 审批只认管理员会话:创建和决定两个端点都是
auth:"session"。consumeApproval与authorizeMove都用"status=approved才更新"来保证只消费一次。
5.3 脱敏键表四处对照
| 参数 | contracts(logger + redactForModel) | rss/redact.ts | browser/redact.ts |
|---|---|---|---|
| passkey / authkey / torrent_pass / rsskey / downhash / apikey / api_key / access_token / token / secret / sign / credential / key | ✓ | ✓ | ✓ |
pass_key / auth_key 等带分隔符的写法 |
✓ | ✗ | ✗ |
| c_secure_pass(作为 query 参数) | ✓ | ✗ | ✗(只遮 cookie 里的 c_secure_*) |
| refresh_token / password / uid_key | ✓ | ✗ | ✗ |
| auth | ✓ | ✓ | ✗ |
| signature | ✓ | ✗ | ✓ |
| 请求头、Bearer、URL userinfo | ✓ | 只有 userinfo | cookie 头、userinfo |
结论:logger 与模型上下文共用同一份实现;rss 和 browser 仍各自维护(S-4)。
6. 工程规范
| 项 | 结论 |
|---|---|
| 提交信息 | 82 个提交全部合规,0 个违规 |
| README、runbook 与 deno 任务 | 文档里出现的 19 个 deno task 全部存在(根目录或成员包);没有残留 pnpm/npm 用法 |
| 包 README 与导出 | adapters 新增的 ./fsworker 导出、contracts 新增的 4 个文件、server 的 file-manager 模块没有写进 README(S-9) |
| CHANGELOG | 没有覆盖 W4-E4 到 W5(B1) |
| 迁移与生成类型 | 0024 trash_item、0025 订阅单元、0026 整理动作,编号连续(0017 空缺是历史遗留);db/src/types.ts 已含 trash_item、slot_unit、slot_deferral、subscription_history、episode_total*,以及 action/mode 的 trash/delete/restore |
| api.ts 双向登记测试 | api-registry.test 4 条通过("OpenAPI 路由都已登记"、"登记的都已实现"、public 端点集合、session 端点集合);漏标见 S-2 |
7. 抽查 12 个新增测试
| 用例 | 结论 |
|---|---|
write.test「TOCTOU:预览后源被换成指向根外的链接」 |
有效:断言 400 path_escape、根外文件内容不变、目标不存在 |
trash.test「永久删除默认关闭;API Key 删除需一次性审批」 |
有效:断言 403 permanent_disabled、无审批时 approval_required、审批后执行成功且文件已删除。缺一条"重放 → 403" |
w5a2.test「下载:仅管理员会话……」 |
有效:断言 MIME、disposition、nosniff、READ/EXEC key 403、视频 403、超限 413、path_escape、目录 400、审计 2 条。缺"admin scope 的 key" |
browse.test「根离线 → 503,不返回空列表」 |
有效(按用例名与 root_unavailable 断言判断) |
organize-fs.test「对账挂死 → TIMEOUT(不是 EEXIST)→ 有限重试 → 待处理」 |
有效:逐次断言错误信息、文件状态保持 running、最终 failed 且 code=TIMEOUT、decision 里 attempts=3 |
fsproxy.test「复制中途挂死 → SIGKILL → TIMEOUT」 |
有效:断言错误类别为 retry、耗时下限、进度大于 0、timeouts/killed=1、最终名不存在、partial 可被清理 |
subscription.test「Slot×单元多在途……移除释放」 |
有效:20 个断言(在途单元、延后、释放、重评) |
e9「Slot 忙时 E02/E03 同时在途,另一版本延后」 |
有效(E9 三步串联,最后断言入库和历史) |
e6-auto-recover「授权内 recheck → 系统核实」 |
有效:断言 run 字段、调用序列 [task.inspect, download.recheck]、download__submit 被拒、grant_id、qB recheck 只 1 次、decision resolved、去重。只核实到 downloading |
e8「qB 任务消失 → removed;资产不变,不重下」 |
有效:断言位置前后相同、Slot 仍 satisfied、计划只有 1 个、torrents/add 计数 |
approvals.test「改计划 → 被拒 → 重批 → 只消费一次 → 不可重放」 |
有效 |
engine-retry.test「按原任务名与输入重新入队,序号递增,写审计」 |
部分有效:enqueue 是 mock,没有验证真实引擎里的新 run |
8. v0.1 已知限制(供 CHANGELOG 引用)
范围外(00 §3.2 / 02 后置)
- 国产综艺分段、
require_distinct_asset组合校验、浏览器/camofox 与原站视图、HHClub/CHD、旧库分范围接管、Agent 主观选片与规则学习、多用户。 - Passkey、主密钥轮换、子代理、Transmission、TTG 自定义适配、馒头 adult 模式。
订阅(05 遗留)
3. 完成判定前不会强制刷新元数据;titleQuery 订阅不会自动建档回填;季范围语义(current_season 在没有季单元时固定)未做;偏好标签不参与评估。
4. 同作品同季重复订阅没有查重;prefer_pack、回退等待期、Agent 选片理由与独立预算、队列按优先级出队、实体级识别词都未做;没有"清账重置"(只能把状态改回 active 重新订阅)。
5. 完成判定暂时有两条路径(S-8)。
文件管理
6. NFO/图片投影(scrape 的 writeNfo/images)、硬链接、蓝光原盘整目录操作未做;路径守卫没有下沉到 adapters。
7. 从回收站还原不会恢复资产状态,需要重新整理登记;永久删除开关是环境变量,不是设置项。
8. 跨卷进回收站(≤2 GiB)、跨卷还原、永久删除、identify(每项超时 15 秒,不经 TMDB 限流器)、probe 都在 HTTP 请求内执行,也都不经 fsproxy(S-6、S-7)。
9. 前端没有向导 e2e 和移动端快照,页面行为靠 model 单测和冒烟覆盖。
转移与文件系统
10. planImport 规划期的 stat/哈希、资产对账和文件管理不经 fsproxy;没有根级熔断;失效软链一律按冲突处理;hardlink、organize 源侧空目录上溯清理、蓝光原盘未做。
11. 二进制必须位于 /app/umt-server,否则 fsproxy 回退为进程内看门狗(能按时报 TIMEOUT,但卡住的线程无法回收)。基准 B1(50 GB)、B2(并发)、B3(真实 FUSE 冻结)没有跑。
Agent 与安全
12. 控制台没有 TOTP 开关入口(S-3)。
13. rss 和 browser 的脱敏键表没有与 contracts 统一(S-4)。
14. GET /inventory/media-servers 在请求内串行查 Emby;能力调用超时后,底层调用仍会跑完。
15. Agent 页的幂等键来自审计(最多 3 页),审批卡片的 planId 靠解析工具摘要得到,较早的运行可能取不到。
16. 自动恢复只核实到下载恢复,入库由后续流程负责;恢复白名单只有 task.retry、download.recheck、identity.resolve。
识别与站点 17. 字幕搜索只到解析层;RSS 不带时区的时间与 HTML 命名实体未处理;过滤器的 tmdb 放行、SPECSUB 等属于 Slot 能力扩展。
质量与运维
18. 恢复流程只有人工演练;流程版本升级没有产品侧测试;下载状态机 ALLOWED 未收紧;scanIncremental 出错时仍返回 complete;AT18 恶意标题 e2e、AT42 未纳管占用没有测试;vertical.test 的用例依赖执行顺序;web 单测少。
19. deno task ci 约 135 秒以上,server/tasks 测试是主要耗时。
9. 给 M2 的前 5 条建议
- 把 IO 统一收到 fsproxy 并加根级熔断:
planImport、reconcile、文件管理的 stat/readdir/复制全部经 FsProxy;根超时标为 suspect,暂停向它派发,建待处理;回收站和还原的跨卷复制改为入队。跑 B1–B3 基准,并把结果写进性能基线。 - 凭证与授权收口:只保留一份脱敏实现(rss/browser 改用 contracts)并加全仓凭证泄漏的故障注入测试;注册表的
auth标注由处理器的真实行为自动核对(对每个端点用 admin scope 的 key 发一次请求,断言 403 与标注一致);多作品 Move 按作品集合授权;补 TOTP 入口。 - 订阅闭环第二阶段:消除双轨完成判定;完成前强制刷新;偏好标签编译进 SlotSpec;
require_distinct_asset接入评估与入库(这是综艺和双版本的前置);队列按优先级出队。 - 文件管理补全:NFO 投影(带 umt 标记与字段锁)、hardlink(
link自带 no-clobber)、蓝光原盘目录、空目录上溯清理;前端补向导 e2e、移动端快照和键盘可访问性测试。 - 把 CI 当作发布门禁:e2e 按包并行、给 server 测试分片,把 ci 控制在 90 秒内;夜间跑"备份 → 清库 → 恢复 → 只读对账 → 映射比对"和流程版本升级回归;CHANGELOG 与已知限制由每个波次的汇报强制更新,避免再出现 B1。
10. W6-B 处理状态(2026-09-28,打 tag 前)
| # | 状态 | 提交 | 说明 |
|---|---|---|---|
| B1 | 已修 | b48d859、本文档提交 | CHANGELOG [0.1.0] 按功能补齐文件管理、订阅增强、转移语义、Agent 会话页、识别补迁、稳定性(W4-D~W6-A)与 W6-B 安全修复;"已知限制"换成本文 §8 的 19 条,过时的集单元条目已删 |
| S-1 | 已修 | 17cf3cb |
authorizeMoveEntities:每个源的作品集合(声明作品 ∪ 源下已登记资产的作品)逐一核对 grant,一次性审批覆盖整请求;无法归属作品的源只认审批;拒绝时列出未授权项并写审计。/files/move 与 /files/organize(move,先整体预检、不消费审批)都接入;copy 不删源、delete 每次审批,无此问题。测试:write.test「两个作品只授权一个 → 403」、identify-organize.test「源是别的作品的已登记资产 → 403」 |
| S-2 | 已修 | 17cf3cb |
download / trash purge 标 auth:"session",files 路由按注册表挂 requireAdminSession;口径:DEV key 等同 admin scope Key,仅会话端点与永久删除一律拒绝,Move/删除审批的免授权仍放行 DEV key(与 authorizeMove 一致)。api-registry.test 会话列表加这两个,新增反向断言(未标 session 的端点对 admin scope Bearer 不得 403,会话中间件在入参校验之前,暗挂必暴露);download/purge 用例补 admin scope Key 与 DEV key 403 |
| S-3 | 已修 | ba1361a |
账户弹层"二次验证":生成密钥(base32 分组 + otpauth 链接)→ 6 位码确认开启;已开启时凭当前密码 + 验证码关闭。没有二维码(不引新依赖);没有组件测试,只有类型检查 |
| S-4 | 已修 | 47bd04a |
contracts 新增 isSensitiveQueryParam;rss 删掉本地 SENSITIVE_QUERY_KEYS,browser 的 CREDENTIAL_PARAM 由 SENSITIVE_QUERY_PARAMS 生成,各自只保留特有规则;adapters/src/redact-consistency.test.ts 24 种写法三处一致 |
| S-5 | 已修 | 17cf3cb |
还原目标走 isWritableTarget;原位还原 mkdirWithin 逐级 lstat + realpath,不跟随符号链接;跨卷还原受 trashCrossMaxBytes,进出回收站的跨卷复制经 fsproxy。测试:下载根 / register_only 目标 403、父目录换成根外链接 → path_escape 且根外无新目录、逐级重建、跨卷超限 too_large |
| S-6 | 已修 | b4560be |
sharedInvokeLimiter() 进程共享;identify 的远端查询经 TMDB 信号量 + 15 s 超时,单次请求去重后最多 20 个,超出项给 warning |
| S-7 | 部分,余下延期至 v0.1.1 | 17cf3cb |
做了:跨卷还原加上限、回收站进出的跨卷复制经 fsproxy。未做:回收站 / 还原 / 永久删除 / identify / probe 改入队,根探测与 readdir 经 FsProxy 快操作。已写入 CHANGELOG 已知限制第 8 条 |
| S-8 | 已修(W6-A) | 887843f |
已核对:organize.ts 不再有本地 slotComplete 与订阅完成写入,手动整理指定 Slot 改调 services/subscriptions.ts slotComplete;完成与历史只由 applySlotEvent(verify_passed) 负责 |
| S-9 | 已修 | 2e3c209、cb3fb9b | rust-transfer §7 遗留(编译二进制 fsworker、trash_item)标已做;contracts / adapters / server README 补齐;04 表 S9 标已修;files runbook 授权矩阵同步 |
| S-10 | 已修 | 2ddaf7b |
开关需同时 UMT_FS_TEST_HOOKS=1(fsTestHookFromEnv 单测);preflight 发现已设置就 warn;deploy.md 演练命令同步 |
§1.3 建议:
| 建议 | 状态 |
|---|---|
probe 标 organize.read 却写 media_info |
已修(d3de2c8):改为 organize.execute;仍在请求内串行,见 S-7 |
| 多项永久删除没有服务端确认 | 延期至 v0.1.1(写入 CHANGELOG 已知限制第 8 条) |
| trash 用例补"重放 → 拒绝" | 已补(d3de2c8) |
| download 用例补"admin scope 的 Key → 403" | 已补(17cf3cb) |
| E6 自动恢复延伸到入库、engine-retry 真实引擎用例 | 延期至 v0.1.1(已知限制第 18 条) |
| e2e 编号重复 | 已修(d3de2c8):e6-w4a-endpoints → e10-w4a-endpoints |
| 04 §1.3 的 7 条 | 延期(已知限制第 18 条) |
| biome 1 warning + 1 info | 已清(9887d8b) |
另:第一次全量 ci 暴露 fsproxy.test「正常复制」偶发失败(fsworker 的进度节流吞掉了复制阶段最后一跳,收到 6291456 而非 6291461),7daf544 改为阶段完成立即上报,单测连跑 3 次通过。
ci:修复后连续两次 deno task ci 全绿:357 s、341 s(server 44 文件 / 267 用例)。未打 tag。