From 4edd0ce2a4ec3ed4b4aa17b8a2c5c07d9912ffbd Mon Sep 17 00:00:00 2001 From: hex2077 Date: Wed, 7 Jan 2026 22:22:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BD=91=E7=BB=9C=E9=94=99=E8=AF=AF=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=9C=BA=E5=88=B6=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在common.js中定义可重试网络错误列表和检查函数 修改各API服务(qwen/iflow/claude/gemini/openai/antigravity)调用逻辑 添加网络错误检测和指数退避重试机制 统一错误日志格式包含状态码和错误标识 --- src/claude/claude-core.js | 70 +++++++++++++++++++++++-------- src/claude/claude-kiro.js | 77 ++++++++++++++++++++++++++-------- src/common.js | 37 ++++++++++++++++ src/gemini/antigravity-core.js | 71 +++++++++++++++++++++++-------- src/gemini/gemini-core.js | 63 +++++++++++++++++++++------- src/oauth-handlers.js | 26 +++++------- src/openai/iflow-core.js | 51 ++++------------------ src/openai/openai-core.js | 56 +++++++++++++++++++------ src/openai/qwen-core.js | 17 +++++++- 9 files changed, 328 insertions(+), 140 deletions(-) diff --git a/src/claude/claude-core.js b/src/claude/claude-core.js index 7dd8ea2..44bb45f 100644 --- a/src/claude/claude-core.js +++ b/src/claude/claude-core.js @@ -2,6 +2,7 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; import { configureAxiosProxy } from '../proxy-utils.js'; +import { isRetryableNetworkError } from '../common.js'; /** * Claude API Core Service Class. @@ -75,36 +76,52 @@ export class ClaudeApiService { * @returns {Promise} API response data. */ async callApi(endpoint, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES; - const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const response = await this.client.post(endpoint, body); return response.data; } catch (error) { + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + // 对于 Claude API,401 通常意味着 API Key 无效,不进行重试 - if (error.response?.status === 401 || error.response?.status === 403) { - console.error(`[API] Received ${error.response.status}. API Key might be invalid or expired.`); + if (status === 401 || status === 403) { + console.error(`[Claude API] Received ${status}. API Key might be invalid or expired.`); throw error; } // 处理 429 (Too Many Requests) 与指数退避 - if (error.response?.status === 429 && retryCount < maxRetries) { + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Claude API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } // 处理其他可重试错误 (5xx 服务器错误) - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { + if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${error.response.status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Claude API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } - console.error("[ClaudeApiService] Error calling API:", error.response ? error.response.data : error.message); + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Claude API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(endpoint, body, isRetry, retryCount + 1); + } + + console.error(`[Claude API] Error calling API (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message); throw error; } } @@ -118,8 +135,8 @@ export class ClaudeApiService { * @returns {AsyncIterable} API response stream. */ async *streamApi(endpoint, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES; - const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const response = await this.client.post(endpoint, { ...body, stream: true }, { responseType: 'stream' }); @@ -155,31 +172,48 @@ export class ClaudeApiService { } } } catch (error) { + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + // 对于 Claude API,401 通常意味着 API Key 无效,不进行重试 - if (error.response?.status === 401 || error.response?.status === 403) { - console.error(`[API] Received ${error.response.status} during stream. API Key might be invalid or expired.`); + if (status === 401 || status === 403) { + console.error(`[Claude API] Received ${status} during stream. API Key might be invalid or expired.`); throw error; } // 处理 429 (Too Many Requests) 与指数退避 - if (error.response?.status === 429 && retryCount < maxRetries) { + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Claude API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; } // 处理其他可重试错误 (5xx 服务器错误) - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { + if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${error.response.status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Claude API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; } - console.error("[ClaudeApiService] Error generating content stream:", error.response ? error.response.data : error.message); + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Claude API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); + return; + } + + console.error(`[Claude API] Error generating content stream (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message); throw error; } } diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 233c97e..78d5496 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -9,6 +9,7 @@ import * as https from 'https'; import { getProviderModels } from '../provider-models.js'; import { countTokens } from '@anthropic-ai/tokenizer'; import { configureAxiosProxy } from '../proxy-utils.js'; +import { isRetryableNetworkError } from '../common.js'; const KIRO_CONSTANTS = { REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', @@ -1051,7 +1052,14 @@ async initializeAuth(forceRefresh = false) { const response = await this.axiosInstance.post(requestUrl, requestData, { headers }); return response; } catch (error) { - if (error.response?.status === 403 && !isRetry) { + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + + if (status === 403 && !isRetry) { console.log('[Kiro] Received 403. Attempting token refresh and retrying...'); try { await this.initializeAuth(true); // Force refresh token @@ -1063,22 +1071,31 @@ async initializeAuth(forceRefresh = false) { } // Handle 429 (Too Many Requests) with exponential backoff - if (error.response?.status === 429 && retryCount < maxRetries) { + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); console.log(`[Kiro] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); - await new Promise(resolve => setTimeout(resolve, delay)); - return this.callApi(method, model, body, isRetry, retryCount + 1); - } - - // Handle other retryable errors (5xx server errors) - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Kiro] Received ${error.response.status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, model, body, isRetry, retryCount + 1); } - console.error('[Kiro] API call failed:', error.message); + // Handle other retryable errors (5xx server errors) + if (status >= 500 && status < 600 && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`[Kiro] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(method, model, body, isRetry, retryCount + 1); + } + + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Kiro] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(method, model, body, isRetry, retryCount + 1); + } + + console.error(`[Kiro] API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); throw error; } } @@ -1347,22 +1364,48 @@ async initializeAuth(forceRefresh = false) { stream.destroy(); } - if (error.response?.status === 403 && !isRetry) { + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + + if (status === 403 && !isRetry) { console.log('[Kiro] Received 403 in stream. Attempting token refresh and retrying...'); await this.initializeAuth(true); yield* this.streamApiReal(method, model, body, true, retryCount); return; } - if (error.response?.status === 429 && retryCount < maxRetries) { + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Kiro] Received 429 in stream. Retrying in ${delay}ms...`); - await new Promise(resolve => setTimeout(resolve, delay)); - yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1); + console.log(`[Kiro] Received 429 in stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1); return; } - console.error('[Kiro] Stream API call failed:', error.message); + // Handle 5xx server errors with exponential backoff + if (status >= 500 && status < 600 && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + console.log(`[Kiro] Received ${status} server error in stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1); + return; + } + + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Kiro] Network error (${errorIdentifier}) in stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1); + return; + } + + console.error(`[Kiro] Stream API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); throw error; } finally { // 确保流被关闭,释放资源 diff --git a/src/common.js b/src/common.js index e5cd676..7b1928a 100644 --- a/src/common.js +++ b/src/common.js @@ -5,6 +5,43 @@ import * as crypto from 'crypto'; // Import crypto for MD5 hashing import { convertData, getOpenAIStreamChunkStop } from './convert.js'; import { ProviderStrategyFactory } from './provider-strategies.js'; +// ==================== 网络错误处理 ==================== + +/** + * 可重试的网络错误标识列表 + * 这些错误可能出现在 error.code 或 error.message 中 + */ +export const RETRYABLE_NETWORK_ERRORS = [ + 'ECONNRESET', // 连接被重置 + 'ETIMEDOUT', // 连接超时 + 'ECONNREFUSED', // 连接被拒绝 + 'ENOTFOUND', // DNS 解析失败 + 'ENETUNREACH', // 网络不可达 + 'EHOSTUNREACH', // 主机不可达 + 'EPIPE', // 管道破裂 + 'EAI_AGAIN', // DNS 临时失败 + 'ECONNABORTED', // 连接中止 + 'ESOCKETTIMEDOUT', // Socket 超时 +]; + +/** + * 检查是否为可重试的网络错误 + * @param {Error} error - 错误对象 + * @returns {boolean} - 是否为可重试的网络错误 + */ +export function isRetryableNetworkError(error) { + if (!error) return false; + + const errorCode = error.code || ''; + const errorMessage = error.message || ''; + + return RETRYABLE_NETWORK_ERRORS.some(errId => + errorCode === errId || errorMessage.includes(errId) + ); +} + +// ==================== API 常量 ==================== + export const API_ACTIONS = { GENERATE_CONTENT: 'generateContent', STREAM_GENERATE_CONTENT: 'streamGenerateContent', diff --git a/src/gemini/antigravity-core.js b/src/gemini/antigravity-core.js index 37db51e..165bdc9 100644 --- a/src/gemini/antigravity-core.js +++ b/src/gemini/antigravity-core.js @@ -9,7 +9,7 @@ import * as os from 'os'; import * as readline from 'readline'; import { v4 as uuidv4 } from 'uuid'; import open from 'open'; -import { formatExpiryTime } from '../common.js'; +import { formatExpiryTime, isRetryableNetworkError } from '../common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; @@ -991,15 +991,22 @@ export class AntigravityApiService { const res = await this.authClient.request(requestOptions); return res.data; } catch (error) { - console.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, error.response?.status, error.message); + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + + console.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, status, error.message); - if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) { + if ((status === 400 || status === 401) && !isRetry) { console.log('[Antigravity API] Received 401/400. Refreshing auth and retrying...'); await this.initializeAuth(true); return this.callApi(method, body, true, retryCount, baseURLIndex); } - if (error.response?.status === 429) { + if (status === 429) { if (baseURLIndex + 1 < this.baseURLs.length) { console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`); return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1); @@ -1011,14 +1018,24 @@ export class AntigravityApiService { } } - if (!error.response && baseURLIndex + 1 < this.baseURLs.length) { - console.log(`[Antigravity API] Network error on ${baseURL}. Trying next base URL...`); - return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1); + // Handle network errors - try next base URL first, then retry with backoff + if (isNetworkError) { + if (baseURLIndex + 1 < this.baseURLs.length) { + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Antigravity API] Network error (${errorIdentifier}) on ${baseURL}. Trying next base URL...`); + return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1); + } else if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Antigravity API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(method, body, isRetry, retryCount + 1, 0); + } } - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { + if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Antigravity API] Server error ${error.response.status}. Retrying in ${delay}ms...`); + console.log(`[Antigravity API] Server error ${status}. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, baseURLIndex); } @@ -1063,16 +1080,23 @@ export class AntigravityApiService { yield* this.parseSSEStream(res.data); } catch (error) { - console.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, error.response?.status, error.message); + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + + console.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, status, error.message); - if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) { + if ((status === 400 || status === 401) && !isRetry) { console.log('[Antigravity API] Received 401/400 during stream. Refreshing auth and retrying...'); await this.initializeAuth(true); yield* this.streamApi(method, body, true, retryCount, baseURLIndex); return; } - if (error.response?.status === 429) { + if (status === 429) { if (baseURLIndex + 1 < this.baseURLs.length) { console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`); yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1); @@ -1086,15 +1110,26 @@ export class AntigravityApiService { } } - if (!error.response && baseURLIndex + 1 < this.baseURLs.length) { - console.log(`[Antigravity API] Network error on ${baseURL}. Trying next base URL...`); - yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1); - return; + // Handle network errors - try next base URL first, then retry with backoff + if (isNetworkError) { + if (baseURLIndex + 1 < this.baseURLs.length) { + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Antigravity API] Network error (${errorIdentifier}) on ${baseURL} during stream. Trying next base URL...`); + yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1); + return; + } else if (retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Antigravity API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(method, body, isRetry, retryCount + 1, 0); + return; + } } - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { + if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Antigravity API] Server error ${error.response.status} during stream. Retrying in ${delay}ms...`); + console.log(`[Antigravity API] Server error ${status} during stream. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, baseURLIndex); return; diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js index 065ff28..da494d4 100644 --- a/src/gemini/gemini-core.js +++ b/src/gemini/gemini-core.js @@ -6,7 +6,7 @@ import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; import open from 'open'; -import { API_ACTIONS, formatExpiryTime } from '../common.js'; +import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError } from '../common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiCliOAuth } from '../oauth-handlers.js'; import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; @@ -436,27 +436,43 @@ export class GeminiApiService { const res = await this.authClient.request(requestOptions); return res.data; } catch (error) { - console.error(`[API] Error calling ${method}:`, error.response?.status, error.message); + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + + console.error(`[Gemini API] Error calling ${method}:`, status, error.message); // Handle 401 (Unauthorized) - refresh auth and retry once - if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) { - console.log('[API] Received 401/400. Refreshing auth and retrying...'); + if ((status === 400 || status === 401) && !isRetry) { + console.log('[Gemini API] Received 401/400. Refreshing auth and retrying...'); await this.initializeAuth(true); return this.callApi(method, body, true, retryCount); } // Handle 429 (Too Many Requests) with exponential backoff - if (error.response?.status === 429 && retryCount < maxRetries) { + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Gemini API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1); } // Handle other retryable errors (5xx server errors) - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { + if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${error.response.status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Gemini API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(method, body, isRetry, retryCount + 1); + } + + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Gemini API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1); } @@ -486,29 +502,46 @@ export class GeminiApiService { } yield* this.parseSSEStream(res.data); } catch (error) { - console.error(`[API] Error during stream ${method}:`, error.response?.status, error.message); + const status = error.response?.status; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + + console.error(`[Gemini API] Error during stream ${method}:`, status, error.message); // Handle 401 (Unauthorized) - refresh auth and retry once - if ((error.response?.status === 400 || error.response?.status === 401) && !isRetry) { - console.log('[API] Received 401/400 during stream. Refreshing auth and retrying...'); + if ((status === 400 || status === 401) && !isRetry) { + console.log('[Gemini API] Received 401/400 during stream. Refreshing auth and retrying...'); await this.initializeAuth(true); yield* this.streamApi(method, body, true, retryCount); return; } // Handle 429 (Too Many Requests) with exponential backoff - if (error.response?.status === 429 && retryCount < maxRetries) { + if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Gemini API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1); return; } // Handle other retryable errors (5xx server errors) - if (error.response?.status >= 500 && error.response?.status < 600 && retryCount < maxRetries) { + if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${error.response.status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[Gemini API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(method, body, isRetry, retryCount + 1); + return; + } + + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[Gemini API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1); return; diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index 6a8f332..8069b4e 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -1290,15 +1290,12 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { // 3. 组合完整的凭据数据 const credentialsData = { - accessToken: tokenData.accessToken, - refreshToken: tokenData.refreshToken, - tokenType: tokenData.tokenType, + access_token: tokenData.accessToken, + refresh_token: tokenData.refreshToken, + expiry_date: new Date(tokenData.expiresAt).getTime(), + token_type: tokenData.tokenType, scope: tokenData.scope, - expiresAt: tokenData.expiresAt, - apiKey: userInfo.apiKey, - email: userInfo.email, - lastRefresh: new Date().toISOString(), - type: 'iflow' + apiKey: userInfo.apiKey }; // 4. 保存凭据 @@ -1474,14 +1471,11 @@ export async function refreshIFlowTokens(refreshToken) { const userInfo = await fetchIFlowUserInfo(tokenData.access_token); return { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - tokenType: tokenData.token_type, + access_token: tokenData.access_token, + refresh_token: tokenData.refresh_token, + expiry_date: Date.now() + tokenData.expires_in * 1000, + token_type: tokenData.token_type, scope: tokenData.scope, - expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString(), - apiKey: userInfo.apiKey, - email: userInfo.email, - lastRefresh: new Date().toISOString(), - type: 'iflow' + apiKey: userInfo.apiKey }; } \ No newline at end of file diff --git a/src/openai/iflow-core.js b/src/openai/iflow-core.js index 1574352..841c382 100644 --- a/src/openai/iflow-core.js +++ b/src/openai/iflow-core.js @@ -23,6 +23,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; import { configureAxiosProxy } from '../proxy-utils.js'; +import { isRetryableNetworkError } from '../common.js'; // iFlow API 端点 const IFLOW_API_BASE_URL = 'https://apis.iflow.cn/v1'; @@ -71,10 +72,8 @@ class IFlowTokenStorage { this.refreshToken = data.refreshToken || data.refresh_token || ''; this.expiryDate = data.expiryDate || data.expiry_date || ''; this.apiKey = data.apiKey || data.api_key || ''; - this.email = data.email || ''; this.tokenType = data.tokenType || data.token_type || ''; this.scope = data.scope || ''; - this.type = data.type || 'iflow'; } /** @@ -87,9 +86,7 @@ class IFlowTokenStorage { expiry_date: this.expiryDate, token_type: this.tokenType, scope: this.scope, - apiKey: this.apiKey, - email: this.email, - type: this.type + apiKey: this.apiKey }; } @@ -743,24 +740,8 @@ export class IFlowApiService { const errorCode = error.code; const errorMessage = error.message || ''; - // 定义可重试的网络错误标识(可能出现在 code 或 message 中) - const retryableNetworkErrors = [ - 'ECONNRESET', // 连接被重置 - 'ETIMEDOUT', // 连接超时 - 'ECONNREFUSED', // 连接被拒绝 - 'ENOTFOUND', // DNS 解析失败 - 'ENETUNREACH', // 网络不可达 - 'EHOSTUNREACH', // 主机不可达 - 'EPIPE', // 管道破裂 - 'EAI_AGAIN', // DNS 临时失败 - 'ECONNABORTED', // 连接中止 - 'ESOCKETTIMEDOUT', // Socket 超时 - ]; - - // 检查是否为可重试的网络错误(检查 code 和 message) - const isRetryableNetworkError = retryableNetworkErrors.some(errId => - errorCode === errId || errorMessage.includes(errId) - ); + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); if (status === 401 || status === 403) { console.error(`[iFlow] Received ${status}. API Key might be invalid or expired.`); @@ -784,7 +765,7 @@ export class IFlowApiService { } // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff - if (isRetryableNetworkError && retryCount < maxRetries) { + if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); console.log(`[iFlow] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); @@ -897,24 +878,8 @@ export class IFlowApiService { const errorCode = error.code; const errorMessage = error.message || ''; - // 定义可重试的网络错误标识(可能出现在 code 或 message 中) - const retryableNetworkErrors = [ - 'ECONNRESET', // 连接被重置 - 'ETIMEDOUT', // 连接超时 - 'ECONNREFUSED', // 连接被拒绝 - 'ENOTFOUND', // DNS 解析失败 - 'ENETUNREACH', // 网络不可达 - 'EHOSTUNREACH', // 主机不可达 - 'EPIPE', // 管道破裂 - 'EAI_AGAIN', // DNS 临时失败 - 'ECONNABORTED', // 连接中止 - 'ESOCKETTIMEDOUT', // Socket 超时 - ]; - - // 检查是否为可重试的网络错误(检查 code 和 message) - const isRetryableNetworkError = retryableNetworkErrors.some(errId => - errorCode === errId || errorMessage.includes(errId) - ); + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); if (status === 401 || status === 403) { console.error(`[iFlow] Received ${status} during stream. API Key might be invalid or expired.`); @@ -940,7 +905,7 @@ export class IFlowApiService { } // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff - if (isRetryableNetworkError && retryCount < maxRetries) { + if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); console.log(`[iFlow] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); diff --git a/src/openai/openai-core.js b/src/openai/openai-core.js index c22a164..ca4812d 100644 --- a/src/openai/openai-core.js +++ b/src/openai/openai-core.js @@ -2,6 +2,7 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; import { configureAxiosProxy } from '../proxy-utils.js'; +import { isRetryableNetworkError } from '../common.js'; // Assumed OpenAI API specification service for interacting with third-party models export class OpenAIApiService { @@ -51,8 +52,8 @@ export class OpenAIApiService { } async callApi(endpoint, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES; - const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { const response = await this.axiosInstance.post(endpoint, body); @@ -60,15 +61,21 @@ export class OpenAIApiService { } catch (error) { const status = error.response?.status; const data = error.response?.data; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + if (status === 401 || status === 403) { - console.error(`[API] Received ${status}. API Key might be invalid or expired.`); + console.error(`[OpenAI API] Received ${status}. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[OpenAI API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } @@ -76,19 +83,28 @@ export class OpenAIApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[OpenAI API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } - console.error(`Error calling OpenAI API (Status: ${status}):`, data || error.message); + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[OpenAI API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApi(endpoint, body, isRetry, retryCount + 1); + } + + console.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } async *streamApi(endpoint, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES; - const baseDelay = this.config.REQUEST_BASE_DELAY; // 1 second base delay + const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; + const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay // OpenAI 的流式请求需要将 stream 设置为 true const streamRequestBody = { ...body, stream: true }; @@ -127,15 +143,21 @@ export class OpenAIApiService { } catch (error) { const status = error.response?.status; const data = error.response?.data; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); + if (status === 401 || status === 403) { - console.error(`[API] Received ${status} during stream. API Key might be invalid or expired.`); + console.error(`[OpenAI API] Received ${status} during stream. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[OpenAI API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -144,13 +166,23 @@ export class OpenAIApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + console.log(`[OpenAI API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; } - console.error(`Error calling OpenAI streaming API (Status: ${status}):`, data || error.message); + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[OpenAI API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); + return; + } + + console.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } diff --git a/src/openai/qwen-core.js b/src/openai/qwen-core.js index 7ff8e36..43dd8bd 100644 --- a/src/openai/qwen-core.js +++ b/src/openai/qwen-core.js @@ -11,6 +11,7 @@ import { randomUUID } from 'node:crypto'; import { getProviderModels } from '../provider-models.js'; import { handleQwenOAuth } from '../oauth-handlers.js'; import { configureAxiosProxy } from '../proxy-utils.js'; +import { isRetryableNetworkError } from '../common.js'; // --- Constants --- const QWEN_DIR = '.qwen'; @@ -550,6 +551,11 @@ export class QwenApiService { } catch (error) { const status = error.response?.status; const data = error.response?.data || error.message; + const errorCode = error.code; + const errorMessage = error.message || ''; + + // 检查是否为可重试的网络错误 + const isNetworkError = isRetryableNetworkError(error); if (this.isAuthError(error) && retryCount === 0) { console.warn(`[QwenApiService] Auth error (${status}). Refreshing token...`); @@ -573,7 +579,16 @@ export class QwenApiService { return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1); } - console.error(`Error calling Qwen API (Status: ${status}):`, data); + // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff + if (isNetworkError && retryCount < maxRetries) { + const delay = baseDelay * Math.pow(2, retryCount); + const errorIdentifier = errorCode || errorMessage.substring(0, 50); + console.log(`[QwenApiService] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1); + } + + console.error(`[QwenApiService] Error calling API (Status: ${status}, Code: ${errorCode}):`, data); throw error; } }