Просмотр исходного кода

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 2 месяцев назад
Родитель
Сommit
66c807999a

Разница между файлами не показана из-за своего большого размера
+ 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 (nonSwitchablePatterns.some(p => message.includes(p))) return false;
   if (status === 401) return false; // 认证失败
   if (status === 401) return false; // 认证失败
 
 
-  // 可切换的错误:限流、余额不足、服务不可用、模型不存在
+  // 可切换的错误:限流、余额不足、服务不可用、模型不存在、网络/超时
   const switchablePatterns = [
   const switchablePatterns = [
     'rate limit', 'rate_limit', 'too many requests', '请求过于频繁',
     'rate limit', 'rate_limit', 'too many requests', '请求过于频繁',
     'quota', 'balance', 'insufficient', 'usage limit',
     'quota', 'balance', 'insufficient', 'usage limit',
     'model not found', 'model not support', 'does not exist', 'invalid model',
     'model not found', 'model not support', 'does not exist', 'invalid model',
     'service unavailable', 'bad gateway', 'gateway timeout',
     'service unavailable', 'bad gateway', 'gateway timeout',
     'internal server error',
     '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;
   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: {
   nodeTimeout: {
     deep_plan: 3 * 60 * 1000,          // 深度规划:3分钟
     deep_plan: 3 * 60 * 1000,          // 深度规划:3分钟
     generate_outline: 5 * 60 * 1000,    // 大纲生成:5分钟
     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_sections: 10 * 60 * 1000,  // 节生成:10分钟
     generate_subsections: 15 * 60 * 1000, // 小节生成:15分钟
     generate_subsections: 15 * 60 * 1000, // 小节生成:15分钟
     write_chapters: 30 * 60 * 1000,     // 内容生成:30分钟
     write_chapters: 30 * 60 * 1000,     // 内容生成:30分钟

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

@@ -497,6 +497,8 @@ export function clearModelCache() {
  * 检查错误是否需要切换供应商
  * 检查错误是否需要切换供应商
  * 调度策略:同 Key 换模型无意义(额度共享),直接切换到下一个供应商
  * 调度策略:同 Key 换模型无意义(额度共享),直接切换到下一个供应商
  * 优先查找同名/别名模型(canonicalModel),保持模型一致性
  * 优先查找同名/别名模型(canonicalModel),保持模型一致性
+ *
+ * 优化:timeout 类错误直接触发切换(不依赖熔断器),避免卡在死掉的供应商上
  */
  */
 function trySwitchModel(currentModelId: string, error: any): string | null {
 function trySwitchModel(currentModelId: string, error: any): string | null {
   const errorMessage = error?.message || '';
   const errorMessage = error?.message || '';
@@ -516,7 +518,18 @@ function trySwitchModel(currentModelId: string, error: any): string | null {
     return switchToNextVendorModel(currentModelId);
     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)) {
   if (config.models.shouldSwitchModel(errorMessage)) {
     return switchToNextVendorModel(currentModelId);
     return switchToNextVendorModel(currentModelId);
   }
   }

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

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

Некоторые файлы не были показаны из-за большого количества измененных файлов