diff --git a/VERSION b/VERSION index 6f182425..32ffe120 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.21 +1.1.23 diff --git a/src/models/redis.js b/src/models/redis.js index f2664f0e..f59ba93a 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -282,6 +282,104 @@ class RedisClient { ]); } + // 📊 记录账户级别的使用统计 + async incrementAccountUsage(accountId, totalTokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') { + const now = new Date(); + const today = getDateStringInTimezone(now); + const tzDate = getDateInTimezone(now); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`; + + // 账户级别统计的键 + const accountKey = `account_usage:${accountId}`; + const accountDaily = `account_usage:daily:${accountId}:${today}`; + const accountMonthly = `account_usage:monthly:${accountId}:${currentMonth}`; + const accountHourly = `account_usage:hourly:${accountId}:${currentHour}`; + + // 账户按模型统计的键 + const accountModelDaily = `account_usage:model:daily:${accountId}:${model}:${today}`; + const accountModelMonthly = `account_usage:model:monthly:${accountId}:${model}:${currentMonth}`; + const accountModelHourly = `account_usage:model:hourly:${accountId}:${model}:${currentHour}`; + + // 处理token分配 + const finalInputTokens = inputTokens || 0; + const finalOutputTokens = outputTokens || 0; + const finalCacheCreateTokens = cacheCreateTokens || 0; + const finalCacheReadTokens = cacheReadTokens || 0; + const actualTotalTokens = finalInputTokens + finalOutputTokens + finalCacheCreateTokens + finalCacheReadTokens; + const coreTokens = finalInputTokens + finalOutputTokens; + + await Promise.all([ + // 账户总体统计 + this.client.hincrby(accountKey, 'totalTokens', coreTokens), + this.client.hincrby(accountKey, 'totalInputTokens', finalInputTokens), + this.client.hincrby(accountKey, 'totalOutputTokens', finalOutputTokens), + this.client.hincrby(accountKey, 'totalCacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountKey, 'totalCacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountKey, 'totalAllTokens', actualTotalTokens), + this.client.hincrby(accountKey, 'totalRequests', 1), + + // 账户每日统计 + this.client.hincrby(accountDaily, 'tokens', coreTokens), + this.client.hincrby(accountDaily, 'inputTokens', finalInputTokens), + this.client.hincrby(accountDaily, 'outputTokens', finalOutputTokens), + this.client.hincrby(accountDaily, 'cacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountDaily, 'cacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountDaily, 'allTokens', actualTotalTokens), + this.client.hincrby(accountDaily, 'requests', 1), + + // 账户每月统计 + this.client.hincrby(accountMonthly, 'tokens', coreTokens), + this.client.hincrby(accountMonthly, 'inputTokens', finalInputTokens), + this.client.hincrby(accountMonthly, 'outputTokens', finalOutputTokens), + this.client.hincrby(accountMonthly, 'cacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountMonthly, 'cacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountMonthly, 'allTokens', actualTotalTokens), + this.client.hincrby(accountMonthly, 'requests', 1), + + // 账户每小时统计 + this.client.hincrby(accountHourly, 'tokens', coreTokens), + this.client.hincrby(accountHourly, 'inputTokens', finalInputTokens), + this.client.hincrby(accountHourly, 'outputTokens', finalOutputTokens), + this.client.hincrby(accountHourly, 'cacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountHourly, 'cacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountHourly, 'allTokens', actualTotalTokens), + this.client.hincrby(accountHourly, 'requests', 1), + + // 账户按模型统计 - 每日 + this.client.hincrby(accountModelDaily, 'inputTokens', finalInputTokens), + this.client.hincrby(accountModelDaily, 'outputTokens', finalOutputTokens), + this.client.hincrby(accountModelDaily, 'cacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountModelDaily, 'cacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountModelDaily, 'allTokens', actualTotalTokens), + this.client.hincrby(accountModelDaily, 'requests', 1), + + // 账户按模型统计 - 每月 + this.client.hincrby(accountModelMonthly, 'inputTokens', finalInputTokens), + this.client.hincrby(accountModelMonthly, 'outputTokens', finalOutputTokens), + this.client.hincrby(accountModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountModelMonthly, 'cacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountModelMonthly, 'allTokens', actualTotalTokens), + this.client.hincrby(accountModelMonthly, 'requests', 1), + + // 账户按模型统计 - 每小时 + this.client.hincrby(accountModelHourly, 'inputTokens', finalInputTokens), + this.client.hincrby(accountModelHourly, 'outputTokens', finalOutputTokens), + this.client.hincrby(accountModelHourly, 'cacheCreateTokens', finalCacheCreateTokens), + this.client.hincrby(accountModelHourly, 'cacheReadTokens', finalCacheReadTokens), + this.client.hincrby(accountModelHourly, 'allTokens', actualTotalTokens), + this.client.hincrby(accountModelHourly, 'requests', 1), + + // 设置过期时间 + this.client.expire(accountDaily, 86400 * 32), // 32天过期 + this.client.expire(accountMonthly, 86400 * 365), // 1年过期 + this.client.expire(accountHourly, 86400 * 7), // 7天过期 + this.client.expire(accountModelDaily, 86400 * 32), // 32天过期 + this.client.expire(accountModelMonthly, 86400 * 365), // 1年过期 + this.client.expire(accountModelHourly, 86400 * 7) // 7天过期 + ]); + } + async getUsageStats(keyId) { const totalKey = `usage:${keyId}`; const today = getDateStringInTimezone(); @@ -369,6 +467,110 @@ class RedisClient { }; } + // 📊 获取账户使用统计 + async getAccountUsageStats(accountId) { + const accountKey = `account_usage:${accountId}`; + const today = getDateStringInTimezone(); + const accountDailyKey = `account_usage:daily:${accountId}:${today}`; + const tzDate = getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const accountMonthlyKey = `account_usage:monthly:${accountId}:${currentMonth}`; + + const [total, daily, monthly] = await Promise.all([ + this.client.hgetall(accountKey), + this.client.hgetall(accountDailyKey), + this.client.hgetall(accountMonthlyKey) + ]); + + // 获取账户创建时间来计算平均值 + const accountData = await this.client.hgetall(`claude_account:${accountId}`); + const createdAt = accountData.createdAt ? new Date(accountData.createdAt) : new Date(); + const now = new Date(); + const daysSinceCreated = Math.max(1, Math.ceil((now - createdAt) / (1000 * 60 * 60 * 24))); + + const totalTokens = parseInt(total.totalTokens) || 0; + const totalRequests = parseInt(total.totalRequests) || 0; + + // 计算平均RPM和TPM + const totalMinutes = Math.max(1, daysSinceCreated * 24 * 60); + const avgRPM = totalRequests / totalMinutes; + const avgTPM = totalTokens / totalMinutes; + + // 处理账户统计数据 + const handleAccountData = (data) => { + const tokens = parseInt(data.totalTokens) || parseInt(data.tokens) || 0; + const inputTokens = parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0; + const outputTokens = parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0; + const requests = parseInt(data.totalRequests) || parseInt(data.requests) || 0; + const cacheCreateTokens = parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0; + const cacheReadTokens = parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0; + const allTokens = parseInt(data.totalAllTokens) || parseInt(data.allTokens) || 0; + + const actualAllTokens = allTokens || (inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens); + + return { + tokens: tokens, + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreateTokens: cacheCreateTokens, + cacheReadTokens: cacheReadTokens, + allTokens: actualAllTokens, + requests: requests + }; + }; + + const totalData = handleAccountData(total); + const dailyData = handleAccountData(daily); + const monthlyData = handleAccountData(monthly); + + return { + accountId: accountId, + total: totalData, + daily: dailyData, + monthly: monthlyData, + averages: { + rpm: Math.round(avgRPM * 100) / 100, + tpm: Math.round(avgTPM * 100) / 100, + dailyRequests: Math.round((totalRequests / daysSinceCreated) * 100) / 100, + dailyTokens: Math.round((totalTokens / daysSinceCreated) * 100) / 100 + } + }; + } + + // 📈 获取所有账户的使用统计 + async getAllAccountsUsageStats() { + try { + // 获取所有Claude账户 + const accountKeys = await this.client.keys('claude_account:*'); + const accountStats = []; + + for (const accountKey of accountKeys) { + const accountId = accountKey.replace('claude_account:', ''); + const accountData = await this.client.hgetall(accountKey); + + if (accountData.name) { + const stats = await this.getAccountUsageStats(accountId); + accountStats.push({ + id: accountId, + name: accountData.name, + email: accountData.email || '', + status: accountData.status || 'unknown', + isActive: accountData.isActive === 'true', + ...stats + }); + } + } + + // 按当日token使用量排序 + accountStats.sort((a, b) => (b.daily.allTokens || 0) - (a.daily.allTokens || 0)); + + return accountStats; + } catch (error) { + logger.error('❌ Failed to get all accounts usage stats:', error); + return []; + } + } + // 🧹 清空所有API Key的使用统计数据 async resetAllUsageStats() { const client = this.getClientSafe(); diff --git a/src/routes/admin.js b/src/routes/admin.js index 3788a86c..0ff4ca62 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -575,7 +575,34 @@ router.post('/claude-accounts/exchange-code', authenticateAdmin, async (req, res router.get('/claude-accounts', authenticateAdmin, async (req, res) => { try { const accounts = await claudeAccountService.getAllAccounts(); - res.json({ success: true, data: accounts }); + + // 为每个账户添加使用统计信息 + const accountsWithStats = await Promise.all(accounts.map(async (account) => { + try { + const usageStats = await redis.getAccountUsageStats(account.id); + return { + ...account, + usage: { + daily: usageStats.daily, + total: usageStats.total, + averages: usageStats.averages + } + }; + } catch (statsError) { + logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message); + // 如果获取统计失败,返回空统计 + return { + ...account, + usage: { + daily: { tokens: 0, requests: 0, allTokens: 0 }, + total: { tokens: 0, requests: 0, allTokens: 0 }, + averages: { rpm: 0, tpm: 0 } + } + }; + } + })); + + res.json({ success: true, data: accountsWithStats }); } catch (error) { logger.error('❌ Failed to get Claude accounts:', error); res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message }); @@ -762,7 +789,18 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { try { const accounts = await geminiAccountService.getAllAccounts(); - res.json({ success: true, data: accounts }); + + // 为Gemini账户添加空的使用统计(暂时) + const accountsWithStats = accounts.map(account => ({ + ...account, + usage: { + daily: { tokens: 0, requests: 0, allTokens: 0 }, + total: { tokens: 0, requests: 0, allTokens: 0 }, + averages: { rpm: 0, tpm: 0 } + } + })); + + res.json({ success: true, data: accountsWithStats }); } catch (error) { logger.error('❌ Failed to get Gemini accounts:', error); res.status(500).json({ error: 'Failed to get accounts', message: error.message }); @@ -835,6 +873,73 @@ router.post('/gemini-accounts/:accountId/refresh', authenticateAdmin, async (req } }); +// 📊 账户使用统计 + +// 获取所有账户的使用统计 +router.get('/accounts/usage-stats', authenticateAdmin, async (req, res) => { + try { + const accountsStats = await redis.getAllAccountsUsageStats(); + + res.json({ + success: true, + data: accountsStats, + summary: { + totalAccounts: accountsStats.length, + activeToday: accountsStats.filter(account => account.daily.requests > 0).length, + totalDailyTokens: accountsStats.reduce((sum, account) => sum + (account.daily.allTokens || 0), 0), + totalDailyRequests: accountsStats.reduce((sum, account) => sum + (account.daily.requests || 0), 0) + }, + timestamp: new Date().toISOString() + }); + } catch (error) { + logger.error('❌ Failed to get accounts usage stats:', error); + res.status(500).json({ + success: false, + error: 'Failed to get accounts usage stats', + message: error.message + }); + } +}); + +// 获取单个账户的使用统计 +router.get('/accounts/:accountId/usage-stats', authenticateAdmin, async (req, res) => { + try { + const { accountId } = req.params; + const accountStats = await redis.getAccountUsageStats(accountId); + + // 获取账户基本信息 + const accountData = await claudeAccountService.getAccount(accountId); + if (!accountData) { + return res.status(404).json({ + success: false, + error: 'Account not found' + }); + } + + res.json({ + success: true, + data: { + ...accountStats, + accountInfo: { + name: accountData.name, + email: accountData.email, + status: accountData.status, + isActive: accountData.isActive, + createdAt: accountData.createdAt + } + }, + timestamp: new Date().toISOString() + }); + } catch (error) { + logger.error('❌ Failed to get account usage stats:', error); + res.status(500).json({ + success: false, + error: 'Failed to get account usage stats', + message: error.message + }); + } +}); + // 📊 系统统计 // 获取系统概览 diff --git a/src/routes/api.js b/src/routes/api.js index 8f38347b..3027de5e 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -68,8 +68,9 @@ async function handleMessagesRequest(req, res) { const cacheReadTokens = usageData.cache_read_input_tokens || 0; const model = usageData.model || 'unknown'; - // 记录真实的token使用量(包含模型信息和所有4种token) - apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model).catch(error => { + // 记录真实的token使用量(包含模型信息和所有4种token以及账户ID) + const accountId = usageData.accountId; + apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model, accountId).catch(error => { logger.error('❌ Failed to record stream usage:', error); }); @@ -135,8 +136,9 @@ async function handleMessagesRequest(req, res) { const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0; const model = jsonData.model || req.body.model || 'unknown'; - // 记录真实的token使用量(包含模型信息和所有4种token) - await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model); + // 记录真实的token使用量(包含模型信息和所有4种token以及账户ID) + const accountId = response.accountId; + await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model, accountId); // 更新时间窗口内的token计数 if (req.rateLimitInfo) { diff --git a/src/routes/openaiClaudeRoutes.js b/src/routes/openaiClaudeRoutes.js index b72c1257..247fc127 100644 --- a/src/routes/openaiClaudeRoutes.js +++ b/src/routes/openaiClaudeRoutes.js @@ -258,7 +258,8 @@ async function handleChatCompletion(req, res, apiKeyData) { outputTokens, cacheCreateTokens, cacheReadTokens, - model + model, + accountId ).catch(error => { logger.error('❌ Failed to record usage:', error); }); @@ -327,7 +328,8 @@ async function handleChatCompletion(req, res, apiKeyData) { usage.output_tokens || 0, usage.cache_creation_input_tokens || 0, usage.cache_read_input_tokens || 0, - claudeRequest.model + claudeRequest.model, + accountId ).catch(error => { logger.error('❌ Failed to record usage:', error); }); diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 2541b352..3cfffe9f 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -256,18 +256,28 @@ class ApiKeyService { } } - // 📊 记录使用情况(支持缓存token) - async recordUsage(keyId, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') { + // 📊 记录使用情况(支持缓存token和账户级别统计) + async recordUsage(keyId, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown', accountId = null) { try { const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens; + + // 记录API Key级别的使用统计 await redis.incrementTokenUsage(keyId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model); - // 更新最后使用时间(性能优化:只在实际使用时更新) + // 获取API Key数据以确定关联的账户 const keyData = await redis.getApiKey(keyId); if (keyData && Object.keys(keyData).length > 0) { + // 更新最后使用时间 keyData.lastUsedAt = new Date().toISOString(); - // 使用记录时不需要重新建立哈希映射 await redis.setApiKey(keyId, keyData); + + // 记录账户级别的使用统计(只统计实际处理请求的账户) + if (accountId) { + await redis.incrementAccountUsage(accountId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model); + logger.database(`📊 Recorded account usage: ${accountId} - ${totalTokens} tokens (API Key: ${keyId})`); + } else { + logger.debug(`⚠️ No accountId provided for usage recording, skipping account-level statistics`); + } } const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]; @@ -296,6 +306,16 @@ class ApiKeyService { return await redis.getUsageStats(keyId); } + // 📊 获取账户使用统计 + async getAccountUsageStats(accountId) { + return await redis.getAccountUsageStats(accountId); + } + + // 📈 获取所有账户使用统计 + async getAllAccountsUsageStats() { + return await redis.getAllAccountsUsageStats(); + } + // 🧹 清理过期的API Keys async cleanupExpiredKeys() { diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 76585547..f246063d 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -181,6 +181,8 @@ class ClaudeRelayService { logger.info(`✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens, Output: ~${Math.round(outputTokens)} tokens`); + // 在响应中添加accountId,以便调用方记录账户级别统计 + response.accountId = accountId; return response; } catch (error) { logger.error(`❌ Claude relay request failed for key: ${apiKeyData.name || apiKeyData.id}:`, error.message); @@ -619,7 +621,10 @@ class ClaudeRelayService { const proxyAgent = await this._getProxyAgent(accountId); // 发送流式请求并捕获usage数据 - return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer, options); + return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, (usageData) => { + // 在usageCallback中添加accountId + usageCallback({ ...usageData, accountId }); + }, accountId, sessionHash, streamTransformer, options); } catch (error) { logger.error('❌ Claude stream relay with usage capture failed:', error); throw error; diff --git a/web/admin/app.js b/web/admin/app.js index 9d7714c4..ad0a7682 100644 --- a/web/admin/app.js +++ b/web/admin/app.js @@ -112,6 +112,8 @@ const app = createApp({ apiKeys: [], apiKeysLoading: false, apiKeyStatsTimeRange: 'all', // API Key统计时间范围:all, 7days, monthly + apiKeysSortBy: '', // 当前排序字段 + apiKeysSortOrder: 'asc', // 排序顺序 'asc' 或 'desc' showCreateApiKeyModal: false, createApiKeyLoading: false, apiKeyForm: { @@ -199,6 +201,8 @@ const app = createApp({ // 账户 accounts: [], accountsLoading: false, + accountSortBy: 'dailyTokens', // 默认按今日Token排序 + accountsSortOrder: 'asc', // 排序顺序 'asc' 或 'desc' showCreateAccountModal: false, createAccountLoading: false, accountForm: { @@ -302,6 +306,83 @@ const app = createApp({ return `${window.location.protocol}//${window.location.host}/api/`; }, + // 排序后的账户列表 + sortedAccounts() { + if (!this.accountsSortBy) { + return this.accounts; + } + + return [...this.accounts].sort((a, b) => { + let aValue = a[this.accountsSortBy]; + let bValue = b[this.accountsSortBy]; + + // 特殊处理状态字段 + if (this.accountsSortBy === 'status') { + aValue = a.isActive ? 1 : 0; + bValue = b.isActive ? 1 : 0; + } + + // 处理字符串比较 + if (typeof aValue === 'string' && typeof bValue === 'string') { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + + // 排序 + if (this.accountsSortOrder === 'asc') { + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + } else { + return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; + } + }); + }, + + // 排序后的API Keys列表 + sortedApiKeys() { + if (!this.apiKeysSortBy) { + return this.apiKeys; + } + + return [...this.apiKeys].sort((a, b) => { + let aValue, bValue; + + // 特殊处理不同字段 + switch (this.apiKeysSortBy) { + case 'status': + aValue = a.isActive ? 1 : 0; + bValue = b.isActive ? 1 : 0; + break; + case 'cost': + // 计算费用,转换为数字比较 + aValue = this.calculateApiKeyCostNumber(a.usage); + bValue = this.calculateApiKeyCostNumber(b.usage); + break; + case 'createdAt': + case 'expiresAt': + // 日期比较 + aValue = a[this.apiKeysSortBy] ? new Date(a[this.apiKeysSortBy]).getTime() : 0; + bValue = b[this.apiKeysSortBy] ? new Date(b[this.apiKeysSortBy]).getTime() : 0; + break; + default: + aValue = a[this.apiKeysSortBy]; + bValue = b[this.apiKeysSortBy]; + + // 处理字符串比较 + if (typeof aValue === 'string' && typeof bValue === 'string') { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + } + + // 排序 + if (this.apiKeysSortOrder === 'asc') { + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + } else { + return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; + } + }); + }, + // 获取专属账号列表 dedicatedAccounts() { return this.accounts.filter(account => @@ -407,6 +488,30 @@ const app = createApp({ }, methods: { + // 账户列表排序 + sortAccounts(field) { + if (this.accountsSortBy === field) { + // 如果点击的是当前排序字段,切换排序顺序 + this.accountsSortOrder = this.accountsSortOrder === 'asc' ? 'desc' : 'asc'; + } else { + // 如果点击的是新字段,设置为升序 + this.accountsSortBy = field; + this.accountsSortOrder = 'asc'; + } + }, + + // API Keys列表排序 + sortApiKeys(field) { + if (this.apiKeysSortBy === field) { + // 如果点击的是当前排序字段,切换排序顺序 + this.apiKeysSortOrder = this.apiKeysSortOrder === 'asc' ? 'desc' : 'asc'; + } else { + // 如果点击的是新字段,设置为升序 + this.apiKeysSortBy = field; + this.apiKeysSortOrder = 'asc'; + } + }, + // 从URL读取tab参数并设置activeTab initializeTabFromUrl() { const urlParams = new URLSearchParams(window.location.search); @@ -1888,6 +1993,9 @@ const app = createApp({ account.boundApiKeysCount = this.apiKeys.filter(key => key.geminiAccountId === account.id).length; } }); + + // 加载完成后自动排序 + this.sortAccounts(); } catch (error) { console.error('Failed to load accounts:', error); } finally { @@ -1895,6 +2003,35 @@ const app = createApp({ } }, + // 账户排序 + sortAccounts() { + if (!this.accounts || this.accounts.length === 0) return; + + this.accounts.sort((a, b) => { + switch (this.accountSortBy) { + case 'name': + return a.name.localeCompare(b.name); + case 'dailyTokens': + const aTokens = (a.usage && a.usage.daily && a.usage.daily.allTokens) || 0; + const bTokens = (b.usage && b.usage.daily && b.usage.daily.allTokens) || 0; + return bTokens - aTokens; // 降序 + case 'dailyRequests': + const aRequests = (a.usage && a.usage.daily && a.usage.daily.requests) || 0; + const bRequests = (b.usage && b.usage.daily && b.usage.daily.requests) || 0; + return bRequests - aRequests; // 降序 + case 'totalTokens': + const aTotalTokens = (a.usage && a.usage.total && a.usage.total.allTokens) || 0; + const bTotalTokens = (b.usage && b.usage.total && b.usage.total.allTokens) || 0; + return bTotalTokens - aTotalTokens; // 降序 + case 'lastUsed': + const aLastUsed = a.lastUsedAt ? new Date(a.lastUsedAt) : new Date(0); + const bLastUsed = b.lastUsedAt ? new Date(b.lastUsedAt) : new Date(0); + return bLastUsed - aLastUsed; // 降序(最近使用的在前) + default: + return 0; + } + }); + }, async loadModelStats() { this.modelStatsLoading = true; @@ -3180,6 +3317,19 @@ const app = createApp({ // 如果没有后端费用数据,返回默认值 return '$0.000000'; }, + + // 计算API Key费用数值(用于排序) + calculateApiKeyCostNumber(usage) { + if (!usage || !usage.total) return 0; + + // 使用后端返回的准确费用数据 + if (usage.total.cost) { + return usage.total.cost; + } + + // 如果没有后端费用数据,返回0 + return 0; + }, // 初始化日期筛选器 initializeDateFilter() { diff --git a/web/admin/index.html b/web/admin/index.html index dfc0e764..46c12223 100644 --- a/web/admin/index.html +++ b/web/admin/index.html @@ -575,17 +575,40 @@ - + - - - - + + + + -
名称 + 名称 + + + API Key状态使用统计创建时间过期时间 + 状态 + + + + 使用统计 + + (费用 + + ) + + + 创建时间 + + + + 过期时间 + + + 操作