mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 实现 OpenAI token 自动刷新功能并优化账户管理界面
主要更改: 1. OpenAI Token 自动刷新 - 实现 refreshAccessToken 函数,支持 OAuth 2.0 refresh_token grant type - 使用 Codex CLI 官方 CLIENT_ID (app_EMoamEEZ73f0CkXaXp7hrann) - 支持 SOCKS5 和 HTTP/HTTPS 代理 - 自动更新 access token、id token 和 refresh token 2. 账户管理界面优化 - 移除手动刷新 token 按钮(桌面端和移动端) - 保留后端自动刷新机制 - 优化代码结构,删除不再需要的函数和变量 3. 测试和文档 - 添加 test-openai-refresh.js 测试脚本 - 创建详细的实现文档 技术细节: - Token 端点: https://auth.openai.com/oauth/token - 默认有效期: 1小时 - 加密存储: AES-256-CBC 所有平台现在都支持自动 token 刷新: ✅ Claude - OAuth 自动刷新 ✅ Gemini - Google OAuth2 自动刷新 ✅ OpenAI - OAuth 自动刷新(新实现) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -303,7 +303,10 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
name: validation.keyData.name,
|
||||
tokenLimit: validation.keyData.tokenLimit,
|
||||
claudeAccountId: validation.keyData.claudeAccountId,
|
||||
claudeConsoleAccountId: validation.keyData.claudeConsoleAccountId, // 添加 Claude Console 账号ID
|
||||
geminiAccountId: validation.keyData.geminiAccountId,
|
||||
openaiAccountId: validation.keyData.openaiAccountId, // 添加 OpenAI 账号ID
|
||||
bedrockAccountId: validation.keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
||||
permissions: validation.keyData.permissions,
|
||||
concurrencyLimit: validation.keyData.concurrencyLimit,
|
||||
rateLimitWindow: validation.keyData.rateLimitWindow,
|
||||
|
||||
@@ -391,6 +391,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
claudeConsoleAccountId,
|
||||
geminiAccountId,
|
||||
openaiAccountId,
|
||||
bedrockAccountId,
|
||||
permissions,
|
||||
concurrencyLimit,
|
||||
rateLimitWindow,
|
||||
@@ -487,6 +488,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
claudeConsoleAccountId,
|
||||
geminiAccountId,
|
||||
openaiAccountId,
|
||||
bedrockAccountId,
|
||||
permissions,
|
||||
concurrencyLimit,
|
||||
rateLimitWindow,
|
||||
@@ -633,6 +635,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
claudeConsoleAccountId,
|
||||
geminiAccountId,
|
||||
openaiAccountId,
|
||||
bedrockAccountId,
|
||||
permissions,
|
||||
enableModelRestriction,
|
||||
restrictedModels,
|
||||
@@ -696,6 +699,11 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.openaiAccountId = openaiAccountId || ''
|
||||
}
|
||||
|
||||
if (bedrockAccountId !== undefined) {
|
||||
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
||||
updates.bedrockAccountId = bedrockAccountId || ''
|
||||
}
|
||||
|
||||
if (permissions !== undefined) {
|
||||
// 验证权限值
|
||||
if (!['claude', 'gemini', 'openai', 'all'].includes(permissions)) {
|
||||
|
||||
@@ -20,6 +20,7 @@ class ApiKeyService {
|
||||
claudeConsoleAccountId = null,
|
||||
geminiAccountId = null,
|
||||
openaiAccountId = null,
|
||||
bedrockAccountId = null, // 添加 Bedrock 账号ID支持
|
||||
permissions = 'all', // 'claude', 'gemini', 'openai', 'all'
|
||||
isActive = true,
|
||||
concurrencyLimit = 0,
|
||||
@@ -52,6 +53,7 @@ class ApiKeyService {
|
||||
claudeConsoleAccountId: claudeConsoleAccountId || '',
|
||||
geminiAccountId: geminiAccountId || '',
|
||||
openaiAccountId: openaiAccountId || '',
|
||||
bedrockAccountId: bedrockAccountId || '', // 添加 Bedrock 账号ID
|
||||
permissions: permissions || 'all',
|
||||
enableModelRestriction: String(enableModelRestriction),
|
||||
restrictedModels: JSON.stringify(restrictedModels || []),
|
||||
@@ -84,6 +86,7 @@ class ApiKeyService {
|
||||
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
|
||||
geminiAccountId: keyData.geminiAccountId,
|
||||
openaiAccountId: keyData.openaiAccountId,
|
||||
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
||||
permissions: keyData.permissions,
|
||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||
restrictedModels: JSON.parse(keyData.restrictedModels),
|
||||
@@ -171,6 +174,7 @@ class ApiKeyService {
|
||||
claudeConsoleAccountId: keyData.claudeConsoleAccountId,
|
||||
geminiAccountId: keyData.geminiAccountId,
|
||||
openaiAccountId: keyData.openaiAccountId,
|
||||
bedrockAccountId: keyData.bedrockAccountId, // 添加 Bedrock 账号ID
|
||||
permissions: keyData.permissions || 'all',
|
||||
tokenLimit: parseInt(keyData.tokenLimit),
|
||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||
@@ -304,6 +308,7 @@ class ApiKeyService {
|
||||
'claudeConsoleAccountId',
|
||||
'geminiAccountId',
|
||||
'openaiAccountId',
|
||||
'bedrockAccountId', // 添加 Bedrock 账号ID
|
||||
'permissions',
|
||||
'expiresAt',
|
||||
'enableModelRestriction',
|
||||
|
||||
@@ -432,6 +432,11 @@ class ClaudeAccountService {
|
||||
lastUsedAt: account.lastUsedAt,
|
||||
lastRefreshAt: account.lastRefreshAt,
|
||||
expiresAt: account.expiresAt,
|
||||
// 添加 scopes 字段用于判断认证方式
|
||||
// 处理空字符串的情况,避免返回 ['']
|
||||
scopes: account.scopes && account.scopes.trim() ? account.scopes.split(' ') : [],
|
||||
// 添加 refreshToken 是否存在的标记(不返回实际值)
|
||||
hasRefreshToken: !!account.refreshToken,
|
||||
// 添加套餐信息(如果存在)
|
||||
subscriptionInfo: account.subscriptionInfo
|
||||
? JSON.parse(account.subscriptionInfo)
|
||||
|
||||
@@ -291,7 +291,8 @@ async function createAccount(accountData) {
|
||||
accessToken: accessToken ? encrypt(accessToken) : '',
|
||||
refreshToken: refreshToken ? encrypt(refreshToken) : '',
|
||||
expiresAt,
|
||||
scopes: accountData.scopes || OAUTH_SCOPES.join(' '),
|
||||
// 只有OAuth方式才有scopes,手动添加的没有
|
||||
scopes: accountData.geminiOauth ? accountData.scopes || OAUTH_SCOPES.join(' ') : '',
|
||||
|
||||
// 代理设置
|
||||
proxy: accountData.proxy ? JSON.stringify(accountData.proxy) : '',
|
||||
@@ -551,6 +552,12 @@ async function getAllAccounts() {
|
||||
geminiOauth: accountData.geminiOauth ? '[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
|
||||
? {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 config = require('../../config/config')
|
||||
const logger = require('../utils/logger')
|
||||
// const { maskToken } = require('../utils/tokenMask')
|
||||
@@ -65,15 +68,85 @@ function decrypt(text) {
|
||||
}
|
||||
|
||||
// 刷新访问令牌
|
||||
async function refreshAccessToken(_refreshToken) {
|
||||
async function refreshAccessToken(refreshToken, proxy = null) {
|
||||
try {
|
||||
// OpenAI OAuth token 刷新实现
|
||||
// TODO: 实现具体的 OpenAI OAuth token 刷新逻辑
|
||||
logger.warn('OpenAI token refresh not yet implemented')
|
||||
return null
|
||||
// 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秒超时
|
||||
}
|
||||
|
||||
// 配置代理(如果有)
|
||||
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 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) {
|
||||
logger.error('Error refreshing OpenAI access token:', error)
|
||||
throw 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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,17 +175,41 @@ async function refreshAccountToken(accountId) {
|
||||
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)
|
||||
const newTokens = await refreshAccessToken(refreshToken, proxy)
|
||||
if (!newTokens) {
|
||||
throw new Error('Failed to refresh token')
|
||||
}
|
||||
|
||||
// 更新账户信息
|
||||
await updateAccount(accountId, {
|
||||
// 准备更新数据
|
||||
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
|
||||
@@ -374,6 +471,12 @@ async function getAllAccounts() {
|
||||
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
|
||||
? {
|
||||
|
||||
@@ -35,6 +35,28 @@ class UnifiedOpenAIScheduler {
|
||||
// 普通专属账户
|
||||
const boundAccount = await openaiAccountService.getAccount(apiKeyData.openaiAccountId)
|
||||
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
|
||||
// 检查是否被限流
|
||||
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
||||
if (isRateLimited) {
|
||||
const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited`
|
||||
logger.warn(`⚠️ ${errorMsg}`)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
// 专属账户:可选的模型检查(只有明确配置了supportedModels且不为空才检查)
|
||||
if (
|
||||
requestedModel &&
|
||||
boundAccount.supportedModels &&
|
||||
boundAccount.supportedModels.length > 0
|
||||
) {
|
||||
const modelSupported = boundAccount.supportedModels.includes(requestedModel)
|
||||
if (!modelSupported) {
|
||||
const errorMsg = `Dedicated account ${boundAccount.name} does not support model ${requestedModel}`
|
||||
logger.warn(`⚠️ ${errorMsg}`)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`🎯 Using bound dedicated OpenAI account: ${boundAccount.name} (${apiKeyData.openaiAccountId}) for API key ${apiKeyData.name}`
|
||||
)
|
||||
@@ -45,9 +67,12 @@ class UnifiedOpenAIScheduler {
|
||||
accountType: 'openai'
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
`⚠️ Bound OpenAI account ${apiKeyData.openaiAccountId} is not available, falling back to pool`
|
||||
)
|
||||
// 专属账户不可用时直接报错,不降级到共享池
|
||||
const errorMsg = boundAccount
|
||||
? `Dedicated account ${boundAccount.name} is not available (inactive or error status)`
|
||||
: `Dedicated account ${apiKeyData.openaiAccountId} not found`
|
||||
logger.warn(`⚠️ ${errorMsg}`)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +115,12 @@ class UnifiedOpenAIScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级和最后使用时间排序
|
||||
const sortedAccounts = this._sortAccountsByPriority(availableAccounts)
|
||||
// 按最后使用时间排序(最久未使用的优先,与 Claude 保持一致)
|
||||
const sortedAccounts = availableAccounts.sort((a, b) => {
|
||||
const aLastUsed = new Date(a.lastUsedAt || 0).getTime()
|
||||
const bLastUsed = new Date(b.lastUsedAt || 0).getTime()
|
||||
return aLastUsed - bLastUsed // 最久未使用的优先
|
||||
})
|
||||
|
||||
// 选择第一个账户
|
||||
const selectedAccount = sortedAccounts[0]
|
||||
@@ -109,7 +138,7 @@ class UnifiedOpenAIScheduler {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}`
|
||||
`🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) for API key ${apiKeyData.name}`
|
||||
)
|
||||
|
||||
// 更新账户的最后使用时间
|
||||
@@ -125,49 +154,12 @@ class UnifiedOpenAIScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 📋 获取所有可用账户
|
||||
// 📋 获取所有可用账户(仅共享池)
|
||||
async _getAllAvailableAccounts(apiKeyData, requestedModel = null) {
|
||||
const availableAccounts = []
|
||||
|
||||
// 如果API Key绑定了专属账户,优先返回
|
||||
if (apiKeyData.openaiAccountId) {
|
||||
const boundAccount = await openaiAccountService.getAccount(apiKeyData.openaiAccountId)
|
||||
if (boundAccount && boundAccount.isActive === 'true' && boundAccount.status !== 'error') {
|
||||
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
||||
if (!isRateLimited) {
|
||||
// 检查模型支持(仅在明确设置了supportedModels且不为空时才检查)
|
||||
// 如果没有设置supportedModels或为空数组,则支持所有模型
|
||||
if (
|
||||
requestedModel &&
|
||||
boundAccount.supportedModels &&
|
||||
boundAccount.supportedModels.length > 0
|
||||
) {
|
||||
const modelSupported = boundAccount.supportedModels.includes(requestedModel)
|
||||
if (!modelSupported) {
|
||||
logger.warn(
|
||||
`⚠️ Bound OpenAI account ${boundAccount.name} does not support model ${requestedModel}`
|
||||
)
|
||||
return availableAccounts
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`🎯 Using bound dedicated OpenAI account: ${boundAccount.name} (${apiKeyData.openaiAccountId})`
|
||||
)
|
||||
return [
|
||||
{
|
||||
...boundAccount,
|
||||
accountId: boundAccount.id,
|
||||
accountType: 'openai',
|
||||
priority: parseInt(boundAccount.priority) || 50,
|
||||
lastUsedAt: boundAccount.lastUsedAt || '0'
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
logger.warn(`⚠️ Bound OpenAI account ${apiKeyData.openaiAccountId} is not available`)
|
||||
}
|
||||
}
|
||||
// 注意:专属账户的处理已经在 selectAccountForApiKey 中完成
|
||||
// 这里只处理共享池账户
|
||||
|
||||
// 获取所有OpenAI账户(共享池)
|
||||
const openaiAccounts = await openaiAccountService.getAllAccounts()
|
||||
@@ -221,20 +213,20 @@ class UnifiedOpenAIScheduler {
|
||||
return availableAccounts
|
||||
}
|
||||
|
||||
// 🔢 按优先级和最后使用时间排序账户
|
||||
_sortAccountsByPriority(accounts) {
|
||||
return accounts.sort((a, b) => {
|
||||
// 首先按优先级排序(数字越小优先级越高)
|
||||
if (a.priority !== b.priority) {
|
||||
return a.priority - b.priority
|
||||
}
|
||||
// 🔢 按优先级和最后使用时间排序账户(已废弃,改为与 Claude 保持一致,只按最后使用时间排序)
|
||||
// _sortAccountsByPriority(accounts) {
|
||||
// return accounts.sort((a, b) => {
|
||||
// // 首先按优先级排序(数字越小优先级越高)
|
||||
// if (a.priority !== b.priority) {
|
||||
// return a.priority - b.priority
|
||||
// }
|
||||
|
||||
// 优先级相同时,按最后使用时间排序(最久未使用的优先)
|
||||
const aLastUsed = new Date(a.lastUsedAt || 0).getTime()
|
||||
const bLastUsed = new Date(b.lastUsedAt || 0).getTime()
|
||||
return aLastUsed - bLastUsed
|
||||
})
|
||||
}
|
||||
// // 优先级相同时,按最后使用时间排序(最久未使用的优先)
|
||||
// const aLastUsed = new Date(a.lastUsedAt || 0).getTime()
|
||||
// const bLastUsed = new Date(b.lastUsedAt || 0).getTime()
|
||||
// return aLastUsed - bLastUsed
|
||||
// })
|
||||
// }
|
||||
|
||||
// 🔍 检查账户是否可用
|
||||
async _isAccountAvailable(accountId, accountType) {
|
||||
@@ -449,8 +441,12 @@ class UnifiedOpenAIScheduler {
|
||||
throw new Error(`No available accounts in group ${group.name}`)
|
||||
}
|
||||
|
||||
// 按优先级和最后使用时间排序
|
||||
const sortedAccounts = this._sortAccountsByPriority(availableAccounts)
|
||||
// 按最后使用时间排序(最久未使用的优先,与 Claude 保持一致)
|
||||
const sortedAccounts = availableAccounts.sort((a, b) => {
|
||||
const aLastUsed = new Date(a.lastUsedAt || 0).getTime()
|
||||
const bLastUsed = new Date(b.lastUsedAt || 0).getTime()
|
||||
return aLastUsed - bLastUsed // 最久未使用的优先
|
||||
})
|
||||
|
||||
// 选择第一个账户
|
||||
const selectedAccount = sortedAccounts[0]
|
||||
@@ -468,7 +464,7 @@ class UnifiedOpenAIScheduler {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`🎯 Selected account from group: ${selectedAccount.name} (${selectedAccount.accountId}) with priority ${selectedAccount.priority}`
|
||||
`🎯 Selected account from group: ${selectedAccount.name} (${selectedAccount.accountId})`
|
||||
)
|
||||
|
||||
// 更新账户的最后使用时间
|
||||
|
||||
Reference in New Issue
Block a user