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 os from 'os';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import open from 'open';
|
import open from 'open';
|
||||||
|
import axios from 'axios';
|
||||||
import { broadcastEvent } from '../services/ui-manager.js';
|
import { broadcastEvent } from '../services/ui-manager.js';
|
||||||
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
||||||
import { CONFIG } from '../core/config-manager.js';
|
import { CONFIG } from '../core/config-manager.js';
|
||||||
|
|
@ -138,22 +139,64 @@ const activeKiroPollingTasks = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建带代理支持的 fetch 请求
|
* 创建带代理支持的 fetch 请求
|
||||||
|
* 使用 axios 替代原生 fetch,以正确支持代理配置
|
||||||
* @param {string} url - 请求 URL
|
* @param {string} url - 请求 URL
|
||||||
* @param {Object} options - fetch 选项
|
* @param {Object} options - fetch 选项(兼容 fetch API 格式)
|
||||||
* @param {string} providerType - 提供商类型,用于获取代理配置
|
* @param {string} providerType - 提供商类型,用于获取代理配置
|
||||||
* @returns {Promise<Response>}
|
* @returns {Promise<Object>} 返回类似 fetch Response 的对象
|
||||||
*/
|
*/
|
||||||
async function fetchWithProxy(url, options = {}, providerType) {
|
async function fetchWithProxy(url, options = {}, providerType) {
|
||||||
const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
|
const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
|
||||||
|
|
||||||
if (proxyConfig) {
|
// 构建 axios 配置
|
||||||
// 根据 URL 协议选择合适的 agent
|
const axiosConfig = {
|
||||||
const urlObj = new URL(url);
|
url,
|
||||||
const agent = urlObj.protocol === 'https:' ? proxyConfig.httpsAgent : proxyConfig.httpAgent;
|
method: options.method || 'GET',
|
||||||
options.dispatcher = agent;
|
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.
|
* @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) {
|
async _checkProviderHealth(providerType, providerConfig, forceCheck = false) {
|
||||||
// 确定健康检查使用的模型名称
|
// 如果未启用健康检查且不是强制检查,返回 null(提前返回,避免不必要的计算)
|
||||||
const modelName = providerConfig.checkModelName ||
|
|
||||||
ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType];
|
|
||||||
|
|
||||||
// 如果未启用健康检查且不是强制检查,返回 null
|
|
||||||
if (!providerConfig.checkHealth && !forceCheck) {
|
if (!providerConfig.checkHealth && !forceCheck) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确定健康检查使用的模型名称
|
||||||
|
const modelName = providerConfig.checkModelName ||
|
||||||
|
ProviderPoolManager.DEFAULT_HEALTH_CHECK_MODELS[providerType];
|
||||||
|
|
||||||
if (!modelName) {
|
if (!modelName) {
|
||||||
this._log('warn', `Unknown provider type for health check: ${providerType}`);
|
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 for health check' };
|
return {
|
||||||
|
success: false,
|
||||||
|
modelName: null,
|
||||||
|
errorMessage: `Unknown provider type '${providerType}'. No default health check model configured.`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 快速预检:从内存缓存检查 OAuth 凭证状态 ==========
|
// ========== 快速预检:从内存缓存检查 OAuth 凭证状态 ==========
|
||||||
// 对于 OAuth 类型的 provider,先检查 token 是否过期,避免阻塞
|
// 对于 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'];
|
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 credentialCache = CredentialCacheManager.getInstance();
|
||||||
const cachedEntry = credentialCache.getCredentials(providerType, providerConfig.uuid);
|
const cachedEntry = credentialCache.getCredentials(providerType, providerConfig.uuid);
|
||||||
|
|
||||||
|
|
@ -916,19 +924,10 @@ export class ProviderPoolManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 实际 API 健康检查(带超时保护)==========
|
// ========== 实际 API 健康检查(带超时保护)==========
|
||||||
const proxyKeys = ['GEMINI', 'OPENAI', 'CLAUDE', 'QWEN', 'KIRO'];
|
|
||||||
const tempConfig = {
|
const tempConfig = {
|
||||||
...providerConfig,
|
...providerConfig,
|
||||||
MODEL_PROVIDER: providerType
|
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);
|
const serviceAdapter = getServiceAdapter(tempConfig);
|
||||||
|
|
||||||
// 获取所有可能的请求格式
|
// 获取所有可能的请求格式
|
||||||
|
|
@ -936,31 +935,31 @@ export class ProviderPoolManager {
|
||||||
|
|
||||||
// 健康检查超时时间(15秒,避免长时间阻塞)
|
// 健康检查超时时间(15秒,避免长时间阻塞)
|
||||||
const healthCheckTimeout = 15000;
|
const healthCheckTimeout = 15000;
|
||||||
|
|
||||||
// 重试机制:尝试不同的请求格式
|
|
||||||
const maxRetries = healthCheckRequests.length;
|
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
// 重试机制:尝试不同的请求格式
|
||||||
|
for (let i = 0; i < healthCheckRequests.length; i++) {
|
||||||
const healthCheckRequest = healthCheckRequests[i];
|
const healthCheckRequest = healthCheckRequests[i];
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => abortController.abort(), healthCheckTimeout);
|
||||||
|
|
||||||
try {
|
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)}`);
|
||||||
|
|
||||||
// 带超时的健康检查
|
// 尝试将 signal 注入请求体,供支持的适配器使用
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const requestWithSignal = {
|
||||||
setTimeout(() => reject(new Error('Health check timeout')), healthCheckTimeout)
|
...healthCheckRequest,
|
||||||
);
|
signal: abortController.signal
|
||||||
|
};
|
||||||
|
|
||||||
await Promise.race([
|
await serviceAdapter.generateContent(modelName, requestWithSignal);
|
||||||
serviceAdapter.generateContent(modelName, healthCheckRequest),
|
|
||||||
timeoutPromise
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
return { success: true, modelName, errorMessage: null };
|
return { success: true, modelName, errorMessage: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
lastError = error;
|
lastError = error;
|
||||||
this._log('debug', `Health check attempt ${i + 1} failed for ${providerType}: ${error.message}`);
|
this._log('debug', `Health check attempt ${i + 1} failed for ${providerType}: ${error.message}`);
|
||||||
// 继续尝试下一个格式
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue