Files
claude-relay-service/src/routes/admin.js
shaw 7712d5516c merge: 合并远程 dev 分支,整合 CCR 和 OpenAI-Responses 功能
## 合并内容
- 成功合并远程 dev 分支的 CCR (Claude Connector) 功能
- 保留本地的 OpenAI-Responses 账户管理功能
- 解决所有合并冲突,保留双方功能

## UI 调整
- 将 CCR 平台归类到 Claude 分组中
- 保留新的平台分组选择器设计
- 支持所有平台类型:Claude、CCR、OpenAI、OpenAI-Responses、Gemini、Azure OpenAI、Bedrock

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 15:49:52 +08:00

7459 lines
250 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ccrAccountService = require('../services/ccrAccountService')
const geminiAccountService = require('../services/geminiAccountService')
const openaiAccountService = require('../services/openaiAccountService')
const openaiResponsesAccountService = require('../services/openaiResponsesAccountService')
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 Key分配
router.get('/users', authenticateAdmin, async (req, res) => {
try {
const userService = require('../services/userService')
// Extract query parameters for filtering
const { role, isActive } = req.query
const options = { limit: 1000 }
// Apply role filter if provided
if (role) {
options.role = role
}
// Apply isActive filter if provided, otherwise default to active users only
if (isActive !== undefined) {
options.isActive = isActive === 'true'
} else {
options.isActive = true // Default to active users for backwards compatibility
}
const result = await userService.getAllUsers(options)
// Extract users array from the paginated result
const allUsers = result.users || []
// Map to the format needed for the dropdown
const activeUsers = allUsers.map((user) => ({
id: user.id,
username: user.username,
displayName: user.displayName || user.username,
email: user.email,
role: user.role
}))
// 添加Admin选项作为第一个
const usersWithAdmin = [
{
id: 'admin',
username: 'admin',
displayName: 'Admin',
email: '',
role: 'admin'
},
...activeUsers
]
return res.json({
success: true,
data: usersWithAdmin
})
} catch (error) {
logger.error('❌ Failed to get users list:', error)
return res.status(500).json({
error: 'Failed to get users list',
message: error.message
})
}
})
// 🔑 API Keys 管理
// 调试获取API Key费用详情
router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
const costStats = await redis.getCostStats(keyId)
const dailyCost = await redis.getDailyCost(keyId)
const today = redis.getDateStringInTimezone()
const client = redis.getClientSafe()
// 获取所有相关的Redis键
const costKeys = await client.keys(`usage:cost:*:${keyId}:*`)
const keyValues = {}
for (const key of costKeys) {
keyValues[key] = await client.get(key)
}
return res.json({
keyId,
today,
dailyCost,
costStats,
redisKeys: keyValues,
timezone: config.system.timezoneOffset || 8
})
} catch (error) {
logger.error('❌ Failed to get cost debug info:', error)
return res.status(500).json({ error: 'Failed to get cost debug info', message: error.message })
}
})
// 获取所有API Keys
router.get('/api-keys', authenticateAdmin, async (req, res) => {
try {
const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom
const apiKeys = await apiKeyService.getAllApiKeys()
// 获取用户服务来补充owner信息
const userService = require('../services/userService')
// 根据时间范围计算查询模式
const now = new Date()
const searchPatterns = []
if (timeRange === 'custom' && startDate && endDate) {
// 自定义日期范围
const redisClient = require('../models/redis')
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' })
}
// 限制最大范围为365天
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
if (daysDiff > 365) {
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
}
// 生成日期范围内每天的搜索模式
const currentDate = new Date(start)
while (currentDate <= end) {
const tzDate = redisClient.getDateInTimezone(currentDate)
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
searchPatterns.push(`usage:daily:*:${dateStr}`)
currentDate.setDate(currentDate.getDate() + 1)
}
} else 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')}`
let modelKeys = []
if (timeRange === 'custom' && startDate && endDate) {
// 自定义日期范围:获取范围内所有日期的模型统计
const start = new Date(startDate)
const end = new Date(endDate)
const currentDate = new Date(start)
while (currentDate <= end) {
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}`
const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`)
modelKeys = modelKeys.concat(dayKeys)
currentDate.setDate(currentDate.getDate() + 1)
}
} else {
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' || timeRange === 'custom') {
// today和custom选项已经在查询时过滤了不需要额外处理
}
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]
}
}
// 为每个API Key添加owner的displayName
for (const apiKey of apiKeys) {
// 如果API Key有关联的用户ID获取用户信息
if (apiKey.userId) {
try {
const user = await userService.getUserById(apiKey.userId, false)
if (user) {
apiKey.ownerDisplayName = user.displayName || user.username || 'Unknown User'
} else {
apiKey.ownerDisplayName = 'Unknown User'
}
} catch (error) {
logger.debug(`无法获取用户 ${apiKey.userId} 的信息:`, error)
apiKey.ownerDisplayName = 'Unknown User'
}
} else {
// 如果没有userId使用createdBy字段或默认为Admin
apiKey.ownerDisplayName =
apiKey.createdBy === 'admin' ? 'Admin' : apiKey.createdBy || 'Admin'
}
}
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,
rateLimitCost,
enableModelRestriction,
restrictedModels,
enableClientRestriction,
allowedClients,
dailyCostLimit,
weeklyOpusCostLimit,
tags,
activationDays, // 新增:激活后有效天数
expirationMode, // 新增:过期模式
icon // 新增:图标
} = req.body
// 输入验证
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required and must be a non-empty string' })
}
if (name.length > 100) {
return res.status(400).json({ error: 'Name must be less than 100 characters' })
}
if (description && (typeof description !== 'string' || description.length > 500)) {
return res
.status(400)
.json({ error: 'Description must be a string with less than 500 characters' })
}
if (tokenLimit && (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0)) {
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
}
if (
concurrencyLimit !== undefined &&
concurrencyLimit !== null &&
concurrencyLimit !== '' &&
(!Number.isInteger(Number(concurrencyLimit)) || Number(concurrencyLimit) < 0)
) {
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' })
}
if (
rateLimitWindow !== undefined &&
rateLimitWindow !== null &&
rateLimitWindow !== '' &&
(!Number.isInteger(Number(rateLimitWindow)) || Number(rateLimitWindow) < 1)
) {
return res
.status(400)
.json({ error: 'Rate limit window must be a positive integer (minutes)' })
}
if (
rateLimitRequests !== undefined &&
rateLimitRequests !== null &&
rateLimitRequests !== '' &&
(!Number.isInteger(Number(rateLimitRequests)) || Number(rateLimitRequests) < 1)
) {
return res.status(400).json({ error: 'Rate limit requests must be a positive integer' })
}
// 验证模型限制字段
if (enableModelRestriction !== undefined && typeof enableModelRestriction !== 'boolean') {
return res.status(400).json({ error: 'Enable model restriction must be a boolean' })
}
if (restrictedModels !== undefined && !Array.isArray(restrictedModels)) {
return res.status(400).json({ error: 'Restricted models must be an array' })
}
// 验证客户端限制字段
if (enableClientRestriction !== undefined && typeof enableClientRestriction !== 'boolean') {
return res.status(400).json({ error: 'Enable client restriction must be a boolean' })
}
if (allowedClients !== undefined && !Array.isArray(allowedClients)) {
return res.status(400).json({ error: 'Allowed clients must be an array' })
}
// 验证标签字段
if (tags !== undefined && !Array.isArray(tags)) {
return res.status(400).json({ error: 'Tags must be an array' })
}
if (tags && tags.some((tag) => typeof tag !== 'string' || tag.trim().length === 0)) {
return res.status(400).json({ error: 'All tags must be non-empty strings' })
}
// 验证激活相关字段
if (expirationMode && !['fixed', 'activation'].includes(expirationMode)) {
return res
.status(400)
.json({ error: 'Expiration mode must be either "fixed" or "activation"' })
}
if (expirationMode === 'activation') {
if (
!activationDays ||
!Number.isInteger(Number(activationDays)) ||
Number(activationDays) < 1
) {
return res
.status(400)
.json({ error: 'Activation days must be a positive integer when using activation mode' })
}
// 激活模式下不应该设置固定过期时间
if (expiresAt) {
return res
.status(400)
.json({ error: 'Cannot set fixed expiration date when using activation mode' })
}
}
const newKey = await apiKeyService.generateApiKey({
name,
description,
tokenLimit,
expiresAt,
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
bedrockAccountId,
permissions,
concurrencyLimit,
rateLimitWindow,
rateLimitRequests,
rateLimitCost,
enableModelRestriction,
restrictedModels,
enableClientRestriction,
allowedClients,
dailyCostLimit,
weeklyOpusCostLimit,
tags,
activationDays,
expirationMode,
icon
})
logger.success(`🔑 Admin created new API key: ${name}`)
return res.json({ success: true, data: newKey })
} catch (error) {
logger.error('❌ Failed to create API key:', error)
return res.status(500).json({ error: 'Failed to create API key', message: error.message })
}
})
// 批量创建API Keys
router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
try {
const {
baseName,
count,
description,
tokenLimit,
expiresAt,
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
permissions,
concurrencyLimit,
rateLimitWindow,
rateLimitRequests,
enableModelRestriction,
restrictedModels,
enableClientRestriction,
allowedClients,
dailyCostLimit,
weeklyOpusCostLimit,
tags,
activationDays,
expirationMode,
icon
} = req.body
// 输入验证
if (!baseName || typeof baseName !== 'string' || baseName.trim().length === 0) {
return res.status(400).json({ error: 'Base name is required and must be a non-empty string' })
}
if (!count || !Number.isInteger(count) || count < 2 || count > 500) {
return res.status(400).json({ error: 'Count must be an integer between 2 and 500' })
}
if (baseName.length > 90) {
return res
.status(400)
.json({ error: 'Base name must be less than 90 characters to allow for numbering' })
}
// 生成批量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,
weeklyOpusCostLimit,
tags,
activationDays,
expirationMode,
icon
})
// 保留原始 API Key 供返回
createdKeys.push({
...newKey,
apiKey: newKey.apiKey
})
} catch (error) {
errors.push({
index: i,
name: `${baseName}_${i}`,
error: error.message
})
}
}
// 如果有部分失败,返回部分成功的结果
if (errors.length > 0 && createdKeys.length === 0) {
return res.status(400).json({
success: false,
error: 'Failed to create any API keys',
errors
})
}
// 返回创建的keys包含完整的apiKey
return res.json({
success: true,
data: createdKeys,
errors: errors.length > 0 ? errors : undefined,
summary: {
requested: count,
created: createdKeys.length,
failed: errors.length
}
})
} catch (error) {
logger.error('Failed to batch create API keys:', error)
return res.status(500).json({
success: false,
error: 'Failed to batch create API keys',
message: error.message
})
}
})
// 批量编辑API Keys
router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
try {
const { keyIds, updates } = req.body
if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) {
return res.status(400).json({
error: 'Invalid input',
message: 'keyIds must be a non-empty array'
})
}
if (!updates || typeof updates !== 'object') {
return res.status(400).json({
error: 'Invalid input',
message: 'updates must be an object'
})
}
logger.info(
`🔄 Admin batch editing ${keyIds.length} API keys with updates: ${JSON.stringify(updates)}`
)
logger.info(`🔍 Debug: keyIds received: ${JSON.stringify(keyIds)}`)
const results = {
successCount: 0,
failedCount: 0,
errors: []
}
// 处理每个API Key
for (const keyId of keyIds) {
try {
// 获取当前API Key信息
const currentKey = await redis.getApiKey(keyId)
if (!currentKey || Object.keys(currentKey).length === 0) {
results.failedCount++
results.errors.push(`API key ${keyId} not found`)
continue
}
// 构建最终更新数据
const finalUpdates = {}
// 处理普通字段
if (updates.name) {
finalUpdates.name = updates.name
}
if (updates.tokenLimit !== undefined) {
finalUpdates.tokenLimit = updates.tokenLimit
}
if (updates.rateLimitCost !== undefined) {
finalUpdates.rateLimitCost = updates.rateLimitCost
}
if (updates.concurrencyLimit !== undefined) {
finalUpdates.concurrencyLimit = updates.concurrencyLimit
}
if (updates.rateLimitWindow !== undefined) {
finalUpdates.rateLimitWindow = updates.rateLimitWindow
}
if (updates.rateLimitRequests !== undefined) {
finalUpdates.rateLimitRequests = updates.rateLimitRequests
}
if (updates.dailyCostLimit !== undefined) {
finalUpdates.dailyCostLimit = updates.dailyCostLimit
}
if (updates.weeklyOpusCostLimit !== undefined) {
finalUpdates.weeklyOpusCostLimit = updates.weeklyOpusCostLimit
}
if (updates.permissions !== undefined) {
finalUpdates.permissions = updates.permissions
}
if (updates.isActive !== undefined) {
finalUpdates.isActive = updates.isActive
}
if (updates.monthlyLimit !== undefined) {
finalUpdates.monthlyLimit = updates.monthlyLimit
}
if (updates.priority !== undefined) {
finalUpdates.priority = updates.priority
}
if (updates.enabled !== undefined) {
finalUpdates.enabled = updates.enabled
}
// 处理账户绑定
if (updates.claudeAccountId !== undefined) {
finalUpdates.claudeAccountId = updates.claudeAccountId
}
if (updates.claudeConsoleAccountId !== undefined) {
finalUpdates.claudeConsoleAccountId = updates.claudeConsoleAccountId
}
if (updates.geminiAccountId !== undefined) {
finalUpdates.geminiAccountId = updates.geminiAccountId
}
if (updates.openaiAccountId !== undefined) {
finalUpdates.openaiAccountId = updates.openaiAccountId
}
if (updates.bedrockAccountId !== undefined) {
finalUpdates.bedrockAccountId = updates.bedrockAccountId
}
// 处理标签操作
if (updates.tags !== undefined) {
if (updates.tagOperation) {
const currentTags = currentKey.tags ? JSON.parse(currentKey.tags) : []
const operationTags = updates.tags
switch (updates.tagOperation) {
case 'replace': {
finalUpdates.tags = operationTags
break
}
case 'add': {
const newTags = [...currentTags]
operationTags.forEach((tag) => {
if (!newTags.includes(tag)) {
newTags.push(tag)
}
})
finalUpdates.tags = newTags
break
}
case 'remove': {
finalUpdates.tags = currentTags.filter((tag) => !operationTags.includes(tag))
break
}
}
} else {
// 如果没有指定操作类型,默认为替换
finalUpdates.tags = updates.tags
}
}
// 执行更新
await apiKeyService.updateApiKey(keyId, finalUpdates)
results.successCount++
logger.success(`✅ Batch edit: API key ${keyId} updated successfully`)
} catch (error) {
results.failedCount++
results.errors.push(`Failed to update key ${keyId}: ${error.message}`)
logger.error(`❌ Batch edit failed for key ${keyId}:`, error)
}
}
// 记录批量编辑结果
if (results.successCount > 0) {
logger.success(
`🎉 Batch edit completed: ${results.successCount} successful, ${results.failedCount} failed`
)
} else {
logger.warn(
`⚠️ Batch edit completed with no successful updates: ${results.failedCount} failed`
)
}
return res.json({
success: true,
message: `批量编辑完成`,
data: results
})
} catch (error) {
logger.error('❌ Failed to batch edit API keys:', error)
return res.status(500).json({
error: 'Batch edit failed',
message: error.message
})
}
})
// 更新API Key
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
const {
name, // 添加名称字段
tokenLimit,
concurrencyLimit,
rateLimitWindow,
rateLimitRequests,
rateLimitCost,
isActive,
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
bedrockAccountId,
permissions,
enableModelRestriction,
restrictedModels,
enableClientRestriction,
allowedClients,
expiresAt,
dailyCostLimit,
weeklyOpusCostLimit,
tags,
ownerId // 新增所有者ID字段
} = req.body
// 只允许更新指定字段
const updates = {}
// 处理名称字段
if (name !== undefined && name !== null && name !== '') {
const trimmedName = name.toString().trim()
if (trimmedName.length === 0) {
return res.status(400).json({ error: 'API Key name cannot be empty' })
}
if (trimmedName.length > 100) {
return res.status(400).json({ error: 'API Key name must be less than 100 characters' })
}
updates.name = trimmedName
}
if (tokenLimit !== undefined && tokenLimit !== null && tokenLimit !== '') {
if (!Number.isInteger(Number(tokenLimit)) || Number(tokenLimit) < 0) {
return res.status(400).json({ error: 'Token limit must be a non-negative integer' })
}
updates.tokenLimit = Number(tokenLimit)
}
if (concurrencyLimit !== undefined && concurrencyLimit !== null && concurrencyLimit !== '') {
if (!Number.isInteger(Number(concurrencyLimit)) || Number(concurrencyLimit) < 0) {
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' })
}
updates.concurrencyLimit = Number(concurrencyLimit)
}
if (rateLimitWindow !== undefined && rateLimitWindow !== null && rateLimitWindow !== '') {
if (!Number.isInteger(Number(rateLimitWindow)) || Number(rateLimitWindow) < 0) {
return res
.status(400)
.json({ error: 'Rate limit window must be a non-negative integer (minutes)' })
}
updates.rateLimitWindow = Number(rateLimitWindow)
}
if (rateLimitRequests !== undefined && rateLimitRequests !== null && rateLimitRequests !== '') {
if (!Number.isInteger(Number(rateLimitRequests)) || Number(rateLimitRequests) < 0) {
return res.status(400).json({ error: 'Rate limit requests must be a non-negative integer' })
}
updates.rateLimitRequests = Number(rateLimitRequests)
}
if (rateLimitCost !== undefined && rateLimitCost !== null && rateLimitCost !== '') {
const cost = Number(rateLimitCost)
if (isNaN(cost) || cost < 0) {
return res.status(400).json({ error: 'Rate limit cost must be a non-negative number' })
}
updates.rateLimitCost = cost
}
if (claudeAccountId !== undefined) {
// 空字符串表示解绑null或空字符串都设置为空字符串
updates.claudeAccountId = claudeAccountId || ''
}
if (claudeConsoleAccountId !== undefined) {
// 空字符串表示解绑null或空字符串都设置为空字符串
updates.claudeConsoleAccountId = claudeConsoleAccountId || ''
}
if (geminiAccountId !== undefined) {
// 空字符串表示解绑null或空字符串都设置为空字符串
updates.geminiAccountId = geminiAccountId || ''
}
if (openaiAccountId !== undefined) {
// 空字符串表示解绑null或空字符串都设置为空字符串
updates.openaiAccountId = openaiAccountId || ''
}
if (bedrockAccountId !== undefined) {
// 空字符串表示解绑null或空字符串都设置为空字符串
updates.bedrockAccountId = bedrockAccountId || ''
}
if (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
}
// 处理 Opus 周费用限制
if (
weeklyOpusCostLimit !== undefined &&
weeklyOpusCostLimit !== null &&
weeklyOpusCostLimit !== ''
) {
const costLimit = Number(weeklyOpusCostLimit)
// 明确验证非负数0 表示禁用,负数无意义)
if (isNaN(costLimit) || costLimit < 0) {
return res
.status(400)
.json({ error: 'Weekly Opus cost limit must be a non-negative number' })
}
updates.weeklyOpusCostLimit = costLimit
}
// 处理标签
if (tags !== undefined) {
if (!Array.isArray(tags)) {
return res.status(400).json({ error: 'Tags must be an array' })
}
if (tags.some((tag) => typeof tag !== 'string' || tag.trim().length === 0)) {
return res.status(400).json({ error: 'All tags must be non-empty strings' })
}
updates.tags = tags
}
// 处理活跃/禁用状态状态, 放在过期处理后以确保后续增加禁用key功能
if (isActive !== undefined) {
if (typeof isActive !== 'boolean') {
return res.status(400).json({ error: 'isActive must be a boolean' })
}
updates.isActive = isActive
}
// 处理所有者变更
if (ownerId !== undefined) {
const userService = require('../services/userService')
if (ownerId === 'admin') {
// 分配给Admin
updates.userId = ''
updates.userUsername = ''
updates.createdBy = 'admin'
} else if (ownerId) {
// 分配给用户
try {
const user = await userService.getUserById(ownerId, false)
if (!user) {
return res.status(400).json({ error: 'Invalid owner: User not found' })
}
if (!user.isActive) {
return res.status(400).json({ error: 'Cannot assign to inactive user' })
}
// 设置新的所有者信息
updates.userId = ownerId
updates.userUsername = user.username
updates.createdBy = user.username
// 管理员重新分配时不检查用户的API Key数量限制
logger.info(`🔄 Admin reassigning API key ${keyId} to user ${user.username}`)
} catch (error) {
logger.error('Error fetching user for owner reassignment:', error)
return res.status(400).json({ error: 'Invalid owner ID' })
}
} else {
// 清空所有者分配给Admin
updates.userId = ''
updates.userUsername = ''
updates.createdBy = 'admin'
}
}
await apiKeyService.updateApiKey(keyId, updates)
logger.success(`📝 Admin updated API key: ${keyId}`)
return res.json({ success: true, message: 'API key updated successfully' })
} catch (error) {
logger.error('❌ Failed to update API key:', error)
return res.status(500).json({ error: 'Failed to update API key', message: error.message })
}
})
// 修改API Key过期时间包括手动激活功能
router.patch('/api-keys/:keyId/expiration', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
const { expiresAt, activateNow } = req.body
// 获取当前API Key信息
const keyData = await redis.getApiKey(keyId)
if (!keyData || Object.keys(keyData).length === 0) {
return res.status(404).json({ error: 'API key not found' })
}
const updates = {}
// 如果是激活操作用于未激活的key
if (activateNow === true) {
if (keyData.expirationMode === 'activation' && keyData.isActivated !== 'true') {
const now = new Date()
const activationDays = parseInt(keyData.activationDays || 30)
const newExpiresAt = new Date(now.getTime() + activationDays * 24 * 60 * 60 * 1000)
updates.isActivated = 'true'
updates.activatedAt = now.toISOString()
updates.expiresAt = newExpiresAt.toISOString()
logger.success(
`🔓 API key manually activated by admin: ${keyId} (${keyData.name}), expires at ${newExpiresAt.toISOString()}`
)
} else {
return res.status(400).json({
error: 'Cannot activate',
message: 'Key is either already activated or not in activation mode'
})
}
}
// 如果提供了新的过期时间(但不是激活操作)
if (expiresAt !== undefined && activateNow !== true) {
// 验证过期时间格式
if (expiresAt && isNaN(Date.parse(expiresAt))) {
return res.status(400).json({ error: 'Invalid expiration date format' })
}
// 如果设置了过期时间确保key是激活状态
if (expiresAt) {
updates.expiresAt = new Date(expiresAt).toISOString()
// 如果之前是未激活状态,现在激活它
if (keyData.isActivated !== 'true') {
updates.isActivated = 'true'
updates.activatedAt = new Date().toISOString()
}
} else {
// 清除过期时间(永不过期)
updates.expiresAt = ''
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No valid updates provided' })
}
// 更新API Key
await apiKeyService.updateApiKey(keyId, updates)
logger.success(`📝 Updated API key expiration: ${keyId} (${keyData.name})`)
return res.json({
success: true,
message: 'API key expiration updated successfully',
updates
})
} catch (error) {
logger.error('❌ Failed to update API key expiration:', error)
return res.status(500).json({
error: 'Failed to update API key expiration',
message: error.message
})
}
})
// 批量删除API Keys必须在 :keyId 路由之前定义)
router.delete('/api-keys/batch', authenticateAdmin, async (req, res) => {
try {
const { keyIds } = req.body
// 调试信息
logger.info(`🐛 Batch delete request body: ${JSON.stringify(req.body)}`)
logger.info(`🐛 keyIds type: ${typeof keyIds}, value: ${JSON.stringify(keyIds)}`)
// 参数验证
if (!keyIds || !Array.isArray(keyIds) || keyIds.length === 0) {
logger.warn(
`🚨 Invalid keyIds: ${JSON.stringify({ keyIds, type: typeof keyIds, isArray: Array.isArray(keyIds) })}`
)
return res.status(400).json({
error: 'Invalid request',
message: 'keyIds 必须是一个非空数组'
})
}
if (keyIds.length > 100) {
return res.status(400).json({
error: 'Too many keys',
message: '每次最多只能删除100个API Keys'
})
}
// 验证keyIds格式
const invalidKeys = keyIds.filter((id) => !id || typeof id !== 'string')
if (invalidKeys.length > 0) {
return res.status(400).json({
error: 'Invalid key IDs',
message: '包含无效的API Key ID'
})
}
logger.info(
`🗑️ Admin attempting batch delete of ${keyIds.length} API keys: ${JSON.stringify(keyIds)}`
)
const results = {
successCount: 0,
failedCount: 0,
errors: []
}
// 逐个删除,记录成功和失败情况
for (const keyId of keyIds) {
try {
// 检查API Key是否存在
const apiKey = await redis.getApiKey(keyId)
if (!apiKey || Object.keys(apiKey).length === 0) {
results.failedCount++
results.errors.push({ keyId, error: 'API Key 不存在' })
continue
}
// 执行删除
await apiKeyService.deleteApiKey(keyId)
results.successCount++
logger.success(`✅ Batch delete: API key ${keyId} deleted successfully`)
} catch (error) {
results.failedCount++
results.errors.push({
keyId,
error: error.message || '删除失败'
})
logger.error(`❌ Batch delete failed for key ${keyId}:`, error)
}
}
// 记录批量删除结果
if (results.successCount > 0) {
logger.success(
`🎉 Batch delete completed: ${results.successCount} successful, ${results.failedCount} failed`
)
} else {
logger.warn(
`⚠️ Batch delete completed with no successful deletions: ${results.failedCount} failed`
)
}
return res.json({
success: true,
message: `批量删除完成`,
data: results
})
} catch (error) {
logger.error('❌ Failed to batch delete API keys:', error)
return res.status(500).json({
error: 'Batch delete failed',
message: error.message
})
}
})
// 删除单个API Key必须在批量删除路由之后定义
router.delete('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
await apiKeyService.deleteApiKey(keyId, req.admin.username, 'admin')
logger.success(`🗑️ Admin deleted API key: ${keyId}`)
return res.json({ success: true, message: 'API key deleted successfully' })
} catch (error) {
logger.error('❌ Failed to delete API key:', error)
return res.status(500).json({ error: 'Failed to delete API key', message: error.message })
}
})
// 📋 获取已删除的API Keys
router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => {
try {
const deletedApiKeys = await apiKeyService.getAllApiKeys(true) // Include deleted
const onlyDeleted = deletedApiKeys.filter((key) => key.isDeleted === 'true')
// Add additional metadata for deleted keys
const enrichedKeys = onlyDeleted.map((key) => ({
...key,
isDeleted: key.isDeleted === 'true',
deletedAt: key.deletedAt,
deletedBy: key.deletedBy,
deletedByType: key.deletedByType,
canRestore: true // 已删除的API Key可以恢复
}))
logger.success(`📋 Admin retrieved ${enrichedKeys.length} deleted API keys`)
return res.json({ success: true, apiKeys: enrichedKeys, total: enrichedKeys.length })
} catch (error) {
logger.error('❌ Failed to get deleted API keys:', error)
return res
.status(500)
.json({ error: 'Failed to retrieve deleted API keys', message: error.message })
}
})
// 🔄 恢复已删除的API Key
router.post('/api-keys/:keyId/restore', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
const adminUsername = req.session?.admin?.username || 'unknown'
// 调用服务层的恢复方法
const result = await apiKeyService.restoreApiKey(keyId, adminUsername, 'admin')
if (result.success) {
logger.success(`✅ Admin ${adminUsername} restored API key: ${keyId}`)
return res.json({
success: true,
message: 'API Key 已成功恢复',
apiKey: result.apiKey
})
} else {
return res.status(400).json({
success: false,
error: 'Failed to restore API key'
})
}
} catch (error) {
logger.error('❌ Failed to restore API key:', error)
// 根据错误类型返回适当的响应
if (error.message === 'API key not found') {
return res.status(404).json({
success: false,
error: 'API Key 不存在'
})
} else if (error.message === 'API key is not deleted') {
return res.status(400).json({
success: false,
error: '该 API Key 未被删除,无需恢复'
})
}
return res.status(500).json({
success: false,
error: '恢复 API Key 失败',
message: error.message
})
}
})
// 🗑️ 彻底删除API Key物理删除
router.delete('/api-keys/:keyId/permanent', authenticateAdmin, async (req, res) => {
try {
const { keyId } = req.params
const adminUsername = req.session?.admin?.username || 'unknown'
// 调用服务层的彻底删除方法
const result = await apiKeyService.permanentDeleteApiKey(keyId)
if (result.success) {
logger.success(`🗑️ Admin ${adminUsername} permanently deleted API key: ${keyId}`)
return res.json({
success: true,
message: 'API Key 已彻底删除'
})
}
} catch (error) {
logger.error('❌ Failed to permanently delete API key:', error)
if (error.message === 'API key not found') {
return res.status(404).json({
success: false,
error: 'API Key 不存在'
})
} else if (error.message === '只能彻底删除已经删除的API Key') {
return res.status(400).json({
success: false,
error: '只能彻底删除已经删除的API Key'
})
}
return res.status(500).json({
success: false,
error: '彻底删除 API Key 失败',
message: error.message
})
}
})
// 🧹 清空所有已删除的API Keys
router.delete('/api-keys/deleted/clear-all', authenticateAdmin, async (req, res) => {
try {
const adminUsername = req.session?.admin?.username || 'unknown'
// 调用服务层的清空方法
const result = await apiKeyService.clearAllDeletedApiKeys()
logger.success(
`🧹 Admin ${adminUsername} cleared deleted API keys: ${result.successCount}/${result.total}`
)
return res.json({
success: true,
message: `成功清空 ${result.successCount} 个已删除的 API Keys`,
details: {
total: result.total,
successCount: result.successCount,
failedCount: result.failedCount,
errors: result.errors
}
})
} catch (error) {
logger.error('❌ Failed to clear all deleted API keys:', error)
return res.status(500).json({
success: false,
error: '清空已删除的 API Keys 失败',
message: error.message
})
}
})
// 👥 账户分组管理
// 创建账户分组
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') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(account.id)
// 获取会话窗口使用统计(仅对有活跃窗口的账户)
let sessionWindowUsage = null
if (account.sessionWindow && account.sessionWindow.hasActiveWindow) {
const windowUsage = await redis.getAccountSessionWindowUsage(
account.id,
account.sessionWindow.windowStart,
account.sessionWindow.windowEnd
)
// 计算会话窗口的总费用
let totalCost = 0
const modelCosts = {}
for (const [modelName, usage] of Object.entries(windowUsage.modelUsage)) {
const usageData = {
input_tokens: usage.inputTokens,
output_tokens: usage.outputTokens,
cache_creation_input_tokens: usage.cacheCreateTokens,
cache_read_input_tokens: usage.cacheReadTokens
}
logger.debug(`💰 Calculating cost for model ${modelName}:`, JSON.stringify(usageData))
const costResult = CostCalculator.calculateCost(usageData, modelName)
logger.debug(`💰 Cost result for ${modelName}: total=${costResult.costs.total}`)
modelCosts[modelName] = {
...usage,
cost: costResult.costs.total
}
totalCost += costResult.costs.total
}
sessionWindowUsage = {
totalTokens: windowUsage.totalAllTokens,
totalRequests: windowUsage.totalRequests,
totalCost,
modelUsage: modelCosts
}
}
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
groupInfos,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages,
sessionWindow: sessionWindowUsage
}
}
} catch (statsError) {
logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message)
// 如果获取统计失败,返回空统计
try {
const groupInfos = await accountGroupService.getAccountGroups(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 },
sessionWindow: null
}
}
} 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 },
sessionWindow: null
}
}
}
}
})
)
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,
groupIds,
autoStopOnWarning,
useUnifiedUserAgent,
useUnifiedClientId,
unifiedClientId
} = 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或groupIds
if (accountType === 'group' && !groupId && (!groupIds || groupIds.length === 0)) {
return res
.status(400)
.json({ error: 'Group ID or Group IDs are 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
autoStopOnWarning: autoStopOnWarning === true, // 默认为false
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
useUnifiedClientId: useUnifiedClientId === true, // 默认为false
unifiedClientId: unifiedClientId || '' // 统一的客户端标识
})
// 如果是分组类型,将账户添加到分组
if (accountType === 'group') {
if (groupIds && groupIds.length > 0) {
// 使用多分组设置
await accountGroupService.setAccountGroups(newAccount.id, groupIds, newAccount.platform)
} else if (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或groupIds
if (
updates.accountType === 'group' &&
!updates.groupId &&
(!updates.groupIds || updates.groupIds.length === 0)
) {
return res
.status(400)
.json({ error: 'Group ID or Group IDs are 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') {
await accountGroupService.removeAccountFromAllGroups(accountId)
}
// 如果新类型是分组,添加到新分组
if (updates.accountType === 'group') {
// 处理多分组/单分组的兼容性
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
if (updates.groupIds && updates.groupIds.length > 0) {
// 使用多分组设置
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (updates.groupId) {
// 兼容单分组模式
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') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
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.getAccountGroups(account.id)
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
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,
dailyQuota,
quotaResetTime
} = 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',
dailyQuota: dailyQuota || 0,
quotaResetTime: quotaResetTime || '00:00'
})
// 如果是分组类型将账户添加到分组CCR 归属 Claude 平台分组)
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.getAccountGroups(accountId)
for (const oldGroup of oldGroups) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
}
}
// 如果新类型是分组,处理多分组支持
if (updates.accountType === 'group') {
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
// 如果明确提供了 groupIds 参数(包括空数组)
if (updates.groupIds && updates.groupIds.length > 0) {
// 设置新的多分组
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (updates.groupId) {
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
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 })
}
}
)
// 获取Claude Console账户的使用统计
router.get('/claude-console-accounts/:accountId/usage', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const usageStats = await claudeConsoleAccountService.getAccountUsageStats(accountId)
if (!usageStats) {
return res.status(404).json({ error: 'Account not found' })
}
return res.json(usageStats)
} catch (error) {
logger.error('❌ Failed to get Claude Console account usage stats:', error)
return res.status(500).json({ error: 'Failed to get usage stats', message: error.message })
}
})
// 手动重置Claude Console账户的每日使用量
router.post(
'/claude-console-accounts/:accountId/reset-usage',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
await claudeConsoleAccountService.resetDailyUsage(accountId)
logger.success(`✅ Admin manually reset daily usage for Claude Console account: ${accountId}`)
return res.json({ success: true, message: 'Daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset Claude Console account daily usage:', error)
return res.status(500).json({ error: 'Failed to reset daily usage', message: error.message })
}
}
)
// 重置Claude Console账户状态清除所有异常状态
router.post(
'/claude-console-accounts/:accountId/reset-status',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
const result = await claudeConsoleAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for Claude Console account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Claude Console account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
}
)
// 手动重置所有Claude Console账户的每日使用量
router.post('/claude-console-accounts/reset-all-usage', authenticateAdmin, async (req, res) => {
try {
await claudeConsoleAccountService.resetAllDailyUsage()
logger.success('✅ Admin manually reset daily usage for all Claude Console accounts')
return res.json({ success: true, message: 'All daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset all Claude Console accounts daily usage:', error)
return res
.status(500)
.json({ error: 'Failed to reset all daily usage', message: error.message })
}
})
// 🔧 CCR 账户管理
// 获取所有CCR账户
router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
let accounts = await ccrAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'ccr') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id)
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
groupInfos,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
}
} catch (statsError) {
logger.warn(
`⚠️ Failed to get usage stats for CCR account ${account.id}:`,
statsError.message
)
try {
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
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 CCR 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 CCR accounts:', error)
return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
}
})
// 创建新的CCR账户
router.post('/ccr-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
apiUrl,
apiKey,
priority,
supportedModels,
userAgent,
rateLimitDuration,
proxy,
accountType,
groupId,
dailyQuota,
quotaResetTime
} = 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 ccrAccountService.createAccount({
name,
description,
apiUrl,
apiKey,
priority: priority || 50,
supportedModels: supportedModels || [],
userAgent,
rateLimitDuration:
rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60,
proxy,
accountType: accountType || 'shared',
dailyQuota: dailyQuota || 0,
quotaResetTime: quotaResetTime || '00:00'
})
// 如果是分组类型,将账户添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, groupId)
}
logger.success(`🔧 Admin created CCR account: ${name}`)
return res.json({ success: true, data: newAccount })
} catch (error) {
logger.error('❌ Failed to create CCR account:', error)
return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
}
})
// 更新CCR账户
router.put('/ccr-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 ccrAccountService.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.getAccountGroups(accountId)
for (const oldGroup of oldGroups) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
}
}
// 如果新类型是分组,处理多分组支持
if (updates.accountType === 'group') {
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
// 如果明确提供了 groupIds 参数(包括空数组)
if (updates.groupIds && updates.groupIds.length > 0) {
// 设置新的多分组
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (updates.groupId) {
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
}
}
}
await ccrAccountService.updateAccount(accountId, updates)
logger.success(`📝 Admin updated CCR account: ${accountId}`)
return res.json({ success: true, message: 'CCR account updated successfully' })
} catch (error) {
logger.error('❌ Failed to update CCR account:', error)
return res.status(500).json({ error: 'Failed to update CCR account', message: error.message })
}
})
// 删除CCR账户
router.delete('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
// 获取账户信息以检查是否在分组中
const account = await ccrAccountService.getAccount(accountId)
if (account && account.accountType === 'group') {
const groups = await accountGroupService.getAccountGroups(accountId)
for (const group of groups) {
await accountGroupService.removeAccountFromGroup(accountId, group.id)
}
}
await ccrAccountService.deleteAccount(accountId)
logger.success(`🗑️ Admin deleted CCR account: ${accountId}`)
return res.json({ success: true, message: 'CCR account deleted successfully' })
} catch (error) {
logger.error('❌ Failed to delete CCR account:', error)
return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message })
}
})
// 切换CCR账户状态
router.put('/ccr-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const account = await ccrAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
const newStatus = !account.isActive
await ccrAccountService.updateAccount(accountId, { isActive: newStatus })
logger.success(
`🔄 Admin toggled CCR account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`
)
return res.json({ success: true, isActive: newStatus })
} catch (error) {
logger.error('❌ Failed to toggle CCR account status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle account status', message: error.message })
}
})
// 切换CCR账户调度状态
router.put('/ccr-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const account = await ccrAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
const newSchedulable = !account.schedulable
await ccrAccountService.updateAccount(accountId, { schedulable: newSchedulable })
// 如果账号被禁用发送webhook通知
if (!newSchedulable) {
await webhookNotifier.sendAccountAnomalyNotification({
accountId: account.id,
accountName: account.name || 'CCR Account',
platform: 'ccr',
status: 'disabled',
errorCode: 'CCR_MANUALLY_DISABLED',
reason: '账号已被管理员手动禁用调度',
timestamp: new Date().toISOString()
})
}
logger.success(
`🔄 Admin toggled CCR account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`
)
return res.json({ success: true, schedulable: newSchedulable })
} catch (error) {
logger.error('❌ Failed to toggle CCR account schedulable status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: error.message })
}
})
// 获取CCR账户的使用统计
router.get('/ccr-accounts/:accountId/usage', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const usageStats = await ccrAccountService.getAccountUsageStats(accountId)
if (!usageStats) {
return res.status(404).json({ error: 'Account not found' })
}
return res.json(usageStats)
} catch (error) {
logger.error('❌ Failed to get CCR account usage stats:', error)
return res.status(500).json({ error: 'Failed to get usage stats', message: error.message })
}
})
// 手动重置CCR账户的每日使用量
router.post('/ccr-accounts/:accountId/reset-usage', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
await ccrAccountService.resetDailyUsage(accountId)
logger.success(`✅ Admin manually reset daily usage for CCR account: ${accountId}`)
return res.json({ success: true, message: 'Daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset CCR account daily usage:', error)
return res.status(500).json({ error: 'Failed to reset daily usage', message: error.message })
}
})
// 重置CCR账户状态清除所有异常状态
router.post('/ccr-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await ccrAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for CCR account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset CCR account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
// 手动重置所有CCR账户的每日使用量
router.post('/ccr-accounts/reset-all-usage', authenticateAdmin, async (req, res) => {
try {
await ccrAccountService.resetAllDailyUsage()
logger.success('✅ Admin manually reset daily usage for all CCR accounts')
return res.json({ success: true, message: 'All daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset all CCR accounts daily usage:', error)
return res
.status(500)
.json({ error: 'Failed to reset all daily usage', 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') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(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.getAccountGroups(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, proxy)
// 创建 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') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息与Claude账户相同的逻辑
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(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.getAccountGroups(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.getAccountGroups(accountId)
for (const oldGroup of oldGroups) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
}
}
// 如果新类型是分组,处理多分组支持
if (updates.accountType === 'group') {
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
// 如果明确提供了 groupIds 参数(包括空数组)
if (updates.groupIds && updates.groupIds.length > 0) {
// 设置新的多分组
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'gemini')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (updates.groupId) {
// 向后兼容:仅当没有 groupIds 但有 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,
ccrAccounts,
todayStats,
systemAverages,
realtimeMetrics
] = await Promise.all([
redis.getSystemStats(),
apiKeyService.getAllApiKeys(),
claudeAccountService.getAllAccounts(),
claudeConsoleAccountService.getAllAccounts(),
geminiAccountService.getAllAccounts(),
bedrockAccountService.getAllAccounts(),
ccrAccountService.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
// CCR账户统计
const normalCcrAccounts = ccrAccounts.filter(
(acc) =>
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
const abnormalCcrAccounts = ccrAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
).length
const pausedCcrAccounts = ccrAccounts.filter(
(acc) =>
acc.schedulable === false &&
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length
const rateLimitedCcrAccounts = ccrAccounts.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 +
ccrAccounts.length,
normalAccounts:
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts +
normalOpenAIAccounts +
normalCcrAccounts,
abnormalAccounts:
abnormalClaudeAccounts +
abnormalClaudeConsoleAccounts +
abnormalGeminiAccounts +
abnormalBedrockAccounts +
abnormalOpenAIAccounts +
abnormalCcrAccounts,
pausedAccounts:
pausedClaudeAccounts +
pausedClaudeConsoleAccounts +
pausedGeminiAccounts +
pausedBedrockAccounts +
pausedOpenAIAccounts +
pausedCcrAccounts,
rateLimitedAccounts:
rateLimitedClaudeAccounts +
rateLimitedClaudeConsoleAccounts +
rateLimitedGeminiAccounts +
rateLimitedBedrockAccounts +
rateLimitedOpenAIAccounts +
rateLimitedCcrAccounts,
// 各平台详细统计
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
},
ccr: {
total: ccrAccounts.length,
normal: normalCcrAccounts,
abnormal: abnormalCcrAccounts,
paused: pausedCcrAccounts,
rateLimited: rateLimitedCcrAccounts
}
},
// 保留旧字段以兼容
activeAccounts:
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts +
normalOpenAIAccounts +
normalCcrAccounts,
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' })
}
// 限制最大范围为365天
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
if (daysDiff > 365) {
return res.status(400).json({ error: 'Date range cannot exceed 365 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' })
}
// 限制最大范围为365天
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
if (daysDiff > 365) {
return res.status(400).json({ error: 'Date range cannot exceed 365 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编码的图标数据
showAdminButton: true, // 是否显示管理后台按钮
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)
}
}
// 添加 LDAP 启用状态到响应中
return res.json({
success: true,
data: {
...settings,
ldapEnabled: config.ldap && config.ldap.enabled === true
}
})
} 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, showAdminButton } = 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数据
showAdminButton: showAdminButton !== false, // 默认为true
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') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
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,
needsImmediateRefresh, // 是否需要立即刷新
requireRefreshSuccess // 是否必须刷新成功才能创建
} = 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
}
// 如果需要立即刷新且必须成功OpenAI 手动模式)
if (needsImmediateRefresh && requireRefreshSuccess) {
// 先创建临时账户以测试刷新
const tempAccount = await openaiAccountService.createAccount(accountData)
try {
logger.info(`🔄 测试刷新 OpenAI 账户以获取完整 token 信息`)
// 尝试刷新 token会自动使用账户配置的代理
await openaiAccountService.refreshAccountToken(tempAccount.id)
// 刷新成功,获取更新后的账户信息
const refreshedAccount = await openaiAccountService.getAccount(tempAccount.id)
// 检查是否获取到了 ID Token
if (!refreshedAccount.idToken || refreshedAccount.idToken === '') {
// 没有获取到 ID Token删除账户
await openaiAccountService.deleteAccount(tempAccount.id)
throw new Error('无法获取 ID Token请检查 Refresh Token 是否有效')
}
// 如果是分组类型,添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(tempAccount.id, groupId, 'openai')
}
// 清除敏感信息后返回
delete refreshedAccount.idToken
delete refreshedAccount.accessToken
delete refreshedAccount.refreshToken
logger.success(`✅ 创建并验证 OpenAI 账户成功: ${name} (ID: ${tempAccount.id})`)
return res.json({
success: true,
data: refreshedAccount,
message: '账户创建成功,并已获取完整 token 信息'
})
} catch (refreshError) {
// 刷新失败,删除临时创建的账户
logger.warn(`❌ 刷新失败,删除临时账户: ${refreshError.message}`)
await openaiAccountService.deleteAccount(tempAccount.id)
// 构建详细的错误信息
const errorResponse = {
success: false,
message: '账户创建失败',
error: refreshError.message
}
// 添加更详细的错误信息
if (refreshError.status) {
errorResponse.errorCode = refreshError.status
}
if (refreshError.details) {
errorResponse.errorDetails = refreshError.details
}
if (refreshError.code) {
errorResponse.networkError = refreshError.code
}
// 提供更友好的错误提示
if (refreshError.message.includes('Refresh Token 无效')) {
errorResponse.suggestion = '请检查 Refresh Token 是否正确,或重新通过 OAuth 授权获取'
} else if (refreshError.message.includes('代理')) {
errorResponse.suggestion = '请检查代理配置是否正确,包括地址、端口和认证信息'
} else if (refreshError.message.includes('过于频繁')) {
errorResponse.suggestion = '请稍后再试,或更换代理 IP'
} else if (refreshError.message.includes('连接')) {
errorResponse.suggestion = '请检查网络连接和代理设置'
}
return res.status(400).json(errorResponse)
}
}
// 不需要强制刷新的情况OAuth 模式或其他平台)
const createdAccount = await openaiAccountService.createAccount(accountData)
// 如果是分组类型,添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(createdAccount.id, groupId, 'openai')
}
// 如果需要刷新但不强制成功OAuth 模式可能已有完整信息)
if (needsImmediateRefresh && !requireRefreshSuccess) {
try {
logger.info(`🔄 尝试刷新 OpenAI 账户 ${createdAccount.id}`)
await openaiAccountService.refreshAccountToken(createdAccount.id)
logger.info(`✅ 刷新成功`)
} catch (refreshError) {
logger.warn(`⚠️ 刷新失败,但账户已创建: ${refreshError.message}`)
}
}
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
const { needsImmediateRefresh, requireRefreshSuccess } = updates
// 验证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' })
}
// 如果更新了 Refresh Token需要验证其有效性
if (updates.openaiOauth?.refreshToken && needsImmediateRefresh && requireRefreshSuccess) {
// 先更新 token 信息
const tempUpdateData = {}
if (updates.openaiOauth.refreshToken) {
tempUpdateData.refreshToken = updates.openaiOauth.refreshToken
}
if (updates.openaiOauth.accessToken) {
tempUpdateData.accessToken = updates.openaiOauth.accessToken
}
// 更新代理配置(如果有)
if (updates.proxy !== undefined) {
tempUpdateData.proxy = updates.proxy
}
// 临时更新账户以测试新的 token
await openaiAccountService.updateAccount(id, tempUpdateData)
try {
logger.info(`🔄 验证更新的 OpenAI token (账户: ${id})`)
// 尝试刷新 token会使用账户配置的代理
await openaiAccountService.refreshAccountToken(id)
// 获取刷新后的账户信息
const refreshedAccount = await openaiAccountService.getAccount(id)
// 检查是否获取到了 ID Token
if (!refreshedAccount.idToken || refreshedAccount.idToken === '') {
// 恢复原始 token
await openaiAccountService.updateAccount(id, {
refreshToken: currentAccount.refreshToken,
accessToken: currentAccount.accessToken,
idToken: currentAccount.idToken
})
return res.status(400).json({
success: false,
message: '无法获取 ID Token请检查 Refresh Token 是否有效',
error: 'Invalid refresh token'
})
}
logger.success(`✅ Token 验证成功,继续更新账户信息`)
} catch (refreshError) {
// 刷新失败,恢复原始 token
logger.warn(`❌ Token 验证失败,恢复原始配置: ${refreshError.message}`)
await openaiAccountService.updateAccount(id, {
refreshToken: currentAccount.refreshToken,
accessToken: currentAccount.accessToken,
idToken: currentAccount.idToken,
proxy: currentAccount.proxy
})
// 构建详细的错误信息
const errorResponse = {
success: false,
message: '更新失败',
error: refreshError.message
}
// 添加更详细的错误信息
if (refreshError.status) {
errorResponse.errorCode = refreshError.status
}
if (refreshError.details) {
errorResponse.errorDetails = refreshError.details
}
if (refreshError.code) {
errorResponse.networkError = refreshError.code
}
// 提供更友好的错误提示
if (refreshError.message.includes('Refresh Token 无效')) {
errorResponse.suggestion = '请检查 Refresh Token 是否正确,或重新通过 OAuth 授权获取'
} else if (refreshError.message.includes('代理')) {
errorResponse.suggestion = '请检查代理配置是否正确,包括地址、端口和认证信息'
} else if (refreshError.message.includes('过于频繁')) {
errorResponse.suggestion = '请稍后再试,或更换代理 IP'
} else if (refreshError.message.includes('连接')) {
errorResponse.suggestion = '请检查网络连接和代理设置'
}
return res.status(400).json(errorResponse)
}
}
// 处理分组的变更
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
// 编辑时不允许直接输入 ID Token只能通过刷新获取
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)
// 如果需要刷新但不强制成功(非关键更新)
if (needsImmediateRefresh && !requireRefreshSuccess) {
try {
logger.info(`🔄 尝试刷新 OpenAI 账户 ${id}`)
await openaiAccountService.refreshAccountToken(id)
logger.info(`✅ 刷新成功`)
} catch (refreshError) {
logger.warn(`⚠️ 刷新失败,但账户信息已更新: ${refreshError.message}`)
}
}
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.post('/openai-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await openaiAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for OpenAI account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset OpenAI account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: 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 { platform, groupId } = req.query
let accounts = await azureOpenaiAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'azure_openai') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息和分组信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
groupInfos,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
}
} catch (error) {
logger.debug(`Failed to get usage stats for Azure OpenAI account ${account.id}:`, error)
try {
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
groupInfos,
usage: {
daily: { requests: 0, tokens: 0, allTokens: 0 },
total: { requests: 0, tokens: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
} catch (groupError) {
logger.debug(`Failed to get group info for account ${account.id}:`, groupError)
return {
...account,
groupInfos: [],
usage: {
daily: { requests: 0, tokens: 0, allTokens: 0 },
total: { requests: 0, tokens: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
}
}
})
)
res.json({
success: true,
data: accountsWithStats
})
} 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,
groupIds,
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
})
// 如果是分组类型,将账户添加到分组
if (accountType === 'group') {
if (groupIds && groupIds.length > 0) {
// 使用多分组设置
await accountGroupService.setAccountGroups(account.id, groupIds, 'azure_openai')
} else if (groupId) {
// 兼容单分组模式
await accountGroupService.addAccountToGroup(account.id, groupId, 'azure_openai')
}
}
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
})
}
})
// 📋 获取统一Claude Code User-Agent信息
router.get('/claude-code-version', authenticateAdmin, async (req, res) => {
try {
const CACHE_KEY = 'claude_code_user_agent:daily'
// 获取缓存的统一User-Agent
const unifiedUserAgent = await redis.client.get(CACHE_KEY)
const ttl = unifiedUserAgent ? await redis.client.ttl(CACHE_KEY) : 0
res.json({
success: true,
userAgent: unifiedUserAgent,
isActive: !!unifiedUserAgent,
ttlSeconds: ttl,
lastUpdated: unifiedUserAgent ? new Date().toISOString() : null
})
} catch (error) {
logger.error('❌ Get unified Claude Code User-Agent error:', error)
res.status(500).json({
success: false,
message: 'Failed to get User-Agent information',
error: error.message
})
}
})
// 🗑️ 清除统一Claude Code User-Agent缓存
router.post('/claude-code-version/clear', authenticateAdmin, async (req, res) => {
try {
const CACHE_KEY = 'claude_code_user_agent:daily'
// 删除缓存的统一User-Agent
await redis.client.del(CACHE_KEY)
logger.info(`🗑️ Admin manually cleared unified Claude Code User-Agent cache`)
res.json({
success: true,
message: 'Unified User-Agent cache cleared successfully'
})
} catch (error) {
logger.error('❌ Clear unified User-Agent cache error:', error)
res.status(500).json({
success: false,
message: 'Failed to clear cache',
error: error.message
})
}
})
// ==================== OpenAI-Responses 账户管理 API ====================
// 获取所有 OpenAI-Responses 账户
router.get('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
let accounts = await openaiResponsesAccountService.getAllAccounts(true)
// 根据查询参数进行筛选
if (platform && platform !== 'openai-responses') {
accounts = []
}
// 根据分组ID筛选
if (groupId) {
const group = await accountGroupService.getGroup(groupId)
if (group && group.platform === 'openai' && group.memberIds && group.memberIds.length > 0) {
accounts = accounts.filter((account) => group.memberIds.includes(account.id))
} else {
accounts = []
}
}
// 处理额度信息、使用统计和绑定的 API Key 数量
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
// 检查是否需要重置额度
const today = redis.getDateStringInTimezone()
if (account.lastResetDate !== today) {
// 今天还没重置过,需要重置
await openaiResponsesAccountService.updateAccount(account.id, {
dailyUsage: '0',
lastResetDate: today,
quotaStoppedAt: ''
})
account.dailyUsage = '0'
account.lastResetDate = today
account.quotaStoppedAt = ''
}
// 检查并清除过期的限流状态
await openaiResponsesAccountService.checkAndClearRateLimit(account.id)
// 获取使用统计信息
let usageStats
try {
usageStats = await redis.getAccountUsageStats(account.id, 'openai-responses')
} catch (error) {
logger.debug(
`Failed to get usage stats for OpenAI-Responses account ${account.id}:`,
error
)
usageStats = {
daily: { requests: 0, tokens: 0, allTokens: 0 },
total: { requests: 0, tokens: 0, allTokens: 0 },
monthly: { requests: 0, tokens: 0, allTokens: 0 }
}
}
// 计算绑定的API Key数量支持 responses: 前缀)
const allKeys = await redis.getAllApiKeys()
let boundCount = 0
for (const key of allKeys) {
// 检查是否绑定了该账户(包括 responses: 前缀)
if (
key.openaiAccountId === account.id ||
key.openaiAccountId === `responses:${account.id}`
) {
boundCount++
}
}
// 调试日志:检查绑定计数
if (boundCount > 0) {
logger.info(`OpenAI-Responses account ${account.id} has ${boundCount} bound API keys`)
}
return {
...account,
boundApiKeysCount: boundCount,
usage: {
daily: usageStats.daily,
total: usageStats.total,
monthly: usageStats.monthly
}
}
} catch (error) {
logger.error(`Failed to process OpenAI-Responses account ${account.id}:`, error)
return {
...account,
boundApiKeysCount: 0,
usage: {
daily: { requests: 0, tokens: 0, allTokens: 0 },
total: { requests: 0, tokens: 0, allTokens: 0 },
monthly: { requests: 0, tokens: 0, allTokens: 0 }
}
}
}
})
)
res.json({ success: true, data: accountsWithStats })
} catch (error) {
logger.error('Failed to get OpenAI-Responses accounts:', error)
res.status(500).json({ success: false, message: error.message })
}
})
// 创建 OpenAI-Responses 账户
router.post('/openai-responses-accounts', authenticateAdmin, async (req, res) => {
try {
const account = await openaiResponsesAccountService.createAccount(req.body)
res.json({ success: true, account })
} catch (error) {
logger.error('Failed to create OpenAI-Responses account:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
// 更新 OpenAI-Responses 账户
router.put('/openai-responses-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const updates = req.body
// 验证priority的有效性1-100
if (updates.priority !== undefined) {
const priority = parseInt(updates.priority)
if (isNaN(priority) || priority < 1 || priority > 100) {
return res.status(400).json({
success: false,
message: 'Priority must be a number between 1 and 100'
})
}
updates.priority = priority.toString()
}
const result = await openaiResponsesAccountService.updateAccount(id, updates)
if (!result.success) {
return res.status(400).json(result)
}
res.json({ success: true, ...result })
} catch (error) {
logger.error('Failed to update OpenAI-Responses account:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
// 删除 OpenAI-Responses 账户
router.delete('/openai-responses-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await openaiResponsesAccountService.getAccount(id)
if (!account) {
return res.status(404).json({
success: false,
message: 'Account not found'
})
}
// 检查是否在分组中
const groups = await accountGroupService.getAllGroups()
for (const group of groups) {
if (group.platform === 'openai' && group.memberIds && group.memberIds.includes(id)) {
await accountGroupService.removeMemberFromGroup(group.id, id)
logger.info(`Removed OpenAI-Responses account ${id} from group ${group.id}`)
}
}
const result = await openaiResponsesAccountService.deleteAccount(id)
res.json({ success: true, ...result })
} catch (error) {
logger.error('Failed to delete OpenAI-Responses account:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
// 切换 OpenAI-Responses 账户调度状态
router.put(
'/openai-responses-accounts/:id/toggle-schedulable',
authenticateAdmin,
async (req, res) => {
try {
const { id } = req.params
const result = await openaiResponsesAccountService.toggleSchedulable(id)
if (!result.success) {
return res.status(400).json(result)
}
// 仅在停止调度时发送通知
if (!result.schedulable) {
await webhookNotifier.sendAccountEvent('account.status_changed', {
accountId: id,
platform: 'openai-responses',
schedulable: result.schedulable,
changedBy: 'admin',
action: 'stopped_scheduling'
})
}
res.json(result)
} catch (error) {
logger.error('Failed to toggle OpenAI-Responses account schedulable status:', error)
res.status(500).json({
success: false,
error: error.message
})
}
}
)
// 切换 OpenAI-Responses 账户激活状态
router.put('/openai-responses-accounts/:id/toggle', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await openaiResponsesAccountService.getAccount(id)
if (!account) {
return res.status(404).json({
success: false,
message: 'Account not found'
})
}
const newActiveStatus = account.isActive === 'true' ? 'false' : 'true'
await openaiResponsesAccountService.updateAccount(id, {
isActive: newActiveStatus
})
res.json({
success: true,
isActive: newActiveStatus === 'true'
})
} catch (error) {
logger.error('Failed to toggle OpenAI-Responses account status:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
// 重置 OpenAI-Responses 账户限流状态
router.post(
'/openai-responses-accounts/:id/reset-rate-limit',
authenticateAdmin,
async (req, res) => {
try {
const { id } = req.params
await openaiResponsesAccountService.updateAccount(id, {
rateLimitedAt: '',
rateLimitStatus: '',
status: 'active',
errorMessage: ''
})
logger.info(`🔄 Admin manually reset rate limit for OpenAI-Responses account ${id}`)
res.json({
success: true,
message: 'Rate limit reset successfully'
})
} catch (error) {
logger.error('Failed to reset OpenAI-Responses account rate limit:', error)
res.status(500).json({
success: false,
error: error.message
})
}
}
)
// 重置 OpenAI-Responses 账户状态(清除所有异常状态)
router.post('/openai-responses-accounts/:id/reset-status', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const result = await openaiResponsesAccountService.resetAccountStatus(id)
logger.success(`✅ Admin reset status for OpenAI-Responses account: ${id}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset OpenAI-Responses account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
// 手动重置 OpenAI-Responses 账户的每日使用量
router.post('/openai-responses-accounts/:id/reset-usage', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
await openaiResponsesAccountService.updateAccount(id, {
dailyUsage: '0',
lastResetDate: redis.getDateStringInTimezone(),
quotaStoppedAt: ''
})
logger.success(`✅ Admin manually reset daily usage for OpenAI-Responses account ${id}`)
res.json({
success: true,
message: 'Daily usage reset successfully'
})
} catch (error) {
logger.error('Failed to reset OpenAI-Responses account usage:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
module.exports = router