edit | blame | history | raw

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

状态只修一个既有分支:

VIDEO_MOVED_SOURCE_RETAINED
  -- 相同 mapping 重跑 --> VIDEO_MOVED_SOURCE_RETAINED / exit 4

不新增“自动清理源”或“自动关闭告警”功能。源副本即使被人工删除,也不由本工具猜测为已解决;未来如需显式确认,另立需求,本轮保持告警粘性。

4. F2:mapping 全选择器一致与批次预检零副作用

4.1 selector 合同

mapping 每项允许提供 dynamic_idopus_idbvidsource_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

{
  "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 合同

输出必须是可直接作为一次精确目标消息发送的完整 <codex_native_handoff>

  1. 顶层必有 project_idmessage_type=video_processing_requesthandoff_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. 实施顺序与门禁

V001 限定方案审核 PASS
-> 修改现有代码/配置/测试/说明
-> L0 + 聚焦反例 + 新鲜 tmp smoke + project 回归
-> 冻结证据
-> 同一 dev.reviewer.project 一次限定实现复审
-> PASS/0 或明确剩余阻断回传请求方

V001 未取得独立 PASS 前,代码、配置、测试和 fixture 继续冻结;只允许本设计、目录入口、账本和审核通信变化。