From e553734e42d2dac0b0a0114431cc13f84f1a5b92 Mon Sep 17 00:00:00 2001 From: andersonby Date: Thu, 7 Aug 2025 00:53:14 +0800 Subject: [PATCH 1/4] chore: update user agent version to 1.0.69 in claudeConsoleAccountService and claudeConsoleRelayService --- src/services/claudeConsoleAccountService.js | 2 +- src/services/claudeConsoleRelayService.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index db5a73bf..c78a89bc 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -26,7 +26,7 @@ class ClaudeConsoleAccountService { apiKey = '', priority = 50, // 默认优先级50(1-100) supportedModels = [], // 支持的模型列表或映射表,空数组/对象表示支持所有 - userAgent = 'claude-cli/1.0.61 (console, cli)', + userAgent = 'claude-cli/1.0.69 (external, cli)', rateLimitDuration = 60, // 限流时间(分钟) proxy = null, isActive = true, diff --git a/src/services/claudeConsoleRelayService.js b/src/services/claudeConsoleRelayService.js index 361d106a..40a8b02d 100644 --- a/src/services/claudeConsoleRelayService.js +++ b/src/services/claudeConsoleRelayService.js @@ -5,7 +5,7 @@ const config = require('../../config/config'); class ClaudeConsoleRelayService { constructor() { - this.defaultUserAgent = 'claude-cli/1.0.61 (console, cli)'; + this.defaultUserAgent = 'claude-cli/1.0.69 (external, cli)'; } // 🚀 转发请求到Claude Console API From 3dc0b7ff4fbe253972fb0c54252236f346209fcb Mon Sep 17 00:00:00 2001 From: andersonby Date: Thu, 7 Aug 2025 01:02:26 +0800 Subject: [PATCH 2/4] fix: improve decryption logic in BedrockAccountService to handle both encrypted and plaintext AWS credentials --- src/services/bedrockAccountService.js | 33 ++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/services/bedrockAccountService.js b/src/services/bedrockAccountService.js index 587bd637..83049314 100644 --- a/src/services/bedrockAccountService.js +++ b/src/services/bedrockAccountService.js @@ -340,23 +340,30 @@ class BedrockAccountService { throw new Error('Invalid encrypted data format'); } - // 检查必要字段 - if (!encryptedData.encrypted || !encryptedData.iv) { + // 检查是否为加密格式 (有 encrypted 和 iv 字段) + if (encryptedData.encrypted && encryptedData.iv) { + // 加密数据 - 进行解密 + const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest(); + 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); + } else if (encryptedData.accessKeyId) { + // 纯文本数据 - 直接返回 (向后兼容) + logger.warn('⚠️ 发现未加密的AWS凭证,建议更新账户以启用加密'); + return encryptedData; + } else { + // 既不是加密格式也不是有效的凭证格式 logger.error('❌ 缺少加密数据字段:', { hasEncrypted: !!encryptedData.encrypted, - hasIv: !!encryptedData.iv + hasIv: !!encryptedData.iv, + hasAccessKeyId: !!encryptedData.accessKeyId }); - throw new Error('Missing encrypted data fields'); + throw new Error('Missing encrypted data fields or valid credentials'); } - - const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest(); - 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); } catch (error) { logger.error('❌ AWS凭证解密失败', error); throw new Error('Credentials decryption failed'); From 622c4047e953a9c3bc58c6540de4ab7c9b3e25f7 Mon Sep 17 00:00:00 2001 From: andersonby Date: Thu, 7 Aug 2025 01:10:30 +0800 Subject: [PATCH 3/4] fix: update account retrieval logic in BedrockAccountService to handle missing accounts and re-encrypt AWS credentials --- src/services/bedrockAccountService.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/services/bedrockAccountService.js b/src/services/bedrockAccountService.js index 83049314..e919e460 100644 --- a/src/services/bedrockAccountService.js +++ b/src/services/bedrockAccountService.js @@ -155,12 +155,14 @@ class BedrockAccountService { // ✏️ 更新账户信息 async updateAccount(accountId, updates = {}) { try { - const accountResult = await this.getAccount(accountId); - if (!accountResult.success) { - return accountResult; + // 获取原始账户数据(不解密凭证) + const client = redis.getClientSafe(); + const accountData = await client.get(`bedrock_account:${accountId}`); + if (!accountData) { + return { success: false, error: 'Account not found' }; } - const account = accountResult.data; + const account = JSON.parse(accountData); // 更新字段 if (updates.name !== undefined) account.name = updates.name; @@ -180,11 +182,15 @@ class BedrockAccountService { } 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}`); } account.updatedAt = new Date().toISOString(); - const client = redis.getClientSafe(); await client.set(`bedrock_account:${accountId}`, JSON.stringify(account)); logger.info(`✅ 更新Bedrock账户成功 - ID: ${accountId}, 名称: ${account.name}`); From 7ea899bd30508cebd6801bdb93ac6ce026df0301 Mon Sep 17 00:00:00 2001 From: andersonby Date: Thu, 7 Aug 2025 01:35:01 +0800 Subject: [PATCH 4/4] feat: add model name normalization for statistics aggregation in Redis and admin routes --- src/models/redis.js | 42 +++++++++++++++++++++------ src/routes/admin.js | 52 +++++++++++++++++++++++++++++----- src/services/pricingService.js | 24 ++++++++++++++++ 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/models/redis.js b/src/models/redis.js index 5d934750..b27fdf9c 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -164,6 +164,24 @@ class RedisClient { } // 📊 使用统计相关操作(支持缓存token统计和模型信息) + // 标准化模型名称,用于统计聚合 + _normalizeModelName(model) { + if (!model || model === 'unknown') return model; + + // 对于Bedrock模型,去掉区域前缀进行统一 + if (model.includes('.anthropic.') || model.includes('.claude')) { + // 匹配所有AWS区域格式:region.anthropic.model-name-v1:0 -> claude-model-name + // 支持所有AWS区域格式,如:us-east-1, eu-west-1, ap-southeast-1, ca-central-1等 + let normalized = model.replace(/^[a-z0-9-]+\./, ''); // 去掉任何区域前缀(更通用) + normalized = normalized.replace('anthropic.', ''); // 去掉anthropic前缀 + normalized = normalized.replace(/-v\d+:\d+$/, ''); // 去掉版本后缀(如-v1:0, -v2:1等) + return normalized; + } + + // 对于其他模型,去掉常见的版本后缀 + return model.replace(/-v\d+:\d+$|:latest$/, ''); + } + async incrementTokenUsage(keyId, tokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') { const key = `usage:${keyId}`; const now = new Date(); @@ -176,15 +194,18 @@ class RedisClient { const monthly = `usage:monthly:${keyId}:${currentMonth}`; const hourly = `usage:hourly:${keyId}:${currentHour}`; // 新增小时级别key + // 标准化模型名用于统计聚合 + const normalizedModel = this._normalizeModelName(model); + // 按模型统计的键 - const modelDaily = `usage:model:daily:${model}:${today}`; - const modelMonthly = `usage:model:monthly:${model}:${currentMonth}`; - const modelHourly = `usage:model:hourly:${model}:${currentHour}`; // 新增模型小时级别 + const modelDaily = `usage:model:daily:${normalizedModel}:${today}`; + const modelMonthly = `usage:model:monthly:${normalizedModel}:${currentMonth}`; + const modelHourly = `usage:model:hourly:${normalizedModel}:${currentHour}`; // 新增模型小时级别 // API Key级别的模型统计 - const keyModelDaily = `usage:${keyId}:model:daily:${model}:${today}`; - const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`; - const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别 + const keyModelDaily = `usage:${keyId}:model:daily:${normalizedModel}:${today}`; + const keyModelMonthly = `usage:${keyId}:model:monthly:${normalizedModel}:${currentMonth}`; + const keyModelHourly = `usage:${keyId}:model:hourly:${normalizedModel}:${currentHour}`; // 新增API Key模型小时级别 // 新增:系统级分钟统计 const minuteTimestamp = Math.floor(now.getTime() / 60000); @@ -333,10 +354,13 @@ class RedisClient { const accountMonthly = `account_usage:monthly:${accountId}:${currentMonth}`; const accountHourly = `account_usage:hourly:${accountId}:${currentHour}`; + // 标准化模型名用于统计聚合 + const normalizedModel = this._normalizeModelName(model); + // 账户按模型统计的键 - const accountModelDaily = `account_usage:model:daily:${accountId}:${model}:${today}`; - const accountModelMonthly = `account_usage:model:monthly:${accountId}:${model}:${currentMonth}`; - const accountModelHourly = `account_usage:model:hourly:${accountId}:${model}:${currentHour}`; + const accountModelDaily = `account_usage:model:daily:${accountId}:${normalizedModel}:${today}`; + const accountModelMonthly = `account_usage:model:monthly:${accountId}:${normalizedModel}:${currentMonth}`; + const accountModelHourly = `account_usage:model:hourly:${accountId}:${normalizedModel}:${currentHour}`; // 处理token分配 const finalInputTokens = inputTokens || 0; diff --git a/src/routes/admin.js b/src/routes/admin.js index 643fb2e4..01c75810 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -2095,6 +2095,24 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { logger.info(`📊 Found ${allKeys.length} matching keys in total`); + // 模型名标准化函数(与redis.js保持一致) + const normalizeModelName = (model) => { + if (!model || model === 'unknown') return model; + + // 对于Bedrock模型,去掉区域前缀进行统一 + if (model.includes('.anthropic.') || model.includes('.claude')) { + // 匹配所有AWS区域格式:region.anthropic.model-name-v1:0 -> claude-model-name + // 支持所有AWS区域格式,如:us-east-1, eu-west-1, ap-southeast-1, ca-central-1等 + let normalized = model.replace(/^[a-z0-9-]+\./, ''); // 去掉任何区域前缀(更通用) + normalized = normalized.replace('anthropic.', ''); // 去掉anthropic前缀 + normalized = normalized.replace(/-v\d+:\d+$/, ''); // 去掉版本后缀(如-v1:0, -v2:1等) + return normalized; + } + + // 对于其他模型,去掉常见的版本后缀 + return model.replace(/-v\d+:\d+$|:latest$/, ''); + }; + // 聚合相同模型的数据 const modelStatsMap = new Map(); @@ -2106,11 +2124,12 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { continue; } - const model = match[1]; + const rawModel = match[1]; + const normalizedModel = normalizeModelName(rawModel); const data = await client.hgetall(key); if (data && Object.keys(data).length > 0) { - const stats = modelStatsMap.get(model) || { + const stats = modelStatsMap.get(normalizedModel) || { requests: 0, inputTokens: 0, outputTokens: 0, @@ -2126,7 +2145,7 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0; stats.allTokens += parseInt(data.allTokens) || 0; - modelStatsMap.set(model, stats); + modelStatsMap.set(normalizedModel, stats); } } @@ -2950,6 +2969,24 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { logger.info(`💰 Calculating usage costs for period: ${period}`); + // 模型名标准化函数(与redis.js保持一致) + const normalizeModelName = (model) => { + if (!model || model === 'unknown') return model; + + // 对于Bedrock模型,去掉区域前缀进行统一 + if (model.includes('.anthropic.') || model.includes('.claude')) { + // 匹配所有AWS区域格式:region.anthropic.model-name-v1:0 -> claude-model-name + // 支持所有AWS区域格式,如:us-east-1, eu-west-1, ap-southeast-1, ca-central-1等 + let normalized = model.replace(/^[a-z0-9-]+\./, ''); // 去掉任何区域前缀(更通用) + normalized = normalized.replace('anthropic.', ''); // 去掉anthropic前缀 + normalized = normalized.replace(/-v\d+:\d+$/, ''); // 去掉版本后缀(如-v1:0, -v2:1等) + return normalized; + } + + // 对于其他模型,去掉常见的版本后缀 + return model.replace(/-v\d+:\d+$|:latest$/, ''); + }; + // 获取所有API Keys的使用统计 const apiKeys = await apiKeyService.getAllApiKeys(); @@ -2992,12 +3029,13 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/); if (!modelMatch) continue; - const model = modelMatch[1]; + const rawModel = modelMatch[1]; + const normalizedModel = normalizeModelName(rawModel); const data = await client.hgetall(key); if (data && Object.keys(data).length > 0) { - if (!modelUsageMap.has(model)) { - modelUsageMap.set(model, { + if (!modelUsageMap.has(normalizedModel)) { + modelUsageMap.set(normalizedModel, { inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, @@ -3005,7 +3043,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { }); } - const modelUsage = modelUsageMap.get(model); + const modelUsage = modelUsageMap.get(normalizedModel); modelUsage.inputTokens += parseInt(data.inputTokens) || 0; modelUsage.outputTokens += parseInt(data.outputTokens) || 0; modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0; diff --git a/src/services/pricingService.js b/src/services/pricingService.js index 6d8b586d..be4ef21c 100644 --- a/src/services/pricingService.js +++ b/src/services/pricingService.js @@ -203,6 +203,17 @@ class PricingService { return this.pricingData[modelName]; } + // 对于Bedrock区域前缀模型(如 us.anthropic.claude-sonnet-4-20250514-v1:0), + // 尝试去掉区域前缀进行匹配 + if (modelName.includes('.anthropic.') || modelName.includes('.claude')) { + // 提取不带区域前缀的模型名 + const withoutRegion = modelName.replace(/^(us|eu|apac)\./, ''); + if (this.pricingData[withoutRegion]) { + logger.debug(`💰 Found pricing for ${modelName} by removing region prefix: ${withoutRegion}`); + return this.pricingData[withoutRegion]; + } + } + // 尝试模糊匹配(处理版本号等变化) const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, ''); @@ -214,6 +225,19 @@ class PricingService { } } + // 对于Bedrock模型,尝试更智能的匹配 + if (modelName.includes('anthropic.claude')) { + // 提取核心模型名部分(去掉区域和前缀) + const coreModel = modelName.replace(/^(us|eu|apac)\./, '').replace('anthropic.', ''); + + for (const [key, value] of Object.entries(this.pricingData)) { + if (key.includes(coreModel) || key.replace('anthropic.', '').includes(coreModel)) { + logger.debug(`💰 Found pricing for ${modelName} using Bedrock core model match: ${key}`); + return value; + } + } + } + logger.debug(`💰 No pricing found for model: ${modelName}`); return null; }