/** * Admin Routes - Claude Console 账户管理 * API Key 方式的 Claude Console 账户 */ const express = require('express') const router = express.Router() const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService') const claudeConsoleRelayService = require('../../services/claudeConsoleRelayService') 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, disableAutoProtection, interceptWarmup } = 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' }) } } // 校验上游错误自动防护开关 const normalizedDisableAutoProtection = disableAutoProtection === true || disableAutoProtection === 'true' // 验证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, disableAutoProtection: normalizedDisableAutoProtection, interceptWarmup: interceptWarmup === true || interceptWarmup === 'true' }) // 如果是分组类型,将账户添加到分组(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.disableAutoProtection !== undefined) { mappedUpdates.disableAutoProtection = mappedUpdates.disableAutoProtection === true || mappedUpdates.disableAutoProtection === 'true' } // 处理分组的变更 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 }) } }) // 测试Claude Console账户连通性(流式响应)- 复用 claudeConsoleRelayService router.post('/claude-console-accounts/:accountId/test', authenticateAdmin, async (req, res) => { const { accountId } = req.params try { // 直接调用服务层的测试方法 await claudeConsoleRelayService.testAccountConnection(accountId, res) } catch (error) { logger.error(`❌ Failed to test Claude Console account:`, error) // 错误已在服务层处理,这里仅做日志记录 } }) module.exports = router