Merge remote-tracking branch 'f3n9/main' into main-um-8

This commit is contained in:
Feng Yue
2025-09-03 15:40:28 +08:00
22 changed files with 2247 additions and 683 deletions

View File

@@ -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,