mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
refacto: 重构admin.js
This commit is contained in:
416
src/routes/admin/ccrAccounts.js
Normal file
416
src/routes/admin/ccrAccounts.js
Normal file
@@ -0,0 +1,416 @@
|
||||
const express = require('express')
|
||||
const ccrAccountService = require('../../services/ccrAccountService')
|
||||
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')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 🔧 CCR 账户管理
|
||||
|
||||
// 获取所有CCR账户
|
||||
router.get('/', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { platform, groupId } = req.query
|
||||
let accounts = await ccrAccountService.getAllAccounts()
|
||||
|
||||
// 根据查询参数进行筛选
|
||||
if (platform && platform !== 'all' && platform !== 'ccr') {
|
||||
// 如果指定了其他平台,返回空数组
|
||||
accounts = []
|
||||
}
|
||||
|
||||
// 如果指定了分组筛选
|
||||
if (groupId && groupId !== 'all') {
|
||||
if (groupId === 'ungrouped') {
|
||||
// 筛选未分组账户
|
||||
const filteredAccounts = []
|
||||
for (const account of accounts) {
|
||||
const groups = await accountGroupService.getAccountGroups(account.id)
|
||||
if (!groups || groups.length === 0) {
|
||||
filteredAccounts.push(account)
|
||||
}
|
||||
}
|
||||
accounts = filteredAccounts
|
||||
} else {
|
||||
// 筛选特定分组的账户
|
||||
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个账户添加使用统计信息
|
||||
const accountsWithStats = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
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 CCR 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 CCR account ${account.id}:`,
|
||||
groupError.message
|
||||
)
|
||||
return {
|
||||
...account,
|
||||
groupInfos: [],
|
||||
usage: {
|
||||
daily: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
total: { tokens: 0, requests: 0, allTokens: 0 },
|
||||
averages: { rpm: 0, tpm: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return res.json({ success: true, data: accountsWithStats })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get CCR accounts:', error)
|
||||
return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 创建新的CCR账户
|
||||
router.post('/', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
priority,
|
||||
supportedModels,
|
||||
userAgent,
|
||||
rateLimitDuration,
|
||||
proxy,
|
||||
accountType,
|
||||
groupId,
|
||||
dailyQuota,
|
||||
quotaResetTime
|
||||
} = req.body
|
||||
|
||||
if (!name || !apiUrl || !apiKey) {
|
||||
return res.status(400).json({ error: 'Name, API URL and API Key are required' })
|
||||
}
|
||||
|
||||
// 验证priority的有效性(1-100)
|
||||
if (priority !== undefined && (priority < 1 || priority > 100)) {
|
||||
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
|
||||
}
|
||||
|
||||
// 验证accountType的有效性
|
||||
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
|
||||
}
|
||||
|
||||
// 如果是分组类型,验证groupId
|
||||
if (accountType === 'group' && !groupId) {
|
||||
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
|
||||
}
|
||||
|
||||
const newAccount = await ccrAccountService.createAccount({
|
||||
name,
|
||||
description,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
priority: priority || 50,
|
||||
supportedModels: supportedModels || [],
|
||||
userAgent,
|
||||
rateLimitDuration:
|
||||
rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60,
|
||||
proxy,
|
||||
accountType: accountType || 'shared',
|
||||
dailyQuota: dailyQuota || 0,
|
||||
quotaResetTime: quotaResetTime || '00:00'
|
||||
})
|
||||
|
||||
// 如果是分组类型,将账户添加到分组
|
||||
if (accountType === 'group' && groupId) {
|
||||
await accountGroupService.addAccountToGroup(newAccount.id, groupId)
|
||||
}
|
||||
|
||||
logger.success(`🔧 Admin created CCR account: ${name}`)
|
||||
const formattedAccount = formatAccountExpiry(newAccount)
|
||||
return res.json({ success: true, data: formattedAccount })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to create CCR account:', error)
|
||||
return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新CCR账户
|
||||
router.put('/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
const updates = req.body
|
||||
|
||||
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
|
||||
const mappedUpdates = mapExpiryField(updates, 'CCR', 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' })
|
||||
}
|
||||
|
||||
// 验证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 ccrAccountService.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 ccrAccountService.updateAccount(accountId, mappedUpdates)
|
||||
|
||||
logger.success(`📝 Admin updated CCR account: ${accountId}`)
|
||||
return res.json({ success: true, message: 'CCR account updated successfully' })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to update CCR account:', error)
|
||||
return res.status(500).json({ error: 'Failed to update CCR account', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 删除CCR账户
|
||||
router.delete('/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
|
||||
// 尝试自动解绑(CCR账户实际上不会绑定API Key,但保持代码一致性)
|
||||
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'ccr')
|
||||
|
||||
// 获取账户信息以检查是否在分组中
|
||||
const account = await ccrAccountService.getAccount(accountId)
|
||||
if (account && account.accountType === 'group') {
|
||||
const groups = await accountGroupService.getAccountGroups(accountId)
|
||||
for (const group of groups) {
|
||||
await accountGroupService.removeAccountFromGroup(accountId, group.id)
|
||||
}
|
||||
}
|
||||
|
||||
await ccrAccountService.deleteAccount(accountId)
|
||||
|
||||
let message = 'CCR账号已成功删除'
|
||||
if (unboundCount > 0) {
|
||||
// 理论上不会发生,但保持消息格式一致
|
||||
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||
}
|
||||
|
||||
logger.success(`🗑️ Admin deleted CCR account: ${accountId}`)
|
||||
return res.json({
|
||||
success: true,
|
||||
message,
|
||||
unboundKeys: unboundCount
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to delete CCR account:', error)
|
||||
return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 切换CCR账户状态
|
||||
router.put('/:accountId/toggle', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
|
||||
const account = await ccrAccountService.getAccount(accountId)
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
|
||||
const newStatus = !account.isActive
|
||||
await ccrAccountService.updateAccount(accountId, { isActive: newStatus })
|
||||
|
||||
logger.success(
|
||||
`🔄 Admin toggled CCR account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`
|
||||
)
|
||||
return res.json({ success: true, isActive: newStatus })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle CCR account status:', error)
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to toggle account status', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 切换CCR账户调度状态
|
||||
router.put('/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
|
||||
const account = await ccrAccountService.getAccount(accountId)
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
|
||||
const newSchedulable = !account.schedulable
|
||||
await ccrAccountService.updateAccount(accountId, { schedulable: newSchedulable })
|
||||
|
||||
// 如果账号被禁用,发送webhook通知
|
||||
if (!newSchedulable) {
|
||||
await webhookNotifier.sendAccountAnomalyNotification({
|
||||
accountId: account.id,
|
||||
accountName: account.name || 'CCR Account',
|
||||
platform: 'ccr',
|
||||
status: 'disabled',
|
||||
errorCode: 'CCR_MANUALLY_DISABLED',
|
||||
reason: '账号已被管理员手动禁用调度',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
logger.success(
|
||||
`🔄 Admin toggled CCR account schedulable status: ${accountId} -> ${
|
||||
newSchedulable ? 'schedulable' : 'not schedulable'
|
||||
}`
|
||||
)
|
||||
return res.json({ success: true, schedulable: newSchedulable })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to toggle CCR account schedulable status:', error)
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to toggle schedulable status', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 获取CCR账户的使用统计
|
||||
router.get('/:accountId/usage', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
const usageStats = await ccrAccountService.getAccountUsageStats(accountId)
|
||||
|
||||
if (!usageStats) {
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
|
||||
return res.json(usageStats)
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get CCR account usage stats:', error)
|
||||
return res.status(500).json({ error: 'Failed to get usage stats', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 手动重置CCR账户的每日使用量
|
||||
router.post('/:accountId/reset-usage', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
await ccrAccountService.resetDailyUsage(accountId)
|
||||
|
||||
logger.success(`✅ Admin manually reset daily usage for CCR account: ${accountId}`)
|
||||
return res.json({ success: true, message: 'Daily usage reset successfully' })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to reset CCR account daily usage:', error)
|
||||
return res.status(500).json({ error: 'Failed to reset daily usage', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 重置CCR账户状态(清除所有异常状态)
|
||||
router.post('/:accountId/reset-status', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params
|
||||
const result = await ccrAccountService.resetAccountStatus(accountId)
|
||||
logger.success(`✅ Admin reset status for CCR account: ${accountId}`)
|
||||
return res.json({ success: true, data: result })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to reset CCR account status:', error)
|
||||
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
// 手动重置所有CCR账户的每日使用量
|
||||
router.post('/reset-all-usage', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
await ccrAccountService.resetAllDailyUsage()
|
||||
|
||||
logger.success('✅ Admin manually reset daily usage for all CCR accounts')
|
||||
return res.json({ success: true, message: 'All daily usage reset successfully' })
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to reset all CCR accounts daily usage:', error)
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to reset all daily usage', message: error.message })
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Reference in New Issue
Block a user