cc / src /services /bedrockAccountService.js
hequ's picture
Upload 220 files
497686c verified
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const bedrockRelayService = require('./bedrockRelayService')
const LRUCache = require('../utils/lruCache')
class BedrockAccountService {
constructor() {
// 加密相关常量
this.ENCRYPTION_ALGORITHM = 'aes-256-cbc'
this.ENCRYPTION_SALT = 'salt'
// 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算
this._encryptionKeyCache = null
// 🔄 解密结果缓存,提高解密性能
this._decryptCache = new LRUCache(500)
// 🧹 定期清理缓存(每10分钟)
setInterval(
() => {
this._decryptCache.cleanup()
logger.info('🧹 Bedrock decrypt cache cleanup completed', this._decryptCache.getStats())
},
10 * 60 * 1000
)
}
// 🏢 创建Bedrock账户
async createAccount(options = {}) {
const {
name = 'Unnamed Bedrock Account',
description = '',
region = process.env.AWS_REGION || 'us-east-1',
awsCredentials = null, // { accessKeyId, secretAccessKey, sessionToken }
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'
} = options
const accountId = uuidv4()
const accountData = {
id: accountId,
name,
description,
region,
defaultModel,
isActive,
accountType,
priority,
schedulable,
credentialType,
// ✅ 新增:账户订阅到期时间(业务字段,手动管理)
// 注意:Bedrock 使用 AWS 凭证,没有 OAuth token,因此没有 expiresAt
subscriptionExpiresAt: options.subscriptionExpiresAt || null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
type: 'bedrock' // 标识这是Bedrock账户
}
// 加密存储AWS凭证
if (awsCredentials) {
accountData.awsCredentials = this._encryptAwsCredentials(awsCredentials)
}
const client = redis.getClientSafe()
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData))
logger.info(`✅ 创建Bedrock账户成功 - ID: ${accountId}, 名称: ${name}, 区域: ${region}`)
return {
success: true,
data: {
id: accountId,
name,
description,
region,
defaultModel,
isActive,
accountType,
priority,
schedulable,
credentialType,
createdAt: accountData.createdAt,
type: 'bedrock'
}
}
}
// 🔍 获取账户信息
async getAccount(accountId) {
try {
const client = redis.getClientSafe()
const accountData = await client.get(`bedrock_account:${accountId}`)
if (!accountData) {
return { success: false, error: 'Account not found' }
}
const account = JSON.parse(accountData)
// 解密AWS凭证用于内部使用
if (account.awsCredentials) {
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials)
}
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`)
return {
success: true,
data: account
}
} catch (error) {
logger.error(`❌ 获取Bedrock账户失败 - ID: ${accountId}`, error)
return { success: false, error: error.message }
}
}
// 📋 获取所有账户列表
async getAllAccounts() {
try {
const client = redis.getClientSafe()
const keys = await client.keys('bedrock_account:*')
const accounts = []
for (const key of keys) {
const accountData = await client.get(key)
if (accountData) {
const account = JSON.parse(accountData)
// 返回给前端时,不包含敏感信息,只显示掩码
accounts.push({
id: account.id,
name: account.name,
description: account.description,
region: account.region,
defaultModel: account.defaultModel,
isActive: account.isActive,
accountType: account.accountType,
priority: account.priority,
schedulable: account.schedulable,
credentialType: account.credentialType,
// ✅ 前端显示订阅过期时间(业务字段)
expiresAt: account.subscriptionExpiresAt || null,
createdAt: account.createdAt,
updatedAt: account.updatedAt,
type: 'bedrock',
platform: 'bedrock',
hasCredentials: !!account.awsCredentials
})
}
}
// 按优先级和名称排序
accounts.sort((a, b) => {
if (a.priority !== b.priority) {
return a.priority - b.priority
}
return a.name.localeCompare(b.name)
})
logger.debug(`📋 获取所有Bedrock账户 - 共 ${accounts.length} 个`)
return {
success: true,
data: accounts
}
} catch (error) {
logger.error('❌ 获取Bedrock账户列表失败', error)
return { success: false, error: error.message }
}
}
// ✏️ 更新账户信息
async updateAccount(accountId, updates = {}) {
try {
// 获取原始账户数据(不解密凭证)
const client = redis.getClientSafe()
const accountData = await client.get(`bedrock_account:${accountId}`)
if (!accountData) {
return { success: false, error: 'Account not found' }
}
const account = JSON.parse(accountData)
// 更新字段
if (updates.name !== undefined) {
account.name = updates.name
}
if (updates.description !== undefined) {
account.description = updates.description
}
if (updates.region !== undefined) {
account.region = updates.region
}
if (updates.defaultModel !== undefined) {
account.defaultModel = updates.defaultModel
}
if (updates.isActive !== undefined) {
account.isActive = updates.isActive
}
if (updates.accountType !== undefined) {
account.accountType = updates.accountType
}
if (updates.priority !== undefined) {
account.priority = updates.priority
}
if (updates.schedulable !== undefined) {
account.schedulable = updates.schedulable
}
if (updates.credentialType !== undefined) {
account.credentialType = updates.credentialType
}
// 更新AWS凭证
if (updates.awsCredentials !== undefined) {
if (updates.awsCredentials) {
account.awsCredentials = this._encryptAwsCredentials(updates.awsCredentials)
} else {
delete account.awsCredentials
}
} else if (account.awsCredentials && account.awsCredentials.accessKeyId) {
// 如果没有提供新凭证但现有凭证是明文格式,重新加密
const plainCredentials = account.awsCredentials
account.awsCredentials = this._encryptAwsCredentials(plainCredentials)
logger.info(`🔐 重新加密Bedrock账户凭证 - ID: ${accountId}`)
}
// ✅ 直接保存 subscriptionExpiresAt(如果提供)
// Bedrock 没有 token 刷新逻辑,不会覆盖此字段
if (updates.subscriptionExpiresAt !== undefined) {
account.subscriptionExpiresAt = updates.subscriptionExpiresAt
}
account.updatedAt = new Date().toISOString()
await client.set(`bedrock_account:${accountId}`, JSON.stringify(account))
logger.info(`✅ 更新Bedrock账户成功 - ID: ${accountId}, 名称: ${account.name}`)
return {
success: true,
data: {
id: account.id,
name: account.name,
description: account.description,
region: account.region,
defaultModel: account.defaultModel,
isActive: account.isActive,
accountType: account.accountType,
priority: account.priority,
schedulable: account.schedulable,
credentialType: account.credentialType,
updatedAt: account.updatedAt,
type: 'bedrock'
}
}
} catch (error) {
logger.error(`❌ 更新Bedrock账户失败 - ID: ${accountId}`, error)
return { success: false, error: error.message }
}
}
// 🗑️ 删除账户
async deleteAccount(accountId) {
try {
const accountResult = await this.getAccount(accountId)
if (!accountResult.success) {
return accountResult
}
const client = redis.getClientSafe()
await client.del(`bedrock_account:${accountId}`)
logger.info(`✅ 删除Bedrock账户成功 - ID: ${accountId}`)
return { success: true }
} catch (error) {
logger.error(`❌ 删除Bedrock账户失败 - ID: ${accountId}`, error)
return { success: false, error: error.message }
}
}
// 🎯 选择可用的Bedrock账户 (用于请求转发)
async selectAvailableAccount() {
try {
const accountsResult = await this.getAllAccounts()
if (!accountsResult.success) {
return { success: false, error: 'Failed to get accounts' }
}
const availableAccounts = accountsResult.data.filter((account) => {
// ✅ 检查账户订阅是否过期
if (this.isSubscriptionExpired(account)) {
logger.debug(
`⏰ Skipping expired Bedrock account: ${account.name}, expired at ${account.subscriptionExpiresAt || account.expiresAt}`
)
return false
}
return account.isActive && account.schedulable
})
if (availableAccounts.length === 0) {
return { success: false, error: 'No available Bedrock accounts' }
}
// 简单的轮询选择策略 - 选择优先级最高的账户
const selectedAccount = availableAccounts[0]
// 获取完整账户信息(包含解密的凭证)
const fullAccountResult = await this.getAccount(selectedAccount.id)
if (!fullAccountResult.success) {
return { success: false, error: 'Failed to get selected account details' }
}
logger.debug(`🎯 选择Bedrock账户 - ID: ${selectedAccount.id}, 名称: ${selectedAccount.name}`)
return {
success: true,
data: fullAccountResult.data
}
} catch (error) {
logger.error('❌ 选择Bedrock账户失败', error)
return { success: false, error: error.message }
}
}
// 🧪 测试账户连接
async testAccount(accountId) {
try {
const accountResult = await this.getAccount(accountId)
if (!accountResult.success) {
return accountResult
}
const account = accountResult.data
logger.info(`🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}`)
// 尝试获取模型列表来测试连接
const models = await bedrockRelayService.getAvailableModels(account)
if (models && models.length > 0) {
logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`)
return {
success: true,
data: {
status: 'connected',
modelsCount: models.length,
region: account.region,
credentialType: account.credentialType
}
}
} else {
return {
success: false,
error: 'Unable to retrieve models from Bedrock'
}
}
} catch (error) {
logger.error(`❌ 测试Bedrock账户失败 - ID: ${accountId}`, error)
return {
success: false,
error: error.message
}
}
}
/**
* 检查账户订阅是否过期
* @param {Object} account - 账户对象
* @returns {boolean} - true: 已过期, false: 未过期
*/
isSubscriptionExpired(account) {
if (!account.subscriptionExpiresAt) {
return false // 未设置视为永不过期
}
const expiryDate = new Date(account.subscriptionExpiresAt)
return expiryDate <= new Date()
}
// 🔑 生成加密密钥(缓存优化)
_generateEncryptionKey() {
if (!this._encryptionKeyCache) {
this._encryptionKeyCache = crypto
.createHash('sha256')
.update(config.security.encryptionKey)
.digest()
logger.info('🔑 Bedrock encryption key derived and cached for performance optimization')
}
return this._encryptionKeyCache
}
// 🔐 加密AWS凭证
_encryptAwsCredentials(credentials) {
try {
const key = this._generateEncryptionKey()
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv)
const credentialsString = JSON.stringify(credentials)
let encrypted = cipher.update(credentialsString, 'utf8', 'hex')
encrypted += cipher.final('hex')
return {
encrypted,
iv: iv.toString('hex')
}
} catch (error) {
logger.error('❌ AWS凭证加密失败', error)
throw new Error('Credentials encryption failed')
}
}
// 🔓 解密AWS凭证
_decryptAwsCredentials(encryptedData) {
try {
// 检查数据格式
if (!encryptedData || typeof encryptedData !== 'object') {
logger.error('❌ 无效的加密数据格式:', encryptedData)
throw new Error('Invalid encrypted data format')
}
// 检查是否为加密格式 (有 encrypted 和 iv 字段)
if (encryptedData.encrypted && encryptedData.iv) {
// 🎯 检查缓存
const cacheKey = crypto
.createHash('sha256')
.update(JSON.stringify(encryptedData))
.digest('hex')
const cached = this._decryptCache.get(cacheKey)
if (cached !== undefined) {
return cached
}
// 加密数据 - 进行解密
const key = this._generateEncryptionKey()
const iv = Buffer.from(encryptedData.iv, 'hex')
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv)
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
const result = JSON.parse(decrypted)
// 💾 存入缓存(5分钟过期)
this._decryptCache.set(cacheKey, result, 5 * 60 * 1000)
// 📊 定期打印缓存统计
if ((this._decryptCache.hits + this._decryptCache.misses) % 1000 === 0) {
this._decryptCache.printStats()
}
return result
} else if (encryptedData.accessKeyId) {
// 纯文本数据 - 直接返回 (向后兼容)
logger.warn('⚠️ 发现未加密的AWS凭证,建议更新账户以启用加密')
return encryptedData
} else {
// 既不是加密格式也不是有效的凭证格式
logger.error('❌ 缺少加密数据字段:', {
hasEncrypted: !!encryptedData.encrypted,
hasIv: !!encryptedData.iv,
hasAccessKeyId: !!encryptedData.accessKeyId
})
throw new Error('Missing encrypted data fields or valid credentials')
}
} catch (error) {
logger.error('❌ AWS凭证解密失败', error)
throw new Error('Credentials decryption failed')
}
}
// 🔍 获取账户统计信息
async getAccountStats() {
try {
const accountsResult = await this.getAllAccounts()
if (!accountsResult.success) {
return { success: false, error: accountsResult.error }
}
const accounts = accountsResult.data
const stats = {
total: accounts.length,
active: accounts.filter((acc) => acc.isActive).length,
inactive: accounts.filter((acc) => !acc.isActive).length,
schedulable: accounts.filter((acc) => acc.schedulable).length,
byRegion: {},
byCredentialType: {}
}
// 按区域统计
accounts.forEach((acc) => {
stats.byRegion[acc.region] = (stats.byRegion[acc.region] || 0) + 1
stats.byCredentialType[acc.credentialType] =
(stats.byCredentialType[acc.credentialType] || 0) + 1
})
return { success: true, data: stats }
} catch (error) {
logger.error('❌ 获取Bedrock账户统计失败', error)
return { success: false, error: error.message }
}
}
}
module.exports = new BedrockAccountService()