const express = require('express') const apiKeyService = require('../../services/apiKeyService') const redis = require('../../models/redis') const { authenticateAdmin } = require('../../middleware/auth') const logger = require('../../utils/logger') const CostCalculator = require('../../utils/costCalculator') const config = require('../../../config/config') const router = express.Router() // 👥 用户管理 (用于API Key分配) // 获取所有用户列表(用于API Key分配) router.get('/users', authenticateAdmin, async (req, res) => { try { const userService = require('../../services/userService') // Extract query parameters for filtering const { role, isActive } = req.query const options = { limit: 1000 } // Apply role filter if provided if (role) { options.role = role } // Apply isActive filter if provided, otherwise default to active users only if (isActive !== undefined) { options.isActive = isActive === 'true' } else { options.isActive = true // Default to active users for backwards compatibility } const result = await userService.getAllUsers(options) // Extract users array from the paginated result const allUsers = result.users || [] // Map to the format needed for the dropdown const activeUsers = allUsers.map((user) => ({ id: user.id, username: user.username, displayName: user.displayName || user.username, email: user.email, role: user.role })) // 添加Admin选项作为第一个 const usersWithAdmin = [ { id: 'admin', username: 'admin', displayName: 'Admin', email: '', role: 'admin' }, ...activeUsers ] return res.json({ success: true, data: usersWithAdmin }) } catch (error) { logger.error('❌ Failed to get users list:', error) return res.status(500).json({ error: 'Failed to get users list', message: error.message }) } }) // 🔑 API Keys 管理 // 调试:获取API Key费用详情 router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const costStats = await redis.getCostStats(keyId) const dailyCost = await redis.getDailyCost(keyId) const today = redis.getDateStringInTimezone() const client = redis.getClientSafe() // 获取所有相关的Redis键 const costKeys = await client.keys(`usage:cost:*:${keyId}:*`) const keyValues = {} for (const key of costKeys) { keyValues[key] = await client.get(key) } return res.json({ keyId, today, dailyCost, costStats, redisKeys: keyValues, timezone: config.system.timezoneOffset || 8 }) } catch (error) { logger.error('❌ Failed to get cost debug info:', error) return res.status(500).json({ error: 'Failed to get cost debug info', message: error.message }) } }) // 获取所有被使用过的模型列表 router.get('/api-keys/used-models', authenticateAdmin, async (req, res) => { try { const models = await redis.getAllUsedModels() return res.json({ success: true, data: models }) } catch (error) { logger.error('❌ Failed to get used models:', error) return res.status(500).json({ error: 'Failed to get used models', message: error.message }) } }) // 获取所有API Keys router.get('/api-keys', authenticateAdmin, async (req, res) => { try { const { // 分页参数 page = 1, pageSize = 20, // 搜索参数 searchMode = 'apiKey', search = '', // 筛选参数 tag = '', isActive = '', models = '', // 模型筛选(逗号分隔) // 排序参数 sortBy = 'createdAt', sortOrder = 'desc', // 费用排序参数 costTimeRange = '7days', // 费用排序的时间范围 costStartDate = '', // custom 时间范围的开始日期 costEndDate = '', // custom 时间范围的结束日期 // 兼容旧参数(不再用于费用计算,仅标记) timeRange = 'all' } = req.query // 解析模型筛选参数 const modelFilter = models ? models.split(',').filter((m) => m.trim()) : [] // 验证分页参数 const pageNum = Math.max(1, parseInt(page) || 1) const pageSizeNum = [10, 20, 50, 100].includes(parseInt(pageSize)) ? parseInt(pageSize) : 20 // 验证排序参数(新增 cost 排序) const validSortFields = [ 'name', 'createdAt', 'expiresAt', 'lastUsedAt', 'isActive', 'status', 'cost' ] const validSortBy = validSortFields.includes(sortBy) ? sortBy : 'createdAt' const validSortOrder = ['asc', 'desc'].includes(sortOrder) ? sortOrder : 'desc' // 获取用户服务来补充owner信息 const userService = require('../../services/userService') // 如果是绑定账号搜索模式,先刷新账户名称缓存 if (searchMode === 'bindingAccount' && search) { const accountNameCacheService = require('../../services/accountNameCacheService') await accountNameCacheService.refreshIfNeeded() } let result let costSortStatus = null // 如果是费用排序 if (validSortBy === 'cost') { const costRankService = require('../../services/costRankService') // 验证费用排序的时间范围 const validCostTimeRanges = ['today', '7days', '30days', 'all', 'custom'] const effectiveCostTimeRange = validCostTimeRanges.includes(costTimeRange) ? costTimeRange : '7days' // 如果是 custom 时间范围,使用实时计算 if (effectiveCostTimeRange === 'custom') { // 验证日期参数 if (!costStartDate || !costEndDate) { return res.status(400).json({ success: false, error: 'INVALID_DATE_RANGE', message: '自定义时间范围需要提供 costStartDate 和 costEndDate 参数' }) } const start = new Date(costStartDate) const end = new Date(costEndDate) if (isNaN(start.getTime()) || isNaN(end.getTime())) { return res.status(400).json({ success: false, error: 'INVALID_DATE_FORMAT', message: '日期格式无效' }) } if (start > end) { return res.status(400).json({ success: false, error: 'INVALID_DATE_RANGE', message: '开始日期不能晚于结束日期' }) } // 限制最大范围为 365 天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 if (daysDiff > 365) { return res.status(400).json({ success: false, error: 'DATE_RANGE_TOO_LARGE', message: '日期范围不能超过365天' }) } logger.info(`📊 Cost sort with custom range: ${costStartDate} to ${costEndDate}`) // 实时计算费用排序 result = await getApiKeysSortedByCostCustom({ page: pageNum, pageSize: pageSizeNum, sortOrder: validSortOrder, startDate: costStartDate, endDate: costEndDate, search, searchMode, tag, isActive, modelFilter }) costSortStatus = { status: 'ready', isRealTimeCalculation: true } } else { // 使用预计算索引 const rankStatus = await costRankService.getRankStatus() costSortStatus = rankStatus[effectiveCostTimeRange] // 检查索引是否就绪 if (!costSortStatus || costSortStatus.status !== 'ready') { return res.status(503).json({ success: false, error: 'RANK_NOT_READY', message: `费用排序索引 (${effectiveCostTimeRange}) 正在更新中,请稍后重试`, costSortStatus: costSortStatus || { status: 'unknown' } }) } logger.info(`📊 Cost sort using precomputed index: ${effectiveCostTimeRange}`) // 使用预计算索引排序 result = await getApiKeysSortedByCostPrecomputed({ page: pageNum, pageSize: pageSizeNum, sortOrder: validSortOrder, costTimeRange: effectiveCostTimeRange, search, searchMode, tag, isActive, modelFilter }) costSortStatus.isRealTimeCalculation = false } } else { // 原有的非费用排序逻辑 result = await redis.getApiKeysPaginated({ page: pageNum, pageSize: pageSizeNum, searchMode, search, tag, isActive, sortBy: validSortBy, sortOrder: validSortOrder, modelFilter }) } // 为每个API Key添加owner的displayName for (const apiKey of result.items) { if (apiKey.userId) { try { const user = await userService.getUserById(apiKey.userId, false) if (user) { apiKey.ownerDisplayName = user.displayName || user.username || 'Unknown User' } else { apiKey.ownerDisplayName = 'Unknown User' } } catch (error) { logger.debug(`无法获取用户 ${apiKey.userId} 的信息:`, error) apiKey.ownerDisplayName = 'Unknown User' } } else { apiKey.ownerDisplayName = apiKey.createdBy === 'admin' ? 'Admin' : apiKey.createdBy || 'Admin' } // 初始化空的 usage 对象(费用通过 batch-stats 接口获取) if (!apiKey.usage) { apiKey.usage = { total: { requests: 0, tokens: 0, cost: 0, formattedCost: '$0.00' } } } } // 返回分页数据 const responseData = { success: true, data: { items: result.items, pagination: result.pagination, availableTags: result.availableTags }, // 标记当前请求的时间范围(供前端参考) timeRange } // 如果是费用排序,附加排序状态 if (costSortStatus) { responseData.data.costSortStatus = costSortStatus } return res.json(responseData) } catch (error) { logger.error('❌ Failed to get API keys:', error) return res.status(500).json({ error: 'Failed to get API keys', message: error.message }) } }) /** * 使用预计算索引进行费用排序的分页查询 */ async function getApiKeysSortedByCostPrecomputed(options) { const { page, pageSize, sortOrder, costTimeRange, search, searchMode, tag, isActive, modelFilter = [] } = options const costRankService = require('../../services/costRankService') // 1. 获取排序后的全量 keyId 列表 const rankedKeyIds = await costRankService.getSortedKeyIds(costTimeRange, sortOrder) if (rankedKeyIds.length === 0) { return { items: [], pagination: { page: 1, pageSize, total: 0, totalPages: 1 }, availableTags: [] } } // 2. 批量获取 API Key 基础数据 const allKeys = await redis.batchGetApiKeys(rankedKeyIds) // 3. 保持排序顺序(使用 Map 优化查找) const keyMap = new Map(allKeys.map((k) => [k.id, k])) let orderedKeys = rankedKeyIds.map((id) => keyMap.get(id)).filter((k) => k && !k.isDeleted) // 4. 应用筛选条件 // 状态筛选 if (isActive !== '' && isActive !== undefined && isActive !== null) { const activeValue = isActive === 'true' || isActive === true orderedKeys = orderedKeys.filter((k) => k.isActive === activeValue) } // 标签筛选 if (tag) { orderedKeys = orderedKeys.filter((k) => { const tags = Array.isArray(k.tags) ? k.tags : [] return tags.includes(tag) }) } // 搜索筛选 if (search) { const lowerSearch = search.toLowerCase().trim() if (searchMode === 'apiKey') { orderedKeys = orderedKeys.filter((k) => k.name && k.name.toLowerCase().includes(lowerSearch)) } else if (searchMode === 'bindingAccount') { const accountNameCacheService = require('../../services/accountNameCacheService') orderedKeys = accountNameCacheService.searchByBindingAccount(orderedKeys, lowerSearch) } } // 模型筛选 if (modelFilter.length > 0) { const keyIdsWithModels = await redis.getKeyIdsWithModels( orderedKeys.map((k) => k.id), modelFilter ) orderedKeys = orderedKeys.filter((k) => keyIdsWithModels.has(k.id)) } // 5. 收集所有可用标签 const allTags = new Set() for (const key of allKeys) { if (!key.isDeleted) { const tags = Array.isArray(key.tags) ? key.tags : [] tags.forEach((t) => allTags.add(t)) } } const availableTags = [...allTags].sort() // 6. 分页 const total = orderedKeys.length const totalPages = Math.ceil(total / pageSize) || 1 const validPage = Math.min(Math.max(1, page), totalPages) const start = (validPage - 1) * pageSize const items = orderedKeys.slice(start, start + pageSize) // 7. 为当前页的 Keys 附加费用数据 const keyCosts = await costRankService.getBatchKeyCosts( costTimeRange, items.map((k) => k.id) ) for (const key of items) { key._cost = keyCosts.get(key.id) || 0 } return { items, pagination: { page: validPage, pageSize, total, totalPages }, availableTags } } /** * 使用实时计算进行 custom 时间范围的费用排序 */ async function getApiKeysSortedByCostCustom(options) { const { page, pageSize, sortOrder, startDate, endDate, search, searchMode, tag, isActive, modelFilter = [] } = options const costRankService = require('../../services/costRankService') // 1. 实时计算所有 Keys 的费用 const costs = await costRankService.calculateCustomRangeCosts(startDate, endDate) if (costs.size === 0) { return { items: [], pagination: { page: 1, pageSize, total: 0, totalPages: 1 }, availableTags: [] } } // 2. 转换为数组并排序 const sortedEntries = [...costs.entries()].sort((a, b) => sortOrder === 'desc' ? b[1] - a[1] : a[1] - b[1] ) const rankedKeyIds = sortedEntries.map(([keyId]) => keyId) // 3. 批量获取 API Key 基础数据 const allKeys = await redis.batchGetApiKeys(rankedKeyIds) // 4. 保持排序顺序 const keyMap = new Map(allKeys.map((k) => [k.id, k])) let orderedKeys = rankedKeyIds.map((id) => keyMap.get(id)).filter((k) => k && !k.isDeleted) // 5. 应用筛选条件 // 状态筛选 if (isActive !== '' && isActive !== undefined && isActive !== null) { const activeValue = isActive === 'true' || isActive === true orderedKeys = orderedKeys.filter((k) => k.isActive === activeValue) } // 标签筛选 if (tag) { orderedKeys = orderedKeys.filter((k) => { const tags = Array.isArray(k.tags) ? k.tags : [] return tags.includes(tag) }) } // 搜索筛选 if (search) { const lowerSearch = search.toLowerCase().trim() if (searchMode === 'apiKey') { orderedKeys = orderedKeys.filter((k) => k.name && k.name.toLowerCase().includes(lowerSearch)) } else if (searchMode === 'bindingAccount') { const accountNameCacheService = require('../../services/accountNameCacheService') orderedKeys = accountNameCacheService.searchByBindingAccount(orderedKeys, lowerSearch) } } // 模型筛选 if (modelFilter.length > 0) { const keyIdsWithModels = await redis.getKeyIdsWithModels( orderedKeys.map((k) => k.id), modelFilter ) orderedKeys = orderedKeys.filter((k) => keyIdsWithModels.has(k.id)) } // 6. 收集所有可用标签 const allTags = new Set() for (const key of allKeys) { if (!key.isDeleted) { const tags = Array.isArray(key.tags) ? key.tags : [] tags.forEach((t) => allTags.add(t)) } } const availableTags = [...allTags].sort() // 7. 分页 const total = orderedKeys.length const totalPages = Math.ceil(total / pageSize) || 1 const validPage = Math.min(Math.max(1, page), totalPages) const start = (validPage - 1) * pageSize const items = orderedKeys.slice(start, start + pageSize) // 8. 为当前页的 Keys 附加费用数据 for (const key of items) { key._cost = costs.get(key.id) || 0 } return { items, pagination: { page: validPage, pageSize, total, totalPages }, availableTags } } // 获取费用排序索引状态 router.get('/api-keys/cost-sort-status', authenticateAdmin, async (req, res) => { try { const costRankService = require('../../services/costRankService') const status = await costRankService.getRankStatus() return res.json({ success: true, data: status }) } catch (error) { logger.error('❌ Failed to get cost sort status:', error) return res.status(500).json({ success: false, error: 'Failed to get cost sort status', message: error.message }) } }) // 强制刷新费用排序索引 router.post('/api-keys/cost-sort-refresh', authenticateAdmin, async (req, res) => { try { const { timeRange } = req.body const costRankService = require('../../services/costRankService') // 验证时间范围 if (timeRange) { const validTimeRanges = ['today', '7days', '30days', 'all'] if (!validTimeRanges.includes(timeRange)) { return res.status(400).json({ success: false, error: 'INVALID_TIME_RANGE', message: '无效的时间范围,可选值:today, 7days, 30days, all' }) } } // 异步刷新,不等待完成 costRankService.forceRefresh(timeRange || null).catch((err) => { logger.error('❌ Failed to refresh cost rank:', err) }) return res.json({ success: true, message: timeRange ? `费用排序索引 (${timeRange}) 刷新已开始` : '所有费用排序索引刷新已开始' }) } catch (error) { logger.error('❌ Failed to trigger cost sort refresh:', error) return res.status(500).json({ success: false, error: 'Failed to trigger refresh', message: error.message }) } }) // 获取支持的客户端列表(使用新的验证器) router.get('/supported-clients', authenticateAdmin, async (req, res) => { try { // 使用新的 ClientValidator 获取所有可用客户端 const ClientValidator = require('../../validators/clientValidator') const availableClients = ClientValidator.getAvailableClients() // 格式化返回数据 const clients = availableClients.map((client) => ({ id: client.id, name: client.name, description: client.description, icon: client.icon })) logger.info(`📱 Returning ${clients.length} supported clients`) return res.json({ success: true, data: clients }) } catch (error) { logger.error('❌ Failed to get supported clients:', error) return res .status(500) .json({ error: 'Failed to get supported clients', message: error.message }) } }) // 获取已存在的标签列表 router.get('/api-keys/tags', authenticateAdmin, async (req, res) => { try { const apiKeys = await apiKeyService.getAllApiKeys() const tagSet = new Set() // 收集所有API Keys的标签 for (const apiKey of apiKeys) { if (apiKey.tags && Array.isArray(apiKey.tags)) { apiKey.tags.forEach((tag) => { if (tag && tag.trim()) { tagSet.add(tag.trim()) } }) } } // 转换为数组并排序 const tags = Array.from(tagSet).sort() logger.info(`📋 Retrieved ${tags.length} unique tags from API keys`) return res.json({ success: true, data: tags }) } catch (error) { logger.error('❌ Failed to get API key tags:', error) return res.status(500).json({ error: 'Failed to get API key tags', message: error.message }) } }) /** * 获取账户绑定的 API Key 数量统计 * GET /admin/accounts/binding-counts * * 返回每种账户类型的绑定数量统计,用于账户列表页面显示"绑定: X 个API Key" * 这是一个轻量级接口,只返回计数而不是完整的 API Key 数据 */ router.get('/accounts/binding-counts', authenticateAdmin, async (req, res) => { try { // 使用优化的分页方法获取所有非删除的 API Keys(只需要绑定字段) const result = await redis.getApiKeysPaginated({ page: 1, pageSize: 10000, // 获取所有 excludeDeleted: true }) const apiKeys = result.items // 初始化统计对象 const bindingCounts = { claudeAccountId: {}, claudeConsoleAccountId: {}, geminiAccountId: {}, openaiAccountId: {}, azureOpenaiAccountId: {}, bedrockAccountId: {}, droidAccountId: {}, ccrAccountId: {} } // 遍历一次,统计每个账户的绑定数量 for (const key of apiKeys) { // Claude 账户 if (key.claudeAccountId) { const id = key.claudeAccountId bindingCounts.claudeAccountId[id] = (bindingCounts.claudeAccountId[id] || 0) + 1 } // Claude Console 账户 if (key.claudeConsoleAccountId) { const id = key.claudeConsoleAccountId bindingCounts.claudeConsoleAccountId[id] = (bindingCounts.claudeConsoleAccountId[id] || 0) + 1 } // Gemini 账户(包括 api: 前缀的 Gemini-API 账户) if (key.geminiAccountId) { const id = key.geminiAccountId bindingCounts.geminiAccountId[id] = (bindingCounts.geminiAccountId[id] || 0) + 1 } // OpenAI 账户(包括 responses: 前缀的 OpenAI-Responses 账户) if (key.openaiAccountId) { const id = key.openaiAccountId bindingCounts.openaiAccountId[id] = (bindingCounts.openaiAccountId[id] || 0) + 1 } // Azure OpenAI 账户 if (key.azureOpenaiAccountId) { const id = key.azureOpenaiAccountId bindingCounts.azureOpenaiAccountId[id] = (bindingCounts.azureOpenaiAccountId[id] || 0) + 1 } // Bedrock 账户 if (key.bedrockAccountId) { const id = key.bedrockAccountId bindingCounts.bedrockAccountId[id] = (bindingCounts.bedrockAccountId[id] || 0) + 1 } // Droid 账户 if (key.droidAccountId) { const id = key.droidAccountId bindingCounts.droidAccountId[id] = (bindingCounts.droidAccountId[id] || 0) + 1 } // CCR 账户 if (key.ccrAccountId) { const id = key.ccrAccountId bindingCounts.ccrAccountId[id] = (bindingCounts.ccrAccountId[id] || 0) + 1 } } logger.debug(`📊 Account binding counts calculated from ${apiKeys.length} API keys`) return res.json({ success: true, data: bindingCounts }) } catch (error) { logger.error('❌ Failed to get account binding counts:', error) return res.status(500).json({ error: 'Failed to get account binding counts', message: error.message }) } }) /** * 批量获取指定 Keys 的统计数据和费用 * POST /admin/api-keys/batch-stats * * 用于 API Keys 列表页面异步加载统计数据 */ router.post('/api-keys/batch-stats', authenticateAdmin, async (req, res) => { try { const { keyIds, // 必需:API Key ID 数组 timeRange = 'all', // 时间范围:all, today, 7days, monthly, custom startDate, // custom 时必需 endDate // custom 时必需 } = req.body // 参数验证 if (!Array.isArray(keyIds) || keyIds.length === 0) { return res.status(400).json({ success: false, error: 'keyIds is required and must be a non-empty array' }) } // 限制单次最多处理 100 个 Key if (keyIds.length > 100) { return res.status(400).json({ success: false, error: 'Max 100 keys per request' }) } // 验证 custom 时间范围的参数 if (timeRange === 'custom') { if (!startDate || !endDate) { return res.status(400).json({ success: false, error: 'startDate and endDate are required for custom time range' }) } const start = new Date(startDate) const end = new Date(endDate) if (isNaN(start.getTime()) || isNaN(end.getTime())) { return res.status(400).json({ success: false, error: 'Invalid date format' }) } if (start > end) { return res.status(400).json({ success: false, error: 'startDate must be before or equal to endDate' }) } // 限制最大范围为 365 天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 if (daysDiff > 365) { return res.status(400).json({ success: false, error: 'Date range cannot exceed 365 days' }) } } logger.info( `📊 Batch stats request: ${keyIds.length} keys, timeRange=${timeRange}`, timeRange === 'custom' ? `, ${startDate} to ${endDate}` : '' ) const stats = {} // 并行计算每个 Key 的统计数据 await Promise.all( keyIds.map(async (keyId) => { try { stats[keyId] = await calculateKeyStats(keyId, timeRange, startDate, endDate) } catch (error) { logger.error(`❌ Failed to calculate stats for key ${keyId}:`, error) stats[keyId] = { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, cost: 0, formattedCost: '$0.00', dailyCost: 0, currentWindowCost: 0, windowRemainingSeconds: null, allTimeCost: 0, error: error.message } } }) ) return res.json({ success: true, data: stats }) } catch (error) { logger.error('❌ Failed to calculate batch stats:', error) return res.status(500).json({ success: false, error: 'Failed to calculate stats', message: error.message }) } }) /** * 计算单个 Key 的统计数据 * @param {string} keyId - API Key ID * @param {string} timeRange - 时间范围 * @param {string} startDate - 开始日期 (custom 模式) * @param {string} endDate - 结束日期 (custom 模式) * @returns {Object} 统计数据 */ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { const client = redis.getClientSafe() const tzDate = redis.getDateInTimezone() const today = redis.getDateStringInTimezone() // 构建搜索模式 const searchPatterns = [] if (timeRange === 'custom' && startDate && endDate) { // 自定义日期范围 const start = new Date(startDate) const end = new Date(endDate) for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { const dateStr = redis.getDateStringInTimezone(d) searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`) } } else if (timeRange === 'today') { searchPatterns.push(`usage:${keyId}:model:daily:*:${today}`) } else if (timeRange === '7days') { // 最近7天 for (let i = 0; i < 7; i++) { const d = new Date(tzDate) d.setDate(d.getDate() - i) const dateStr = redis.getDateStringInTimezone(d) searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`) } } else if (timeRange === 'monthly') { // 当月 const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` searchPatterns.push(`usage:${keyId}:model:monthly:*:${currentMonth}`) } else { // all - 获取所有数据(日和月数据都查) searchPatterns.push(`usage:${keyId}:model:daily:*`) searchPatterns.push(`usage:${keyId}:model:monthly:*`) } // 使用 SCAN 收集所有匹配的 keys const allKeys = [] for (const pattern of searchPatterns) { let cursor = '0' do { const [newCursor, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', 100) cursor = newCursor allKeys.push(...keys) } while (cursor !== '0') } // 去重(避免日数据和月数据重复计算) const uniqueKeys = [...new Set(allKeys)] // 获取实时限制数据(窗口数据不受时间范围筛选影响,始终获取当前窗口状态) let dailyCost = 0 let currentWindowCost = 0 let windowRemainingSeconds = null let windowStartTime = null let windowEndTime = null let allTimeCost = 0 try { // 先获取 API Key 配置,判断是否需要查询限制相关数据 const apiKey = await redis.getApiKey(keyId) const rateLimitWindow = parseInt(apiKey?.rateLimitWindow) || 0 const dailyCostLimit = parseFloat(apiKey?.dailyCostLimit) || 0 const totalCostLimit = parseFloat(apiKey?.totalCostLimit) || 0 // 只在启用了每日费用限制时查询 if (dailyCostLimit > 0) { dailyCost = await redis.getDailyCost(keyId) } // 只在启用了总费用限制时查询 if (totalCostLimit > 0) { const totalCostKey = `usage:cost:total:${keyId}` allTimeCost = parseFloat((await client.get(totalCostKey)) || '0') } // 只在启用了窗口限制时查询窗口数据 if (rateLimitWindow > 0) { const costCountKey = `rate_limit:cost:${keyId}` const windowStartKey = `rate_limit:window_start:${keyId}` currentWindowCost = parseFloat((await client.get(costCountKey)) || '0') // 获取窗口开始时间和计算剩余时间 const windowStart = await client.get(windowStartKey) if (windowStart) { const now = Date.now() windowStartTime = parseInt(windowStart) const windowDuration = rateLimitWindow * 60 * 1000 // 转换为毫秒 windowEndTime = windowStartTime + windowDuration // 如果窗口还有效 if (now < windowEndTime) { windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000)) } else { // 窗口已过期 windowRemainingSeconds = 0 currentWindowCost = 0 } } } } catch (error) { logger.warn(`⚠️ 获取实时限制数据失败 (key: ${keyId}):`, error.message) } // 如果没有使用数据,返回零值但包含窗口数据 if (uniqueKeys.length === 0) { return { requests: 0, tokens: 0, inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, cost: 0, formattedCost: '$0.00', // 实时限制数据(始终返回,不受时间范围影响) dailyCost, currentWindowCost, windowRemainingSeconds, windowStartTime, windowEndTime, allTimeCost } } // 使用 Pipeline 批量获取数据 const pipeline = client.pipeline() for (const key of uniqueKeys) { pipeline.hgetall(key) } const results = await pipeline.exec() // 汇总计算 const modelStatsMap = new Map() let totalRequests = 0 // 用于去重:只统计日数据,避免与月数据重复 const dailyKeyPattern = /usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/ const monthlyKeyPattern = /usage:.+:model:monthly:(.+):\d{4}-\d{2}$/ // 检查是否有日数据 const hasDailyData = uniqueKeys.some((key) => dailyKeyPattern.test(key)) for (let i = 0; i < results.length; i++) { const [err, data] = results[i] if (err || !data || Object.keys(data).length === 0) { continue } const key = uniqueKeys[i] let model = null let isMonthly = false // 提取模型名称 const dailyMatch = key.match(dailyKeyPattern) const monthlyMatch = key.match(monthlyKeyPattern) if (dailyMatch) { model = dailyMatch[1] } else if (monthlyMatch) { model = monthlyMatch[1] isMonthly = true } if (!model) { continue } // 如果有日数据,则跳过月数据以避免重复 if (hasDailyData && isMonthly) { continue } if (!modelStatsMap.has(model)) { modelStatsMap.set(model, { inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, requests: 0 }) } const stats = modelStatsMap.get(model) stats.inputTokens += parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0 stats.outputTokens += parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0 stats.cacheCreateTokens += parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0 stats.requests += parseInt(data.totalRequests) || parseInt(data.requests) || 0 totalRequests += parseInt(data.totalRequests) || parseInt(data.requests) || 0 } // 计算费用 let totalCost = 0 let inputTokens = 0 let outputTokens = 0 let cacheCreateTokens = 0 let cacheReadTokens = 0 for (const [model, stats] of modelStatsMap) { inputTokens += stats.inputTokens outputTokens += stats.outputTokens cacheCreateTokens += stats.cacheCreateTokens cacheReadTokens += stats.cacheReadTokens const costResult = CostCalculator.calculateCost( { input_tokens: stats.inputTokens, output_tokens: stats.outputTokens, cache_creation_input_tokens: stats.cacheCreateTokens, cache_read_input_tokens: stats.cacheReadTokens }, model ) totalCost += costResult.costs.total } const tokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens return { requests: totalRequests, tokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, cost: totalCost, formattedCost: CostCalculator.formatCost(totalCost), // 实时限制数据 dailyCost, currentWindowCost, windowRemainingSeconds, windowStartTime, windowEndTime, allTimeCost // 历史总费用(用于总费用限制) } } /** * 批量获取指定 Keys 的最后使用账号信息 * POST /admin/api-keys/batch-last-usage * * 用于 API Keys 列表页面异步加载最后使用账号数据 */ router.post('/api-keys/batch-last-usage', authenticateAdmin, async (req, res) => { try { const { keyIds } = req.body // 参数验证 if (!Array.isArray(keyIds) || keyIds.length === 0) { return res.status(400).json({ success: false, error: 'keyIds is required and must be a non-empty array' }) } // 限制单次最多处理 100 个 Key if (keyIds.length > 100) { return res.status(400).json({ success: false, error: 'Max 100 keys per request' }) } logger.debug(`📊 Batch last-usage request: ${keyIds.length} keys`) const client = redis.getClientSafe() const lastUsageData = {} const accountInfoCache = new Map() // 并行获取每个 Key 的最后使用记录 await Promise.all( keyIds.map(async (keyId) => { try { // 获取最新的使用记录 const usageRecords = await redis.getUsageRecords(keyId, 1) if (!Array.isArray(usageRecords) || usageRecords.length === 0) { lastUsageData[keyId] = null return } const lastUsageRecord = usageRecords[0] if (!lastUsageRecord || (!lastUsageRecord.accountId && !lastUsageRecord.accountType)) { lastUsageData[keyId] = null return } // 解析账号信息 const resolvedAccount = await apiKeyService._resolveAccountByUsageRecord( lastUsageRecord, accountInfoCache, client ) if (resolvedAccount) { lastUsageData[keyId] = { accountId: resolvedAccount.accountId, rawAccountId: lastUsageRecord.accountId || resolvedAccount.accountId, accountType: resolvedAccount.accountType, accountCategory: resolvedAccount.accountCategory, accountName: resolvedAccount.accountName, recordedAt: lastUsageRecord.timestamp || null } } else { // 账号已删除 lastUsageData[keyId] = { accountId: null, rawAccountId: lastUsageRecord.accountId || null, accountType: 'deleted', accountCategory: 'deleted', accountName: '已删除', recordedAt: lastUsageRecord.timestamp || null } } } catch (error) { logger.debug(`获取 API Key ${keyId} 的最后使用记录失败:`, error) lastUsageData[keyId] = null } }) ) return res.json({ success: true, data: lastUsageData }) } catch (error) { logger.error('❌ Failed to get batch last-usage:', error) return res.status(500).json({ success: false, error: 'Failed to get last-usage data', message: error.message }) } }) // 创建新的API Key router.post('/api-keys', authenticateAdmin, async (req, res) => { try { const { name, description, tokenLimit, expiresAt, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, droidAccountId, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, rateLimitCost, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, tags, activationDays, // 新增:激活后有效天数 activationUnit, // 新增:激活时间单位 (hours/days) expirationMode, // 新增:过期模式 icon // 新增:图标 } = req.body // 输入验证 if (!name || typeof name !== 'string' || name.trim().length === 0) { return res.status(400).json({ error: 'Name is required and must be a non-empty string' }) } if (name.length > 100) { return res.status(400).json({ error: 'Name must be less than 100 characters' }) } if (description && (typeof description !== 'string' || description.length > 500)) { return res .status(400) .json({ error: 'Description must be a string with less than 500 characters' }) } if (tokenLimit && (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0)) { return res.status(400).json({ error: 'Token limit must be a non-negative integer' }) } if ( concurrencyLimit !== undefined && concurrencyLimit !== null && concurrencyLimit !== '' && (!Number.isInteger(Number(concurrencyLimit)) || Number(concurrencyLimit) < 0) ) { return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' }) } if ( rateLimitWindow !== undefined && rateLimitWindow !== null && rateLimitWindow !== '' && (!Number.isInteger(Number(rateLimitWindow)) || Number(rateLimitWindow) < 1) ) { return res .status(400) .json({ error: 'Rate limit window must be a positive integer (minutes)' }) } if ( rateLimitRequests !== undefined && rateLimitRequests !== null && rateLimitRequests !== '' && (!Number.isInteger(Number(rateLimitRequests)) || Number(rateLimitRequests) < 1) ) { return res.status(400).json({ error: 'Rate limit requests must be a positive integer' }) } // 验证模型限制字段 if (enableModelRestriction !== undefined && typeof enableModelRestriction !== 'boolean') { return res.status(400).json({ error: 'Enable model restriction must be a boolean' }) } if (restrictedModels !== undefined && !Array.isArray(restrictedModels)) { return res.status(400).json({ error: 'Restricted models must be an array' }) } // 验证客户端限制字段 if (enableClientRestriction !== undefined && typeof enableClientRestriction !== 'boolean') { return res.status(400).json({ error: 'Enable client restriction must be a boolean' }) } if (allowedClients !== undefined && !Array.isArray(allowedClients)) { return res.status(400).json({ error: 'Allowed clients must be an array' }) } // 验证标签字段 if (tags !== undefined && !Array.isArray(tags)) { return res.status(400).json({ error: 'Tags must be an array' }) } if (tags && tags.some((tag) => typeof tag !== 'string' || tag.trim().length === 0)) { return res.status(400).json({ error: 'All tags must be non-empty strings' }) } if ( totalCostLimit !== undefined && totalCostLimit !== null && totalCostLimit !== '' && (Number.isNaN(Number(totalCostLimit)) || Number(totalCostLimit) < 0) ) { return res.status(400).json({ error: 'Total cost limit must be a non-negative number' }) } // 验证激活相关字段 if (expirationMode && !['fixed', 'activation'].includes(expirationMode)) { return res .status(400) .json({ error: 'Expiration mode must be either "fixed" or "activation"' }) } if (expirationMode === 'activation') { // 验证激活时间单位 if (!activationUnit || !['hours', 'days'].includes(activationUnit)) { return res.status(400).json({ error: 'Activation unit must be either "hours" or "days" when using activation mode' }) } // 验证激活时间数值 if ( !activationDays || !Number.isInteger(Number(activationDays)) || Number(activationDays) < 1 ) { const unitText = activationUnit === 'hours' ? 'hours' : 'days' return res.status(400).json({ error: `Activation ${unitText} must be a positive integer when using activation mode` }) } // 激活模式下不应该设置固定过期时间 if (expiresAt) { return res .status(400) .json({ error: 'Cannot set fixed expiration date when using activation mode' }) } } // 验证服务权限字段 if ( permissions !== undefined && permissions !== null && permissions !== '' && !['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions) ) { return res.status(400).json({ error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' }) } const newKey = await apiKeyService.generateApiKey({ name, description, tokenLimit, expiresAt, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, droidAccountId, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, rateLimitCost, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, tags, activationDays, activationUnit, expirationMode, icon }) logger.success(`🔑 Admin created new API key: ${name}`) return res.json({ success: true, data: newKey }) } catch (error) { logger.error('❌ Failed to create API key:', error) return res.status(500).json({ error: 'Failed to create API key', message: error.message }) } }) // 批量创建API Keys router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { try { const { baseName, count, description, tokenLimit, expiresAt, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, droidAccountId, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, rateLimitCost, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, tags, activationDays, activationUnit, expirationMode, icon } = req.body // 输入验证 if (!baseName || typeof baseName !== 'string' || baseName.trim().length === 0) { return res.status(400).json({ error: 'Base name is required and must be a non-empty string' }) } if (!count || !Number.isInteger(count) || count < 2 || count > 500) { return res.status(400).json({ error: 'Count must be an integer between 2 and 500' }) } if (baseName.length > 90) { return res .status(400) .json({ error: 'Base name must be less than 90 characters to allow for numbering' }) } if ( permissions !== undefined && permissions !== null && permissions !== '' && !['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions) ) { return res.status(400).json({ error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' }) } // 生成批量API Keys const createdKeys = [] const errors = [] for (let i = 1; i <= count; i++) { try { const name = `${baseName}_${i}` const newKey = await apiKeyService.generateApiKey({ name, description, tokenLimit, expiresAt, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, droidAccountId, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, rateLimitCost, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, tags, activationDays, activationUnit, expirationMode, icon }) // 保留原始 API Key 供返回 createdKeys.push({ ...newKey, apiKey: newKey.apiKey }) } catch (error) { errors.push({ index: i, name: `${baseName}_${i}`, error: error.message }) } } // 如果有部分失败,返回部分成功的结果 if (errors.length > 0 && createdKeys.length === 0) { return res.status(400).json({ success: false, error: 'Failed to create any API keys', errors }) } // 返回创建的keys(包含完整的apiKey) return res.json({ success: true, data: createdKeys, errors: errors.length > 0 ? errors : undefined, summary: { requested: count, created: createdKeys.length, failed: errors.length } }) } catch (error) { logger.error('Failed to batch create API keys:', error) return res.status(500).json({ success: false, error: 'Failed to batch create API keys', message: error.message }) } }) // 批量编辑API Keys router.put('/api-keys/batch', authenticateAdmin, async (req, res) => { try { const { keyIds, updates } = req.body if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) { return res.status(400).json({ error: 'Invalid input', message: 'keyIds must be a non-empty array' }) } if (!updates || typeof updates !== 'object') { return res.status(400).json({ error: 'Invalid input', message: 'updates must be an object' }) } if ( updates.permissions !== undefined && !['claude', 'gemini', 'openai', 'droid', 'all'].includes(updates.permissions) ) { return res.status(400).json({ error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' }) } logger.info( `🔄 Admin batch editing ${keyIds.length} API keys with updates: ${JSON.stringify(updates)}` ) logger.info(`🔍 Debug: keyIds received: ${JSON.stringify(keyIds)}`) const results = { successCount: 0, failedCount: 0, errors: [] } // 处理每个API Key for (const keyId of keyIds) { try { // 获取当前API Key信息 const currentKey = await redis.getApiKey(keyId) if (!currentKey || Object.keys(currentKey).length === 0) { results.failedCount++ results.errors.push(`API key ${keyId} not found`) continue } // 构建最终更新数据 const finalUpdates = {} // 处理普通字段 if (updates.name) { finalUpdates.name = updates.name } if (updates.tokenLimit !== undefined) { finalUpdates.tokenLimit = updates.tokenLimit } if (updates.rateLimitCost !== undefined) { finalUpdates.rateLimitCost = updates.rateLimitCost } if (updates.concurrencyLimit !== undefined) { finalUpdates.concurrencyLimit = updates.concurrencyLimit } if (updates.rateLimitWindow !== undefined) { finalUpdates.rateLimitWindow = updates.rateLimitWindow } if (updates.rateLimitRequests !== undefined) { finalUpdates.rateLimitRequests = updates.rateLimitRequests } if (updates.dailyCostLimit !== undefined) { finalUpdates.dailyCostLimit = updates.dailyCostLimit } if (updates.totalCostLimit !== undefined) { finalUpdates.totalCostLimit = updates.totalCostLimit } if (updates.weeklyOpusCostLimit !== undefined) { finalUpdates.weeklyOpusCostLimit = updates.weeklyOpusCostLimit } if (updates.permissions !== undefined) { finalUpdates.permissions = updates.permissions } if (updates.isActive !== undefined) { finalUpdates.isActive = updates.isActive } if (updates.monthlyLimit !== undefined) { finalUpdates.monthlyLimit = updates.monthlyLimit } if (updates.priority !== undefined) { finalUpdates.priority = updates.priority } if (updates.enabled !== undefined) { finalUpdates.enabled = updates.enabled } // 处理账户绑定 if (updates.claudeAccountId !== undefined) { finalUpdates.claudeAccountId = updates.claudeAccountId } if (updates.claudeConsoleAccountId !== undefined) { finalUpdates.claudeConsoleAccountId = updates.claudeConsoleAccountId } if (updates.geminiAccountId !== undefined) { finalUpdates.geminiAccountId = updates.geminiAccountId } if (updates.openaiAccountId !== undefined) { finalUpdates.openaiAccountId = updates.openaiAccountId } if (updates.bedrockAccountId !== undefined) { finalUpdates.bedrockAccountId = updates.bedrockAccountId } if (updates.droidAccountId !== undefined) { finalUpdates.droidAccountId = updates.droidAccountId || '' } // 处理标签操作 if (updates.tags !== undefined) { if (updates.tagOperation) { const currentTags = currentKey.tags ? JSON.parse(currentKey.tags) : [] const operationTags = updates.tags switch (updates.tagOperation) { case 'replace': { finalUpdates.tags = operationTags break } case 'add': { const newTags = [...currentTags] operationTags.forEach((tag) => { if (!newTags.includes(tag)) { newTags.push(tag) } }) finalUpdates.tags = newTags break } case 'remove': { finalUpdates.tags = currentTags.filter((tag) => !operationTags.includes(tag)) break } } } else { // 如果没有指定操作类型,默认为替换 finalUpdates.tags = updates.tags } } // 执行更新 await apiKeyService.updateApiKey(keyId, finalUpdates) results.successCount++ logger.success(`✅ Batch edit: API key ${keyId} updated successfully`) } catch (error) { results.failedCount++ results.errors.push(`Failed to update key ${keyId}: ${error.message}`) logger.error(`❌ Batch edit failed for key ${keyId}:`, error) } } // 记录批量编辑结果 if (results.successCount > 0) { logger.success( `🎉 Batch edit completed: ${results.successCount} successful, ${results.failedCount} failed` ) } else { logger.warn( `⚠️ Batch edit completed with no successful updates: ${results.failedCount} failed` ) } return res.json({ success: true, message: `批量编辑完成`, data: results }) } catch (error) { logger.error('❌ Failed to batch edit API keys:', error) return res.status(500).json({ error: 'Batch edit failed', message: error.message }) } }) // 更新API Key router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const { name, // 添加名称字段 tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, rateLimitCost, isActive, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, droidAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, tags, ownerId // 新增:所有者ID字段 } = req.body // 只允许更新指定字段 const updates = {} // 处理名称字段 if (name !== undefined && name !== null && name !== '') { const trimmedName = name.toString().trim() if (trimmedName.length === 0) { return res.status(400).json({ error: 'API Key name cannot be empty' }) } if (trimmedName.length > 100) { return res.status(400).json({ error: 'API Key name must be less than 100 characters' }) } updates.name = trimmedName } if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') { if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) { return res.status(400).json({ error: 'Token limit must be a non-negative integer' }) } updates.tokenLimit = Number(tokenLimit) } if (concurrencyLimit !== undefined && concurrencyLimit !== null && concurrencyLimit !== '') { if (!Number.isInteger(Number(concurrencyLimit)) || Number(concurrencyLimit) < 0) { return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' }) } updates.concurrencyLimit = Number(concurrencyLimit) } if (rateLimitWindow !== undefined && rateLimitWindow !== null && rateLimitWindow !== '') { if (!Number.isInteger(Number(rateLimitWindow)) || Number(rateLimitWindow) < 0) { return res .status(400) .json({ error: 'Rate limit window must be a non-negative integer (minutes)' }) } updates.rateLimitWindow = Number(rateLimitWindow) } if (rateLimitRequests !== undefined && rateLimitRequests !== null && rateLimitRequests !== '') { if (!Number.isInteger(Number(rateLimitRequests)) || Number(rateLimitRequests) < 0) { return res.status(400).json({ error: 'Rate limit requests must be a non-negative integer' }) } updates.rateLimitRequests = Number(rateLimitRequests) } if (rateLimitCost !== undefined && rateLimitCost !== null && rateLimitCost !== '') { const cost = Number(rateLimitCost) if (isNaN(cost) || cost < 0) { return res.status(400).json({ error: 'Rate limit cost must be a non-negative number' }) } updates.rateLimitCost = cost } if (claudeAccountId !== undefined) { // 空字符串表示解绑,null或空字符串都设置为空字符串 updates.claudeAccountId = claudeAccountId || '' } if (claudeConsoleAccountId !== undefined) { // 空字符串表示解绑,null或空字符串都设置为空字符串 updates.claudeConsoleAccountId = claudeConsoleAccountId || '' } if (geminiAccountId !== undefined) { // 空字符串表示解绑,null或空字符串都设置为空字符串 updates.geminiAccountId = geminiAccountId || '' } if (openaiAccountId !== undefined) { // 空字符串表示解绑,null或空字符串都设置为空字符串 updates.openaiAccountId = openaiAccountId || '' } if (bedrockAccountId !== undefined) { // 空字符串表示解绑,null或空字符串都设置为空字符串 updates.bedrockAccountId = bedrockAccountId || '' } if (droidAccountId !== undefined) { // 空字符串表示解绑,null或空字符串都设置为空字符串 updates.droidAccountId = droidAccountId || '' } if (permissions !== undefined) { // 验证权限值 if (!['claude', 'gemini', 'openai', 'droid', 'all'].includes(permissions)) { return res.status(400).json({ error: 'Invalid permissions value. Must be claude, gemini, openai, droid, or all' }) } updates.permissions = permissions } // 处理模型限制字段 if (enableModelRestriction !== undefined) { if (typeof enableModelRestriction !== 'boolean') { return res.status(400).json({ error: 'Enable model restriction must be a boolean' }) } updates.enableModelRestriction = enableModelRestriction } if (restrictedModels !== undefined) { if (!Array.isArray(restrictedModels)) { return res.status(400).json({ error: 'Restricted models must be an array' }) } updates.restrictedModels = restrictedModels } // 处理客户端限制字段 if (enableClientRestriction !== undefined) { if (typeof enableClientRestriction !== 'boolean') { return res.status(400).json({ error: 'Enable client restriction must be a boolean' }) } updates.enableClientRestriction = enableClientRestriction } if (allowedClients !== undefined) { if (!Array.isArray(allowedClients)) { return res.status(400).json({ error: 'Allowed clients must be an array' }) } updates.allowedClients = allowedClients } // 处理过期时间字段 if (expiresAt !== undefined) { if (expiresAt === null) { // null 表示永不过期 updates.expiresAt = null updates.isActive = true } else { // 验证日期格式 const expireDate = new Date(expiresAt) if (isNaN(expireDate.getTime())) { return res.status(400).json({ error: 'Invalid expiration date format' }) } updates.expiresAt = expiresAt updates.isActive = expireDate > new Date() // 如果过期时间在当前时间之后,则设置为激活状态 } } // 处理每日费用限制 if (dailyCostLimit !== undefined && dailyCostLimit !== null && dailyCostLimit !== '') { const costLimit = Number(dailyCostLimit) if (isNaN(costLimit) || costLimit < 0) { return res.status(400).json({ error: 'Daily cost limit must be a non-negative number' }) } updates.dailyCostLimit = costLimit } if (totalCostLimit !== undefined && totalCostLimit !== null && totalCostLimit !== '') { const costLimit = Number(totalCostLimit) if (isNaN(costLimit) || costLimit < 0) { return res.status(400).json({ error: 'Total cost limit must be a non-negative number' }) } updates.totalCostLimit = costLimit } // 处理 Opus 周费用限制 if ( weeklyOpusCostLimit !== undefined && weeklyOpusCostLimit !== null && weeklyOpusCostLimit !== '' ) { const costLimit = Number(weeklyOpusCostLimit) // 明确验证非负数(0 表示禁用,负数无意义) if (isNaN(costLimit) || costLimit < 0) { return res .status(400) .json({ error: 'Weekly Opus cost limit must be a non-negative number' }) } updates.weeklyOpusCostLimit = costLimit } // 处理标签 if (tags !== undefined) { if (!Array.isArray(tags)) { return res.status(400).json({ error: 'Tags must be an array' }) } if (tags.some((tag) => typeof tag !== 'string' || tag.trim().length === 0)) { return res.status(400).json({ error: 'All tags must be non-empty strings' }) } updates.tags = tags } // 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能 if (isActive !== undefined) { if (typeof isActive !== 'boolean') { return res.status(400).json({ error: 'isActive must be a boolean' }) } updates.isActive = isActive } // 处理所有者变更 if (ownerId !== undefined) { const userService = require('../../services/userService') if (ownerId === 'admin') { // 分配给Admin updates.userId = '' updates.userUsername = '' updates.createdBy = 'admin' } else if (ownerId) { // 分配给用户 try { const user = await userService.getUserById(ownerId, false) if (!user) { return res.status(400).json({ error: 'Invalid owner: User not found' }) } if (!user.isActive) { return res.status(400).json({ error: 'Cannot assign to inactive user' }) } // 设置新的所有者信息 updates.userId = ownerId updates.userUsername = user.username updates.createdBy = user.username // 管理员重新分配时,不检查用户的API Key数量限制 logger.info(`🔄 Admin reassigning API key ${keyId} to user ${user.username}`) } catch (error) { logger.error('Error fetching user for owner reassignment:', error) return res.status(400).json({ error: 'Invalid owner ID' }) } } else { // 清空所有者(分配给Admin) updates.userId = '' updates.userUsername = '' updates.createdBy = 'admin' } } await apiKeyService.updateApiKey(keyId, updates) logger.success(`📝 Admin updated API key: ${keyId}`) return res.json({ success: true, message: 'API key updated successfully' }) } catch (error) { logger.error('❌ Failed to update API key:', error) return res.status(500).json({ error: 'Failed to update API key', message: error.message }) } }) // 修改API Key过期时间(包括手动激活功能) router.patch('/api-keys/:keyId/expiration', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const { expiresAt, activateNow } = req.body // 获取当前API Key信息 const keyData = await redis.getApiKey(keyId) if (!keyData || Object.keys(keyData).length === 0) { return res.status(404).json({ error: 'API key not found' }) } const updates = {} // 如果是激活操作(用于未激活的key) if (activateNow === true) { if (keyData.expirationMode === 'activation' && keyData.isActivated !== 'true') { const now = new Date() const activationDays = parseInt(keyData.activationDays || 30) const newExpiresAt = new Date(now.getTime() + activationDays * 24 * 60 * 60 * 1000) updates.isActivated = 'true' updates.activatedAt = now.toISOString() updates.expiresAt = newExpiresAt.toISOString() logger.success( `🔓 API key manually activated by admin: ${keyId} (${ keyData.name }), expires at ${newExpiresAt.toISOString()}` ) } else { return res.status(400).json({ error: 'Cannot activate', message: 'Key is either already activated or not in activation mode' }) } } // 如果提供了新的过期时间(但不是激活操作) if (expiresAt !== undefined && activateNow !== true) { // 验证过期时间格式 if (expiresAt && isNaN(Date.parse(expiresAt))) { return res.status(400).json({ error: 'Invalid expiration date format' }) } // 如果设置了过期时间,确保key是激活状态 if (expiresAt) { updates.expiresAt = new Date(expiresAt).toISOString() // 如果之前是未激活状态,现在激活它 if (keyData.isActivated !== 'true') { updates.isActivated = 'true' updates.activatedAt = new Date().toISOString() } } else { // 清除过期时间(永不过期) updates.expiresAt = '' } } if (Object.keys(updates).length === 0) { return res.status(400).json({ error: 'No valid updates provided' }) } // 更新API Key await apiKeyService.updateApiKey(keyId, updates) logger.success(`📝 Updated API key expiration: ${keyId} (${keyData.name})`) return res.json({ success: true, message: 'API key expiration updated successfully', updates }) } catch (error) { logger.error('❌ Failed to update API key expiration:', error) return res.status(500).json({ error: 'Failed to update API key expiration', message: error.message }) } }) // 批量删除API Keys(必须在 :keyId 路由之前定义) router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => { try { const { keyIds } = req.body // 调试信息 logger.info(`🐛 Batch delete request body: ${JSON.stringify(req.body)}`) logger.info(`🐛 keyIds type: ${typeof keyIds}, value: ${JSON.stringify(keyIds)}`) // 参数验证 if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) { logger.warn( `🚨 Invalid keyIds: ${JSON.stringify({ keyIds, type: typeof keyIds, isArray: Array.isArray(keyIds) })}` ) return res.status(400).json({ error: 'Invalid request', message: 'keyIds 必须是一个非空数组' }) } if (keyIds.length > 100) { return res.status(400).json({ error: 'Too many keys', message: '每次最多只能删除100个API Keys' }) } // 验证keyIds格式 const invalidKeys = keyIds.filter((id) => !id || typeof id !== 'string') if (invalidKeys.length > 0) { return res.status(400).json({ error: 'Invalid key IDs', message: '包含无效的API Key ID' }) } logger.info( `🗑️ Admin attempting batch delete of ${keyIds.length} API keys: ${JSON.stringify(keyIds)}` ) const results = { successCount: 0, failedCount: 0, errors: [] } // 逐个删除,记录成功和失败情况 for (const keyId of keyIds) { try { // 检查API Key是否存在 const apiKey = await redis.getApiKey(keyId) if (!apiKey || Object.keys(apiKey).length === 0) { results.failedCount++ results.errors.push({ keyId, error: 'API Key 不存在' }) continue } // 执行删除 await apiKeyService.deleteApiKey(keyId) results.successCount++ logger.success(`✅ Batch delete: API key ${keyId} deleted successfully`) } catch (error) { results.failedCount++ results.errors.push({ keyId, error: error.message || '删除失败' }) logger.error(`❌ Batch delete failed for key ${keyId}:`, error) } } // 记录批量删除结果 if (results.successCount > 0) { logger.success( `🎉 Batch delete completed: ${results.successCount} successful, ${results.failedCount} failed` ) } else { logger.warn( `⚠️ Batch delete completed with no successful deletions: ${results.failedCount} failed` ) } return res.json({ success: true, message: `批量删除完成`, data: results }) } catch (error) { logger.error('❌ Failed to batch delete API keys:', error) return res.status(500).json({ error: 'Batch delete failed', message: error.message }) } }) // 删除单个API Key(必须在批量删除路由之后定义) router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params await apiKeyService.deleteApiKey(keyId, req.admin.username, 'admin') logger.success(`🗑️ Admin deleted API key: ${keyId}`) return res.json({ success: true, message: 'API key deleted successfully' }) } catch (error) { logger.error('❌ Failed to delete API key:', error) return res.status(500).json({ error: 'Failed to delete API key', message: error.message }) } }) // 📋 获取已删除的API Keys router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => { try { const deletedApiKeys = await apiKeyService.getAllApiKeys(true) // Include deleted const onlyDeleted = deletedApiKeys.filter((key) => key.isDeleted === 'true') // Add additional metadata for deleted keys const enrichedKeys = onlyDeleted.map((key) => ({ ...key, isDeleted: key.isDeleted === 'true', deletedAt: key.deletedAt, deletedBy: key.deletedBy, deletedByType: key.deletedByType, canRestore: true // 已删除的API Key可以恢复 })) logger.success(`📋 Admin retrieved ${enrichedKeys.length} deleted API keys`) return res.json({ success: true, apiKeys: enrichedKeys, total: enrichedKeys.length }) } catch (error) { logger.error('❌ Failed to get deleted API keys:', error) return res .status(500) .json({ error: 'Failed to retrieve deleted API keys', message: error.message }) } }) // 🔄 恢复已删除的API Key router.post('/api-keys/:keyId/restore', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const adminUsername = req.session?.admin?.username || 'unknown' // 调用服务层的恢复方法 const result = await apiKeyService.restoreApiKey(keyId, adminUsername, 'admin') if (result.success) { logger.success(`✅ Admin ${adminUsername} restored API key: ${keyId}`) return res.json({ success: true, message: 'API Key 已成功恢复', apiKey: result.apiKey }) } else { return res.status(400).json({ success: false, error: 'Failed to restore API key' }) } } catch (error) { logger.error('❌ Failed to restore API key:', error) // 根据错误类型返回适当的响应 if (error.message === 'API key not found') { return res.status(404).json({ success: false, error: 'API Key 不存在' }) } else if (error.message === 'API key is not deleted') { return res.status(400).json({ success: false, error: '该 API Key 未被删除,无需恢复' }) } return res.status(500).json({ success: false, error: '恢复 API Key 失败', message: error.message }) } }) // 🗑️ 彻底删除API Key(物理删除) router.delete('/api-keys/:keyId/permanent', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const adminUsername = req.session?.admin?.username || 'unknown' // 调用服务层的彻底删除方法 const result = await apiKeyService.permanentDeleteApiKey(keyId) if (result.success) { logger.success(`🗑️ Admin ${adminUsername} permanently deleted API key: ${keyId}`) return res.json({ success: true, message: 'API Key 已彻底删除' }) } } catch (error) { logger.error('❌ Failed to permanently delete API key:', error) if (error.message === 'API key not found') { return res.status(404).json({ success: false, error: 'API Key 不存在' }) } else if (error.message === '只能彻底删除已经删除的API Key') { return res.status(400).json({ success: false, error: '只能彻底删除已经删除的API Key' }) } return res.status(500).json({ success: false, error: '彻底删除 API Key 失败', message: error.message }) } }) // 🧹 清空所有已删除的API Keys router.delete('/api-keys/deleted/clear-all', authenticateAdmin, async (req, res) => { try { const adminUsername = req.session?.admin?.username || 'unknown' // 调用服务层的清空方法 const result = await apiKeyService.clearAllDeletedApiKeys() logger.success( `🧹 Admin ${adminUsername} cleared deleted API keys: ${result.successCount}/${result.total}` ) return res.json({ success: true, message: `成功清空 ${result.successCount} 个已删除的 API Keys`, details: { total: result.total, successCount: result.successCount, failedCount: result.failedCount, errors: result.errors } }) } catch (error) { logger.error('❌ Failed to clear all deleted API keys:', error) return res.status(500).json({ success: false, error: '清空已删除的 API Keys 失败', message: error.message }) } }) module.exports = router