@@ -82,8 +82,12 @@ async function ensureParentDir(filePath) {
8282 await fs . mkdir ( dir , { recursive : true } ) ;
8383}
8484
85+ // 后端响应超时上限:Vercel build 单步通常 5min 内,给后端 15s 足够
86+ // (Caffeine 命中是毫秒级,未命中走 JDBC 全表扫描也就秒级)。超时即降级。
87+ const FETCH_TIMEOUT_MS = 15_000 ;
88+
8589/**
86- * 拉后端聚合数据。任何错误都返回 null,让调用方决定降级策略
90+ * 拉后端聚合数据。任何错误(含超时)都返回 null,让调用方决定降级策略
8791 * (生成空榜单放行 build vs. 整个失败)。
8892 *
8993 * 后端 ApiResponse 形如 { success, message, data },data 是 LeaderboardEntryDto[]。
@@ -92,12 +96,16 @@ async function fetchAggregatedFromBackend() {
9296 console . log (
9397 `[generate-leaderboard] 拉聚合数据:${ LEADERBOARD_API_URL } | Fetching aggregated contributions from backend...` ,
9498 ) ;
99+ // AbortController 超时:防止后端 TCP 建立后不返回时 build 无限挂起
100+ const controller = new AbortController ( ) ;
101+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , FETCH_TIMEOUT_MS ) ;
95102 try {
96103 const res = await fetch ( LEADERBOARD_API_URL , {
97104 headers : {
98105 accept : "application/json" ,
99106 "user-agent" : "InvolutionHell-build/1.0 (generate-leaderboard.mjs)" ,
100107 } ,
108+ signal : controller . signal ,
101109 } ) ;
102110 if ( ! res . ok ) {
103111 console . error (
@@ -115,11 +123,19 @@ async function fetchAggregatedFromBackend() {
115123 }
116124 return data ;
117125 } catch ( err ) {
118- console . error (
119- "[generate-leaderboard] 调用后端失败:" ,
120- err instanceof Error ? err . message : err ,
121- ) ;
126+ if ( err && err . name === "AbortError" ) {
127+ console . error (
128+ `[generate-leaderboard] 后端响应超时(${ FETCH_TIMEOUT_MS } ms),降级为空榜单` ,
129+ ) ;
130+ } else {
131+ console . error (
132+ "[generate-leaderboard] 调用后端失败:" ,
133+ err instanceof Error ? err . message : err ,
134+ ) ;
135+ }
122136 return null ;
137+ } finally {
138+ clearTimeout ( timeoutId ) ;
123139 }
124140}
125141
@@ -132,13 +148,20 @@ async function main() {
132148 console . error (
133149 "[generate-leaderboard] 后端不可用,写入空榜单以放行构建。 | Backend unreachable, writing empty leaderboard to unblock build." ,
134150 ) ;
135- await ensureParentDir ( outputAbs ) ;
151+ // mkdir + writeFile 必须放同一个 try:任一步失败都意味着 generated/site-leaderboard.json
152+ // 不存在,后续 Next 端 import 会抛更难定位的 ENOENT。这种情况 build 必须 fail-fast,
153+ // 不能 exit 0 让"看起来一切正常"的 deploy 把站点搞挂。
136154 try {
155+ await ensureParentDir ( outputAbs ) ;
137156 await fs . writeFile ( outputAbs , "[]" , "utf-8" ) ;
138- } catch {
139- // ignore
157+ process . exit ( 0 ) ;
158+ } catch ( err ) {
159+ console . error (
160+ "[generate-leaderboard] 写入空榜单失败,无法继续放行构建:" ,
161+ err instanceof Error ? err . stack || err . message : err ,
162+ ) ;
163+ process . exit ( 1 ) ;
140164 }
141- process . exit ( 0 ) ;
142165 }
143166
144167 // 构建 docId → {title, url} 映射,从 .source/index.ts 提取(Fumadocs 生成的 manifest)
0 commit comments