mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge remote-tracking branch 'f3n9/main' into main-um-8
This commit is contained in:
@@ -749,6 +749,9 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => {
|
||||
if (updates.tokenLimit !== undefined) {
|
||||
finalUpdates.tokenLimit = updates.tokenLimit
|
||||
}
|
||||
if (updates.rateLimitCost !== undefined) {
|
||||
finalUpdates.rateLimitCost = updates.rateLimitCost
|
||||
}
|
||||
if (updates.concurrencyLimit !== undefined) {
|
||||
finalUpdates.concurrencyLimit = updates.concurrencyLimit
|
||||
}
|
||||
@@ -1235,7 +1238,7 @@ router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => {
|
||||
deletedAt: key.deletedAt,
|
||||
deletedBy: key.deletedBy,
|
||||
deletedByType: key.deletedByType,
|
||||
canRestore: false // Deleted keys cannot be restored per requirement
|
||||
canRestore: true // 已删除的API Key可以恢复
|
||||
}))
|
||||
|
||||
logger.success(`📋 Admin retrieved ${enrichedKeys.length} deleted API keys`)
|
||||
@@ -1248,6 +1251,123 @@ router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 🔄 恢复已删除的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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 👥 账户分组管理
|
||||
|
||||
// 创建账户分组
|
||||
@@ -1620,15 +1740,18 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
if (groupId && groupId !== 'all') {
|
||||
if (groupId === 'ungrouped') {
|
||||
// 筛选未分组账户
|
||||
accounts = accounts.filter(
|
||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
||||
)
|
||||
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 {
|
||||
// 筛选特定分组的账户
|
||||
accounts = accounts.filter(
|
||||
(account) =>
|
||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
||||
)
|
||||
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1637,7 +1760,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
// 获取会话窗口使用统计(仅对有活跃窗口的账户)
|
||||
let sessionWindowUsage = null
|
||||
@@ -1695,7 +1818,7 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
logger.warn(`⚠️ Failed to get usage stats for account ${account.id}:`, statsError.message)
|
||||
// 如果获取统计失败,返回空统计
|
||||
try {
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
return {
|
||||
...account,
|
||||
groupInfos,
|
||||
@@ -1748,6 +1871,7 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
platform = 'claude',
|
||||
priority,
|
||||
groupId,
|
||||
groupIds,
|
||||
autoStopOnWarning
|
||||
} = req.body
|
||||
|
||||
@@ -1762,9 +1886,11 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
.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' })
|
||||
// 如果是分组类型,验证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的有效性
|
||||
@@ -1790,8 +1916,14 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
|
||||
})
|
||||
|
||||
// 如果是分组类型,将账户添加到分组
|
||||
if (accountType === 'group' && groupId) {
|
||||
await accountGroupService.addAccountToGroup(newAccount.id, groupId, newAccount.platform)
|
||||
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'})`)
|
||||
@@ -1825,9 +1957,15 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
||||
.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' })
|
||||
// 如果更新为分组类型,验证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' })
|
||||
}
|
||||
|
||||
// 获取账户当前信息以处理分组变更
|
||||
@@ -1840,16 +1978,24 @@ router.put('/claude-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
||||
if (updates.accountType !== undefined) {
|
||||
// 如果之前是分组类型,需要从所有分组中移除
|
||||
if (currentAccount.accountType === 'group') {
|
||||
const oldGroups = await accountGroupService.getAccountGroup(accountId)
|
||||
for (const oldGroup of oldGroups) {
|
||||
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
||||
}
|
||||
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||
}
|
||||
|
||||
// 如果新类型是分组,添加到新分组
|
||||
if (updates.accountType === 'group' && updates.groupId) {
|
||||
// 从路由知道这是 Claude OAuth 账户,平台为 'claude'
|
||||
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2023,15 +2169,18 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
if (groupId && groupId !== 'all') {
|
||||
if (groupId === 'ungrouped') {
|
||||
// 筛选未分组账户
|
||||
accounts = accounts.filter(
|
||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
||||
)
|
||||
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 {
|
||||
// 筛选特定分组的账户
|
||||
accounts = accounts.filter(
|
||||
(account) =>
|
||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
||||
)
|
||||
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2040,7 +2189,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
return {
|
||||
...account,
|
||||
@@ -2059,7 +2208,7 @@ router.get('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
statsError.message
|
||||
)
|
||||
try {
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
return {
|
||||
...account,
|
||||
// 转换schedulable为布尔值
|
||||
@@ -2153,7 +2302,7 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
|
||||
|
||||
// 如果是分组类型,将账户添加到分组
|
||||
if (accountType === 'group' && groupId) {
|
||||
await accountGroupService.addAccountToGroup(newAccount.id, groupId, 'claude')
|
||||
await accountGroupService.addAccountToGroup(newAccount.id, groupId)
|
||||
}
|
||||
|
||||
logger.success(`🎮 Admin created Claude Console account: ${name}`)
|
||||
@@ -2199,15 +2348,26 @@ router.put('/claude-console-accounts/:accountId', authenticateAdmin, async (req,
|
||||
if (updates.accountType !== undefined) {
|
||||
// 如果之前是分组类型,需要从所有分组中移除
|
||||
if (currentAccount.accountType === 'group') {
|
||||
const oldGroups = await accountGroupService.getAccountGroup(accountId)
|
||||
const oldGroups = await accountGroupService.getAccountGroups(accountId)
|
||||
for (const oldGroup of oldGroups) {
|
||||
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
||||
}
|
||||
}
|
||||
// 如果新类型是分组,添加到新分组
|
||||
if (updates.accountType === 'group' && updates.groupId) {
|
||||
// Claude Console 账户在分组中被视为 'claude' 平台
|
||||
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
|
||||
// 如果新类型是分组,处理多分组支持
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2341,15 +2501,18 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
||||
if (groupId && groupId !== 'all') {
|
||||
if (groupId === 'ungrouped') {
|
||||
// 筛选未分组账户
|
||||
accounts = accounts.filter(
|
||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
||||
)
|
||||
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 {
|
||||
// 筛选特定分组的账户
|
||||
accounts = accounts.filter(
|
||||
(account) =>
|
||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
||||
)
|
||||
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2358,7 +2521,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
return {
|
||||
...account,
|
||||
@@ -2375,7 +2538,7 @@ router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
|
||||
statsError.message
|
||||
)
|
||||
try {
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
return {
|
||||
...account,
|
||||
groupInfos,
|
||||
@@ -2788,15 +2951,18 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
||||
if (groupId && groupId !== 'all') {
|
||||
if (groupId === 'ungrouped') {
|
||||
// 筛选未分组账户
|
||||
accounts = accounts.filter(
|
||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
||||
)
|
||||
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 {
|
||||
// 筛选特定分组的账户
|
||||
accounts = accounts.filter(
|
||||
(account) =>
|
||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
||||
)
|
||||
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2805,7 +2971,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
||||
accounts.map(async (account) => {
|
||||
try {
|
||||
const usageStats = await redis.getAccountUsageStats(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
|
||||
return {
|
||||
...account,
|
||||
@@ -2823,7 +2989,7 @@ router.get('/gemini-accounts', authenticateAdmin, async (req, res) => {
|
||||
)
|
||||
// 如果获取统计失败,返回空统计
|
||||
try {
|
||||
const groupInfos = await accountGroupService.getAccountGroup(account.id)
|
||||
const groupInfos = await accountGroupService.getAccountGroups(account.id)
|
||||
return {
|
||||
...account,
|
||||
groupInfos,
|
||||
@@ -2927,14 +3093,26 @@ router.put('/gemini-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
||||
if (updates.accountType !== undefined) {
|
||||
// 如果之前是分组类型,需要从所有分组中移除
|
||||
if (currentAccount.accountType === 'group') {
|
||||
const oldGroups = await accountGroupService.getAccountGroup(accountId)
|
||||
const oldGroups = await accountGroupService.getAccountGroups(accountId)
|
||||
for (const oldGroup of oldGroups) {
|
||||
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
|
||||
}
|
||||
}
|
||||
// 如果新类型是分组,添加到新分组
|
||||
if (updates.accountType === 'group' && updates.groupId) {
|
||||
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'gemini')
|
||||
// 如果新类型是分组,处理多分组支持
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5299,15 +5477,18 @@ router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
|
||||
if (groupId && groupId !== 'all') {
|
||||
if (groupId === 'ungrouped') {
|
||||
// 筛选未分组账户
|
||||
accounts = accounts.filter(
|
||||
(account) => !account.groupInfos || account.groupInfos.length === 0
|
||||
)
|
||||
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 {
|
||||
// 筛选特定分组的账户
|
||||
accounts = accounts.filter(
|
||||
(account) =>
|
||||
account.groupInfos && account.groupInfos.some((group) => group.id === groupId)
|
||||
)
|
||||
const groupMembers = await accountGroupService.getGroupMembers(groupId)
|
||||
accounts = accounts.filter((account) => groupMembers.includes(account.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5628,10 +5809,81 @@ router.put(
|
||||
// 获取所有 Azure OpenAI 账户
|
||||
router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const accounts = await azureOpenaiAccountService.getAllAccounts()
|
||||
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: accounts
|
||||
data: accountsWithStats
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch Azure OpenAI accounts:', error)
|
||||
@@ -5657,6 +5909,7 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
||||
supportedModels,
|
||||
proxy,
|
||||
groupId,
|
||||
groupIds,
|
||||
priority,
|
||||
isActive,
|
||||
schedulable
|
||||
@@ -5736,6 +5989,17 @@ router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
|
||||
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,
|
||||
|
||||
@@ -407,6 +407,317 @@ router.post('/api/user-stats', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 📊 批量查询统计数据接口
|
||||
router.post('/api/batch-stats', async (req, res) => {
|
||||
try {
|
||||
const { apiIds } = req.body
|
||||
|
||||
// 验证输入
|
||||
if (!apiIds || !Array.isArray(apiIds) || apiIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid input',
|
||||
message: 'API IDs array is required'
|
||||
})
|
||||
}
|
||||
|
||||
// 限制最多查询 30 个
|
||||
if (apiIds.length > 30) {
|
||||
return res.status(400).json({
|
||||
error: 'Too many keys',
|
||||
message: 'Maximum 30 API keys can be queried at once'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证所有 ID 格式
|
||||
const uuidRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
|
||||
const invalidIds = apiIds.filter((id) => !uuidRegex.test(id))
|
||||
if (invalidIds.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid API ID format',
|
||||
message: `Invalid API IDs: ${invalidIds.join(', ')}`
|
||||
})
|
||||
}
|
||||
|
||||
const individualStats = []
|
||||
const aggregated = {
|
||||
totalKeys: apiIds.length,
|
||||
activeKeys: 0,
|
||||
usage: {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreateTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
allTokens: 0,
|
||||
cost: 0,
|
||||
formattedCost: '$0.000000'
|
||||
},
|
||||
dailyUsage: {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreateTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
allTokens: 0,
|
||||
cost: 0,
|
||||
formattedCost: '$0.000000'
|
||||
},
|
||||
monthlyUsage: {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreateTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
allTokens: 0,
|
||||
cost: 0,
|
||||
formattedCost: '$0.000000'
|
||||
}
|
||||
}
|
||||
|
||||
// 并行查询所有 API Key 数据(复用单key查询逻辑)
|
||||
const results = await Promise.allSettled(
|
||||
apiIds.map(async (apiId) => {
|
||||
const keyData = await redis.getApiKey(apiId)
|
||||
|
||||
if (!keyData || Object.keys(keyData).length === 0) {
|
||||
return { error: 'Not found', apiId }
|
||||
}
|
||||
|
||||
// 检查是否激活
|
||||
if (keyData.isActive !== 'true') {
|
||||
return { error: 'Disabled', apiId }
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) {
|
||||
return { error: 'Expired', apiId }
|
||||
}
|
||||
|
||||
// 复用单key查询的逻辑:获取使用统计
|
||||
const usage = await redis.getUsageStats(apiId)
|
||||
|
||||
// 获取费用统计(与单key查询一致)
|
||||
const costStats = await redis.getCostStats(apiId)
|
||||
|
||||
return {
|
||||
apiId,
|
||||
name: keyData.name,
|
||||
description: keyData.description || '',
|
||||
isActive: true,
|
||||
createdAt: keyData.createdAt,
|
||||
usage: usage.total || {},
|
||||
dailyStats: {
|
||||
...usage.daily,
|
||||
cost: costStats.daily
|
||||
},
|
||||
monthlyStats: {
|
||||
...usage.monthly,
|
||||
cost: costStats.monthly
|
||||
},
|
||||
totalCost: costStats.total
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// 处理结果并聚合
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled' && result.value && !result.value.error) {
|
||||
const stats = result.value
|
||||
aggregated.activeKeys++
|
||||
|
||||
// 聚合总使用量
|
||||
if (stats.usage) {
|
||||
aggregated.usage.requests += stats.usage.requests || 0
|
||||
aggregated.usage.inputTokens += stats.usage.inputTokens || 0
|
||||
aggregated.usage.outputTokens += stats.usage.outputTokens || 0
|
||||
aggregated.usage.cacheCreateTokens += stats.usage.cacheCreateTokens || 0
|
||||
aggregated.usage.cacheReadTokens += stats.usage.cacheReadTokens || 0
|
||||
aggregated.usage.allTokens += stats.usage.allTokens || 0
|
||||
}
|
||||
|
||||
// 聚合总费用
|
||||
aggregated.usage.cost += stats.totalCost || 0
|
||||
|
||||
// 聚合今日使用量
|
||||
aggregated.dailyUsage.requests += stats.dailyStats.requests || 0
|
||||
aggregated.dailyUsage.inputTokens += stats.dailyStats.inputTokens || 0
|
||||
aggregated.dailyUsage.outputTokens += stats.dailyStats.outputTokens || 0
|
||||
aggregated.dailyUsage.cacheCreateTokens += stats.dailyStats.cacheCreateTokens || 0
|
||||
aggregated.dailyUsage.cacheReadTokens += stats.dailyStats.cacheReadTokens || 0
|
||||
aggregated.dailyUsage.allTokens += stats.dailyStats.allTokens || 0
|
||||
aggregated.dailyUsage.cost += stats.dailyStats.cost || 0
|
||||
|
||||
// 聚合本月使用量
|
||||
aggregated.monthlyUsage.requests += stats.monthlyStats.requests || 0
|
||||
aggregated.monthlyUsage.inputTokens += stats.monthlyStats.inputTokens || 0
|
||||
aggregated.monthlyUsage.outputTokens += stats.monthlyStats.outputTokens || 0
|
||||
aggregated.monthlyUsage.cacheCreateTokens += stats.monthlyStats.cacheCreateTokens || 0
|
||||
aggregated.monthlyUsage.cacheReadTokens += stats.monthlyStats.cacheReadTokens || 0
|
||||
aggregated.monthlyUsage.allTokens += stats.monthlyStats.allTokens || 0
|
||||
aggregated.monthlyUsage.cost += stats.monthlyStats.cost || 0
|
||||
|
||||
// 添加到个体统计
|
||||
individualStats.push({
|
||||
apiId: stats.apiId,
|
||||
name: stats.name,
|
||||
isActive: true,
|
||||
usage: stats.usage,
|
||||
dailyUsage: {
|
||||
...stats.dailyStats,
|
||||
formattedCost: CostCalculator.formatCost(stats.dailyStats.cost || 0)
|
||||
},
|
||||
monthlyUsage: {
|
||||
...stats.monthlyStats,
|
||||
formattedCost: CostCalculator.formatCost(stats.monthlyStats.cost || 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化费用显示
|
||||
aggregated.usage.formattedCost = CostCalculator.formatCost(aggregated.usage.cost)
|
||||
aggregated.dailyUsage.formattedCost = CostCalculator.formatCost(aggregated.dailyUsage.cost)
|
||||
aggregated.monthlyUsage.formattedCost = CostCalculator.formatCost(aggregated.monthlyUsage.cost)
|
||||
|
||||
logger.api(`📊 Batch stats query for ${apiIds.length} keys from ${req.ip || 'unknown'}`)
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
aggregated,
|
||||
individual: individualStats
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to process batch stats query:', error)
|
||||
return res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to retrieve batch statistics'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 📊 批量模型统计查询接口
|
||||
router.post('/api/batch-model-stats', async (req, res) => {
|
||||
try {
|
||||
const { apiIds, period = 'daily' } = req.body
|
||||
|
||||
// 验证输入
|
||||
if (!apiIds || !Array.isArray(apiIds) || apiIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid input',
|
||||
message: 'API IDs array is required'
|
||||
})
|
||||
}
|
||||
|
||||
// 限制最多查询 30 个
|
||||
if (apiIds.length > 30) {
|
||||
return res.status(400).json({
|
||||
error: 'Too many keys',
|
||||
message: 'Maximum 30 API keys can be queried at once'
|
||||
})
|
||||
}
|
||||
|
||||
const client = redis.getClientSafe()
|
||||
const tzDate = redis.getDateInTimezone()
|
||||
const today = redis.getDateStringInTimezone()
|
||||
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`
|
||||
|
||||
const modelUsageMap = new Map()
|
||||
|
||||
// 并行查询所有 API Key 的模型统计
|
||||
await Promise.all(
|
||||
apiIds.map(async (apiId) => {
|
||||
const pattern =
|
||||
period === 'daily'
|
||||
? `usage:${apiId}:model:daily:*:${today}`
|
||||
: `usage:${apiId}:model:monthly:*:${currentMonth}`
|
||||
|
||||
const keys = await client.keys(pattern)
|
||||
|
||||
for (const key of keys) {
|
||||
const match = key.match(
|
||||
period === 'daily'
|
||||
? /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) {
|
||||
if (!modelUsageMap.has(model)) {
|
||||
modelUsageMap.set(model, {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreateTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
allTokens: 0
|
||||
})
|
||||
}
|
||||
|
||||
const modelUsage = modelUsageMap.get(model)
|
||||
modelUsage.requests += parseInt(data.requests) || 0
|
||||
modelUsage.inputTokens += parseInt(data.inputTokens) || 0
|
||||
modelUsage.outputTokens += parseInt(data.outputTokens) || 0
|
||||
modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0
|
||||
modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0
|
||||
modelUsage.allTokens += parseInt(data.allTokens) || 0
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// 转换为数组并计算费用
|
||||
const modelStats = []
|
||||
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 costData = CostCalculator.calculateCost(usageData, model)
|
||||
|
||||
modelStats.push({
|
||||
model,
|
||||
requests: usage.requests,
|
||||
inputTokens: usage.inputTokens,
|
||||
outputTokens: usage.outputTokens,
|
||||
cacheCreateTokens: usage.cacheCreateTokens,
|
||||
cacheReadTokens: usage.cacheReadTokens,
|
||||
allTokens: usage.allTokens,
|
||||
costs: costData.costs,
|
||||
formatted: costData.formatted,
|
||||
pricing: costData.pricing
|
||||
})
|
||||
}
|
||||
|
||||
// 按总 token 数降序排列
|
||||
modelStats.sort((a, b) => b.allTokens - a.allTokens)
|
||||
|
||||
logger.api(`📊 Batch model stats query for ${apiIds.length} keys, period: ${period}`)
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: modelStats,
|
||||
period
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to process batch model stats query:', error)
|
||||
return res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to retrieve batch model statistics'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 📊 用户模型统计查询接口 - 安全的自查询接口
|
||||
router.post('/api/user-model-stats', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@ class AccountGroupService {
|
||||
* 创建账户分组
|
||||
* @param {Object} groupData - 分组数据
|
||||
* @param {string} groupData.name - 分组名称
|
||||
* @param {string} groupData.platform - 平台类型 (claude/gemini)
|
||||
* @param {string} groupData.platform - 平台类型 (claude/gemini/openai)
|
||||
* @param {string} groupData.description - 分组描述
|
||||
* @returns {Object} 创建的分组
|
||||
*/
|
||||
@@ -327,12 +327,36 @@ class AccountGroupService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账户ID获取其所属的分组(兼容性方法,返回单个分组)
|
||||
* @param {string} accountId - 账户ID
|
||||
* @returns {Object|null} 分组信息
|
||||
*/
|
||||
async getAccountGroup(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||
|
||||
for (const groupId of allGroupIds) {
|
||||
const isMember = await client.sismember(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
|
||||
if (isMember) {
|
||||
return await this.getGroup(groupId)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('❌ 获取账户所属分组失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据账户ID获取其所属的所有分组
|
||||
* @param {string} accountId - 账户ID
|
||||
* @returns {Array} 分组信息数组
|
||||
*/
|
||||
async getAccountGroup(accountId) {
|
||||
async getAccountGroups(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||
@@ -357,6 +381,49 @@ class AccountGroupService {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置账户的分组
|
||||
* @param {string} accountId - 账户ID
|
||||
* @param {Array} groupIds - 分组ID数组
|
||||
* @param {string} accountPlatform - 账户平台
|
||||
*/
|
||||
async setAccountGroups(accountId, groupIds, accountPlatform) {
|
||||
try {
|
||||
// 首先移除账户的所有现有分组
|
||||
await this.removeAccountFromAllGroups(accountId)
|
||||
|
||||
// 然后添加到新的分组中
|
||||
for (const groupId of groupIds) {
|
||||
await this.addAccountToGroup(accountId, groupId, accountPlatform)
|
||||
}
|
||||
|
||||
logger.success(`✅ 批量设置账户分组成功: ${accountId} -> [${groupIds.join(', ')}]`)
|
||||
} catch (error) {
|
||||
logger.error('❌ 批量设置账户分组失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有分组中移除账户
|
||||
* @param {string} accountId - 账户ID
|
||||
*/
|
||||
async removeAccountFromAllGroups(accountId) {
|
||||
try {
|
||||
const client = redis.getClientSafe()
|
||||
const allGroupIds = await client.smembers(this.GROUPS_KEY)
|
||||
|
||||
for (const groupId of allGroupIds) {
|
||||
await client.srem(`${this.GROUP_MEMBERS_PREFIX}${groupId}`, accountId)
|
||||
}
|
||||
|
||||
logger.success(`✅ 从所有分组移除账户成功: ${accountId}`)
|
||||
} catch (error) {
|
||||
logger.error('❌ 从所有分组移除账户失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AccountGroupService()
|
||||
|
||||
@@ -437,6 +437,139 @@ class ApiKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔄 恢复已删除的API Key
|
||||
async restoreApiKey(keyId, restoredBy = 'system', restoredByType = 'system') {
|
||||
try {
|
||||
const keyData = await redis.getApiKey(keyId)
|
||||
if (!keyData || Object.keys(keyData).length === 0) {
|
||||
throw new Error('API key not found')
|
||||
}
|
||||
|
||||
// 检查是否确实是已删除的key
|
||||
if (keyData.isDeleted !== 'true') {
|
||||
throw new Error('API key is not deleted')
|
||||
}
|
||||
|
||||
// 准备更新的数据
|
||||
const updatedData = { ...keyData }
|
||||
updatedData.isActive = 'true'
|
||||
updatedData.restoredAt = new Date().toISOString()
|
||||
updatedData.restoredBy = restoredBy
|
||||
updatedData.restoredByType = restoredByType
|
||||
|
||||
// 从更新的数据中移除删除相关的字段
|
||||
delete updatedData.isDeleted
|
||||
delete updatedData.deletedAt
|
||||
delete updatedData.deletedBy
|
||||
delete updatedData.deletedByType
|
||||
|
||||
// 保存更新后的数据
|
||||
await redis.setApiKey(keyId, updatedData)
|
||||
|
||||
// 使用Redis的hdel命令删除不需要的字段
|
||||
const keyName = `apikey:${keyId}`
|
||||
await redis.client.hdel(keyName, 'isDeleted', 'deletedAt', 'deletedBy', 'deletedByType')
|
||||
|
||||
// 重新建立哈希映射(恢复API Key的使用能力)
|
||||
if (keyData.apiKey) {
|
||||
await redis.setApiKeyHash(keyData.apiKey, {
|
||||
id: keyId,
|
||||
name: keyData.name,
|
||||
isActive: 'true'
|
||||
})
|
||||
}
|
||||
|
||||
logger.success(`✅ Restored API key: ${keyId} by ${restoredBy} (${restoredByType})`)
|
||||
|
||||
return { success: true, apiKey: updatedData }
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to restore API key:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🗑️ 彻底删除API Key(物理删除)
|
||||
async permanentDeleteApiKey(keyId) {
|
||||
try {
|
||||
const keyData = await redis.getApiKey(keyId)
|
||||
if (!keyData || Object.keys(keyData).length === 0) {
|
||||
throw new Error('API key not found')
|
||||
}
|
||||
|
||||
// 确保只能彻底删除已经软删除的key
|
||||
if (keyData.isDeleted !== 'true') {
|
||||
throw new Error('只能彻底删除已经删除的API Key')
|
||||
}
|
||||
|
||||
// 删除所有相关的使用统计数据
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
|
||||
|
||||
// 删除每日统计
|
||||
await redis.client.del(`usage:daily:${today}:${keyId}`)
|
||||
await redis.client.del(`usage:daily:${yesterday}:${keyId}`)
|
||||
|
||||
// 删除月度统计
|
||||
const currentMonth = today.substring(0, 7)
|
||||
await redis.client.del(`usage:monthly:${currentMonth}:${keyId}`)
|
||||
|
||||
// 删除所有相关的统计键(通过模式匹配)
|
||||
const usageKeys = await redis.client.keys(`usage:*:${keyId}*`)
|
||||
if (usageKeys.length > 0) {
|
||||
await redis.client.del(...usageKeys)
|
||||
}
|
||||
|
||||
// 删除API Key本身
|
||||
await redis.deleteApiKey(keyId)
|
||||
|
||||
logger.success(`🗑️ Permanently deleted API key: ${keyId}`)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to permanently delete API key:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 🧹 清空所有已删除的API Keys
|
||||
async clearAllDeletedApiKeys() {
|
||||
try {
|
||||
const allKeys = await this.getAllApiKeys(true)
|
||||
const deletedKeys = allKeys.filter((key) => key.isDeleted === 'true')
|
||||
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
const errors = []
|
||||
|
||||
for (const key of deletedKeys) {
|
||||
try {
|
||||
await this.permanentDeleteApiKey(key.id)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
failedCount++
|
||||
errors.push({
|
||||
keyId: key.id,
|
||||
keyName: key.name,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.success(`🧹 Cleared deleted API keys: ${successCount} success, ${failedCount} failed`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
total: deletedKeys.length,
|
||||
successCount,
|
||||
failedCount,
|
||||
errors
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to clear all deleted API keys:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 记录使用情况(支持缓存token和账户级别统计)
|
||||
async recordUsage(
|
||||
keyId,
|
||||
|
||||
@@ -249,6 +249,10 @@ async function updateAccount(accountId, updates) {
|
||||
|
||||
// 删除账户
|
||||
async function deleteAccount(accountId) {
|
||||
// 首先从所有分组中移除此账户
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||
|
||||
const client = redisClient.getClientSafe()
|
||||
const accountKey = `${AZURE_OPENAI_ACCOUNT_KEY_PREFIX}${accountId}`
|
||||
|
||||
|
||||
@@ -621,10 +621,7 @@ class ClaudeAccountService {
|
||||
try {
|
||||
// 首先从所有分组中移除此账户
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
const groups = await accountGroupService.getAccountGroup(accountId)
|
||||
for (const group of groups) {
|
||||
await accountGroupService.removeAccountFromGroup(accountId, group.id)
|
||||
}
|
||||
await accountGroupService.removeAccountFromAllGroups(accountId)
|
||||
|
||||
const result = await redis.deleteClaudeAccount(accountId)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user