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 @@
-
- -