mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
主要改进: 1. 创建 mapExpiryField() 工具函数统一处理前后端字段映射(expiresAt -> subscriptionExpiresAt) 2. 统一 subscriptionExpiresAt 初始值为 null(替代空字符串) 3. 规范过期检查方法名为 isSubscriptionExpired(),返回 true 表示已过期 4. 优化过期检查条件判断,只检查 null 而非空字符串 5. 补充 OpenAI-Responses 和调度器中缺失的过期检查逻辑 6. 添加代码评审文档记录未修复问题 影响范围: - 所有 9 种账户服务的过期字段处理 - admin.js 中所有账户更新路由 - 统一调度器的过期账户过滤逻辑 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
518 lines
16 KiB
JavaScript
518 lines
16 KiB
JavaScript
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()
|