Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions scripts/generate-leaderboard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,15 @@ async function fetchAggregatedFromBackend() {
const res = await fetch(LEADERBOARD_API_URL, {
headers: {
accept: "application/json",
"user-agent": "InvolutionHell-build/1.0 (generate-leaderboard.mjs)",
// UA 必须包含 "InvolutionHell-SSR" —— 这是 CF Custom Rule 的匹配触发词:
// (http.host eq "api.involutionhell.com" and http.user_agent contains "InvolutionHell-SSR")
// → Skip Bot Fight / Browser Integrity / Managed Rules
// 之前用 Chrome 伪装时 UA 不含这个 token,CF 规则不匹配,Vercel build runner
// 仍然被 IP 信誉评分判定为 bot 回 403("Just a moment..." 挑战页)。
// 改回带 token 的 UA 让 CF 规则真正放行。
"user-agent":
"InvolutionHell-SSR/1.0 (build; generate-leaderboard.mjs; " +
"+https://involutionhell.com)",
},
signal: controller.signal,
});
Expand Down Expand Up @@ -145,12 +153,53 @@ async function main() {
const aggregated = await fetchAggregatedFromBackend();

if (aggregated === null) {
// 拉不到后端时优先保留 generated/site-leaderboard.json 旧版本:
// 一次 fetch 失败(CF 临时挑战 / Vercel runner IP 信誉低 / 后端短暂抖动)
// 不应该把 commit 进 git 的好数据冲成空数组上线。
//
// 三种情况:
// 1. 文件已存在 + 内容是非空数组 → 保留旧数据 exit 0
// 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;
Comment on lines +162 to +178
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.
} catch {
// 文件存在但 JSON 损坏:当作没有,走下面写空兜底
console.warn(
`[generate-leaderboard] ${OUTPUT} 已存在但 JSON 解析失败,按"首次 build"兜底覆盖空数组。`,
);
}
} catch (readErr) {
// ENOENT 等:文件不存在,走兜底
if (readErr && readErr.code !== "ENOENT") {
console.warn(
"[generate-leaderboard] 读取既有 leaderboard 失败:",
readErr instanceof Error ? readErr.message : readErr,
);
}
}

if (preservedExisting) {
process.exit(0);
}

// 文件不存在 / 损坏:写空兜底,避免后续 Next import 抛 ENOENT
console.error(
"[generate-leaderboard] 后端不可用,写入空榜单以放行构建。 | Backend unreachable, writing empty leaderboard to unblock build.",
"[generate-leaderboard] 后端不可用且本地无可保留数据,写入空榜单以放行构建。 | Backend unreachable and no existing data, writing empty leaderboard to unblock build.",
);
// mkdir + writeFile 必须放同一个 try:任一步失败都意味着 generated/site-leaderboard.json
// 不存在,后续 Next 端 import 会抛更难定位的 ENOENT。这种情况 build 必须 fail-fast,
// 不能 exit 0 让"看起来一切正常"的 deploy 把站点搞挂。
try {
await ensureParentDir(outputAbs);
await fs.writeFile(outputAbs, "[]", "utf-8");
Expand Down
Loading