mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge pull request #885 from junejuneli/feat/add_claude_aws_type
为 AWS Bedrock 账户添加 Bearer Token 认证支持
This commit is contained in:
@@ -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 })
|
||||
// 错误已在服务层处理,这里仅做日志记录
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const geminiApiAccountService = require('../../services/geminiApiAccountService'
|
||||
const openaiAccountService = require('../../services/openaiAccountService')
|
||||
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
|
||||
const droidAccountService = require('../../services/droidAccountService')
|
||||
const bedrockAccountService = require('../../services/bedrockAccountService')
|
||||
const redis = require('../../models/redis')
|
||||
const { authenticateAdmin } = require('../../middleware/auth')
|
||||
const logger = require('../../utils/logger')
|
||||
@@ -25,6 +26,7 @@ const accountTypeNames = {
|
||||
gemini: 'Gemini',
|
||||
'gemini-api': 'Gemini API',
|
||||
droid: 'Droid',
|
||||
bedrock: 'AWS Bedrock',
|
||||
unknown: '未知渠道'
|
||||
}
|
||||
|
||||
@@ -37,7 +39,8 @@ const resolveAccountByPlatform = async (accountId, platform) => {
|
||||
openai: openaiAccountService,
|
||||
'openai-responses': openaiResponsesAccountService,
|
||||
droid: droidAccountService,
|
||||
ccr: ccrAccountService
|
||||
ccr: ccrAccountService,
|
||||
bedrock: bedrockAccountService
|
||||
}
|
||||
|
||||
if (platform && serviceMap[platform]) {
|
||||
@@ -161,7 +164,8 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
||||
'openai-responses',
|
||||
'gemini',
|
||||
'gemini-api',
|
||||
'droid'
|
||||
'droid',
|
||||
'bedrock'
|
||||
]
|
||||
if (!allowedPlatforms.includes(platform)) {
|
||||
return res.status(400).json({
|
||||
@@ -174,7 +178,8 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
||||
openai: 'openai',
|
||||
'openai-responses': 'openai-responses',
|
||||
'gemini-api': 'gemini-api',
|
||||
droid: 'droid'
|
||||
droid: 'droid',
|
||||
bedrock: 'bedrock'
|
||||
}
|
||||
|
||||
const fallbackModelMap = {
|
||||
@@ -184,7 +189,8 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
||||
'openai-responses': 'gpt-4o-mini-2024-07-18',
|
||||
gemini: 'gemini-1.5-flash',
|
||||
'gemini-api': 'gemini-2.0-flash',
|
||||
droid: 'unknown'
|
||||
droid: 'unknown',
|
||||
bedrock: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0'
|
||||
}
|
||||
|
||||
// 获取账户信息以获取创建时间
|
||||
@@ -215,6 +221,11 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
||||
case 'droid':
|
||||
accountData = await droidAccountService.getAccount(accountId)
|
||||
break
|
||||
case 'bedrock': {
|
||||
const result = await bedrockAccountService.getAccount(accountId)
|
||||
accountData = result?.success ? result.data : null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (accountData && accountData.createdAt) {
|
||||
@@ -882,7 +893,7 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { granularity = 'day', group = 'claude', days = 7, startDate, endDate } = req.query
|
||||
|
||||
const allowedGroups = ['claude', 'openai', 'gemini', 'droid']
|
||||
const allowedGroups = ['claude', 'openai', 'gemini', 'droid', 'bedrock']
|
||||
if (!allowedGroups.includes(group)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -894,7 +905,8 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
claude: 'Claude账户',
|
||||
openai: 'OpenAI账户',
|
||||
gemini: 'Gemini账户',
|
||||
droid: 'Droid账户'
|
||||
droid: 'Droid账户',
|
||||
bedrock: 'Bedrock账户'
|
||||
}
|
||||
|
||||
// 拉取各平台账号列表
|
||||
@@ -988,6 +1000,18 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => {
|
||||
platform: 'droid'
|
||||
}
|
||||
})
|
||||
} else if (group === 'bedrock') {
|
||||
const result = await bedrockAccountService.getAllAccounts()
|
||||
const bedrockAccounts = result?.success ? result.data : []
|
||||
accounts = bedrockAccounts.map((account) => {
|
||||
const id = String(account.id || '')
|
||||
const shortId = id ? id.slice(0, 8) : '未知'
|
||||
return {
|
||||
id,
|
||||
name: account.name || `Bedrock账号 ${shortId}`,
|
||||
platform: 'bedrock'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 - 账户对象
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -431,6 +435,18 @@ class BedrockRelayService {
|
||||
_mapToBedrockModel(modelName) {
|
||||
// 标准Claude模型名到Bedrock模型名的映射表
|
||||
const modelMapping = {
|
||||
// Claude 4.5 Opus
|
||||
'claude-opus-4-5': 'us.anthropic.claude-opus-4-5-20251101-v1:0',
|
||||
'claude-opus-4-5-20251101': 'us.anthropic.claude-opus-4-5-20251101-v1:0',
|
||||
|
||||
// Claude 4.5 Sonnet
|
||||
'claude-sonnet-4-5': 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
'claude-sonnet-4-5-20250929': 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
|
||||
// Claude 4.5 Haiku
|
||||
'claude-haiku-4-5': 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
|
||||
'claude-haiku-4-5-20251001': 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
|
||||
|
||||
// Claude Sonnet 4
|
||||
'claude-sonnet-4': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
'claude-sonnet-4-20250514': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
|
||||
Reference in New Issue
Block a user