Skip to content

fix(leaderboard): UA 改回 InvolutionHell-SSR + fallback 保留旧 JSON (re-PR #325)#326

Merged
longsizhuo merged 3 commits intomainfrom
fix/leaderboard-cf-bypass-v2
Apr 27, 2026
Merged

fix(leaderboard): UA 改回 InvolutionHell-SSR + fallback 保留旧 JSON (re-PR #325)#326
longsizhuo merged 3 commits intomainfrom
fix/leaderboard-cf-bypass-v2

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

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 恢复。

故障复盘
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 恢复显示。
Copilot AI review requested due to automatic review settings April 27, 2026 16:01
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
involutionhell-github-io Ready Ready Preview, Comment Apr 27, 2026 4:16pm
website-preview Ready Ready Preview, Comment Apr 27, 2026 4:16pm

@longsizhuo longsizhuo merged commit 6ce8104 into main Apr 27, 2026
6 of 8 checks passed
@longsizhuo longsizhuo deleted the fix/leaderboard-cf-bypass-v2 branch April 27, 2026 16:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-Agent to include InvolutionHell-SSR so Cloudflare custom rule can match and skip bot protections.
  • Adjust fallback behavior: when backend fetch fails, prefer keeping an existing generated/site-leaderboard.json instead of overwriting with [].

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +162 to +178
// 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;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前逻辑在后端不可用时,只要 generated/site-leaderboard.json 能被 JSON.parse 成功就会 preservedExisting = true 并退出;即使解析结果不是数组也会被“维持原状”。但仓库里有多处 import ... from "@/generated/site-leaderboard.json" 并直接对其调用 .filter/.map(例如 rank 页),如果文件内容是对象等非数组,会导致 Next 在构建/运行时直接报 filter is not a function。建议把“可保留”的条件收紧为:解析结果必须是数组(最好还校验元素结构/至少是数组);否则应视为无效数据,走写入 [] 的兜底分支以保证 build 可继续。

Suggested change
// 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 uses AI. Check for mistakes.
longsizhuo added a commit that referenced this pull request Apr 27, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants