创建人员:dev.developer.project.secondary / infodev-2
文件职责:冻结 DEV-PROJECT-INFO-BILI-DYNAMIC-REFRESH-COLLECTOR-20260813-001 在实现前的页面刷新、证据、状态、正式保存、重跑和验收合同。
管理规范/模板:../../../../common/dev-doc/编码规范.md;../../../../common/dev-doc/开发审计规范.md;../../../../common/dev-doc/编码方案范本.md。
引用文件:../../../开发事项总纲.md;../../../开发事项计划.md;../../../开发执行日志.md;../../../../ai-video-downloader/draft/2026-08-04-哔哩哔哩动态采集需求.md;../../../../dev/project-dev/bili_dynamic_collector.py;../../../../dev/project-dev/test/test_bili_dynamic_collector.py。
记录方式:重型开发方案;本方案独立审核 PASS/0 前不得修改产品代码、测试、fixture、正式采集目录或小时任务。
DEV-PROJECT-INFO-BILI-DYNAMIC-REFRESH-COLLECTOR-20260813-001。REQ-BILI-DYNAMIC-COLLECTOR-20260804-001;正式分派消息=msg_20260813110304034_ea344f45。dev.developer.project.secondary / infodev-2 / 019fbcbb-bed7-7c90-83ab-f50610f80d3a。dev.reviewer.project / inforev / 019fb7c5-e1fd-7df0-8ec9-c87edea4215d。case_analysis.video_downloader / 019fcc5d-798f-7ea1-8325-3a4d1f2dc5a5。check 的局部改名。单一实施与审核链保持不变;原 MVP DEV-PROJECT-INFO-BILI-DYNAMIC-COLLECTOR-MVP-20260804-001 的 PASS/0 / ACCEPTED_CLOSED 是冻结历史,本事项不改写它。
现有小时流程在页面读取失效后,只使用公开搜索或 API 摘要;空结果被误表述为“无新增”。公开索引既不能证明页面已刷新,也不能证明身份、正文、72 小时覆盖和分页完整性,必须从权威 no-new 判定中移除。
| 工件 | bytes | SHA-256 |
|---|---|---|
dev/project-dev/bili_dynamic_collector.py |
60090 | 67B2E6FB34F100C57BF50A4D3067D6FDD6E47C5D3FBEF58D2CF5D8796B07AB8B |
dev/project-dev/bili_dynamic_collector.example.json |
1117 | 42A9AA7EE29ED061DC408C6C9A907FA7B20FD8C2B41605C54B6716A0FE388951 |
dev/project-dev/test/test_bili_dynamic_collector.py |
31303 | D82BFF8AD9B78FD749A7536FF9FED0486CF87D6B9FDDEE7947202C2414AAE322 |
dev/project-dev/test/fixtures/bili_dynamic_collector/dynamic_items.json |
1216 | 0CD53C7C93D7E05B4A913EBF5223366E3A06A17E2474DF52BC64F0BB4EBEA71F |
dev-doc/project-doc/B站博主动态采集辅助工具.md |
11950 | 2EE6B01528509D8D6E57D7800BF877FAB7C7F63AE83C1EE8E86D7D247FA730A2 |
ai-video-downloader/draft/2026-08-04-哔哩哔哩动态采集需求.md |
4114 | DC5807D6325AA13B1E0C72C92B9534003CC4F8046A1789AA70FCC98DECC9AB87 |
原公开命令 check / move-completed / handoff、退出码、安全路径、secret gate、选择器、移动事务、terminal evidence 和 canonical handoff 均为已通过合同,不得回退。
ana-data/news-青枫浦上Q/manifest.jsonl=51598 bytes / 0E69AF2AF1C4B0014D2033A21F3986E283ECFD758EA652E7F2416006736C8D73 / 48 lines。SAVED=16,其余主要是同一视频的 blocked/pending/processing 状态。collected_at=2026-08-10T00:41:10+08:00;文件系统 manifest 写入时间约 2026-08-10T00:41:54+08:00。20260809-201835_video-cover_...BV1dwum6UEEq.jpg。Python CLI 不假装能够直接调用 Codex Chrome 工具,也不引入 CDP、remote-debugging、profile 读取、Playwright 持久 profile、浏览器 Cookie 导出或未审核扩展。小时流程由两个已登记组件组成:
case_analysis.video_downloader 的 Codex 小时任务使用项目可用的 Chrome 控制能力,对唯一 creator.dynamic_url 最多执行一次 navigate 或 reload,读取页面可见 DOM,并生成无秘密 browser-evidence.json 与本地 intake 附件。bili_dynamic_collector.py refresh-begin/refresh-commit 创建运行合同、严格验证浏览器证据、统一标准化/去重、原子落盘并输出五种业务终态。因此“主动刷新”属于小时任务编排的第一步,CLI 是其 durable begin/commit 与正式保存入口。文档必须明确:只运行 refresh-commit 或只查公开索引不等于完成刷新。
不得使用 chrome://extensions、开发者模式、--load-extension、Raw CDP、坐标脚本、profile 注入、Cookie/localStorage 读取、签名媒体 URL、HTTP 412 绕过、验证码处理、循环刷新或更换 header/参数规避平台限制。本事项不下载视频,不修改/发布扩展,不写 policy/HKCU。
保留 schema version 1 和现有字段,新增必填 refresh object;缺少时只有旧三命令可用,调用 refresh 命令必须 E_CONFIG,避免旧配置静默获得正式写权限。
{
"creator": {
"name": "青枫浦上Q",
"uid": "1420210197",
"dynamic_url": "https://space.bilibili.com/1420210197/dynamic"
},
"refresh": {
"archive_dir": "<authorized archive root>",
"formal_manifest": "<archive root>/manifest.jsonl",
"intake_dir": "<owned credential-free intake root>",
"timeout_seconds": 30,
"page_ready_timeout_seconds": 15,
"dom_read_timeout_seconds": 8,
"max_refresh_count": 1,
"run_history_slots": 168,
"max_items": 200,
"max_images_per_item": 20,
"max_image_bytes": 31457280,
"max_text_bytes": 5242880
}
}
冻结规则:
青枫浦上Q / 1420210197 / https://space.bilibili.com/1420210197/dynamic;本事项不支持第二个账号。archive_dir、formal manifest 和 intake root 必须 lexical lstat 后位于配置允许根内,父链无 reparse;不得依赖个人机器绝对路径写进代码。dev/project-dev/tmp 或 TemporaryDirectory。window_hours、超时、数量和大小上限,但 window 仍限定 1..744h,一次刷新仍固定为 1。refresh-beginpython dev/project-dev/bili_dynamic_collector.py --config <authorized.json> refresh-begin --now <offset-aware-time>
在现有 StateLock 下创建 state_dir/refresh/pending.json,schema 1 必含:
run_id:由 task、creator UID、started_at 和 128-bit OS random nonce 派生的 32 位小写十六进制;nonce 不属于认证秘密。task_id、creator_name、creator_uid、creator_dynamic_url。started_at、deadline_at、window_start、window_end。config_sha256、formal_manifest_preimage_bytes/sha256、state_manifest_preimage_bytes/sha256。max_refresh_count=1、page_ready_timeout_seconds=15、dom_read_timeout_seconds=8。phase=BROWSER_REFRESH_REQUIRED。pending 采用 CreateNew + flush/fsync;已有 pending 时先按第 11 节恢复,不能覆盖。begin 不读取浏览器、不碰 archive 内容或两个 manifest。
小时任务取得 begin stdout 后:
refresh-commitpython dev/project-dev/bili_dynamic_collector.py --config <authorized.json> refresh-commit --input <browser-evidence.json> --now <offset-aware-time>
commit 必须绑定唯一 pending run_id、config hash 和 deadline;没有 begin、run ID 不一致、deadline 超期或 refresh count 不等于 1均失败。它先完成证据/身份/覆盖/秘密/内容校验,再进行任何正式写入。
根 exact keys:
schema_version, run_id, transport, requested_url, final_url,
refresh_action, refresh_count, refresh_started_at, refresh_finished_at,
read_finished_at, page_outcome, page_title, creator,
coverage, items, discovery_summary, safe_diagnostics
固定合同:
transport=codex_chrome_visible_page。refresh_action=navigate|reload;refresh_count=1。page_outcome=READABLE|UNREADABLE_TIMEOUT|UNREADABLE_ERROR|ACCESS_BLOCKED。uid,name,profile_url;READABLE 时必须精确匹配 UID/name/profile URL。requested_window_start,requested_window_end,oldest_item_published_at,reached_window_start,end_of_feed,pagination_complete,page_truncated,all_cards_parsed,unparsed_card_count。items 最多 200;每项 canonical fields=dynamic_id,opus_id,bvid,content_type,published_at,title,source_url,body_text,body_complete,duration_seconds,artifacts。discovery_summary 只允许状态与计数,不携带响应体;公开搜索/API 不得给 page_outcome=READABLE 或 coverage complete。safe_diagnostics 只允许固定枚举、timeout 秒数和 bounded count,不接受 raw exception、HTML、header、response body 或 URL query。所有未知字段、secret-like key、控制字符、绝对 intake 路径、reparse、hash/size 不符、超限或 URL credential/query 风险均在正式写前停止;错误 stdout 只含固定 code/path,不回显值。
| status | 必要条件 | exit | 正式业务写入 |
|---|---|---|---|
NEW_ITEMS_SAVED |
页面 READABLE、身份匹配;至少一条完整新内容经事务保存。coverage 可完整或不完整,但 stdout 必须给 no_new_confirmed=false |
0 | 新 artifact、state event、formal event |
REFRESH_CONFIRMED_NO_NEW |
READABLE、身份匹配、相对 formal/state catalog 无新增,且 coverage 全部硬门通过 | 0 | 无;只更新有界 run evidence |
REFRESH_FAILED_PAGE_UNREADABLE |
页面 ready/DOM/evaluate 超时或无可靠 DOM,且没有明确 access interstitial | 4 | 无 |
REFRESH_BLOCKED_AUTH_OR_ACCESS |
登录、验证码、会员/DRM/访问控制提示,或 UID/name/profile 身份不匹配 | 3 | 无 |
PARTIAL_DISCOVERY_UNCONFIRMED |
只有公开搜索/API 摘要;或页面可读但 coverage 不足且没有可安全保存的新完整内容 | 4 | 无 |
REFRESH_CONFIRMED_NO_NEW 的 coverage 硬门全部为真:时间字段绑定 pending;page_truncated=false;all_cards_parsed=true;unparsed_card_count=0;pagination_complete=true;并且 reached_window_start=true,其依据必须是 oldest_item_published_at<=window_start 或 exact end-of-feed。空页面只有身份匹配且 explicit end-of-feed 才可 no-new。
优先级:creator mismatch/access blocked > unreadable > readable new items > readable complete no-new > partial。公开 discovery 永远不能提升 page 权威级别。
https://space.bilibili.com/1420210197/dynamic;tab title=青枫浦上Q个人动态-青枫浦上Q动态记录-哔哩哔哩视频。REFRESH_FAILED_PAGE_UNREADABLE / exit4,不得是 no-new。该真实刷新已满足本事项验收 B 的“只允许一次”样本;实现阶段只把脱敏同构 fixture 送入 CLI,不重复真实刷新。Accept: application/json 请求 https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?host_mid=1420210197。code=-412,message=request was banned;重试=0,header/参数绕过=0。discovery_summary=BLOCKED_412,总体在页面不可读时仍为 REFRESH_FAILED_PAGE_UNREADABLE;如果本轮只有 discovery 证据则为 PARTIAL_DISCOVERY_UNCONFIRMED。不得用它证明 no-new,也不得再次尝试规避。新入口必须复用/抽取原代码的时间、URL、Windows stem、stable token、entity ID、atomic write、StateLock 和 manifest helper;不得复制一套不同的 dynamic/opus/BV/source URL 判定。
新增私有 load_formal_catalog() 只读处理:
schema_version=1 事件:从 stable_id、bvid、source_url、可选 creator UID 抽 token。schema_version=2,event_type=DYNAMIC_CONTENT_SAVED:从 dedupe_keys 和 artifacts 抽 token。历史 48 行保持 byte prefix;不把历史 blocked 状态当新内容,也不因 SAVED=16 就推断唯一动态恰为 16。
CONTENT_SAVED 并加入 ACTIVE_OR_SUCCESS_STATUSES,避免旧 check 再次生成待办。TODO_QUEUED,继续走已通过的 move/handoff 主链;本事项不下载视频。check/move-completed/handoff 对既有状态和退出码不变。body_complete=true,正文按 LF 规范化、strict UTF-8、无 BOM、一个终止 LF,exact 1 个 .txt;不得保存 HTML 或摘要冒充全文。.txt;原图为 .jpg/.jpeg/.png/.webp,逐件 magic/extension/size/hash、sequence 唯一,最多 20 张;不转码、不缩放。YYYYMMDD-HHmmss_<type>_<safe-title> 和实体冲突后缀;正式目标 CreateNew/no-overwrite,同名异 hash 停止。每个新实体只追加一条 canonical schema 2 event:
schema_version=2, event_type=DYNAMIC_CONTENT_SAVED, creator,
creator_uid, entity_id, dynamic_id, opus_id, bvid, dedupe_keys,
content_type, published_at, title, source_url, collected_at,
artifacts[{kind,sequence,path,bytes,sha256}], duration_seconds,
page_run_id, coverage_complete, status=SAVED
相同实体重跑不再追加 SAVED;no-new、unreadable、blocked 和 partial 不写 formal manifest。
在任何正式发布前,写 state_dir/refresh/transaction.pending.json,绑定:run、证据 hash、所有源/目标 bytes/hash、state/formal 两份 manifest preimage 和候选 after hash、created targets 列表、phase。阶段顺序:
old exact bytes + canonical new lines,这是业务 commit point;E_RECOVERY_AMBIGUOUS,不猜测删除。E_RECOVERY_FAILED,不改业务文件。E_MANIFEST_RACE,不覆盖,不删除别人的文件。不得把每小时 no-new/timeout/412 追加到 formal manifest。state_dir/refresh/runs/latest.json 加 168 个固定环形 slot(epoch hour modulo 168),每次原子替换对应 slot;最多 169 个状态文件。slot 必含 run_id/time/status/evidence hash/count/error code,不含正文或秘密。相同 run 重放字节一致;旧 slot 被新的小时证据覆盖是预期 bounded 状态,不是业务历史删除。
schema_version,ok,status,error_code,run_id,creator_uid,refresh_count,page_authoritative,coverage_complete,new_items,saved_artifacts,formal_manifest_changed,run_evidence_path。NEW_ITEMS_SAVED、REFRESH_CONFIRMED_NO_NEW exit 0;unreadable/partial exit 4;access/identity/safety exit 3;输入合同 exit 2;内部失败 exit 1;用户中断 exit 130。E_PAGE_TIMEOUT/E_PAGE_UNREADABLE/E_ACCESS_BLOCKED/E_CREATOR_MISMATCH/E_PARTIAL_DISCOVERY/E_COVERAGE_INCOMPLETE/E_SECRET_FIELD/E_MANIFEST_RACE/E_RECOVERY_* 等,不拼接 raw browser/API error。设计 PASS 后最小变更:
dev/project-dev/bili_dynamic_collector.py:增加 config、begin/commit CLI 和共享 helper 调用;保留原三命令。dev/project-dev/bili_dynamic_refresh.py:页面 evidence schema、formal catalog、内容计划和事务/恢复;不包含浏览器/CDP/network client。dev/project-dev/bili_dynamic_collector.example.json:仅加入开发 tmp refresh 配置。dev/project-dev/test/test_bili_dynamic_collector.py:冻结原 13 项并补公开 CLI non-regression。dev/project-dev/test/test_bili_dynamic_refresh.py 与 fixtures/bili_dynamic_refresh/:新增脱敏 fixtures。dev-doc/project-doc/B站博主动态主动刷新采集器.md 与实现证据;原工具文档增加迁移说明,不改写旧验收结论。不新增服务、常驻 daemon、数据库、浏览器扩展、API server、Web UI 或下载器。
必须从 public CLI/main 入口覆盖:
REFRESH_FAILED_PAGE_UNREADABLE,不得出现 no-new,正式树和 manifest bytes 不变。-412 request was banned、页面 partial/truncated/unparsed/pagination incomplete:不得 no-new。测试只用 TemporaryDirectory、数十字节合成图片与短文本,不复制真实正文/图片/视频,不联网、不启动 Chrome、不写正式路径。
本事项已经取得第 8.1 节恰好一次真实 refresh timeout 样本,且公开 API 样本没有重试。实现后将用脱敏、字段等价 fixture 通过 public CLI 重放,断言 REFRESH_FAILED_PAGE_UNREADABLE、formal write 0、no-new token 0。为尊重“一次刷新、无重试”,开发/审核阶段不再次刷新目标页。
若请求方在实现 PASS 后另行运行正常小时采集,那是按既有用户授权的运营动作,不是本开发验收补跑;仍须每轮一次刷新和本方案全部状态门禁。
<1s,100 items/20 images 元数据规划 <2s(不含外部 Chrome/intake 下载时间)。设计审核需一次检查全目标:状态优先级、支持的 Chrome 编排、evidence schema、覆盖充分性、formal mixed-history、两个 manifest、内容事务、恢复、secret、fixture 与真实样本解释。PASS/0 前实现/测试/fixture修改=0。
实现后证据至少包含:
实现提交原 dev.reviewer.project 一次完整审核;只有实现 PASS/0 后,视频下载员才可把新入口用于正式小时运行。审核 PASS 不授权视频下载、扩展发布、policy/HKCU 或访问控制绕过。
状态=V001_DESIGN_COMPLETE_PENDING_ORIGINAL_REVIEWER_IMPLEMENTATION_FROZEN。
当前动作计数:产品 code/test/fixture modification=0;真实 Chrome refresh 新增=0(只接收请求方既有单次证据);network retry=0;download/video=0;formal ana-data write=0;extension publish/policy/HKCU/session secret=0。