From 0974a5e83d34fc850633d30677ed0c8d7f533d3e Mon Sep 17 00:00:00 2001 From: hex2077 Date: Fri, 16 Jan 2026 17:35:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20=E4=BD=BF=E7=94=A8=20axios=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20fetch=20=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 OAuth 处理中的网络请求模块,将原生 fetch 替换为 axios,以正确处理代理配置。axios 提供了更完善的代理支持和错误处理机制,同时保持与原有 fetch API 的兼容性。 refactor(providers): 优化健康检查逻辑 - 提前返回未启用健康检查的情况 - 仅在开启自动刷新 token 时执行快速预检 - 使用 AbortController 替代 Promise.race 实现超时控制 - 优化错误消息和日志信息 --- src/auth/oauth-handlers.js | 59 ++++++++++++++++++++---- src/providers/provider-pool-manager.js | 63 +++++++++++++------------- 2 files changed, 82 insertions(+), 40 deletions(-) diff --git a/src/auth/oauth-handlers.js b/src/auth/oauth-handlers.js index c2e578a..fb4eeb7 100644 --- a/src/auth/oauth-handlers.js +++ b/src/auth/oauth-handlers.js @@ -5,6 +5,7 @@ import path from 'path'; import os from 'os'; import crypto from 'crypto'; import open from 'open'; +import axios from 'axios'; import { broadcastEvent } from '../services/ui-manager.js'; import { autoLinkProviderConfigs } from '../services/service-manager.js'; import { CONFIG } from '../core/config-manager.js'; @@ -138,22 +139,64 @@ const activeKiroPollingTasks = new Map(); /** * 创建带代理支持的 fetch 请求 + * 使用 axios 替代原生 fetch,以正确支持代理配置 * @param {string} url - 请求 URL - * @param {Object} options - fetch 选项 + * @param {Object} options - fetch 选项(兼容 fetch API 格式) * @param {string} providerType - 提供商类型,用于获取代理配置 - * @returns {Promise} + * @returns {Promise} 返回类似 fetch Response 的对象 */ async function fetchWithProxy(url, options = {}, providerType) { const proxyConfig = getProxyConfigForProvider(CONFIG, providerType); - if (proxyConfig) { - // 根据 URL 协议选择合适的 agent - const urlObj = new URL(url); - const agent = urlObj.protocol === 'https:' ? proxyConfig.httpsAgent : proxyConfig.httpAgent; - options.dispatcher = agent; + // 构建 axios 配置 + const axiosConfig = { + url, + method: options.method || 'GET', + headers: options.headers || {}, + timeout: 30000, // 30 秒超时 + }; + + // 处理请求体 + if (options.body) { + axiosConfig.data = options.body; } - return fetch(url, options); + // 配置代理 + if (proxyConfig) { + axiosConfig.httpAgent = proxyConfig.httpAgent; + axiosConfig.httpsAgent = proxyConfig.httpsAgent; + axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent + console.log(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); + } + + try { + const response = await axios(axiosConfig); + + // 返回类似 fetch Response 的对象 + return { + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: response.statusText, + headers: response.headers, + json: async () => response.data, + text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data), + }; + } catch (error) { + // 处理 axios 错误,转换为类似 fetch 的响应格式 + if (error.response) { + // 服务器返回了错误状态码 + return { + ok: false, + status: error.response.status, + statusText: error.response.statusText, + headers: error.response.headers, + json: async () => error.response.data, + text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data), + }; + } + // 网络错误或其他错误 + throw error; + } } /** diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index cf2053b..469df17 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -866,24 +866,32 @@ export class ProviderPoolManager { * @returns {Promise<{success: boolean, modelName: string, errorMessage: string}|null>} - Health check result object or null if check not implemented. */ async _checkProviderHealth(providerType, providerConfig, forceCheck = false) { - // 确定健康检查使用的模型名称 - const modelName = providerConfig.checkModelName || - ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType]; - - // 如果未启用健康检查且不是强制检查,返回 null + // 如果未启用健康检查且不是强制检查,返回 null(提前返回,避免不必要的计算) if (!providerConfig.checkHealth && !forceCheck) { return null; } + // 确定健康检查使用的模型名称 + const modelName = providerConfig.checkModelName || + ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType]; + if (!modelName) { - this._log('warn', `Unknown provider type for health check: ${providerType}`); - return { success: false, modelName: null, errorMessage: 'Unknown provider type for health check' }; + this._log('warn', `Unknown provider type for health check: ${providerType}. Please check DEFAULT_HEALTH_CHECK_MODELS.`); + return { + success: false, + modelName: null, + errorMessage: `Unknown provider type '${providerType}'. No default health check model configured.` + }; } // ========== 快速预检:从内存缓存检查 OAuth 凭证状态 ========== // 对于 OAuth 类型的 provider,先检查 token 是否过期,避免阻塞 + // 注意:只有在开启自动刷新 token (CRON_REFRESH_TOKEN) 时才执行快速预检 + // 因为如果没有自动刷新机制,缓存中的 token 状态可能不准确 const oauthProviderTypes = ['claude-kiro-oauth', 'gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'openai-iflow-oauth', 'claude-orchids-oauth']; - if (oauthProviderTypes.includes(providerType) && providerConfig.uuid) { + const cronRefreshTokenEnabled = this.globalConfig?.CRON_REFRESH_TOKEN === true; + + if (cronRefreshTokenEnabled && oauthProviderTypes.includes(providerType) && providerConfig.uuid) { const credentialCache = CredentialCacheManager.getInstance(); const cachedEntry = credentialCache.getCredentials(providerType, providerConfig.uuid); @@ -916,19 +924,10 @@ export class ProviderPoolManager { } // ========== 实际 API 健康检查(带超时保护)========== - const proxyKeys = ['GEMINI', 'OPENAI', 'CLAUDE', 'QWEN', 'KIRO']; const tempConfig = { ...providerConfig, MODEL_PROVIDER: providerType }; - - proxyKeys.forEach(key => { - const proxyKey = `USE_SYSTEM_PROXY_${key}`; - if (this.globalConfig[proxyKey] !== undefined) { - tempConfig[proxyKey] = this.globalConfig[proxyKey]; - } - }); - const serviceAdapter = getServiceAdapter(tempConfig); // 获取所有可能的请求格式 @@ -936,31 +935,31 @@ export class ProviderPoolManager { // 健康检查超时时间(15秒,避免长时间阻塞) const healthCheckTimeout = 15000; - - // 重试机制:尝试不同的请求格式 - const maxRetries = healthCheckRequests.length; let lastError = null; - for (let i = 0; i < maxRetries; i++) { + // 重试机制:尝试不同的请求格式 + for (let i = 0; i < healthCheckRequests.length; i++) { const healthCheckRequest = healthCheckRequests[i]; + const abortController = new AbortController(); + const timeoutId = setTimeout(() => abortController.abort(), healthCheckTimeout); + try { - this._log('debug', `Health check attempt ${i + 1}/${maxRetries} for ${modelName}: ${JSON.stringify(healthCheckRequest)}`); + this._log('debug', `Health check attempt ${i + 1}/${healthCheckRequests.length} for ${modelName}: ${JSON.stringify(healthCheckRequest)}`); - // 带超时的健康检查 - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Health check timeout')), healthCheckTimeout) - ); - - await Promise.race([ - serviceAdapter.generateContent(modelName, healthCheckRequest), - timeoutPromise - ]); + // 尝试将 signal 注入请求体,供支持的适配器使用 + const requestWithSignal = { + ...healthCheckRequest, + signal: abortController.signal + }; + await serviceAdapter.generateContent(modelName, requestWithSignal); + + clearTimeout(timeoutId); return { success: true, modelName, errorMessage: null }; } catch (error) { + clearTimeout(timeoutId); lastError = error; this._log('debug', `Health check attempt ${i + 1} failed for ${providerType}: ${error.message}`); - // 继续尝试下一个格式 } }