refactor(auth): 使用 axios 替代 fetch 以支持代理配置
重构 OAuth 处理中的网络请求模块,将原生 fetch 替换为 axios,以正确处理代理配置。axios 提供了更完善的代理支持和错误处理机制,同时保持与原有 fetch API 的兼容性。 refactor(providers): 优化健康检查逻辑 - 提前返回未启用健康检查的情况 - 仅在开启自动刷新 token 时执行快速预检 - 使用 AbortController 替代 Promise.race 实现超时控制 - 优化错误消息和日志信息
This commit is contained in:
parent
3ee5e29180
commit
0974a5e83d
2 changed files with 82 additions and 40 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
// 继续尝试下一个格式
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue