From 4bcd2878f2025fcd8649c524fab72e27144beb87 Mon Sep 17 00:00:00 2001 From: mouyong Date: Sun, 10 Aug 2025 12:38:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=92=8C=E7=BC=93=E5=AD=98=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加平台筛选功能到账户管理页面 * 后端:在所有账户接口中支持platform和groupId查询参数 * 前端:添加平台筛选下拉框,支持条件性API请求 - 使用智能缓存机制优化数据加载 * 缓存API Keys、账户分组和分组成员数据 * 通过Ctrl/⌘+点击刷新按钮实现强制重新加载 * 在数据变更时自动清除相关缓存(创建/编辑/删除) - 改进Gemini账户限流状态显示 * 在geminiAccountService中添加限流信息支持 * 统一所有平台的限流状态格式 * 修复仪表板统计,排除被限流的账户 - 提升用户界面体验 * 将原生title提示替换为Element Plus的el-tooltip组件 * 支持跨平台键盘快捷键(Ctrl/⌘+点击) * ESLint规范合规和代码格式化改进 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/admin.js | 109 ++++++++- src/services/geminiAccountService.js | 57 ++++- web/admin-spa/src/views/AccountsView.vue | 298 +++++++++++++++++------ 3 files changed, 375 insertions(+), 89 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 7db8efb9..676f419a 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1146,7 +1146,27 @@ router.post('/claude-accounts/exchange-setup-token-code', authenticateAdmin, asy // 获取所有Claude账户 router.get('/claude-accounts', authenticateAdmin, async (req, res) => { try { - const accounts = await claudeAccountService.getAllAccounts() + const { platform, groupId } = req.query + let accounts = await claudeAccountService.getAllAccounts() + + // 根据查询参数进行筛选 + if (platform && platform !== 'all' && platform !== 'claude') { + // 如果指定了其他平台,返回空数组 + accounts = [] + } + + // 如果指定了分组筛选 + if (groupId && groupId !== 'all') { + if (groupId === 'ungrouped') { + // 筛选未分组账户 + accounts = accounts.filter((account) => !account.groupInfo) + } else { + // 筛选特定分组的账户 + accounts = accounts.filter( + (account) => account.groupInfo && account.groupInfo.id === groupId + ) + } + } // 为每个账户添加使用统计信息 const accountsWithStats = await Promise.all( @@ -1403,7 +1423,27 @@ router.put( // 获取所有Claude Console账户 router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => { try { - const accounts = await claudeConsoleAccountService.getAllAccounts() + const { platform, groupId } = req.query + let accounts = await claudeConsoleAccountService.getAllAccounts() + + // 根据查询参数进行筛选 + if (platform && platform !== 'all' && platform !== 'claude-console') { + // 如果指定了其他平台,返回空数组 + accounts = [] + } + + // 如果指定了分组筛选 + if (groupId && groupId !== 'all') { + if (groupId === 'ungrouped') { + // 筛选未分组账户 + accounts = accounts.filter((account) => !account.groupInfo) + } else { + // 筛选特定分组的账户 + accounts = accounts.filter( + (account) => account.groupInfo && account.groupInfo.id === groupId + ) + } + } // 为每个账户添加使用统计信息 const accountsWithStats = await Promise.all( @@ -1652,6 +1692,7 @@ router.put( // 获取所有Bedrock账户 router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { try { + const { platform, groupId } = req.query const result = await bedrockAccountService.getAllAccounts() if (!result.success) { return res @@ -1659,9 +1700,30 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { .json({ error: 'Failed to get Bedrock accounts', message: result.error }) } + let accounts = result.data + + // 根据查询参数进行筛选 + if (platform && platform !== 'all' && platform !== 'bedrock') { + // 如果指定了其他平台,返回空数组 + accounts = [] + } + + // 如果指定了分组筛选 + if (groupId && groupId !== 'all') { + if (groupId === 'ungrouped') { + // 筛选未分组账户 + accounts = accounts.filter((account) => !account.groupInfo) + } else { + // 筛选特定分组的账户 + accounts = accounts.filter( + (account) => account.groupInfo && account.groupInfo.id === groupId + ) + } + } + // 为每个账户添加使用统计信息 const accountsWithStats = await Promise.all( - result.data.map(async (account) => { + accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) return { @@ -2027,7 +2089,27 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res // 获取所有 Gemini 账户 router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { try { - const accounts = await geminiAccountService.getAllAccounts() + const { platform, groupId } = req.query + let accounts = await geminiAccountService.getAllAccounts() + + // 根据查询参数进行筛选 + if (platform && platform !== 'all' && platform !== 'gemini') { + // 如果指定了其他平台,返回空数组 + accounts = [] + } + + // 如果指定了分组筛选 + if (groupId && groupId !== 'all') { + if (groupId === 'ungrouped') { + // 筛选未分组账户 + accounts = accounts.filter((account) => !account.groupInfo) + } else { + // 筛选特定分组的账户 + accounts = accounts.filter( + (account) => account.groupInfo && account.groupInfo.id === groupId + ) + } + } // 为每个账户添加使用统计信息(与Claude账户相同的逻辑) const accountsWithStats = await Promise.all( @@ -2368,7 +2450,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && - acc.schedulable !== false + acc.schedulable !== false && + !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalClaudeAccounts = claudeAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' @@ -2390,7 +2473,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && - acc.schedulable !== false + acc.schedulable !== false && + !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' @@ -2412,7 +2496,11 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && - acc.schedulable !== false + acc.schedulable !== false && + !( + acc.rateLimitStatus === 'limited' || + (acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) + ) ).length const abnormalGeminiAccounts = geminiAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' @@ -2425,7 +2513,9 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { acc.status !== 'unauthorized' ).length const rateLimitedGeminiAccounts = geminiAccounts.filter( - (acc) => acc.rateLimitStatus === 'limited' + (acc) => + acc.rateLimitStatus === 'limited' || + (acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length // Bedrock账户统计 @@ -2434,7 +2524,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && - acc.schedulable !== false + acc.schedulable !== false && + !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalBedrockAccounts = bedrockAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index ad6a30b3..d800fb3d 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -504,6 +504,9 @@ async function getAllAccounts() { for (const key of keys) { const accountData = await client.hgetall(key) if (accountData && Object.keys(accountData).length > 0) { + // 获取限流状态信息 + const rateLimitInfo = await getAccountRateLimitInfo(accountData.id) + // 解析代理配置 if (accountData.proxy) { try { @@ -519,7 +522,19 @@ async function getAllAccounts() { ...accountData, geminiOauth: accountData.geminiOauth ? '[ENCRYPTED]' : '', accessToken: accountData.accessToken ? '[ENCRYPTED]' : '', - refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '' + refreshToken: accountData.refreshToken ? '[ENCRYPTED]' : '', + // 添加限流状态信息(统一格式) + rateLimitStatus: rateLimitInfo + ? { + isRateLimited: rateLimitInfo.isRateLimited, + rateLimitedAt: rateLimitInfo.rateLimitedAt, + minutesRemaining: rateLimitInfo.minutesRemaining + } + : { + isRateLimited: false, + rateLimitedAt: null, + minutesRemaining: 0 + } }) } } @@ -774,6 +789,45 @@ async function setAccountRateLimited(accountId, isLimited = true) { await updateAccount(accountId, updates) } +// 获取账户的限流信息(参考 claudeAccountService 的实现) +async function getAccountRateLimitInfo(accountId) { + try { + const account = await getAccount(accountId) + if (!account) { + return null + } + + if (account.rateLimitStatus === 'limited' && account.rateLimitedAt) { + const rateLimitedAt = new Date(account.rateLimitedAt) + const now = new Date() + const minutesSinceRateLimit = Math.floor((now - rateLimitedAt) / (1000 * 60)) + + // Gemini 限流持续时间为 1 小时 + const minutesRemaining = Math.max(0, 60 - minutesSinceRateLimit) + const rateLimitEndAt = new Date(rateLimitedAt.getTime() + 60 * 60 * 1000).toISOString() + + return { + isRateLimited: minutesRemaining > 0, + rateLimitedAt: account.rateLimitedAt, + minutesSinceRateLimit, + minutesRemaining, + rateLimitEndAt + } + } + + return { + isRateLimited: false, + rateLimitedAt: null, + minutesSinceRateLimit: 0, + minutesRemaining: 0, + rateLimitEndAt: null + } + } catch (error) { + logger.error(`❌ Failed to get rate limit info for Gemini account: ${accountId}`, error) + return null + } +} + // 获取配置的OAuth客户端 - 参考GeminiCliSimulator的getOauthClient方法 async function getOauthClient(accessToken, refreshToken) { const client = new OAuth2Client({ @@ -1137,6 +1191,7 @@ module.exports = { refreshAccountToken, markAccountUsed, setAccountRateLimited, + getAccountRateLimitInfo, isTokenExpired, getOauthClient, loadCodeAssist, diff --git a/web/admin-spa/src/views/AccountsView.vue b/web/admin-spa/src/views/AccountsView.vue index 1234a53d..0c7a7444 100644 --- a/web/admin-spa/src/views/AccountsView.vue +++ b/web/admin-spa/src/views/AccountsView.vue @@ -24,6 +24,21 @@ /> + +
+
+ +
+
- +
+ + + +
@@ -307,11 +332,22 @@ }} - 限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟) + 限流中 + ({{ account.rateLimitStatus.minutesRemaining }}分钟)