From a45c832278a7c0489bcf51ca8c41bf2ef8b176d3 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 20 Aug 2025 22:36:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?IPv4/IPv6=E5=8D=8F=E8=AE=AE=E6=97=8F=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增统一代理工具 ProxyHelper,支持 SOCKS5/HTTP/HTTPS 代理 - 添加 IPv4/IPv6 协议族配置选项,默认使用 IPv4 确保兼容性 - 移除 OpenAI 路由中硬编码的 family: 4 限制 - 统一 8 个服务文件中的代理创建逻辑,避免重复维护 - 支持 OAuth 和 token 交换过程中的代理使用 - 新增配置项:PROXY_USE_IPV4(默认 true) - 向后兼容:现有配置无需手动更新 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .env.example | 2 + config/config.example.js | 4 +- src/routes/admin.js | 20 +-- src/routes/openaiRoutes.js | 39 ++--- src/services/claudeAccountService.js | 27 +-- src/services/claudeConsoleAccountService.js | 27 +-- src/services/claudeRelayService.js | 20 +-- src/services/geminiRelayService.js | 32 +--- src/services/openaiAccountService.js | 18 +- src/utils/oauthHelper.js | 31 +--- src/utils/proxyHelper.js | 179 ++++++++++++++++++++ 11 files changed, 219 insertions(+), 180 deletions(-) create mode 100644 src/utils/proxyHelper.js 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/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/src/routes/admin.js b/src/routes/admin.js index da2670ea..ba822f21 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() @@ -4649,19 +4648,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/openaiRoutes.js b/src/routes/openaiRoutes.js index 7a0c9692..07c7bcdc 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -8,32 +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, { - family: 4 - }) - } 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 账户 @@ -149,11 +128,13 @@ router.post('/responses', authenticateApiKey, async (req, res) => { } // 使用调度器选择账户 - const { accessToken, accountId, accountName, proxy, account } = await getOpenAIAuthToken( - apiKeyData, - sessionId, - requestedModel - ) + const { + accessToken, + accountId, + accountName: _accountName, + proxy, + account + } = await getOpenAIAuthToken(apiKeyData, sessionId, requestedModel) // 基于白名单构造上游所需的请求头,确保键为小写且值受控 const incoming = req.headers || {} diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index cc2a8089..1195c370 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') @@ -861,29 +860,9 @@ class ClaudeAccountService { } } - // 🌐 创建代理agent + // 🌐 创建代理agent(使用统一的代理工具) _createProxyAgent(proxyConfig) { - if (!proxyConfig) { - return null - } - - 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 ProxyHelper.createProxyAgent(proxyConfig) } // 🔐 加密敏感数据 diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index fd211651..73dc50b3 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,9 @@ class ClaudeConsoleAccountService { } } - // 🌐 创建代理agent + // 🌐 创建代理agent(使用统一的代理工具) _createProxyAgent(proxyConfig) { - if (!proxyConfig) { - return null - } - - 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 ProxyHelper.createProxyAgent(proxyConfig) } // 🔐 加密敏感数据 diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index fa6c39b4..dcf087ff 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,7 +495,7 @@ class ClaudeRelayService { } } - // 🌐 获取代理Agent + // 🌐 获取代理Agent(使用统一的代理工具) async _getProxyAgent(accountId) { try { const accountData = await claudeAccountService.getAllAccounts() @@ -506,22 +505,11 @@ class ClaudeRelayService { 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) - } + return ProxyHelper.createProxyAgent(account.proxy) } catch (error) { logger.warn('⚠️ Failed to create proxy agent:', error) + return null } - - return null } // 🔧 过滤客户端请求头 diff --git a/src/services/geminiRelayService.js b/src/services/geminiRelayService.js index 35423632..0a3f560b 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 格式 diff --git a/src/services/openaiAccountService.js b/src/services/openaiAccountService.js index 5326abb2..d4d1abae 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,9 @@ 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 } // 发送请求 diff --git a/src/utils/oauthHelper.js b/src/utils/oauthHelper.js index 36cb48aa..84cd2554 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) } /** diff --git a/src/utils/proxyHelper.js b/src/utils/proxyHelper.js new file mode 100644 index 00000000..7bf2da86 --- /dev/null +++ b/src/utils/proxyHelper.js @@ -0,0 +1,179 @@ +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' + } + } + + /** + * 创建代理 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