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 }}分钟)