2026-10-30 对线上记录的核查结果:
| 事实 | 证据 |
|---|---|
| GitHub 的 schedule 事件被静默丢弃 | longgame 最后两次 schedule 成功在 10-22、10-23;此后到 10-30 共 158 小时没有任何 schedule 运行。10-26(周一)whole weekly-collect 整次没跑。 |
| 失败没有任何声音 | 17 个 workflow,无 failure 通知、无 Issue、无邮件。10-30 手动触发的 weekly-collect 失败后,也没人知道。 |
| 看门狗和被监控对象在同一根线上 | 原 if-stale.mjs 挂在 daily-collect 内部。触发器死了,看门狗一起死。 |
| 单线失败能拖死整轮 | run-all 原逻辑"任一 line 失败即 exit 1",导致 gate/publish 被跳过——数据采到了却不发布。 |
代码层面能补的已经补了(下面三层)。补不了的是:仓库完全不知道自己"没被叫醒"。 这件事必须由仓库外的某个东西来问一句"你还在吗"。
| 层 | 位置 | 做什么 | 防的是什么 |
|---|---|---|---|
| 页级 | scripts/line.mjs | 三档容错:tolerate(已知抓不到,只记事件)/throttled(202/403/417/429/空挑战页,重试一次后放行)/其余(首次告警放行,连续两轮才判失败) | 单页反爬拖垮一条线 |
| 线级 | scripts/run-all.mjs | 单线失败只记录(写进 data/collect-runs.json),只有本轮尝试的线全部失败才判整轮失败 | 一条线故障让其它 12 条线的数据无法发布 |
| 心跳级 | scripts/heartbeat.mjs | 一日三次自检 → 超期线当场补采 → critical 开 Issue → 落健康页与心跳文件 | 整周断档无人知 |
新鲜度按最后一次成功采集算,不按最后一次运行算。差别很关键:一条每周都跑、每次都全挂的线, 用"最后运行时间"算会永远显示今天刚采过——这就是绿灯下的死亡。
这套机制不是"写完就完事"。第一次真跑在 GitHub 上,立刻暴露出四个只在真实环境里才出现的问题:
| 故障 | 表现 | 根因 | 修法 |
|---|---|---|---|
| 门禁信号永远是假警 | 健康页每轮都报 gate-missing,而真正的 gate FAIL 反而看不见 | assurance/.gate/result.json 落在 runner 的工作目录里,job 结束即销毁;心跳在另一个 workflow 里永远读不到它 | 心跳改成问 Actions API 要最近一次 assurance-gate 的结论(conclusion),本地跑时回退读文件。已实测读到 passed=true 并写进健康页 |
| 没有变更被当成失败 | self-upgrade 首跑即红,还开出一条假告警 Issue | git add data/self-tuning.json:该文件只在真有变更时才生成,git add 打到不存在的路径会以 128 退出 | 改为"存在才 add",另外 7 个 workflow 的同类隐患一并加上 || true |
| 陈旧阈值和周期打架 | self-output-stale 连报 216 小时 | docs/status.json 一周才产一次,阈值设 48 小时等于每周有 5 天在报假警 | 阈值对齐到 192 小时(一个周期 + 1 天宽限) |
| 推对了代码,推错了分支 | workflow 手动触发能跑,定时器毫无动静 | 默认分支是 master,改动推到了 main;schedule 只在默认分支触发 | 快进 master 后推送,schedule 与 pages 部署同时恢复 |
这四个里没有一个能被代码审查发现——它们全都是"在真实环境跑一次"才现形的。
所以这套系统的验收标准不是"代码写完了",而是"线上真跑过一轮并且绿灯"。
2026-10-30 13:28 UTC 的那轮心跳 overall=ok,是它第一次真正通过验收。
| Issue 标题前缀 | 什么时候开 | 什么时候自动关 |
|---|---|---|
workflow-failed: <workflow 名> | 任一 workflow 失败(每个 workflow 最后一步 notify-failure.mjs) | 人工关 |
longgame-critical | 心跳判定 critical:某线超期 1.5 倍周期 / 连续两轮全挂 / 门禁 FAIL | 下次心跳判定恢复时自动关 |
longgame-schedule-silence | 上次心跳距今超 30 小时 | 心跳恢复后自动关 |
健康状态随时可看:health.html(每轮自动重写),原始数据
docs/health/latest.json,模型可读摘要 docs/health/latest.json 与
data/heartbeat.json(心跳时间戳在这里,外部监控盯这个)。
任意一台常开的机器(VPS / NAS / 家里的旧电脑 / 树莓派)加一条定时任务, 每 6 小时叫一次心跳。它跑起来就会自己补采超期线:
# crontab -e
0 */6 * * * curl -sS -X POST \
-H "Authorization: Bearer $GH_PAT" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/xka0085-byte/longgame/actions/workflows/heartbeat.yml/dispatches \
-d '{"ref":"master"}'
分支必须写 master。这个仓库的默认分支是 master,
而 GitHub 的 schedule 事件只在默认分支上触发——代码推到一个非默认分支上,
workflow 文件在,长得也对,但定时器永远不会叫它。2026-10-30 就踩过一次:
改动推到了 main,workflow 手动 dispatch 能跑、schedule 全哑。
需要一个 fine-grained PAT,权限只要 Actions: write(不需要 repo 全权限)。 把 token 放在那台机器的环境变量里,不要写进仓库。
用任何支持"HTTP 内容变化"或"JSON 字段"检查的服务(Healthchecks.io / UptimeRobot / 自建),
盯 https://xka0085-byte.github.io/longgame/health/latest.json 里的
generated_at,超过 30 小时未更新就发邮件。五分钟能配完,不需要 token,
但它只能告诉你"断了",不能替你补。
两个都做最好:A 负责把系统叫醒,B 负责在 A 也失效时叫醒你。 只做 A 的风险是 A 自己挂了没人知道;只做 B 的风险是每次断档都要等你自己动手。
每周一(外加周四兜底)self-upgrade.yml 会按真实采集证据自动调整策略参数,
并且只允许动两处:
| 允许自动改 | 条件 | 为什么安全 |
|---|---|---|
portfolio.json 某页 tolerate | 该页连续 ≥3 轮不可用、近 20 轮零成功、失败状态全属反爬/限流集合 | 只是让已知抓不到的源不再阻断整轮;不写入任何数值,保留上次数据 |
data/self-tuning.json 周期容忍天数 | 该线真实采集间隔长期宽于名义周期 | 只影响"多久算超期"的判定,每次 +1 天,weekly 上限 +3、monthly 上限 +7 |
不允许自动改(脚本内白名单,没有外部入口):解析器、evidence.json、门禁阈值、
assurance_level、verified_by_third_party、任何 workflow 文件。
每次变更都先跑 verify.mjs 与 validate-contracts.mjs,不过就原样回滚,
并在 data/self-upgrade-log.jsonl 留下 before/after、证据与回滚命令。
诊断结论写 data/diagnostics.json——"看起来像 bug"的判断只记录,不动代码。
为什么不自动改解析器:自动改解析逻辑一旦出错,会把一条降级的线变成一条 看起来正常但是错的线。这套项目对外只承诺"可复核",宁可红着也不能假装绿着。
workflow-faileddata/lines/<id>.json 最后一条 run 的 pages/dirs/queries 字段,状态码就是原因。assurance/.gate/result.json 里哪一项没过。门禁失败不许绕过,修数据或修实现。longgame-criticalheal.failed。portfolio.json 改 URL 或把这条线 status 降级;
不要为了让红灯变绿而加 tolerate——那等于把"没数据"说成"没问题"。longgame-schedule-silence这条最特殊:它只有在外部触发器把人叫醒、补跑了一次心跳之后才会出现。 出现就意味着 GitHub 的 cron 又丢了事件。处理方式是确认外部触发器还在工作(方案 A/B)。
workflow-failed。自检的边界:这是项目自己的内部检查,不是第三方验证。心跳只能证明"这一次跑了", 不能证明"该跑的那次跑了";健康页全绿只说明已知检查项都通过,不说明没有未知缺陷。