Download src/routes/apiStats.js from hequ/cc: direct link, hf CLI and curl.
- Browser
- Download file 32.1 kB
-
https://huggingface.co/spaces/hequ/cc/resolve/main/src/routes/apiStats.js
- Command line
-
hf download hf://spaces/hequ/cc/src/routes/apiStats.js
-
curl -L -o apiStats.js https://huggingface.co/spaces/hequ/cc/resolve/main/src/routes/apiStats.js
32.1 kB
| const express = require('express') | |
| const redis = require('../models/redis') | |
| const logger = require('../utils/logger') | |
| const apiKeyService = require('../services/apiKeyService') | |
| const CostCalculator = require('../utils/costCalculator') | |
| const claudeAccountService = require('../services/claudeAccountService') | |
| const openaiAccountService = require('../services/openaiAccountService') | |
| const router = express.Router() | |
| // 🏠 重定向页面请求到新版 admin-spa | |
| router.get('/', (req, res) => { | |
| res.redirect(301, '/admin-next/api-stats') | |
| }) | |
| // 🔑 获取 API Key 对应的 ID | |
| router.post('/api/get-key-id', async (req, res) => { | |
| try { | |
| const { apiKey } = req.body | |
| if (!apiKey) { | |
| return res.status(400).json({ | |
| error: 'API Key is required', | |
| message: 'Please provide your API Key' | |
| }) | |
| } | |
| // 基本API Key格式验证 | |
| if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) { | |
| return res.status(400).json({ | |
| error: 'Invalid API key format', | |
| message: 'API key format is invalid' | |
| }) | |
| } | |
| // 验证API Key(使用不触发激活的验证方法) | |
| const validation = await apiKeyService.validateApiKeyForStats(apiKey) | |
| if (!validation.valid) { | |
| const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' | |
| logger.security(`🔒 Invalid API key in get-key-id: ${validation.error} from ${clientIP}`) | |
| return res.status(401).json({ | |
| error: 'Invalid API key', | |
| message: validation.error | |
| }) | |
| } | |
| const { keyData } = validation | |
| return res.json({ | |
| success: true, | |
| data: { | |
| id: keyData.id | |
| } | |
| }) | |
| } catch (error) { | |
| logger.error('❌ Failed to get API key ID:', error) | |
| return res.status(500).json({ | |
| error: 'Internal server error', | |
| message: 'Failed to retrieve API key ID' | |
| }) | |
| } | |
| }) | |
| // 📊 用户API Key统计查询接口 - 安全的自查询接口 | |
| router.post('/api/user-stats', async (req, res) => { | |
| try { | |
| const { apiKey, apiId } = req.body | |
| let keyData | |
| let keyId | |
| if (apiId) { | |
| // 通过 apiId 查询 | |
| if ( | |
| typeof apiId !== 'string' || | |
| !apiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i) | |
| ) { | |
| return res.status(400).json({ | |
| error: 'Invalid API ID format', | |
| message: 'API ID must be a valid UUID' | |
| }) | |
| } | |
| // 直接通过 ID 获取 API Key 数据 | |
| keyData = await redis.getApiKey(apiId) | |
| if (!keyData || Object.keys(keyData).length === 0) { | |
| logger.security(`🔒 API key not found for ID: ${apiId} from ${req.ip || 'unknown'}`) | |
| return res.status(404).json({ | |
| error: 'API key not found', | |
| message: 'The specified API key does not exist' | |
| }) | |
| } | |
| // 检查是否激活 | |
| if (keyData.isActive !== 'true') { | |
| return res.status(403).json({ | |
| error: 'API key is disabled', | |
| message: 'This API key has been disabled' | |
| }) | |
| } | |
| // 检查是否过期 | |
| if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) { | |
| return res.status(403).json({ | |
| error: 'API key has expired', | |
| message: 'This API key has expired' | |
| }) | |
| } | |
| keyId = apiId | |
| // 获取使用统计 | |
| const usage = await redis.getUsageStats(keyId) | |
| // 获取当日费用统计 | |
| const dailyCost = await redis.getDailyCost(keyId) | |
| const costStats = await redis.getCostStats(keyId) | |
| // 处理数据格式,与 validateApiKey 返回的格式保持一致 | |
| // 解析限制模型数据 | |
| let restrictedModels = [] | |
| try { | |
| restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : [] | |
| } catch (e) { | |
| restrictedModels = [] | |
| } | |
| // 解析允许的客户端数据 | |
| let allowedClients = [] | |
| try { | |
| allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : [] | |
| } catch (e) { | |
| allowedClients = [] | |
| } | |
| // 格式化 keyData | |
| keyData = { | |
| ...keyData, | |
| tokenLimit: parseInt(keyData.tokenLimit) || 0, | |
| concurrencyLimit: parseInt(keyData.concurrencyLimit) || 0, | |
| rateLimitWindow: parseInt(keyData.rateLimitWindow) || 0, | |
| rateLimitRequests: parseInt(keyData.rateLimitRequests) || 0, | |
| dailyCostLimit: parseFloat(keyData.dailyCostLimit) || 0, | |
| totalCostLimit: parseFloat(keyData.totalCostLimit) || 0, | |
| dailyCost: dailyCost || 0, | |
| totalCost: costStats.total || 0, | |
| enableModelRestriction: keyData.enableModelRestriction === 'true', | |
| restrictedModels, | |
| enableClientRestriction: keyData.enableClientRestriction === 'true', | |
| allowedClients, | |
| permissions: keyData.permissions || 'all', | |
| // 添加激活相关字段 | |
| expirationMode: keyData.expirationMode || 'fixed', | |
| isActivated: keyData.isActivated === 'true', | |
| activationDays: parseInt(keyData.activationDays || 0), | |
| activatedAt: keyData.activatedAt || null, | |
| usage // 使用完整的 usage 数据,而不是只有 total | |
| } | |
| } else if (apiKey) { | |
| // 通过 apiKey 查询(保持向后兼容) | |
| if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) { | |
| logger.security(`🔒 Invalid API key format in user stats query from ${req.ip || 'unknown'}`) | |
| return res.status(400).json({ | |
| error: 'Invalid API key format', | |
| message: 'API key format is invalid' | |
| }) | |
| } | |
| // 验证API Key(使用不触发激活的验证方法) | |
| const validation = await apiKeyService.validateApiKeyForStats(apiKey) | |
| if (!validation.valid) { | |
| const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' | |
| logger.security( | |
| `🔒 Invalid API key in user stats query: ${validation.error} from ${clientIP}` | |
| ) | |
| return res.status(401).json({ | |
| error: 'Invalid API key', | |
| message: validation.error | |
| }) | |
| } | |
| const { keyData: validatedKeyData } = validation | |
| keyData = validatedKeyData | |
| keyId = keyData.id | |
| } else { | |
| logger.security(`🔒 Missing API key or ID in user stats query from ${req.ip || 'unknown'}`) | |
| return res.status(400).json({ | |
| error: 'API Key or ID is required', | |
| message: 'Please provide your API Key or API ID' | |
| }) | |
| } | |
| // 记录合法查询 | |
| logger.api( | |
| `📊 User stats query from key: ${keyData.name} (${keyId}) from ${req.ip || 'unknown'}` | |
| ) | |
| // 获取验证结果中的完整keyData(包含isActive状态和cost信息) | |
| const fullKeyData = keyData | |
| // 计算总费用 - 使用与模型统计相同的逻辑(按模型分别计算) | |
| let totalCost = 0 | |
| let formattedCost = '$0.000000' | |
| try { | |
| const client = redis.getClientSafe() | |
| // 获取所有月度模型统计(与model-stats接口相同的逻辑) | |
| const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`) | |
| const modelUsageMap = new Map() | |
| for (const key of allModelKeys) { | |
| const modelMatch = key.match(/usage:.+:model:monthly:(.+):(\d{4}-\d{2})$/) | |
| if (!modelMatch) { | |
| continue | |
| } | |
| const model = modelMatch[1] | |
| const data = await client.hgetall(key) | |
| if (data && Object.keys(data).length > 0) { | |
| if (!modelUsageMap.has(model)) { | |
| modelUsageMap.set(model, { | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheCreateTokens: 0, | |
| cacheReadTokens: 0 | |
| }) | |
| } | |
| const modelUsage = modelUsageMap.get(model) | |
| modelUsage.inputTokens += parseInt(data.inputTokens) || 0 | |
| modelUsage.outputTokens += parseInt(data.outputTokens) || 0 | |
| modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 | |
| modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 | |
| } | |
| } | |
| // 按模型计算费用并汇总 | |
| for (const [model, usage] of modelUsageMap) { | |
| const usageData = { | |
| input_tokens: usage.inputTokens, | |
| output_tokens: usage.outputTokens, | |
| cache_creation_input_tokens: usage.cacheCreateTokens, | |
| cache_read_input_tokens: usage.cacheReadTokens | |
| } | |
| const costResult = CostCalculator.calculateCost(usageData, model) | |
| totalCost += costResult.costs.total | |
| } | |
| // 如果没有模型级别的详细数据,回退到总体数据计算 | |
| if (modelUsageMap.size === 0 && fullKeyData.usage?.total?.allTokens > 0) { | |
| const usage = fullKeyData.usage.total | |
| const costUsage = { | |
| input_tokens: usage.inputTokens || 0, | |
| output_tokens: usage.outputTokens || 0, | |
| cache_creation_input_tokens: usage.cacheCreateTokens || 0, | |
| cache_read_input_tokens: usage.cacheReadTokens || 0 | |
| } | |
| const costResult = CostCalculator.calculateCost(costUsage, 'claude-3-5-sonnet-20241022') | |
| totalCost = costResult.costs.total | |
| } | |
| formattedCost = CostCalculator.formatCost(totalCost) | |
| } catch (error) { | |
| logger.warn(`Failed to calculate detailed cost for key ${keyId}:`, error) | |
| // 回退到简单计算 | |
| if (fullKeyData.usage?.total?.allTokens > 0) { | |
| const usage = fullKeyData.usage.total | |
| const costUsage = { | |
| input_tokens: usage.inputTokens || 0, | |
| output_tokens: usage.outputTokens || 0, | |
| cache_creation_input_tokens: usage.cacheCreateTokens || 0, | |
| cache_read_input_tokens: usage.cacheReadTokens || 0 | |
| } | |
| const costResult = CostCalculator.calculateCost(costUsage, 'claude-3-5-sonnet-20241022') | |
| totalCost = costResult.costs.total | |
| formattedCost = costResult.formatted.total | |
| } | |
| } | |
| // 获取当前使用量 | |
| let currentWindowRequests = 0 | |
| let currentWindowTokens = 0 | |
| let currentWindowCost = 0 // 新增:当前窗口费用 | |
| let currentDailyCost = 0 | |
| let windowStartTime = null | |
| let windowEndTime = null | |
| let windowRemainingSeconds = null | |
| try { | |
| // 获取当前时间窗口的请求次数、Token使用量和费用 | |
| if (fullKeyData.rateLimitWindow > 0) { | |
| const client = redis.getClientSafe() | |
| const requestCountKey = `rate_limit:requests:${keyId}` | |
| const tokenCountKey = `rate_limit:tokens:${keyId}` | |
| const costCountKey = `rate_limit:cost:${keyId}` // 新增:费用计数key | |
| const windowStartKey = `rate_limit:window_start:${keyId}` | |
| currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0') | |
| currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0') | |
| currentWindowCost = parseFloat((await client.get(costCountKey)) || '0') // 新增:获取当前窗口费用 | |
| // 获取窗口开始时间和计算剩余时间 | |
| const windowStart = await client.get(windowStartKey) | |
| if (windowStart) { | |
| const now = Date.now() | |
| windowStartTime = parseInt(windowStart) | |
| const windowDuration = fullKeyData.rateLimitWindow * 60 * 1000 // 转换为毫秒 | |
| windowEndTime = windowStartTime + windowDuration | |
| // 如果窗口还有效 | |
| if (now < windowEndTime) { | |
| windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000)) | |
| } else { | |
| // 窗口已过期,下次请求会重置 | |
| windowStartTime = null | |
| windowEndTime = null | |
| windowRemainingSeconds = 0 | |
| // 重置计数为0,因为窗口已过期 | |
| currentWindowRequests = 0 | |
| currentWindowTokens = 0 | |
| currentWindowCost = 0 // 新增:重置窗口费用 | |
| } | |
| } | |
| } | |
| // 获取当日费用 | |
| currentDailyCost = (await redis.getDailyCost(keyId)) || 0 | |
| } catch (error) { | |
| logger.warn(`Failed to get current usage for key ${keyId}:`, error) | |
| } | |
| const boundAccountDetails = {} | |
| const accountDetailTasks = [] | |
| if (fullKeyData.claudeAccountId) { | |
| accountDetailTasks.push( | |
| (async () => { | |
| try { | |
| const overview = await claudeAccountService.getAccountOverview( | |
| fullKeyData.claudeAccountId | |
| ) | |
| if (overview && overview.accountType === 'dedicated') { | |
| boundAccountDetails.claude = overview | |
| } | |
| } catch (error) { | |
| logger.warn(`⚠️ Failed to load Claude account overview for key ${keyId}:`, error) | |
| } | |
| })() | |
| ) | |
| } | |
| if (fullKeyData.openaiAccountId) { | |
| accountDetailTasks.push( | |
| (async () => { | |
| try { | |
| const overview = await openaiAccountService.getAccountOverview( | |
| fullKeyData.openaiAccountId | |
| ) | |
| if (overview && overview.accountType === 'dedicated') { | |
| boundAccountDetails.openai = overview | |
| } | |
| } catch (error) { | |
| logger.warn(`⚠️ Failed to load OpenAI account overview for key ${keyId}:`, error) | |
| } | |
| })() | |
| ) | |
| } | |
| if (accountDetailTasks.length > 0) { | |
| await Promise.allSettled(accountDetailTasks) | |
| } | |
| // 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息) | |
| const responseData = { | |
| id: keyId, | |
| name: fullKeyData.name, | |
| description: fullKeyData.description || keyData.description || '', | |
| isActive: true, // 如果能通过validateApiKey验证,说明一定是激活的 | |
| createdAt: fullKeyData.createdAt || keyData.createdAt, | |
| expiresAt: fullKeyData.expiresAt || keyData.expiresAt, | |
| // 添加激活相关字段 | |
| expirationMode: fullKeyData.expirationMode || 'fixed', | |
| isActivated: fullKeyData.isActivated === true || fullKeyData.isActivated === 'true', | |
| activationDays: parseInt(fullKeyData.activationDays || 0), | |
| activatedAt: fullKeyData.activatedAt || null, | |
| permissions: fullKeyData.permissions, | |
| // 使用统计(使用验证结果中的完整数据) | |
| usage: { | |
| total: { | |
| ...(fullKeyData.usage?.total || { | |
| requests: 0, | |
| tokens: 0, | |
| allTokens: 0, | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheCreateTokens: 0, | |
| cacheReadTokens: 0 | |
| }), | |
| cost: totalCost, | |
| formattedCost | |
| } | |
| }, | |
| // 限制信息(显示配置和当前使用量) | |
| limits: { | |
| tokenLimit: fullKeyData.tokenLimit || 0, | |
| concurrencyLimit: fullKeyData.concurrencyLimit || 0, | |
| rateLimitWindow: fullKeyData.rateLimitWindow || 0, | |
| rateLimitRequests: fullKeyData.rateLimitRequests || 0, | |
| rateLimitCost: parseFloat(fullKeyData.rateLimitCost) || 0, // 新增:费用限制 | |
| dailyCostLimit: fullKeyData.dailyCostLimit || 0, | |
| totalCostLimit: fullKeyData.totalCostLimit || 0, | |
| weeklyOpusCostLimit: parseFloat(fullKeyData.weeklyOpusCostLimit) || 0, // Opus 周费用限制 | |
| // 当前使用量 | |
| currentWindowRequests, | |
| currentWindowTokens, | |
| currentWindowCost, // 新增:当前窗口费用 | |
| currentDailyCost, | |
| currentTotalCost: totalCost, | |
| weeklyOpusCost: (await redis.getWeeklyOpusCost(keyId)) || 0, // 当前 Opus 周费用 | |
| // 时间窗口信息 | |
| windowStartTime, | |
| windowEndTime, | |
| windowRemainingSeconds | |
| }, | |
| // 绑定的账户信息(只显示ID,不显示敏感信息) | |
| accounts: { | |
| claudeAccountId: | |
| fullKeyData.claudeAccountId && fullKeyData.claudeAccountId !== '' | |
| ? fullKeyData.claudeAccountId | |
| : null, | |
| geminiAccountId: | |
| fullKeyData.geminiAccountId && fullKeyData.geminiAccountId !== '' | |
| ? fullKeyData.geminiAccountId | |
| : null, | |
| openaiAccountId: | |
| fullKeyData.openaiAccountId && fullKeyData.openaiAccountId !== '' | |
| ? fullKeyData.openaiAccountId | |
| : null, | |
| details: Object.keys(boundAccountDetails).length > 0 ? boundAccountDetails : null | |
| }, | |
| // 模型和客户端限制信息 | |
| restrictions: { | |
| enableModelRestriction: fullKeyData.enableModelRestriction || false, | |
| restrictedModels: fullKeyData.restrictedModels || [], | |
| enableClientRestriction: fullKeyData.enableClientRestriction || false, | |
| allowedClients: fullKeyData.allowedClients || [] | |
| } | |
| } | |
| return res.json({ | |
| success: true, | |
| data: responseData | |
| }) | |
| } catch (error) { | |
| logger.error('❌ Failed to process user stats query:', error) | |
| return res.status(500).json({ | |
| error: 'Internal server error', | |
| message: 'Failed to retrieve API key statistics' | |
| }) | |
| } | |
| }) | |
| // 📊 批量查询统计数据接口 | |
| router.post('/api/batch-stats', async (req, res) => { | |
| try { | |
| const { apiIds } = req.body | |
| // 验证输入 | |
| if (!apiIds || !Array.isArray(apiIds) || apiIds.length === 0) { | |
| return res.status(400).json({ | |
| error: 'Invalid input', | |
| message: 'API IDs array is required' | |
| }) | |
| } | |
| // 限制最多查询 30 个 | |
| if (apiIds.length > 30) { | |
| return res.status(400).json({ | |
| error: 'Too many keys', | |
| message: 'Maximum 30 API keys can be queried at once' | |
| }) | |
| } | |
| // 验证所有 ID 格式 | |
| const uuidRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i | |
| const invalidIds = apiIds.filter((id) => !uuidRegex.test(id)) | |
| if (invalidIds.length > 0) { | |
| return res.status(400).json({ | |
| error: 'Invalid API ID format', | |
| message: `Invalid API IDs: ${invalidIds.join(', ')}` | |
| }) | |
| } | |
| const individualStats = [] | |
| const aggregated = { | |
| totalKeys: apiIds.length, | |
| activeKeys: 0, | |
| usage: { | |
| requests: 0, | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheCreateTokens: 0, | |
| cacheReadTokens: 0, | |
| allTokens: 0, | |
| cost: 0, | |
| formattedCost: '$0.000000' | |
| }, | |
| dailyUsage: { | |
| requests: 0, | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheCreateTokens: 0, | |
| cacheReadTokens: 0, | |
| allTokens: 0, | |
| cost: 0, | |
| formattedCost: '$0.000000' | |
| }, | |
| monthlyUsage: { | |
| requests: 0, | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheCreateTokens: 0, | |
| cacheReadTokens: 0, | |
| allTokens: 0, | |
| cost: 0, | |
| formattedCost: '$0.000000' | |
| } | |
| } | |
| // 并行查询所有 API Key 数据(复用单key查询逻辑) | |
| const results = await Promise.allSettled( | |
| apiIds.map(async (apiId) => { | |
| const keyData = await redis.getApiKey(apiId) | |
| if (!keyData || Object.keys(keyData).length === 0) { | |
| return { error: 'Not found', apiId } | |
| } | |
| // 检查是否激活 | |
| if (keyData.isActive !== 'true') { | |
| return { error: 'Disabled', apiId } | |
| } | |
| // 检查是否过期 | |
| if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) { | |
| return { error: 'Expired', apiId } | |
| } | |
| // 复用单key查询的逻辑:获取使用统计 | |
| const usage = await redis.getUsageStats(apiId) | |
| // 获取费用统计(与单key查询一致) | |
| const costStats = await redis.getCostStats(apiId) | |
| return { | |
| apiId, | |
| name: keyData.name, | |
| description: keyData.description || '', | |
| isActive: true, | |
| createdAt: keyData.createdAt, | |
| usage: usage.total || {}, | |
| dailyStats: { | |
| ...usage.daily, | |
| cost: costStats.daily | |
| }, | |
| monthlyStats: { | |
| ...usage.monthly, | |
| cost: costStats.monthly | |
| }, | |
| totalCost: costStats.total | |
| } | |
| }) | |
| ) | |
| // 处理结果并聚合 | |
| results.forEach((result) => { | |
| if (result.status === 'fulfilled' && result.value && !result.value.error) { | |
| const stats = result.value | |
| aggregated.activeKeys++ | |
| // 聚合总使用量 | |
| if (stats.usage) { | |
| aggregated.usage.requests += stats.usage.requests || 0 | |
| aggregated.usage.inputTokens += stats.usage.inputTokens || 0 | |
| aggregated.usage.outputTokens += stats.usage.outputTokens || 0 | |
| aggregated.usage.cacheCreateTokens += stats.usage.cacheCreateTokens || 0 | |
| aggregated.usage.cacheReadTokens += stats.usage.cacheReadTokens || 0 | |
| aggregated.usage.allTokens += stats.usage.allTokens || 0 | |
| } | |
| // 聚合总费用 | |
| aggregated.usage.cost += stats.totalCost || 0 | |
| // 聚合今日使用量 | |
| aggregated.dailyUsage.requests += stats.dailyStats.requests || 0 | |
| aggregated.dailyUsage.inputTokens += stats.dailyStats.inputTokens || 0 | |
| aggregated.dailyUsage.outputTokens += stats.dailyStats.outputTokens || 0 | |
| aggregated.dailyUsage.cacheCreateTokens += stats.dailyStats.cacheCreateTokens || 0 | |
| aggregated.dailyUsage.cacheReadTokens += stats.dailyStats.cacheReadTokens || 0 | |
| aggregated.dailyUsage.allTokens += stats.dailyStats.allTokens || 0 | |
| aggregated.dailyUsage.cost += stats.dailyStats.cost || 0 | |
| // 聚合本月使用量 | |
| aggregated.monthlyUsage.requests += stats.monthlyStats.requests || 0 | |
| aggregated.monthlyUsage.inputTokens += stats.monthlyStats.inputTokens || 0 | |
| aggregated.monthlyUsage.outputTokens += stats.monthlyStats.outputTokens || 0 | |
| aggregated.monthlyUsage.cacheCreateTokens += stats.monthlyStats.cacheCreateTokens || 0 | |
| aggregated.monthlyUsage.cacheReadTokens += stats.monthlyStats.cacheReadTokens || 0 | |
| aggregated.monthlyUsage.allTokens += stats.monthlyStats.allTokens || 0 | |
| aggregated.monthlyUsage.cost += stats.monthlyStats.cost || 0 | |
| // 添加到个体统计 | |
| individualStats.push({ | |
| apiId: stats.apiId, | |
| name: stats.name, | |
| isActive: true, | |
| usage: stats.usage, | |
| dailyUsage: { | |
| ...stats.dailyStats, | |
| formattedCost: CostCalculator.formatCost(stats.dailyStats.cost || 0) | |
| }, | |
| monthlyUsage: { | |
| ...stats.monthlyStats, | |
| formattedCost: CostCalculator.formatCost(stats.monthlyStats.cost || 0) | |
| } | |
| }) | |
| } | |
| }) | |
| // 格式化费用显示 | |
| aggregated.usage.formattedCost = CostCalculator.formatCost(aggregated.usage.cost) | |
| aggregated.dailyUsage.formattedCost = CostCalculator.formatCost(aggregated.dailyUsage.cost) | |
| aggregated.monthlyUsage.formattedCost = CostCalculator.formatCost(aggregated.monthlyUsage.cost) | |
| logger.api(`📊 Batch stats query for ${apiIds.length} keys from ${req.ip || 'unknown'}`) | |
| return res.json({ | |
| success: true, | |
| data: { | |
| aggregated, | |
| individual: individualStats | |
| } | |
| }) | |
| } catch (error) { | |
| logger.error('❌ Failed to process batch stats query:', error) | |
| return res.status(500).json({ | |
| error: 'Internal server error', | |
| message: 'Failed to retrieve batch statistics' | |
| }) | |
| } | |
| }) | |
| // 📊 批量模型统计查询接口 | |
| router.post('/api/batch-model-stats', async (req, res) => { | |
| try { | |
| const { apiIds, period = 'daily' } = req.body | |
| // 验证输入 | |
| if (!apiIds || !Array.isArray(apiIds) || apiIds.length === 0) { | |
| return res.status(400).json({ | |
| error: 'Invalid input', | |
| message: 'API IDs array is required' | |
| }) | |
| } | |
| // 限制最多查询 30 个 | |
| if (apiIds.length > 30) { | |
| return res.status(400).json({ | |
| error: 'Too many keys', | |
| message: 'Maximum 30 API keys can be queried at once' | |
| }) | |
| } | |
| const client = redis.getClientSafe() | |
| const tzDate = redis.getDateInTimezone() | |
| const today = redis.getDateStringInTimezone() | |
| const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}` | |
| const modelUsageMap = new Map() | |
| // 并行查询所有 API Key 的模型统计 | |
| await Promise.all( | |
| apiIds.map(async (apiId) => { | |
| const pattern = | |
| period === 'daily' | |
| ? `usage:${apiId}:model:daily:*:${today}` | |
| : `usage:${apiId}:model:monthly:*:${currentMonth}` | |
| const keys = await client.keys(pattern) | |
| for (const key of keys) { | |
| const match = key.match( | |
| period === 'daily' | |
| ? /usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ | |
| : /usage:.+:model:monthly:(.+):\d{4}-\d{2}$/ | |
| ) | |
| if (!match) { | |
| continue | |
| } | |
| const model = match[1] | |
| const data = await client.hgetall(key) | |
| if (data && Object.keys(data).length > 0) { | |
| if (!modelUsageMap.has(model)) { | |
| modelUsageMap.set(model, { | |
| requests: 0, | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheCreateTokens: 0, | |
| cacheReadTokens: 0, | |
| allTokens: 0 | |
| }) | |
| } | |
| const modelUsage = modelUsageMap.get(model) | |
| modelUsage.requests += parseInt(data.requests) || 0 | |
| modelUsage.inputTokens += parseInt(data.inputTokens) || 0 | |
| modelUsage.outputTokens += parseInt(data.outputTokens) || 0 | |
| modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 | |
| modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 | |
| modelUsage.allTokens += parseInt(data.allTokens) || 0 | |
| } | |
| } | |
| }) | |
| ) | |
| // 转换为数组并计算费用 | |
| const modelStats = [] | |
| for (const [model, usage] of modelUsageMap) { | |
| const usageData = { | |
| input_tokens: usage.inputTokens, | |
| output_tokens: usage.outputTokens, | |
| cache_creation_input_tokens: usage.cacheCreateTokens, | |
| cache_read_input_tokens: usage.cacheReadTokens | |
| } | |
| const costData = CostCalculator.calculateCost(usageData, model) | |
| modelStats.push({ | |
| model, | |
| requests: usage.requests, | |
| inputTokens: usage.inputTokens, | |
| outputTokens: usage.outputTokens, | |
| cacheCreateTokens: usage.cacheCreateTokens, | |
| cacheReadTokens: usage.cacheReadTokens, | |
| allTokens: usage.allTokens, | |
| costs: costData.costs, | |
| formatted: costData.formatted, | |
| pricing: costData.pricing | |
| }) | |
| } | |
| // 按总 token 数降序排列 | |
| modelStats.sort((a, b) => b.allTokens - a.allTokens) | |
| logger.api(`📊 Batch model stats query for ${apiIds.length} keys, period: ${period}`) | |
| return res.json({ | |
| success: true, | |
| data: modelStats, | |
| period | |
| }) | |
| } catch (error) { | |
| logger.error('❌ Failed to process batch model stats query:', error) | |
| return res.status(500).json({ | |
| error: 'Internal server error', | |
| message: 'Failed to retrieve batch model statistics' | |
| }) | |
| } | |
| }) | |
| // 📊 用户模型统计查询接口 - 安全的自查询接口 | |
| router.post('/api/user-model-stats', async (req, res) => { | |
| try { | |
| const { apiKey, apiId, period = 'monthly' } = req.body | |
| let keyData | |
| let keyId | |
| if (apiId) { | |
| // 通过 apiId 查询 | |
| if ( | |
| typeof apiId !== 'string' || | |
| !apiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i) | |
| ) { | |
| return res.status(400).json({ | |
| error: 'Invalid API ID format', | |
| message: 'API ID must be a valid UUID' | |
| }) | |
| } | |
| // 直接通过 ID 获取 API Key 数据 | |
| keyData = await redis.getApiKey(apiId) | |
| if (!keyData || Object.keys(keyData).length === 0) { | |
| logger.security(`🔒 API key not found for ID: ${apiId} from ${req.ip || 'unknown'}`) | |
| return res.status(404).json({ | |
| error: 'API key not found', | |
| message: 'The specified API key does not exist' | |
| }) | |
| } | |
| // 检查是否激活 | |
| if (keyData.isActive !== 'true') { | |
| return res.status(403).json({ | |
| error: 'API key is disabled', | |
| message: 'This API key has been disabled' | |
| }) | |
| } | |
| keyId = apiId | |
| // 获取使用统计 | |
| const usage = await redis.getUsageStats(keyId) | |
| keyData.usage = { total: usage.total } | |
| } else if (apiKey) { | |
| // 通过 apiKey 查询(保持向后兼容) | |
| // 验证API Key | |
| const validation = await apiKeyService.validateApiKey(apiKey) | |
| if (!validation.valid) { | |
| const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' | |
| logger.security( | |
| `🔒 Invalid API key in user model stats query: ${validation.error} from ${clientIP}` | |
| ) | |
| return res.status(401).json({ | |
| error: 'Invalid API key', | |
| message: validation.error | |
| }) | |
| } | |
| const { keyData: validatedKeyData } = validation | |
| keyData = validatedKeyData | |
| keyId = keyData.id | |
| } else { | |
| logger.security( | |
| `🔒 Missing API key or ID in user model stats query from ${req.ip || 'unknown'}` | |
| ) | |
| return res.status(400).json({ | |
| error: 'API Key or ID is required', | |
| message: 'Please provide your API Key or API ID' | |
| }) | |
| } | |
| logger.api( | |
| `📊 User model stats query from key: ${keyData.name} (${keyId}) for period: ${period}` | |
| ) | |
| // 重用管理后台的模型统计逻辑,但只返回该API Key的数据 | |
| const client = redis.getClientSafe() | |
| // 使用与管理页面相同的时区处理逻辑 | |
| const tzDate = redis.getDateInTimezone() | |
| const today = redis.getDateStringInTimezone() | |
| const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}` | |
| const pattern = | |
| period === 'daily' | |
| ? `usage:${keyId}:model:daily:*:${today}` | |
| : `usage:${keyId}:model:monthly:*:${currentMonth}` | |
| const keys = await client.keys(pattern) | |
| const modelStats = [] | |
| for (const key of keys) { | |
| const match = key.match( | |
| period === 'daily' | |
| ? /usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ | |
| : /usage:.+:model:monthly:(.+):\d{4}-\d{2}$/ | |
| ) | |
| if (!match) { | |
| continue | |
| } | |
| const model = match[1] | |
| const data = await client.hgetall(key) | |
| if (data && Object.keys(data).length > 0) { | |
| const usage = { | |
| input_tokens: parseInt(data.inputTokens) || 0, | |
| output_tokens: parseInt(data.outputTokens) || 0, | |
| cache_creation_input_tokens: parseInt(data.cacheCreateTokens) || 0, | |
| cache_read_input_tokens: parseInt(data.cacheReadTokens) || 0 | |
| } | |
| const costData = CostCalculator.calculateCost(usage, model) | |
| modelStats.push({ | |
| model, | |
| requests: parseInt(data.requests) || 0, | |
| inputTokens: usage.input_tokens, | |
| outputTokens: usage.output_tokens, | |
| cacheCreateTokens: usage.cache_creation_input_tokens, | |
| cacheReadTokens: usage.cache_read_input_tokens, | |
| allTokens: parseInt(data.allTokens) || 0, | |
| costs: costData.costs, | |
| formatted: costData.formatted, | |
| pricing: costData.pricing | |
| }) | |
| } | |
| } | |
| // 如果没有详细的模型数据,不显示历史数据以避免混淆 | |
| // 只有在查询特定时间段时返回空数组,表示该时间段确实没有数据 | |
| if (modelStats.length === 0) { | |
| logger.info(`📊 No model stats found for key ${keyId} in period ${period}`) | |
| } | |
| // 按总token数降序排列 | |
| modelStats.sort((a, b) => b.allTokens - a.allTokens) | |
| return res.json({ | |
| success: true, | |
| data: modelStats, | |
| period | |
| }) | |
| } catch (error) { | |
| logger.error('❌ Failed to process user model stats query:', error) | |
| return res.status(500).json({ | |
| error: 'Internal server error', | |
| message: 'Failed to retrieve model statistics' | |
| }) | |
| } | |
| }) | |
| module.exports = router | |