refactor(auth): 使用 axios 替代 fetch 以支持代理配置

重构 OAuth 处理中的网络请求模块,将原生 fetch 替换为 axios,以正确处理代理配置。axios 提供了更完善的代理支持和错误处理机制,同时保持与原有 fetch API 的兼容性。

refactor(providers): 优化健康检查逻辑

- 提前返回未启用健康检查的情况
- 仅在开启自动刷新 token 时执行快速预检
- 使用 AbortController 替代 Promise.race 实现超时控制
- 优化错误消息和日志信息
This commit is contained in:
hex2077 2026-01-16 17:35:15 +08:00
parent 3ee5e29180
commit 0974a5e83d
2 changed files with 82 additions and 40 deletions

View file

@ -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<Response>}
* @returns {Promise<Object>} 返回类似 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;
}
}
/**

View file

@ -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}`);
// 继续尝试下一个格式
}
}