diff --git a/.env.example b/.env.example index e2949998..b62987ab 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,8 @@ CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-20 # 🌐 代理配置 DEFAULT_PROXY_TIMEOUT=60000 MAX_PROXY_RETRIES=3 +# IP协议族配置:true=IPv4, false=IPv6, 默认IPv4(兼容性更好) +PROXY_USE_IPV4=true # 📈 使用限制 DEFAULT_TOKEN_LIMIT=1000000 diff --git a/VERSION b/VERSION index 1f837000..97553d5b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.114 +1.1.115 diff --git a/config/config.example.js b/config/config.example.js index 1ab101cc..ec3ff3d2 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -57,7 +57,9 @@ const config = { // 🌐 代理配置 proxy: { timeout: parseInt(process.env.DEFAULT_PROXY_TIMEOUT) || 30000, - maxRetries: parseInt(process.env.MAX_PROXY_RETRIES) || 3 + maxRetries: parseInt(process.env.MAX_PROXY_RETRIES) || 3, + // IP协议族配置:true=IPv4, false=IPv6, 默认IPv4(兼容性更好) + useIPv4: process.env.PROXY_USE_IPV4 !== 'false' // 默认 true,只有明确设置为 'false' 才使用 IPv6 }, // 📈 使用限制 diff --git a/scripts/test-api-response.js b/scripts/test-api-response.js index 02453708..8131e0f9 100644 --- a/scripts/test-api-response.js +++ b/scripts/test-api-response.js @@ -79,7 +79,7 @@ async function testApiResponse() { console.log('\n\n📊 验证结果:') // 检查 platform 字段 - const claudeWithPlatform = claudeAccounts.filter((a) => a.platform === 'claude-oauth') + const claudeWithPlatform = claudeAccounts.filter((a) => a.platform === 'claude') const consoleWithPlatform = consoleAccounts.filter((a) => a.platform === 'claude-console') if (claudeWithPlatform.length === claudeAccounts.length) { diff --git a/src/app.js b/src/app.js index 1a6a7d56..2f6d09cb 100644 --- a/src/app.js +++ b/src/app.js @@ -141,62 +141,10 @@ class Application { if (buf && buf.length && !buf.toString(encoding || 'utf8').trim()) { throw new Error('Invalid JSON: empty body') } - - // Unicode 字符清理 - 清理无效的 UTF-16 代理对 - if (buf && buf.length) { - try { - const str = buf.toString(encoding || 'utf8') - // 移除无效的 UTF-16 代理对字符 - const cleanedStr = str.replace( - /[\uDC00-\uDFFF](?![\uD800-\uDBFF])|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, - '\uFFFD' - ) - - // 如果字符串被清理过,重新写入buffer - if (cleanedStr !== str) { - logger.warn('🧹 Cleaned invalid Unicode characters from request body') - const cleanedBuf = Buffer.from(cleanedStr, encoding || 'utf8') - // 将清理后的内容复制回原buffer - cleanedBuf.copy(buf, 0) - // 调整buffer长度 - buf._charsWritten = cleanedBuf.length - } - } catch (error) { - logger.warn( - '⚠️ Unicode cleaning failed, proceeding with original buffer:', - error.message - ) - } - } } }) ) this.app.use(express.urlencoded({ extended: true, limit: '10mb' })) - - // 🧹 Unicode 错误处理中间件 - this.app.use((err, req, res, next) => { - if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { - // 检查是否是Unicode相关的JSON解析错误 - if ( - err.message.includes('surrogate') || - err.message.includes('UTF-16') || - err.message.includes('invalid character') - ) { - logger.warn('🧹 Detected Unicode JSON parsing error, attempting recovery:', err.message) - - return res.status(400).json({ - type: 'error', - error: { - type: 'invalid_request_error', - message: - 'The request body contains invalid Unicode characters. Please ensure your text uses valid UTF-8 encoding and does not contain malformed surrogate pairs.' - } - }) - } - } - next(err) - }) - this.app.use(securityMiddleware) // 🎯 信任代理 diff --git a/src/routes/admin.js b/src/routes/admin.js index 7b674e6e..10c1757c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -18,8 +18,7 @@ const crypto = require('crypto') const fs = require('fs') const path = require('path') const config = require('../../config/config') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') const router = express.Router() @@ -1340,6 +1339,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { claudeAiOauth, proxy, accountType, + platform = 'claude', priority, groupId } = req.body @@ -1377,6 +1377,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => { claudeAiOauth, proxy, accountType: accountType || 'shared', // 默认为共享类型 + platform, priority: priority || 50 // 默认优先级为50 }) @@ -2151,7 +2152,7 @@ router.post('/bedrock-accounts/:accountId/test', authenticateAdmin, async (req, // 生成 Gemini OAuth 授权 URL router.post('/gemini-accounts/generate-auth-url', authenticateAdmin, async (req, res) => { try { - const { state } = req.body + const { state, proxy } = req.body // 接收代理配置 // 使用新的 codeassist.google.com 回调地址 const redirectUri = 'https://codeassist.google.com/authcode' @@ -2165,13 +2166,14 @@ router.post('/gemini-accounts/generate-auth-url', authenticateAdmin, async (req, redirectUri: finalRedirectUri } = await geminiAccountService.generateAuthUrl(state, redirectUri) - // 创建 OAuth 会话,包含 codeVerifier + // 创建 OAuth 会话,包含 codeVerifier 和代理配置 const sessionId = authState await redis.setOAuthSession(sessionId, { state: authState, type: 'gemini', redirectUri: finalRedirectUri, codeVerifier, // 保存 PKCE code verifier + proxy: proxy || null, // 保存代理配置 createdAt: new Date().toISOString() }) @@ -2215,7 +2217,7 @@ router.post('/gemini-accounts/poll-auth-status', authenticateAdmin, async (req, // 交换 Gemini 授权码 router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res) => { try { - const { code, sessionId } = req.body + const { code, sessionId, proxy: requestProxy } = req.body if (!code) { return res.status(400).json({ error: 'Authorization code is required' }) @@ -2223,21 +2225,40 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res let redirectUri = 'https://codeassist.google.com/authcode' let codeVerifier = null + let proxyConfig = null // 如果提供了 sessionId,从 OAuth 会话中获取信息 if (sessionId) { const sessionData = await redis.getOAuthSession(sessionId) if (sessionData) { - const { redirectUri: sessionRedirectUri, codeVerifier: sessionCodeVerifier } = sessionData + const { + redirectUri: sessionRedirectUri, + codeVerifier: sessionCodeVerifier, + proxy + } = sessionData redirectUri = sessionRedirectUri || redirectUri codeVerifier = sessionCodeVerifier + proxyConfig = proxy // 获取代理配置 logger.info( - `Using session redirect_uri: ${redirectUri}, has codeVerifier: ${!!codeVerifier}` + `Using session redirect_uri: ${redirectUri}, has codeVerifier: ${!!codeVerifier}, has proxy from session: ${!!proxyConfig}` ) } } - const tokens = await geminiAccountService.exchangeCodeForTokens(code, redirectUri, codeVerifier) + // 如果请求体中直接提供了代理配置,优先使用它 + if (requestProxy) { + proxyConfig = requestProxy + logger.info( + `Using proxy from request body: ${proxyConfig ? JSON.stringify(proxyConfig) : 'none'}` + ) + } + + const tokens = await geminiAccountService.exchangeCodeForTokens( + code, + redirectUri, + codeVerifier, + proxyConfig // 传递代理配置 + ) // 清理 OAuth 会话 if (sessionId) { @@ -4647,19 +4668,10 @@ router.post('/openai-accounts/exchange-code', authenticateAdmin, async (req, res } } - if (sessionData.proxy) { - const { type, host, port, username, password } = sessionData.proxy - if (type === 'socks5') { - // SOCKS5 代理 - const auth = username && password ? `${username}:${password}@` : '' - const socksUrl = `socks5://${auth}${host}:${port}` - axiosConfig.httpsAgent = new SocksProxyAgent(socksUrl) - } else if (type === 'http' || type === 'https') { - // HTTP/HTTPS 代理 - const auth = username && password ? `${username}:${password}@` : '' - const proxyUrl = `${type}://${auth}${host}:${port}` - axiosConfig.httpsAgent = new HttpsProxyAgent(proxyUrl) - } + // 配置代理(如果有) + const proxyAgent = ProxyHelper.createProxyAgent(sessionData.proxy) + if (proxyAgent) { + axiosConfig.httpsAgent = proxyAgent } // 交换 authorization code 获取 tokens diff --git a/src/routes/api.js b/src/routes/api.js index 9f66bc6d..3b1c4160 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -12,52 +12,11 @@ const sessionHelper = require('../utils/sessionHelper') const router = express.Router() -// 🧹 Unicode 字符清理函数 -function cleanUnicodeString(str) { - if (typeof str !== 'string') { - return str - } - - // 移除无效的 UTF-16 代理对字符 - // 匹配无效的低代理字符 (0xDC00-0xDFFF) 没有对应的高代理字符 - // 匹配无效的高代理字符 (0xD800-0xDBFF) 没有对应的低代理字符 - return str.replace( - /[\uDC00-\uDFFF](?![\uD800-\uDBFF])|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, - '\uFFFD' - ) -} - -// 🧹 递归清理对象中的 Unicode 字符 -function cleanUnicodeInObject(obj) { - if (typeof obj === 'string') { - return cleanUnicodeString(obj) - } - - if (Array.isArray(obj)) { - return obj.map((item) => cleanUnicodeInObject(item)) - } - - if (obj && typeof obj === 'object') { - const cleaned = {} - for (const [key, value] of Object.entries(obj)) { - cleaned[cleanUnicodeString(key)] = cleanUnicodeInObject(value) - } - return cleaned - } - - return obj -} - // 🔧 共享的消息处理函数 async function handleMessagesRequest(req, res) { try { const startTime = Date.now() - // Unicode 字符清理 - 在输入验证之前清理请求体 - if (req.body) { - req.body = cleanUnicodeInObject(req.body) - } - // 严格的输入验证 if (!req.body || typeof req.body !== 'object') { return res.status(400).json({ diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index ce6ea479..c5d706a3 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -541,12 +541,24 @@ async function handleGenerateContent(req, res) { }) const client = await geminiAccountService.getOauthClient(accessToken, refreshToken) + + // 解析账户的代理配置 + let proxyConfig = null + if (account.proxy) { + try { + proxyConfig = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy + } catch (e) { + logger.warn('Failed to parse proxy configuration:', e) + } + } + const response = await geminiAccountService.generateContent( client, { model, request: actualRequestData }, user_prompt_id, account.projectId, // 始终使用账户配置的项目ID,忽略请求中的project - req.apiKey?.id // 使用 API Key ID 作为 session ID + req.apiKey?.id, // 使用 API Key ID 作为 session ID + proxyConfig // 传递代理配置 ) // 记录使用统计 @@ -573,7 +585,16 @@ async function handleGenerateContent(req, res) { res.json(response) } catch (error) { const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' - logger.error(`Error in generateContent endpoint (${version})`, { error: error.message }) + // 打印详细的错误信息 + logger.error(`Error in generateContent endpoint (${version})`, { + message: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + responseData: error.response?.data, + requestUrl: error.config?.url, + requestMethod: error.config?.method, + stack: error.stack + }) res.status(500).json({ error: { message: error.message || 'Internal server error', @@ -654,13 +675,25 @@ async function handleStreamGenerateContent(req, res) { }) const client = await geminiAccountService.getOauthClient(accessToken, refreshToken) + + // 解析账户的代理配置 + let proxyConfig = null + if (account.proxy) { + try { + proxyConfig = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy + } catch (e) { + logger.warn('Failed to parse proxy configuration:', e) + } + } + const streamResponse = await geminiAccountService.generateContentStream( client, { model, request: actualRequestData }, user_prompt_id, account.projectId, // 始终使用账户配置的项目ID,忽略请求中的project req.apiKey?.id, // 使用 API Key ID 作为 session ID - abortController.signal // 传递中止信号 + abortController.signal, // 传递中止信号 + proxyConfig // 传递代理配置 ) // 设置 SSE 响应头 @@ -756,7 +789,16 @@ async function handleStreamGenerateContent(req, res) { }) } catch (error) { const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' - logger.error(`Error in streamGenerateContent endpoint (${version})`, { error: error.message }) + // 打印详细的错误信息 + logger.error(`Error in streamGenerateContent endpoint (${version})`, { + message: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + responseData: error.response?.data, + requestUrl: error.config?.url, + requestMethod: error.config?.method, + stack: error.stack + }) if (!res.headersSent) { res.status(500).json({ diff --git a/src/routes/openaiRoutes.js b/src/routes/openaiRoutes.js index 0f46595c..a18f6d4a 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -8,30 +8,11 @@ const unifiedOpenAIScheduler = require('../services/unifiedOpenAIScheduler') const openaiAccountService = require('../services/openaiAccountService') const apiKeyService = require('../services/apiKeyService') const crypto = require('crypto') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') -// 创建代理 Agent +// 创建代理 Agent(使用统一的代理工具) function createProxyAgent(proxy) { - if (!proxy) { - return null - } - - try { - if (proxy.type === 'socks5') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}` - return new SocksProxyAgent(socksUrl) - } else if (proxy.type === 'http' || proxy.type === 'https') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const proxyUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}` - return new HttpsProxyAgent(proxyUrl) - } - } catch (error) { - logger.warn('Failed to create proxy agent:', error) - } - - return null + return ProxyHelper.createProxyAgent(proxy) } // 使用统一调度器选择 OpenAI 账户 @@ -80,7 +61,8 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = accessToken, accountId: result.accountId, accountName: account.name, - proxy + proxy, + account } } catch (error) { logger.error('Failed to get OpenAI auth token:', error) @@ -129,6 +111,7 @@ router.post('/responses', authenticateApiKey, async (req, res) => { 'user', 'text_formatting', 'truncation', + 'text', 'service_tier' ] fieldsToRemove.forEach((field) => { @@ -145,11 +128,13 @@ router.post('/responses', authenticateApiKey, async (req, res) => { } // 使用调度器选择账户 - const { accessToken, accountId, proxy } = await getOpenAIAuthToken( - apiKeyData, - sessionId, - requestedModel - ) + const { + accessToken, + accountId, + accountName: _accountName, + proxy, + account + } = await getOpenAIAuthToken(apiKeyData, sessionId, requestedModel) // 基于白名单构造上游所需的请求头,确保键为小写且值受控 const incoming = req.headers || {} @@ -164,7 +149,7 @@ router.post('/responses', authenticateApiKey, async (req, res) => { // 覆盖或新增必要头部 headers['authorization'] = `Bearer ${accessToken}` - headers['chatgpt-account-id'] = accountId + headers['chatgpt-account-id'] = account.chatgptUserId || account.accountId || accountId headers['host'] = 'chatgpt.com' headers['accept'] = isStream ? 'text/event-stream' : 'application/json' headers['content-type'] = 'application/json' @@ -183,7 +168,9 @@ router.post('/responses', authenticateApiKey, async (req, res) => { // 如果有代理,添加代理配置 if (proxyAgent) { axiosConfig.httpsAgent = proxyAgent - logger.info('Using proxy for OpenAI request') + logger.info(`🌐 Using proxy for OpenAI request: ${ProxyHelper.getProxyDescription(proxy)}`) + } else { + logger.debug('🌐 No proxy configured for OpenAI request') } // 根据 stream 参数决定请求类型 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 6577535d..7ef2c2d9 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -1,7 +1,6 @@ const { v4: uuidv4 } = require('uuid') const crypto = require('crypto') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') const axios = require('axios') const redis = require('../models/redis') const logger = require('../utils/logger') @@ -55,6 +54,7 @@ class ClaudeAccountService { proxy = null, // { type: 'socks5', host: 'localhost', port: 1080, username: '', password: '' } isActive = true, accountType = 'shared', // 'dedicated' or 'shared' + platform = 'claude', priority = 50, // 调度优先级 (1-100,数字越小优先级越高) schedulable = true, // 是否可被调度 subscriptionInfo = null // 手动设置的订阅信息 @@ -79,7 +79,8 @@ class ClaudeAccountService { scopes: claudeAiOauth.scopes.join(' '), proxy: proxy ? JSON.stringify(proxy) : '', isActive: isActive.toString(), - accountType, // 账号类型:'dedicated' 或 'shared' + accountType, // 账号类型:'dedicated' 或 'shared' 或 'group' + platform, priority: priority.toString(), // 调度优先级 createdAt: new Date().toISOString(), lastUsedAt: '', @@ -108,7 +109,8 @@ class ClaudeAccountService { scopes: '', proxy: proxy ? JSON.stringify(proxy) : '', isActive: isActive.toString(), - accountType, // 账号类型:'dedicated' 或 'shared' + accountType, // 账号类型:'dedicated' 或 'shared' 或 'group' + platform, priority: priority.toString(), // 调度优先级 createdAt: new Date().toISOString(), lastUsedAt: '', @@ -151,6 +153,7 @@ class ClaudeAccountService { isActive, proxy, accountType, + platform, priority, status: accountData.status, createdAt: accountData.createdAt, @@ -444,7 +447,7 @@ class ClaudeAccountService { errorMessage: account.errorMessage, accountType: account.accountType || 'shared', // 兼容旧数据,默认为共享 priority: parseInt(account.priority) || 50, // 兼容旧数据,默认优先级50 - platform: 'claude-oauth', // 添加平台标识,用于前端区分 + platform: account.platform || 'claude', // 添加平台标识,用于前端区分 createdAt: account.createdAt, lastUsedAt: account.lastUsedAt, lastRefreshAt: account.lastRefreshAt, @@ -857,29 +860,19 @@ class ClaudeAccountService { } } - // 🌐 创建代理agent + // 🌐 创建代理agent(使用统一的代理工具) _createProxyAgent(proxyConfig) { - if (!proxyConfig) { - return null + const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) + if (proxyAgent) { + logger.info( + `🌐 Using proxy for Claude request: ${ProxyHelper.getProxyDescription(proxyConfig)}` + ) + } else if (proxyConfig) { + logger.debug('🌐 Failed to create proxy agent for Claude') + } else { + logger.debug('🌐 No proxy configured for Claude request') } - - try { - const proxy = JSON.parse(proxyConfig) - - if (proxy.type === 'socks5') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}` - return new SocksProxyAgent(socksUrl) - } else if (proxy.type === 'http' || proxy.type === 'https') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const httpUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}` - return new HttpsProxyAgent(httpUrl) - } - } catch (error) { - logger.warn('⚠️ Invalid proxy configuration:', error) - } - - return null + return proxyAgent } // 🔐 加密敏感数据 diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index fd211651..30b53fff 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -1,7 +1,6 @@ const { v4: uuidv4 } = require('uuid') const crypto = require('crypto') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') const redis = require('../models/redis') const logger = require('../utils/logger') const config = require('../../config/config') @@ -480,29 +479,19 @@ class ClaudeConsoleAccountService { } } - // 🌐 创建代理agent + // 🌐 创建代理agent(使用统一的代理工具) _createProxyAgent(proxyConfig) { - if (!proxyConfig) { - return null + const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) + if (proxyAgent) { + logger.info( + `🌐 Using proxy for Claude Console request: ${ProxyHelper.getProxyDescription(proxyConfig)}` + ) + } else if (proxyConfig) { + logger.debug('🌐 Failed to create proxy agent for Claude Console') + } else { + logger.debug('🌐 No proxy configured for Claude Console request') } - - try { - const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig - - if (proxy.type === 'socks5') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}` - return new SocksProxyAgent(socksUrl) - } else if (proxy.type === 'http' || proxy.type === 'https') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const httpUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}` - return new HttpsProxyAgent(httpUrl) - } - } catch (error) { - logger.warn('⚠️ Invalid proxy configuration:', error) - } - - return null + return proxyAgent } // 🔐 加密敏感数据 diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index fa6c39b4..cb7949bd 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -2,8 +2,7 @@ const https = require('https') const zlib = require('zlib') const fs = require('fs') const path = require('path') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') const claudeAccountService = require('./claudeAccountService') const unifiedClaudeScheduler = require('./unifiedClaudeScheduler') const sessionHelper = require('../utils/sessionHelper') @@ -496,32 +495,28 @@ class ClaudeRelayService { } } - // 🌐 获取代理Agent + // 🌐 获取代理Agent(使用统一的代理工具) async _getProxyAgent(accountId) { try { const accountData = await claudeAccountService.getAllAccounts() const account = accountData.find((acc) => acc.id === accountId) if (!account || !account.proxy) { + logger.debug('🌐 No proxy configured for Claude account') return null } - const { proxy } = account - - if (proxy.type === 'socks5') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}` - return new SocksProxyAgent(socksUrl) - } else if (proxy.type === 'http' || proxy.type === 'https') { - const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const httpUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}` - return new HttpsProxyAgent(httpUrl) + const proxyAgent = ProxyHelper.createProxyAgent(account.proxy) + if (proxyAgent) { + logger.info( + `🌐 Using proxy for Claude request: ${ProxyHelper.getProxyDescription(account.proxy)}` + ) } + return proxyAgent } catch (error) { logger.warn('⚠️ Failed to create proxy agent:', error) + return null } - - return null } // 🔧 过滤客户端请求头 diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index a63ffc99..78e1d5a1 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -5,6 +5,7 @@ const config = require('../../config/config') const logger = require('../utils/logger') const { OAuth2Client } = require('google-auth-library') const { maskToken } = require('../utils/tokenMask') +const ProxyHelper = require('../utils/proxyHelper') const { logRefreshStart, logRefreshSuccess, @@ -109,11 +110,32 @@ setInterval( 10 * 60 * 1000 ) -// 创建 OAuth2 客户端 -function createOAuth2Client(redirectUri = null) { +// 创建 OAuth2 客户端(支持代理配置) +function createOAuth2Client(redirectUri = null, proxyConfig = null) { // 如果没有提供 redirectUri,使用默认值 const uri = redirectUri || 'http://localhost:45462' - return new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, uri) + + // 准备客户端选项 + const clientOptions = { + clientId: OAUTH_CLIENT_ID, + clientSecret: OAUTH_CLIENT_SECRET, + redirectUri: uri + } + + // 如果有代理配置,设置 transporterOptions + if (proxyConfig) { + const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) + if (proxyAgent) { + // 通过 transporterOptions 传递代理配置给底层的 Gaxios + clientOptions.transporterOptions = { + agent: proxyAgent, + httpsAgent: proxyAgent + } + logger.debug('Created OAuth2Client with proxy configuration') + } + } + + return new OAuth2Client(clientOptions) } // 生成授权 URL (支持 PKCE) @@ -196,11 +218,25 @@ async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2 } } -// 交换授权码获取 tokens (支持 PKCE) -async function exchangeCodeForTokens(code, redirectUri = null, codeVerifier = null) { - const oAuth2Client = createOAuth2Client(redirectUri) - +// 交换授权码获取 tokens (支持 PKCE 和代理) +async function exchangeCodeForTokens( + code, + redirectUri = null, + codeVerifier = null, + proxyConfig = null +) { try { + // 创建带代理配置的 OAuth2Client + const oAuth2Client = createOAuth2Client(redirectUri, proxyConfig) + + if (proxyConfig) { + logger.info( + `🌐 Using proxy for Gemini token exchange: ${ProxyHelper.getProxyDescription(proxyConfig)}` + ) + } else { + logger.debug('🌐 No proxy configured for Gemini token exchange') + } + const tokenParams = { code, redirect_uri: redirectUri @@ -228,8 +264,9 @@ async function exchangeCodeForTokens(code, redirectUri = null, codeVerifier = nu } // 刷新访问令牌 -async function refreshAccessToken(refreshToken) { - const oAuth2Client = createOAuth2Client() +async function refreshAccessToken(refreshToken, proxyConfig = null) { + // 创建带代理配置的 OAuth2Client + const oAuth2Client = createOAuth2Client(null, proxyConfig) try { // 设置 refresh_token @@ -237,6 +274,14 @@ async function refreshAccessToken(refreshToken) { refresh_token: refreshToken }) + if (proxyConfig) { + logger.info( + `🔄 Using proxy for Gemini token refresh: ${ProxyHelper.maskProxyInfo(proxyConfig)}` + ) + } else { + logger.debug('🔄 No proxy configured for Gemini token refresh') + } + // 调用 refreshAccessToken 获取新的 tokens const response = await oAuth2Client.refreshAccessToken() const { credentials } = response @@ -261,7 +306,9 @@ async function refreshAccessToken(refreshToken) { logger.error('Error refreshing access token:', { message: error.message, code: error.code, - response: error.response?.data + response: error.response?.data, + hasProxy: !!proxyConfig, + proxy: proxyConfig ? ProxyHelper.maskProxyInfo(proxyConfig) : 'No proxy' }) throw new Error(`Failed to refresh access token: ${error.message}`) } @@ -786,7 +833,8 @@ async function refreshAccountToken(accountId) { logger.info(`🔄 Starting token refresh for Gemini account: ${account.name} (${accountId})`) // account.refreshToken 已经是解密后的值(从 getAccount 返回) - const newTokens = await refreshAccessToken(account.refreshToken) + // 传入账户的代理配置 + const newTokens = await refreshAccessToken(account.refreshToken, account.proxy) // 更新账户信息 const updates = { @@ -1169,7 +1217,8 @@ async function generateContent( requestData, userPromptId, projectId = null, - sessionId = null + sessionId = null, + proxyConfig = null ) { const axios = require('axios') const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com' @@ -1206,6 +1255,17 @@ async function generateContent( timeout: 60000 // 生成内容可能需要更长时间 } + // 添加代理配置 + const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) + if (proxyAgent) { + axiosConfig.httpsAgent = proxyAgent + logger.info( + `🌐 Using proxy for Gemini generateContent: ${ProxyHelper.getProxyDescription(proxyConfig)}` + ) + } else { + logger.debug('🌐 No proxy configured for Gemini generateContent') + } + const response = await axios(axiosConfig) logger.info('✅ generateContent API调用成功') @@ -1219,7 +1279,8 @@ async function generateContentStream( userPromptId, projectId = null, sessionId = null, - signal = null + signal = null, + proxyConfig = null ) { const axios = require('axios') const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com' @@ -1260,6 +1321,17 @@ async function generateContentStream( timeout: 60000 } + // 添加代理配置 + const proxyAgent = ProxyHelper.createProxyAgent(proxyConfig) + if (proxyAgent) { + axiosConfig.httpsAgent = proxyAgent + logger.info( + `🌐 Using proxy for Gemini streamGenerateContent: ${ProxyHelper.getProxyDescription(proxyConfig)}` + ) + } else { + logger.debug('🌐 No proxy configured for Gemini streamGenerateContent') + } + // 如果提供了中止信号,添加到配置中 if (signal) { axiosConfig.signal = signal diff --git a/src/services/geminiRelayService.js b/src/services/geminiRelayService.js index 35423632..60030d3e 100644 --- a/src/services/geminiRelayService.js +++ b/src/services/geminiRelayService.js @@ -1,6 +1,5 @@ const axios = require('axios') -const { HttpsProxyAgent } = require('https-proxy-agent') -const { SocksProxyAgent } = require('socks-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') const logger = require('../utils/logger') const config = require('../../config/config') const apiKeyService = require('./apiKeyService') @@ -9,34 +8,9 @@ const apiKeyService = require('./apiKeyService') const GEMINI_API_BASE = 'https://cloudcode.googleapis.com/v1' const DEFAULT_MODEL = 'models/gemini-2.0-flash-exp' -// 创建代理 agent +// 创建代理 agent(使用统一的代理工具) function createProxyAgent(proxyConfig) { - if (!proxyConfig) { - return null - } - - try { - const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig - - if (!proxy.type || !proxy.host || !proxy.port) { - return null - } - - const proxyUrl = - proxy.username && proxy.password - ? `${proxy.type}://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}` - : `${proxy.type}://${proxy.host}:${proxy.port}` - - if (proxy.type === 'socks5') { - return new SocksProxyAgent(proxyUrl) - } else if (proxy.type === 'http' || proxy.type === 'https') { - return new HttpsProxyAgent(proxyUrl) - } - } catch (error) { - logger.error('Error creating proxy agent:', error) - } - - return null + return ProxyHelper.createProxyAgent(proxyConfig) } // 转换 OpenAI 消息格式到 Gemini 格式 @@ -306,7 +280,9 @@ async function sendGeminiRequest({ const proxyAgent = createProxyAgent(proxy) if (proxyAgent) { axiosConfig.httpsAgent = proxyAgent - logger.debug('Using proxy for Gemini request') + logger.info(`🌐 Using proxy for Gemini API request: ${ProxyHelper.getProxyDescription(proxy)}`) + } else { + logger.debug('🌐 No proxy configured for Gemini API request') } // 添加 AbortController 信号支持 @@ -412,6 +388,11 @@ async function getAvailableModels(accessToken, proxy, projectId, location = 'us- const proxyAgent = createProxyAgent(proxy) if (proxyAgent) { axiosConfig.httpsAgent = proxyAgent + logger.info( + `🌐 Using proxy for Gemini models request: ${ProxyHelper.getProxyDescription(proxy)}` + ) + } else { + logger.debug('🌐 No proxy configured for Gemini models request') } try { @@ -508,7 +489,11 @@ async function countTokens({ const proxyAgent = createProxyAgent(proxy) if (proxyAgent) { axiosConfig.httpsAgent = proxyAgent - logger.debug('Using proxy for Gemini countTokens request') + logger.info( + `🌐 Using proxy for Gemini countTokens request: ${ProxyHelper.getProxyDescription(proxy)}` + ) + } else { + logger.debug('🌐 No proxy configured for Gemini countTokens request') } try { diff --git a/src/services/openaiAccountService.js b/src/services/openaiAccountService.js index 5326abb2..1e88cdec 100644 --- a/src/services/openaiAccountService.js +++ b/src/services/openaiAccountService.js @@ -2,8 +2,7 @@ const redisClient = require('../models/redis') const { v4: uuidv4 } = require('uuid') const crypto = require('crypto') const axios = require('axios') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('../utils/proxyHelper') const config = require('../../config/config') const logger = require('../utils/logger') // const { maskToken } = require('../utils/tokenMask') @@ -133,18 +132,14 @@ async function refreshAccessToken(refreshToken, proxy = null) { } // 配置代理(如果有) - if (proxy && proxy.host && proxy.port) { - if (proxy.type === 'socks5') { - const proxyAuth = - proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const socksProxy = `socks5://${proxyAuth}${proxy.host}:${proxy.port}` - requestOptions.httpsAgent = new SocksProxyAgent(socksProxy) - } else if (proxy.type === 'http' || proxy.type === 'https') { - const proxyAuth = - proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' - const httpProxy = `http://${proxyAuth}${proxy.host}:${proxy.port}` - requestOptions.httpsAgent = new HttpsProxyAgent(httpProxy) - } + const proxyAgent = ProxyHelper.createProxyAgent(proxy) + if (proxyAgent) { + requestOptions.httpsAgent = proxyAgent + logger.info( + `🌐 Using proxy for OpenAI token refresh: ${ProxyHelper.getProxyDescription(proxy)}` + ) + } else { + logger.debug('🌐 No proxy configured for OpenAI token refresh') } // 发送请求 diff --git a/src/utils/logger.js b/src/utils/logger.js index 9de2ec8f..ac4cd618 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -5,7 +5,7 @@ const path = require('path') const fs = require('fs') const os = require('os') -// 安全的 JSON 序列化函数,处理循环引用 +// 安全的 JSON 序列化函数,处理循环引用和特殊字符 const safeStringify = (obj, maxDepth = 3, fullDepth = false) => { const seen = new WeakSet() // 如果是fullDepth模式,增加深度限制 @@ -16,6 +16,28 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => { return '[Max Depth Reached]' } + // 处理字符串值,清理可能导致JSON解析错误的特殊字符 + if (typeof value === 'string') { + try { + // 移除或转义可能导致JSON解析错误的字符 + let cleanValue = value + // eslint-disable-next-line no-control-regex + .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') // 移除控制字符 + .replace(/[\uD800-\uDFFF]/g, '') // 移除孤立的代理对字符 + // eslint-disable-next-line no-control-regex + .replace(/\u0000/g, '') // 移除NUL字节 + + // 如果字符串过长,截断并添加省略号 + if (cleanValue.length > 1000) { + cleanValue = `${cleanValue.substring(0, 997)}...` + } + + return cleanValue + } catch (error) { + return '[Invalid String Data]' + } + } + if (value !== null && typeof value === 'object') { if (seen.has(value)) { return '[Circular Reference]' @@ -40,7 +62,10 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => { } else { const result = {} for (const [k, v] of Object.entries(value)) { - result[k] = replacer(k, v, depth + 1) + // 确保键名也是安全的 + // eslint-disable-next-line no-control-regex + const safeKey = typeof k === 'string' ? k.replace(/[\u0000-\u001F\u007F]/g, '') : k + result[safeKey] = replacer(safeKey, v, depth + 1) } return result } @@ -50,9 +75,20 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => { } try { - return JSON.stringify(replacer('', obj)) + const processed = replacer('', obj) + return JSON.stringify(processed) } catch (error) { - return JSON.stringify({ error: 'Failed to serialize object', message: error.message }) + // 如果JSON.stringify仍然失败,使用更保守的方法 + try { + return JSON.stringify({ + error: 'Failed to serialize object', + message: error.message, + type: typeof obj, + keys: obj && typeof obj === 'object' ? Object.keys(obj) : undefined + }) + } catch (finalError) { + return '{"error":"Critical serialization failure","message":"Unable to serialize any data"}' + } } } @@ -60,8 +96,8 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => { const createLogFormat = (colorize = false) => { const formats = [ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - winston.format.errors({ stack: true }), - winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'stack'] }) + winston.format.errors({ stack: true }) + // 移除 winston.format.metadata() 来避免自动包装 ] if (colorize) { @@ -69,7 +105,7 @@ const createLogFormat = (colorize = false) => { } formats.push( - winston.format.printf(({ level, message, timestamp, stack, metadata, ...rest }) => { + winston.format.printf(({ level, message, timestamp, stack, ...rest }) => { const emoji = { error: '❌', warn: '⚠️ ', @@ -80,12 +116,7 @@ const createLogFormat = (colorize = false) => { let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}` - // 添加元数据 - if (metadata && Object.keys(metadata).length > 0) { - logMessage += ` | ${safeStringify(metadata)}` - } - - // 添加其他属性 + // 直接处理额外数据,不需要metadata包装 const additionalData = { ...rest } delete additionalData.level delete additionalData.message diff --git a/src/utils/oauthHelper.js b/src/utils/oauthHelper.js index 36cb48aa..ac33b71e 100644 --- a/src/utils/oauthHelper.js +++ b/src/utils/oauthHelper.js @@ -4,8 +4,7 @@ */ const crypto = require('crypto') -const { SocksProxyAgent } = require('socks-proxy-agent') -const { HttpsProxyAgent } = require('https-proxy-agent') +const ProxyHelper = require('./proxyHelper') const axios = require('axios') const logger = require('./logger') @@ -125,36 +124,12 @@ function generateSetupTokenParams() { } /** - * 创建代理agent + * 创建代理agent(使用统一的代理工具) * @param {object|null} proxyConfig - 代理配置对象 * @returns {object|null} 代理agent或null */ function createProxyAgent(proxyConfig) { - if (!proxyConfig) { - return null - } - - try { - if (proxyConfig.type === 'socks5') { - const auth = - proxyConfig.username && proxyConfig.password - ? `${proxyConfig.username}:${proxyConfig.password}@` - : '' - const socksUrl = `socks5://${auth}${proxyConfig.host}:${proxyConfig.port}` - return new SocksProxyAgent(socksUrl) - } else if (proxyConfig.type === 'http' || proxyConfig.type === 'https') { - const auth = - proxyConfig.username && proxyConfig.password - ? `${proxyConfig.username}:${proxyConfig.password}@` - : '' - const httpUrl = `${proxyConfig.type}://${auth}${proxyConfig.host}:${proxyConfig.port}` - return new HttpsProxyAgent(httpUrl) - } - } catch (error) { - console.warn('⚠️ Invalid proxy configuration:', error) - } - - return null + return ProxyHelper.createProxyAgent(proxyConfig) } /** @@ -182,6 +157,14 @@ async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, pro const agent = createProxyAgent(proxyConfig) try { + if (agent) { + logger.info( + `🌐 Using proxy for OAuth token exchange: ${ProxyHelper.maskProxyInfo(proxyConfig)}` + ) + } else { + logger.debug('🌐 No proxy configured for OAuth token exchange') + } + logger.debug('🔄 Attempting OAuth token exchange', { url: OAUTH_CONFIG.TOKEN_URL, codeLength: cleanedCode.length, @@ -379,6 +362,14 @@ async function exchangeSetupTokenCode(authorizationCode, codeVerifier, state, pr const agent = createProxyAgent(proxyConfig) try { + if (agent) { + logger.info( + `🌐 Using proxy for Setup Token exchange: ${ProxyHelper.maskProxyInfo(proxyConfig)}` + ) + } else { + logger.debug('🌐 No proxy configured for Setup Token exchange') + } + logger.debug('🔄 Attempting Setup Token exchange', { url: OAUTH_CONFIG.TOKEN_URL, codeLength: cleanedCode.length, diff --git a/src/utils/proxyHelper.js b/src/utils/proxyHelper.js new file mode 100644 index 00000000..ca409e62 --- /dev/null +++ b/src/utils/proxyHelper.js @@ -0,0 +1,212 @@ +const { SocksProxyAgent } = require('socks-proxy-agent') +const { HttpsProxyAgent } = require('https-proxy-agent') +const logger = require('./logger') +const config = require('../../config/config') + +/** + * 统一的代理创建工具 + * 支持 SOCKS5 和 HTTP/HTTPS 代理,可配置 IPv4/IPv6 + */ +class ProxyHelper { + /** + * 创建代理 Agent + * @param {object|string|null} proxyConfig - 代理配置对象或 JSON 字符串 + * @param {object} options - 额外选项 + * @param {boolean|number} options.useIPv4 - 是否使用 IPv4 (true=IPv4, false=IPv6, undefined=auto) + * @returns {Agent|null} 代理 Agent 实例或 null + */ + static createProxyAgent(proxyConfig, options = {}) { + if (!proxyConfig) { + return null + } + + try { + // 解析代理配置 + const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig + + // 验证必要字段 + if (!proxy.type || !proxy.host || !proxy.port) { + logger.warn('⚠️ Invalid proxy configuration: missing required fields (type, host, port)') + return null + } + + // 获取 IPv4/IPv6 配置 + const useIPv4 = ProxyHelper._getIPFamilyPreference(options.useIPv4) + + // 构建认证信息 + const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' + + // 根据代理类型创建 Agent + if (proxy.type === 'socks5') { + const socksUrl = `socks5://${auth}${proxy.host}:${proxy.port}` + const socksOptions = {} + + // 设置 IP 协议族(如果指定) + if (useIPv4 !== null) { + socksOptions.family = useIPv4 ? 4 : 6 + } + + return new SocksProxyAgent(socksUrl, socksOptions) + } else if (proxy.type === 'http' || proxy.type === 'https') { + const proxyUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}` + const httpOptions = {} + + // HttpsProxyAgent 支持 family 参数(通过底层的 agent-base) + if (useIPv4 !== null) { + httpOptions.family = useIPv4 ? 4 : 6 + } + + return new HttpsProxyAgent(proxyUrl, httpOptions) + } else { + logger.warn(`⚠️ Unsupported proxy type: ${proxy.type}`) + return null + } + } catch (error) { + logger.warn('⚠️ Failed to create proxy agent:', error.message) + return null + } + } + + /** + * 获取 IP 协议族偏好设置 + * @param {boolean|number|string} preference - 用户偏好设置 + * @returns {boolean|null} true=IPv4, false=IPv6, null=auto + * @private + */ + static _getIPFamilyPreference(preference) { + // 如果没有指定偏好,使用配置文件或默认值 + if (preference === undefined) { + // 从配置文件读取默认设置,默认使用 IPv4 + const defaultUseIPv4 = config.proxy?.useIPv4 + if (defaultUseIPv4 !== undefined) { + return defaultUseIPv4 + } + // 默认值:IPv4(兼容性更好) + return true + } + + // 处理各种输入格式 + if (typeof preference === 'boolean') { + return preference + } + if (typeof preference === 'number') { + return preference === 4 ? true : preference === 6 ? false : null + } + if (typeof preference === 'string') { + const lower = preference.toLowerCase() + if (lower === 'ipv4' || lower === '4') { + return true + } + if (lower === 'ipv6' || lower === '6') { + return false + } + if (lower === 'auto' || lower === 'both') { + return null + } + } + + // 无法识别的值,返回默认(IPv4) + return true + } + + /** + * 验证代理配置 + * @param {object|string} proxyConfig - 代理配置 + * @returns {boolean} 是否有效 + */ + static validateProxyConfig(proxyConfig) { + if (!proxyConfig) { + return false + } + + try { + const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig + + // 检查必要字段 + if (!proxy.type || !proxy.host || !proxy.port) { + return false + } + + // 检查支持的类型 + if (!['socks5', 'http', 'https'].includes(proxy.type)) { + return false + } + + // 检查端口范围 + const port = parseInt(proxy.port) + if (isNaN(port) || port < 1 || port > 65535) { + return false + } + + return true + } catch (error) { + return false + } + } + + /** + * 获取代理配置的描述信息 + * @param {object|string} proxyConfig - 代理配置 + * @returns {string} 代理描述 + */ + static getProxyDescription(proxyConfig) { + if (!proxyConfig) { + return 'No proxy' + } + + try { + const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig + const hasAuth = proxy.username && proxy.password + return `${proxy.type}://${proxy.host}:${proxy.port}${hasAuth ? ' (with auth)' : ''}` + } catch (error) { + return 'Invalid proxy config' + } + } + + /** + * 脱敏代理配置信息用于日志记录 + * @param {object|string} proxyConfig - 代理配置 + * @returns {string} 脱敏后的代理信息 + */ + static maskProxyInfo(proxyConfig) { + if (!proxyConfig) { + return 'No proxy' + } + + try { + const proxy = typeof proxyConfig === 'string' ? JSON.parse(proxyConfig) : proxyConfig + + let proxyDesc = `${proxy.type}://${proxy.host}:${proxy.port}` + + // 如果有认证信息,进行脱敏处理 + if (proxy.username && proxy.password) { + const maskedUsername = + proxy.username.length <= 2 + ? proxy.username + : proxy.username[0] + + '*'.repeat(Math.max(1, proxy.username.length - 2)) + + proxy.username.slice(-1) + const maskedPassword = '*'.repeat(Math.min(8, proxy.password.length)) + proxyDesc += ` (auth: ${maskedUsername}:${maskedPassword})` + } + + return proxyDesc + } catch (error) { + return 'Invalid proxy config' + } + } + + /** + * 创建代理 Agent(兼容旧的函数接口) + * @param {object|string|null} proxyConfig - 代理配置 + * @param {boolean} useIPv4 - 是否使用 IPv4 + * @returns {Agent|null} 代理 Agent 实例或 null + * @deprecated 使用 createProxyAgent 替代 + */ + static createProxy(proxyConfig, useIPv4 = true) { + logger.warn('⚠️ ProxyHelper.createProxy is deprecated, use createProxyAgent instead') + return ProxyHelper.createProxyAgent(proxyConfig, { useIPv4 }) + } +} + +module.exports = ProxyHelper diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 661bc7f4..625d712f 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -2081,8 +2081,8 @@ const updateAccount = async () => { } } - if (props.account.platform === 'gemini' && form.value.projectId) { - data.projectId = form.value.projectId + if (props.account.platform === 'gemini') { + data.projectId = form.value.projectId || '' } // Claude 官方账号优先级和订阅类型更新 diff --git a/web/admin-spa/src/components/accounts/GroupManagementModal.vue b/web/admin-spa/src/components/accounts/GroupManagementModal.vue index b70fa1d6..b793c356 100644 --- a/web/admin-spa/src/components/accounts/GroupManagementModal.vue +++ b/web/admin-spa/src/components/accounts/GroupManagementModal.vue @@ -172,7 +172,7 @@