diff --git a/scripts/test-openai-refresh.js b/scripts/test-openai-refresh.js
new file mode 100644
index 00000000..5158e9f8
--- /dev/null
+++ b/scripts/test-openai-refresh.js
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+
+/**
+ * OpenAI Token 刷新功能测试脚本
+ * 用于测试 openaiAccountService 的 token 刷新功能
+ */
+
+const openaiAccountService = require('../src/services/openaiAccountService')
+const logger = require('../src/utils/logger')
+
+// 测试配置(可以通过环境变量或命令行参数传入)
+const TEST_REFRESH_TOKEN = process.env.OPENAI_REFRESH_TOKEN || process.argv[2]
+
+async function testRefreshToken() {
+ if (!TEST_REFRESH_TOKEN) {
+ console.error('❌ 请提供 refresh token 作为参数或设置环境变量 OPENAI_REFRESH_TOKEN')
+ console.log('使用方法:')
+ console.log(' node scripts/test-openai-refresh.js ')
+ console.log(' 或')
+ console.log(' OPENAI_REFRESH_TOKEN= node scripts/test-openai-refresh.js')
+ process.exit(1)
+ }
+
+ console.log('🔄 开始测试 OpenAI token 刷新功能...\n')
+
+ try {
+ // 测试不带代理的刷新
+ console.log('1️⃣ 测试直接刷新(无代理)...')
+ const result = await openaiAccountService.refreshAccessToken(TEST_REFRESH_TOKEN)
+
+ console.log('✅ 刷新成功!')
+ console.log(' Access Token:', result.access_token ? result.access_token.substring(0, 30) + '...' : 'N/A')
+ console.log(' ID Token:', result.id_token ? result.id_token.substring(0, 30) + '...' : 'N/A')
+ console.log(' Refresh Token:', result.refresh_token ? result.refresh_token.substring(0, 30) + '...' : 'N/A')
+ console.log(' 有效期:', result.expires_in, '秒')
+ console.log(' 过期时间:', new Date(result.expiry_date).toLocaleString())
+
+ // 如果返回了新的 refresh token
+ if (result.refresh_token && result.refresh_token !== TEST_REFRESH_TOKEN) {
+ console.log('\n⚠️ 注意:收到了新的 refresh token,请保存以供后续使用')
+ }
+
+ // 测试带代理的刷新(如果配置了代理)
+ if (process.env.PROXY_HOST && process.env.PROXY_PORT) {
+ console.log('\n2️⃣ 测试通过代理刷新...')
+ const proxy = {
+ type: process.env.PROXY_TYPE || 'http',
+ host: process.env.PROXY_HOST,
+ port: parseInt(process.env.PROXY_PORT),
+ username: process.env.PROXY_USERNAME,
+ password: process.env.PROXY_PASSWORD
+ }
+
+ console.log(' 代理配置:', `${proxy.type}://${proxy.host}:${proxy.port}`)
+
+ const proxyResult = await openaiAccountService.refreshAccessToken(
+ result.refresh_token || TEST_REFRESH_TOKEN,
+ proxy
+ )
+
+ console.log('✅ 通过代理刷新成功!')
+ console.log(' Access Token:', proxyResult.access_token ? proxyResult.access_token.substring(0, 30) + '...' : 'N/A')
+ }
+
+ // 测试完整的账户刷新流程(如果提供了账户ID)
+ if (process.env.OPENAI_ACCOUNT_ID) {
+ console.log('\n3️⃣ 测试账户刷新流程...')
+ console.log(' 账户ID:', process.env.OPENAI_ACCOUNT_ID)
+
+ try {
+ const account = await openaiAccountService.getAccount(process.env.OPENAI_ACCOUNT_ID)
+ if (account) {
+ console.log(' 账户名称:', account.name)
+ console.log(' 当前过期时间:', account.expiresAt)
+
+ const refreshResult = await openaiAccountService.refreshAccountToken(process.env.OPENAI_ACCOUNT_ID)
+ console.log('✅ 账户 token 刷新成功!')
+ console.log(' 新的过期时间:', new Date(refreshResult.expiry_date).toLocaleString())
+ }
+ } catch (error) {
+ console.log('⚠️ 账户刷新测试失败:', error.message)
+ }
+ }
+
+ console.log('\n✅ 所有测试完成!')
+
+ } catch (error) {
+ console.error('\n❌ 测试失败:', error.message)
+ if (error.response) {
+ console.error('响应状态:', error.response.status)
+ console.error('响应数据:', error.response.data)
+ }
+ process.exit(1)
+ }
+}
+
+// 运行测试
+testRefreshToken().then(() => {
+ process.exit(0)
+}).catch((error) => {
+ console.error('Unexpected error:', error)
+ process.exit(1)
+})
\ No newline at end of file
diff --git a/src/middleware/auth.js b/src/middleware/auth.js
index 660bb72d..38c43485 100644
--- a/src/middleware/auth.js
+++ b/src/middleware/auth.js
@@ -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,
diff --git a/src/routes/admin.js b/src/routes/admin.js
index 3ecf71ff..f6e9cf2d 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -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)) {
diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js
index 4309e665..fcb49e34 100644
--- a/src/services/apiKeyService.js
+++ b/src/services/apiKeyService.js
@@ -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',
diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js
index 1dc26459..23f0c9d4 100644
--- a/src/services/claudeAccountService.js
+++ b/src/services/claudeAccountService.js
@@ -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)
diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js
index ee8d2b60..050eac9f 100644
--- a/src/services/geminiAccountService.js
+++ b/src/services/geminiAccountService.js
@@ -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
? {
diff --git a/src/services/openaiAccountService.js b/src/services/openaiAccountService.js
index fe2baf26..d5e13cf6 100644
--- a/src/services/openaiAccountService.js
+++ b/src/services/openaiAccountService.js
@@ -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
? {
diff --git a/src/services/unifiedOpenAIScheduler.js b/src/services/unifiedOpenAIScheduler.js
index 93b5e108..f800621c 100644
--- a/src/services/unifiedOpenAIScheduler.js
+++ b/src/services/unifiedOpenAIScheduler.js
@@ -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})`
)
// 更新账户的最后使用时间
diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue
index c8dcdcb8..661bc7f4 100644
--- a/web/admin-spa/src/components/accounts/AccountForm.vue
+++ b/web/admin-spa/src/components/accounts/AccountForm.vue
@@ -584,14 +584,8 @@
-
-
+
+
@@ -1019,14 +1013,8 @@
-
-
+
+
{
if (form.value.projectId) {
data.projectId = form.value.projectId
}
+ // 添加 Gemini 优先级
+ data.priority = form.value.priority || 50
} else if (form.value.platform === 'openai') {
data.openaiOauth = tokenInfo.tokens || tokenInfo
data.accountInfo = tokenInfo.accountInfo
@@ -1869,7 +1859,7 @@ const createAccount = async () => {
accessToken: form.value.accessToken,
refreshToken: form.value.refreshToken || '',
expiresAt: Date.now() + expiresInMs,
- scopes: ['user:inference']
+ scopes: [] // 手动添加没有 scopes
}
data.priority = form.value.priority || 50
// 添加订阅类型信息
@@ -1896,6 +1886,9 @@ const createAccount = async () => {
if (form.value.projectId) {
data.projectId = form.value.projectId
}
+
+ // 添加 Gemini 优先级
+ data.priority = form.value.priority || 50
} else if (form.value.platform === 'openai') {
// OpenAI手动模式需要构建openaiOauth对象
const expiresInMs = form.value.refreshToken
@@ -2058,7 +2051,7 @@ const updateAccount = async () => {
accessToken: form.value.accessToken || '',
refreshToken: form.value.refreshToken || '',
expiresAt: Date.now() + expiresInMs,
- scopes: ['user:inference']
+ scopes: props.account.scopes || [] // 保持原有的 scopes,如果没有则为空数组
}
} else if (props.account.platform === 'gemini') {
// Gemini需要构建geminiOauth对象
@@ -2109,6 +2102,11 @@ const updateAccount = async () => {
data.priority = form.value.priority || 50
}
+ // Gemini 账号优先级更新
+ if (props.account.platform === 'gemini') {
+ data.priority = form.value.priority || 50
+ }
+
// Claude Console 特定更新
if (props.account.platform === 'claude-console') {
data.apiUrl = form.value.apiUrl
diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
index 1949eb5f..a0068ddb 100644
--- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
+++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
@@ -436,6 +436,18 @@
platform="openai"
/>
+
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
@@ -618,6 +630,7 @@ const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
+ bedrock: [], // 添加 Bedrock 账号列表
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
@@ -658,6 +671,7 @@ const form = reactive({
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
+ bedrockAccountId: '', // 添加 Bedrock 账号ID
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -676,6 +690,7 @@ onMounted(async () => {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
+ bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
@@ -687,13 +702,15 @@ onMounted(async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
- const [claudeData, claudeConsoleData, geminiData, openaiData, groupsData] = await Promise.all([
- apiClient.get('/admin/claude-accounts'),
- apiClient.get('/admin/claude-console-accounts'),
- apiClient.get('/admin/gemini-accounts'),
- apiClient.get('/admin/openai-accounts'),
- apiClient.get('/admin/account-groups')
- ])
+ const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
+ await Promise.all([
+ apiClient.get('/admin/claude-accounts'),
+ apiClient.get('/admin/claude-console-accounts'),
+ apiClient.get('/admin/gemini-accounts'),
+ apiClient.get('/admin/openai-accounts'),
+ apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
+ apiClient.get('/admin/account-groups')
+ ])
// 合并Claude OAuth账户和Claude Console账户
const claudeAccounts = []
@@ -734,6 +751,13 @@ const refreshAccounts = async () => {
}))
}
+ if (bedrockData.success) {
+ localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
+ ...account,
+ isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
+ }))
+ }
+
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
@@ -939,6 +963,11 @@ const createApiKey = async () => {
baseData.openaiAccountId = form.openaiAccountId
}
+ // Bedrock账户绑定
+ if (form.bedrockAccountId) {
+ baseData.bedrockAccountId = form.bedrockAccountId
+ }
+
if (form.createType === 'single') {
// 单个创建
const data = {
diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue
index 796768d1..65374e8d 100644
--- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue
+++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue
@@ -339,6 +339,18 @@
platform="openai"
/>
+
修改绑定账号将影响此API Key的请求路由
@@ -522,6 +534,7 @@ const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
+ bedrock: [], // 添加 Bedrock 账号列表
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
@@ -551,6 +564,7 @@ const form = reactive({
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
+ bedrockAccountId: '', // 添加 Bedrock 账号ID
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -673,6 +687,13 @@ const updateApiKey = async () => {
data.openaiAccountId = null
}
+ // Bedrock账户绑定
+ if (form.bedrockAccountId) {
+ data.bedrockAccountId = form.bedrockAccountId
+ } else {
+ data.bedrockAccountId = null
+ }
+
// 模型限制 - 始终提交这些字段
data.enableModelRestriction = form.enableModelRestriction
data.restrictedModels = form.restrictedModels
@@ -703,13 +724,15 @@ const updateApiKey = async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
- const [claudeData, claudeConsoleData, geminiData, openaiData, groupsData] = await Promise.all([
- apiClient.get('/admin/claude-accounts'),
- apiClient.get('/admin/claude-console-accounts'),
- apiClient.get('/admin/gemini-accounts'),
- apiClient.get('/admin/openai-accounts'),
- apiClient.get('/admin/account-groups')
- ])
+ const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
+ await Promise.all([
+ apiClient.get('/admin/claude-accounts'),
+ apiClient.get('/admin/claude-console-accounts'),
+ apiClient.get('/admin/gemini-accounts'),
+ apiClient.get('/admin/openai-accounts'),
+ apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
+ apiClient.get('/admin/account-groups')
+ ])
// 合并Claude OAuth账户和Claude Console账户
const claudeAccounts = []
@@ -750,6 +773,13 @@ const refreshAccounts = async () => {
}))
}
+ if (bedrockData.success) {
+ localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
+ ...account,
+ isDedicated: account.accountType === 'dedicated'
+ }))
+ }
+
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
@@ -778,6 +808,7 @@ onMounted(async () => {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
+ bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
@@ -799,6 +830,7 @@ onMounted(async () => {
}
form.geminiAccountId = props.apiKey.geminiAccountId || ''
form.openaiAccountId = props.apiKey.openaiAccountId || ''
+ form.bedrockAccountId = props.apiKey.bedrockAccountId || '' // 添加 Bedrock 账号ID初始化
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []
form.tags = props.apiKey.tags || []
diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue
index d05e796a..5a751f44 100644
--- a/web/admin-spa/src/views/AccountsView.vue
+++ b/web/admin-spa/src/views/AccountsView.vue
@@ -261,7 +261,7 @@
Gemini
- {{ account.scopes && account.scopes.length > 0 ? 'OAuth' : '传统' }}
+ {{ getGeminiAuthType() }}
OpenAi
- Oauth
+ {{ getOpenAIAuthType() }}
- {{ account.scopes && account.scopes.length > 0 ? 'OAuth' : '传统' }}
+ {{ getClaudeAuthType(account) }}
@@ -491,21 +493,6 @@
-
-
-
|