const express = require('express') const apiKeyService = require('../services/apiKeyService') const claudeAccountService = require('../services/claudeAccountService') const claudeConsoleAccountService = require('../services/claudeConsoleAccountService') const bedrockAccountService = require('../services/bedrockAccountService') const geminiAccountService = require('../services/geminiAccountService') const openaiAccountService = require('../services/openaiAccountService') const azureOpenaiAccountService = require('../services/azureOpenaiAccountService') const accountGroupService = require('../services/accountGroupService') const redis = require('../models/redis') const { authenticateAdmin } = require('../middleware/auth') const logger = require('../utils/logger') const oauthHelper = require('../utils/oauthHelper') const CostCalculator = require('../utils/costCalculator') const pricingService = require('../services/pricingService') const claudeCodeHeadersService = require('../services/claudeCodeHeadersService') const webhookNotifier = require('../utils/webhookNotifier') const axios = require('axios') const crypto = require('crypto') const fs = require('fs') const path = require('path') const config = require('../../config/config') const ProxyHelper = require('../utils/proxyHelper') const router = express.Router() // 🔑 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 }) } }) // 获取所有API Keys router.get('/api-keys', authenticateAdmin, async (req, res) => { try { const { timeRange = 'all' } = req.query // all, 7days, monthly const apiKeys = await apiKeyService.getAllApiKeys() // 根据时间范围计算查询模式 const now = new Date() const searchPatterns = [] if (timeRange === 'today') { // 今日 - 使用时区日期 const redisClient = require('../models/redis') const tzDate = redisClient.getDateInTimezone(now) const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}` searchPatterns.push(`usage:daily:*:${dateStr}`) } else if (timeRange === '7days') { // 最近7天 const redisClient = require('../models/redis') for (let i = 0; i < 7; i++) { const date = new Date(now) date.setDate(date.getDate() - i) const tzDate = redisClient.getDateInTimezone(date) const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}` searchPatterns.push(`usage:daily:*:${dateStr}`) } } else if (timeRange === 'monthly') { // 本月 const redisClient = require('../models/redis') const tzDate = redisClient.getDateInTimezone(now) const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` searchPatterns.push(`usage:monthly:*:${currentMonth}`) } // 为每个API Key计算准确的费用和统计数据 for (const apiKey of apiKeys) { const client = redis.getClientSafe() if (timeRange === 'all') { // 全部时间:保持原有逻辑 if (apiKey.usage && apiKey.usage.total) { // 使用与展开模型统计相同的数据源 // 获取所有时间的模型统计数据 const monthlyKeys = await client.keys(`usage:${apiKey.id}:model:monthly:*:*`) const modelStatsMap = new Map() // 汇总所有月份的数据 for (const key of monthlyKeys) { const match = key.match(/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 (!modelStatsMap.has(model)) { modelStatsMap.set(model, { inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 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 } } let totalCost = 0 // 计算每个模型的费用 for (const [model, stats] of modelStatsMap) { const usage = { input_tokens: stats.inputTokens, output_tokens: stats.outputTokens, cache_creation_input_tokens: stats.cacheCreateTokens, cache_read_input_tokens: stats.cacheReadTokens } const costResult = CostCalculator.calculateCost(usage, model) totalCost += costResult.costs.total } // 如果没有详细的模型数据,使用总量数据和默认模型计算 if (modelStatsMap.size === 0) { const usage = { input_tokens: apiKey.usage.total.inputTokens || 0, output_tokens: apiKey.usage.total.outputTokens || 0, cache_creation_input_tokens: apiKey.usage.total.cacheCreateTokens || 0, cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0 } const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022') totalCost = costResult.costs.total } // 添加格式化的费用到响应数据 apiKey.usage.total.cost = totalCost apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost) } } else { // 7天或本月:重新计算统计数据 const tempUsage = { requests: 0, tokens: 0, allTokens: 0, // 添加allTokens字段 inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0 } // 获取指定时间范围的统计数据 for (const pattern of searchPatterns) { const keys = await client.keys(pattern.replace('*', apiKey.id)) for (const key of keys) { const data = await client.hgetall(key) if (data && Object.keys(data).length > 0) { // 使用与 redis.js incrementTokenUsage 中相同的字段名 tempUsage.requests += parseInt(data.totalRequests) || parseInt(data.requests) || 0 tempUsage.tokens += parseInt(data.totalTokens) || parseInt(data.tokens) || 0 tempUsage.allTokens += parseInt(data.totalAllTokens) || parseInt(data.allTokens) || 0 // 读取包含所有Token的字段 tempUsage.inputTokens += parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0 tempUsage.outputTokens += parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0 tempUsage.cacheCreateTokens += parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0 tempUsage.cacheReadTokens += parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0 } } } // 计算指定时间范围的费用 let totalCost = 0 const redisClient = require('../models/redis') const tzToday = redisClient.getDateStringInTimezone(now) const tzDate = redisClient.getDateInTimezone(now) const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` const modelKeys = timeRange === 'today' ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) : timeRange === '7days' ? await client.keys(`usage:${apiKey.id}:model:daily:*:*`) : await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`) const modelStatsMap = new Map() // 过滤和汇总相应时间范围的模型数据 for (const key of modelKeys) { if (timeRange === '7days') { // 检查是否在最近7天内 const dateMatch = key.match(/\d{4}-\d{2}-\d{2}$/) if (dateMatch) { const keyDate = new Date(dateMatch[0]) const daysDiff = Math.floor((now - keyDate) / (1000 * 60 * 60 * 24)) if (daysDiff > 6) { continue } } } else if (timeRange === 'today') { // today选项已经在查询时过滤了,不需要额外处理 } const modelMatch = key.match( /usage:.+:model:(?:daily|monthly):(.+):\d{4}-\d{2}(?:-\d{2})?$/ ) if (!modelMatch) { continue } const model = modelMatch[1] const data = await client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!modelStatsMap.has(model)) { modelStatsMap.set(model, { inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 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 } } // 计算费用 for (const [model, stats] of modelStatsMap) { const usage = { input_tokens: stats.inputTokens, output_tokens: stats.outputTokens, cache_creation_input_tokens: stats.cacheCreateTokens, cache_read_input_tokens: stats.cacheReadTokens } const costResult = CostCalculator.calculateCost(usage, model) totalCost += costResult.costs.total } // 如果没有模型数据,使用临时统计数据计算 if (modelStatsMap.size === 0 && tempUsage.tokens > 0) { const usage = { input_tokens: tempUsage.inputTokens, output_tokens: tempUsage.outputTokens, cache_creation_input_tokens: tempUsage.cacheCreateTokens, cache_read_input_tokens: tempUsage.cacheReadTokens } const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022') totalCost = costResult.costs.total } // 使用从Redis读取的allTokens,如果没有则计算 const allTokens = tempUsage.allTokens || tempUsage.inputTokens + tempUsage.outputTokens + tempUsage.cacheCreateTokens + tempUsage.cacheReadTokens // 更新API Key的usage数据为指定时间范围的数据 apiKey.usage[timeRange] = { ...tempUsage, tokens: allTokens, // 使用包含所有Token的总数 allTokens, cost: totalCost, formattedCost: CostCalculator.formatCost(totalCost) } // 为了保持兼容性,也更新total字段 apiKey.usage.total = apiKey.usage[timeRange] } } return res.json({ success: true, data: apiKeys }) } catch (error) { logger.error('❌ Failed to get API keys:', error) return res.status(500).json({ error: 'Failed to get API keys', message: error.message }) } }) // 获取支持的客户端列表 router.get('/supported-clients', authenticateAdmin, async (req, res) => { try { // 检查配置是否存在,如果不存在则使用默认值 const predefinedClients = config.clientRestrictions?.predefinedClients || [ { id: 'claude_code', name: 'ClaudeCode', description: 'Official Claude Code CLI' }, { id: 'gemini_cli', name: 'Gemini-CLI', description: 'Gemini Command Line Interface' } ] const clients = predefinedClients.map((client) => ({ id: client.id, name: client.name, description: client.description })) 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 router.post('/api-keys', authenticateAdmin, async (req, res) => { try { const { name, description, tokenLimit, expiresAt, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, tags } = 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' }) } const newKey = await apiKeyService.generateApiKey({ name, description, tokenLimit, expiresAt, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, tags }) 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, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, tags } = 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' }) } // 生成批量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, permissions, concurrencyLimit, rateLimitWindow, rateLimitRequests, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, dailyCostLimit, tags }) // 保留原始 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' }) } 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.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.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.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 { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, isActive, claudeAccountId, claudeConsoleAccountId, geminiAccountId, openaiAccountId, bedrockAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt, dailyCostLimit, tags } = req.body // 只允许更新指定字段 const updates = {} 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 (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 (permissions !== undefined) { // 验证权限值 if (!['claude', 'gemini', 'openai', 'all'].includes(permissions)) { return res .status(400) .json({ error: 'Invalid permissions value. Must be claude, gemini, openai, 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 (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 } 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 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) 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 }) } }) // 👥 账户分组管理 // 创建账户分组 router.post('/account-groups', authenticateAdmin, async (req, res) => { try { const { name, platform, description } = req.body const group = await accountGroupService.createGroup({ name, platform, description }) return res.json({ success: true, data: group }) } catch (error) { logger.error('❌ Failed to create account group:', error) return res.status(400).json({ error: error.message }) } }) // 获取所有分组 router.get('/account-groups', authenticateAdmin, async (req, res) => { try { const { platform } = req.query const groups = await accountGroupService.getAllGroups(platform) return res.json({ success: true, data: groups }) } catch (error) { logger.error('❌ Failed to get account groups:', error) return res.status(500).json({ error: error.message }) } }) // 获取分组详情 router.get('/account-groups/:groupId', authenticateAdmin, async (req, res) => { try { const { groupId } = req.params const group = await accountGroupService.getGroup(groupId) if (!group) { return res.status(404).json({ error: '分组不存在' }) } return res.json({ success: true, data: group }) } catch (error) { logger.error('❌ Failed to get account group:', error) return res.status(500).json({ error: error.message }) } }) // 更新分组 router.put('/account-groups/:groupId', authenticateAdmin, async (req, res) => { try { const { groupId } = req.params const updates = req.body const updatedGroup = await accountGroupService.updateGroup(groupId, updates) return res.json({ success: true, data: updatedGroup }) } catch (error) { logger.error('❌ Failed to update account group:', error) return res.status(400).json({ error: error.message }) } }) // 删除分组 router.delete('/account-groups/:groupId', authenticateAdmin, async (req, res) => { try { const { groupId } = req.params await accountGroupService.deleteGroup(groupId) return res.json({ success: true, message: '分组删除成功' }) } catch (error) { logger.error('❌ Failed to delete account group:', error) return res.status(400).json({ error: error.message }) } }) // 获取分组成员 router.get('/account-groups/:groupId/members', authenticateAdmin, async (req, res) => { try { const { groupId } = req.params const memberIds = await accountGroupService.getGroupMembers(groupId) // 获取成员详细信息 const members = [] for (const memberId of memberIds) { // 尝试从不同的服务获取账户信息 let account = null // 先尝试Claude OAuth账户 account = await claudeAccountService.getAccount(memberId) // 如果找不到,尝试Claude Console账户 if (!account) { account = await claudeConsoleAccountService.getAccount(memberId) } // 如果还找不到,尝试Gemini账户 if (!account) { account = await geminiAccountService.getAccount(memberId) } // 如果还找不到,尝试OpenAI账户 if (!account) { account = await openaiAccountService.getAccount(memberId) } if (account) { members.push(account) } } return res.json({ success: true, data: members }) } catch (error) { logger.error('❌ Failed to get group members:', error) return res.status(500).json({ error: error.message }) } }) // 🏢 Claude 账户管理 // 生成OAuth授权URL router.post('/claude-accounts/generate-auth-url', authenticateAdmin, async (req, res) => { try { const { proxy } = req.body // 接收代理配置 const oauthParams = await oauthHelper.generateOAuthParams() // 将codeVerifier和state临时存储到Redis,用于后续验证 const sessionId = require('crypto').randomUUID() await redis.setOAuthSession(sessionId, { codeVerifier: oauthParams.codeVerifier, state: oauthParams.state, codeChallenge: oauthParams.codeChallenge, proxy: proxy || null, // 存储代理配置 createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString() // 10分钟过期 }) logger.success('🔗 Generated OAuth authorization URL with proxy support') return res.json({ success: true, data: { authUrl: oauthParams.authUrl, sessionId, instructions: [ '1. 复制上面的链接到浏览器中打开', '2. 登录您的 Anthropic 账户', '3. 同意应用权限', '4. 复制浏览器地址栏中的完整 URL', '5. 在添加账户表单中粘贴完整的回调 URL 和授权码' ] } }) } catch (error) { logger.error('❌ Failed to generate OAuth URL:', error) return res.status(500).json({ error: 'Failed to generate OAuth URL', message: error.message }) } }) // 验证授权码并获取token router.post('/claude-accounts/exchange-code', authenticateAdmin, async (req, res) => { try { const { sessionId, authorizationCode, callbackUrl } = req.body if (!sessionId || (!authorizationCode && !callbackUrl)) { return res .status(400) .json({ error: 'Session ID and authorization code (or callback URL) are required' }) } // 从Redis获取OAuth会话信息 const oauthSession = await redis.getOAuthSession(sessionId) if (!oauthSession) { return res.status(400).json({ error: 'Invalid or expired OAuth session' }) } // 检查会话是否过期 if (new Date() > new Date(oauthSession.expiresAt)) { await redis.deleteOAuthSession(sessionId) return res .status(400) .json({ error: 'OAuth session has expired, please generate a new authorization URL' }) } // 统一处理授权码输入(可能是直接的code或完整的回调URL) let finalAuthCode const inputValue = callbackUrl || authorizationCode try { finalAuthCode = oauthHelper.parseCallbackUrl(inputValue) } catch (parseError) { return res .status(400) .json({ error: 'Failed to parse authorization input', message: parseError.message }) } // 交换访问令牌 const tokenData = await oauthHelper.exchangeCodeForTokens( finalAuthCode, oauthSession.codeVerifier, oauthSession.state, oauthSession.proxy // 传递代理配置 ) // 清理OAuth会话 await redis.deleteOAuthSession(sessionId) logger.success('🎉 Successfully exchanged authorization code for tokens') return res.json({ success: true, data: { claudeAiOauth: tokenData } }) } catch (error) { logger.error('❌ Failed to exchange authorization code:', { error: error.message, sessionId: req.body.sessionId, // 不记录完整的授权码,只记录长度和前几个字符 codeLength: req.body.callbackUrl ? req.body.callbackUrl.length : req.body.authorizationCode ? req.body.authorizationCode.length : 0, codePrefix: req.body.callbackUrl ? `${req.body.callbackUrl.substring(0, 10)}...` : req.body.authorizationCode ? `${req.body.authorizationCode.substring(0, 10)}...` : 'N/A' }) return res .status(500) .json({ error: 'Failed to exchange authorization code', message: error.message }) } }) // 生成Claude setup-token授权URL router.post('/claude-accounts/generate-setup-token-url', authenticateAdmin, async (req, res) => { try { const { proxy } = req.body // 接收代理配置 const setupTokenParams = await oauthHelper.generateSetupTokenParams() // 将codeVerifier和state临时存储到Redis,用于后续验证 const sessionId = require('crypto').randomUUID() await redis.setOAuthSession(sessionId, { type: 'setup-token', // 标记为setup-token类型 codeVerifier: setupTokenParams.codeVerifier, state: setupTokenParams.state, codeChallenge: setupTokenParams.codeChallenge, proxy: proxy || null, // 存储代理配置 createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString() // 10分钟过期 }) logger.success('🔗 Generated Setup Token authorization URL with proxy support') return res.json({ success: true, data: { authUrl: setupTokenParams.authUrl, sessionId, instructions: [ '1. 复制上面的链接到浏览器中打开', '2. 登录您的 Claude 账户并授权 Claude Code', '3. 完成授权后,从返回页面复制 Authorization Code', '4. 在添加账户表单中粘贴 Authorization Code' ] } }) } catch (error) { logger.error('❌ Failed to generate Setup Token URL:', error) return res .status(500) .json({ error: 'Failed to generate Setup Token URL', message: error.message }) } }) // 验证setup-token授权码并获取token router.post('/claude-accounts/exchange-setup-token-code', authenticateAdmin, async (req, res) => { try { const { sessionId, authorizationCode, callbackUrl } = req.body if (!sessionId || (!authorizationCode && !callbackUrl)) { return res .status(400) .json({ error: 'Session ID and authorization code (or callback URL) are required' }) } // 从Redis获取OAuth会话信息 const oauthSession = await redis.getOAuthSession(sessionId) if (!oauthSession) { return res.status(400).json({ error: 'Invalid or expired OAuth session' }) } // 检查是否是setup-token类型 if (oauthSession.type !== 'setup-token') { return res.status(400).json({ error: 'Invalid session type for setup token exchange' }) } // 检查会话是否过期 if (new Date() > new Date(oauthSession.expiresAt)) { await redis.deleteOAuthSession(sessionId) return res .status(400) .json({ error: 'OAuth session has expired, please generate a new authorization URL' }) } // 统一处理授权码输入(可能是直接的code或完整的回调URL) let finalAuthCode const inputValue = callbackUrl || authorizationCode try { finalAuthCode = oauthHelper.parseCallbackUrl(inputValue) } catch (parseError) { return res .status(400) .json({ error: 'Failed to parse authorization input', message: parseError.message }) } // 交换Setup Token const tokenData = await oauthHelper.exchangeSetupTokenCode( finalAuthCode, oauthSession.codeVerifier, oauthSession.state, oauthSession.proxy // 传递代理配置 ) // 清理OAuth会话 await redis.deleteOAuthSession(sessionId) logger.success('🎉 Successfully exchanged setup token authorization code for tokens') return res.json({ success: true, data: { claudeAiOauth: tokenData } }) } catch (error) { logger.error('❌ Failed to exchange setup token authorization code:', { error: error.message, sessionId: req.body.sessionId, // 不记录完整的授权码,只记录长度和前几个字符 codeLength: req.body.callbackUrl ? req.body.callbackUrl.length : req.body.authorizationCode ? req.body.authorizationCode.length : 0, codePrefix: req.body.callbackUrl ? `${req.body.callbackUrl.substring(0, 10)}...` : req.body.authorizationCode ? `${req.body.authorizationCode.substring(0, 10)}...` : 'N/A' }) return res .status(500) .json({ error: 'Failed to exchange setup token authorization code', message: error.message }) } }) // 获取所有Claude账户 router.get('/claude-accounts', authenticateAdmin, async (req, res) => { try { const { platform, groupId } = req.query let accounts = await claudeAccountService.getAllAccounts() // 根据查询参数进行筛选 if (platform && platform !== 'all' && platform !== 'claude') { // 如果指定了其他平台,返回空数组 accounts = [] } // 如果指定了分组筛选 if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 accounts = accounts.filter( (account) => !account.groupInfos || account.groupInfos.length === 0 ) } else { // 筛选特定分组的账户 accounts = accounts.filter( (account) => account.groupInfos && account.groupInfos.some((group) => group.id === groupId) ) } } // 为每个账户添加使用统计信息 const accountsWithStats = await Promise.all( accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, 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) // 如果获取统计失败,返回空统计 try { const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } catch (groupError) { logger.warn( `⚠️ Failed to get group info for account ${account.id}:`, groupError.message ) return { ...account, groupInfos: [], usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } } }) ) return res.json({ success: true, data: accountsWithStats }) } catch (error) { logger.error('❌ Failed to get Claude accounts:', error) return res.status(500).json({ error: 'Failed to get Claude accounts', message: error.message }) } }) // 创建新的Claude账户 router.post('/claude-accounts', authenticateAdmin, async (req, res) => { try { const { name, description, email, password, refreshToken, claudeAiOauth, proxy, accountType, platform = 'claude', priority, groupId } = req.body if (!name) { return res.status(400).json({ error: 'Name is required' }) } // 验证accountType的有效性 if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果是分组类型,验证groupId if (accountType === 'group' && !groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } // 验证priority的有效性 if ( priority !== undefined && (typeof priority !== 'number' || priority < 1 || priority > 100) ) { return res.status(400).json({ error: 'Priority must be a number between 1 and 100' }) } const newAccount = await claudeAccountService.createAccount({ name, description, email, password, refreshToken, claudeAiOauth, proxy, accountType: accountType || 'shared', // 默认为共享类型 platform, priority: priority || 50 // 默认优先级为50 }) // 如果是分组类型,将账户添加到分组 if (accountType === 'group' && groupId) { await accountGroupService.addAccountToGroup(newAccount.id, groupId, newAccount.platform) } logger.success(`🏢 Admin created new Claude account: ${name} (${accountType || 'shared'})`) return res.json({ success: true, data: newAccount }) } catch (error) { logger.error('❌ Failed to create Claude account:', error) return res .status(500) .json({ error: 'Failed to create Claude account', message: error.message }) } }) // 更新Claude账户 router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const updates = req.body // 验证priority的有效性 if ( updates.priority !== undefined && (typeof updates.priority !== 'number' || updates.priority < 1 || updates.priority > 100) ) { return res.status(400).json({ error: 'Priority must be a number between 1 and 100' }) } // 验证accountType的有效性 if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果更新为分组类型,验证groupId if (updates.accountType === 'group' && !updates.groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } // 获取账户当前信息以处理分组变更 const currentAccount = await claudeAccountService.getAccount(accountId) if (!currentAccount) { return res.status(404).json({ error: 'Account not found' }) } // 处理分组的变更 if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从所有分组中移除 if (currentAccount.accountType === 'group') { const oldGroups = await accountGroupService.getAccountGroup(accountId) for (const oldGroup of oldGroups) { await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id) } } // 如果新类型是分组,添加到新分组 if (updates.accountType === 'group' && updates.groupId) { // 从路由知道这是 Claude OAuth 账户,平台为 'claude' await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude') } } await claudeAccountService.updateAccount(accountId, updates) logger.success(`📝 Admin updated Claude account: ${accountId}`) return res.json({ success: true, message: 'Claude account updated successfully' }) } catch (error) { logger.error('❌ Failed to update Claude account:', error) return res .status(500) .json({ error: 'Failed to update Claude account', message: error.message }) } }) // 删除Claude账户 router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params // 获取账户信息以检查是否在分组中 const account = await claudeAccountService.getAccount(accountId) if (account && account.accountType === 'group') { const groups = await accountGroupService.getAccountGroup(accountId) for (const group of groups) { await accountGroupService.removeAccountFromGroup(accountId, group.id) } } await claudeAccountService.deleteAccount(accountId) logger.success(`🗑️ Admin deleted Claude account: ${accountId}`) return res.json({ success: true, message: 'Claude account deleted successfully' }) } catch (error) { logger.error('❌ Failed to delete Claude account:', error) return res .status(500) .json({ error: 'Failed to delete Claude account', message: error.message }) } }) // 更新单个Claude账户的Profile信息 router.post('/claude-accounts/:accountId/update-profile', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const profileInfo = await claudeAccountService.fetchAndUpdateAccountProfile(accountId) logger.success(`✅ Updated profile for Claude account: ${accountId}`) return res.json({ success: true, message: 'Account profile updated successfully', data: profileInfo }) } catch (error) { logger.error('❌ Failed to update account profile:', error) return res .status(500) .json({ error: 'Failed to update account profile', message: error.message }) } }) // 批量更新所有Claude账户的Profile信息 router.post('/claude-accounts/update-all-profiles', authenticateAdmin, async (req, res) => { try { const result = await claudeAccountService.updateAllAccountProfiles() logger.success('✅ Batch profile update completed') return res.json({ success: true, message: 'Batch profile update completed', data: result }) } catch (error) { logger.error('❌ Failed to update all account profiles:', error) return res .status(500) .json({ error: 'Failed to update all account profiles', message: error.message }) } }) // 刷新Claude账户token router.post('/claude-accounts/:accountId/refresh', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await claudeAccountService.refreshAccountToken(accountId) logger.success(`🔄 Admin refreshed token for Claude account: ${accountId}`) return res.json({ success: true, data: result }) } catch (error) { logger.error('❌ Failed to refresh Claude account token:', error) return res.status(500).json({ error: 'Failed to refresh token', message: error.message }) } }) // 重置Claude账户状态(清除所有异常状态) router.post('/claude-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await claudeAccountService.resetAccountStatus(accountId) logger.success(`✅ Admin reset status for Claude account: ${accountId}`) return res.json({ success: true, data: result }) } catch (error) { logger.error('❌ Failed to reset Claude account status:', error) return res.status(500).json({ error: 'Failed to reset status', message: error.message }) } }) // 切换Claude账户调度状态 router.put( '/claude-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const accounts = await claudeAccountService.getAllAccounts() const account = accounts.find((acc) => acc.id === accountId) if (!account) { return res.status(404).json({ error: 'Account not found' }) } const newSchedulable = !account.schedulable await claudeAccountService.updateAccount(accountId, { schedulable: newSchedulable }) // 如果账号被禁用,发送webhook通知 if (!newSchedulable) { await webhookNotifier.sendAccountAnomalyNotification({ accountId: account.id, accountName: account.name || account.claudeAiOauth?.email || 'Claude Account', platform: 'claude-oauth', status: 'disabled', errorCode: 'CLAUDE_OAUTH_MANUALLY_DISABLED', reason: '账号已被管理员手动禁用调度', timestamp: new Date().toISOString() }) } logger.success( `🔄 Admin toggled Claude account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}` ) return res.json({ success: true, schedulable: newSchedulable }) } catch (error) { logger.error('❌ Failed to toggle Claude account schedulable status:', error) return res .status(500) .json({ error: 'Failed to toggle schedulable status', message: error.message }) } } ) // 🎮 Claude Console 账户管理 // 获取所有Claude Console账户 router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => { try { const { platform, groupId } = req.query let accounts = await claudeConsoleAccountService.getAllAccounts() // 根据查询参数进行筛选 if (platform && platform !== 'all' && platform !== 'claude-console') { // 如果指定了其他平台,返回空数组 accounts = [] } // 如果指定了分组筛选 if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 accounts = accounts.filter( (account) => !account.groupInfos || account.groupInfos.length === 0 ) } else { // 筛选特定分组的账户 accounts = accounts.filter( (account) => account.groupInfos && account.groupInfos.some((group) => group.id === groupId) ) } } // 为每个账户添加使用统计信息 const accountsWithStats = await Promise.all( accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: usageStats.daily, total: usageStats.total, averages: usageStats.averages } } } catch (statsError) { logger.warn( `⚠️ Failed to get usage stats for Claude Console account ${account.id}:`, statsError.message ) try { const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } catch (groupError) { logger.warn( `⚠️ Failed to get group info for Claude Console account ${account.id}:`, groupError.message ) return { ...account, groupInfos: [], usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } } }) ) return res.json({ success: true, data: accountsWithStats }) } catch (error) { logger.error('❌ Failed to get Claude Console accounts:', error) return res .status(500) .json({ error: 'Failed to get Claude Console accounts', message: error.message }) } }) // 创建新的Claude Console账户 router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => { try { const { name, description, apiUrl, apiKey, priority, supportedModels, userAgent, rateLimitDuration, proxy, accountType, groupId } = req.body if (!name || !apiUrl || !apiKey) { return res.status(400).json({ error: 'Name, API URL and API Key are required' }) } // 验证priority的有效性(1-100) if (priority !== undefined && (priority < 1 || priority > 100)) { return res.status(400).json({ error: 'Priority must be between 1 and 100' }) } // 验证accountType的有效性 if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果是分组类型,验证groupId if (accountType === 'group' && !groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } const newAccount = await claudeConsoleAccountService.createAccount({ name, description, apiUrl, apiKey, priority: priority || 50, supportedModels: supportedModels || [], userAgent, rateLimitDuration: rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60, proxy, accountType: accountType || 'shared' }) // 如果是分组类型,将账户添加到分组 if (accountType === 'group' && groupId) { await accountGroupService.addAccountToGroup(newAccount.id, groupId, 'claude') } logger.success(`🎮 Admin created Claude Console account: ${name}`) return res.json({ success: true, data: newAccount }) } catch (error) { logger.error('❌ Failed to create Claude Console account:', error) return res .status(500) .json({ error: 'Failed to create Claude Console account', message: error.message }) } }) // 更新Claude Console账户 router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const updates = req.body // 验证priority的有效性(1-100) if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) { return res.status(400).json({ error: 'Priority must be between 1 and 100' }) } // 验证accountType的有效性 if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果更新为分组类型,验证groupId if (updates.accountType === 'group' && !updates.groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } // 获取账户当前信息以处理分组变更 const currentAccount = await claudeConsoleAccountService.getAccount(accountId) if (!currentAccount) { return res.status(404).json({ error: 'Account not found' }) } // 处理分组的变更 if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从所有分组中移除 if (currentAccount.accountType === 'group') { const oldGroups = await accountGroupService.getAccountGroup(accountId) for (const oldGroup of oldGroups) { await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id) } } // 如果新类型是分组,添加到新分组 if (updates.accountType === 'group' && updates.groupId) { // Claude Console 账户在分组中被视为 'claude' 平台 await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude') } } await claudeConsoleAccountService.updateAccount(accountId, updates) logger.success(`📝 Admin updated Claude Console account: ${accountId}`) return res.json({ success: true, message: 'Claude Console account updated successfully' }) } catch (error) { logger.error('❌ Failed to update Claude Console account:', error) return res .status(500) .json({ error: 'Failed to update Claude Console account', message: error.message }) } }) // 删除Claude Console账户 router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params // 获取账户信息以检查是否在分组中 const account = await claudeConsoleAccountService.getAccount(accountId) if (account && account.accountType === 'group') { const groups = await accountGroupService.getAccountGroup(accountId) for (const group of groups) { await accountGroupService.removeAccountFromGroup(accountId, group.id) } } await claudeConsoleAccountService.deleteAccount(accountId) logger.success(`🗑️ Admin deleted Claude Console account: ${accountId}`) return res.json({ success: true, message: 'Claude Console account deleted successfully' }) } catch (error) { logger.error('❌ Failed to delete Claude Console account:', error) return res .status(500) .json({ error: 'Failed to delete Claude Console account', message: error.message }) } }) // 切换Claude Console账户状态 router.put('/claude-console-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const account = await claudeConsoleAccountService.getAccount(accountId) if (!account) { return res.status(404).json({ error: 'Account not found' }) } const newStatus = !account.isActive await claudeConsoleAccountService.updateAccount(accountId, { isActive: newStatus }) logger.success( `🔄 Admin toggled Claude Console account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}` ) return res.json({ success: true, isActive: newStatus }) } catch (error) { logger.error('❌ Failed to toggle Claude Console account status:', error) return res .status(500) .json({ error: 'Failed to toggle account status', message: error.message }) } }) // 切换Claude Console账户调度状态 router.put( '/claude-console-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const account = await claudeConsoleAccountService.getAccount(accountId) if (!account) { return res.status(404).json({ error: 'Account not found' }) } const newSchedulable = !account.schedulable await claudeConsoleAccountService.updateAccount(accountId, { schedulable: newSchedulable }) // 如果账号被禁用,发送webhook通知 if (!newSchedulable) { await webhookNotifier.sendAccountAnomalyNotification({ accountId: account.id, accountName: account.name || 'Claude Console Account', platform: 'claude-console', status: 'disabled', errorCode: 'CLAUDE_CONSOLE_MANUALLY_DISABLED', reason: '账号已被管理员手动禁用调度', timestamp: new Date().toISOString() }) } logger.success( `🔄 Admin toggled Claude Console account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}` ) return res.json({ success: true, schedulable: newSchedulable }) } catch (error) { logger.error('❌ Failed to toggle Claude Console account schedulable status:', error) return res .status(500) .json({ error: 'Failed to toggle schedulable status', message: error.message }) } } ) // ☁️ Bedrock 账户管理 // 获取所有Bedrock账户 router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => { try { const { platform, groupId } = req.query const result = await bedrockAccountService.getAllAccounts() if (!result.success) { return res .status(500) .json({ error: 'Failed to get Bedrock accounts', message: result.error }) } let accounts = result.data // 根据查询参数进行筛选 if (platform && platform !== 'all' && platform !== 'bedrock') { // 如果指定了其他平台,返回空数组 accounts = [] } // 如果指定了分组筛选 if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 accounts = accounts.filter( (account) => !account.groupInfos || account.groupInfos.length === 0 ) } else { // 筛选特定分组的账户 accounts = accounts.filter( (account) => account.groupInfos && account.groupInfos.some((group) => group.id === groupId) ) } } // 为每个账户添加使用统计信息 const accountsWithStats = await Promise.all( accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: usageStats.daily, total: usageStats.total, averages: usageStats.averages } } } catch (statsError) { logger.warn( `⚠️ Failed to get usage stats for Bedrock account ${account.id}:`, statsError.message ) try { const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } catch (groupError) { logger.warn( `⚠️ Failed to get group info for account ${account.id}:`, groupError.message ) return { ...account, groupInfos: [], usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } } }) ) return res.json({ success: true, data: accountsWithStats }) } catch (error) { logger.error('❌ Failed to get Bedrock accounts:', error) return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message }) } }) // 创建新的Bedrock账户 router.post('/bedrock-accounts', authenticateAdmin, async (req, res) => { try { const { name, description, region, awsCredentials, defaultModel, priority, accountType, credentialType } = req.body if (!name) { return res.status(400).json({ error: 'Name is required' }) } // 验证priority的有效性(1-100) if (priority !== undefined && (priority < 1 || priority > 100)) { return res.status(400).json({ error: 'Priority must be between 1 and 100' }) } // 验证accountType的有效性 if (accountType && !['shared', 'dedicated'].includes(accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared" or "dedicated"' }) } // 验证credentialType的有效性 if (credentialType && !['default', 'access_key', 'bearer_token'].includes(credentialType)) { return res.status(400).json({ error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' }) } const result = await bedrockAccountService.createAccount({ name, description: description || '', region: region || 'us-east-1', awsCredentials, defaultModel, priority: priority || 50, accountType: accountType || 'shared', credentialType: credentialType || 'default' }) if (!result.success) { return res .status(500) .json({ error: 'Failed to create Bedrock account', message: result.error }) } logger.success(`☁️ Admin created Bedrock account: ${name}`) return res.json({ success: true, data: result.data }) } catch (error) { logger.error('❌ Failed to create Bedrock account:', error) return res .status(500) .json({ error: 'Failed to create Bedrock account', message: error.message }) } }) // 更新Bedrock账户 router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const updates = req.body // 验证priority的有效性(1-100) if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) { return res.status(400).json({ error: 'Priority must be between 1 and 100' }) } // 验证accountType的有效性 if (updates.accountType && !['shared', 'dedicated'].includes(updates.accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared" or "dedicated"' }) } // 验证credentialType的有效性 if ( updates.credentialType && !['default', 'access_key', 'bearer_token'].includes(updates.credentialType) ) { return res.status(400).json({ error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' }) } const result = await bedrockAccountService.updateAccount(accountId, updates) if (!result.success) { return res .status(500) .json({ error: 'Failed to update Bedrock account', message: result.error }) } logger.success(`📝 Admin updated Bedrock account: ${accountId}`) return res.json({ success: true, message: 'Bedrock account updated successfully' }) } catch (error) { logger.error('❌ Failed to update Bedrock account:', error) return res .status(500) .json({ error: 'Failed to update Bedrock account', message: error.message }) } }) // 删除Bedrock账户 router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await bedrockAccountService.deleteAccount(accountId) if (!result.success) { return res .status(500) .json({ error: 'Failed to delete Bedrock account', message: result.error }) } logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}`) return res.json({ success: true, message: 'Bedrock account deleted successfully' }) } catch (error) { logger.error('❌ Failed to delete Bedrock account:', error) return res .status(500) .json({ error: 'Failed to delete Bedrock account', message: error.message }) } }) // 切换Bedrock账户状态 router.put('/bedrock-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const accountResult = await bedrockAccountService.getAccount(accountId) if (!accountResult.success) { return res.status(404).json({ error: 'Account not found' }) } const newStatus = !accountResult.data.isActive const updateResult = await bedrockAccountService.updateAccount(accountId, { isActive: newStatus }) if (!updateResult.success) { return res .status(500) .json({ error: 'Failed to toggle account status', message: updateResult.error }) } logger.success( `🔄 Admin toggled Bedrock account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}` ) return res.json({ success: true, isActive: newStatus }) } catch (error) { logger.error('❌ Failed to toggle Bedrock account status:', error) return res .status(500) .json({ error: 'Failed to toggle account status', message: error.message }) } }) // 切换Bedrock账户调度状态 router.put( '/bedrock-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const accountResult = await bedrockAccountService.getAccount(accountId) if (!accountResult.success) { return res.status(404).json({ error: 'Account not found' }) } const newSchedulable = !accountResult.data.schedulable const updateResult = await bedrockAccountService.updateAccount(accountId, { schedulable: newSchedulable }) if (!updateResult.success) { return res .status(500) .json({ error: 'Failed to toggle schedulable status', message: updateResult.error }) } // 如果账号被禁用,发送webhook通知 if (!newSchedulable) { await webhookNotifier.sendAccountAnomalyNotification({ accountId: accountResult.data.id, accountName: accountResult.data.name || 'Bedrock Account', platform: 'bedrock', status: 'disabled', errorCode: 'BEDROCK_MANUALLY_DISABLED', reason: '账号已被管理员手动禁用调度', timestamp: new Date().toISOString() }) } logger.success( `🔄 Admin toggled Bedrock account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}` ) return res.json({ success: true, schedulable: newSchedulable }) } catch (error) { logger.error('❌ Failed to toggle Bedrock account schedulable status:', error) return res .status(500) .json({ error: 'Failed to toggle schedulable status', message: error.message }) } } ) // 测试Bedrock账户连接 router.post('/bedrock-accounts/:accountId/test', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await bedrockAccountService.testAccount(accountId) if (!result.success) { return res.status(500).json({ error: 'Account test failed', message: result.error }) } logger.success(`🧪 Admin tested Bedrock account: ${accountId} - ${result.data.status}`) return res.json({ success: true, data: result.data }) } catch (error) { logger.error('❌ Failed to test Bedrock account:', error) return res.status(500).json({ error: 'Failed to test Bedrock account', message: error.message }) } }) // 🤖 Gemini 账户管理 // 生成 Gemini OAuth 授权 URL router.post('/gemini-accounts/generate-auth-url', authenticateAdmin, async (req, res) => { try { const { state, proxy } = req.body // 接收代理配置 // 使用新的 codeassist.google.com 回调地址 const redirectUri = 'https://codeassist.google.com/authcode' logger.info(`Generating Gemini OAuth URL with redirect_uri: ${redirectUri}`) const { authUrl, state: authState, codeVerifier, redirectUri: finalRedirectUri } = await geminiAccountService.generateAuthUrl(state, redirectUri) // 创建 OAuth 会话,包含 codeVerifier 和代理配置 const sessionId = authState await redis.setOAuthSession(sessionId, { state: authState, type: 'gemini', redirectUri: finalRedirectUri, codeVerifier, // 保存 PKCE code verifier proxy: proxy || null, // 保存代理配置 createdAt: new Date().toISOString() }) logger.info(`Generated Gemini OAuth URL with session: ${sessionId}`) return res.json({ success: true, data: { authUrl, sessionId } }) } catch (error) { logger.error('❌ Failed to generate Gemini auth URL:', error) return res.status(500).json({ error: 'Failed to generate auth URL', message: error.message }) } }) // 轮询 Gemini OAuth 授权状态 router.post('/gemini-accounts/poll-auth-status', authenticateAdmin, async (req, res) => { try { const { sessionId } = req.body if (!sessionId) { return res.status(400).json({ error: 'Session ID is required' }) } const result = await geminiAccountService.pollAuthorizationStatus(sessionId) if (result.success) { logger.success(`✅ Gemini OAuth authorization successful for session: ${sessionId}`) return res.json({ success: true, data: { tokens: result.tokens } }) } else { return res.json({ success: false, error: result.error }) } } catch (error) { logger.error('❌ Failed to poll Gemini auth status:', error) return res.status(500).json({ error: 'Failed to poll auth status', message: error.message }) } }) // 交换 Gemini 授权码 router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res) => { try { const { code, sessionId, proxy: requestProxy } = req.body if (!code) { return res.status(400).json({ error: 'Authorization code is required' }) } let redirectUri = 'https://codeassist.google.com/authcode' let codeVerifier = null let proxyConfig = null // 如果提供了 sessionId,从 OAuth 会话中获取信息 if (sessionId) { const sessionData = await redis.getOAuthSession(sessionId) if (sessionData) { const { redirectUri: sessionRedirectUri, codeVerifier: sessionCodeVerifier, proxy } = sessionData redirectUri = sessionRedirectUri || redirectUri codeVerifier = sessionCodeVerifier proxyConfig = proxy // 获取代理配置 logger.info( `Using session redirect_uri: ${redirectUri}, has codeVerifier: ${!!codeVerifier}, has proxy from session: ${!!proxyConfig}` ) } } // 如果请求体中直接提供了代理配置,优先使用它 if (requestProxy) { proxyConfig = requestProxy logger.info( `Using proxy from request body: ${proxyConfig ? JSON.stringify(proxyConfig) : 'none'}` ) } const tokens = await geminiAccountService.exchangeCodeForTokens( code, redirectUri, codeVerifier, proxyConfig // 传递代理配置 ) // 清理 OAuth 会话 if (sessionId) { await redis.deleteOAuthSession(sessionId) } logger.success('✅ Successfully exchanged Gemini authorization code') return res.json({ success: true, data: { tokens } }) } catch (error) { logger.error('❌ Failed to exchange Gemini authorization code:', error) return res.status(500).json({ error: 'Failed to exchange code', message: error.message }) } }) // 获取所有 Gemini 账户 router.get('/gemini-accounts', authenticateAdmin, async (req, res) => { try { const { platform, groupId } = req.query let accounts = await geminiAccountService.getAllAccounts() // 根据查询参数进行筛选 if (platform && platform !== 'all' && platform !== 'gemini') { // 如果指定了其他平台,返回空数组 accounts = [] } // 如果指定了分组筛选 if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 accounts = accounts.filter( (account) => !account.groupInfos || account.groupInfos.length === 0 ) } else { // 筛选特定分组的账户 accounts = accounts.filter( (account) => account.groupInfos && account.groupInfos.some((group) => group.id === groupId) ) } } // 为每个账户添加使用统计信息(与Claude账户相同的逻辑) const accountsWithStats = await Promise.all( accounts.map(async (account) => { try { const usageStats = await redis.getAccountUsageStats(account.id) const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: usageStats.daily, total: usageStats.total, averages: usageStats.averages } } } catch (statsError) { logger.warn( `⚠️ Failed to get usage stats for Gemini account ${account.id}:`, statsError.message ) // 如果获取统计失败,返回空统计 try { const groupInfos = await accountGroupService.getAccountGroup(account.id) return { ...account, groupInfos, usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } catch (groupError) { logger.warn( `⚠️ Failed to get group info for account ${account.id}:`, groupError.message ) return { ...account, groupInfos: [], usage: { daily: { tokens: 0, requests: 0, allTokens: 0 }, total: { tokens: 0, requests: 0, allTokens: 0 }, averages: { rpm: 0, tpm: 0 } } } } } }) ) return res.json({ success: true, data: accountsWithStats }) } catch (error) { logger.error('❌ Failed to get Gemini accounts:', error) return res.status(500).json({ error: 'Failed to get accounts', message: error.message }) } }) // 创建新的 Gemini 账户 router.post('/gemini-accounts', authenticateAdmin, async (req, res) => { try { const accountData = req.body // 输入验证 if (!accountData.name) { return res.status(400).json({ error: 'Account name is required' }) } // 验证accountType的有效性 if ( accountData.accountType && !['shared', 'dedicated', 'group'].includes(accountData.accountType) ) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果是分组类型,验证groupId if (accountData.accountType === 'group' && !accountData.groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } const newAccount = await geminiAccountService.createAccount(accountData) // 如果是分组类型,将账户添加到分组 if (accountData.accountType === 'group' && accountData.groupId) { await accountGroupService.addAccountToGroup(newAccount.id, accountData.groupId, 'gemini') } logger.success(`🏢 Admin created new Gemini account: ${accountData.name}`) return res.json({ success: true, data: newAccount }) } catch (error) { logger.error('❌ Failed to create Gemini account:', error) return res.status(500).json({ error: 'Failed to create account', message: error.message }) } }) // 更新 Gemini 账户 router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const updates = req.body // 验证accountType的有效性 if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果更新为分组类型,验证groupId if (updates.accountType === 'group' && !updates.groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } // 获取账户当前信息以处理分组变更 const currentAccount = await geminiAccountService.getAccount(accountId) if (!currentAccount) { return res.status(404).json({ error: 'Account not found' }) } // 处理分组的变更 if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从所有分组中移除 if (currentAccount.accountType === 'group') { const oldGroups = await accountGroupService.getAccountGroup(accountId) for (const oldGroup of oldGroups) { await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id) } } // 如果新类型是分组,添加到新分组 if (updates.accountType === 'group' && updates.groupId) { await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'gemini') } } const updatedAccount = await geminiAccountService.updateAccount(accountId, updates) logger.success(`📝 Admin updated Gemini account: ${accountId}`) return res.json({ success: true, data: updatedAccount }) } catch (error) { logger.error('❌ Failed to update Gemini account:', error) return res.status(500).json({ error: 'Failed to update account', message: error.message }) } }) // 删除 Gemini 账户 router.delete('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params // 获取账户信息以检查是否在分组中 const account = await geminiAccountService.getAccount(accountId) if (account && account.accountType === 'group') { const groups = await accountGroupService.getAccountGroup(accountId) for (const group of groups) { await accountGroupService.removeAccountFromGroup(accountId, group.id) } } await geminiAccountService.deleteAccount(accountId) logger.success(`🗑️ Admin deleted Gemini account: ${accountId}`) return res.json({ success: true, message: 'Gemini account deleted successfully' }) } catch (error) { logger.error('❌ Failed to delete Gemini account:', error) return res.status(500).json({ error: 'Failed to delete account', message: error.message }) } }) // 刷新 Gemini 账户 token router.post('/gemini-accounts/:accountId/refresh', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await geminiAccountService.refreshAccountToken(accountId) logger.success(`🔄 Admin refreshed token for Gemini account: ${accountId}`) return res.json({ success: true, data: result }) } catch (error) { logger.error('❌ Failed to refresh Gemini account token:', error) return res.status(500).json({ error: 'Failed to refresh token', message: error.message }) } }) // 切换 Gemini 账户调度状态 router.put( '/gemini-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const account = await geminiAccountService.getAccount(accountId) if (!account) { return res.status(404).json({ error: 'Account not found' }) } // 现在 account.schedulable 已经是布尔值了,直接取反即可 const newSchedulable = !account.schedulable await geminiAccountService.updateAccount(accountId, { schedulable: String(newSchedulable) }) // 验证更新是否成功,重新获取账户信息 const updatedAccount = await geminiAccountService.getAccount(accountId) const actualSchedulable = updatedAccount ? updatedAccount.schedulable : newSchedulable // 如果账号被禁用,发送webhook通知 if (!actualSchedulable) { await webhookNotifier.sendAccountAnomalyNotification({ accountId: account.id, accountName: account.accountName || 'Gemini Account', platform: 'gemini', status: 'disabled', errorCode: 'GEMINI_MANUALLY_DISABLED', reason: '账号已被管理员手动禁用调度', timestamp: new Date().toISOString() }) } logger.success( `🔄 Admin toggled Gemini account schedulable status: ${accountId} -> ${actualSchedulable ? 'schedulable' : 'not schedulable'}` ) // 返回实际的数据库值,确保前端状态与后端一致 return res.json({ success: true, schedulable: actualSchedulable }) } catch (error) { logger.error('❌ Failed to toggle Gemini account schedulable status:', error) return res .status(500) .json({ error: 'Failed to toggle schedulable status', message: error.message }) } } ) // 📊 账户使用统计 // 获取所有账户的使用统计 router.get('/accounts/usage-stats', authenticateAdmin, async (req, res) => { try { const accountsStats = await redis.getAllAccountsUsageStats() return 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) return 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' }) } return 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) return res.status(500).json({ success: false, error: 'Failed to get account usage stats', message: error.message }) } }) // 📊 系统统计 // 获取系统概览 router.get('/dashboard', authenticateAdmin, async (req, res) => { try { const [ , apiKeys, claudeAccounts, claudeConsoleAccounts, geminiAccounts, bedrockAccountsResult, openaiAccounts, todayStats, systemAverages, realtimeMetrics ] = await Promise.all([ redis.getSystemStats(), apiKeyService.getAllApiKeys(), claudeAccountService.getAllAccounts(), claudeConsoleAccountService.getAllAccounts(), geminiAccountService.getAllAccounts(), bedrockAccountService.getAllAccounts(), redis.getAllOpenAIAccounts(), redis.getTodayStats(), redis.getSystemAverages(), redis.getRealtimeSystemMetrics() ]) // 处理Bedrock账户数据 const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : [] // 计算使用统计(统一使用allTokens) const totalTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.allTokens || 0), 0 ) const totalRequestsUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.requests || 0), 0 ) const totalInputTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.inputTokens || 0), 0 ) const totalOutputTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.outputTokens || 0), 0 ) const totalCacheCreateTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.cacheCreateTokens || 0), 0 ) const totalCacheReadTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.cacheReadTokens || 0), 0 ) const totalAllTokensUsed = apiKeys.reduce( (sum, key) => sum + (key.usage?.total?.allTokens || 0), 0 ) const activeApiKeys = apiKeys.filter((key) => key.isActive).length // Claude账户统计 - 根据账户管理页面的判断逻辑 const normalClaudeAccounts = claudeAccounts.filter( (acc) => acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && acc.schedulable !== false && !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalClaudeAccounts = claudeAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' ).length const pausedClaudeAccounts = claudeAccounts.filter( (acc) => acc.schedulable === false && acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' ).length const rateLimitedClaudeAccounts = claudeAccounts.filter( (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited ).length // Claude Console账户统计 const normalClaudeConsoleAccounts = claudeConsoleAccounts.filter( (acc) => acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && acc.schedulable !== false && !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' ).length const pausedClaudeConsoleAccounts = claudeConsoleAccounts.filter( (acc) => acc.schedulable === false && acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' ).length const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter( (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited ).length // Gemini账户统计 const normalGeminiAccounts = geminiAccounts.filter( (acc) => acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && acc.schedulable !== false && !( acc.rateLimitStatus === 'limited' || (acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ) ).length const abnormalGeminiAccounts = geminiAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' ).length const pausedGeminiAccounts = geminiAccounts.filter( (acc) => acc.schedulable === false && acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' ).length const rateLimitedGeminiAccounts = geminiAccounts.filter( (acc) => acc.rateLimitStatus === 'limited' || (acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length // Bedrock账户统计 const normalBedrockAccounts = bedrockAccounts.filter( (acc) => acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' && acc.schedulable !== false && !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalBedrockAccounts = bedrockAccounts.filter( (acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized' ).length const pausedBedrockAccounts = bedrockAccounts.filter( (acc) => acc.schedulable === false && acc.isActive && acc.status !== 'blocked' && acc.status !== 'unauthorized' ).length const rateLimitedBedrockAccounts = bedrockAccounts.filter( (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited ).length // OpenAI账户统计 // 注意:OpenAI账户的isActive和schedulable是字符串类型,默认值为'true' const normalOpenAIAccounts = openaiAccounts.filter( (acc) => (acc.isActive === 'true' || acc.isActive === true || (!acc.isActive && acc.isActive !== 'false' && acc.isActive !== false)) && acc.status !== 'blocked' && acc.status !== 'unauthorized' && acc.schedulable !== 'false' && acc.schedulable !== false && // 包括'true'、true和undefined !(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited) ).length const abnormalOpenAIAccounts = openaiAccounts.filter( (acc) => acc.isActive === 'false' || acc.isActive === false || acc.status === 'blocked' || acc.status === 'unauthorized' ).length const pausedOpenAIAccounts = openaiAccounts.filter( (acc) => (acc.schedulable === 'false' || acc.schedulable === false) && (acc.isActive === 'true' || acc.isActive === true || (!acc.isActive && acc.isActive !== 'false' && acc.isActive !== false)) && acc.status !== 'blocked' && acc.status !== 'unauthorized' ).length const rateLimitedOpenAIAccounts = openaiAccounts.filter( (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited ).length const dashboard = { overview: { totalApiKeys: apiKeys.length, activeApiKeys, // 总账户统计(所有平台) totalAccounts: claudeAccounts.length + claudeConsoleAccounts.length + geminiAccounts.length + bedrockAccounts.length + openaiAccounts.length, normalAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts + normalGeminiAccounts + normalBedrockAccounts + normalOpenAIAccounts, abnormalAccounts: abnormalClaudeAccounts + abnormalClaudeConsoleAccounts + abnormalGeminiAccounts + abnormalBedrockAccounts + abnormalOpenAIAccounts, pausedAccounts: pausedClaudeAccounts + pausedClaudeConsoleAccounts + pausedGeminiAccounts + pausedBedrockAccounts + pausedOpenAIAccounts, rateLimitedAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts + rateLimitedGeminiAccounts + rateLimitedBedrockAccounts + rateLimitedOpenAIAccounts, // 各平台详细统计 accountsByPlatform: { claude: { total: claudeAccounts.length, normal: normalClaudeAccounts, abnormal: abnormalClaudeAccounts, paused: pausedClaudeAccounts, rateLimited: rateLimitedClaudeAccounts }, 'claude-console': { total: claudeConsoleAccounts.length, normal: normalClaudeConsoleAccounts, abnormal: abnormalClaudeConsoleAccounts, paused: pausedClaudeConsoleAccounts, rateLimited: rateLimitedClaudeConsoleAccounts }, gemini: { total: geminiAccounts.length, normal: normalGeminiAccounts, abnormal: abnormalGeminiAccounts, paused: pausedGeminiAccounts, rateLimited: rateLimitedGeminiAccounts }, bedrock: { total: bedrockAccounts.length, normal: normalBedrockAccounts, abnormal: abnormalBedrockAccounts, paused: pausedBedrockAccounts, rateLimited: rateLimitedBedrockAccounts }, openai: { total: openaiAccounts.length, normal: normalOpenAIAccounts, abnormal: abnormalOpenAIAccounts, paused: pausedOpenAIAccounts, rateLimited: rateLimitedOpenAIAccounts } }, // 保留旧字段以兼容 activeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts + normalGeminiAccounts + normalBedrockAccounts + normalOpenAIAccounts, totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length, activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts, rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts, totalGeminiAccounts: geminiAccounts.length, activeGeminiAccounts: normalGeminiAccounts, rateLimitedGeminiAccounts, totalTokensUsed, totalRequestsUsed, totalInputTokensUsed, totalOutputTokensUsed, totalCacheCreateTokensUsed, totalCacheReadTokensUsed, totalAllTokensUsed }, recentActivity: { apiKeysCreatedToday: todayStats.apiKeysCreatedToday, requestsToday: todayStats.requestsToday, tokensToday: todayStats.tokensToday, inputTokensToday: todayStats.inputTokensToday, outputTokensToday: todayStats.outputTokensToday, cacheCreateTokensToday: todayStats.cacheCreateTokensToday || 0, cacheReadTokensToday: todayStats.cacheReadTokensToday || 0 }, systemAverages: { rpm: systemAverages.systemRPM, tpm: systemAverages.systemTPM }, realtimeMetrics: { rpm: realtimeMetrics.realtimeRPM, tpm: realtimeMetrics.realtimeTPM, windowMinutes: realtimeMetrics.windowMinutes, isHistorical: realtimeMetrics.windowMinutes === 0 // 标识是否使用了历史数据 }, systemHealth: { redisConnected: redis.isConnected, claudeAccountsHealthy: normalClaudeAccounts + normalClaudeConsoleAccounts > 0, geminiAccountsHealthy: normalGeminiAccounts > 0, uptime: process.uptime() }, systemTimezone: config.system.timezoneOffset || 8 } return res.json({ success: true, data: dashboard }) } catch (error) { logger.error('❌ Failed to get dashboard data:', error) return res.status(500).json({ error: 'Failed to get dashboard data', message: error.message }) } }) // 获取使用统计 router.get('/usage-stats', authenticateAdmin, async (req, res) => { try { const { period = 'daily' } = req.query // daily, monthly // 获取基础API Key统计 const apiKeys = await apiKeyService.getAllApiKeys() const stats = apiKeys.map((key) => ({ keyId: key.id, keyName: key.name, usage: key.usage })) return res.json({ success: true, data: { period, stats } }) } catch (error) { logger.error('❌ Failed to get usage stats:', error) return res.status(500).json({ error: 'Failed to get usage stats', message: error.message }) } }) // 获取按模型的使用统计和费用 router.get('/model-stats', authenticateAdmin, async (req, res) => { try { const { period = 'daily', startDate, endDate } = req.query // daily, monthly, 支持自定义时间范围 const today = redis.getDateStringInTimezone() const tzDate = redis.getDateInTimezone() const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` logger.info( `📊 Getting global model stats, period: ${period}, startDate: ${startDate}, endDate: ${endDate}, today: ${today}, currentMonth: ${currentMonth}` ) const client = redis.getClientSafe() // 获取所有模型的统计数据 let searchPatterns = [] if (startDate && endDate) { // 自定义日期范围,生成多个日期的搜索模式 const start = new Date(startDate) const end = new Date(endDate) // 确保日期范围有效 if (start > end) { return res.status(400).json({ error: 'Start date must be before or equal to end date' }) } // 限制最大范围为31天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 if (daysDiff > 31) { return res.status(400).json({ error: 'Date range cannot exceed 31 days' }) } // 生成日期范围内所有日期的搜索模式 const currentDate = new Date(start) while (currentDate <= end) { const dateStr = redis.getDateStringInTimezone(currentDate) searchPatterns.push(`usage:model:daily:*:${dateStr}`) currentDate.setDate(currentDate.getDate() + 1) } logger.info(`📊 Generated ${searchPatterns.length} search patterns for date range`) } else { // 使用默认的period const pattern = period === 'daily' ? `usage:model:daily:*:${today}` : `usage:model:monthly:*:${currentMonth}` searchPatterns = [pattern] } logger.info('📊 Searching patterns:', searchPatterns) // 获取所有匹配的keys const allKeys = [] for (const pattern of searchPatterns) { const keys = await client.keys(pattern) allKeys.push(...keys) } logger.info(`📊 Found ${allKeys.length} matching keys in total`) // 模型名标准化函数(与redis.js保持一致) const normalizeModelName = (model) => { if (!model || model === 'unknown') { return model } // 对于Bedrock模型,去掉区域前缀进行统一 if (model.includes('.anthropic.') || model.includes('.claude')) { // 匹配所有AWS区域格式:region.anthropic.model-name-v1:0 -> claude-model-name // 支持所有AWS区域格式,如:us-east-1, eu-west-1, ap-southeast-1, ca-central-1等 let normalized = model.replace(/^[a-z0-9-]+\./, '') // 去掉任何区域前缀(更通用) normalized = normalized.replace('anthropic.', '') // 去掉anthropic前缀 normalized = normalized.replace(/-v\d+:\d+$/, '') // 去掉版本后缀(如-v1:0, -v2:1等) return normalized } // 对于其他模型,去掉常见的版本后缀 return model.replace(/-v\d+:\d+$|:latest$/, '') } // 聚合相同模型的数据 const modelStatsMap = new Map() for (const key of allKeys) { const match = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) if (!match) { logger.warn(`📊 Pattern mismatch for key: ${key}`) continue } const rawModel = match[1] const normalizedModel = normalizeModelName(rawModel) const data = await client.hgetall(key) if (data && Object.keys(data).length > 0) { const stats = modelStatsMap.get(normalizedModel) || { requests: 0, inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, allTokens: 0 } stats.requests += parseInt(data.requests) || 0 stats.inputTokens += parseInt(data.inputTokens) || 0 stats.outputTokens += parseInt(data.outputTokens) || 0 stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 stats.allTokens += parseInt(data.allTokens) || 0 modelStatsMap.set(normalizedModel, stats) } } // 转换为数组并计算费用 const modelStats = [] for (const [model, stats] of modelStatsMap) { const usage = { input_tokens: stats.inputTokens, output_tokens: stats.outputTokens, cache_creation_input_tokens: stats.cacheCreateTokens, cache_read_input_tokens: stats.cacheReadTokens } // 计算费用 const costData = CostCalculator.calculateCost(usage, model) modelStats.push({ model, period: startDate && endDate ? 'custom' : period, requests: stats.requests, inputTokens: usage.input_tokens, outputTokens: usage.output_tokens, cacheCreateTokens: usage.cache_creation_input_tokens, cacheReadTokens: usage.cache_read_input_tokens, allTokens: stats.allTokens, usage: { requests: stats.requests, inputTokens: usage.input_tokens, outputTokens: usage.output_tokens, cacheCreateTokens: usage.cache_creation_input_tokens, cacheReadTokens: usage.cache_read_input_tokens, totalTokens: usage.input_tokens + usage.output_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens }, costs: costData.costs, formatted: costData.formatted, pricing: costData.pricing }) } // 按总费用排序 modelStats.sort((a, b) => b.costs.total - a.costs.total) logger.info( `📊 Returning ${modelStats.length} global model stats for period ${period}:`, modelStats ) return res.json({ success: true, data: modelStats }) } catch (error) { logger.error('❌ Failed to get model stats:', error) return res.status(500).json({ error: 'Failed to get model stats', message: error.message }) } }) // 🔧 系统管理 // 清理过期数据 router.post('/cleanup', authenticateAdmin, async (req, res) => { try { const [expiredKeys, errorAccounts] = await Promise.all([ apiKeyService.cleanupExpiredKeys(), claudeAccountService.cleanupErrorAccounts() ]) await redis.cleanup() logger.success( `🧹 Admin triggered cleanup: ${expiredKeys} expired keys, ${errorAccounts} error accounts` ) return res.json({ success: true, message: 'Cleanup completed', data: { expiredKeysRemoved: expiredKeys, errorAccountsReset: errorAccounts } }) } catch (error) { logger.error('❌ Cleanup failed:', error) return res.status(500).json({ error: 'Cleanup failed', message: error.message }) } }) // 获取使用趋势数据 router.get('/usage-trend', authenticateAdmin, async (req, res) => { try { const { days = 7, granularity = 'day', startDate, endDate } = req.query const client = redis.getClientSafe() const trendData = [] if (granularity === 'hour') { // 小时粒度统计 let startTime, endTime if (startDate && endDate) { // 使用自定义时间范围 startTime = new Date(startDate) endTime = new Date(endDate) // 调试日志 logger.info('📊 Usage trend hour granularity - received times:') logger.info(` startDate (raw): ${startDate}`) logger.info(` endDate (raw): ${endDate}`) logger.info(` startTime (parsed): ${startTime.toISOString()}`) logger.info(` endTime (parsed): ${endTime.toISOString()}`) logger.info(` System timezone offset: ${config.system.timezoneOffset || 8}`) } else { // 默认最近24小时 endTime = new Date() startTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000) } // 确保时间范围不超过24小时 const timeDiff = endTime - startTime if (timeDiff > 24 * 60 * 60 * 1000) { return res.status(400).json({ error: '小时粒度查询时间范围不能超过24小时' }) } // 按小时遍历 const currentHour = new Date(startTime) currentHour.setMinutes(0, 0, 0) while (currentHour <= endTime) { // 注意:前端发送的时间已经是UTC时间,不需要再次转换 // 直接从currentHour生成对应系统时区的日期和小时 const tzCurrentHour = redis.getDateInTimezone(currentHour) const dateStr = redis.getDateStringInTimezone(currentHour) const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0') const hourKey = `${dateStr}:${hour}` // 获取当前小时的模型统计数据 const modelPattern = `usage:model:hourly:*:${hourKey}` const modelKeys = await client.keys(modelPattern) let hourInputTokens = 0 let hourOutputTokens = 0 let hourRequests = 0 let hourCacheCreateTokens = 0 let hourCacheReadTokens = 0 let hourCost = 0 for (const modelKey of modelKeys) { const modelMatch = modelKey.match(/usage:model:hourly:(.+):\d{4}-\d{2}-\d{2}:\d{2}$/) if (!modelMatch) { continue } const model = modelMatch[1] const data = await client.hgetall(modelKey) if (data && Object.keys(data).length > 0) { const modelInputTokens = parseInt(data.inputTokens) || 0 const modelOutputTokens = parseInt(data.outputTokens) || 0 const modelCacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const modelCacheReadTokens = parseInt(data.cacheReadTokens) || 0 const modelRequests = parseInt(data.requests) || 0 hourInputTokens += modelInputTokens hourOutputTokens += modelOutputTokens hourCacheCreateTokens += modelCacheCreateTokens hourCacheReadTokens += modelCacheReadTokens hourRequests += modelRequests const modelUsage = { input_tokens: modelInputTokens, output_tokens: modelOutputTokens, cache_creation_input_tokens: modelCacheCreateTokens, cache_read_input_tokens: modelCacheReadTokens } const modelCostResult = CostCalculator.calculateCost(modelUsage, model) hourCost += modelCostResult.costs.total } } // 如果没有模型级别的数据,尝试API Key级别的数据 if (modelKeys.length === 0) { const pattern = `usage:hourly:*:${hourKey}` const keys = await client.keys(pattern) for (const key of keys) { const data = await client.hgetall(key) if (data) { hourInputTokens += parseInt(data.inputTokens) || 0 hourOutputTokens += parseInt(data.outputTokens) || 0 hourRequests += parseInt(data.requests) || 0 hourCacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 hourCacheReadTokens += parseInt(data.cacheReadTokens) || 0 } } const usage = { input_tokens: hourInputTokens, output_tokens: hourOutputTokens, cache_creation_input_tokens: hourCacheCreateTokens, cache_read_input_tokens: hourCacheReadTokens } const costResult = CostCalculator.calculateCost(usage, 'unknown') hourCost = costResult.costs.total } // 格式化时间标签 - 使用系统时区的显示 const tzDateForLabel = redis.getDateInTimezone(currentHour) const month = String(tzDateForLabel.getUTCMonth() + 1).padStart(2, '0') const day = String(tzDateForLabel.getUTCDate()).padStart(2, '0') const hourStr = String(tzDateForLabel.getUTCHours()).padStart(2, '0') trendData.push({ // 对于小时粒度,只返回hour字段,不返回date字段 hour: currentHour.toISOString(), // 保留原始ISO时间用于排序 label: `${month}/${day} ${hourStr}:00`, // 添加格式化的标签 inputTokens: hourInputTokens, outputTokens: hourOutputTokens, requests: hourRequests, cacheCreateTokens: hourCacheCreateTokens, cacheReadTokens: hourCacheReadTokens, totalTokens: hourInputTokens + hourOutputTokens + hourCacheCreateTokens + hourCacheReadTokens, cost: hourCost }) // 移到下一个小时 currentHour.setHours(currentHour.getHours() + 1) } } else { // 天粒度统计(保持原有逻辑) const daysCount = parseInt(days) || 7 const today = new Date() // 获取过去N天的数据 for (let i = 0; i < daysCount; i++) { const date = new Date(today) date.setDate(date.getDate() - i) const dateStr = redis.getDateStringInTimezone(date) // 汇总当天所有API Key的使用数据 const pattern = `usage:daily:*:${dateStr}` const keys = await client.keys(pattern) let dayInputTokens = 0 let dayOutputTokens = 0 let dayRequests = 0 let dayCacheCreateTokens = 0 let dayCacheReadTokens = 0 let dayCost = 0 // 按模型统计使用量 // const modelUsageMap = new Map(); // 获取当天所有模型的使用数据 const modelPattern = `usage:model:daily:*:${dateStr}` const modelKeys = await client.keys(modelPattern) for (const modelKey of modelKeys) { // 解析模型名称 const modelMatch = modelKey.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) if (!modelMatch) { continue } const model = modelMatch[1] const data = await client.hgetall(modelKey) if (data && Object.keys(data).length > 0) { const modelInputTokens = parseInt(data.inputTokens) || 0 const modelOutputTokens = parseInt(data.outputTokens) || 0 const modelCacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const modelCacheReadTokens = parseInt(data.cacheReadTokens) || 0 const modelRequests = parseInt(data.requests) || 0 // 累加总数 dayInputTokens += modelInputTokens dayOutputTokens += modelOutputTokens dayCacheCreateTokens += modelCacheCreateTokens dayCacheReadTokens += modelCacheReadTokens dayRequests += modelRequests // 按模型计算费用 const modelUsage = { input_tokens: modelInputTokens, output_tokens: modelOutputTokens, cache_creation_input_tokens: modelCacheCreateTokens, cache_read_input_tokens: modelCacheReadTokens } const modelCostResult = CostCalculator.calculateCost(modelUsage, model) dayCost += modelCostResult.costs.total } } // 如果没有模型级别的数据,回退到原始方法 if (modelKeys.length === 0 && keys.length > 0) { for (const key of keys) { const data = await client.hgetall(key) if (data) { dayInputTokens += parseInt(data.inputTokens) || 0 dayOutputTokens += parseInt(data.outputTokens) || 0 dayRequests += parseInt(data.requests) || 0 dayCacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 dayCacheReadTokens += parseInt(data.cacheReadTokens) || 0 } } // 使用默认模型价格计算 const usage = { input_tokens: dayInputTokens, output_tokens: dayOutputTokens, cache_creation_input_tokens: dayCacheCreateTokens, cache_read_input_tokens: dayCacheReadTokens } const costResult = CostCalculator.calculateCost(usage, 'unknown') dayCost = costResult.costs.total } trendData.push({ date: dateStr, inputTokens: dayInputTokens, outputTokens: dayOutputTokens, requests: dayRequests, cacheCreateTokens: dayCacheCreateTokens, cacheReadTokens: dayCacheReadTokens, totalTokens: dayInputTokens + dayOutputTokens + dayCacheCreateTokens + dayCacheReadTokens, cost: dayCost, formattedCost: CostCalculator.formatCost(dayCost) }) } } // 按日期正序排列 if (granularity === 'hour') { trendData.sort((a, b) => new Date(a.hour) - new Date(b.hour)) } else { trendData.sort((a, b) => new Date(a.date) - new Date(b.date)) } return res.json({ success: true, data: trendData, granularity }) } catch (error) { logger.error('❌ Failed to get usage trend:', error) return res.status(500).json({ error: 'Failed to get usage trend', message: error.message }) } }) // 获取单个API Key的模型统计 router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) => { try { const { keyId } = req.params const { period = 'monthly', startDate, endDate } = req.query logger.info( `📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}` ) const client = redis.getClientSafe() const today = redis.getDateStringInTimezone() const tzDate = redis.getDateInTimezone() const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` let searchPatterns = [] if (period === 'custom' && startDate && endDate) { // 自定义日期范围,生成多个日期的搜索模式 const start = new Date(startDate) const end = new Date(endDate) // 确保日期范围有效 if (start > end) { return res.status(400).json({ error: 'Start date must be before or equal to end date' }) } // 限制最大范围为31天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 if (daysDiff > 31) { return res.status(400).json({ error: 'Date range cannot exceed 31 days' }) } // 生成日期范围内所有日期的搜索模式 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}`) } logger.info( `📊 Custom date range patterns: ${searchPatterns.length} days from ${startDate} to ${endDate}` ) } else { // 原有的预设期间逻辑 const pattern = period === 'daily' ? `usage:${keyId}:model:daily:*:${today}` : `usage:${keyId}:model:monthly:*:${currentMonth}` searchPatterns = [pattern] logger.info(`📊 Preset period pattern: ${pattern}`) } // 汇总所有匹配的数据 const modelStatsMap = new Map() const modelStats = [] // 定义结果数组 for (const pattern of searchPatterns) { const keys = await client.keys(pattern) logger.info(`📊 Pattern ${pattern} found ${keys.length} keys`) for (const key of keys) { const match = key.match(/usage:.+:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) || key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/) if (!match) { logger.warn(`📊 Pattern mismatch for key: ${key}`) continue } const model = match[1] const data = await client.hgetall(key) if (data && Object.keys(data).length > 0) { // 累加同一模型的数据 if (!modelStatsMap.has(model)) { modelStatsMap.set(model, { requests: 0, inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, allTokens: 0 }) } const stats = modelStatsMap.get(model) stats.requests += parseInt(data.requests) || 0 stats.inputTokens += parseInt(data.inputTokens) || 0 stats.outputTokens += parseInt(data.outputTokens) || 0 stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 stats.allTokens += parseInt(data.allTokens) || 0 } } } // 将汇总的数据转换为最终结果 for (const [model, stats] of modelStatsMap) { logger.info(`📊 Model ${model} aggregated data:`, stats) const usage = { input_tokens: stats.inputTokens, output_tokens: stats.outputTokens, cache_creation_input_tokens: stats.cacheCreateTokens, cache_read_input_tokens: stats.cacheReadTokens } // 使用CostCalculator计算费用 const costData = CostCalculator.calculateCost(usage, model) modelStats.push({ model, requests: stats.requests, inputTokens: stats.inputTokens, outputTokens: stats.outputTokens, cacheCreateTokens: stats.cacheCreateTokens, cacheReadTokens: stats.cacheReadTokens, allTokens: stats.allTokens, // 添加费用信息 costs: costData.costs, formatted: costData.formatted, pricing: costData.pricing, usingDynamicPricing: costData.usingDynamicPricing }) } // 如果没有找到模型级别的详细数据,尝试从汇总数据中生成展示 if (modelStats.length === 0) { logger.info( `📊 No detailed model stats found, trying to get aggregate data for API key ${keyId}` ) // 尝试从API Keys列表中获取usage数据作为备选方案 try { const apiKeys = await apiKeyService.getAllApiKeys() const targetApiKey = apiKeys.find((key) => key.id === keyId) if (targetApiKey && targetApiKey.usage) { logger.info( `📊 Found API key usage data from getAllApiKeys for ${keyId}:`, targetApiKey.usage ) // 从汇总数据创建展示条目 let usageData if (period === 'custom' || period === 'daily') { // 对于自定义或日统计,使用daily数据或total数据 usageData = targetApiKey.usage.daily || targetApiKey.usage.total } else { // 对于月统计,使用monthly数据或total数据 usageData = targetApiKey.usage.monthly || targetApiKey.usage.total } if (usageData && usageData.allTokens > 0) { const usage = { input_tokens: usageData.inputTokens || 0, output_tokens: usageData.outputTokens || 0, cache_creation_input_tokens: usageData.cacheCreateTokens || 0, cache_read_input_tokens: usageData.cacheReadTokens || 0 } // 对于汇总数据,使用默认模型计算费用 const costData = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022') modelStats.push({ model: '总体使用 (历史数据)', requests: usageData.requests || 0, inputTokens: usageData.inputTokens || 0, outputTokens: usageData.outputTokens || 0, cacheCreateTokens: usageData.cacheCreateTokens || 0, cacheReadTokens: usageData.cacheReadTokens || 0, allTokens: usageData.allTokens || 0, // 添加费用信息 costs: costData.costs, formatted: costData.formatted, pricing: costData.pricing, usingDynamicPricing: costData.usingDynamicPricing }) logger.info('📊 Generated display data from API key usage stats') } else { logger.info(`📊 No usage data found for period ${period} in API key data`) } } else { logger.info(`📊 API key ${keyId} not found or has no usage data`) } } catch (error) { logger.error('❌ Error fetching API key usage data:', error) } } // 按总token数降序排列 modelStats.sort((a, b) => b.allTokens - a.allTokens) logger.info(`📊 Returning ${modelStats.length} model stats for API key ${keyId}:`, modelStats) return res.json({ success: true, data: modelStats }) } catch (error) { logger.error('❌ Failed to get API key model stats:', error) return res .status(500) .json({ error: 'Failed to get API key model stats', message: error.message }) } }) // 获取按API Key分组的使用趋势 router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { try { const { granularity = 'day', days = 7, startDate, endDate } = req.query logger.info(`📊 Getting API keys usage trend, granularity: ${granularity}, days: ${days}`) const client = redis.getClientSafe() const trendData = [] // 获取所有API Keys const apiKeys = await apiKeyService.getAllApiKeys() const apiKeyMap = new Map(apiKeys.map((key) => [key.id, key])) if (granularity === 'hour') { // 小时粒度统计 let endTime, startTime if (startDate && endDate) { // 自定义时间范围 startTime = new Date(startDate) endTime = new Date(endDate) } else { // 默认近24小时 endTime = new Date() startTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000) } // 按小时遍历 const currentHour = new Date(startTime) currentHour.setMinutes(0, 0, 0) while (currentHour <= endTime) { // 使用时区转换后的时间来生成键 const tzCurrentHour = redis.getDateInTimezone(currentHour) const dateStr = redis.getDateStringInTimezone(currentHour) const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0') const hourKey = `${dateStr}:${hour}` // 获取这个小时所有API Key的数据 const pattern = `usage:hourly:*:${hourKey}` const keys = await client.keys(pattern) // 格式化时间标签 const tzDateForLabel = redis.getDateInTimezone(currentHour) const monthLabel = String(tzDateForLabel.getUTCMonth() + 1).padStart(2, '0') const dayLabel = String(tzDateForLabel.getUTCDate()).padStart(2, '0') const hourLabel = String(tzDateForLabel.getUTCHours()).padStart(2, '0') const hourData = { hour: currentHour.toISOString(), // 使用原始时间,不进行时区转换 label: `${monthLabel}/${dayLabel} ${hourLabel}:00`, // 添加格式化的标签 apiKeys: {} } // 先收集基础数据 const apiKeyDataMap = new Map() for (const key of keys) { const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/) if (!match) { continue } const apiKeyId = match[1] const data = await client.hgetall(key) if (data && apiKeyMap.has(apiKeyId)) { const inputTokens = parseInt(data.inputTokens) || 0 const outputTokens = parseInt(data.outputTokens) || 0 const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const cacheReadTokens = parseInt(data.cacheReadTokens) || 0 const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens apiKeyDataMap.set(apiKeyId, { name: apiKeyMap.get(apiKeyId).name, tokens: totalTokens, requests: parseInt(data.requests) || 0, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens }) } } // 获取该小时的模型级别数据来计算准确费用 const modelPattern = `usage:*:model:hourly:*:${hourKey}` const modelKeys = await client.keys(modelPattern) const apiKeyCostMap = new Map() for (const modelKey of modelKeys) { const match = modelKey.match(/usage:(.+?):model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/) if (!match) { continue } const apiKeyId = match[1] const model = match[2] const modelData = await client.hgetall(modelKey) if (modelData && apiKeyDataMap.has(apiKeyId)) { const usage = { input_tokens: parseInt(modelData.inputTokens) || 0, output_tokens: parseInt(modelData.outputTokens) || 0, cache_creation_input_tokens: parseInt(modelData.cacheCreateTokens) || 0, cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0 } const costResult = CostCalculator.calculateCost(usage, model) const currentCost = apiKeyCostMap.get(apiKeyId) || 0 apiKeyCostMap.set(apiKeyId, currentCost + costResult.costs.total) } } // 组合数据 for (const [apiKeyId, data] of apiKeyDataMap) { const cost = apiKeyCostMap.get(apiKeyId) || 0 // 如果没有模型级别数据,使用默认模型计算(降级方案) let finalCost = cost let formattedCost = CostCalculator.formatCost(cost) if (cost === 0 && data.tokens > 0) { const usage = { input_tokens: data.inputTokens, output_tokens: data.outputTokens, cache_creation_input_tokens: data.cacheCreateTokens, cache_read_input_tokens: data.cacheReadTokens } const fallbackResult = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022') finalCost = fallbackResult.costs.total formattedCost = fallbackResult.formatted.total } hourData.apiKeys[apiKeyId] = { name: data.name, tokens: data.tokens, requests: data.requests, cost: finalCost, formattedCost } } trendData.push(hourData) currentHour.setHours(currentHour.getHours() + 1) } } else { // 天粒度统计 const daysCount = parseInt(days) || 7 const today = new Date() // 获取过去N天的数据 for (let i = 0; i < daysCount; i++) { const date = new Date(today) date.setDate(date.getDate() - i) const dateStr = redis.getDateStringInTimezone(date) // 获取这一天所有API Key的数据 const pattern = `usage:daily:*:${dateStr}` const keys = await client.keys(pattern) const dayData = { date: dateStr, apiKeys: {} } // 先收集基础数据 const apiKeyDataMap = new Map() for (const key of keys) { const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/) if (!match) { continue } const apiKeyId = match[1] const data = await client.hgetall(key) if (data && apiKeyMap.has(apiKeyId)) { const inputTokens = parseInt(data.inputTokens) || 0 const outputTokens = parseInt(data.outputTokens) || 0 const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const cacheReadTokens = parseInt(data.cacheReadTokens) || 0 const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens apiKeyDataMap.set(apiKeyId, { name: apiKeyMap.get(apiKeyId).name, tokens: totalTokens, requests: parseInt(data.requests) || 0, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens }) } } // 获取该天的模型级别数据来计算准确费用 const modelPattern = `usage:*:model:daily:*:${dateStr}` const modelKeys = await client.keys(modelPattern) const apiKeyCostMap = new Map() for (const modelKey of modelKeys) { const match = modelKey.match(/usage:(.+?):model:daily:(.+?):\d{4}-\d{2}-\d{2}/) if (!match) { continue } const apiKeyId = match[1] const model = match[2] const modelData = await client.hgetall(modelKey) if (modelData && apiKeyDataMap.has(apiKeyId)) { const usage = { input_tokens: parseInt(modelData.inputTokens) || 0, output_tokens: parseInt(modelData.outputTokens) || 0, cache_creation_input_tokens: parseInt(modelData.cacheCreateTokens) || 0, cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0 } const costResult = CostCalculator.calculateCost(usage, model) const currentCost = apiKeyCostMap.get(apiKeyId) || 0 apiKeyCostMap.set(apiKeyId, currentCost + costResult.costs.total) } } // 组合数据 for (const [apiKeyId, data] of apiKeyDataMap) { const cost = apiKeyCostMap.get(apiKeyId) || 0 // 如果没有模型级别数据,使用默认模型计算(降级方案) let finalCost = cost let formattedCost = CostCalculator.formatCost(cost) if (cost === 0 && data.tokens > 0) { const usage = { input_tokens: data.inputTokens, output_tokens: data.outputTokens, cache_creation_input_tokens: data.cacheCreateTokens, cache_read_input_tokens: data.cacheReadTokens } const fallbackResult = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022') finalCost = fallbackResult.costs.total formattedCost = fallbackResult.formatted.total } dayData.apiKeys[apiKeyId] = { name: data.name, tokens: data.tokens, requests: data.requests, cost: finalCost, formattedCost } } trendData.push(dayData) } } // 按时间正序排列 if (granularity === 'hour') { trendData.sort((a, b) => new Date(a.hour) - new Date(b.hour)) } else { trendData.sort((a, b) => new Date(a.date) - new Date(b.date)) } // 计算每个API Key的总token数,用于排序 const apiKeyTotals = new Map() for (const point of trendData) { for (const [apiKeyId, data] of Object.entries(point.apiKeys)) { apiKeyTotals.set(apiKeyId, (apiKeyTotals.get(apiKeyId) || 0) + data.tokens) } } // 获取前10个使用量最多的API Key const topApiKeys = Array.from(apiKeyTotals.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([apiKeyId]) => apiKeyId) return res.json({ success: true, data: trendData, granularity, topApiKeys, totalApiKeys: apiKeyTotals.size }) } catch (error) { logger.error('❌ Failed to get API keys usage trend:', error) return res .status(500) .json({ error: 'Failed to get API keys usage trend', message: error.message }) } }) // 计算总体使用费用 router.get('/usage-costs', authenticateAdmin, async (req, res) => { try { const { period = 'all' } = req.query // all, today, monthly, 7days logger.info(`💰 Calculating usage costs for period: ${period}`) // 模型名标准化函数(与redis.js保持一致) const normalizeModelName = (model) => { if (!model || model === 'unknown') { return model } // 对于Bedrock模型,去掉区域前缀进行统一 if (model.includes('.anthropic.') || model.includes('.claude')) { // 匹配所有AWS区域格式:region.anthropic.model-name-v1:0 -> claude-model-name // 支持所有AWS区域格式,如:us-east-1, eu-west-1, ap-southeast-1, ca-central-1等 let normalized = model.replace(/^[a-z0-9-]+\./, '') // 去掉任何区域前缀(更通用) normalized = normalized.replace('anthropic.', '') // 去掉anthropic前缀 normalized = normalized.replace(/-v\d+:\d+$/, '') // 去掉版本后缀(如-v1:0, -v2:1等) return normalized } // 对于其他模型,去掉常见的版本后缀 return model.replace(/-v\d+:\d+$|:latest$/, '') } // 获取所有API Keys的使用统计 const apiKeys = await apiKeyService.getAllApiKeys() const totalCosts = { inputCost: 0, outputCost: 0, cacheCreateCost: 0, cacheReadCost: 0, totalCost: 0 } const modelCosts = {} // 按模型统计费用 const client = redis.getClientSafe() const today = redis.getDateStringInTimezone() const tzDate = redis.getDateInTimezone() const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` let pattern if (period === 'today') { pattern = `usage:model:daily:*:${today}` } else if (period === 'monthly') { pattern = `usage:model:monthly:*:${currentMonth}` } else if (period === '7days') { // 最近7天:汇总daily数据 const modelUsageMap = new Map() // 获取最近7天的所有daily统计数据 for (let i = 0; i < 7; i++) { const date = new Date() date.setDate(date.getDate() - i) const currentTzDate = redis.getDateInTimezone(date) const dateStr = `${currentTzDate.getUTCFullYear()}-${String(currentTzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(currentTzDate.getUTCDate()).padStart(2, '0')}` const dayPattern = `usage:model:daily:*:${dateStr}` const dayKeys = await client.keys(dayPattern) for (const key of dayKeys) { const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/) if (!modelMatch) { continue } const rawModel = modelMatch[1] const normalizedModel = normalizeModelName(rawModel) const data = await client.hgetall(key) if (data && Object.keys(data).length > 0) { if (!modelUsageMap.has(normalizedModel)) { modelUsageMap.set(normalizedModel, { inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0 }) } const modelUsage = modelUsageMap.get(normalizedModel) modelUsage.inputTokens += parseInt(data.inputTokens) || 0 modelUsage.outputTokens += parseInt(data.outputTokens) || 0 modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 } } } // 计算7天统计的费用 logger.info(`💰 Processing ${modelUsageMap.size} unique models for 7days cost calculation`) 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) totalCosts.inputCost += costResult.costs.input totalCosts.outputCost += costResult.costs.output totalCosts.cacheCreateCost += costResult.costs.cacheWrite totalCosts.cacheReadCost += costResult.costs.cacheRead totalCosts.totalCost += costResult.costs.total logger.info( `💰 Model ${model} (7days): ${usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens} tokens, cost: ${costResult.formatted.total}` ) // 记录模型费用 modelCosts[model] = { model, requests: 0, // 7天汇总数据没有请求数统计 usage: usageData, costs: costResult.costs, formatted: costResult.formatted, usingDynamicPricing: costResult.usingDynamicPricing } } // 返回7天统计结果 return res.json({ success: true, data: { period, totalCosts: { ...totalCosts, formatted: { inputCost: CostCalculator.formatCost(totalCosts.inputCost), outputCost: CostCalculator.formatCost(totalCosts.outputCost), cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost), cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost), totalCost: CostCalculator.formatCost(totalCosts.totalCost) } }, modelCosts: Object.values(modelCosts) } }) } else { // 全部时间,先尝试从Redis获取所有历史模型统计数据(只使用monthly数据避免重复计算) const allModelKeys = await client.keys('usage:model:monthly:*:*') logger.info(`💰 Total period calculation: found ${allModelKeys.length} monthly model keys`) if (allModelKeys.length > 0) { // 如果有详细的模型统计数据,使用模型级别的计算 const modelUsageMap = new Map() for (const key of allModelKeys) { // 解析模型名称(只处理monthly数据) 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 } } // 使用模型级别的数据计算费用 logger.info(`💰 Processing ${modelUsageMap.size} unique models for total cost calculation`) 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) totalCosts.inputCost += costResult.costs.input totalCosts.outputCost += costResult.costs.output totalCosts.cacheCreateCost += costResult.costs.cacheWrite totalCosts.cacheReadCost += costResult.costs.cacheRead totalCosts.totalCost += costResult.costs.total logger.info( `💰 Model ${model}: ${usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens} tokens, cost: ${costResult.formatted.total}` ) // 记录模型费用 modelCosts[model] = { model, requests: 0, // 历史汇总数据没有请求数 usage: usageData, costs: costResult.costs, formatted: costResult.formatted, usingDynamicPricing: costResult.usingDynamicPricing } } } else { // 如果没有详细的模型统计数据,回退到API Key汇总数据 logger.warn('No detailed model statistics found, falling back to API Key aggregated data') for (const apiKey of apiKeys) { if (apiKey.usage && apiKey.usage.total) { const usage = { input_tokens: apiKey.usage.total.inputTokens || 0, output_tokens: apiKey.usage.total.outputTokens || 0, cache_creation_input_tokens: apiKey.usage.total.cacheCreateTokens || 0, cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0 } // 使用加权平均价格计算(基于当前活跃模型的价格分布) const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022') totalCosts.inputCost += costResult.costs.input totalCosts.outputCost += costResult.costs.output totalCosts.cacheCreateCost += costResult.costs.cacheWrite totalCosts.cacheReadCost += costResult.costs.cacheRead totalCosts.totalCost += costResult.costs.total } } } return res.json({ success: true, data: { period, totalCosts: { ...totalCosts, formatted: { inputCost: CostCalculator.formatCost(totalCosts.inputCost), outputCost: CostCalculator.formatCost(totalCosts.outputCost), cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost), cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost), totalCost: CostCalculator.formatCost(totalCosts.totalCost) } }, modelCosts: Object.values(modelCosts).sort((a, b) => b.costs.total - a.costs.total), pricingServiceStatus: pricingService.getStatus() } }) } // 对于今日或本月,从Redis获取详细的模型统计 const keys = await client.keys(pattern) for (const key of keys) { const match = key.match( period === 'today' ? /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 costResult = CostCalculator.calculateCost(usage, model) // 累加总费用 totalCosts.inputCost += costResult.costs.input totalCosts.outputCost += costResult.costs.output totalCosts.cacheCreateCost += costResult.costs.cacheWrite totalCosts.cacheReadCost += costResult.costs.cacheRead totalCosts.totalCost += costResult.costs.total // 记录模型费用 modelCosts[model] = { model, requests: parseInt(data.requests) || 0, usage, costs: costResult.costs, formatted: costResult.formatted, usingDynamicPricing: costResult.usingDynamicPricing } } } return res.json({ success: true, data: { period, totalCosts: { ...totalCosts, formatted: { inputCost: CostCalculator.formatCost(totalCosts.inputCost), outputCost: CostCalculator.formatCost(totalCosts.outputCost), cacheCreateCost: CostCalculator.formatCost(totalCosts.cacheCreateCost), cacheReadCost: CostCalculator.formatCost(totalCosts.cacheReadCost), totalCost: CostCalculator.formatCost(totalCosts.totalCost) } }, modelCosts: Object.values(modelCosts).sort((a, b) => b.costs.total - a.costs.total), pricingServiceStatus: pricingService.getStatus() } }) } catch (error) { logger.error('❌ Failed to calculate usage costs:', error) return res .status(500) .json({ error: 'Failed to calculate usage costs', message: error.message }) } }) // 📋 获取所有账号的 Claude Code headers 信息 router.get('/claude-code-headers', authenticateAdmin, async (req, res) => { try { const allHeaders = await claudeCodeHeadersService.getAllAccountHeaders() // 获取所有 Claude 账号信息 const accounts = await claudeAccountService.getAllAccounts() const accountMap = {} accounts.forEach((account) => { accountMap[account.id] = account.name }) // 格式化输出 const formattedData = Object.entries(allHeaders).map(([accountId, data]) => ({ accountId, accountName: accountMap[accountId] || 'Unknown', version: data.version, userAgent: data.headers['user-agent'], updatedAt: data.updatedAt, headers: data.headers })) return res.json({ success: true, data: formattedData }) } catch (error) { logger.error('❌ Failed to get Claude Code headers:', error) return res .status(500) .json({ error: 'Failed to get Claude Code headers', message: error.message }) } }) // 🗑️ 清除指定账号的 Claude Code headers router.delete('/claude-code-headers/:accountId', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params await claudeCodeHeadersService.clearAccountHeaders(accountId) return res.json({ success: true, message: `Claude Code headers cleared for account ${accountId}` }) } catch (error) { logger.error('❌ Failed to clear Claude Code headers:', error) return res .status(500) .json({ error: 'Failed to clear Claude Code headers', message: error.message }) } }) // 🔄 版本检查 router.get('/check-updates', authenticateAdmin, async (req, res) => { // 读取当前版本 const versionPath = path.join(__dirname, '../../VERSION') let currentVersion = '1.0.0' try { currentVersion = fs.readFileSync(versionPath, 'utf8').trim() } catch (err) { logger.warn('⚠️ Could not read VERSION file:', err.message) } try { // 从缓存获取 const cacheKey = 'version_check_cache' const cached = await redis.getClient().get(cacheKey) if (cached && !req.query.force) { const cachedData = JSON.parse(cached) const cacheAge = Date.now() - cachedData.timestamp // 缓存有效期1小时 if (cacheAge < 3600000) { // 实时计算 hasUpdate,不使用缓存的值 const hasUpdate = compareVersions(currentVersion, cachedData.latest) < 0 return res.json({ success: true, data: { current: currentVersion, latest: cachedData.latest, hasUpdate, // 实时计算,不用缓存 releaseInfo: cachedData.releaseInfo, cached: true } }) } } // 请求 GitHub API const githubRepo = 'wei-shaw/claude-relay-service' const response = await axios.get(`https://api.github.com/repos/${githubRepo}/releases/latest`, { headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'Claude-Relay-Service' }, timeout: 10000 }) const release = response.data const latestVersion = release.tag_name.replace(/^v/, '') // 比较版本 const hasUpdate = compareVersions(currentVersion, latestVersion) < 0 const releaseInfo = { name: release.name, body: release.body, publishedAt: release.published_at, htmlUrl: release.html_url } // 缓存结果(不缓存 hasUpdate,因为它应该实时计算) await redis.getClient().set( cacheKey, JSON.stringify({ latest: latestVersion, releaseInfo, timestamp: Date.now() }), 'EX', 3600 ) // 1小时过期 return res.json({ success: true, data: { current: currentVersion, latest: latestVersion, hasUpdate, releaseInfo, cached: false } }) } catch (error) { // 改进错误日志记录 const errorDetails = { message: error.message || 'Unknown error', code: error.code, response: error.response ? { status: error.response.status, statusText: error.response.statusText, data: error.response.data } : null, request: error.request ? 'Request was made but no response received' : null } logger.error('❌ Failed to check for updates:', errorDetails.message) // 处理 404 错误 - 仓库或版本不存在 if (error.response && error.response.status === 404) { return res.json({ success: true, data: { current: currentVersion, latest: currentVersion, hasUpdate: false, releaseInfo: { name: 'No releases found', body: 'The GitHub repository has no releases yet.', publishedAt: new Date().toISOString(), htmlUrl: '#' }, warning: 'GitHub repository has no releases' } }) } // 如果是网络错误,尝试返回缓存的数据 if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') { const cacheKey = 'version_check_cache' const cached = await redis.getClient().get(cacheKey) if (cached) { const cachedData = JSON.parse(cached) // 实时计算 hasUpdate const hasUpdate = compareVersions(currentVersion, cachedData.latest) < 0 return res.json({ success: true, data: { current: currentVersion, latest: cachedData.latest, hasUpdate, // 实时计算 releaseInfo: cachedData.releaseInfo, cached: true, warning: 'Using cached data due to network error' } }) } } // 其他错误返回当前版本信息 return res.json({ success: true, data: { current: currentVersion, latest: currentVersion, hasUpdate: false, releaseInfo: { name: 'Update check failed', body: `Unable to check for updates: ${error.message || 'Unknown error'}`, publishedAt: new Date().toISOString(), htmlUrl: '#' }, error: true, warning: error.message || 'Failed to check for updates' } }) } }) // 版本比较函数 function compareVersions(current, latest) { const parseVersion = (v) => { const parts = v.split('.').map(Number) return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 } } const currentV = parseVersion(current) const latestV = parseVersion(latest) if (currentV.major !== latestV.major) { return currentV.major - latestV.major } if (currentV.minor !== latestV.minor) { return currentV.minor - latestV.minor } return currentV.patch - latestV.patch } // 🎨 OEM设置管理 // 获取OEM设置(公开接口,用于显示) router.get('/oem-settings', async (req, res) => { try { const client = redis.getClient() const oemSettings = await client.get('oem:settings') // 默认设置 const defaultSettings = { siteName: 'Claude Relay Service', siteIcon: '', siteIconData: '', // Base64编码的图标数据 updatedAt: new Date().toISOString() } let settings = defaultSettings if (oemSettings) { try { settings = { ...defaultSettings, ...JSON.parse(oemSettings) } } catch (err) { logger.warn('⚠️ Failed to parse OEM settings, using defaults:', err.message) } } return res.json({ success: true, data: settings }) } catch (error) { logger.error('❌ Failed to get OEM settings:', error) return res.status(500).json({ error: 'Failed to get OEM settings', message: error.message }) } }) // 更新OEM设置 router.put('/oem-settings', authenticateAdmin, async (req, res) => { try { const { siteName, siteIcon, siteIconData } = req.body // 验证输入 if (!siteName || typeof siteName !== 'string' || siteName.trim().length === 0) { return res.status(400).json({ error: 'Site name is required' }) } if (siteName.length > 100) { return res.status(400).json({ error: 'Site name must be less than 100 characters' }) } // 验证图标数据大小(如果是base64) if (siteIconData && siteIconData.length > 500000) { // 约375KB return res.status(400).json({ error: 'Icon file must be less than 350KB' }) } // 验证图标URL(如果提供) if (siteIcon && !siteIconData) { // 简单验证URL格式 try { new URL(siteIcon) } catch (err) { return res.status(400).json({ error: 'Invalid icon URL format' }) } } const settings = { siteName: siteName.trim(), siteIcon: (siteIcon || '').trim(), siteIconData: (siteIconData || '').trim(), // Base64数据 updatedAt: new Date().toISOString() } const client = redis.getClient() await client.set('oem:settings', JSON.stringify(settings)) logger.info(`✅ OEM settings updated: ${siteName}`) return res.json({ success: true, message: 'OEM settings updated successfully', data: settings }) } catch (error) { logger.error('❌ Failed to update OEM settings:', error) return res.status(500).json({ error: 'Failed to update OEM settings', message: error.message }) } }) // 🤖 OpenAI 账户管理 // OpenAI OAuth 配置 const OPENAI_CONFIG = { BASE_URL: 'https://auth.openai.com', CLIENT_ID: 'app_EMoamEEZ73f0CkXaXp7hrann', REDIRECT_URI: 'http://localhost:1455/auth/callback', SCOPE: 'openid profile email offline_access' } // 生成 PKCE 参数 function generateOpenAIPKCE() { const codeVerifier = crypto.randomBytes(64).toString('hex') const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url') return { codeVerifier, codeChallenge } } // 生成 OpenAI OAuth 授权 URL router.post('/openai-accounts/generate-auth-url', authenticateAdmin, async (req, res) => { try { const { proxy } = req.body // 生成 PKCE 参数 const pkce = generateOpenAIPKCE() // 生成随机 state const state = crypto.randomBytes(32).toString('hex') // 创建会话 ID const sessionId = crypto.randomUUID() // 将 PKCE 参数和代理配置存储到 Redis await redis.setOAuthSession(sessionId, { codeVerifier: pkce.codeVerifier, codeChallenge: pkce.codeChallenge, state, proxy: proxy || null, platform: 'openai', createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString() }) // 构建授权 URL 参数 const params = new URLSearchParams({ response_type: 'code', client_id: OPENAI_CONFIG.CLIENT_ID, redirect_uri: OPENAI_CONFIG.REDIRECT_URI, scope: OPENAI_CONFIG.SCOPE, code_challenge: pkce.codeChallenge, code_challenge_method: 'S256', state, id_token_add_organizations: 'true', codex_cli_simplified_flow: 'true' }) const authUrl = `${OPENAI_CONFIG.BASE_URL}/oauth/authorize?${params.toString()}` logger.success('🔗 Generated OpenAI OAuth authorization URL') return res.json({ success: true, data: { authUrl, sessionId, instructions: [ '1. 复制上面的链接到浏览器中打开', '2. 登录您的 OpenAI 账户', '3. 同意应用权限', '4. 复制浏览器地址栏中的完整 URL(包含 code 参数)', '5. 在添加账户表单中粘贴完整的回调 URL' ] } }) } catch (error) { logger.error('生成 OpenAI OAuth URL 失败:', error) return res.status(500).json({ success: false, message: '生成授权链接失败', error: error.message }) } }) // 交换 OpenAI 授权码 router.post('/openai-accounts/exchange-code', authenticateAdmin, async (req, res) => { try { const { code, sessionId } = req.body if (!code || !sessionId) { return res.status(400).json({ success: false, message: '缺少必要参数' }) } // 从 Redis 获取会话数据 const sessionData = await redis.getOAuthSession(sessionId) if (!sessionData) { return res.status(400).json({ success: false, message: '会话已过期或无效' }) } // 准备 token 交换请求 const tokenData = { grant_type: 'authorization_code', code: code.trim(), redirect_uri: OPENAI_CONFIG.REDIRECT_URI, client_id: OPENAI_CONFIG.CLIENT_ID, code_verifier: sessionData.codeVerifier } logger.info('Exchanging OpenAI authorization code:', { sessionId, codeLength: code.length, hasCodeVerifier: !!sessionData.codeVerifier }) // 配置代理(如果有) const axiosConfig = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } // 配置代理(如果有) const proxyAgent = ProxyHelper.createProxyAgent(sessionData.proxy) if (proxyAgent) { axiosConfig.httpsAgent = proxyAgent } // 交换 authorization code 获取 tokens const tokenResponse = await axios.post( `${OPENAI_CONFIG.BASE_URL}/oauth/token`, new URLSearchParams(tokenData).toString(), axiosConfig ) const { id_token, access_token, refresh_token, expires_in } = tokenResponse.data // 解析 ID token 获取用户信息 const idTokenParts = id_token.split('.') if (idTokenParts.length !== 3) { throw new Error('Invalid ID token format') } // 解码 JWT payload const payload = JSON.parse(Buffer.from(idTokenParts[1], 'base64url').toString()) // 获取 OpenAI 特定的声明 const authClaims = payload['https://api.openai.com/auth'] || {} const accountId = authClaims.chatgpt_account_id || '' const chatgptUserId = authClaims.chatgpt_user_id || authClaims.user_id || '' const planType = authClaims.chatgpt_plan_type || '' // 获取组织信息 const organizations = authClaims.organizations || [] const defaultOrg = organizations.find((org) => org.is_default) || organizations[0] || {} const organizationId = defaultOrg.id || '' const organizationRole = defaultOrg.role || '' const organizationTitle = defaultOrg.title || '' // 清理 Redis 会话 await redis.deleteOAuthSession(sessionId) logger.success('✅ OpenAI OAuth token exchange successful') return res.json({ success: true, data: { tokens: { idToken: id_token, accessToken: access_token, refreshToken: refresh_token, expires_in }, accountInfo: { accountId, chatgptUserId, organizationId, organizationRole, organizationTitle, planType, email: payload.email || '', name: payload.name || '', emailVerified: payload.email_verified || false, organizations } } }) } catch (error) { logger.error('OpenAI OAuth token exchange failed:', error) return res.status(500).json({ success: false, message: '交换授权码失败', error: error.message }) } }) // 获取所有 OpenAI 账户 router.get('/openai-accounts', authenticateAdmin, async (req, res) => { try { const { platform, groupId } = req.query let accounts = await openaiAccountService.getAllAccounts() // 根据查询参数进行筛选 if (platform && platform !== 'all' && platform !== 'openai') { // 如果指定了其他平台,返回空数组 accounts = [] } // 如果指定了分组筛选 if (groupId && groupId !== 'all') { if (groupId === 'ungrouped') { // 筛选未分组账户 accounts = accounts.filter( (account) => !account.groupInfos || account.groupInfos.length === 0 ) } else { // 筛选特定分组的账户 accounts = accounts.filter( (account) => account.groupInfos && account.groupInfos.some((group) => group.id === groupId) ) } } // 为每个账户添加使用统计信息 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, monthly: usageStats.monthly } } } catch (error) { logger.debug(`Failed to get usage stats for OpenAI account ${account.id}:`, error) return { ...account, usage: { daily: { requests: 0, tokens: 0, allTokens: 0 }, total: { requests: 0, tokens: 0, allTokens: 0 }, monthly: { requests: 0, tokens: 0, allTokens: 0 } } } } }) ) logger.info(`获取 OpenAI 账户列表: ${accountsWithStats.length} 个账户`) return res.json({ success: true, data: accountsWithStats }) } catch (error) { logger.error('获取 OpenAI 账户列表失败:', error) return res.status(500).json({ success: false, message: '获取账户列表失败', error: error.message }) } }) // 创建 OpenAI 账户 router.post('/openai-accounts', authenticateAdmin, async (req, res) => { try { const { name, description, openaiOauth, accountInfo, proxy, accountType, groupId, rateLimitDuration, priority } = req.body if (!name) { return res.status(400).json({ success: false, message: '账户名称不能为空' }) } // 创建账户数据 const accountData = { name, description: description || '', accountType: accountType || 'shared', priority: priority || 50, rateLimitDuration: rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60, openaiOauth: openaiOauth || {}, accountInfo: accountInfo || {}, proxy: proxy || null, isActive: true, schedulable: true } // 创建账户 const createdAccount = await openaiAccountService.createAccount(accountData) // 如果是分组类型,添加到分组 if (accountType === 'group' && groupId) { await accountGroupService.addAccountToGroup(createdAccount.id, groupId, 'openai') } logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${createdAccount.id})`) return res.json({ success: true, data: createdAccount }) } catch (error) { logger.error('创建 OpenAI 账户失败:', error) return res.status(500).json({ success: false, message: '创建账户失败', error: error.message }) } }) // 更新 OpenAI 账户 router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => { try { const { id } = req.params const updates = req.body // 验证accountType的有效性 if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) { return res .status(400) .json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' }) } // 如果更新为分组类型,验证groupId if (updates.accountType === 'group' && !updates.groupId) { return res.status(400).json({ error: 'Group ID is required for group type accounts' }) } // 获取账户当前信息以处理分组变更 const currentAccount = await openaiAccountService.getAccount(id) if (!currentAccount) { return res.status(404).json({ error: 'Account not found' }) } // 处理分组的变更 if (updates.accountType !== undefined) { // 如果之前是分组类型,需要从原分组中移除 if (currentAccount.accountType === 'group') { const oldGroup = await accountGroupService.getAccountGroup(id) if (oldGroup) { await accountGroupService.removeAccountFromGroup(id, oldGroup.id) } } // 如果新类型是分组,添加到新分组 if (updates.accountType === 'group' && updates.groupId) { await accountGroupService.addAccountToGroup(id, updates.groupId, 'openai') } } // 准备更新数据 const updateData = { ...updates } // 处理敏感数据加密 if (updates.openaiOauth) { updateData.openaiOauth = updates.openaiOauth if (updates.openaiOauth.idToken) { updateData.idToken = updates.openaiOauth.idToken } if (updates.openaiOauth.accessToken) { updateData.accessToken = updates.openaiOauth.accessToken } if (updates.openaiOauth.refreshToken) { updateData.refreshToken = updates.openaiOauth.refreshToken } if (updates.openaiOauth.expires_in) { updateData.expiresAt = new Date( Date.now() + updates.openaiOauth.expires_in * 1000 ).toISOString() } } // 更新账户信息 if (updates.accountInfo) { updateData.accountId = updates.accountInfo.accountId || currentAccount.accountId updateData.chatgptUserId = updates.accountInfo.chatgptUserId || currentAccount.chatgptUserId updateData.organizationId = updates.accountInfo.organizationId || currentAccount.organizationId updateData.organizationRole = updates.accountInfo.organizationRole || currentAccount.organizationRole updateData.organizationTitle = updates.accountInfo.organizationTitle || currentAccount.organizationTitle updateData.planType = updates.accountInfo.planType || currentAccount.planType updateData.email = updates.accountInfo.email || currentAccount.email updateData.emailVerified = updates.accountInfo.emailVerified !== undefined ? updates.accountInfo.emailVerified : currentAccount.emailVerified } const updatedAccount = await openaiAccountService.updateAccount(id, updateData) logger.success(`📝 Admin updated OpenAI account: ${id}`) return res.json({ success: true, data: updatedAccount }) } catch (error) { logger.error('❌ Failed to update OpenAI account:', error) return res.status(500).json({ error: 'Failed to update account', message: error.message }) } }) // 删除 OpenAI 账户 router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => { try { const { id } = req.params const account = await openaiAccountService.getAccount(id) if (!account) { return res.status(404).json({ success: false, message: '账户不存在' }) } // 如果账户在分组中,从分组中移除 if (account.accountType === 'group') { const group = await accountGroupService.getAccountGroup(id) if (group) { await accountGroupService.removeAccountFromGroup(id, group.id) } } await openaiAccountService.deleteAccount(id) logger.success(`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id})`) return res.json({ success: true, message: '账户删除成功' }) } catch (error) { logger.error('删除 OpenAI 账户失败:', error) return res.status(500).json({ success: false, message: '删除账户失败', error: error.message }) } }) // 切换 OpenAI 账户状态 router.put('/openai-accounts/:id/toggle', authenticateAdmin, async (req, res) => { try { const { id } = req.params const account = await redis.getOpenAiAccount(id) if (!account) { return res.status(404).json({ success: false, message: '账户不存在' }) } // 切换启用状态 account.enabled = !account.enabled account.updatedAt = new Date().toISOString() // TODO: 更新方法 // await redis.updateOpenAiAccount(id, account) logger.success( `✅ ${account.enabled ? '启用' : '禁用'} OpenAI 账户: ${account.name} (ID: ${id})` ) return res.json({ success: true, data: account }) } catch (error) { logger.error('切换 OpenAI 账户状态失败:', error) return res.status(500).json({ success: false, message: '切换账户状态失败', error: error.message }) } }) // 切换 OpenAI 账户调度状态 router.put( '/openai-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await openaiAccountService.toggleSchedulable(accountId) // 如果账号被禁用,发送webhook通知 if (!result.schedulable) { // 获取账号信息 const account = await redis.getOpenAiAccount(accountId) if (account) { await webhookNotifier.sendAccountAnomalyNotification({ accountId: account.id, accountName: account.name || 'OpenAI Account', platform: 'openai', status: 'disabled', errorCode: 'OPENAI_MANUALLY_DISABLED', reason: '账号已被管理员手动禁用调度', timestamp: new Date().toISOString() }) } } return res.json({ success: result.success, schedulable: result.schedulable, message: result.schedulable ? '已启用调度' : '已禁用调度' }) } catch (error) { logger.error('切换 OpenAI 账户调度状态失败:', error) return res.status(500).json({ success: false, message: '切换调度状态失败', error: error.message }) } } ) // 🌐 Azure OpenAI 账户管理 // 获取所有 Azure OpenAI 账户 router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => { try { const accounts = await azureOpenaiAccountService.getAllAccounts() res.json({ success: true, data: accounts }) } catch (error) { logger.error('Failed to fetch Azure OpenAI accounts:', error) res.status(500).json({ success: false, message: 'Failed to fetch accounts', error: error.message }) } }) // 创建 Azure OpenAI 账户 router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => { try { const { name, description, accountType, azureEndpoint, apiVersion, deploymentName, apiKey, supportedModels, proxy, groupId, priority, isActive, schedulable } = req.body // 验证必填字段 if (!name) { return res.status(400).json({ success: false, message: 'Account name is required' }) } if (!azureEndpoint) { return res.status(400).json({ success: false, message: 'Azure endpoint is required' }) } if (!apiKey) { return res.status(400).json({ success: false, message: 'API key is required' }) } if (!deploymentName) { return res.status(400).json({ success: false, message: 'Deployment name is required' }) } // 验证 Azure endpoint 格式 if (!azureEndpoint.match(/^https:\/\/[\w-]+\.openai\.azure\.com$/)) { return res.status(400).json({ success: false, message: 'Invalid Azure OpenAI endpoint format. Expected: https://your-resource.openai.azure.com' }) } // 测试连接 try { const testUrl = `${azureEndpoint}/openai/deployments/${deploymentName}?api-version=${apiVersion || '2024-02-01'}` await axios.get(testUrl, { headers: { 'api-key': apiKey }, timeout: 5000 }) } catch (testError) { if (testError.response?.status === 404) { logger.warn('Azure OpenAI deployment not found, but continuing with account creation') } else if (testError.response?.status === 401) { return res.status(400).json({ success: false, message: 'Invalid API key or unauthorized access' }) } } const account = await azureOpenaiAccountService.createAccount({ name, description, accountType: accountType || 'shared', azureEndpoint, apiVersion: apiVersion || '2024-02-01', deploymentName, apiKey, supportedModels, proxy, groupId, priority: priority || 50, isActive: isActive !== false, schedulable: schedulable !== false }) res.json({ success: true, data: account, message: 'Azure OpenAI account created successfully' }) } catch (error) { logger.error('Failed to create Azure OpenAI account:', error) res.status(500).json({ success: false, message: 'Failed to create account', error: error.message }) } }) // 更新 Azure OpenAI 账户 router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) => { try { const { id } = req.params const updates = req.body const account = await azureOpenaiAccountService.updateAccount(id, updates) res.json({ success: true, data: account, message: 'Azure OpenAI account updated successfully' }) } catch (error) { logger.error('Failed to update Azure OpenAI account:', error) res.status(500).json({ success: false, message: 'Failed to update account', error: error.message }) } }) // 删除 Azure OpenAI 账户 router.delete('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) => { try { const { id } = req.params await azureOpenaiAccountService.deleteAccount(id) res.json({ success: true, message: 'Azure OpenAI account deleted successfully' }) } catch (error) { logger.error('Failed to delete Azure OpenAI account:', error) res.status(500).json({ success: false, message: 'Failed to delete account', error: error.message }) } }) // 切换 Azure OpenAI 账户状态 router.put('/azure-openai-accounts/:id/toggle', authenticateAdmin, async (req, res) => { try { const { id } = req.params const account = await azureOpenaiAccountService.getAccount(id) if (!account) { return res.status(404).json({ success: false, message: 'Account not found' }) } const newStatus = account.isActive === 'true' ? 'false' : 'true' await azureOpenaiAccountService.updateAccount(id, { isActive: newStatus }) res.json({ success: true, message: `Account ${newStatus === 'true' ? 'activated' : 'deactivated'} successfully`, isActive: newStatus === 'true' }) } catch (error) { logger.error('Failed to toggle Azure OpenAI account status:', error) res.status(500).json({ success: false, message: 'Failed to toggle account status', error: error.message }) } }) // 切换 Azure OpenAI 账户调度状态 router.put( '/azure-openai-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => { try { const { accountId } = req.params const result = await azureOpenaiAccountService.toggleSchedulable(accountId) // 如果账号被禁用,发送webhook通知 if (!result.schedulable) { // 获取账号信息 const account = await azureOpenaiAccountService.getAccount(accountId) if (account) { await webhookNotifier.sendAccountAnomalyNotification({ accountId: account.id, accountName: account.name || 'Azure OpenAI Account', platform: 'azure-openai', status: 'disabled', errorCode: 'AZURE_OPENAI_MANUALLY_DISABLED', reason: '账号已被管理员手动禁用调度', timestamp: new Date().toISOString() }) } } return res.json({ success: true, schedulable: result.schedulable, message: result.schedulable ? '已启用调度' : '已禁用调度' }) } catch (error) { logger.error('切换 Azure OpenAI 账户调度状态失败:', error) return res.status(500).json({ success: false, message: '切换调度状态失败', error: error.message }) } } ) // 健康检查单个 Azure OpenAI 账户 router.post('/azure-openai-accounts/:id/health-check', authenticateAdmin, async (req, res) => { try { const { id } = req.params const healthResult = await azureOpenaiAccountService.healthCheckAccount(id) res.json({ success: true, data: healthResult }) } catch (error) { logger.error('Failed to perform health check:', error) res.status(500).json({ success: false, message: 'Failed to perform health check', error: error.message }) } }) // 批量健康检查所有 Azure OpenAI 账户 router.post('/azure-openai-accounts/health-check-all', authenticateAdmin, async (req, res) => { try { const healthResults = await azureOpenaiAccountService.performHealthChecks() res.json({ success: true, data: healthResults }) } catch (error) { logger.error('Failed to perform batch health check:', error) res.status(500).json({ success: false, message: 'Failed to perform batch health check', error: error.message }) } }) // 迁移 API Keys 以支持 Azure OpenAI router.post('/migrate-api-keys-azure', authenticateAdmin, async (req, res) => { try { const migratedCount = await azureOpenaiAccountService.migrateApiKeysForAzureSupport() res.json({ success: true, message: `Successfully migrated ${migratedCount} API keys for Azure OpenAI support` }) } catch (error) { logger.error('Failed to migrate API keys:', error) res.status(500).json({ success: false, message: 'Failed to migrate API keys', error: error.message }) } }) module.exports = router