mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 21:17:30 +00:00
Merge branch 'Wei-Shaw:dev' into dev
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
// 📈 使用限制
|
||||
|
||||
@@ -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) {
|
||||
|
||||
52
src/app.js
52
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)
|
||||
|
||||
// 🎯 信任代理
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 参数决定请求类型
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 🔐 加密敏感数据
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 🔐 加密敏感数据
|
||||
|
||||
@@ -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,33 +495,29 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 过滤客户端请求头
|
||||
_filterClientHeaders(clientHeaders) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
212
src/utils/proxyHelper.js
Normal file
212
src/utils/proxyHelper.js
Normal file
@@ -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
|
||||
@@ -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 官方账号优先级和订阅类型更新
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<!-- 编辑分组模态框 -->
|
||||
<div
|
||||
v-if="showEditForm"
|
||||
class="modal z-60 fixed inset-0 flex items-center justify-center p-3 sm:p-4"
|
||||
class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4"
|
||||
>
|
||||
<div class="modal-content w-full max-w-lg p-4 sm:p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user