发布于 更新于 约 11 分钟读完 · 4,142 字

本文目录 16 节
  1. 事实记录(不可修改区)
  2. 排障过程
  3. 结果分析
  4. 方法论沉淀
  5. ⭐⭐ GitHub schedule 必须配 GitHub 之外的触发兜底(二次验证)
  6. ⚠️ 版本指纹先定性,再拆管道(首次发现)
  7. ⚠️ 校验器 ERROR 先复现其解析逻辑(首次发现)
  8. ⚠️ 沙箱 iframe 内外部协议链接会静默失效(首次发现,未真机验证)
  9. ⚠️ 单源静态分发升级为多镜像取最新(首次发现)
  10. ⚠️ 版本指纹的另一面:分享链接别从页面地址拼(2026-09-29,工位池塘)
  11. ⚠️ 所有 Toy 同源:本机存储别的 Toy 读得到(2026-10-01,风眼)
  12. ⚠️ 外层地址里的版本号不保证出现(2026-10-01)
  13. ⚠️ PAT 轮换:请求头要带 Bearer ,换完删旧的再看一班(2026-10-01,风眼)
  14. 下次改进
  15. 附录:发布平台网页端更新 Toy 的正确姿势(实测 SOP,2026-07-12 回填)
  16. 关联文档

B站Toy同步事故复盘:版本指纹先定性,外部cron做兜底

一句话:分发平台的线上产物自带版本指纹(URL 后缀 -v1、外壳 meta),先抓线上实际版本定性”管道坏了”还是”从未走通”,再回头拆管道;调度、分发、更新三环,各自都需要一条 GitHub 之外的兜底。

事实记录(不可修改区)

  • 项目:风眼 · Typhoon Eye(台风实况 + 分级应急预案,零依赖静态单页;仓库 Mr-Salticidae/typhoon-eye),发布为 B 站 Toy(id 8980525008896,slug YliUzbE5TOqySu4G),B站 AI 创造公开赛参赛作品,获官方推流
  • 症状(2026-07-11 晚):“Toy 平台无法同步更新”——线上仍是 v0.2 首版,包内数据停在 07-10 12:25;而仓库数据管道(Actions 每 30 分钟抓浙江台风网 → Pages)全程正常
  • 诊断结论:三环各自独立断裂——① CI 静态校验 toy_doctor.py 把 index.html 的 href="./" 解析成资产 . 判 missing ERROR,preview 工作流两次全挂;② toy.yaml 缺项目 id,且仓库根本没有任何会调用 update 的链路;③ 线上外壳 iframe 指向 …-v1 ——首版之后从未成功更新过
  • 修复与加固时间线:07-11 夜修 doctor 误报 + 补 id,经 B 站发布平台网页端提交更新(v0.3 = v2 过审);07-12 晨 v0.4 = v3 上线(四源镜像取数 / 15 分钟班次 / jsDelivr 主动 purge / tel 拨号三级唤起);07-12 07:52 外部 cron-job.org 兜底首次执行即真实补救一次 GitHub 丢班(数据已过期 37 分钟,watchdog 补抓提交)
  • GitHub 调度丢班实录:07-11 18:00–23:00 UTC,每小时 4 班定时任务仅被放行约 1 班;22:03 UTC 后连丢 4 班,数据滞后 69 分钟;GitHub 状态页全程显示 operational
  • 数据来源:GitHub Actions API 运行记录、线上外壳页与 bebox.net 内容抓取比对、git log、cron-job.org 执行记录

排障过程

  1. 拉 Actions 运行记录:数据管道(抓取/Pages)全绿,唯独 toy-preview 两次失败,全部死在 Validate 步——问题不在数据侧。
  2. 复现校验器判定(本机无 Python,直接读 check_ref 源码走查):href="./" 被按相对资产拼成 .,不在文件集合 → ERROR。index.html 全文唯一 ERROR 即此误报,改 href="index.html" 一行过检。
  3. 抓线上外壳页:B 站 Toy URL 只是壳,真实内容在沙箱 iframe(bebox.net/toy/<slug>/<id>-v1/)。-v1 一锤定音”首版后从未更新”;且壳里 window.__TOY_META__ 直接暴露了 update 流程缺失的 toy_id。
  4. 走网页端更新:CLI update 需要 cookie secret(未配置且不宜经手);发布平台网页端在已登录会话下走”发布记录 → 更新”,slug/标题/可见性全保留,不会误建新 Toy。
  5. 修完复发再排查:次日用户仍见旧数据——Actions 记录证明是 GitHub schedule 整点丢班(非应用问题)→ 上外部 cron 兜底,首跑即真实补救。

结果分析

  • 符合预期:三环全部修复,v3 线上代码验证含全部加固;外部兜底与 GitHub 自身班次形成三路触发,任一路活着数据滞后不超过约 20–35 分钟;四源镜像实测全 200 + CORS=*,数据更新后主动 purge,jsDelivr 1 分钟内跟上
  • 未验证部分(诚实边界):① tel 拨号三级唤起只验证了代码部署,B站 App 真机唤起成功率未实测,当前保底是”号码已复制”提示;② jsDelivr 大陆可达性是常识判断 + 本机验证,未从墙内多地实测;③ cron-job.org 免费档长期稳定性待观察;④ 15 分钟页内自动刷新未在真实用户端观测;⑤ PAT 90 天有效期(2026-10-10 到期),轮换是人肉环节

方法论沉淀

⭐⭐ GitHub schedule 必须配 GitHub 之外的触发兜底(二次验证)

核心:GitHub Actions 的 cron 是尽力而为,高峰期丢班/长时间不触发无 SLA,且状态页不反映——不能作为生产级定时任务的唯一触发源。

来源:两个仓库、两种表现——快讯 workflow 的 schedule 自上线起零自然触发(见 2026-07-03_云端定时内容生产连环坑复盘_全绿不等于已发_v1 坑①);本次 typhoon-eye 每小时 4 班仅放行约 1 班、连丢 4 班致数据滞后 69 分钟。

验证状态:二次验证 ⭐⭐(跨仓库、跨月份两例)

操作规则:

  1. 外部定时源(cron-job.org / Cloudflare Worker Cron)按业务周期 workflow_dispatch 兜底;
  2. 外部触发的目标选”过期才动手”的 watchdog 任务而非主任务——幂等,GitHub 自身正常时空转零成本,不产生重复提交;
  3. PAT 用细粒度、单仓库、仅 actions: write;
  4. 兜底线路自身要可观测:开外部服务的失败邮件告警。

反例/边界:分钟级实时性要求不适用(此方案最坏仍有 20–35 分钟滞后),应改常驻服务;外部服务与 GitHub 同时故障无解,属可接受残余风险。

⚠️ 版本指纹先定性,再拆管道(首次发现)

核心:分发平台的线上产物往往自带版本指纹(URL 版本后缀、外壳页 meta、页脚版本号)。排”不更新”类故障,第一步抓线上实际版本——一步区分”管道坏了”和”从未走通过”,避免在正常环节空转;外壳页还可能直接暴露你缺失的元数据(本次 __TOY_META__ 给出 update 必需的 toy_id)。

来源:iframe -v1 后缀直接证明首版后从未更新;三环断裂里两环由此反推锁定。

验证状态:首次发现 ⚠️

⚠️ 校验器 ERROR 先复现其解析逻辑(首次发现)

核心:静态校验器(doctor/linter)报 ERROR ≠ 代码真有缺陷。toy_doctor 把合法的 href="./" 按相对资产解析成 . 判 missing——改代码前先读校验器源码复现判定路径,分清”真缺陷”与”解析盲区”,再选成本最低的一侧改(本次改代码一行,不动校验器)。

来源:preview 工作流两次全挂的唯一 ERROR 即此误报。

验证状态:首次发现 ⚠️

⚠️ 沙箱 iframe 内外部协议链接会静默失效(首次发现,未真机验证)

核心:托管平台把第三方内容装进 sandbox iframe(B站 Toy → bebox.net)时,tel: / mailto: 等外部协议的本框架导航可能被静默吞掉,用户只觉得”点了没反应”。

操作规则:检测 window.self !== window.top 后走三级唤起(top 导航 → window.open → 本框架导航)+ 始终自动复制号码 + toast 提示;顶层环境保持原生行为不动。

反例/边界:B站 App 真机唤起成功率未实测,此条整体仍是待验证假设;保底体验(“号码已复制,可粘贴拨打”)必须存在。

验证状态:首次发现 ⚠️

真机追记(2026-07-12,勘误不覆盖原文):B站 App 真机实测,上面的”三级唤起”第一级(顶层导航)是有害的——App 的 webview 未注册 tel: 处理器,顶层导航把整个 Toy 页跳到 net::ERR_UNKNOWN_URL_SCHEME 全屏错误页,体验比”点了没反应”更糟。修正后的规则:沙箱内禁止一切页面级导航(顶层 / window.open / 本框架都不行),改用隐藏 iframe src = tel:... 触发——支持 tel: 的浏览器静默唤起拨号,不支持的静默失败、页面不动;复制号码 + toast 保底不变。教训:webview 对未注册 scheme 的处置(静默吞 / 全屏报错)因宿主 App 而异,“尝试拨号”的任何路径都必须保证失败时页面无感。

⚠️ 单源静态分发升级为多镜像取最新(首次发现)

核心:*.github.io 在大陆不稳,单源在线数据一失败就静默降级旧快照,用户观感 = “从不更新”。并行请求 Pages + jsDelivr×2 + raw 四源,以数据内时间戳取最新(而非先到先得);镜像有缓存 TTL,数据每次更新后必须主动 purge(实测 purge 后 jsDelivr 1 分钟内跟上)。

来源:v1/v2 时期在线取数逻辑存在但用户仍见旧数据的成因之一。

反例/边界:首个成功源之后留 800ms 收集窗口,是首屏延迟与数据新鲜度的折中;jsDelivr 大陆可达性未多地实测。

验证状态:首次发现 ⚠️

⚠️ 版本指纹的另一面:分享链接别从页面地址拼(2026-09-29,工位池塘)

版本号写在内层 iframe 的路径里(bilibilitoy.com/toy/<slug>/<id>-v18577/index.html)。反过来说,游戏自己读到的 location 每次更新都会变。

  • 工位池塘 v0.6.0 的”分享我的缸”,网页版原本从页面地址拼分享链接,拼出来的是带版本号的内层地址。Toy 一更新,旧版本路径就作废,朋友收到的链接就打不开。
  • 修法:分享链接的落地页写死成 Toy 外层地址(www.bilibili.com/toy/<slug>/index.html?tank=…)。实测外层会把 ?tank= 查询参数原样转给当前版本的内层 iframe,朋友点开就直接进缸。电脑版”复制链接”也给这个外层地址。
  • 重新打包提交后,审核通过,内层从 -v18577 变成 -v19760。验证线上包的办法:curl --compressed 抓外层页,grep <id>-v[0-9]+ 拿到内层地址;再比对线上 index.data 和本地上传包的字节数、尾部 64KB 哈希,一致即上线。外层页是 gzip 压缩的,不加 --compressed 抓下来是乱码。
  • 规则:托管平台下,任何要”发出去、以后还要能点开”的链接(分享、邀请、回访),都用平台的稳定外层地址,不用运行时读到的自身地址。

来源:2026-09-29_右下角的池塘_游戏宣传微电影_实录混剪到双语字幕发布_复盘_v1

⚠️ 所有 Toy 同源:本机存储别的 Toy 读得到(2026-10-01,风眼)

外层页的 iframe 带 sandbox="… allow-same-origin …",而所有作品都托管在 https://www.bilibilitoy.com/toy/<slug>/… 下——同一个源。所以 localStorage / sessionStorage / IndexedDB 对平台上所有 Toy 是共享的:别人的 Toy 能读到你存的键,键名冲突也会互相覆盖。

  • 键名一律加项目前缀(风眼用 typhoon-eye:)。
  • 位置、联系人这类敏感信息不写本机存储。风眼 v1.1「我关心的城市」因此只放内存、刷新即清空,与 v0.7 地区选择同一条隐私边界。
  • 工位池塘(Godot Web)的存档在 IndexedDB 里,同理对其他 Toy 可见,只是内容不敏感。

⚠️ 外层地址里的版本号不保证出现(2026-10-01)

风眼 v1.0 过审后,外层页的 iframe 变成不带版本号的 …/YliUzbE5TOqySu4G/index.html;同一天工位池塘仍是 …/7114672086016-v19881/index.html。上面”版本指纹”那一招不能再当唯一判据:过审判据改为直接抓内层文件(选一个新版才有的文件,或比对 app.js)与提交的包逐字节一致。

订正(同日晚,勘误不覆盖原文):风眼 v1.1 过审后,内层地址又带回了版本号 …/8980525008896-v20874/index.html。我的轮询写死了不带版本号的路径 …/YliUzbE5TOqySu4G/assets/nearby.js,一直 404,连报”3 小时内未上线”,其实早已上线。规则补全:内层路径每次都从外层页现抓(curl --compressed 外层页,取 bilibilitoy.com/toy/<slug>/…index.html),再在那个路径下比对新文件;不要写死任何一种形态。比对时 index.html 跳过——平台会往里注入运行时代码(本次 15 KB → 25 KB),必然不一致。

⚠️ PAT 轮换:请求头要带 Bearer ,换完删旧的再看一班(2026-10-01,风眼)

旧 PAT 10-10 到期,10-01 换新(细粒度、仅本仓库、Actions 读写,有效期到 2027-09-30)。换的时候踩了一个坑:

  • cron-job.org 的 Authorization 头,值要写成 Bearer <token>。作者粘贴时把整格换成了 token 本身,漏了前缀,下一班直接 401 Unauthorized。补上 Bearer 后恢复。
  • 怎么确认真换成了新的:先看下一班 workflow_dispatch 成功;再删掉旧 token,再看一班成功。旧的已经不存在,还能成功就只可能是新的。GitHub token 列表里的 “Last used” 有延迟,刚换完会显示 “Never used”,不能当判据。
  • 轮换全程是作者本人操作(生成、粘贴、删除),助手只补前缀、看触发记录,不读、不复制 token。

下次改进

  • B站 App 真机实测:拨号三级唤起成功率、15 分钟自动刷新是否生效
  • 观察 cron-job.org 一周执行记录与 GitHub schedule 自然触发率,回填本档验证等级
  • 2026-10 上旬 PAT 轮换(90 天到期) 已于 2026-10-01 完成,新 PAT 2027-09-30 到期;断更告警(data-health)兜底
  • 官方推流若带来流量,关注 Pages 带宽配额与 jsDelivr 命中情况

附录:发布平台网页端更新 Toy 的正确姿势(实测 SOP,2026-07-12 回填)

CLI 更新需要 cookie secret,网页端(bilibili.com/toy/publish)是已登录会话下的首选路径。三次实战(v2/v3/v4)踩出的坑与固定动作:

  1. “更新”按钮有概率把你带进新建表单——实测 3 轮里 2 轮首次点击后落到”上传发布”页:页面地址栏是预生成的随机 slug、标题为空、底部按钮是”发布”。此时上传等于创建第二个 Toy,不是更新。成因疑似 SPA 状态未就绪;窗口较窄时记录行按钮还会从”更新/删除”文字变成 ↻/× 图标,更易误判。
  2. 进入更新模式的三重判据(缺一不可,建议程序化断言而不是目测):① 顶部出现”正在更新 <原标题>“横幅(带”取消”);② 页面地址锁定为原 slug;③ 底部按钮文案是”更新”而非”发布”。首点失效就回”发布记录”等列表渲染完,悬停目标行重点一次。改良(第 4 轮验证):不用坐标点击,直接在页面 JS 上下文定位记录行的 button.update-btn 调 .click(),再原子断言 ①②——实测首次即中,绕开了坐标点击对悬停态/渲染时序的依赖。
  3. 自动化上传的可行注入法:浏览器扩展的文件上传白名单不含任意本地路径;绕法是在页面 JS 上下文里 fetch 仓库 raw 地址的 zip(按 commit 固定 URL),构造 File + DataTransfer 赋给 input[type=file](accept=“.zip,.html,.htm” 的那个)并派发 change 事件。注入前后都校验字节数与预期一致。
  4. 点”更新”前验预览包:上传成功后页面会生成 toy/preview/preview_xxx/ 预览地址,先 fetch 预览包内的关键文件(如本次的 assets/toy-runtime.js)确认确实是新代码,再提交——防止传错包白走一轮审核。
  5. 过审判据:外壳 iframe 的版本后缀 +1(-v3 → -v4)即上线;两次实测审核耗时约 10 分钟。更新不改 slug、标题、封面与可见性(全部保留原值)。
  6. 补(2026-10-07,工位池塘 v0.6.2,第 5 轮):
    • 「发布记录 → 更新」用扩展的元素定位(find 拿到的 ref)点,首次就进了更新模式,第 2 条的三重判据全中。
    • 文件注入走不通,退回人工:Godot Web 包 12.8 MB,超过扩展单次上传的 10 MB 上限;里面的 wasm 有 37.7 MB,压缩后就要 10 MB 左右,压不下去。第 3 条的 fetch 注入要求包放在页面能跨域读到的地址上,而这个包不入库。最后请作者手动拖,文件事先放进下载文件夹。
    • 点「更新」前看预览:预览里出现了只有新版才有的「倒计时」开关。
    • 00:40 提交,5 分钟内查到已上线:外层页现抓内层路径 …/7114672086016-v23165/index.html。index.html 虽然被平台注入过,里面 GODOT_CONFIG 的 fileSizes 仍然可读,和本地一致;index.data 的 Content-Length 5,999,956,和本地 pck 一致。
    • 来源:2026-10-06_工位池塘v0.6.2_老issue分诊到三端发版_复盘_v1

关联文档

类型/平台工程主题/踩坑主题/自动化生产