Skip to content

Commit 7c4d5a7

Browse files
fix(leaderboard): fallback 优先保留旧 JSON,避免一次拉失败抹掉好数据
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 抖动整块消失。
1 parent e30e231 commit 7c4d5a7

1 file changed

Lines changed: 45 additions & 4 deletions

File tree

scripts/generate-leaderboard.mjs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,53 @@ async function main() {
153153
const aggregated = await fetchAggregatedFromBackend();
154154

155155
if (aggregated === null) {
156+
// 拉不到后端时优先保留 generated/site-leaderboard.json 旧版本:
157+
// 一次 fetch 失败(CF 临时挑战 / Vercel runner IP 信誉低 / 后端短暂抖动)
158+
// 不应该把 commit 进 git 的好数据冲成空数组上线。
159+
//
160+
// 三种情况:
161+
// 1. 文件已存在 + 内容是非空数组 → 保留旧数据 exit 0
162+
// 2. 文件已存在但是空数组 / 不是数组 → 维持原状 exit 0(不主动覆盖)
163+
// 3. 文件不存在(首次 build / 干净 cache)→ 写空数组兜底 exit 0
164+
let preservedExisting = false;
165+
try {
166+
const existing = await fs.readFile(outputAbs, "utf-8");
167+
try {
168+
const parsed = JSON.parse(existing);
169+
if (Array.isArray(parsed) && parsed.length > 0) {
170+
console.warn(
171+
`[generate-leaderboard] 后端不可用,但保留 ${OUTPUT} 已有 ${parsed.length} 条数据,不覆盖。 | Backend unreachable; keeping existing leaderboard with ${parsed.length} entries.`,
172+
);
173+
} else {
174+
console.warn(
175+
`[generate-leaderboard] 后端不可用,且 ${OUTPUT} 已有内容非有效非空数组,维持原状。`,
176+
);
177+
}
178+
preservedExisting = true;
179+
} catch {
180+
// 文件存在但 JSON 损坏:当作没有,走下面写空兜底
181+
console.warn(
182+
`[generate-leaderboard] ${OUTPUT} 已存在但 JSON 解析失败,按"首次 build"兜底覆盖空数组。`,
183+
);
184+
}
185+
} catch (readErr) {
186+
// ENOENT 等:文件不存在,走兜底
187+
if (readErr && readErr.code !== "ENOENT") {
188+
console.warn(
189+
"[generate-leaderboard] 读取既有 leaderboard 失败:",
190+
readErr instanceof Error ? readErr.message : readErr,
191+
);
192+
}
193+
}
194+
195+
if (preservedExisting) {
196+
process.exit(0);
197+
}
198+
199+
// 文件不存在 / 损坏:写空兜底,避免后续 Next import 抛 ENOENT
156200
console.error(
157-
"[generate-leaderboard] 后端不可用,写入空榜单以放行构建。 | Backend unreachable, writing empty leaderboard to unblock build.",
201+
"[generate-leaderboard] 后端不可用且本地无可保留数据,写入空榜单以放行构建。 | Backend unreachable and no existing data, writing empty leaderboard to unblock build.",
158202
);
159-
// mkdir + writeFile 必须放同一个 try:任一步失败都意味着 generated/site-leaderboard.json
160-
// 不存在,后续 Next 端 import 会抛更难定位的 ENOENT。这种情况 build 必须 fail-fast,
161-
// 不能 exit 0 让"看起来一切正常"的 deploy 把站点搞挂。
162203
try {
163204
await ensureParentDir(outputAbs);
164205
await fs.writeFile(outputAbs, "[]", "utf-8");

0 commit comments

Comments
 (0)