diff --git a/src/app.js b/src/app.js index e7dfd7e7..2f6d09cb 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ const config = require('../config/config') const logger = require('./utils/logger') const redis = require('./models/redis') const pricingService = require('./services/pricingService') +const cacheMonitor = require('./utils/cacheMonitor') // Import routes const apiRoutes = require('./routes/api') @@ -49,6 +50,9 @@ class Application { logger.info('🔄 Initializing pricing service...') await pricingService.initialize() + // 📊 初始化缓存监控 + await this.initializeCacheMonitoring() + // 🔧 初始化管理员凭据 logger.info('🔄 Initializing admin credentials...') await this.initializeAdmin() @@ -456,6 +460,40 @@ class Application { } } + // 📊 初始化缓存监控 + async initializeCacheMonitoring() { + try { + logger.info('🔄 Initializing cache monitoring...') + + // 注册各个服务的缓存实例 + const services = [ + { name: 'claudeAccount', service: require('./services/claudeAccountService') }, + { name: 'claudeConsole', service: require('./services/claudeConsoleAccountService') }, + { name: 'bedrockAccount', service: require('./services/bedrockAccountService') } + ] + + // 注册已加载的服务缓存 + for (const { name, service } of services) { + if (service && (service._decryptCache || service.decryptCache)) { + const cache = service._decryptCache || service.decryptCache + cacheMonitor.registerCache(`${name}_decrypt`, cache) + logger.info(`✅ Registered ${name} decrypt cache for monitoring`) + } + } + + // 初始化时打印一次统计 + setTimeout(() => { + const stats = cacheMonitor.getGlobalStats() + logger.info(`📊 Cache System - Registered: ${stats.cacheCount} caches`) + }, 5000) + + logger.success('✅ Cache monitoring initialized') + } catch (error) { + logger.error('❌ Failed to initialize cache monitoring:', error) + // 不阻止应用启动 + } + } + startCleanupTasks() { // 🧹 每小时清理一次过期数据 setInterval(async () => { diff --git a/src/models/redis.js b/src/models/redis.js index 554638c5..4d62bda7 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -191,7 +191,9 @@ class RedisClient { outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, - model = 'unknown' + model = 'unknown', + ephemeral5mTokens = 0, // 新增:5分钟缓存 tokens + ephemeral1hTokens = 0 // 新增:1小时缓存 tokens ) { const key = `usage:${keyId}` const now = new Date() @@ -245,6 +247,9 @@ class RedisClient { pipeline.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens) pipeline.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens) pipeline.hincrby(key, 'totalAllTokens', totalTokens) // 包含所有类型的总token + // 详细缓存类型统计(新增) + pipeline.hincrby(key, 'totalEphemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(key, 'totalEphemeral1hTokens', ephemeral1hTokens) // 请求计数 pipeline.hincrby(key, 'totalRequests', 1) @@ -256,6 +261,9 @@ class RedisClient { pipeline.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(daily, 'allTokens', totalTokens) pipeline.hincrby(daily, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(daily, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(daily, 'ephemeral1hTokens', ephemeral1hTokens) // 每月统计 pipeline.hincrby(monthly, 'tokens', coreTokens) @@ -265,6 +273,9 @@ class RedisClient { pipeline.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(monthly, 'allTokens', totalTokens) pipeline.hincrby(monthly, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(monthly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(monthly, 'ephemeral1hTokens', ephemeral1hTokens) // 按模型统计 - 每日 pipeline.hincrby(modelDaily, 'inputTokens', finalInputTokens) @@ -289,6 +300,9 @@ class RedisClient { pipeline.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(keyModelDaily, 'allTokens', totalTokens) pipeline.hincrby(keyModelDaily, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(keyModelDaily, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(keyModelDaily, 'ephemeral1hTokens', ephemeral1hTokens) // API Key级别的模型统计 - 每月 pipeline.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens) @@ -297,6 +311,9 @@ class RedisClient { pipeline.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(keyModelMonthly, 'allTokens', totalTokens) pipeline.hincrby(keyModelMonthly, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(keyModelMonthly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(keyModelMonthly, 'ephemeral1hTokens', ephemeral1hTokens) // 小时级别统计 pipeline.hincrby(hourly, 'tokens', coreTokens) diff --git a/src/routes/openaiClaudeRoutes.js b/src/routes/openaiClaudeRoutes.js index 63308a72..b2c43ed9 100644 --- a/src/routes/openaiClaudeRoutes.js +++ b/src/routes/openaiClaudeRoutes.js @@ -250,19 +250,13 @@ async function handleChatCompletion(req, res, apiKeyData) { (usage) => { // 记录使用统计 if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) { - const inputTokens = usage.input_tokens || 0 - const outputTokens = usage.output_tokens || 0 - const cacheCreateTokens = usage.cache_creation_input_tokens || 0 - const cacheReadTokens = usage.cache_read_input_tokens || 0 const model = usage.model || claudeRequest.model + // 使用新的 recordUsageWithDetails 方法来支持详细的缓存数据 apiKeyService - .recordUsage( + .recordUsageWithDetails( apiKeyData.id, - inputTokens, - outputTokens, - cacheCreateTokens, - cacheReadTokens, + usage, // 直接传递整个 usage 对象,包含可能的 cache_creation 详细数据 model, accountId ) @@ -328,13 +322,11 @@ async function handleChatCompletion(req, res, apiKeyData) { // 记录使用统计 if (claudeData.usage) { const { usage } = claudeData + // 使用新的 recordUsageWithDetails 方法来支持详细的缓存数据 apiKeyService - .recordUsage( + .recordUsageWithDetails( apiKeyData.id, - usage.input_tokens || 0, - usage.output_tokens || 0, - usage.cache_creation_input_tokens || 0, - usage.cache_read_input_tokens || 0, + usage, // 直接传递整个 usage 对象,包含可能的 cache_creation 详细数据 claudeRequest.model, accountId ) diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 9f4033b4..cf0d9e4a 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -466,11 +466,31 @@ class ApiKeyService { const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens - // 计算费用(支持详细的缓存类型) - const pricingService = require('./pricingService') - const costInfo = pricingService.calculateCost(usageObject, model) + // 计算费用(支持详细的缓存类型)- 添加错误处理 + let costInfo = { totalCost: 0, ephemeral5mCost: 0, ephemeral1hCost: 0 } + try { + const pricingService = require('./pricingService') + // 确保 pricingService 已初始化 + if (!pricingService.pricingData) { + logger.warn('⚠️ PricingService not initialized, initializing now...') + await pricingService.initialize() + } + costInfo = pricingService.calculateCost(usageObject, model) + } catch (pricingError) { + logger.error('❌ Failed to calculate cost:', pricingError) + // 继续执行,不要因为费用计算失败而跳过统计记录 + } - // 记录API Key级别的使用统计 + // 提取详细的缓存创建数据 + let ephemeral5mTokens = 0 + let ephemeral1hTokens = 0 + + if (usageObject.cache_creation && typeof usageObject.cache_creation === 'object') { + ephemeral5mTokens = usageObject.cache_creation.ephemeral_5m_input_tokens || 0 + ephemeral1hTokens = usageObject.cache_creation.ephemeral_1h_input_tokens || 0 + } + + // 记录API Key级别的使用统计 - 这个必须执行 await redis.incrementTokenUsage( keyId, totalTokens, @@ -478,7 +498,9 @@ class ApiKeyService { outputTokens, cacheCreateTokens, cacheReadTokens, - model + model, + ephemeral5mTokens, // 传递5分钟缓存 tokens + ephemeral1hTokens // 传递1小时缓存 tokens ) // 记录费用统计 diff --git a/src/services/bedrockAccountService.js b/src/services/bedrockAccountService.js index a4fdbde3..b5e9e1a9 100644 --- a/src/services/bedrockAccountService.js +++ b/src/services/bedrockAccountService.js @@ -4,12 +4,28 @@ 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账户 @@ -336,10 +352,22 @@ class BedrockAccountService { } } + // 🔑 生成加密密钥(缓存优化) + _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 = crypto.createHash('sha256').update(config.security.encryptionKey).digest() + const key = this._generateEncryptionKey() const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv) @@ -368,15 +396,35 @@ class BedrockAccountService { // 检查是否为加密格式 (有 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 = crypto.createHash('sha256').update(config.security.encryptionKey).digest() + 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') - return JSON.parse(decrypted) + 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凭证,建议更新账户以启用加密') diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 23f0c9d4..6577535d 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -15,6 +15,7 @@ const { logRefreshSkipped } = require('../utils/tokenRefreshLogger') const tokenRefreshService = require('./tokenRefreshService') +const LRUCache = require('../utils/lruCache') class ClaudeAccountService { constructor() { @@ -24,6 +25,22 @@ class ClaudeAccountService { // 加密相关常量 this.ENCRYPTION_ALGORITHM = 'aes-256-cbc' this.ENCRYPTION_SALT = 'salt' + + // 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算 + // scryptSync 是 CPU 密集型操作,缓存可以减少 95%+ 的 CPU 占用 + this._encryptionKeyCache = null + + // 🔄 解密结果缓存,提高解密性能 + this._decryptCache = new LRUCache(500) + + // 🧹 定期清理缓存(每10分钟) + setInterval( + () => { + this._decryptCache.cleanup() + logger.info('🧹 Claude decrypt cache cleanup completed', this._decryptCache.getStats()) + }, + 10 * 60 * 1000 + ) } // 🏢 创建Claude账户 @@ -893,7 +910,16 @@ class ClaudeAccountService { return '' } + // 🎯 检查缓存 + const cacheKey = crypto.createHash('sha256').update(encryptedData).digest('hex') + const cached = this._decryptCache.get(cacheKey) + if (cached !== undefined) { + return cached + } + try { + let decrypted = '' + // 检查是否是新格式(包含IV) if (encryptedData.includes(':')) { // 新格式:iv:encryptedData @@ -904,8 +930,17 @@ class ClaudeAccountService { const encrypted = parts[1] const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv) - let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') + + // 💾 存入缓存(5分钟过期) + this._decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000) + + // 📊 定期打印缓存统计 + if ((this._decryptCache.hits + this._decryptCache.misses) % 1000 === 0) { + this._decryptCache.printStats() + } + return decrypted } } @@ -914,8 +949,12 @@ class ClaudeAccountService { // 注意:在新版本Node.js中这将失败,但我们会捕获错误 try { const decipher = crypto.createDecipher('aes-256-cbc', config.security.encryptionKey) - let decrypted = decipher.update(encryptedData, 'hex', 'utf8') + decrypted = decipher.update(encryptedData, 'hex', 'utf8') decrypted += decipher.final('utf8') + + // 💾 旧格式也存入缓存 + this._decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000) + return decrypted } catch (oldError) { // 如果旧方式也失败,返回原数据 @@ -930,7 +969,20 @@ class ClaudeAccountService { // 🔑 生成加密密钥(辅助方法) _generateEncryptionKey() { - return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32) + // 性能优化:缓存密钥派生结果,避免重复的 CPU 密集计算 + // scryptSync 是故意设计为慢速的密钥派生函数(防暴力破解) + // 但在高并发场景下,每次都重新计算会导致 CPU 100% 占用 + if (!this._encryptionKeyCache) { + // 只在第一次调用时计算,后续使用缓存 + // 由于输入参数固定,派生结果永远相同,不影响数据兼容性 + this._encryptionKeyCache = crypto.scryptSync( + config.security.encryptionKey, + this.ENCRYPTION_SALT, + 32 + ) + logger.info('🔑 Encryption key derived and cached for performance optimization') + } + return this._encryptionKeyCache } // 🎭 掩码邮箱地址 diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index 3963c10b..fd211651 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -5,6 +5,7 @@ const { HttpsProxyAgent } = require('https-proxy-agent') const redis = require('../models/redis') const logger = require('../utils/logger') const config = require('../../config/config') +const LRUCache = require('../utils/lruCache') class ClaudeConsoleAccountService { constructor() { @@ -15,6 +16,25 @@ class ClaudeConsoleAccountService { // Redis键前缀 this.ACCOUNT_KEY_PREFIX = 'claude_console_account:' this.SHARED_ACCOUNTS_KEY = 'shared_claude_console_accounts' + + // 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算 + // scryptSync 是 CPU 密集型操作,缓存可以减少 95%+ 的 CPU 密集型操作 + this._encryptionKeyCache = null + + // 🔄 解密结果缓存,提高解密性能 + this._decryptCache = new LRUCache(500) + + // 🧹 定期清理缓存(每10分钟) + setInterval( + () => { + this._decryptCache.cleanup() + logger.info( + '🧹 Claude Console decrypt cache cleanup completed', + this._decryptCache.getStats() + ) + }, + 10 * 60 * 1000 + ) } // 🏢 创建Claude Console账户 @@ -512,6 +532,13 @@ class ClaudeConsoleAccountService { return '' } + // 🎯 检查缓存 + const cacheKey = crypto.createHash('sha256').update(encryptedData).digest('hex') + const cached = this._decryptCache.get(cacheKey) + if (cached !== undefined) { + return cached + } + try { if (encryptedData.includes(':')) { const parts = encryptedData.split(':') @@ -523,6 +550,15 @@ class ClaudeConsoleAccountService { const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') + + // 💾 存入缓存(5分钟过期) + this._decryptCache.set(cacheKey, decrypted, 5 * 60 * 1000) + + // 📊 定期打印缓存统计 + if ((this._decryptCache.hits + this._decryptCache.misses) % 1000 === 0) { + this._decryptCache.printStats() + } + return decrypted } } @@ -536,7 +572,20 @@ class ClaudeConsoleAccountService { // 🔑 生成加密密钥 _generateEncryptionKey() { - return crypto.scryptSync(config.security.encryptionKey, this.ENCRYPTION_SALT, 32) + // 性能优化:缓存密钥派生结果,避免重复的 CPU 密集计算 + // scryptSync 是故意设计为慢速的密钥派生函数(防暴力破解) + // 但在高并发场景下,每次都重新计算会导致 CPU 100% 占用 + if (!this._encryptionKeyCache) { + // 只在第一次调用时计算,后续使用缓存 + // 由于输入参数固定,派生结果永远相同,不影响数据兼容性 + this._encryptionKeyCache = crypto.scryptSync( + config.security.encryptionKey, + this.ENCRYPTION_SALT, + 32 + ) + logger.info('🔑 Console encryption key derived and cached for performance optimization') + } + return this._encryptionKeyCache } // 🎭 掩码API URL diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 71ed496b..fa6c39b4 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -269,17 +269,33 @@ class ClaudeRelayService { } } - // 记录成功的API调用 - const inputTokens = requestBody.messages - ? requestBody.messages.reduce((sum, msg) => sum + (msg.content?.length || 0), 0) / 4 - : 0 // 粗略估算 - const outputTokens = response.content - ? response.content.reduce((sum, content) => sum + (content.text?.length || 0), 0) / 4 - : 0 + // 记录成功的API调用并打印详细的usage数据 + let responseBody = null + try { + responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body + } catch (e) { + logger.debug('Failed to parse response body for usage logging') + } - logger.info( - `✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens, Output: ~${Math.round(outputTokens)} tokens` - ) + if (responseBody && responseBody.usage) { + const { usage } = responseBody + // 打印原始usage数据为JSON字符串 + logger.info( + `📊 === Non-Stream Request Usage Summary === Model: ${requestBody.model}, Usage: ${JSON.stringify(usage)}` + ) + } else { + // 如果没有usage数据,使用估算值 + const inputTokens = requestBody.messages + ? requestBody.messages.reduce((sum, msg) => sum + (msg.content?.length || 0), 0) / 4 + : 0 + const outputTokens = response.content + ? response.content.reduce((sum, content) => sum + (content.text?.length || 0), 0) / 4 + : 0 + + logger.info( + `✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens (estimated), Output: ~${Math.round(outputTokens)} tokens (estimated)` + ) + } // 在响应中添加accountId,以便调用方记录账户级别统计 response.accountId = accountId @@ -893,8 +909,8 @@ class ClaudeRelayService { } let buffer = '' - let finalUsageReported = false // 防止重复统计的标志 - const collectedUsageData = {} // 收集来自不同事件的usage数据 + const allUsageData = [] // 收集所有的usage事件 + let currentUsageData = {} // 当前正在收集的usage数据 let rateLimitDetected = false // 限流检测标志 // 监听数据块,解析SSE并寻找usage信息 @@ -931,34 +947,43 @@ class ClaudeRelayService { // 收集来自不同事件的usage数据 if (data.type === 'message_start' && data.message && data.message.usage) { + // 新的消息开始,如果之前有数据,先保存 + if ( + currentUsageData.input_tokens !== undefined && + currentUsageData.output_tokens !== undefined + ) { + allUsageData.push({ ...currentUsageData }) + currentUsageData = {} + } + // message_start包含input tokens、cache tokens和模型信息 - collectedUsageData.input_tokens = data.message.usage.input_tokens || 0 - collectedUsageData.cache_creation_input_tokens = + currentUsageData.input_tokens = data.message.usage.input_tokens || 0 + currentUsageData.cache_creation_input_tokens = data.message.usage.cache_creation_input_tokens || 0 - collectedUsageData.cache_read_input_tokens = + currentUsageData.cache_read_input_tokens = data.message.usage.cache_read_input_tokens || 0 - collectedUsageData.model = data.message.model + currentUsageData.model = data.message.model // 检查是否有详细的 cache_creation 对象 if ( data.message.usage.cache_creation && typeof data.message.usage.cache_creation === 'object' ) { - collectedUsageData.cache_creation = { + currentUsageData.cache_creation = { ephemeral_5m_input_tokens: data.message.usage.cache_creation.ephemeral_5m_input_tokens || 0, ephemeral_1h_input_tokens: data.message.usage.cache_creation.ephemeral_1h_input_tokens || 0 } - logger.info( + logger.debug( '📊 Collected detailed cache creation data:', - JSON.stringify(collectedUsageData.cache_creation) + JSON.stringify(currentUsageData.cache_creation) ) } - logger.info( + logger.debug( '📊 Collected input/cache data from message_start:', - JSON.stringify(collectedUsageData) + JSON.stringify(currentUsageData) ) } @@ -968,18 +993,27 @@ class ClaudeRelayService { data.usage && data.usage.output_tokens !== undefined ) { - collectedUsageData.output_tokens = data.usage.output_tokens || 0 + currentUsageData.output_tokens = data.usage.output_tokens || 0 - logger.info( + logger.debug( '📊 Collected output data from message_delta:', - JSON.stringify(collectedUsageData) + JSON.stringify(currentUsageData) ) - // 如果已经收集到了input数据,现在有了output数据,可以统计了 - if (collectedUsageData.input_tokens !== undefined && !finalUsageReported) { - logger.info('🎯 Complete usage data collected, triggering callback') - usageCallback(collectedUsageData) - finalUsageReported = true + // 如果已经收集到了input数据和output数据,这是一个完整的usage + if (currentUsageData.input_tokens !== undefined) { + logger.debug( + '🎯 Complete usage data collected for model:', + currentUsageData.model, + '- Input:', + currentUsageData.input_tokens, + 'Output:', + currentUsageData.output_tokens + ) + // 保存到列表中,但不立即触发回调 + allUsageData.push({ ...currentUsageData }) + // 重置当前数据,准备接收下一个 + currentUsageData = {} } } @@ -1037,11 +1071,73 @@ class ClaudeRelayService { logger.error('❌ Error processing stream end:', error) } + // 如果还有未完成的usage数据,尝试保存 + if (currentUsageData.input_tokens !== undefined) { + if (currentUsageData.output_tokens === undefined) { + currentUsageData.output_tokens = 0 // 如果没有output,设为0 + } + allUsageData.push(currentUsageData) + } + // 检查是否捕获到usage数据 - if (!finalUsageReported) { + if (allUsageData.length === 0) { logger.warn( '⚠️ Stream completed but no usage data was captured! This indicates a problem with SSE parsing or Claude API response format.' ) + } else { + // 打印此次请求的所有usage数据汇总 + const totalUsage = allUsageData.reduce( + (acc, usage) => ({ + input_tokens: (acc.input_tokens || 0) + (usage.input_tokens || 0), + output_tokens: (acc.output_tokens || 0) + (usage.output_tokens || 0), + cache_creation_input_tokens: + (acc.cache_creation_input_tokens || 0) + (usage.cache_creation_input_tokens || 0), + cache_read_input_tokens: + (acc.cache_read_input_tokens || 0) + (usage.cache_read_input_tokens || 0), + models: [...(acc.models || []), usage.model].filter(Boolean) + }), + {} + ) + + // 打印原始的usage数据为JSON字符串,避免嵌套问题 + logger.info( + `📊 === Stream Request Usage Summary === Model: ${body.model}, Total Events: ${allUsageData.length}, Usage Data: ${JSON.stringify(allUsageData)}` + ) + + // 一般一个请求只会使用一个模型,即使有多个usage事件也应该合并 + // 计算总的usage + const finalUsage = { + input_tokens: totalUsage.input_tokens, + output_tokens: totalUsage.output_tokens, + cache_creation_input_tokens: totalUsage.cache_creation_input_tokens, + cache_read_input_tokens: totalUsage.cache_read_input_tokens, + model: allUsageData[allUsageData.length - 1].model || body.model // 使用最后一个模型或请求模型 + } + + // 如果有详细的cache_creation数据,合并它们 + let totalEphemeral5m = 0 + let totalEphemeral1h = 0 + allUsageData.forEach((usage) => { + if (usage.cache_creation && typeof usage.cache_creation === 'object') { + totalEphemeral5m += usage.cache_creation.ephemeral_5m_input_tokens || 0 + totalEphemeral1h += usage.cache_creation.ephemeral_1h_input_tokens || 0 + } + }) + + // 如果有详细的缓存数据,添加到finalUsage + if (totalEphemeral5m > 0 || totalEphemeral1h > 0) { + finalUsage.cache_creation = { + ephemeral_5m_input_tokens: totalEphemeral5m, + ephemeral_1h_input_tokens: totalEphemeral1h + } + logger.info( + '📊 Detailed cache creation breakdown:', + JSON.stringify(finalUsage.cache_creation) + ) + } + + // 调用一次usageCallback记录合并后的数据 + usageCallback(finalUsage) } // 处理限流状态 diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index 050eac9f..5b71d566 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -13,6 +13,7 @@ const { logRefreshSkipped } = require('../utils/tokenRefreshLogger') const tokenRefreshService = require('./tokenRefreshService') +const LRUCache = require('../utils/lruCache') // Gemini CLI OAuth 配置 - 这些是公开的 Gemini CLI 凭据 const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com' @@ -24,9 +25,20 @@ const ALGORITHM = 'aes-256-cbc' const ENCRYPTION_SALT = 'gemini-account-salt' const IV_LENGTH = 16 +// 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算 +// scryptSync 是 CPU 密集型操作,缓存可以减少 95%+ 的 CPU 占用 +let _encryptionKeyCache = null + +// 🔄 解密结果缓存,提高解密性能 +const decryptCache = new LRUCache(500) + // 生成加密密钥(使用与 claudeAccountService 相同的方法) function generateEncryptionKey() { - return crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32) + if (!_encryptionKeyCache) { + _encryptionKeyCache = crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32) + logger.info('🔑 Gemini encryption key derived and cached for performance optimization') + } + return _encryptionKeyCache } // Gemini 账户键前缀 @@ -52,6 +64,14 @@ function decrypt(text) { if (!text) { return '' } + + // 🎯 检查缓存 + const cacheKey = crypto.createHash('sha256').update(text).digest('hex') + const cached = decryptCache.get(cacheKey) + if (cached !== undefined) { + return cached + } + try { const key = generateEncryptionKey() // IV 是固定长度的 32 个十六进制字符(16 字节) @@ -63,13 +83,32 @@ function decrypt(text) { const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) let decrypted = decipher.update(encryptedText) decrypted = Buffer.concat([decrypted, decipher.final()]) - return decrypted.toString() + const result = decrypted.toString() + + // 💾 存入缓存(5分钟过期) + decryptCache.set(cacheKey, result, 5 * 60 * 1000) + + // 📊 定期打印缓存统计 + if ((decryptCache.hits + decryptCache.misses) % 1000 === 0) { + decryptCache.printStats() + } + + return result } catch (error) { logger.error('Decryption error:', error) return '' } } +// 🧹 定期清理缓存(每10分钟) +setInterval( + () => { + decryptCache.cleanup() + logger.info('🧹 Gemini decrypt cache cleanup completed', decryptCache.getStats()) + }, + 10 * 60 * 1000 +) + // 创建 OAuth2 客户端 function createOAuth2Client(redirectUri = null) { // 如果没有提供 redirectUri,使用默认值 @@ -1248,6 +1287,10 @@ module.exports = { getOnboardTier, onboardUser, setupUser, + encrypt, + decrypt, + generateEncryptionKey, + decryptCache, // 暴露缓存对象以便测试和监控 countTokens, generateContent, generateContentStream, diff --git a/src/services/openaiAccountService.js b/src/services/openaiAccountService.js index a1265295..5326abb2 100644 --- a/src/services/openaiAccountService.js +++ b/src/services/openaiAccountService.js @@ -14,6 +14,7 @@ const { logTokenUsage, logRefreshSkipped } = require('../utils/tokenRefreshLogger') +const LRUCache = require('../utils/lruCache') // const tokenRefreshService = require('./tokenRefreshService') // 加密相关常量 @@ -21,9 +22,20 @@ const ALGORITHM = 'aes-256-cbc' const ENCRYPTION_SALT = 'openai-account-salt' const IV_LENGTH = 16 +// 🚀 性能优化:缓存派生的加密密钥,避免每次重复计算 +// scryptSync 是 CPU 密集型操作,缓存可以减少 95%+ 的 CPU 占用 +let _encryptionKeyCache = null + +// 🔄 解密结果缓存,提高解密性能 +const decryptCache = new LRUCache(500) + // 生成加密密钥(使用与 claudeAccountService 相同的方法) function generateEncryptionKey() { - return crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32) + if (!_encryptionKeyCache) { + _encryptionKeyCache = crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32) + logger.info('🔑 OpenAI encryption key derived and cached for performance optimization') + } + return _encryptionKeyCache } // OpenAI 账户键前缀 @@ -49,6 +61,14 @@ function decrypt(text) { if (!text) { return '' } + + // 🎯 检查缓存 + const cacheKey = crypto.createHash('sha256').update(text).digest('hex') + const cached = decryptCache.get(cacheKey) + if (cached !== undefined) { + return cached + } + try { const key = generateEncryptionKey() // IV 是固定长度的 32 个十六进制字符(16 字节) @@ -60,13 +80,32 @@ function decrypt(text) { const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) let decrypted = decipher.update(encryptedText) decrypted = Buffer.concat([decrypted, decipher.final()]) - return decrypted.toString() + const result = decrypted.toString() + + // 💾 存入缓存(5分钟过期) + decryptCache.set(cacheKey, result, 5 * 60 * 1000) + + // 📊 定期打印缓存统计 + if ((decryptCache.hits + decryptCache.misses) % 1000 === 0) { + decryptCache.printStats() + } + + return result } catch (error) { logger.error('Decryption error:', error) return '' } } +// 🧹 定期清理缓存(每10分钟) +setInterval( + () => { + decryptCache.cleanup() + logger.info('🧹 OpenAI decrypt cache cleanup completed', decryptCache.getStats()) + }, + 10 * 60 * 1000 +) + // 刷新访问令牌 async function refreshAccessToken(refreshToken, proxy = null) { try { @@ -693,5 +732,7 @@ module.exports = { updateAccountUsage, recordUsage, // 别名,指向updateAccountUsage encrypt, - decrypt + decrypt, + generateEncryptionKey, + decryptCache // 暴露缓存对象以便测试和监控 } diff --git a/src/utils/cacheMonitor.js b/src/utils/cacheMonitor.js new file mode 100644 index 00000000..ece5e478 --- /dev/null +++ b/src/utils/cacheMonitor.js @@ -0,0 +1,294 @@ +/** + * 缓存监控和管理工具 + * 提供统一的缓存监控、统计和安全清理功能 + */ + +const logger = require('./logger') +const crypto = require('crypto') + +class CacheMonitor { + constructor() { + this.monitors = new Map() // 存储所有被监控的缓存实例 + this.startTime = Date.now() + this.totalHits = 0 + this.totalMisses = 0 + this.totalEvictions = 0 + + // 🔒 安全配置 + this.securityConfig = { + maxCacheAge: 15 * 60 * 1000, // 最大缓存年龄 15 分钟 + forceCleanupInterval: 30 * 60 * 1000, // 强制清理间隔 30 分钟 + memoryThreshold: 100 * 1024 * 1024, // 内存阈值 100MB + sensitiveDataPatterns: [/password/i, /token/i, /secret/i, /key/i, /credential/i] + } + + // 🧹 定期执行安全清理 + this.setupSecurityCleanup() + + // 📊 定期报告统计信息 + this.setupPeriodicReporting() + } + + /** + * 注册缓存实例进行监控 + * @param {string} name - 缓存名称 + * @param {LRUCache} cache - 缓存实例 + */ + registerCache(name, cache) { + if (this.monitors.has(name)) { + logger.warn(`⚠️ Cache ${name} is already registered, updating reference`) + } + + this.monitors.set(name, { + cache, + registeredAt: Date.now(), + lastCleanup: Date.now(), + totalCleanups: 0 + }) + + logger.info(`📦 Registered cache for monitoring: ${name}`) + } + + /** + * 获取所有缓存的综合统计 + */ + getGlobalStats() { + const stats = { + uptime: Math.floor((Date.now() - this.startTime) / 1000), // 秒 + cacheCount: this.monitors.size, + totalSize: 0, + totalHits: 0, + totalMisses: 0, + totalEvictions: 0, + averageHitRate: 0, + caches: {} + } + + for (const [name, monitor] of this.monitors) { + const cacheStats = monitor.cache.getStats() + stats.totalSize += cacheStats.size + stats.totalHits += cacheStats.hits + stats.totalMisses += cacheStats.misses + stats.totalEvictions += cacheStats.evictions + + stats.caches[name] = { + ...cacheStats, + lastCleanup: new Date(monitor.lastCleanup).toISOString(), + totalCleanups: monitor.totalCleanups, + age: Math.floor((Date.now() - monitor.registeredAt) / 1000) // 秒 + } + } + + const totalRequests = stats.totalHits + stats.totalMisses + stats.averageHitRate = + totalRequests > 0 ? `${((stats.totalHits / totalRequests) * 100).toFixed(2)}%` : '0%' + + return stats + } + + /** + * 🔒 执行安全清理 + * 清理过期数据和潜在的敏感信息 + */ + performSecurityCleanup() { + logger.info('🔒 Starting security cleanup for all caches') + + for (const [name, monitor] of this.monitors) { + try { + const { cache } = monitor + const beforeSize = cache.cache.size + + // 执行常规清理 + cache.cleanup() + + // 检查缓存年龄,如果太老则完全清空 + const cacheAge = Date.now() - monitor.registeredAt + if (cacheAge > this.securityConfig.maxCacheAge * 2) { + logger.warn( + `⚠️ Cache ${name} is too old (${Math.floor(cacheAge / 60000)}min), performing full clear` + ) + cache.clear() + } + + monitor.lastCleanup = Date.now() + monitor.totalCleanups++ + + const afterSize = cache.cache.size + if (beforeSize !== afterSize) { + logger.info(`🧹 Cache ${name}: Cleaned ${beforeSize - afterSize} items`) + } + } catch (error) { + logger.error(`❌ Error cleaning cache ${name}:`, error) + } + } + } + + /** + * 📊 生成详细报告 + */ + generateReport() { + const stats = this.getGlobalStats() + + logger.info('═══════════════════════════════════════════') + logger.info('📊 Cache System Performance Report') + logger.info('═══════════════════════════════════════════') + logger.info(`⏱️ Uptime: ${this.formatUptime(stats.uptime)}`) + logger.info(`📦 Active Caches: ${stats.cacheCount}`) + logger.info(`📈 Total Cache Size: ${stats.totalSize} items`) + logger.info(`🎯 Global Hit Rate: ${stats.averageHitRate}`) + logger.info(`✅ Total Hits: ${stats.totalHits.toLocaleString()}`) + logger.info(`❌ Total Misses: ${stats.totalMisses.toLocaleString()}`) + logger.info(`🗑️ Total Evictions: ${stats.totalEvictions.toLocaleString()}`) + logger.info('───────────────────────────────────────────') + + // 详细的每个缓存统计 + for (const [name, cacheStats] of Object.entries(stats.caches)) { + logger.info(`\n📦 ${name}:`) + logger.info( + ` Size: ${cacheStats.size}/${cacheStats.maxSize} | Hit Rate: ${cacheStats.hitRate}` + ) + logger.info( + ` Hits: ${cacheStats.hits} | Misses: ${cacheStats.misses} | Evictions: ${cacheStats.evictions}` + ) + logger.info( + ` Age: ${this.formatUptime(cacheStats.age)} | Cleanups: ${cacheStats.totalCleanups}` + ) + } + logger.info('═══════════════════════════════════════════') + } + + /** + * 🧹 设置定期安全清理 + */ + setupSecurityCleanup() { + // 每 10 分钟执行一次安全清理 + setInterval( + () => { + this.performSecurityCleanup() + }, + 10 * 60 * 1000 + ) + + // 每 30 分钟强制完整清理 + setInterval(() => { + logger.warn('⚠️ Performing forced complete cleanup for security') + for (const [name, monitor] of this.monitors) { + monitor.cache.clear() + logger.info(`🗑️ Force cleared cache: ${name}`) + } + }, this.securityConfig.forceCleanupInterval) + } + + /** + * 📊 设置定期报告 + */ + setupPeriodicReporting() { + // 每 5 分钟生成一次简单统计 + setInterval( + () => { + const stats = this.getGlobalStats() + logger.info( + `📊 Quick Stats - Caches: ${stats.cacheCount}, Size: ${stats.totalSize}, Hit Rate: ${stats.averageHitRate}` + ) + }, + 5 * 60 * 1000 + ) + + // 每 30 分钟生成一次详细报告 + setInterval( + () => { + this.generateReport() + }, + 30 * 60 * 1000 + ) + } + + /** + * 格式化运行时间 + */ + formatUptime(seconds) { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + + if (hours > 0) { + return `${hours}h ${minutes}m ${secs}s` + } else if (minutes > 0) { + return `${minutes}m ${secs}s` + } else { + return `${secs}s` + } + } + + /** + * 🔐 生成安全的缓存键 + * 使用 SHA-256 哈希避免暴露原始数据 + */ + static generateSecureCacheKey(data) { + return crypto.createHash('sha256').update(data).digest('hex') + } + + /** + * 🛡️ 验证缓存数据安全性 + * 检查是否包含敏感信息 + */ + validateCacheSecurity(data) { + const dataStr = typeof data === 'string' ? data : JSON.stringify(data) + + for (const pattern of this.securityConfig.sensitiveDataPatterns) { + if (pattern.test(dataStr)) { + logger.warn('⚠️ Potential sensitive data detected in cache') + return false + } + } + + return true + } + + /** + * 💾 获取内存使用估算 + */ + estimateMemoryUsage() { + let totalBytes = 0 + + for (const [, monitor] of this.monitors) { + const { cache } = monitor.cache + for (const [key, item] of cache) { + // 粗略估算:key 长度 + value 序列化长度 + totalBytes += key.length * 2 // UTF-16 + totalBytes += JSON.stringify(item).length * 2 + } + } + + return { + bytes: totalBytes, + mb: (totalBytes / (1024 * 1024)).toFixed(2), + warning: totalBytes > this.securityConfig.memoryThreshold + } + } + + /** + * 🚨 紧急清理 + * 在内存压力大时使用 + */ + emergencyCleanup() { + logger.error('🚨 EMERGENCY CLEANUP INITIATED') + + for (const [name, monitor] of this.monitors) { + const { cache } = monitor + const beforeSize = cache.cache.size + + // 清理一半的缓存项(LRU 会保留最近使用的) + const targetSize = Math.floor(cache.maxSize / 2) + while (cache.cache.size > targetSize) { + const firstKey = cache.cache.keys().next().value + cache.cache.delete(firstKey) + } + + logger.warn(`🚨 Emergency cleaned ${name}: ${beforeSize} -> ${cache.cache.size} items`) + } + } +} + +// 导出单例 +module.exports = new CacheMonitor() diff --git a/src/utils/lruCache.js b/src/utils/lruCache.js new file mode 100644 index 00000000..993089ba --- /dev/null +++ b/src/utils/lruCache.js @@ -0,0 +1,134 @@ +/** + * LRU (Least Recently Used) 缓存实现 + * 用于缓存解密结果,提高性能同时控制内存使用 + */ +class LRUCache { + constructor(maxSize = 500) { + this.maxSize = maxSize + this.cache = new Map() + this.hits = 0 + this.misses = 0 + this.evictions = 0 + this.lastCleanup = Date.now() + this.cleanupInterval = 5 * 60 * 1000 // 5分钟清理一次过期项 + } + + /** + * 获取缓存值 + * @param {string} key - 缓存键 + * @returns {*} 缓存的值,如果不存在则返回 undefined + */ + get(key) { + // 定期清理 + if (Date.now() - this.lastCleanup > this.cleanupInterval) { + this.cleanup() + } + + const item = this.cache.get(key) + if (!item) { + this.misses++ + return undefined + } + + // 检查是否过期 + if (item.expiry && Date.now() > item.expiry) { + this.cache.delete(key) + this.misses++ + return undefined + } + + // 更新访问时间,将元素移到最后(最近使用) + this.cache.delete(key) + this.cache.set(key, { + ...item, + lastAccessed: Date.now() + }) + + this.hits++ + return item.value + } + + /** + * 设置缓存值 + * @param {string} key - 缓存键 + * @param {*} value - 要缓存的值 + * @param {number} ttl - 生存时间(毫秒),默认5分钟 + */ + set(key, value, ttl = 5 * 60 * 1000) { + // 如果缓存已满,删除最少使用的项 + if (this.cache.size >= this.maxSize && !this.cache.has(key)) { + const firstKey = this.cache.keys().next().value + this.cache.delete(firstKey) + this.evictions++ + } + + this.cache.set(key, { + value, + createdAt: Date.now(), + lastAccessed: Date.now(), + expiry: ttl ? Date.now() + ttl : null + }) + } + + /** + * 清理过期项 + */ + cleanup() { + const now = Date.now() + let cleanedCount = 0 + + for (const [key, item] of this.cache.entries()) { + if (item.expiry && now > item.expiry) { + this.cache.delete(key) + cleanedCount++ + } + } + + this.lastCleanup = now + if (cleanedCount > 0) { + console.log(`🧹 LRU Cache: Cleaned ${cleanedCount} expired items`) + } + } + + /** + * 清空缓存 + */ + clear() { + const { size } = this.cache + this.cache.clear() + this.hits = 0 + this.misses = 0 + this.evictions = 0 + console.log(`🗑️ LRU Cache: Cleared ${size} items`) + } + + /** + * 获取缓存统计信息 + */ + getStats() { + const total = this.hits + this.misses + const hitRate = total > 0 ? ((this.hits / total) * 100).toFixed(2) : 0 + + return { + size: this.cache.size, + maxSize: this.maxSize, + hits: this.hits, + misses: this.misses, + evictions: this.evictions, + hitRate: `${hitRate}%`, + total + } + } + + /** + * 打印缓存统计信息 + */ + printStats() { + const stats = this.getStats() + console.log( + `📊 LRU Cache Stats: Size: ${stats.size}/${stats.maxSize}, Hit Rate: ${stats.hitRate}, Hits: ${stats.hits}, Misses: ${stats.misses}, Evictions: ${stats.evictions}` + ) + } +} + +module.exports = LRUCache