|
|
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) |
|
|
|
|
|
|
|
|
setInterval( |
|
|
() => { |
|
|
this._decryptCache.cleanup() |
|
|
logger.info('🧹 Bedrock decrypt cache cleanup completed', this._decryptCache.getStats()) |
|
|
}, |
|
|
10 * 60 * 1000 |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
async createAccount(options = {}) { |
|
|
const { |
|
|
name = 'Unnamed Bedrock Account', |
|
|
description = '', |
|
|
region = process.env.AWS_REGION || 'us-east-1', |
|
|
awsCredentials = null, |
|
|
defaultModel = 'us.anthropic.claude-sonnet-4-20250514-v1:0', |
|
|
isActive = true, |
|
|
accountType = 'shared', |
|
|
priority = 50, |
|
|
schedulable = true, |
|
|
credentialType = 'default' |
|
|
} = options |
|
|
|
|
|
const accountId = uuidv4() |
|
|
|
|
|
const accountData = { |
|
|
id: accountId, |
|
|
name, |
|
|
description, |
|
|
region, |
|
|
defaultModel, |
|
|
isActive, |
|
|
accountType, |
|
|
priority, |
|
|
schedulable, |
|
|
credentialType, |
|
|
|
|
|
|
|
|
|
|
|
subscriptionExpiresAt: options.subscriptionExpiresAt || null, |
|
|
|
|
|
createdAt: new Date().toISOString(), |
|
|
updatedAt: new Date().toISOString(), |
|
|
type: 'bedrock' |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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}`) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
_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') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
_decryptAwsCredentials(encryptedData) { |
|
|
try { |
|
|
|
|
|
if (!encryptedData || typeof encryptedData !== 'object') { |
|
|
logger.error('❌ 无效的加密数据格式:', encryptedData) |
|
|
throw new Error('Invalid encrypted data format') |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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() |
|
|
|