From 4e3c826b6cb59a8985915fac3e38bc0ce20f8d05 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 4 Aug 2025 10:54:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DAPI=20Keys?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=97=B6=E9=97=B4=E7=AA=97=E5=8F=A3=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E5=92=8CToken=E6=95=B0=E5=80=BC=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复时间窗口限制的请求次数和Token使用量进度条不更新的问题 - 在apiKeyService.getAllApiKeys()中添加获取当前窗口统计数据的逻辑 - 从Redis读取rate_limit:requests和rate_limit:tokens键的值 - 优化Token数值展示,添加K/M单位格式化 - UsageDetailModal组件中5处Token数值改用formatTokenCount - ApiKeysView模型统计中5处Token数值改用formatTokenCount - 统一使用K/M单位简化大数字显示 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/services/apiKeyService.js | 14 ++++++++++++++ .../src/components/apikeys/UsageDetailModal.vue | 12 ++++++------ web/admin-spa/src/views/ApiKeysView.vue | 12 ++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 8656c5e9..930fccc8 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -192,6 +192,7 @@ class ApiKeyService { async getAllApiKeys() { try { const apiKeys = await redis.getAllApiKeys(); + const client = redis.getClientSafe(); // 为每个key添加使用统计和当前并发数 for (const key of apiKeys) { @@ -207,6 +208,19 @@ class ApiKeyService { key.permissions = key.permissions || 'all'; // 兼容旧数据 key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0); key.dailyCost = await redis.getDailyCost(key.id) || 0; + + // 获取当前时间窗口的请求次数和Token使用量 + if (key.rateLimitWindow > 0) { + const requestCountKey = `rate_limit:requests:${key.id}`; + const tokenCountKey = `rate_limit:tokens:${key.id}`; + + key.currentWindowRequests = parseInt(await client.get(requestCountKey) || '0'); + key.currentWindowTokens = parseInt(await client.get(tokenCountKey) || '0'); + } else { + key.currentWindowRequests = 0; + key.currentWindowTokens = 0; + } + try { key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : []; } catch (e) { diff --git a/web/admin-spa/src/components/apikeys/UsageDetailModal.vue b/web/admin-spa/src/components/apikeys/UsageDetailModal.vue index 749f2550..f9c4b64d 100644 --- a/web/admin-spa/src/components/apikeys/UsageDetailModal.vue +++ b/web/admin-spa/src/components/apikeys/UsageDetailModal.vue @@ -46,10 +46,10 @@
- {{ formatNumber(totalTokens) }} + {{ formatTokenCount(totalTokens) }}
- 今日: {{ formatNumber(dailyTokens) }} + 今日: {{ formatTokenCount(dailyTokens) }}
@@ -99,7 +99,7 @@ 输入 Token - {{ formatNumber(inputTokens) }} + {{ formatTokenCount(inputTokens) }}
@@ -108,7 +108,7 @@ 输出 Token
- {{ formatNumber(outputTokens) }} + {{ formatTokenCount(outputTokens) }}
缓存创建 Token
- {{ formatNumber(cacheCreateTokens) }} + {{ formatTokenCount(cacheCreateTokens) }}
缓存读取 Token
- {{ formatNumber(cacheReadTokens) }} + {{ formatTokenCount(cacheReadTokens) }} diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index 97e2accb..217180bd 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -546,7 +546,7 @@ 总Token: - {{ formatNumber(stat.allTokens) }} + {{ formatTokenCount(stat.allTokens) }}
@@ -561,14 +561,14 @@ 输入: - {{ formatNumber(stat.inputTokens) }} + {{ formatTokenCount(stat.inputTokens) }}
输出: - {{ formatNumber(stat.outputTokens) }} + {{ formatTokenCount(stat.outputTokens) }}
缓存创建: - {{ formatNumber(stat.cacheCreateTokens) }} + {{ formatTokenCount(stat.cacheCreateTokens) }}
缓存读取: - {{ formatNumber(stat.cacheReadTokens) }} + {{ formatTokenCount(stat.cacheReadTokens) }}
@@ -623,7 +623,7 @@ 总请求: {{ apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.requests, 0) }} - 总Token: {{ formatNumber(apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.allTokens, 0)) }} + 总Token: {{ formatTokenCount(apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.allTokens, 0)) }} From fce6d8e1aca425b0cb702cc375c897ebd2d20ed0 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 4 Aug 2025 11:58:26 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E5=92=8CAPI=E7=BB=9F=E8=AE=A1=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E5=A4=9A=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复仪表盘天粒度下7天/30天快捷选择无数据的问题 - 修复API Keys页面统计按钮链接路由错误(admin -> admin-next) - 改进统计页面限制展示,使用3个进度条更直观显示使用情况 - 后端API响应增加当前使用量数据(currentWindowRequests/Tokens/DailyCost) - 修复教程页面window.location.origin为空的兼容性问题 - 无限制时使用无穷符号(∞)展示,提升用户体验 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/apiStats.js | 30 ++- .../src/components/apistats/LimitConfig.vue | 220 +++++++++++++----- web/admin-spa/src/stores/dashboard.js | 22 ++ web/admin-spa/src/views/ApiKeysView.vue | 2 +- web/admin-spa/src/views/TutorialView.vue | 37 ++- 5 files changed, 253 insertions(+), 58 deletions(-) diff --git a/src/routes/apiStats.js b/src/routes/apiStats.js index e4db6f4f..1241aad6 100644 --- a/src/routes/apiStats.js +++ b/src/routes/apiStats.js @@ -269,6 +269,28 @@ router.post('/api/user-stats', async (req, res) => { } } + // 获取当前使用量 + let currentWindowRequests = 0; + let currentWindowTokens = 0; + let currentDailyCost = 0; + + try { + // 获取当前时间窗口的请求次数和Token使用量 + if (fullKeyData.rateLimitWindow > 0) { + const client = redis.getClientSafe(); + const requestCountKey = `rate_limit:requests:${keyId}`; + const tokenCountKey = `rate_limit:tokens:${keyId}`; + + currentWindowRequests = parseInt(await client.get(requestCountKey) || '0'); + currentWindowTokens = parseInt(await client.get(tokenCountKey) || '0'); + } + + // 获取当日费用 + currentDailyCost = await redis.getDailyCost(keyId) || 0; + } catch (error) { + logger.warn(`Failed to get current usage for key ${keyId}:`, error); + } + // 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息) const responseData = { id: keyId, @@ -296,13 +318,17 @@ router.post('/api/user-stats', async (req, res) => { } }, - // 限制信息(只显示配置,不显示当前使用量) + // 限制信息(显示配置和当前使用量) limits: { tokenLimit: fullKeyData.tokenLimit || 0, concurrencyLimit: fullKeyData.concurrencyLimit || 0, rateLimitWindow: fullKeyData.rateLimitWindow || 0, rateLimitRequests: fullKeyData.rateLimitRequests || 0, - dailyCostLimit: fullKeyData.dailyCostLimit || 0 + dailyCostLimit: fullKeyData.dailyCostLimit || 0, + // 当前使用量 + currentWindowRequests: currentWindowRequests, + currentWindowTokens: currentWindowTokens, + currentDailyCost: currentDailyCost }, // 绑定的账户信息(只显示ID,不显示敏感信息) diff --git a/web/admin-spa/src/components/apistats/LimitConfig.vue b/web/admin-spa/src/components/apistats/LimitConfig.vue index f554359a..96371a29 100644 --- a/web/admin-spa/src/components/apistats/LimitConfig.vue +++ b/web/admin-spa/src/components/apistats/LimitConfig.vue @@ -6,64 +6,131 @@ 限制配置 -
-
- Token 限制 - {{ statsData.limits.tokenLimit > 0 ? formatNumber(statsData.limits.tokenLimit) : '无限制' }} -
-
- 并发限制 - {{ statsData.limits.concurrencyLimit > 0 ? statsData.limits.concurrencyLimit : '无限制' }} -
-
- 速率限制 - - {{ statsData.limits.rateLimitRequests > 0 && statsData.limits.rateLimitWindow > 0 - ? `${statsData.limits.rateLimitRequests}次/${statsData.limits.rateLimitWindow}分钟` - : '无限制' }} - -
-
- 每日费用限制 - {{ statsData.limits.dailyCostLimit > 0 ? '$' + statsData.limits.dailyCostLimit : '无限制' }} -
-
- 模型限制 - - - - 限制 {{ statsData.restrictions.restrictedModels.length }} 个模型 +
+ +
+
+ 每日费用限制 + + + ${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{ statsData.limits.dailyCostLimit.toFixed(2) }} + + + ${{ statsData.limits.currentDailyCost.toFixed(4) }} / + - - - 允许所有模型 - - +
+
+
+
+
+
+
-
- 客户端限制 - - - - 限制 {{ statsData.restrictions.allowedClients.length }} 个客户端 + + +
+
+ + 时间窗口限制 ({{ statsData.limits.rateLimitWindow }}分钟) - - - 允许所有客户端 +
+ + +
+
+ 请求次数 + + {{ formatNumber(statsData.limits.currentWindowRequests) }} / {{ formatNumber(statsData.limits.rateLimitRequests) }} + +
+
+
+
+
+ + +
+
+ Token 使用量 + + {{ formatNumber(statsData.limits.currentWindowTokens) }} / {{ formatNumber(statsData.limits.tokenLimit) }} + +
+
+
+
+
+ +
+ + 请求次数和Token使用量为"或"的关系,任一达到限制即触发限流 +
+
+ + +
+
+ 并发限制 + + + {{ statsData.limits.concurrencyLimit }} + + + + - +
+
+ 模型限制 + + + + 限制 {{ statsData.restrictions.restrictedModels.length }} 个模型 + + + + 允许所有模型 + + +
+
+ 客户端限制 + + + + 限制 {{ statsData.restrictions.allowedClients.length }} 个客户端 + + + + 允许所有客户端 + + +
@@ -158,6 +225,51 @@ const formatNumber = (num) => { return num.toLocaleString() } } + +// 获取每日费用进度 +const getDailyCostProgress = () => { + if (!statsData.value.limits.dailyCostLimit || statsData.value.limits.dailyCostLimit === 0) return 0 + const percentage = (statsData.value.limits.currentDailyCost / statsData.value.limits.dailyCostLimit) * 100 + return Math.min(percentage, 100) +} + +// 获取每日费用进度条颜色 +const getDailyCostProgressColor = () => { + const progress = getDailyCostProgress() + if (progress >= 100) return 'bg-red-500' + if (progress >= 80) return 'bg-yellow-500' + return 'bg-green-500' +} + +// 获取窗口请求进度 +const getWindowRequestProgress = () => { + if (!statsData.value.limits.rateLimitRequests || statsData.value.limits.rateLimitRequests === 0) return 0 + const percentage = (statsData.value.limits.currentWindowRequests / statsData.value.limits.rateLimitRequests) * 100 + return Math.min(percentage, 100) +} + +// 获取窗口请求进度条颜色 +const getWindowRequestProgressColor = () => { + const progress = getWindowRequestProgress() + if (progress >= 100) return 'bg-red-500' + if (progress >= 80) return 'bg-yellow-500' + return 'bg-blue-500' +} + +// 获取窗口Token进度 +const getWindowTokenProgress = () => { + if (!statsData.value.limits.tokenLimit || statsData.value.limits.tokenLimit === 0) return 0 + const percentage = (statsData.value.limits.currentWindowTokens / statsData.value.limits.tokenLimit) * 100 + return Math.min(percentage, 100) +} + +// 获取窗口Token进度条颜色 +const getWindowTokenProgressColor = () => { + const progress = getWindowTokenProgress() + if (progress >= 100) return 'bg-red-500' + if (progress >= 80) return 'bg-yellow-500' + return 'bg-purple-500' +}