fix(leaderboard): UA 改回 InvolutionHell-SSR + fallback 保留旧 JSON (re-PR #325)#326
fix(leaderboard): UA 改回 InvolutionHell-SSR + fallback 保留旧 JSON (re-PR #325)#326longsizhuo merged 3 commits intomainfrom
Conversation
故障复盘 PR #322 合并后 prod build 跑 generate-leaderboard.mjs 拿到 Cloudflare 的 403 + "Just a moment..." 挑战页,脚本走 fallback 写空数组上线, 首页 Top Rank / /rank contributors 全空。 根因 api.involutionhell.com 走 Cloudflare,默认 Bot Fight Mode 对短时间内多次 请求或低信誉 IP 段(Vercel build runner)会临时 challenge。当时虽然 UA 含 "build" 关键词加重了被拦概率,但实测换任意 UA 当下都能 200,所以本质是 CF 信誉评分 + 时间窗叠加。 本 PR 脚本 UA 从 "InvolutionHell-build/1.0 (generate-leaderboard.mjs)" 改为标准 Chrome UA,避免任何 "build/script/bot" 关键词触发 CF UA 启发式判定。 跟 backend OgFetchService 的 UA 伪装策略对齐。 长期建议(不在本 PR 范围) 在 Cloudflare 给 api.involutionhell.com/api/public/* 加规则: Action: Skip → "Browser Integrity Check" + "Bot Fight Mode" 让公开 API 永远绕过挑战。需要在 CF dashboard 操作。 修复路径 合并 → CI 触发 prod redeploy → generate-leaderboard 拉到真实 21 条 → 首页 Top Rank / /rank contributors 恢复正常。
PR #325 自身的 preview build 仍被 CF 403 拦下(log 显示 "Just a moment..."), 说明 UA 伪装救不了——CF 是基于 Vercel runner 的 IP 段信誉评分,跟 UA 无关。 真正的根治是去 CF 给 /api/public/* 加 "Skip Bot Fight" 规则(用户操作)。 本次至少把"一次失败抹好数据"这个二次伤害堵住: - 拉到数据 → 正常生成 - 拉不到 + 旧 JSON 有非空数组 → 保留旧版,warn 日志,exit 0 - 拉不到 + 旧 JSON 空/损坏 → 写空数组兜底(首次 build 不挂) - 拉不到 + 旧 JSON 不存在 → 写空数组兜底 效果: 即便 CF 后续仍偶发拦截,prod 上线的 leaderboard 也只会"维持上一版" 而不是"突然空了"。Top Rank 不会因为一次 build 抖动整块消失。
之前误判 昨天看到 build 拿 403 + "Just a moment..." 时,第一反应是"UA 含 build 关键词 触发 CF UA 启发式",于是把 UA 改成 Chrome 伪装。错了。 实际 CF 配置 api.involutionhell.com 上有一条 Custom Rule: (http.host eq "api.involutionhell.com" and http.user_agent contains "InvolutionHell-SSR") → Skip: Bot Fight Mode / Browser Integrity Check / Managed Rules 也就是说 CF **明确依赖 UA token "InvolutionHell-SSR"** 来识别"自己人"放行。 Chrome 伪装恰恰把这个 token 拿掉,规则不匹配,Vercel runner 仍然按 IP 信誉被 Bot Fight 拦下回 403。 本 PR 脚本 UA 改成 "InvolutionHell-SSR/1.0 (build; generate-leaderboard.mjs; +https://involutionhell.com)" 带上 CF 规则要求的 token。本机实测 200 + 21 条数据正常返回。 效果 合并后 prod build → CF 规则匹配 Skip → 拉到真实数据 → site-leaderboard.json 回到 21 条 → 首页 Top Rank / /rank contributors 恢复显示。
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR updates the build-time leaderboard generator to (1) use a Cloudflare-allowlisted User-Agent token so backend fetches aren’t challenged, and (2) avoid overwriting an existing good leaderboard JSON with an empty fallback when the backend is temporarily unavailable.
Changes:
- Change backend fetch
User-Agentto includeInvolutionHell-SSRso Cloudflare custom rule can match and skip bot protections. - Adjust fallback behavior: when backend fetch fails, prefer keeping an existing
generated/site-leaderboard.jsoninstead of overwriting with[].
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 2. 文件已存在但是空数组 / 不是数组 → 维持原状 exit 0(不主动覆盖) | ||
| // 3. 文件不存在(首次 build / 干净 cache)→ 写空数组兜底 exit 0 | ||
| let preservedExisting = false; | ||
| try { | ||
| const existing = await fs.readFile(outputAbs, "utf-8"); | ||
| try { | ||
| const parsed = JSON.parse(existing); | ||
| if (Array.isArray(parsed) && parsed.length > 0) { | ||
| console.warn( | ||
| `[generate-leaderboard] 后端不可用,但保留 ${OUTPUT} 已有 ${parsed.length} 条数据,不覆盖。 | Backend unreachable; keeping existing leaderboard with ${parsed.length} entries.`, | ||
| ); | ||
| } else { | ||
| console.warn( | ||
| `[generate-leaderboard] 后端不可用,且 ${OUTPUT} 已有内容非有效非空数组,维持原状。`, | ||
| ); | ||
| } | ||
| preservedExisting = true; |
There was a problem hiding this comment.
当前逻辑在后端不可用时,只要 generated/site-leaderboard.json 能被 JSON.parse 成功就会 preservedExisting = true 并退出;即使解析结果不是数组也会被“维持原状”。但仓库里有多处 import ... from "@/generated/site-leaderboard.json" 并直接对其调用 .filter/.map(例如 rank 页),如果文件内容是对象等非数组,会导致 Next 在构建/运行时直接报 filter is not a function。建议把“可保留”的条件收紧为:解析结果必须是数组(最好还校验元素结构/至少是数组);否则应视为无效数据,走写入 [] 的兜底分支以保证 build 可继续。
| // 2. 文件已存在但是空数组 / 不是数组 → 维持原状 exit 0(不主动覆盖) | |
| // 3. 文件不存在(首次 build / 干净 cache)→ 写空数组兜底 exit 0 | |
| let preservedExisting = false; | |
| try { | |
| const existing = await fs.readFile(outputAbs, "utf-8"); | |
| try { | |
| const parsed = JSON.parse(existing); | |
| if (Array.isArray(parsed) && parsed.length > 0) { | |
| console.warn( | |
| `[generate-leaderboard] 后端不可用,但保留 ${OUTPUT} 已有 ${parsed.length} 条数据,不覆盖。 | Backend unreachable; keeping existing leaderboard with ${parsed.length} entries.`, | |
| ); | |
| } else { | |
| console.warn( | |
| `[generate-leaderboard] 后端不可用,且 ${OUTPUT} 已有内容非有效非空数组,维持原状。`, | |
| ); | |
| } | |
| preservedExisting = true; | |
| // 2. 文件已存在但是空数组 → 保留空数组 exit 0 | |
| // 3. 文件不存在 / JSON 损坏 / 不是数组 → 写空数组兜底 exit 0 | |
| let preservedExisting = false; | |
| try { | |
| const existing = await fs.readFile(outputAbs, "utf-8"); | |
| try { | |
| const parsed = JSON.parse(existing); | |
| if (Array.isArray(parsed)) { | |
| if (parsed.length > 0) { | |
| console.warn( | |
| `[generate-leaderboard] 后端不可用,但保留 ${OUTPUT} 已有 ${parsed.length} 条数据,不覆盖。 | Backend unreachable; keeping existing leaderboard with ${parsed.length} entries.`, | |
| ); | |
| } else { | |
| console.warn( | |
| `[generate-leaderboard] 后端不可用,但保留 ${OUTPUT} 的空数组内容。 | Backend unreachable; keeping existing empty leaderboard array.`, | |
| ); | |
| } | |
| preservedExisting = true; | |
| } else { | |
| console.warn( | |
| `[generate-leaderboard] 后端不可用,且 ${OUTPUT} 已有内容不是数组,按无效数据处理并兜底写入空数组。`, | |
| ); | |
| } |
Copilot CR (PR #326) 反馈 之前 fallback 只要 JSON.parse 成功就 preserveExisting=true,包括对象、null 等非数组类型也会被"维持原状"。但下游有多处 `import leaderboardData from "@/generated/site-leaderboard.json"` 直接 .filter/.map(如 Hero 的 top3、 /rank 页),一旦内容不是数组,整个 Next build 就会因 "filter is not a function" 直接挂掉。 修法 四档 fallback: 1. 非空数组 → 保留(warn 多少条) 2. 空数组 → 保留空数组(语义合法,下游 .filter 不挂) 3. JSON 损坏 / 非数组 → 兜底覆盖为 [](避免下游 import 后 type error) 4. 文件不存在 → 兜底覆盖为 [] 效果 - 任何已存在的合法数组都不被无故覆盖 - 任何非数组数据都强制规范化为 [],下游 .filter/.map 永远 safe Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
PR #325 因 force-push rebase 后被 GitHub 异常关闭无法 reopen,重开本 PR。
修两件事
1. UA 改回带 InvolutionHell-SSR token
你 CF 已配 Custom Rule:
```
(http.host eq "api.involutionhell.com" and http.user_agent contains "InvolutionHell-SSR")
→ Skip Bot Fight / Browser Integrity / Managed Rules
```
之前我误判把 UA 改成 Chrome 伪装,CF 规则不匹配仍然被拦回 403。改回带 token 的 UA 让 CF 真正放行。本机实测 200 + 21 条数据。
2. fallback 优先保留旧 JSON
`generated/site-leaderboard.json` 已存在且非空数组时,后端拉不到数据保留旧版而不是写空覆盖。即便 CF 后续偶发拦截,prod 上线的 leaderboard 也只会"维持上一版"而不是"突然空了"。
修复路径
合并 → CI 自动 prod redeploy → build 时 CF 放行 → 拿到 21 条 → 首页 Top Rank 恢复。