# B 站博主动态采集辅助工具 HOLD/5 最小修复编码方案 V001 创建人员:dev.developer.project.secondary / infodev-2 文件职责:冻结 `DEV-PROJECT-INFO-BILI-DYNAMIC-COLLECTOR-MVP-20260804-001` 首次实现审核 HOLD/5 的最小重型修复合同、失败语义和验收矩阵。 管理规范/模板:../../../../common/dev-doc/编码规范.md;../../编码规范.md;../../开发审计规范.md 引用文件:../../../ai-video-downloader/draft/2026-08-04-哔哩哔哩动态采集需求.md;../B站博主动态采集辅助工具.md;../../开发审计报告.md;../../../dev/project-dev/bili_dynamic_collector.py;../../../dev/project-dev/test/test_bili_dynamic_collector.py 记录方式:重型编码方案;方案独立审核 PASS 后才允许修改实现与测试。 ## 1. 事项与修复边界 - 事项:`DEV-PROJECT-INFO-BILI-DYNAMIC-COLLECTOR-MVP-20260804-001`。 - 新授权:请求方在首次终态交付后明确“继续推进”,只允许关闭 `HOLD/5`,不增加新功能。 - 前序审核:`DEV-AUDIT-PROJECT-INFO-BILI-DYNAMIC-COLLECTOR-MVP-IMPLEMENTATION-20260804-001=HOLD/5`。 - owner:`dev.developer.project.secondary / infodev-2`。 - reviewer:`dev.reviewer.project / inforev`。 - 代码/测试:继续只修改现有 `bili_dynamic_collector.py`、示例配置和 `test_bili_dynamic_collector.py`;不拆包、不增加运行模块。 - 文档:只同步现有用法、目录入口、根级账本、worklog 和开发 tmp 证据。 - 外部动作:网络、浏览器、扩展、下载、真实媒体、`F:\video`、正式 `ana-data` 均为 0。 本修复从现在起按重型事项处理,原因是现有工具包含三个持久阶段、跨调用 manifest 状态、互斥、重跑和复制/提交/删除事务。当前代码继续视为 `TERMINAL_HOLD_DEVELOPMENT_SNAPSHOT_ONLY`;本方案 PASS 前不得修改代码或测试。 ## 2. 不变合同 以下已通过合同不重开: 1. `check / move-completed / handoff` 三个公开 CLI 名称、全局 `--config` 和可选 `--now`。 2. offset-aware 时间、最近时间窗、动态/opus/BV/URL 基础去重、Windows 安全 stem 和重复输入不生成空待办。 3. 不联网、不控制浏览器/扩展、不下载内容、不读取浏览器 profile、不自动发送 Codex 消息。 4. 目标存在时不覆盖;happy path 继续为 partial → flush/fsync → SHA-256 → 同目录 hard-link no-overwrite → manifest → 删除源。 5. handoff 生成前继续复核视频位于 `video_dir` 且实物 SHA-256 与 manifest 一致。 6. 只使用脱敏 JSON、TemporaryDirectory/dev tmp 和数十字节非媒体 fixture;不运行真实采集。 ## 3. 模块、接口与状态 仍为单一 stdlib 模块,内部职责按函数分层: | 内部层 | 职责 | 本轮变化 | |---|---|---| | JSON/manifest 输入层 | strict UTF-8、JSON、秘密字段、schema | manifest 事件补统一秘密 gate | | identity/preflight 层 | selector→entity、source→target、批次唯一性 | mapping 改为所有已提供 selector 必须命中同一实体;同源批次拒绝 | | move 状态/事务层 | 预检、copy/commit、manifest、源删除、重跑 | retained-source 重跑保持 exit 4;预检错误零副作用 | | native handoff 层 | 路由、证据、完整 envelope、内容寻址输出 | 配置化精确 source/target 身份并输出 canonical envelope | 状态只修一个既有分支: ```text VIDEO_MOVED_SOURCE_RETAINED -- 相同 mapping 重跑 --> VIDEO_MOVED_SOURCE_RETAINED / exit 4 ``` 不新增“自动清理源”或“自动关闭告警”功能。源副本即使被人工删除,也不由本工具猜测为已解决;未来如需显式确认,另立需求,本轮保持告警粘性。 ## 4. F2:mapping 全选择器一致与批次预检零副作用 ### 4.1 selector 合同 mapping 每项允许提供 `dynamic_id`、`opus_id`、`bvid`、`source_url` 中的一项或多项。与 `check` 的“新实体发现”不同,move mapping 是既有实体的精确引用: 1. 每一个已提供 selector 都必须在 manifest token map 中存在。 2. 每一个 selector 都必须只解析到一个实体。 3. 所有 selector 的唯一实体必须完全相同。 4. 任一未知、冲突或多实体 selector 立即 `SAFETY_STOP`,不忽略错误 selector。 5. mapping 的 `source_url` 必须走与动态导出相同的 HTTPS、无凭据、registered-host 规范化。 现有宽松 `resolve_entity()` 保留给 `check`;新增/改造的 exact resolver 只供 move mapping 调用,避免改变已通过的新增实体去重语义。 ### 4.2 source identity 与批次合同 每项预检解析源文件后形成 source identity:优先使用 `(st_dev, st_ino)`;若平台 inode 为 0,则使用 `normcase(resolve(strict=True))`。在任何复制、目标创建或 manifest 追加前,对整个 mapping 批次检查: 1. 一个 source identity 只能出现一次;无论映射到相同或不同实体,重复源都拒绝。 2. 一个实体在同一批次只能出现一次。 3. 目标路径继续保持大小写不敏感唯一。 4. 任一 selector、源、实体或目标预检失败时,正式目标文件新增数为 0,manifest bytes 不变,所有源文件不变。 因此删除当前“预检异常也追加 MOVE_FAILED”的行为。`MOVE_FAILED` 只记录批次已完整通过预检后,某项在 copy/hash/commit/manifest 的运行期失败;批次运行期仍按顺序记录逐项终态,不承诺跨多个大文件的全批次回滚。 ## 5. F3:retained-source 告警重跑保真 `preflight_move()` 对最新状态的处理冻结为: | 最新状态 | 本次结果 | exit | |---|---|---:| | `VIDEO_MOVED` / `COMPLETE` | `ALREADY_MOVED` | 0 | | `VIDEO_MOVED_SOURCE_RETAINED` | `VIDEO_MOVED_SOURCE_RETAINED`,保留原 local_file/hash/failure_reason | 4 | | `TODO_QUEUED` / `MOVE_FAILED` | 执行正常预检与移动 | 0/3/4 | | 其他 | 安全停止 | 3 | 汇总只要包含一个 retained-source 结果,状态必须是 `COMPLETE_WITH_RETAINED_SOURCE`、exit 4。相同 mapping 重跑不得写新目标、不得删除/改写现有目标、不得把状态降成 `ALREADY_MOVED` 或 exit 0,也不追加重复 manifest 事件。 ## 6. F4:manifest 统一秘密字段门禁 `load_manifest()` 每行完成 JSON parse 后、读取 schema/identity/状态前,必须对完整事件递归调用现有 `reject_secret_keys(event, "$manifest[line]")`。任何层级字段名命中 password/passwd/cookie/token/secret/authorization/captcha/session 或中文对应词,三个命令都通过共同入口得到 `SAFETY_STOP/E_SECRET_FIELD`。 门禁不打印字段值,只返回字段路径。manifest bytes 不改写,state/queue/target/handoff 均无新增。 ## 7. F5:规范且稳定的 Codex 原生 handoff envelope ### 7.1 配置化路由 示例配置新增必需对象 `native_handoff`: ```json { "project_id": "project-info", "source_ai_id": "video-downloader", "source_thread_id": "019fcc5d-798f-7ea1-8325-3a4d1f2dc5a5", "source_role_instance_id": "case_analysis.video_downloader", "target_ai_id": "media-processor", "target_thread_id": "019fb7a4-bdfd-79f2-bd6b-e67e2b7d8efd", "target_role_instance_id": "case_analysis.media_processor", "reply_thread_id": "019fcc5d-798f-7ea1-8325-3a4d1f2dc5a5" } ``` 所有字段为非空字符串;thread ID 必须为小写十六进制 UUID 形式;source/target/reply 三个精确线程与 AI 身份来自当前项目配置快照。该对象不包含认证秘密。工具仍不发送消息。 ### 7.2 envelope 合同 输出必须是可直接作为一次精确目标消息发送的完整 ``: 1. 顶层必有 `project_id`、`message_type=video_processing_request`、`handoff_id`。 2. 必有 source/target AI、thread、role 以及 `reply_thread_id`。 3. `status=PROCESSING_REQUESTED`。 4. 必有显式 `scope:`、`evidence:`、`expected_action:` 区段。 5. evidence 每个视频包含 entity ID、BV、发布时间、JSON 转义标题/URL/本地路径和 SHA-256。 6. `handoff_id` 由 route + 排序后视频身份/路径/hash 的 canonical JSON SHA-256 派生,格式 `HANDOFF-BILI-DYNAMIC-VIDEO-PROCESSING-<24位大写HEX>`。 7. envelope 不含运行时 `generated_at`,因此 ready 集合与 route 不变时,不同 `--now` 重跑仍得到同一 handoff ID、同一 bytes 和 `REUSED`。 8. stdout 继续明确 `sent=false`;生成文件不是自动发送证据。 ## 8. 错误码与失败副作用 继续复用 exit:0 成功、2 输入合同、3 安全停止、4 retained source、130 中断。新增或细化错误码: | 错误码 | 条件 | 文件/manifest 副作用 | |---|---|---| | `E_MAPPING_SELECTOR_UNKNOWN` | 任一 selector 未命中 | 0 | | `E_MAPPING_SELECTOR_CONFLICT` | selectors 指向不同/多个实体 | 0 | | `E_SOURCE_DUPLICATE` | 批次 source identity 重复 | 0 | | `E_ENTITY_DUPLICATE` | 批次实体重复 | 0 | | `E_SECRET_FIELD` | manifest 任意层级秘密字段 | 0 | | `E_CONFIG` | native_handoff 缺字段/格式错误 | 0 | 原有 `E_TARGET_CONFLICT`、下载完成/范围/重名、hash、路径和 atomic create 错误继续有效。 ## 9. 测试与验收矩阵 ### L0 - py_compile、CLI help、strict UTF-8、diff check。 ### L1/L3 聚焦反例 1. 正确 BV + 未知 dynamic ID:`E_MAPPING_SELECTOR_UNKNOWN`;源、目标、manifest bytes 不变。 2. 两个已知 selector 指向不同实体:`E_MAPPING_SELECTOR_CONFLICT`;零副作用。 3. 同一物理源用同路径/相对与绝对 alias 映射两个实体:`E_SOURCE_DUPLICATE`;零副作用。 4. 同一实体在批次重复:`E_ENTITY_DUPLICATE`;零副作用。 5. 注入源删除失败:首次 exit 4;相同 mapping 重跑仍 exit 4,源与可信目标均保留,目标/hash/manifest bytes 不变。 6. manifest 根层或嵌套层加入 `cookie`/`token`:check、move、handoff 共同入口安全停止,字段值不出现在 stdout,state/目标不变。 7. handoff 全部必填身份字段与 `scope/evidence/expected_action` 存在;route/ready 相同但 `--now` 不同仍同一 ID、同一 bytes、`REUSED`。 8. native_handoff 缺字段或 thread 格式错误:配置输入错误且不写 state。 ### L2/L3 回归 - 原有 6 项测试全部保留并按新配置补 route。 - 新鲜 TemporaryDirectory 完成 check → safe move → canonical handoff → repeat check;不使用前序 dev/tmp 持久状态冒充回归。 - 运行 `dev/project-dev/test` project flat discover;不运行网络、浏览器、下载、真实媒体或正式路径。 ## 10. 文档、证据与恢复 1. 更新现有工具说明中的重型分类、exact mapping、retained-source 粘性、manifest secret gate 和完整 handoff route/envelope。 2. 新建独立 repair dev/tmp 证据目录,不覆盖前序 `dev/tmp/bili-dynamic-collector-mvp/` HOLD 快照。 3. worklog、总纲、计划和执行日志 append-only 记录新授权、设计 PASS、实现、测试与复审。 4. 实现复审 PASS 前不得宣称工具可用于真实移动、调度或正式交接;即使 PASS,本事项也不自动触发真实运行。 ## 11. 明确排除 不新增网络请求、B 站 API、浏览器/扩展控制、下载器、登录态、cookie/token、数据库、服务、GUI、自动调度、自动 native send、自动源清理、`F:\video` 写入或正式 `ana-data` 写入。不处理前序 HOLD/5 之外的新需求或非阻断建议。 ## 12. 实施顺序与门禁 ```text V001 限定方案审核 PASS -> 修改现有代码/配置/测试/说明 -> L0 + 聚焦反例 + 新鲜 tmp smoke + project 回归 -> 冻结证据 -> 同一 dev.reviewer.project 一次限定实现复审 -> PASS/0 或明确剩余阻断回传请求方 ``` V001 未取得独立 PASS 前,代码、配置、测试和 fixture 继续冻结;只允许本设计、目录入口、账本和审核通信变化。