From 549c95eb8061c619333e0d67f3f5b3d148f597a9 Mon Sep 17 00:00:00 2001 From: juenjunli Date: Sat, 10 Jan 2026 14:13:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=20AWS=20Bedrock=20=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E6=B7=BB=E5=8A=A0=20Bearer=20Token=20=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 credentialType 字段支持 access_key 和 bearer_token 两种认证方式 - 实现 Bedrock 账户的 testAccountConnection 方法,支持 SSE 流式测试 - 前端账户表单增加认证类型选择器,自动切换输入字段 - 前端测试模态框根据账户类型自动选择测试模型(Bearer Token 使用 Sonnet 4.5,Access Key 使用 Haiku) - 改进测试接口错误处理,避免响应流重复关闭 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/routes/admin/bedrockAccounts.js | 25 +- src/services/accountBalanceService.js | 31 +- src/services/bedrockAccountService.js | 272 +++++++++++++++- src/services/bedrockRelayService.js | 6 +- .../src/components/accounts/AccountForm.vue | 307 ++++++++++++++---- .../components/accounts/AccountTestModal.vue | 73 ++++- web/admin-spa/src/views/AccountsView.vue | 5 +- 7 files changed, 625 insertions(+), 94 deletions(-) diff --git a/src/routes/admin/bedrockAccounts.js b/src/routes/admin/bedrockAccounts.js index c9d3a17c..4b6a365b 100644 --- a/src/routes/admin/bedrockAccounts.js +++ b/src/routes/admin/bedrockAccounts.js @@ -122,6 +122,7 @@ router.post('/', authenticateAdmin, async (req, res) => { description, region, awsCredentials, + bearerToken, defaultModel, priority, accountType, @@ -145,9 +146,9 @@ router.post('/', authenticateAdmin, async (req, res) => { } // 验证credentialType的有效性 - if (credentialType && !['default', 'access_key', 'bearer_token'].includes(credentialType)) { + if (credentialType && !['access_key', 'bearer_token'].includes(credentialType)) { return res.status(400).json({ - error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' + error: 'Invalid credential type. Must be "access_key" or "bearer_token"' }) } @@ -156,10 +157,11 @@ router.post('/', authenticateAdmin, async (req, res) => { description: description || '', region: region || 'us-east-1', awsCredentials, + bearerToken, defaultModel, priority: priority || 50, accountType: accountType || 'shared', - credentialType: credentialType || 'default' + credentialType: credentialType || 'access_key' }) if (!result.success) { @@ -206,10 +208,10 @@ router.put('/:accountId', authenticateAdmin, async (req, res) => { // 验证credentialType的有效性 if ( mappedUpdates.credentialType && - !['default', 'access_key', 'bearer_token'].includes(mappedUpdates.credentialType) + !['access_key', 'bearer_token'].includes(mappedUpdates.credentialType) ) { return res.status(400).json({ - error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' + error: 'Invalid credential type. Must be "access_key" or "bearer_token"' }) } @@ -349,22 +351,15 @@ router.put('/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) } }) -// 测试Bedrock账户连接 +// 测试Bedrock账户连接(SSE 流式) router.post('/:accountId/test', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params - const result = await bedrockAccountService.testAccount(accountId) - - if (!result.success) { - return res.status(500).json({ error: 'Account test failed', message: result.error }) - } - - logger.success(`🧪 Admin tested Bedrock account: ${accountId} - ${result.data.status}`) - return res.json({ success: true, data: result.data }) + await bedrockAccountService.testAccountConnection(accountId, res) } catch (error) { logger.error('❌ Failed to test Bedrock account:', error) - return res.status(500).json({ error: 'Failed to test Bedrock account', message: error.message }) + // 错误已在服务层处理,这里仅做日志记录 } }) diff --git a/src/services/accountBalanceService.js b/src/services/accountBalanceService.js index 8c136e80..ec25f171 100644 --- a/src/services/accountBalanceService.js +++ b/src/services/accountBalanceService.js @@ -226,7 +226,15 @@ class AccountBalanceService { return null } - return await service.getAccount(accountId) + const result = await service.getAccount(accountId) + + // 处理不同服务返回格式的差异 + // Bedrock/CCR/Droid 等服务返回 { success, data } 格式 + if (result && typeof result === 'object' && 'success' in result && 'data' in result) { + return result.success ? result.data : null + } + + return result } async getAllAccountsByPlatform(platform) { @@ -275,10 +283,27 @@ class AccountBalanceService { const accountId = account?.id if (!accountId) { - throw new Error('账户缺少 id') + // 如果账户缺少 id,返回空响应而不是抛出错误,避免接口报错和UI错误 + this.logger.warn('账户缺少 id,返回空余额数据', { account, platform }) + return this._buildResponse( + { + status: 'error', + errorMessage: '账户数据异常', + balance: null, + currency: 'USD', + quota: null, + statistics: {}, + lastRefreshAt: new Date().toISOString() + }, + 'unknown', + platform, + 'local', + null, + { scriptEnabled: false, scriptConfigured: false } + ) } - // 余额脚本配置状态(用于前端控制“刷新余额”按钮) + // 余额脚本配置状态(用于前端控制"刷新余额"按钮) let scriptConfig = null let scriptConfigured = false if (typeof this.redis?.getBalanceScriptConfig === 'function') { diff --git a/src/services/bedrockAccountService.js b/src/services/bedrockAccountService.js index cd404b13..44c7fa95 100644 --- a/src/services/bedrockAccountService.js +++ b/src/services/bedrockAccountService.js @@ -35,12 +35,13 @@ class BedrockAccountService { description = '', region = process.env.AWS_REGION || 'us-east-1', awsCredentials = null, // { accessKeyId, secretAccessKey, sessionToken } + bearerToken = null, // AWS Bearer Token for Bedrock API Keys defaultModel = 'us.anthropic.claude-sonnet-4-20250514-v1:0', isActive = true, accountType = 'shared', // 'dedicated' or 'shared' priority = 50, // 调度优先级 (1-100,数字越小优先级越高) schedulable = true, // 是否可被调度 - credentialType = 'default' // 'default', 'access_key', 'bearer_token' + credentialType = 'access_key' // 'access_key', 'bearer_token'(默认为 access_key) } = options const accountId = uuidv4() @@ -71,6 +72,11 @@ class BedrockAccountService { accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials) } + // 加密存储 Bearer Token + if (bearerToken) { + accountData.bearerToken = this._encryptAwsCredentials({ token: bearerToken }) + } + const client = redis.getClientSafe() await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData)) @@ -106,9 +112,85 @@ class BedrockAccountService { const account = JSON.parse(accountData) - // 解密AWS凭证用于内部使用 - if (account.awsCredentials) { - account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials) + // 根据凭证类型解密对应的凭证 + // 增强逻辑:优先按照 credentialType 解密,如果字段不存在则尝试解密实际存在的字段(兜底) + try { + let accessKeyDecrypted = false + let bearerTokenDecrypted = false + + // 第一步:按照 credentialType 尝试解密对应的凭证 + if (account.credentialType === 'access_key' && account.awsCredentials) { + // Access Key 模式:解密 AWS 凭证 + account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials) + accessKeyDecrypted = true + logger.debug( + `🔓 解密 Access Key 成功 - ID: ${accountId}, 类型: ${account.credentialType}` + ) + } else if (account.credentialType === 'bearer_token' && account.bearerToken) { + // Bearer Token 模式:解密 Bearer Token + const decrypted = this._decryptAwsCredentials(account.bearerToken) + account.bearerToken = decrypted.token + bearerTokenDecrypted = true + logger.debug( + `🔓 解密 Bearer Token 成功 - ID: ${accountId}, 类型: ${account.credentialType}` + ) + } else if (!account.credentialType || account.credentialType === 'default') { + // 向后兼容:旧版本账号可能没有 credentialType 字段,尝试解密所有存在的凭证 + if (account.awsCredentials) { + account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials) + accessKeyDecrypted = true + } + if (account.bearerToken) { + const decrypted = this._decryptAwsCredentials(account.bearerToken) + account.bearerToken = decrypted.token + bearerTokenDecrypted = true + } + logger.debug( + `🔓 兼容模式解密 - ID: ${accountId}, Access Key: ${accessKeyDecrypted}, Bearer Token: ${bearerTokenDecrypted}` + ) + } + + // 第二步:兜底逻辑 - 如果按照 credentialType 没有解密到任何凭证,尝试解密实际存在的字段 + if (!accessKeyDecrypted && !bearerTokenDecrypted) { + logger.warn( + `⚠️ credentialType="${account.credentialType}" 与实际字段不匹配,尝试兜底解密 - ID: ${accountId}` + ) + if (account.awsCredentials) { + account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials) + accessKeyDecrypted = true + logger.warn( + `🔓 兜底解密 Access Key 成功 - ID: ${accountId}, credentialType 应为 'access_key'` + ) + } + if (account.bearerToken) { + const decrypted = this._decryptAwsCredentials(account.bearerToken) + account.bearerToken = decrypted.token + bearerTokenDecrypted = true + logger.warn( + `🔓 兜底解密 Bearer Token 成功 - ID: ${accountId}, credentialType 应为 'bearer_token'` + ) + } + } + + // 验证至少解密了一种凭证 + if (!accessKeyDecrypted && !bearerTokenDecrypted) { + logger.error( + `❌ 未找到任何凭证可解密 - ID: ${accountId}, credentialType: ${account.credentialType}, hasAwsCredentials: ${!!account.awsCredentials}, hasBearerToken: ${!!account.bearerToken}` + ) + return { + success: false, + error: 'No valid credentials found in account data' + } + } + } catch (decryptError) { + logger.error( + `❌ 解密Bedrock凭证失败 - ID: ${accountId}, 类型: ${account.credentialType}`, + decryptError + ) + return { + success: false, + error: `Credentials decryption failed: ${decryptError.message}` + } } logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`) @@ -155,7 +237,11 @@ class BedrockAccountService { updatedAt: account.updatedAt, type: 'bedrock', platform: 'bedrock', - hasCredentials: !!account.awsCredentials + // 根据凭证类型判断是否有凭证 + hasCredentials: + account.credentialType === 'bearer_token' + ? !!account.bearerToken + : !!account.awsCredentials }) } } @@ -235,6 +321,15 @@ class BedrockAccountService { logger.info(`🔐 重新加密Bedrock账户凭证 - ID: ${accountId}`) } + // 更新 Bearer Token + if (updates.bearerToken !== undefined) { + if (updates.bearerToken) { + account.bearerToken = this._encryptAwsCredentials({ token: updates.bearerToken }) + } else { + delete account.bearerToken + } + } + // ✅ 直接保存 subscriptionExpiresAt(如果提供) // Bedrock 没有 token 刷新逻辑,不会覆盖此字段 if (updates.subscriptionExpiresAt !== undefined) { @@ -345,13 +440,45 @@ class BedrockAccountService { const account = accountResult.data - logger.info(`🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}`) + logger.info( + `🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}, 凭证类型: ${account.credentialType}` + ) - // 尝试获取模型列表来测试连接 + // 验证凭证是否已解密 + const hasValidCredentials = + (account.credentialType === 'access_key' && account.awsCredentials) || + (account.credentialType === 'bearer_token' && account.bearerToken) || + (!account.credentialType && (account.awsCredentials || account.bearerToken)) + + if (!hasValidCredentials) { + logger.error( + `❌ 测试失败:账户没有有效凭证 - ID: ${accountId}, credentialType: ${account.credentialType}` + ) + return { + success: false, + error: 'No valid credentials found after decryption' + } + } + + // 尝试创建 Bedrock 客户端来验证凭证格式 + try { + bedrockRelayService._getBedrockClient(account.region, account) + logger.debug(`✅ Bedrock客户端创建成功 - ID: ${accountId}`) + } catch (clientError) { + logger.error(`❌ 创建Bedrock客户端失败 - ID: ${accountId}`, clientError) + return { + success: false, + error: `Failed to create Bedrock client: ${clientError.message}` + } + } + + // 获取可用模型列表(硬编码,但至少验证了凭证格式正确) const models = await bedrockRelayService.getAvailableModels(account) if (models && models.length > 0) { - logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`) + logger.info( + `✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型, 凭证类型: ${account.credentialType}` + ) return { success: true, data: { @@ -376,6 +503,135 @@ class BedrockAccountService { } } + /** + * 🧪 测试 Bedrock 账户连接(SSE 流式返回,供前端测试页面使用) + * @param {string} accountId - 账户ID + * @param {Object} res - Express response 对象 + * @param {string} model - 测试使用的模型 + */ + async testAccountConnection(accountId, res, model = null) { + const { InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime') + + try { + // 获取账户信息 + const accountResult = await this.getAccount(accountId) + if (!accountResult.success) { + throw new Error(accountResult.error || 'Account not found') + } + + const account = accountResult.data + + // 根据账户类型选择合适的测试模型 + if (!model) { + // Access Key 模式使用 Haiku(更快更便宜) + model = account.defaultModel || 'us.anthropic.claude-3-5-haiku-20241022-v1:0' + } + + logger.info( + `🧪 Testing Bedrock account connection: ${account.name} (${accountId}), model: ${model}, credentialType: ${account.credentialType}` + ) + + // 设置 SSE 响应头 + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + res.setHeader('X-Accel-Buffering', 'no') + res.status(200) + + // 发送 test_start 事件 + res.write(`data: ${JSON.stringify({ type: 'test_start' })}\n\n`) + + // 构造测试请求体(Bedrock 格式) + const bedrockPayload = { + anthropic_version: 'bedrock-2023-05-31', + max_tokens: 256, + messages: [ + { + role: 'user', + content: + 'Hello! Please respond with a simple greeting to confirm the connection is working. And tell me who are you?' + } + ] + } + + // 获取 Bedrock 客户端 + const region = account.region || bedrockRelayService.defaultRegion + const client = bedrockRelayService._getBedrockClient(region, account) + + // 创建流式调用命令 + const command = new InvokeModelWithResponseStreamCommand({ + modelId: model, + body: JSON.stringify(bedrockPayload), + contentType: 'application/json', + accept: 'application/json' + }) + + logger.debug(`🌊 Bedrock test stream - model: ${model}, region: ${region}`) + + const startTime = Date.now() + const response = await client.send(command) + + // 处理流式响应 + // let responseText = '' + for await (const chunk of response.body) { + if (chunk.chunk) { + const chunkData = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes)) + + // 提取文本内容 + if (chunkData.type === 'content_block_delta' && chunkData.delta?.text) { + const { text } = chunkData.delta + // responseText += text + + // 发送 content 事件 + res.write(`data: ${JSON.stringify({ type: 'content', text })}\n\n`) + } + + // 检测错误 + if (chunkData.type === 'error') { + throw new Error(chunkData.error?.message || 'Bedrock API error') + } + } + } + + const duration = Date.now() - startTime + logger.info(`✅ Bedrock test completed - model: ${model}, duration: ${duration}ms`) + + // 发送 message_stop 事件(前端兼容) + res.write(`data: ${JSON.stringify({ type: 'message_stop' })}\n\n`) + + // 发送 test_complete 事件 + res.write(`data: ${JSON.stringify({ type: 'test_complete', success: true })}\n\n`) + + // 结束响应 + res.end() + + logger.info(`✅ Test request completed for Bedrock account: ${account.name}`) + } catch (error) { + logger.error(`❌ Test Bedrock account connection failed:`, error) + + // 发送错误事件给前端 + try { + // 检查响应流是否仍然可写 + if (!res.writableEnded && !res.destroyed) { + if (!res.headersSent) { + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + res.status(200) + } + const errorMsg = error.message || '测试失败' + res.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`) + res.end() + } + } catch (writeError) { + logger.error('Failed to write error to response stream:', writeError) + } + + // 不再重新抛出错误,避免路由层再次处理 + // throw error + } + } + /** * 检查账户订阅是否过期 * @param {Object} account - 账户对象 diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index d04e42b2..7bd06e56 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -48,13 +48,17 @@ class BedrockRelayService { secretAccessKey: bedrockAccount.awsCredentials.secretAccessKey, sessionToken: bedrockAccount.awsCredentials.sessionToken } + } else if (bedrockAccount?.bearerToken) { + // Bearer Token 模式:AWS SDK >= 3.400.0 会自动检测环境变量 + clientConfig.token = { token: bedrockAccount.bearerToken } + logger.debug(`🔑 使用 Bearer Token 认证 - 账户: ${bedrockAccount.name || 'unknown'}`) } else { // 检查是否有环境变量凭证 if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { clientConfig.credentials = fromEnv() } else { throw new Error( - 'AWS凭证未配置。请在Bedrock账户中配置AWS访问密钥,或设置环境变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY' + 'AWS凭证未配置。请在Bedrock账户中配置AWS访问密钥或Bearer Token,或设置环境变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY' ) } } diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 1d185fa4..bfb62258 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -852,41 +852,194 @@ -
+
+
凭证类型 * - -

- {{ errors.accessKeyId }} -

+
+ + +
+
+ + +
+
+
+ +
+

+ 使用 AWS Access Key ID 和 Secret Access Key 进行身份验证(支持临时凭证) +

+

+ 使用 AWS Bedrock API Keys 生成的 Bearer Token + 进行身份验证,更简单、权限范围更小 +

+

+ 💡 编辑模式下凭证类型不可更改,如需切换类型请重新创建账户 +

+
+
+
-
+ +
+
+ + +

+ {{ errors.accessKeyId }} +

+

+ 💡 编辑模式下,留空则保持原有 Access Key ID 不变 +

+
+ +
+ + +

+ {{ errors.secretAccessKey }} +

+

+ 💡 编辑模式下,留空则保持原有 Secret Access Key 不变 +

+
+ +
+ + +

+ 仅在使用临时 AWS 凭证时需要填写 +

+
+
+ + +
Bearer Token {{ isEdit ? '' : '*' }} -

- {{ errors.secretAccessKey }} +

+ {{ errors.bearerToken }}

+

+ 💡 编辑模式下,留空则保持原有 Bearer Token 不变 +

+
+
+ +
+

Bearer Token 说明:

+
    +
  • 输入 AWS Bedrock API Keys 生成的 Bearer Token
  • +
  • Bearer Token 仅限 Bedrock 服务访问,权限范围更小
  • +
  • 相比 Access Key 更简单,无需 Secret Key
  • +
  • + 参考:AWS 官方文档 +
  • +
+
+
+
+
{{ errors.region }}

-
+
- -
+ +

常用 AWS 区域参考:

• us-east-1 (美国东部) @@ -915,27 +1070,14 @@ • ap-northeast-1 (东京) • eu-central-1 (法兰克福)
-

💡 请输入完整的区域代码,如 us-east-1

+

+ 💡 请输入完整的区域代码,如 us-east-1 +

-
- - -

- 仅在使用临时 AWS 凭证时需要填写 -

-
-
{ hasError = true } } else if (form.value.platform === 'bedrock') { - // Bedrock 验证 - if (!form.value.accessKeyId || form.value.accessKeyId.trim() === '') { - errors.value.accessKeyId = '请填写 AWS 访问密钥 ID' - hasError = true - } - if (!form.value.secretAccessKey || form.value.secretAccessKey.trim() === '') { - errors.value.secretAccessKey = '请填写 AWS 秘密访问密钥' - hasError = true + // Bedrock 验证 - 根据凭证类型进行不同验证 + if (form.value.credentialType === 'access_key') { + // Access Key 模式:创建时必填,编辑时可选(留空则保持原有凭证) + if (!isEdit.value) { + if (!form.value.accessKeyId || form.value.accessKeyId.trim() === '') { + errors.value.accessKeyId = '请填写 AWS 访问密钥 ID' + hasError = true + } + if (!form.value.secretAccessKey || form.value.secretAccessKey.trim() === '') { + errors.value.secretAccessKey = '请填写 AWS 秘密访问密钥' + hasError = true + } + } + } else if (form.value.credentialType === 'bearer_token') { + // Bearer Token 模式:创建时必填,编辑时可选(留空则保持原有凭证) + if (!isEdit.value) { + if (!form.value.bearerToken || form.value.bearerToken.trim() === '') { + errors.value.bearerToken = '请填写 Bearer Token' + hasError = true + } + } } if (!form.value.region || form.value.region.trim() === '') { errors.value.region = '请选择 AWS 区域' @@ -5246,12 +5404,21 @@ const createAccount = async () => { ? form.value.supportedModels : [] } else if (form.value.platform === 'bedrock') { - // Bedrock 账户特定数据 - 构造 awsCredentials 对象 - data.awsCredentials = { - accessKeyId: form.value.accessKeyId, - secretAccessKey: form.value.secretAccessKey, - sessionToken: form.value.sessionToken || null + // Bedrock 账户特定数据 + data.credentialType = form.value.credentialType || 'access_key' + + // 根据凭证类型构造不同的凭证对象 + if (form.value.credentialType === 'access_key') { + data.awsCredentials = { + accessKeyId: form.value.accessKeyId, + secretAccessKey: form.value.secretAccessKey, + sessionToken: form.value.sessionToken || null + } + } else if (form.value.credentialType === 'bearer_token') { + // Bearer Token 模式:必须传递 Bearer Token + data.bearerToken = form.value.bearerToken } + data.region = form.value.region data.defaultModel = form.value.defaultModel || null data.smallFastModel = form.value.smallFastModel || null @@ -5579,19 +5746,33 @@ const updateAccount = async () => { // Bedrock 特定更新 if (props.account.platform === 'bedrock') { - // 只有当有凭证变更时才构造 awsCredentials 对象 - if (form.value.accessKeyId || form.value.secretAccessKey || form.value.sessionToken) { - data.awsCredentials = {} - if (form.value.accessKeyId) { - data.awsCredentials.accessKeyId = form.value.accessKeyId + // 更新凭证类型 + if (form.value.credentialType) { + data.credentialType = form.value.credentialType + } + + // 根据凭证类型更新凭证 + if (form.value.credentialType === 'access_key') { + // 只有当有凭证变更时才构造 awsCredentials 对象 + if (form.value.accessKeyId || form.value.secretAccessKey || form.value.sessionToken) { + data.awsCredentials = {} + if (form.value.accessKeyId) { + data.awsCredentials.accessKeyId = form.value.accessKeyId + } + if (form.value.secretAccessKey) { + data.awsCredentials.secretAccessKey = form.value.secretAccessKey + } + if (form.value.sessionToken !== undefined) { + data.awsCredentials.sessionToken = form.value.sessionToken || null + } } - if (form.value.secretAccessKey) { - data.awsCredentials.secretAccessKey = form.value.secretAccessKey - } - if (form.value.sessionToken !== undefined) { - data.awsCredentials.sessionToken = form.value.sessionToken || null + } else if (form.value.credentialType === 'bearer_token') { + // Bearer Token 模式:更新 Bearer Token(编辑时可选,留空则保留原有凭证) + if (form.value.bearerToken && form.value.bearerToken.trim()) { + data.bearerToken = form.value.bearerToken } } + if (form.value.region) { data.region = form.value.region } diff --git a/web/admin-spa/src/components/accounts/AccountTestModal.vue b/web/admin-spa/src/components/accounts/AccountTestModal.vue index 634399db..338016ef 100644 --- a/web/admin-spa/src/components/accounts/AccountTestModal.vue +++ b/web/admin-spa/src/components/accounts/AccountTestModal.vue @@ -68,6 +68,22 @@ {{ platformLabel }}
+ +
+ 账号类型 + + + {{ credentialTypeLabel }} + +
测试模型 {{ testModel }} @@ -209,13 +225,15 @@ const platformLabel = computed(() => { const platform = props.account.platform if (platform === 'claude') return 'Claude OAuth' if (platform === 'claude-console') return 'Claude Console' + if (platform === 'bedrock') return 'AWS Bedrock' return platform }) const platformIcon = computed(() => { if (!props.account) return 'fas fa-question' const platform = props.account.platform - if (platform === 'claude' || platform === 'claude-console') return 'fas fa-brain' + if (platform === 'claude' || platform === 'claude-console' || platform === 'bedrock') + return 'fas fa-brain' return 'fas fa-robot' }) @@ -228,6 +246,39 @@ const platformBadgeClass = computed(() => { if (platform === 'claude-console') { return 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300' } + if (platform === 'bedrock') { + return 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300' + } + return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' +}) + +// Bedrock 账号类型相关 +const credentialTypeLabel = computed(() => { + if (!props.account || props.account.platform !== 'bedrock') return '' + const credentialType = props.account.credentialType + if (credentialType === 'access_key') return 'Access Key' + if (credentialType === 'bearer_token') return 'Bearer Token' + return 'Unknown' +}) + +const credentialTypeIcon = computed(() => { + if (!props.account || props.account.platform !== 'bedrock') return '' + const credentialType = props.account.credentialType + if (credentialType === 'access_key') return 'fas fa-key' + if (credentialType === 'bearer_token') return 'fas fa-ticket' + return 'fas fa-question' +}) + +const credentialTypeBadgeClass = computed(() => { + if (!props.account || props.account.platform !== 'bedrock') + return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' + const credentialType = props.account.credentialType + if (credentialType === 'access_key') { + return 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' + } + if (credentialType === 'bearer_token') { + return 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300' + } return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' }) @@ -346,6 +397,9 @@ function getTestEndpoint() { if (platform === 'claude-console') { return `${API_PREFIX}/admin/claude-console-accounts/${props.account.id}/test` } + if (platform === 'bedrock') { + return `${API_PREFIX}/admin/bedrock-accounts/${props.account.id}/test` + } return '' } @@ -469,7 +523,7 @@ function handleClose() { emit('close') } -// 监听show变化,重置状态 +// 监听show变化,重置状态并设置测试模型 watch( () => props.show, (newVal) => { @@ -478,6 +532,21 @@ watch( responseText.value = '' errorMessage.value = '' testDuration.value = 0 + + // 根据平台和账号类型设置测试模型 + if (props.account?.platform === 'bedrock') { + const credentialType = props.account.credentialType + if (credentialType === 'bearer_token') { + // Bearer Token 模式使用 Sonnet 4.5 + testModel.value = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' + } else { + // Access Key 模式使用 Haiku(更快更便宜) + testModel.value = 'us.anthropic.claude-3-5-haiku-20241022-v1:0' + } + } else { + // 其他平台使用默认模型 + testModel.value = 'claude-sonnet-4-5-20250929' + } } } ) diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 5b483805..48d12e8c 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -2203,7 +2203,8 @@ const supportedUsagePlatforms = [ 'openai-responses', 'gemini', 'droid', - 'gemini-api' + 'gemini-api', + 'bedrock' ] // 过期时间编辑弹窗状态 @@ -2547,7 +2548,7 @@ const closeAccountUsageModal = () => { } // 测试账户连通性相关函数 -const supportedTestPlatforms = ['claude', 'claude-console'] +const supportedTestPlatforms = ['claude', 'claude-console', 'bedrock'] const canTestAccount = (account) => { return !!account && supportedTestPlatforms.includes(account.platform)