mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'dev'
This commit is contained in:
38
src/app.js
38
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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
// 记录费用统计
|
||||
|
||||
@@ -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凭证,建议更新账户以启用加密')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 🎭 掩码邮箱地址
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 处理限流状态
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 // 暴露缓存对象以便测试和监控
|
||||
}
|
||||
|
||||
294
src/utils/cacheMonitor.js
Normal file
294
src/utils/cacheMonitor.js
Normal file
@@ -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()
|
||||
134
src/utils/lruCache.js
Normal file
134
src/utils/lruCache.js
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user