Skip to content

feat(leaderboard): 脚本改走后端 /api/public/leaderboard,断 prisma 直连 DB#322

Merged
longsizhuo merged 2 commits intomainfrom
feat/leaderboard-via-backend-api
Apr 26, 2026
Merged

feat(leaderboard): 脚本改走后端 /api/public/leaderboard,断 prisma 直连 DB#322
longsizhuo merged 2 commits intomainfrom
feat/leaderboard-via-backend-api

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

⚠️ 部署依赖

必须先合并并部署InvolutionHell/involutionhell-backend#22

backend endpoint 没上线时合本 PR,build 会走 fallback(写空榜单),排行榜短暂为空。

背景

`generate-leaderboard.mjs` 之前 prisma 直连 Postgres 5432 拉 `doc_contributors`,逼着 DB 端口对公网开放(且服务端没开 SSL)。本周 Vercel preview build 因为 `DATABASE_URL` 凭证失效全军覆没,触发架构整改。方案 B:DB 收回内网,脚本走后端 endpoint。

改动

`scripts/generate-leaderboard.mjs`

  • 删 prisma client + `pg.Pool` 直连,换成 `fetch ${BACKEND_URL}/api/public/leaderboard`
  • 后端不可达时降级写空数组放行 build(不挂整个 deploy)
  • 保留所有本地处理逻辑:`.source/index.ts` → docId→title/url 映射、git log noreply 反推 login、前 100 名 GitHub API 兜底
  • 后端响应兼容 `ApiResponse` 包装和裸数组两种结构

`lib/db.ts`

  • 删除(早就是死代码:grep 验证 0 引用,frontend runtime 早就不直连 DB)

`docs/architecture/frontend-backend-separation.md`

  • 在"文档贡献数据源"小节下补迁移记录 + 环境变量说明

部署后续清理(不在本 PR)

  • frontend Vercel env 删 `DATABASE_URL`(preview / production 都删)
  • 服务器执行 `docker compose up -d postgres` 让 5432 绑定到 127.0.0.1 生效
  • Oracle Cloud 安全组关 5432 入站

Test plan

  • tsc 类型检查通过
  • vitest 通过(lint-staged 跑过)
  • 本地 smoke test:后端不可达时正确写空数组、退出码 0
  • backend PR 合并部署后,本地跑 `pnpm prebuild` 端到端验证
  • PR Vercel preview 通过

依赖:backend PR InvolutionHell/involutionhell-backend#22 必须先合并 + 部署。

背景
generate-leaderboard.mjs 之前 prisma 直连 Postgres 5432,逼着 DB 端口对公网开放
(且服务端没开 SSL)。本周 Vercel preview build 因为 DATABASE_URL 凭证失效全军覆没。
方案 B:DB 收回内网,脚本走后端 endpoint。

改动
scripts/generate-leaderboard.mjs:
- 删 prisma client + pg.Pool 直连,换成 fetch ${BACKEND_URL}/api/public/leaderboard
  (兼容 LEADERBOARD_API_URL 完整覆盖;都没配则走 https://api.involutionhell.com)
- 后端不可达时降级写空数组放行 build (不挂整个 deploy)
- 保留所有本地处理逻辑:.source/index.ts → docId→title/url 映射、git log noreply
  反推 login、前 100 名 GitHub API 兜底
- 后端响应兼容 ApiResponse 包装和裸数组两种结构

lib/db.ts:
- 删除(早就是死代码:grep 验证 0 引用,frontend runtime 不直连 DB)

docs/architecture/frontend-backend-separation.md:
- 在"文档贡献数据源"小节下补迁移记录 + 环境变量说明

后续清理(不在本 PR)
- frontend Vercel env 删 DATABASE_URL(preview/production 都删)
- 服务器执行 docker compose up -d postgres 让 5432 收回 127.0.0.1
- Oracle Cloud 安全组关 5432 入站
Copilot AI review requested due to automatic review settings April 26, 2026 11:59
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 26, 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 26, 2026 0:34am
website-preview Ready Ready Preview, Comment Apr 26, 2026 0:34am

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 migrates the build-time leaderboard generation away from direct Postgres access (Prisma/pg) to a backend public API, so the database can be kept private and frontend builds are less brittle.

Changes:

  • Update scripts/generate-leaderboard.mjs to fetch aggregated leaderboard data from GET /api/public/leaderboard, with a fallback to writing an empty leaderboard when the backend is unreachable.
  • Remove unused Prisma/DB singleton (lib/db.ts) to eliminate dead frontend DB linkage.
  • Document the migration and environment variables in the frontend–backend separation architecture doc.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
scripts/generate-leaderboard.mjs Switches leaderboard aggregation source from direct DB queries to backend API fetch; keeps local enrichment logic and adds a “backend unreachable” fallback.
lib/db.ts Deletes dead Prisma DB singleton previously used for direct DB connections.
docs/architecture/frontend-backend-separation.md Documents the data-source migration and the new env var setup.

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

Comment thread scripts/generate-leaderboard.mjs Outdated
Comment on lines 135 to 141
await ensureParentDir(outputAbs);
try {
// 检查是否已经存在,存在则不覆盖(或者为了容错直接写入空的 array 也行)
await fs.writeFile(outputAbs, "[]", "utf-8");
} catch (e) {
// Ignore
} catch {
// ignore
}
process.exit(0);
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

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

在后端不可达的降级分支里,ensureParentDir(outputAbs) 不在 try/catch 内:如果 mkdir 因权限/只读文件系统失败,会直接抛错并让脚本以非 0 退出,和“写空榜单放行 build”的目标冲突。建议把 ensureParentDir + writeFile 放进同一个 try/catch,并明确在失败时的退出策略(例如记录错误但仍 exit 0,或在无法落盘时 exit 1 以避免后续 Next import 因文件缺失报更难定位的错)。

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +101
async function fetchAggregatedFromBackend() {
console.log(
`[generate-leaderboard] 拉聚合数据:${LEADERBOARD_API_URL} | Fetching aggregated contributions from backend...`,
);
try {
const res = await fetch(LEADERBOARD_API_URL, {
headers: {
accept: "application/json",
"user-agent": "InvolutionHell-build/1.0 (generate-leaderboard.mjs)",
},
});
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

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

fetchAggregatedFromBackend()fetch() 没有设置超时/AbortController;如果后端网络层卡住(例如连接建立但不返回),构建可能无限期挂住。建议加一个合理的超时(如 5–15s)并在超时后返回 null 触发空榜单降级。

Copilot uses AI. Check for mistakes.
Comment thread scripts/generate-leaderboard.mjs Outdated
Comment on lines 138 to 141
} catch {
// ignore
}
process.exit(0);
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

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

降级写空榜单时把 writeFile 异常完全吞掉会掩盖真实故障:脚本 exit 0 但 generated/site-leaderboard.json 可能并未生成,后续 Next import "@/generated/site-leaderboard.json" 会以更晦涩的方式失败。建议至少输出一条明确的 error/warn,并根据是否成功写入决定是否应该继续放行构建。

Suggested change
} catch {
// ignore
}
process.exit(0);
process.exit(0);
} catch (err) {
console.error(
"[generate-leaderboard] 写入空榜单失败,无法继续放行构建:",
err instanceof Error ? err.stack || err.message : err,
);
process.exit(1);
}

Copilot uses AI. Check for mistakes.
- fetch 加 AbortController 15s 超时:防止后端 TCP 建立后不返回时 build 无限挂起
  (Vercel build 单步通常 <5min,留 15s 给后端 Caffeine 命中即毫秒,未命中也秒级)
- 降级写空榜单的 mkdir + writeFile 放同一 try/catch:任一步失败 exit 1 fail-fast
  避免文件不存在但 exit 0 让 deploy "看似正常",后续 Next import 抛 ENOENT 更难定位
@longsizhuo longsizhuo merged commit 1bf07ca into main Apr 26, 2026
8 checks passed
longsizhuo pushed a commit that referenced this pull request Apr 27, 2026
故障复盘
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 恢复正常。
longsizhuo added a commit that referenced this pull request Apr 27, 2026
…325) (#326)

* fix(leaderboard): 脚本 UA 换 Chrome 伪装规避 CF Bot Fight

故障复盘
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 恢复正常。

* 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 抖动整块消失。

* fix(leaderboard): UA 改回 InvolutionHell-SSR 让 CF Custom Rule 真正匹配

之前误判
昨天看到 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 恢复显示。

---------

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