diff --git a/package-lock.json b/package-lock.json index e6898fe4..d9ebcff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "google-auth-library": "^10.1.0", - "heapdump": "^0.3.15", "helmet": "^7.1.0", "https-proxy-agent": "^7.0.2", "inquirer": "^8.2.6", @@ -5399,19 +5398,6 @@ "node": ">= 0.4" } }, - "node_modules/heapdump": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz", - "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "nan": "^2.13.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/helmet": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz", @@ -7027,12 +7013,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "license": "ISC" }, - "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/src/app.js b/src/app.js index d17ea295..7737e1ec 100644 --- a/src/app.js +++ b/src/app.js @@ -94,6 +94,15 @@ class Application { ) } + // 💰 启动回填:本周 Claude 周费用(用于 API Key 维度周限额) + try { + logger.info('💰 Backfilling current-week Claude weekly cost...') + const weeklyClaudeCostInitService = require('./services/weeklyClaudeCostInitService') + await weeklyClaudeCostInitService.backfillCurrentWeekClaudeCosts() + } catch (error) { + logger.warn('⚠️ Weekly Claude cost backfill failed (startup continues):', error.message) + } + // 🕐 初始化Claude账户会话窗口 logger.info('🕐 Initializing Claude account session windows...') const claudeAccountService = require('./services/claudeAccountService') diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 1883a2a0..76a0e371 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -9,6 +9,7 @@ const ClientValidator = require('../validators/clientValidator') const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator') const claudeRelayConfigService = require('../services/claudeRelayConfigService') const { calculateWaitTimeStats } = require('../utils/statsHelper') +const { isClaudeFamilyModel } = require('../utils/modelHelper') // 工具函数 function sleep(ms) { @@ -1239,20 +1240,20 @@ const authenticateApiKey = async (req, res, next) => { ) } - // 检查 Opus 周费用限制(仅对 Opus 模型生效) + // 检查 Claude 周费用限制 const weeklyOpusCostLimit = validation.keyData.weeklyOpusCostLimit || 0 if (weeklyOpusCostLimit > 0) { // 从请求中获取模型信息 const requestBody = req.body || {} const model = requestBody.model || '' - // 判断是否为 Opus 模型 - if (model && model.toLowerCase().includes('claude-opus')) { + // 判断是否为 Claude 模型 + if (isClaudeFamilyModel(model)) { const weeklyOpusCost = validation.keyData.weeklyOpusCost || 0 if (weeklyOpusCost >= weeklyOpusCostLimit) { logger.security( - `💰 Weekly Opus cost limit exceeded for key: ${validation.keyData.id} (${ + `💰 Weekly Claude cost limit exceeded for key: ${validation.keyData.id} (${ validation.keyData.name }), cost: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}` ) @@ -1266,17 +1267,17 @@ const authenticateApiKey = async (req, res, next) => { resetDate.setHours(0, 0, 0, 0) return res.status(429).json({ - error: 'Weekly Opus cost limit exceeded', - message: `已达到 Opus 模型周费用限制 ($${weeklyOpusCostLimit})`, + error: 'Weekly Claude cost limit exceeded', + message: `已达到 Claude 模型周费用限制 ($${weeklyOpusCostLimit})`, currentCost: weeklyOpusCost, costLimit: weeklyOpusCostLimit, resetAt: resetDate.toISOString() // 下周一重置 }) } - // 记录当前 Opus 费用使用情况 + // 记录当前 Claude 费用使用情况 logger.api( - `💰 Opus weekly cost usage for key: ${validation.keyData.id} (${ + `💰 Claude weekly cost usage for key: ${validation.keyData.id} (${ validation.keyData.name }), current: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}` ) diff --git a/src/models/redis.js b/src/models/redis.js index e69ba727..f524489c 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -1081,8 +1081,13 @@ class RedisClient { // 💰 获取本周 Opus 费用 async getWeeklyOpusCost(keyId) { const currentWeek = getWeekStringInTimezone() - const costKey = `usage:opus:weekly:${keyId}:${currentWeek}` - const cost = await this.client.get(costKey) + const costKey = `usage:claude:weekly:${keyId}:${currentWeek}` + let cost = await this.client.get(costKey) + // 向后兼容:如果新 key 不存在,则回退读取旧的(仅 Opus 口径)周费用 key。 + if (cost === null || cost === undefined) { + const legacyKey = `usage:opus:weekly:${keyId}:${currentWeek}` + cost = await this.client.get(legacyKey) + } const result = parseFloat(cost || 0) logger.debug( `💰 Getting weekly Opus cost for ${keyId}, week: ${currentWeek}, key: ${costKey}, value: ${cost}, result: ${result}` @@ -1090,11 +1095,12 @@ class RedisClient { return result } - // 💰 增加本周 Opus 费用 + // 💰 增加本周 Claude 费用 async incrementWeeklyOpusCost(keyId, amount) { const currentWeek = getWeekStringInTimezone() - const weeklyKey = `usage:opus:weekly:${keyId}:${currentWeek}` - const totalKey = `usage:opus:total:${keyId}` + // 注意:尽管函数名沿用旧的 Opus 命名,但当前实现统计的是 Claude 系列模型的“周费用”。 + const weeklyKey = `usage:claude:weekly:${keyId}:${currentWeek}` + const totalKey = `usage:claude:total:${keyId}` logger.debug( `💰 Incrementing weekly Opus cost for ${keyId}, week: ${currentWeek}, amount: $${amount}` @@ -1111,6 +1117,16 @@ class RedisClient { logger.debug(`💰 Opus cost incremented successfully, new weekly total: $${results[0][1]}`) } + // 💰 覆盖设置本周 Claude 费用(用于启动回填/迁移) + async setWeeklyClaudeCost(keyId, amount, weekString = null) { + const currentWeek = weekString || getWeekStringInTimezone() + const weeklyKey = `usage:claude:weekly:${keyId}:${currentWeek}` + + await this.client.set(weeklyKey, String(amount || 0)) + // 保留 2 周,足够覆盖“当前周 + 上周”查看/回填 + await this.client.expire(weeklyKey, 14 * 24 * 3600) + } + // 💰 计算账户的每日费用(基于模型使用) async getAccountDailyCost(accountId) { const CostCalculator = require('../utils/costCalculator') diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 771f973b..831da73a 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -3,6 +3,7 @@ const { v4: uuidv4 } = require('uuid') const config = require('../../config/config') const redis = require('../models/redis') const logger = require('../utils/logger') +const { isClaudeFamilyModel } = require('../utils/modelHelper') const ACCOUNT_TYPE_CONFIG = { claude: { prefix: 'claude:account:' }, @@ -1019,6 +1020,9 @@ class ApiKeyService { logger.database( `💰 Recorded cost for ${keyId}: $${costInfo.costs.total.toFixed(6)}, model: ${model}` ) + + // 记录 Claude 周费用(如果适用) + await this.recordClaudeWeeklyCost(keyId, costInfo.costs.total, model, null) } else { logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`) } @@ -1082,35 +1086,31 @@ class ApiKeyService { } } - // 📊 记录 Opus 模型费用(仅限 claude 和 claude-console 账户) - async recordOpusCost(keyId, cost, model, accountType) { + // 📊 记录 Claude 模型周费用(API Key 维度) + async recordClaudeWeeklyCost(keyId, cost, model, accountType) { try { - // 判断是否为 Opus 模型 - if (!model || !model.toLowerCase().includes('claude-opus')) { - return // 不是 Opus 模型,直接返回 + // 判断是否为 Claude 系列模型(包含 Bedrock 格式等) + if (!isClaudeFamilyModel(model)) { + return } - // 判断是否为 claude、claude-console 或 ccr 账户 - if ( - !accountType || - (accountType !== 'claude' && accountType !== 'claude-console' && accountType !== 'ccr') - ) { - logger.debug(`⚠️ Skipping Opus cost recording for non-Claude account type: ${accountType}`) - return // 不是 claude 账户,直接返回 - } - - // 记录 Opus 周费用 + // 记录 Claude 周费用 await redis.incrementWeeklyOpusCost(keyId, cost) logger.database( - `💰 Recorded Opus weekly cost for ${keyId}: $${cost.toFixed( + `💰 Recorded Claude weekly cost for ${keyId}: $${cost.toFixed( 6 - )}, model: ${model}, account type: ${accountType}` + )}, model: ${model}${accountType ? `, account type: ${accountType}` : ''}` ) } catch (error) { - logger.error('❌ Failed to record Opus cost:', error) + logger.error('❌ Failed to record Claude weekly cost:', error) } } + // 向后兼容:旧名字是 Opus-only 口径;现在周费用统计已扩展为 Claude 全模型口径。 + async recordOpusCost(keyId, cost, model, accountType) { + return this.recordClaudeWeeklyCost(keyId, cost, model, accountType) + } + // 📊 记录使用情况(新版本,支持详细的缓存类型) async recordUsageWithDetails( keyId, @@ -1210,8 +1210,8 @@ class ApiKeyService { `💰 Recorded cost for ${keyId}: $${costInfo.totalCost.toFixed(6)}, model: ${model}` ) - // 记录 Opus 周费用(如果适用) - await this.recordOpusCost(keyId, costInfo.totalCost, model, accountType) + // 记录 Claude 周费用(如果适用) + await this.recordClaudeWeeklyCost(keyId, costInfo.totalCost, model, accountType) // 记录详细的缓存费用(如果有) if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) { diff --git a/src/services/weeklyClaudeCostInitService.js b/src/services/weeklyClaudeCostInitService.js new file mode 100644 index 00000000..ca458ea5 --- /dev/null +++ b/src/services/weeklyClaudeCostInitService.js @@ -0,0 +1,219 @@ +const redis = require('../models/redis') +const logger = require('../utils/logger') +const pricingService = require('./pricingService') +const { isClaudeFamilyModel } = require('../utils/modelHelper') + +function pad2(n) { + return String(n).padStart(2, '0') +} + +// 生成配置时区下的 YYYY-MM-DD 字符串。 +// 注意:入参 date 必须是 redis.getDateInTimezone() 生成的“时区偏移后”的 Date。 +function formatTzDateYmd(tzDate) { + return `${tzDate.getUTCFullYear()}-${pad2(tzDate.getUTCMonth() + 1)}-${pad2(tzDate.getUTCDate())}` +} + +class WeeklyClaudeCostInitService { + _getCurrentWeekDatesInTimezone() { + const tzNow = redis.getDateInTimezone(new Date()) + const tzToday = new Date(tzNow) + tzToday.setUTCHours(0, 0, 0, 0) + + // ISO 周:周一=1 ... 周日=7 + const isoDay = tzToday.getUTCDay() || 7 + const tzMonday = new Date(tzToday) + tzMonday.setUTCDate(tzToday.getUTCDate() - (isoDay - 1)) + + const dates = [] + for (let d = new Date(tzMonday); d <= tzToday; d.setUTCDate(d.getUTCDate() + 1)) { + dates.push(formatTzDateYmd(d)) + } + return dates + } + + _buildWeeklyClaudeKey(keyId, weekString) { + return `usage:claude:weekly:${keyId}:${weekString}` + } + + /** + * 启动回填:把“本周(周一到今天)Claude 全模型”周费用从按日/按模型统计里反算出来, + * 写入 `usage:claude:weekly:*`,保证周限额在重启后不归零。 + * + * 说明: + * - 只回填本周,不做历史回填(符合“只要本周数据”诉求) + * - 会加分布式锁,避免多实例重复跑 + * - 会写 done 标记:同一周内重启默认不重复回填(需要时可手动删掉 done key) + */ + async backfillCurrentWeekClaudeCosts() { + const client = redis.getClientSafe() + if (!client) { + logger.warn('⚠️ 本周 Claude 周费用回填跳过:Redis client 不可用') + return { success: false, reason: 'redis_unavailable' } + } + + if (!pricingService || !pricingService.pricingData) { + logger.warn('⚠️ 本周 Claude 周费用回填跳过:pricing service 未初始化') + return { success: false, reason: 'pricing_uninitialized' } + } + + const weekString = redis.getWeekStringInTimezone() + const doneKey = `init:weekly_claude_cost:${weekString}:done` + + try { + const alreadyDone = await client.get(doneKey) + if (alreadyDone) { + logger.info(`ℹ️ 本周 Claude 周费用回填已完成(${weekString}),跳过`) + return { success: true, skipped: true } + } + } catch (e) { + // 尽力而为:读取失败不阻断启动回填流程。 + } + + const lockKey = `lock:init:weekly_claude_cost:${weekString}` + const lockValue = `${process.pid}:${Date.now()}` + const lockTtlMs = 15 * 60 * 1000 + + const lockAcquired = await redis.setAccountLock(lockKey, lockValue, lockTtlMs) + if (!lockAcquired) { + logger.info(`ℹ️ 本周 Claude 周费用回填已在运行(${weekString}),跳过`) + return { success: true, skipped: true, reason: 'locked' } + } + + const startedAt = Date.now() + try { + logger.info(`💰 开始回填本周 Claude 周费用:${weekString}(仅本周)...`) + + const keyIds = await redis.scanApiKeyIds() + const dates = this._getCurrentWeekDatesInTimezone() + + const costByKeyId = new Map() + let scannedKeys = 0 + let matchedClaudeKeys = 0 + + const toInt = (v) => { + const n = parseInt(v || '0', 10) + return Number.isFinite(n) ? n : 0 + } + + // 扫描“按日 + 按模型”的使用统计 key,并反算 Claude 系列模型的费用。 + for (const dateStr of dates) { + let cursor = '0' + const pattern = `usage:*:model:daily:*:${dateStr}` + + do { + const [nextCursor, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', 1000) + cursor = nextCursor + scannedKeys += keys.length + + const entries = [] + for (const usageKey of keys) { + // usage:{keyId}:model:daily:{model}:{YYYY-MM-DD} + const match = usageKey.match(/^usage:([^:]+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/) + if (!match) { + continue + } + const keyId = match[1] + const model = match[2] + if (!isClaudeFamilyModel(model)) { + continue + } + matchedClaudeKeys++ + entries.push({ usageKey, keyId, model }) + } + + if (entries.length === 0) { + continue + } + + const pipeline = client.pipeline() + for (const entry of entries) { + pipeline.hgetall(entry.usageKey) + } + const results = await pipeline.exec() + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i] + const [, data] = results[i] || [] + if (!data || Object.keys(data).length === 0) { + continue + } + + const inputTokens = toInt(data.totalInputTokens || data.inputTokens) + const outputTokens = toInt(data.totalOutputTokens || data.outputTokens) + const cacheReadTokens = toInt(data.totalCacheReadTokens || data.cacheReadTokens) + const cacheCreateTokens = toInt(data.totalCacheCreateTokens || data.cacheCreateTokens) + const ephemeral5mTokens = toInt(data.ephemeral5mTokens) + const ephemeral1hTokens = toInt(data.ephemeral1hTokens) + + const cacheCreationTotal = + ephemeral5mTokens > 0 || ephemeral1hTokens > 0 + ? ephemeral5mTokens + ephemeral1hTokens + : cacheCreateTokens + + const usage = { + input_tokens: inputTokens, + output_tokens: outputTokens, + cache_creation_input_tokens: cacheCreationTotal, + cache_read_input_tokens: cacheReadTokens + } + + if (ephemeral5mTokens > 0 || ephemeral1hTokens > 0) { + usage.cache_creation = { + ephemeral_5m_input_tokens: ephemeral5mTokens, + ephemeral_1h_input_tokens: ephemeral1hTokens + } + } + + const costInfo = pricingService.calculateCost(usage, entry.model) + const cost = costInfo && costInfo.totalCost ? costInfo.totalCost : 0 + if (cost <= 0) { + continue + } + + costByKeyId.set(entry.keyId, (costByKeyId.get(entry.keyId) || 0) + cost) + } + } while (cursor !== '0') + } + + // 为所有 API Key 写入本周 claude:weekly key,避免读取时回退到旧 opus:weekly 造成口径混淆。 + const ttlSeconds = 14 * 24 * 3600 + const batchSize = 500 + for (let i = 0; i < keyIds.length; i += batchSize) { + const batch = keyIds.slice(i, i + batchSize) + const pipeline = client.pipeline() + for (const keyId of batch) { + const weeklyKey = this._buildWeeklyClaudeKey(keyId, weekString) + const cost = costByKeyId.get(keyId) || 0 + pipeline.set(weeklyKey, String(cost)) + pipeline.expire(weeklyKey, ttlSeconds) + } + await pipeline.exec() + } + + // 写入 done 标记(保留略长于 1 周,避免同一周内重启重复回填)。 + await client.set(doneKey, new Date().toISOString(), 'EX', 10 * 24 * 3600) + + const durationMs = Date.now() - startedAt + logger.info( + `✅ 本周 Claude 周费用回填完成(${weekString}):keys=${keyIds.length}, scanned=${scannedKeys}, matchedClaude=${matchedClaudeKeys}, filled=${costByKeyId.size}(${durationMs}ms)` + ) + + return { + success: true, + weekString, + keyCount: keyIds.length, + scannedKeys, + matchedClaudeKeys, + filledKeys: costByKeyId.size, + durationMs + } + } catch (error) { + logger.error(`❌ 本周 Claude 周费用回填失败(${weekString}):`, error) + return { success: false, error: error.message } + } finally { + await redis.releaseAccountLock(lockKey, lockValue) + } + } +} + +module.exports = new WeeklyClaudeCostInitService() diff --git a/src/utils/modelHelper.js b/src/utils/modelHelper.js index 591c1974..c3fecc98 100644 --- a/src/utils/modelHelper.js +++ b/src/utils/modelHelper.js @@ -188,10 +188,54 @@ function isOpus45OrNewer(modelName) { return false } +/** + * 判断某个 model 名称是否属于 Anthropic Claude 系列模型。 + * + * 用于 API Key 维度的限额/统计(Claude 周费用)。这里刻意覆盖以下命名: + * - 标准 Anthropic 模型:claude-*,包括 claude-3-opus、claude-sonnet-*、claude-haiku-* 等 + * - Bedrock 模型:{region}.anthropic.claude-... / anthropic.claude-... + * - 少数情况下 model 字段可能只包含家族关键词(sonnet/haiku/opus),也视为 Claude 系列 + * + * 注意:会先去掉支持的 vendor 前缀(例如 "ccr,")。 + */ +function isClaudeFamilyModel(modelName) { + if (!modelName || typeof modelName !== 'string') { + return false + } + + const { baseModel } = parseVendorPrefixedModel(modelName) + const m = (baseModel || '').trim().toLowerCase() + if (!m) { + return false + } + + // Bedrock 模型格式 + if ( + m.includes('.anthropic.claude-') || + m.startsWith('anthropic.claude-') || + m.includes('.claude-') + ) { + return true + } + + // 标准 Anthropic 模型 ID + if (m.startsWith('claude-') || m.includes('claude-')) { + return true + } + + // 兜底:某些下游链路里 model 字段可能不带 "claude-" 前缀,但仍包含家族关键词。 + if (m.includes('opus') || m.includes('sonnet') || m.includes('haiku')) { + return true + } + + return false +} + module.exports = { parseVendorPrefixedModel, hasVendorPrefix, getEffectiveModel, getVendorType, - isOpus45OrNewer + isOpus45OrNewer, + isClaudeFamilyModel } diff --git a/web/admin-spa/package-lock.json b/web/admin-spa/package-lock.json index 9405609e..481df56a 100644 --- a/web/admin-spa/package-lock.json +++ b/web/admin-spa/package-lock.json @@ -1157,7 +1157,6 @@ "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/lodash": "*" } @@ -1352,7 +1351,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1589,7 +1587,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3063,15 +3060,13 @@ "version": "4.17.21", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -3623,7 +3618,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3770,7 +3764,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4035,7 +4028,6 @@ "integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -4533,7 +4525,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4924,7 +4915,6 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5125,7 +5115,6 @@ "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz", "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.18", "@vue/compiler-sfc": "3.5.18", diff --git a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue index 39eff929..9aac870c 100644 --- a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue @@ -232,10 +232,10 @@ /> - +

- 设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户 + 设置 Claude 模型的周费用限制(周一到周日),仅对 Claude 模型请求生效

@@ -510,7 +510,7 @@ const form = reactive({ concurrencyLimit: '', dailyCostLimit: '', totalCostLimit: '', - weeklyOpusCostLimit: '', // 新增Opus周费用限制 + weeklyOpusCostLimit: '', // 新增Claude周费用限制 permissions: '', // 空字符串表示不修改 claudeAccountId: '', geminiAccountId: '', diff --git a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue index bd464e16..cf447c07 100644 --- a/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue @@ -386,7 +386,7 @@
Claude 模型周费用限制 (美元)
@@ -428,7 +428,8 @@ type="number" />

- 设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制 + 设置 Claude 模型的周费用限制(周一到周日),仅对 Claude 模型请求生效,0 + 或留空表示无限制

diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index 0b68528b..66f6b8b0 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -324,7 +324,7 @@
Claude 模型周费用限制 (美元)
@@ -366,7 +366,8 @@ type="number" />

- 设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制 + 设置 Claude 模型的周费用限制(周一到周日),仅对 Claude 模型请求生效,0 + 或留空表示无限制

diff --git a/web/admin-spa/src/components/apistats/LimitConfig.vue b/web/admin-spa/src/components/apistats/LimitConfig.vue index 4b71907a..d0fd2e35 100644 --- a/web/admin-spa/src/components/apistats/LimitConfig.vue +++ b/web/admin-spa/src/components/apistats/LimitConfig.vue @@ -167,11 +167,11 @@
- +
Opus 模型周费用限制Claude 模型周费用限制 ${{ statsData.limits.weeklyOpusCost.toFixed(4) }} / ${{ @@ -383,7 +383,7 @@ const getTotalCostProgressColor = () => { return 'bg-blue-500' } -// 获取Opus周费用进度 +// 获取Claude周费用进度 const getOpusWeeklyCostProgress = () => { if ( !statsData.value.limits.weeklyOpusCostLimit || @@ -395,7 +395,7 @@ const getOpusWeeklyCostProgress = () => { return Math.min(percentage, 100) } -// 获取Opus周费用进度条颜色 +// 获取Claude周费用进度条颜色 const getOpusWeeklyCostProgressColor = () => { const progress = getOpusWeeklyCostProgress() if (progress >= 100) return 'bg-red-500'