Files
claude-relay-service/src/routes/admin.js
2025-09-02 21:38:54 +08:00

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