From 4adc8d96951f74bbc3e9dde7bf2f5aad0519b78b Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 8 Aug 2025 11:56:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=B9=B6=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E4=BF=AE=E6=94=B9=EF=BC=9A=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E6=9C=8D=E5=8A=A1=E8=B4=A6=E6=88=B7=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E3=80=81WindowCountdown=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=AD=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 7 - src/routes/admin.js | 158 ++++++++++++++++-- src/services/apiKeyService.js | 33 ++++ .../src/components/accounts/AccountForm.vue | 12 +- .../components/apikeys/UsageDetailModal.vue | 88 ++-------- web/admin-spa/src/stores/dashboard.js | 29 +++- web/admin-spa/src/views/ApiKeysView.vue | 137 +++------------ web/admin-spa/src/views/DashboardView.vue | 79 ++++++++- 8 files changed, 329 insertions(+), 214 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fc293375..b9239d68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,13 +63,6 @@ npm run service:status # 查看服务状态 npm run service:logs # 查看日志 npm run service:stop # 停止服务 -# CLI管理工具 -npm run cli admin # 管理员操作 -npm run cli keys # API Key管理 -npm run cli accounts # Claude账户管理 -npm run cli status # 系统状态 -``` - ### 开发环境配置 必须配置的环境变量: - `JWT_SECRET`: JWT密钥(32字符以上随机字符串) diff --git a/src/routes/admin.js b/src/routes/admin.js index f330f633..892b67ce 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -2300,6 +2300,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { claudeAccounts, claudeConsoleAccounts, geminiAccounts, + bedrockAccountsResult, todayStats, systemAverages, realtimeMetrics @@ -2309,11 +2310,15 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { claudeAccountService.getAllAccounts(), claudeConsoleAccountService.getAllAccounts(), geminiAccountService.getAllAccounts(), + bedrockAccountService.getAllAccounts(), redis.getTodayStats(), redis.getSystemAverages(), redis.getRealtimeSystemMetrics() ]) + // 处理Bedrock账户数据 + const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : [] + // 计算使用统计(统一使用allTokens) const totalTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.allTokens || 0), @@ -2345,34 +2350,167 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { ) const activeApiKeys = apiKeys.filter((key) => key.isActive).length - const activeClaudeAccounts = claudeAccounts.filter( - (acc) => acc.isActive && acc.status === 'active' + + // Claude账户统计 - 根据账户管理页面的判断逻辑 + const normalClaudeAccounts = claudeAccounts.filter( + (acc) => + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' && + acc.schedulable !== false + ).length + const abnormalClaudeAccounts = claudeAccounts.filter( + (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' + ).length + const pausedClaudeAccounts = claudeAccounts.filter( + (acc) => + acc.schedulable === false && + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' ).length const rateLimitedClaudeAccounts = claudeAccounts.filter( (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited ).length - const activeClaudeConsoleAccounts = claudeConsoleAccounts.filter( - (acc) => acc.isActive && acc.status === 'active' + + // Claude Console账户统计 + const normalClaudeConsoleAccounts = claudeConsoleAccounts.filter( + (acc) => + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' && + acc.schedulable !== false + ).length + const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter( + (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' + ).length + const pausedClaudeConsoleAccounts = claudeConsoleAccounts.filter( + (acc) => + acc.schedulable === false && + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' ).length const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter( (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited ).length - const activeGeminiAccounts = geminiAccounts.filter( - (acc) => acc.isActive && acc.status === 'active' + + // Gemini账户统计 + const normalGeminiAccounts = geminiAccounts.filter( + (acc) => + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' && + acc.schedulable !== false + ).length + const abnormalGeminiAccounts = geminiAccounts.filter( + (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' + ).length + const pausedGeminiAccounts = geminiAccounts.filter( + (acc) => + acc.schedulable === false && + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' ).length const rateLimitedGeminiAccounts = geminiAccounts.filter( (acc) => acc.rateLimitStatus === 'limited' ).length + // Bedrock账户统计 + const normalBedrockAccounts = bedrockAccounts.filter( + (acc) => + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' && + acc.schedulable !== false + ).length + const abnormalBedrockAccounts = bedrockAccounts.filter( + (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' + ).length + const pausedBedrockAccounts = bedrockAccounts.filter( + (acc) => + acc.schedulable === false && + acc.isActive && + acc.status !== 'blocked' && + acc.status !== 'unauthorized' + ).length + const rateLimitedBedrockAccounts = bedrockAccounts.filter( + (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited + ).length + const dashboard = { overview: { totalApiKeys: apiKeys.length, activeApiKeys, + // 总账户统计(所有平台) + totalAccounts: + claudeAccounts.length + + claudeConsoleAccounts.length + + geminiAccounts.length + + bedrockAccounts.length, + normalAccounts: + normalClaudeAccounts + + normalClaudeConsoleAccounts + + normalGeminiAccounts + + normalBedrockAccounts, + abnormalAccounts: + abnormalClaudeAccounts + + abnormalClaudeConsoleAccounts + + abnormalGeminiAccounts + + abnormalBedrockAccounts, + pausedAccounts: + pausedClaudeAccounts + + pausedClaudeConsoleAccounts + + pausedGeminiAccounts + + pausedBedrockAccounts, + rateLimitedAccounts: + rateLimitedClaudeAccounts + + rateLimitedClaudeConsoleAccounts + + rateLimitedGeminiAccounts + + rateLimitedBedrockAccounts, + // 各平台详细统计 + accountsByPlatform: { + claude: { + total: claudeAccounts.length, + normal: normalClaudeAccounts, + abnormal: abnormalClaudeAccounts, + paused: pausedClaudeAccounts, + rateLimited: rateLimitedClaudeAccounts + }, + 'claude-console': { + total: claudeConsoleAccounts.length, + normal: normalClaudeConsoleAccounts, + abnormal: abnormalClaudeConsoleAccounts, + paused: pausedClaudeConsoleAccounts, + rateLimited: rateLimitedClaudeConsoleAccounts + }, + gemini: { + total: geminiAccounts.length, + normal: normalGeminiAccounts, + abnormal: abnormalGeminiAccounts, + paused: pausedGeminiAccounts, + rateLimited: rateLimitedGeminiAccounts + }, + bedrock: { + total: bedrockAccounts.length, + normal: normalBedrockAccounts, + abnormal: abnormalBedrockAccounts, + paused: pausedBedrockAccounts, + rateLimited: rateLimitedBedrockAccounts + } + }, + // 保留旧字段以兼容 + activeAccounts: + normalClaudeAccounts + + normalClaudeConsoleAccounts + + normalGeminiAccounts + + normalBedrockAccounts, totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length, - activeClaudeAccounts: activeClaudeAccounts + activeClaudeConsoleAccounts, + activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts, rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts, totalGeminiAccounts: geminiAccounts.length, - activeGeminiAccounts, + activeGeminiAccounts: normalGeminiAccounts, rateLimitedGeminiAccounts, totalTokensUsed, totalRequestsUsed, @@ -2403,8 +2541,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { }, systemHealth: { redisConnected: redis.isConnected, - claudeAccountsHealthy: activeClaudeAccounts + activeClaudeConsoleAccounts > 0, - geminiAccountsHealthy: activeGeminiAccounts > 0, + claudeAccountsHealthy: normalClaudeAccounts + normalClaudeConsoleAccounts > 0, + geminiAccountsHealthy: normalGeminiAccounts > 0, uptime: process.uptime() }, systemTimezone: config.system.timezoneOffset || 8 diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 2172aac7..750990d4 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -213,12 +213,45 @@ class ApiKeyService { if (key.rateLimitWindow > 0) { const requestCountKey = `rate_limit:requests:${key.id}` const tokenCountKey = `rate_limit:tokens:${key.id}` + const windowStartKey = `rate_limit:window_start:${key.id}` key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0') key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0') + + // 获取窗口开始时间和计算剩余时间 + const windowStart = await client.get(windowStartKey) + if (windowStart) { + const now = Date.now() + const windowStartTime = parseInt(windowStart) + const windowDuration = key.rateLimitWindow * 60 * 1000 // 转换为毫秒 + const windowEndTime = windowStartTime + windowDuration + + // 如果窗口还有效 + if (now < windowEndTime) { + key.windowStartTime = windowStartTime + key.windowEndTime = windowEndTime + key.windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000)) + } else { + // 窗口已过期,下次请求会重置 + key.windowStartTime = null + key.windowEndTime = null + key.windowRemainingSeconds = 0 + // 重置计数为0,因为窗口已过期 + key.currentWindowRequests = 0 + key.currentWindowTokens = 0 + } + } else { + // 窗口还未开始(没有任何请求) + key.windowStartTime = null + key.windowEndTime = null + key.windowRemainingSeconds = null + } } else { key.currentWindowRequests = 0 key.currentWindowTokens = 0 + key.windowStartTime = null + key.windowEndTime = null + key.windowRemainingSeconds = null } try { diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index c2f12dd5..a4b82369 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -1828,15 +1828,15 @@ const handleGroupRefresh = async () => { // 监听平台变化,重置表单 watch( () => form.value.platform, - (newPlatform, oldPlatform) => { + (newPlatform) => { // 处理添加方式的自动切换 if (newPlatform === 'claude-console' || newPlatform === 'bedrock') { form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式 - } else if ( - oldPlatform === 'claude-console' && - (newPlatform === 'claude' || newPlatform === 'gemini') - ) { - // 从 Claude Console 切换到其他平台时,恢复为 OAuth + } else if (newPlatform === 'claude') { + // 切换到 Claude 时,使用 Setup Token 作为默认方式 + form.value.addType = 'setup-token' + } else if (newPlatform === 'gemini') { + // 切换到 Gemini 时,使用 OAuth 作为默认方式 form.value.addType = 'oauth' } diff --git a/web/admin-spa/src/components/apikeys/UsageDetailModal.vue b/web/admin-spa/src/components/apikeys/UsageDetailModal.vue index 27e2f807..c443dc9a 100644 --- a/web/admin-spa/src/components/apikeys/UsageDetailModal.vue +++ b/web/admin-spa/src/components/apikeys/UsageDetailModal.vue @@ -183,47 +183,23 @@
-
- 时间窗口 - - {{ apiKey.rateLimitWindow }} 分钟 - -
- - -
-
- 请求限制 - - {{ apiKey.currentWindowRequests || 0 }} / {{ apiKey.rateLimitRequests }} - -
-
-
-
-
- - -
-
- Token限制 - - {{ formatTokenCount(apiKey.currentWindowTokens || 0) }} / - {{ formatTokenCount(apiKey.tokenLimit) }} - -
-
-
-
-
+
+ + 时间窗口限制 +
+
@@ -242,6 +218,7 @@