refacto: 重构admin.js

This commit is contained in:
shaw
2025-11-27 22:16:45 +08:00
parent 851809a132
commit fd2b8a0114
19 changed files with 10161 additions and 9917 deletions

View File

@@ -0,0 +1,469 @@
/**
* Admin Routes - Claude Console 账户管理
* API Key 方式的 Claude Console 账户
*/
const express = require('express')
const router = express.Router()
const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
const webhookNotifier = require('../../utils/webhookNotifier')
const { formatAccountExpiry, mapExpiryField } = require('./utils')
// 获取所有Claude Console账户
router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
let accounts = await claudeConsoleAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'claude-console') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(account.id)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
// 转换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)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
// 转换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
)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
groupInfos: [],
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
}
}
})
)
return res.json({ success: true, data: accountsWithStats })
} catch (error) {
logger.error('❌ Failed to get Claude Console accounts:', error)
return res
.status(500)
.json({ error: 'Failed to get Claude Console accounts', message: error.message })
}
})
// 创建新的Claude Console账户
router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
apiUrl,
apiKey,
priority,
supportedModels,
userAgent,
rateLimitDuration,
proxy,
accountType,
groupId,
dailyQuota,
quotaResetTime,
maxConcurrentTasks
} = 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' })
}
// 验证maxConcurrentTasks的有效性非负整数
if (maxConcurrentTasks !== undefined && maxConcurrentTasks !== null) {
const concurrent = Number(maxConcurrentTasks)
if (!Number.isInteger(concurrent) || concurrent < 0) {
return res.status(400).json({ error: 'maxConcurrentTasks must be a non-negative integer' })
}
}
// 验证accountType的有效性
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
}
// 如果是分组类型验证groupId
if (accountType === 'group' && !groupId) {
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
}
const newAccount = await claudeConsoleAccountService.createAccount({
name,
description,
apiUrl,
apiKey,
priority: priority || 50,
supportedModels: supportedModels || [],
userAgent,
rateLimitDuration:
rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60,
proxy,
accountType: accountType || 'shared',
dailyQuota: dailyQuota || 0,
quotaResetTime: quotaResetTime || '00:00',
maxConcurrentTasks:
maxConcurrentTasks !== undefined && maxConcurrentTasks !== null
? Number(maxConcurrentTasks)
: 0
})
// 如果是分组类型将账户添加到分组CCR 归属 Claude 平台分组)
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, groupId, 'claude')
}
logger.success(`🎮 Admin created Claude Console account: ${name}`)
const formattedAccount = formatAccountExpiry(newAccount)
return res.json({ success: true, data: formattedAccount })
} 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
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = mapExpiryField(updates, 'Claude Console', accountId)
// 验证priority的有效性1-100
if (
mappedUpdates.priority !== undefined &&
(mappedUpdates.priority < 1 || mappedUpdates.priority > 100)
) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
}
// 验证maxConcurrentTasks的有效性非负整数
if (
mappedUpdates.maxConcurrentTasks !== undefined &&
mappedUpdates.maxConcurrentTasks !== null
) {
const concurrent = Number(mappedUpdates.maxConcurrentTasks)
if (!Number.isInteger(concurrent) || concurrent < 0) {
return res.status(400).json({ error: 'maxConcurrentTasks must be a non-negative integer' })
}
// 转换为数字类型
mappedUpdates.maxConcurrentTasks = concurrent
}
// 验证accountType的有效性
if (
mappedUpdates.accountType &&
!['shared', 'dedicated', 'group'].includes(mappedUpdates.accountType)
) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
}
// 如果更新为分组类型验证groupId
if (mappedUpdates.accountType === 'group' && !mappedUpdates.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 (mappedUpdates.accountType !== undefined) {
// 如果之前是分组类型,需要从所有分组中移除
if (currentAccount.accountType === 'group') {
const oldGroups = await accountGroupService.getAccountGroups(accountId)
for (const oldGroup of oldGroups) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
}
}
// 如果新类型是分组,处理多分组支持
if (mappedUpdates.accountType === 'group') {
if (Object.prototype.hasOwnProperty.call(mappedUpdates, 'groupIds')) {
// 如果明确提供了 groupIds 参数(包括空数组)
if (mappedUpdates.groupIds && mappedUpdates.groupIds.length > 0) {
// 设置新的多分组
await accountGroupService.setAccountGroups(accountId, mappedUpdates.groupIds, 'claude')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (mappedUpdates.groupId) {
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
await accountGroupService.addAccountToGroup(accountId, mappedUpdates.groupId, 'claude')
}
}
}
await claudeConsoleAccountService.updateAccount(accountId, mappedUpdates)
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
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'claude-console')
// 获取账户信息以检查是否在分组中
const account = await claudeConsoleAccountService.getAccount(accountId)
if (account && account.accountType === 'group') {
const groups = await accountGroupService.getAccountGroups(accountId)
for (const group of groups) {
await accountGroupService.removeAccountFromGroup(accountId, group.id)
}
}
await claudeConsoleAccountService.deleteAccount(accountId)
let message = 'Claude Console账号已成功删除'
if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(
`🗑️ Admin deleted Claude Console account: ${accountId}, unbound ${unboundCount} keys`
)
return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) {
logger.error('❌ Failed to delete Claude Console account:', error)
return res
.status(500)
.json({ error: 'Failed to delete Claude Console account', message: error.message })
}
})
// 切换Claude Console账户状态
router.put('/claude-console-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const account = await claudeConsoleAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
const newStatus = !account.isActive
await claudeConsoleAccountService.updateAccount(accountId, { isActive: newStatus })
logger.success(
`🔄 Admin toggled Claude Console account status: ${accountId} -> ${
newStatus ? 'active' : 'inactive'
}`
)
return res.json({ success: true, isActive: newStatus })
} catch (error) {
logger.error('❌ Failed to toggle Claude Console account status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle account status', message: error.message })
}
})
// 切换Claude Console账户调度状态
router.put(
'/claude-console-accounts/:accountId/toggle-schedulable',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
const account = await claudeConsoleAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
const newSchedulable = !account.schedulable
await claudeConsoleAccountService.updateAccount(accountId, { schedulable: newSchedulable })
// 如果账号被禁用发送webhook通知
if (!newSchedulable) {
await webhookNotifier.sendAccountAnomalyNotification({
accountId: account.id,
accountName: account.name || 'Claude Console Account',
platform: 'claude-console',
status: 'disabled',
errorCode: 'CLAUDE_CONSOLE_MANUALLY_DISABLED',
reason: '账号已被管理员手动禁用调度',
timestamp: new Date().toISOString()
})
}
logger.success(
`🔄 Admin toggled Claude Console account schedulable status: ${accountId} -> ${
newSchedulable ? 'schedulable' : 'not schedulable'
}`
)
return res.json({ success: true, schedulable: newSchedulable })
} catch (error) {
logger.error('❌ Failed to toggle Claude Console account schedulable status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: error.message })
}
}
)
// 获取Claude Console账户的使用统计
router.get('/claude-console-accounts/:accountId/usage', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const usageStats = await claudeConsoleAccountService.getAccountUsageStats(accountId)
if (!usageStats) {
return res.status(404).json({ error: 'Account not found' })
}
return res.json(usageStats)
} catch (error) {
logger.error('❌ Failed to get Claude Console account usage stats:', error)
return res.status(500).json({ error: 'Failed to get usage stats', message: error.message })
}
})
// 手动重置Claude Console账户的每日使用量
router.post(
'/claude-console-accounts/:accountId/reset-usage',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
await claudeConsoleAccountService.resetDailyUsage(accountId)
logger.success(`✅ Admin manually reset daily usage for Claude Console account: ${accountId}`)
return res.json({ success: true, message: 'Daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset Claude Console account daily usage:', error)
return res.status(500).json({ error: 'Failed to reset daily usage', message: error.message })
}
}
)
// 重置Claude Console账户状态清除所有异常状态
router.post(
'/claude-console-accounts/:accountId/reset-status',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
const result = await claudeConsoleAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for Claude Console account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Claude Console account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
}
)
// 手动重置所有Claude Console账户的每日使用量
router.post('/claude-console-accounts/reset-all-usage', authenticateAdmin, async (req, res) => {
try {
await claudeConsoleAccountService.resetAllDailyUsage()
logger.success('✅ Admin manually reset daily usage for all Claude Console accounts')
return res.json({ success: true, message: 'All daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset all Claude Console accounts daily usage:', error)
return res
.status(500)
.json({ error: 'Failed to reset all daily usage', message: error.message })
}
})
module.exports = router