Files
claude-relay-service/src/services/openaiAccountService.js
shaw a45c832278 feat: 统一代理配置管理,支持IPv4/IPv6协议族选择
- 新增统一代理工具 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 <noreply@anthropic.com>
2025-08-20 22:36:34 +08:00

729 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const redisClient = require('../models/redis')
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const axios = require('axios')
const ProxyHelper = require('../utils/proxyHelper')
const config = require('../../config/config')
const logger = require('../utils/logger')
// const { maskToken } = require('../utils/tokenMask')
const {
logRefreshStart,
logRefreshSuccess,
logRefreshError,
logTokenUsage,
logRefreshSkipped
} = require('../utils/tokenRefreshLogger')
const LRUCache = require('../utils/lruCache')
// const tokenRefreshService = require('./tokenRefreshService')
// 加密相关常量
const ALGORITHM = 'aes-256-cbc'
const ENCRYPTION_SALT = 'openai-account-salt'
const IV_LENGTH = 16
// 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算
// scryptSync 是 CPU 密集型操作,缓存可以减少 95%+ 的 CPU 占用
let _encryptionKeyCache = null
// 🔄 解密结果缓存,提高解密性能
const decryptCache = new LRUCache(500)
// 生成加密密钥(使用与 claudeAccountService 相同的方法)
function generateEncryptionKey() {
if (!_encryptionKeyCache) {
_encryptionKeyCache = crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32)
logger.info('🔑 OpenAI encryption key derived and cached for performance optimization')
}
return _encryptionKeyCache
}
// OpenAI 账户键前缀
const OPENAI_ACCOUNT_KEY_PREFIX = 'openai:account:'
const SHARED_OPENAI_ACCOUNTS_KEY = 'shared_openai_accounts'
const ACCOUNT_SESSION_MAPPING_PREFIX = 'openai_session_account_mapping:'
// 加密函数
function encrypt(text) {
if (!text) {
return ''
}
const key = generateEncryptionKey()
const iv = crypto.randomBytes(IV_LENGTH)
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
let encrypted = cipher.update(text)
encrypted = Buffer.concat([encrypted, cipher.final()])
return `${iv.toString('hex')}:${encrypted.toString('hex')}`
}
// 解密函数
function decrypt(text) {
if (!text) {
return ''
}
// 🎯 检查缓存
const cacheKey = crypto.createHash('sha256').update(text).digest('hex')
const cached = decryptCache.get(cacheKey)
if (cached !== undefined) {
return cached
}
try {
const key = generateEncryptionKey()
// IV 是固定长度的 32 个十六进制字符16 字节)
const ivHex = text.substring(0, 32)
const encryptedHex = text.substring(33) // 跳过冒号
const iv = Buffer.from(ivHex, 'hex')
const encryptedText = Buffer.from(encryptedHex, 'hex')
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
let decrypted = decipher.update(encryptedText)
decrypted = Buffer.concat([decrypted, decipher.final()])
const result = decrypted.toString()
// 💾 存入缓存5分钟过期
decryptCache.set(cacheKey, result, 5 * 60 * 1000)
// 📊 定期打印缓存统计
if ((decryptCache.hits + decryptCache.misses) % 1000 === 0) {
decryptCache.printStats()
}
return result
} catch (error) {
logger.error('Decryption error:', error)
return ''
}
}
// 🧹 定期清理缓存每10分钟
setInterval(
() => {
decryptCache.cleanup()
logger.info('🧹 OpenAI decrypt cache cleanup completed', decryptCache.getStats())
},
10 * 60 * 1000
)
// 刷新访问令牌
async function refreshAccessToken(refreshToken, proxy = null) {
try {
// Codex CLI 的官方 CLIENT_ID
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
// 准备请求数据
const requestData = new URLSearchParams({
grant_type: 'refresh_token',
client_id: CLIENT_ID,
refresh_token: refreshToken,
scope: 'openid profile email'
}).toString()
// 配置请求选项
const requestOptions = {
method: 'POST',
url: 'https://auth.openai.com/oauth/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': requestData.length
},
data: requestData,
timeout: 30000 // 30秒超时
}
// 配置代理(如果有)
const proxyAgent = ProxyHelper.createProxyAgent(proxy)
if (proxyAgent) {
requestOptions.httpsAgent = proxyAgent
}
// 发送请求
const response = await axios(requestOptions)
if (response.status === 200 && response.data) {
const result = response.data
logger.info('✅ Successfully refreshed OpenAI token')
// 返回新的 token 信息
return {
access_token: result.access_token,
id_token: result.id_token,
refresh_token: result.refresh_token || refreshToken, // 如果没有返回新的,保留原来的
expires_in: result.expires_in || 3600,
expiry_date: Date.now() + (result.expires_in || 3600) * 1000 // 计算过期时间
}
} else {
throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}`)
}
} catch (error) {
if (error.response) {
// 服务器响应了错误状态码
logger.error('OpenAI token refresh failed:', {
status: error.response.status,
data: error.response.data,
headers: error.response.headers
})
throw new Error(
`Token refresh failed: ${error.response.status} - ${JSON.stringify(error.response.data)}`
)
} else if (error.request) {
// 请求已发出但没有收到响应
logger.error('OpenAI token refresh no response:', error.message)
throw new Error(`Token refresh failed: No response from server - ${error.message}`)
} else {
// 设置请求时发生错误
logger.error('OpenAI token refresh error:', error.message)
throw new Error(`Token refresh failed: ${error.message}`)
}
}
}
// 检查 token 是否过期
function isTokenExpired(account) {
if (!account.expiresAt) {
return false
}
return new Date(account.expiresAt) <= new Date()
}
// 刷新账户的 access token
async function refreshAccountToken(accountId) {
const account = await getAccount(accountId)
if (!account) {
throw new Error('Account not found')
}
const accountName = account.name || accountId
logRefreshStart(accountId, accountName, 'openai')
// 检查是否有 refresh token
const refreshToken = account.refreshToken ? decrypt(account.refreshToken) : null
if (!refreshToken) {
logRefreshSkipped(accountId, accountName, 'openai', 'No refresh token available')
throw new Error('No refresh token available')
}
// 获取代理配置
let proxy = null
if (account.proxy) {
try {
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
} catch (e) {
logger.warn(`Failed to parse proxy config for account ${accountId}:`, e)
}
}
try {
const newTokens = await refreshAccessToken(refreshToken, proxy)
if (!newTokens) {
throw new Error('Failed to refresh token')
}
// 准备更新数据
const updates = {
accessToken: encrypt(newTokens.access_token),
expiresAt: new Date(newTokens.expiry_date).toISOString()
}
// 如果有新的 ID token也更新它
if (newTokens.id_token) {
updates.idToken = encrypt(newTokens.id_token)
}
// 如果返回了新的 refresh token更新它
if (newTokens.refresh_token && newTokens.refresh_token !== refreshToken) {
updates.refreshToken = encrypt(newTokens.refresh_token)
logger.info(`Updated refresh token for account ${accountId}`)
}
// 更新账户信息
await updateAccount(accountId, updates)
logRefreshSuccess(accountId, accountName, 'openai', newTokens.expiry_date)
return newTokens
} catch (error) {
logRefreshError(accountId, accountName, 'openai', error.message)
throw error
}
}
// 创建账户
async function createAccount(accountData) {
const accountId = uuidv4()
const now = new Date().toISOString()
// 处理OAuth数据
let oauthData = {}
if (accountData.openaiOauth) {
oauthData =
typeof accountData.openaiOauth === 'string'
? JSON.parse(accountData.openaiOauth)
: accountData.openaiOauth
}
// 处理账户信息
const accountInfo = accountData.accountInfo || {}
const account = {
id: accountId,
name: accountData.name,
description: accountData.description || '',
accountType: accountData.accountType || 'shared',
groupId: accountData.groupId || null,
priority: accountData.priority || 50,
rateLimitDuration:
accountData.rateLimitDuration !== undefined && accountData.rateLimitDuration !== null
? accountData.rateLimitDuration
: 60,
// OAuth相关字段加密存储
idToken: encrypt(oauthData.idToken || ''),
accessToken: encrypt(oauthData.accessToken || ''),
refreshToken: encrypt(oauthData.refreshToken || ''),
openaiOauth: encrypt(JSON.stringify(oauthData)),
// 账户信息字段
accountId: accountInfo.accountId || '',
chatgptUserId: accountInfo.chatgptUserId || '',
organizationId: accountInfo.organizationId || '',
organizationRole: accountInfo.organizationRole || '',
organizationTitle: accountInfo.organizationTitle || '',
planType: accountInfo.planType || '',
email: encrypt(accountInfo.email || ''),
emailVerified: accountInfo.emailVerified || false,
// 过期时间
expiresAt: oauthData.expires_in
? new Date(Date.now() + oauthData.expires_in * 1000).toISOString()
: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 默认1年
// 状态字段
isActive: accountData.isActive !== false ? 'true' : 'false',
status: 'active',
schedulable: accountData.schedulable !== false ? 'true' : 'false',
lastRefresh: now,
createdAt: now,
updatedAt: now
}
// 代理配置
if (accountData.proxy) {
account.proxy =
typeof accountData.proxy === 'string' ? accountData.proxy : JSON.stringify(accountData.proxy)
}
const client = redisClient.getClientSafe()
await client.hset(`${OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`, account)
// 如果是共享账户,添加到共享账户集合
if (account.accountType === 'shared') {
await client.sadd(SHARED_OPENAI_ACCOUNTS_KEY, accountId)
}
logger.info(`Created OpenAI account: ${accountId}`)
return account
}
// 获取账户
async function getAccount(accountId) {
const client = redisClient.getClientSafe()
const accountData = await client.hgetall(`${OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`)
if (!accountData || Object.keys(accountData).length === 0) {
return null
}
// 解密敏感数据(仅用于内部处理,不返回给前端)
if (accountData.idToken) {
accountData.idToken = decrypt(accountData.idToken)
}
if (accountData.accessToken) {
accountData.accessToken = decrypt(accountData.accessToken)
}
if (accountData.refreshToken) {
accountData.refreshToken = decrypt(accountData.refreshToken)
}
if (accountData.email) {
accountData.email = decrypt(accountData.email)
}
if (accountData.openaiOauth) {
try {
accountData.openaiOauth = JSON.parse(decrypt(accountData.openaiOauth))
} catch (e) {
accountData.openaiOauth = null
}
}
// 解析代理配置
if (accountData.proxy && typeof accountData.proxy === 'string') {
try {
accountData.proxy = JSON.parse(accountData.proxy)
} catch (e) {
accountData.proxy = null
}
}
return accountData
}
// 更新账户
async function updateAccount(accountId, updates) {
const existingAccount = await getAccount(accountId)
if (!existingAccount) {
throw new Error('Account not found')
}
updates.updatedAt = new Date().toISOString()
// 加密敏感数据
if (updates.openaiOauth) {
const oauthData =
typeof updates.openaiOauth === 'string'
? updates.openaiOauth
: JSON.stringify(updates.openaiOauth)
updates.openaiOauth = encrypt(oauthData)
}
if (updates.idToken) {
updates.idToken = encrypt(updates.idToken)
}
if (updates.accessToken) {
updates.accessToken = encrypt(updates.accessToken)
}
if (updates.refreshToken) {
updates.refreshToken = encrypt(updates.refreshToken)
}
if (updates.email) {
updates.email = encrypt(updates.email)
}
// 处理代理配置
if (updates.proxy) {
updates.proxy =
typeof updates.proxy === 'string' ? updates.proxy : JSON.stringify(updates.proxy)
}
// 更新账户类型时处理共享账户集合
const client = redisClient.getClientSafe()
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
if (updates.accountType === 'shared') {
await client.sadd(SHARED_OPENAI_ACCOUNTS_KEY, accountId)
} else {
await client.srem(SHARED_OPENAI_ACCOUNTS_KEY, accountId)
}
}
await client.hset(`${OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`, updates)
logger.info(`Updated OpenAI account: ${accountId}`)
// 合并更新后的账户数据
const updatedAccount = { ...existingAccount, ...updates }
// 返回时解析代理配置
if (updatedAccount.proxy && typeof updatedAccount.proxy === 'string') {
try {
updatedAccount.proxy = JSON.parse(updatedAccount.proxy)
} catch (e) {
updatedAccount.proxy = null
}
}
return updatedAccount
}
// 删除账户
async function deleteAccount(accountId) {
const account = await getAccount(accountId)
if (!account) {
throw new Error('Account not found')
}
// 从 Redis 删除
const client = redisClient.getClientSafe()
await client.del(`${OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`)
// 从共享账户集合中移除
if (account.accountType === 'shared') {
await client.srem(SHARED_OPENAI_ACCOUNTS_KEY, accountId)
}
// 清理会话映射
const sessionMappings = await client.keys(`${ACCOUNT_SESSION_MAPPING_PREFIX}*`)
for (const key of sessionMappings) {
const mappedAccountId = await client.get(key)
if (mappedAccountId === accountId) {
await client.del(key)
}
}
logger.info(`Deleted OpenAI account: ${accountId}`)
return true
}
// 获取所有账户
async function getAllAccounts() {
const client = redisClient.getClientSafe()
const keys = await client.keys(`${OPENAI_ACCOUNT_KEY_PREFIX}*`)
const accounts = []
for (const key of keys) {
const accountData = await client.hgetall(key)
if (accountData && Object.keys(accountData).length > 0) {
// 解密敏感数据(但不返回给前端)
if (accountData.email) {
accountData.email = decrypt(accountData.email)
}
// 屏蔽敏感信息token等不应该返回给前端
delete accountData.idToken
delete accountData.accessToken
delete accountData.refreshToken
delete accountData.openaiOauth
// 获取限流状态信息
const rateLimitInfo = await getAccountRateLimitInfo(accountData.id)
// 解析代理配置
if (accountData.proxy) {
try {
accountData.proxy = JSON.parse(accountData.proxy)
// 屏蔽代理密码
if (accountData.proxy && accountData.proxy.password) {
accountData.proxy.password = '******'
}
} catch (e) {
// 如果解析失败设置为null
accountData.proxy = null
}
}
// 不解密敏感字段,只返回基本信息
accounts.push({
...accountData,
openaiOauth: accountData.openaiOauth ? '[ENCRYPTED]' : '',
accessToken: accountData.accessToken ? '[ENCRYPTED]' : '',
refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '',
// 添加 scopes 字段用于判断认证方式
// 处理空字符串的情况
scopes:
accountData.scopes && accountData.scopes.trim() ? accountData.scopes.split(' ') : [],
// 添加 hasRefreshToken 标记
hasRefreshToken: !!accountData.refreshToken,
// 添加限流状态信息(统一格式)
rateLimitStatus: rateLimitInfo
? {
isRateLimited: rateLimitInfo.isRateLimited,
rateLimitedAt: rateLimitInfo.rateLimitedAt,
minutesRemaining: rateLimitInfo.minutesRemaining
}
: {
isRateLimited: false,
rateLimitedAt: null,
minutesRemaining: 0
}
})
}
}
return accounts
}
// 选择可用账户(支持专属和共享账户)
async function selectAvailableAccount(apiKeyId, sessionHash = null) {
// 首先检查是否有粘性会话
const client = redisClient.getClientSafe()
if (sessionHash) {
const mappedAccountId = await client.get(`${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`)
if (mappedAccountId) {
const account = await getAccount(mappedAccountId)
if (account && account.isActive === 'true' && !isTokenExpired(account)) {
logger.debug(`Using sticky session account: ${mappedAccountId}`)
return account
}
}
}
// 获取 API Key 信息
const apiKeyData = await client.hgetall(`api_key:${apiKeyId}`)
// 检查是否绑定了 OpenAI 账户
if (apiKeyData.openaiAccountId) {
const account = await getAccount(apiKeyData.openaiAccountId)
if (account && account.isActive === 'true') {
// 检查 token 是否过期
const isExpired = isTokenExpired(account)
// 记录token使用情况
logTokenUsage(account.id, account.name, 'openai', account.expiresAt, isExpired)
if (isExpired) {
await refreshAccountToken(account.id)
return await getAccount(account.id)
}
// 创建粘性会话映射
if (sessionHash) {
await client.setex(
`${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`,
3600, // 1小时过期
account.id
)
}
return account
}
}
// 从共享账户池选择
const sharedAccountIds = await client.smembers(SHARED_OPENAI_ACCOUNTS_KEY)
const availableAccounts = []
for (const accountId of sharedAccountIds) {
const account = await getAccount(accountId)
if (account && account.isActive === 'true' && !isRateLimited(account)) {
availableAccounts.push(account)
}
}
if (availableAccounts.length === 0) {
throw new Error('No available OpenAI accounts')
}
// 选择使用最少的账户
const selectedAccount = availableAccounts.reduce((prev, curr) => {
const prevUsage = parseInt(prev.totalUsage || 0)
const currUsage = parseInt(curr.totalUsage || 0)
return prevUsage <= currUsage ? prev : curr
})
// 检查 token 是否过期
if (isTokenExpired(selectedAccount)) {
await refreshAccountToken(selectedAccount.id)
return await getAccount(selectedAccount.id)
}
// 创建粘性会话映射
if (sessionHash) {
await client.setex(
`${ACCOUNT_SESSION_MAPPING_PREFIX}${sessionHash}`,
3600, // 1小时过期
selectedAccount.id
)
}
return selectedAccount
}
// 检查账户是否被限流
function isRateLimited(account) {
if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
const limitedAt = new Date(account.rateLimitedAt).getTime()
const now = Date.now()
const limitDuration = 60 * 60 * 1000 // 1小时
return now < limitedAt + limitDuration
}
return false
}
// 设置账户限流状态
async function setAccountRateLimited(accountId, isLimited) {
const updates = {
rateLimitStatus: isLimited ? 'limited' : 'normal',
rateLimitedAt: isLimited ? new Date().toISOString() : null
}
await updateAccount(accountId, updates)
logger.info(`Set rate limit status for OpenAI account ${accountId}: ${updates.rateLimitStatus}`)
}
// 切换账户调度状态
async function toggleSchedulable(accountId) {
const account = await getAccount(accountId)
if (!account) {
throw new Error('Account not found')
}
// 切换调度状态
const newSchedulable = account.schedulable === 'false' ? 'true' : 'false'
await updateAccount(accountId, {
schedulable: newSchedulable
})
logger.info(`Toggled schedulable status for OpenAI account ${accountId}: ${newSchedulable}`)
return {
success: true,
schedulable: newSchedulable === 'true'
}
}
// 获取账户限流信息
async function getAccountRateLimitInfo(accountId) {
const account = await getAccount(accountId)
if (!account) {
return null
}
if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) {
const limitedAt = new Date(account.rateLimitedAt).getTime()
const now = Date.now()
const limitDuration = 60 * 60 * 1000 // 1小时
const remainingTime = Math.max(0, limitedAt + limitDuration - now)
return {
isRateLimited: remainingTime > 0,
rateLimitedAt: account.rateLimitedAt,
minutesRemaining: Math.ceil(remainingTime / (60 * 1000))
}
}
return {
isRateLimited: false,
rateLimitedAt: null,
minutesRemaining: 0
}
}
// 更新账户使用统计tokens参数可选默认为0仅更新最后使用时间
async function updateAccountUsage(accountId, tokens = 0) {
const account = await getAccount(accountId)
if (!account) {
return
}
const updates = {
lastUsedAt: new Date().toISOString()
}
// 如果有 tokens 参数且大于0同时更新使用统计
if (tokens > 0) {
const totalUsage = parseInt(account.totalUsage || 0) + tokens
updates.totalUsage = totalUsage.toString()
}
await updateAccount(accountId, updates)
}
// 为了兼容性保留recordUsage作为updateAccountUsage的别名
const recordUsage = updateAccountUsage
module.exports = {
createAccount,
getAccount,
updateAccount,
deleteAccount,
getAllAccounts,
selectAvailableAccount,
refreshAccountToken,
isTokenExpired,
setAccountRateLimited,
toggleSchedulable,
getAccountRateLimitInfo,
updateAccountUsage,
recordUsage, // 别名指向updateAccountUsage
encrypt,
decrypt,
generateEncryptionKey,
decryptCache // 暴露缓存对象以便测试和监控
}