Explorar el Código

fix(book-generator): timeout/网络错误自动切换 LLM 供应商

问题:rich_outline 节点 HTTP 5分钟超时后,shouldSwitchModel 未识别
'Request timed out' 等网络错误,导致系统在同供应商上空转重试,
book 卡在 outlining 阶段 4 天不恢复。

修复:
- shouldSwitchModel 增加 11 个网络/超时模式(timed out/ETIMEDOUT/...)
- trySwitchModel 新增网络错误直接切换分支,不等熔断器积累 3 次失败
- HTTP timeout 5分钟 -> 3分钟(更快触发切换)
- rich_outline 节点超时 8分钟 -> 12分钟(给重试留余量)
caoyg hace 2 meses
padre
commit
66c807999a

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
server/logs/requests.json


+ 5 - 1
server/src/config/index.ts

@@ -59,13 +59,17 @@ function shouldSwitchModel(error: any): boolean {
   if (nonSwitchablePatterns.some(p => message.includes(p))) return false;
   if (status === 401) return false; // 认证失败
 
-  // 可切换的错误:限流、余额不足、服务不可用、模型不存在
+  // 可切换的错误:限流、余额不足、服务不可用、模型不存在、网络/超时
   const switchablePatterns = [
     'rate limit', 'rate_limit', 'too many requests', '请求过于频繁',
     'quota', 'balance', 'insufficient', 'usage limit',
     'model not found', 'model not support', 'does not exist', 'invalid model',
     'service unavailable', 'bad gateway', 'gateway timeout',
     'internal server error',
+    // 网络/超时错误 - 切换供应商是唯一恢复手段
+    'timed out', 'timeout', 'request timeout',
+    'etimedout', 'esockettimedout', 'econnreset', 'econnrefused',
+    'enotfound', 'fetch failed', 'aborted', 'eai_again',
   ];
   if (switchablePatterns.some(p => message.includes(p))) return true;
 

+ 1 - 1
server/src/modules/book-generator/fault-tolerance.ts

@@ -26,7 +26,7 @@ export const FAULT_TOLERANCE_CONFIG = {
   nodeTimeout: {
     deep_plan: 3 * 60 * 1000,          // 深度规划:3分钟
     generate_outline: 5 * 60 * 1000,    // 大纲生成:5分钟
-    rich_outline: 8 * 60 * 1000,        // 富信息大纲:8分钟
+    rich_outline: 12 * 60 * 1000,       // 富信息大纲:12分钟(给供应商切换+重试留余量)
     generate_sections: 10 * 60 * 1000,  // 节生成:10分钟
     generate_subsections: 15 * 60 * 1000, // 小节生成:15分钟
     write_chapters: 30 * 60 * 1000,     // 内容生成:30分钟

+ 14 - 1
server/src/services/llm/index.ts

@@ -497,6 +497,8 @@ export function clearModelCache() {
  * 检查错误是否需要切换供应商
  * 调度策略:同 Key 换模型无意义(额度共享),直接切换到下一个供应商
  * 优先查找同名/别名模型(canonicalModel),保持模型一致性
+ *
+ * 优化:timeout 类错误直接触发切换(不依赖熔断器),避免卡在死掉的供应商上
  */
 function trySwitchModel(currentModelId: string, error: any): string | null {
   const errorMessage = error?.message || '';
@@ -516,7 +518,18 @@ function trySwitchModel(currentModelId: string, error: any): string | null {
     return switchToNextVendorModel(currentModelId);
   }
 
-  // 3. 可恢复错误,直接切换到下一个供应商
+  // 3. 网络/超时错误:直接切换供应商(不等熔断器积累 3 次失败)
+  const NETWORK_ERROR_PATTERNS = [
+    'timed out', 'timeout', 'request timeout',
+    'etimedout', 'esockettimedout', 'econnreset', 'econnrefused',
+    'enotfound', 'fetch failed', 'aborted', 'eai_again',
+  ];
+  if (NETWORK_ERROR_PATTERNS.some(p => errorMessage.toLowerCase().includes(p))) {
+    console.log(`[LLM] 检测到网络/超时错误,立即切换供应商: ${errorMessage.substring(0, 80)}`);
+    return switchToNextVendorModel(currentModelId);
+  }
+
+  // 4. 可恢复错误,调用通用判断(限流/余额/服务不可用等)
   if (config.models.shouldSwitchModel(errorMessage)) {
     return switchToNextVendorModel(currentModelId);
   }

+ 3 - 2
server/src/services/llm/provider.registry.ts

@@ -61,8 +61,9 @@ class OpenAiCompatibleLlmProvider implements ILlmProvider {
       apiKey: this._apiKey,
       temperature: options?.temperature ?? modelCfg.temperature,
       maxTokens: cappedMaxTokens,
-      // 显式 HTTP 超时:默认无超时,请求可能挂死。设置为 5 分钟覆盖大多数生成场景。
-      timeout: 5 * 60 * 1000,
+      // 显式 HTTP 超时:3 分钟覆盖大多数单次生成场景。
+      // 富信息大纲在 16000 maxTokens 下通常 30-60s 完成;超时立即触发供应商切换。
+      timeout: 3 * 60 * 1000,
       configuration: { baseURL: this.baseUrl },
     });
   }

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio