mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 为 AWS Bedrock 账户添加 Bearer Token 认证支持
- 新增 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 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,7 @@ router.post('/', authenticateAdmin, async (req, res) => {
|
|||||||
description,
|
description,
|
||||||
region,
|
region,
|
||||||
awsCredentials,
|
awsCredentials,
|
||||||
|
bearerToken,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
priority,
|
priority,
|
||||||
accountType,
|
accountType,
|
||||||
@@ -145,9 +146,9 @@ router.post('/', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证credentialType的有效性
|
// 验证credentialType的有效性
|
||||||
if (credentialType && !['default', 'access_key', 'bearer_token'].includes(credentialType)) {
|
if (credentialType && !['access_key', 'bearer_token'].includes(credentialType)) {
|
||||||
return res.status(400).json({
|
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 || '',
|
description: description || '',
|
||||||
region: region || 'us-east-1',
|
region: region || 'us-east-1',
|
||||||
awsCredentials,
|
awsCredentials,
|
||||||
|
bearerToken,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
priority: priority || 50,
|
priority: priority || 50,
|
||||||
accountType: accountType || 'shared',
|
accountType: accountType || 'shared',
|
||||||
credentialType: credentialType || 'default'
|
credentialType: credentialType || 'access_key'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -206,10 +208,10 @@ router.put('/:accountId', authenticateAdmin, async (req, res) => {
|
|||||||
// 验证credentialType的有效性
|
// 验证credentialType的有效性
|
||||||
if (
|
if (
|
||||||
mappedUpdates.credentialType &&
|
mappedUpdates.credentialType &&
|
||||||
!['default', 'access_key', 'bearer_token'].includes(mappedUpdates.credentialType)
|
!['access_key', 'bearer_token'].includes(mappedUpdates.credentialType)
|
||||||
) {
|
) {
|
||||||
return res.status(400).json({
|
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) => {
|
router.post('/:accountId/test', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { accountId } = req.params
|
const { accountId } = req.params
|
||||||
|
|
||||||
const result = await bedrockAccountService.testAccount(accountId)
|
await bedrockAccountService.testAccountConnection(accountId, res)
|
||||||
|
|
||||||
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 })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to test Bedrock account:', error)
|
logger.error('❌ Failed to test Bedrock account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to test Bedrock account', message: error.message })
|
// 错误已在服务层处理,这里仅做日志记录
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,15 @@ class AccountBalanceService {
|
|||||||
return null
|
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) {
|
async getAllAccountsByPlatform(platform) {
|
||||||
@@ -275,10 +283,27 @@ class AccountBalanceService {
|
|||||||
|
|
||||||
const accountId = account?.id
|
const accountId = account?.id
|
||||||
if (!accountId) {
|
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 scriptConfig = null
|
||||||
let scriptConfigured = false
|
let scriptConfigured = false
|
||||||
if (typeof this.redis?.getBalanceScriptConfig === 'function') {
|
if (typeof this.redis?.getBalanceScriptConfig === 'function') {
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ class BedrockAccountService {
|
|||||||
description = '',
|
description = '',
|
||||||
region = process.env.AWS_REGION || 'us-east-1',
|
region = process.env.AWS_REGION || 'us-east-1',
|
||||||
awsCredentials = null, // { accessKeyId, secretAccessKey, sessionToken }
|
awsCredentials = null, // { accessKeyId, secretAccessKey, sessionToken }
|
||||||
|
bearerToken = null, // AWS Bearer Token for Bedrock API Keys
|
||||||
defaultModel = 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
defaultModel = 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||||
isActive = true,
|
isActive = true,
|
||||||
accountType = 'shared', // 'dedicated' or 'shared'
|
accountType = 'shared', // 'dedicated' or 'shared'
|
||||||
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
priority = 50, // 调度优先级 (1-100,数字越小优先级越高)
|
||||||
schedulable = true, // 是否可被调度
|
schedulable = true, // 是否可被调度
|
||||||
credentialType = 'default' // 'default', 'access_key', 'bearer_token'
|
credentialType = 'access_key' // 'access_key', 'bearer_token'(默认为 access_key)
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const accountId = uuidv4()
|
const accountId = uuidv4()
|
||||||
@@ -71,6 +72,11 @@ class BedrockAccountService {
|
|||||||
accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials)
|
accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加密存储 Bearer Token
|
||||||
|
if (bearerToken) {
|
||||||
|
accountData.bearerToken = this._encryptAwsCredentials({ token: bearerToken })
|
||||||
|
}
|
||||||
|
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData))
|
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData))
|
||||||
|
|
||||||
@@ -106,9 +112,85 @@ class BedrockAccountService {
|
|||||||
|
|
||||||
const account = JSON.parse(accountData)
|
const account = JSON.parse(accountData)
|
||||||
|
|
||||||
// 解密AWS凭证用于内部使用
|
// 根据凭证类型解密对应的凭证
|
||||||
if (account.awsCredentials) {
|
// 增强逻辑:优先按照 credentialType 解密,如果字段不存在则尝试解密实际存在的字段(兜底)
|
||||||
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
|
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}`)
|
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
|
||||||
@@ -155,7 +237,11 @@ class BedrockAccountService {
|
|||||||
updatedAt: account.updatedAt,
|
updatedAt: account.updatedAt,
|
||||||
type: 'bedrock',
|
type: 'bedrock',
|
||||||
platform: '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}`)
|
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(如果提供)
|
// ✅ 直接保存 subscriptionExpiresAt(如果提供)
|
||||||
// Bedrock 没有 token 刷新逻辑,不会覆盖此字段
|
// Bedrock 没有 token 刷新逻辑,不会覆盖此字段
|
||||||
if (updates.subscriptionExpiresAt !== undefined) {
|
if (updates.subscriptionExpiresAt !== undefined) {
|
||||||
@@ -345,13 +440,45 @@ class BedrockAccountService {
|
|||||||
|
|
||||||
const account = accountResult.data
|
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)
|
const models = await bedrockRelayService.getAvailableModels(account)
|
||||||
|
|
||||||
if (models && models.length > 0) {
|
if (models && models.length > 0) {
|
||||||
logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`)
|
logger.info(
|
||||||
|
`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型, 凭证类型: ${account.credentialType}`
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
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 - 账户对象
|
* @param {Object} account - 账户对象
|
||||||
|
|||||||
@@ -48,13 +48,17 @@ class BedrockRelayService {
|
|||||||
secretAccessKey: bedrockAccount.awsCredentials.secretAccessKey,
|
secretAccessKey: bedrockAccount.awsCredentials.secretAccessKey,
|
||||||
sessionToken: bedrockAccount.awsCredentials.sessionToken
|
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 {
|
} else {
|
||||||
// 检查是否有环境变量凭证
|
// 检查是否有环境变量凭证
|
||||||
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||||
clientConfig.credentials = fromEnv()
|
clientConfig.credentials = fromEnv()
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
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'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -852,41 +852,194 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bedrock 特定字段 -->
|
<!-- Bedrock 特定字段 -->
|
||||||
<div v-if="form.platform === 'bedrock' && !isEdit" class="space-y-4">
|
<div v-if="form.platform === 'bedrock'" class="space-y-4">
|
||||||
|
<!-- 凭证类型选择器 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>AWS 访问密钥 ID *</label
|
>凭证类型 *</label
|
||||||
>
|
>
|
||||||
<input
|
<div v-if="!isEdit" class="flex gap-4">
|
||||||
v-model="form.accessKeyId"
|
<label class="flex cursor-pointer items-center">
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
<input
|
||||||
:class="{ 'border-red-500': errors.accessKeyId }"
|
v-model="form.credentialType"
|
||||||
placeholder="请输入 AWS Access Key ID"
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
required
|
type="radio"
|
||||||
type="text"
|
value="access_key"
|
||||||
/>
|
/>
|
||||||
<p v-if="errors.accessKeyId" class="mt-1 text-xs text-red-500">
|
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||||
{{ errors.accessKeyId }}
|
>AWS Access Key(访问密钥)</span
|
||||||
</p>
|
>
|
||||||
|
</label>
|
||||||
|
<label class="flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="form.credentialType"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="radio"
|
||||||
|
value="bearer_token"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>Bearer Token(长期令牌)</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-4">
|
||||||
|
<label class="flex items-center opacity-60">
|
||||||
|
<input
|
||||||
|
v-model="form.credentialType"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
disabled
|
||||||
|
type="radio"
|
||||||
|
value="access_key"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>AWS Access Key(访问密钥)</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center opacity-60">
|
||||||
|
<input
|
||||||
|
v-model="form.credentialType"
|
||||||
|
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
disabled
|
||||||
|
type="radio"
|
||||||
|
value="bearer_token"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>Bearer Token(长期令牌)</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-2 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<i class="fas fa-info-circle mt-0.5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<div class="text-xs text-blue-700 dark:text-blue-300">
|
||||||
|
<p v-if="form.credentialType === 'access_key'" class="font-medium">
|
||||||
|
使用 AWS Access Key ID 和 Secret Access Key 进行身份验证(支持临时凭证)
|
||||||
|
</p>
|
||||||
|
<p v-else class="font-medium">
|
||||||
|
使用 AWS Bedrock API Keys 生成的 Bearer Token
|
||||||
|
进行身份验证,更简单、权限范围更小
|
||||||
|
</p>
|
||||||
|
<p v-if="isEdit" class="mt-1 text-xs italic">
|
||||||
|
💡 编辑模式下凭证类型不可更改,如需切换类型请重新创建账户
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- AWS Access Key 字段(仅在 access_key 模式下显示)-->
|
||||||
|
<div v-if="form.credentialType === 'access_key'">
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>AWS 访问密钥 ID {{ isEdit ? '' : '*' }}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.accessKeyId"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
|
:class="{ 'border-red-500': errors.accessKeyId }"
|
||||||
|
:placeholder="isEdit ? '留空则保持原有凭证不变' : '请输入 AWS Access Key ID'"
|
||||||
|
:required="!isEdit"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<p v-if="errors.accessKeyId" class="mt-1 text-xs text-red-500">
|
||||||
|
{{ errors.accessKeyId }}
|
||||||
|
</p>
|
||||||
|
<p v-if="isEdit" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
💡 编辑模式下,留空则保持原有 Access Key ID 不变
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>AWS 秘密访问密钥 {{ isEdit ? '' : '*' }}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.secretAccessKey"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
|
:class="{ 'border-red-500': errors.secretAccessKey }"
|
||||||
|
:placeholder="
|
||||||
|
isEdit ? '留空则保持原有凭证不变' : '请输入 AWS Secret Access Key'
|
||||||
|
"
|
||||||
|
:required="!isEdit"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<p v-if="errors.secretAccessKey" class="mt-1 text-xs text-red-500">
|
||||||
|
{{ errors.secretAccessKey }}
|
||||||
|
</p>
|
||||||
|
<p v-if="isEdit" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
💡 编辑模式下,留空则保持原有 Secret Access Key 不变
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
|
>会话令牌 (可选)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.sessionToken"
|
||||||
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
|
:placeholder="
|
||||||
|
isEdit
|
||||||
|
? '留空则保持原有 Session Token 不变'
|
||||||
|
: '如果使用临时凭证,请输入会话令牌'
|
||||||
|
"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
仅在使用临时 AWS 凭证时需要填写
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bearer Token 字段(仅在 bearer_token 模式下显示)-->
|
||||||
|
<div v-if="form.credentialType === 'bearer_token'">
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>AWS 秘密访问密钥 *</label
|
>Bearer Token {{ isEdit ? '' : '*' }}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.secretAccessKey"
|
v-model="form.bearerToken"
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||||
:class="{ 'border-red-500': errors.secretAccessKey }"
|
:class="{ 'border-red-500': errors.bearerToken }"
|
||||||
placeholder="请输入 AWS Secret Access Key"
|
:placeholder="
|
||||||
required
|
isEdit ? '留空则保持原有 Bearer Token 不变' : '请输入 AWS Bearer Token'
|
||||||
|
"
|
||||||
|
:required="!isEdit"
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
<p v-if="errors.secretAccessKey" class="mt-1 text-xs text-red-500">
|
<p v-if="errors.bearerToken" class="mt-1 text-xs text-red-500">
|
||||||
{{ errors.secretAccessKey }}
|
{{ errors.bearerToken }}
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="isEdit" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
💡 编辑模式下,留空则保持原有 Bearer Token 不变
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mt-2 rounded-lg border border-green-200 bg-green-50 p-3 dark:border-green-700 dark:bg-green-900/30"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<i class="fas fa-key mt-0.5 text-green-600 dark:text-green-400" />
|
||||||
|
<div class="text-xs text-green-700 dark:text-green-300">
|
||||||
|
<p class="mb-1 font-medium">Bearer Token 说明:</p>
|
||||||
|
<ul class="list-inside list-disc space-y-1 text-xs">
|
||||||
|
<li>输入 AWS Bedrock API Keys 生成的 Bearer Token</li>
|
||||||
|
<li>Bearer Token 仅限 Bedrock 服务访问,权限范围更小</li>
|
||||||
|
<li>相比 Access Key 更简单,无需 Secret Key</li>
|
||||||
|
<li>
|
||||||
|
参考:<a
|
||||||
|
class="text-green-600 underline dark:text-green-400"
|
||||||
|
href="https://aws.amazon.com/cn/blogs/machine-learning/accelerate-ai-development-with-amazon-bedrock-api-keys/"
|
||||||
|
target="_blank"
|
||||||
|
>AWS 官方文档</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AWS 区域(两种凭证类型都需要)-->
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>AWS 区域 *</label
|
>AWS 区域 *</label
|
||||||
@@ -902,10 +1055,12 @@
|
|||||||
<p v-if="errors.region" class="mt-1 text-xs text-red-500">
|
<p v-if="errors.region" class="mt-1 text-xs text-red-500">
|
||||||
{{ errors.region }}
|
{{ errors.region }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
<div
|
||||||
|
class="mt-2 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/30"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<i class="fas fa-info-circle mt-0.5 text-blue-600" />
|
<i class="fas fa-info-circle mt-0.5 text-blue-600 dark:text-blue-400" />
|
||||||
<div class="text-xs text-blue-700">
|
<div class="text-xs text-blue-700 dark:text-blue-300">
|
||||||
<p class="mb-1 font-medium">常用 AWS 区域参考:</p>
|
<p class="mb-1 font-medium">常用 AWS 区域参考:</p>
|
||||||
<div class="grid grid-cols-2 gap-1 text-xs">
|
<div class="grid grid-cols-2 gap-1 text-xs">
|
||||||
<span>• us-east-1 (美国东部)</span>
|
<span>• us-east-1 (美国东部)</span>
|
||||||
@@ -915,27 +1070,14 @@
|
|||||||
<span>• ap-northeast-1 (东京)</span>
|
<span>• ap-northeast-1 (东京)</span>
|
||||||
<span>• eu-central-1 (法兰克福)</span>
|
<span>• eu-central-1 (法兰克福)</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-blue-600">💡 请输入完整的区域代码,如 us-east-1</p>
|
<p class="mt-2 text-blue-600 dark:text-blue-400">
|
||||||
|
💡 请输入完整的区域代码,如 us-east-1
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
|
||||||
>会话令牌 (可选)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="form.sessionToken"
|
|
||||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
|
||||||
placeholder="如果使用临时凭证,请输入会话令牌"
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
仅在使用临时 AWS 凭证时需要填写
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>默认主模型 (可选)</label
|
>默认主模型 (可选)</label
|
||||||
@@ -4105,10 +4247,12 @@ const form = ref({
|
|||||||
// 并发控制字段
|
// 并发控制字段
|
||||||
maxConcurrentTasks: props.account?.maxConcurrentTasks || 0,
|
maxConcurrentTasks: props.account?.maxConcurrentTasks || 0,
|
||||||
// Bedrock 特定字段
|
// Bedrock 特定字段
|
||||||
|
credentialType: props.account?.credentialType || 'access_key', // 'access_key' 或 'bearer_token'
|
||||||
accessKeyId: props.account?.accessKeyId || '',
|
accessKeyId: props.account?.accessKeyId || '',
|
||||||
secretAccessKey: props.account?.secretAccessKey || '',
|
secretAccessKey: props.account?.secretAccessKey || '',
|
||||||
region: props.account?.region || '',
|
region: props.account?.region || '',
|
||||||
sessionToken: props.account?.sessionToken || '',
|
sessionToken: props.account?.sessionToken || '',
|
||||||
|
bearerToken: props.account?.bearerToken || '', // Bearer Token 字段
|
||||||
defaultModel: props.account?.defaultModel || '',
|
defaultModel: props.account?.defaultModel || '',
|
||||||
smallFastModel: props.account?.smallFastModel || '',
|
smallFastModel: props.account?.smallFastModel || '',
|
||||||
// Azure OpenAI 特定字段
|
// Azure OpenAI 特定字段
|
||||||
@@ -4271,6 +4415,7 @@ const errors = ref({
|
|||||||
accessKeyId: '',
|
accessKeyId: '',
|
||||||
secretAccessKey: '',
|
secretAccessKey: '',
|
||||||
region: '',
|
region: '',
|
||||||
|
bearerToken: '',
|
||||||
azureEndpoint: '',
|
azureEndpoint: '',
|
||||||
deploymentName: ''
|
deploymentName: ''
|
||||||
})
|
})
|
||||||
@@ -4983,14 +5128,27 @@ const createAccount = async () => {
|
|||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
} else if (form.value.platform === 'bedrock') {
|
} else if (form.value.platform === 'bedrock') {
|
||||||
// Bedrock 验证
|
// Bedrock 验证 - 根据凭证类型进行不同验证
|
||||||
if (!form.value.accessKeyId || form.value.accessKeyId.trim() === '') {
|
if (form.value.credentialType === 'access_key') {
|
||||||
errors.value.accessKeyId = '请填写 AWS 访问密钥 ID'
|
// Access Key 模式:创建时必填,编辑时可选(留空则保持原有凭证)
|
||||||
hasError = true
|
if (!isEdit.value) {
|
||||||
}
|
if (!form.value.accessKeyId || form.value.accessKeyId.trim() === '') {
|
||||||
if (!form.value.secretAccessKey || form.value.secretAccessKey.trim() === '') {
|
errors.value.accessKeyId = '请填写 AWS 访问密钥 ID'
|
||||||
errors.value.secretAccessKey = '请填写 AWS 秘密访问密钥'
|
hasError = true
|
||||||
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() === '') {
|
if (!form.value.region || form.value.region.trim() === '') {
|
||||||
errors.value.region = '请选择 AWS 区域'
|
errors.value.region = '请选择 AWS 区域'
|
||||||
@@ -5246,12 +5404,21 @@ const createAccount = async () => {
|
|||||||
? form.value.supportedModels
|
? form.value.supportedModels
|
||||||
: []
|
: []
|
||||||
} else if (form.value.platform === 'bedrock') {
|
} else if (form.value.platform === 'bedrock') {
|
||||||
// Bedrock 账户特定数据 - 构造 awsCredentials 对象
|
// Bedrock 账户特定数据
|
||||||
data.awsCredentials = {
|
data.credentialType = form.value.credentialType || 'access_key'
|
||||||
accessKeyId: form.value.accessKeyId,
|
|
||||||
secretAccessKey: form.value.secretAccessKey,
|
// 根据凭证类型构造不同的凭证对象
|
||||||
sessionToken: form.value.sessionToken || null
|
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.region = form.value.region
|
||||||
data.defaultModel = form.value.defaultModel || null
|
data.defaultModel = form.value.defaultModel || null
|
||||||
data.smallFastModel = form.value.smallFastModel || null
|
data.smallFastModel = form.value.smallFastModel || null
|
||||||
@@ -5579,19 +5746,33 @@ const updateAccount = async () => {
|
|||||||
|
|
||||||
// Bedrock 特定更新
|
// Bedrock 特定更新
|
||||||
if (props.account.platform === 'bedrock') {
|
if (props.account.platform === 'bedrock') {
|
||||||
// 只有当有凭证变更时才构造 awsCredentials 对象
|
// 更新凭证类型
|
||||||
if (form.value.accessKeyId || form.value.secretAccessKey || form.value.sessionToken) {
|
if (form.value.credentialType) {
|
||||||
data.awsCredentials = {}
|
data.credentialType = form.value.credentialType
|
||||||
if (form.value.accessKeyId) {
|
}
|
||||||
data.awsCredentials.accessKeyId = form.value.accessKeyId
|
|
||||||
|
// 根据凭证类型更新凭证
|
||||||
|
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) {
|
} else if (form.value.credentialType === 'bearer_token') {
|
||||||
data.awsCredentials.secretAccessKey = form.value.secretAccessKey
|
// Bearer Token 模式:更新 Bearer Token(编辑时可选,留空则保留原有凭证)
|
||||||
}
|
if (form.value.bearerToken && form.value.bearerToken.trim()) {
|
||||||
if (form.value.sessionToken !== undefined) {
|
data.bearerToken = form.value.bearerToken
|
||||||
data.awsCredentials.sessionToken = form.value.sessionToken || null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.region) {
|
if (form.value.region) {
|
||||||
data.region = form.value.region
|
data.region = form.value.region
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,22 @@
|
|||||||
{{ platformLabel }}
|
{{ platformLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bedrock 账号类型 -->
|
||||||
|
<div
|
||||||
|
v-if="props.account?.platform === 'bedrock'"
|
||||||
|
class="flex items-center justify-between text-sm"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">账号类型</span>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||||
|
credentialTypeBadgeClass
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<i :class="credentialTypeIcon" />
|
||||||
|
{{ credentialTypeLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-gray-500 dark:text-gray-400">测试模型</span>
|
<span class="text-gray-500 dark:text-gray-400">测试模型</span>
|
||||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ testModel }}</span>
|
<span class="font-medium text-gray-700 dark:text-gray-300">{{ testModel }}</span>
|
||||||
@@ -209,13 +225,15 @@ const platformLabel = computed(() => {
|
|||||||
const platform = props.account.platform
|
const platform = props.account.platform
|
||||||
if (platform === 'claude') return 'Claude OAuth'
|
if (platform === 'claude') return 'Claude OAuth'
|
||||||
if (platform === 'claude-console') return 'Claude Console'
|
if (platform === 'claude-console') return 'Claude Console'
|
||||||
|
if (platform === 'bedrock') return 'AWS Bedrock'
|
||||||
return platform
|
return platform
|
||||||
})
|
})
|
||||||
|
|
||||||
const platformIcon = computed(() => {
|
const platformIcon = computed(() => {
|
||||||
if (!props.account) return 'fas fa-question'
|
if (!props.account) return 'fas fa-question'
|
||||||
const platform = props.account.platform
|
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'
|
return 'fas fa-robot'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -228,6 +246,39 @@ const platformBadgeClass = computed(() => {
|
|||||||
if (platform === 'claude-console') {
|
if (platform === 'claude-console') {
|
||||||
return 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300'
|
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'
|
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') {
|
if (platform === 'claude-console') {
|
||||||
return `${API_PREFIX}/admin/claude-console-accounts/${props.account.id}/test`
|
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 ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +523,7 @@ function handleClose() {
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听show变化,重置状态
|
// 监听show变化,重置状态并设置测试模型
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@@ -478,6 +532,21 @@ watch(
|
|||||||
responseText.value = ''
|
responseText.value = ''
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
testDuration.value = 0
|
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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2203,7 +2203,8 @@ const supportedUsagePlatforms = [
|
|||||||
'openai-responses',
|
'openai-responses',
|
||||||
'gemini',
|
'gemini',
|
||||||
'droid',
|
'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) => {
|
const canTestAccount = (account) => {
|
||||||
return !!account && supportedTestPlatforms.includes(account.platform)
|
return !!account && supportedTestPlatforms.includes(account.platform)
|
||||||
|
|||||||
Reference in New Issue
Block a user