mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 优化专属账号删除逻辑
This commit is contained in:
@@ -2256,6 +2256,9 @@ router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res)
|
|||||||
try {
|
try {
|
||||||
const { accountId } = req.params
|
const { accountId } = req.params
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'claude')
|
||||||
|
|
||||||
// 获取账户信息以检查是否在分组中
|
// 获取账户信息以检查是否在分组中
|
||||||
const account = await claudeAccountService.getAccount(accountId)
|
const account = await claudeAccountService.getAccount(accountId)
|
||||||
if (account && account.accountType === 'group') {
|
if (account && account.accountType === 'group') {
|
||||||
@@ -2267,8 +2270,17 @@ router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res)
|
|||||||
|
|
||||||
await claudeAccountService.deleteAccount(accountId)
|
await claudeAccountService.deleteAccount(accountId)
|
||||||
|
|
||||||
logger.success(`🗑️ Admin deleted Claude account: ${accountId}`)
|
let message = 'Claude账号已成功删除'
|
||||||
return res.json({ success: true, message: 'Claude account deleted successfully' })
|
if (unboundCount > 0) {
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`🗑️ Admin deleted Claude account: ${accountId}, unbound ${unboundCount} keys`)
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to delete Claude account:', error)
|
logger.error('❌ Failed to delete Claude account:', error)
|
||||||
return res
|
return res
|
||||||
@@ -2634,6 +2646,9 @@ router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (r
|
|||||||
try {
|
try {
|
||||||
const { accountId } = req.params
|
const { accountId } = req.params
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'claude-console')
|
||||||
|
|
||||||
// 获取账户信息以检查是否在分组中
|
// 获取账户信息以检查是否在分组中
|
||||||
const account = await claudeConsoleAccountService.getAccount(accountId)
|
const account = await claudeConsoleAccountService.getAccount(accountId)
|
||||||
if (account && account.accountType === 'group') {
|
if (account && account.accountType === 'group') {
|
||||||
@@ -2645,8 +2660,19 @@ router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (r
|
|||||||
|
|
||||||
await claudeConsoleAccountService.deleteAccount(accountId)
|
await claudeConsoleAccountService.deleteAccount(accountId)
|
||||||
|
|
||||||
logger.success(`🗑️ Admin deleted Claude Console account: ${accountId}`)
|
let message = 'Claude Console账号已成功删除'
|
||||||
return res.json({ success: true, message: 'Claude Console account deleted successfully' })
|
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) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to delete Claude Console account:', error)
|
logger.error('❌ Failed to delete Claude Console account:', error)
|
||||||
return res
|
return res
|
||||||
@@ -3028,6 +3054,9 @@ router.delete('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
try {
|
try {
|
||||||
const { accountId } = req.params
|
const { accountId } = req.params
|
||||||
|
|
||||||
|
// 尝试自动解绑(CCR账户实际上不会绑定API Key,但保持代码一致性)
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'ccr')
|
||||||
|
|
||||||
// 获取账户信息以检查是否在分组中
|
// 获取账户信息以检查是否在分组中
|
||||||
const account = await ccrAccountService.getAccount(accountId)
|
const account = await ccrAccountService.getAccount(accountId)
|
||||||
if (account && account.accountType === 'group') {
|
if (account && account.accountType === 'group') {
|
||||||
@@ -3039,8 +3068,18 @@ router.delete('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) =>
|
|||||||
|
|
||||||
await ccrAccountService.deleteAccount(accountId)
|
await ccrAccountService.deleteAccount(accountId)
|
||||||
|
|
||||||
|
let message = 'CCR账号已成功删除'
|
||||||
|
if (unboundCount > 0) {
|
||||||
|
// 理论上不会发生,但保持消息格式一致
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
logger.success(`🗑️ Admin deleted CCR account: ${accountId}`)
|
logger.success(`🗑️ Admin deleted CCR account: ${accountId}`)
|
||||||
return res.json({ success: true, message: 'CCR account deleted successfully' })
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to delete CCR account:', error)
|
logger.error('❌ Failed to delete CCR account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message })
|
return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message })
|
||||||
@@ -3384,6 +3423,9 @@ router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res
|
|||||||
try {
|
try {
|
||||||
const { accountId } = req.params
|
const { accountId } = req.params
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'bedrock')
|
||||||
|
|
||||||
const result = await bedrockAccountService.deleteAccount(accountId)
|
const result = await bedrockAccountService.deleteAccount(accountId)
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -3392,8 +3434,17 @@ router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res
|
|||||||
.json({ error: 'Failed to delete Bedrock account', message: result.error })
|
.json({ error: 'Failed to delete Bedrock account', message: result.error })
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}`)
|
let message = 'Bedrock账号已成功删除'
|
||||||
return res.json({ success: true, message: 'Bedrock account deleted successfully' })
|
if (unboundCount > 0) {
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}, unbound ${unboundCount} keys`)
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to delete Bedrock account:', error)
|
logger.error('❌ Failed to delete Bedrock account:', error)
|
||||||
return res
|
return res
|
||||||
@@ -3830,6 +3881,9 @@ router.delete('/gemini-accounts/:accountId', authenticateAdmin, async (req, res)
|
|||||||
try {
|
try {
|
||||||
const { accountId } = req.params
|
const { accountId } = req.params
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'gemini')
|
||||||
|
|
||||||
// 获取账户信息以检查是否在分组中
|
// 获取账户信息以检查是否在分组中
|
||||||
const account = await geminiAccountService.getAccount(accountId)
|
const account = await geminiAccountService.getAccount(accountId)
|
||||||
if (account && account.accountType === 'group') {
|
if (account && account.accountType === 'group') {
|
||||||
@@ -3841,8 +3895,17 @@ router.delete('/gemini-accounts/:accountId', authenticateAdmin, async (req, res)
|
|||||||
|
|
||||||
await geminiAccountService.deleteAccount(accountId)
|
await geminiAccountService.deleteAccount(accountId)
|
||||||
|
|
||||||
logger.success(`🗑️ Admin deleted Gemini account: ${accountId}`)
|
let message = 'Gemini账号已成功删除'
|
||||||
return res.json({ success: true, message: 'Gemini account deleted successfully' })
|
if (unboundCount > 0) {
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`🗑️ Admin deleted Gemini account: ${accountId}, unbound ${unboundCount} keys`)
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to delete Gemini account:', error)
|
logger.error('❌ Failed to delete Gemini account:', error)
|
||||||
return res.status(500).json({ error: 'Failed to delete account', message: error.message })
|
return res.status(500).json({ error: 'Failed to delete account', message: error.message })
|
||||||
@@ -6692,6 +6755,9 @@ router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'openai')
|
||||||
|
|
||||||
// 如果账户在分组中,从分组中移除
|
// 如果账户在分组中,从分组中移除
|
||||||
if (account.accountType === 'group') {
|
if (account.accountType === 'group') {
|
||||||
const group = await accountGroupService.getAccountGroup(id)
|
const group = await accountGroupService.getAccountGroup(id)
|
||||||
@@ -6702,11 +6768,19 @@ router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
await openaiAccountService.deleteAccount(id)
|
await openaiAccountService.deleteAccount(id)
|
||||||
|
|
||||||
logger.success(`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id})`)
|
let message = 'OpenAI账号已成功删除'
|
||||||
|
if (unboundCount > 0) {
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(
|
||||||
|
`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id}), unbound ${unboundCount} keys`
|
||||||
|
)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '账户删除成功'
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('删除 OpenAI 账户失败:', error)
|
logger.error('删除 OpenAI 账户失败:', error)
|
||||||
@@ -7055,11 +7129,22 @@ router.delete('/azure-openai-accounts/:id', authenticateAdmin, async (req, res)
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'azure_openai')
|
||||||
|
|
||||||
await azureOpenaiAccountService.deleteAccount(id)
|
await azureOpenaiAccountService.deleteAccount(id)
|
||||||
|
|
||||||
|
let message = 'Azure OpenAI账号已成功删除'
|
||||||
|
if (unboundCount > 0) {
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`🗑️ Admin deleted Azure OpenAI account: ${id}, unbound ${unboundCount} keys`)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Azure OpenAI account deleted successfully'
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to delete Azure OpenAI account:', error)
|
logger.error('Failed to delete Azure OpenAI account:', error)
|
||||||
@@ -7424,6 +7509,9 @@ router.delete('/openai-responses-accounts/:id', authenticateAdmin, async (req, r
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动解绑所有绑定的 API Keys
|
||||||
|
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'openai-responses')
|
||||||
|
|
||||||
// 检查是否在分组中
|
// 检查是否在分组中
|
||||||
const groups = await accountGroupService.getAllGroups()
|
const groups = await accountGroupService.getAllGroups()
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
@@ -7434,7 +7522,20 @@ router.delete('/openai-responses-accounts/:id', authenticateAdmin, async (req, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await openaiResponsesAccountService.deleteAccount(id)
|
const result = await openaiResponsesAccountService.deleteAccount(id)
|
||||||
res.json({ success: true, ...result })
|
|
||||||
|
let message = 'OpenAI-Responses账号已成功删除'
|
||||||
|
if (unboundCount > 0) {
|
||||||
|
message += `,${unboundCount} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`🗑️ Admin deleted OpenAI-Responses account: ${id}, unbound ${unboundCount} keys`)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
...result,
|
||||||
|
message,
|
||||||
|
unboundKeys: unboundCount
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to delete OpenAI-Responses account:', error)
|
logger.error('Failed to delete OpenAI-Responses account:', error)
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
|
|||||||
@@ -1328,6 +1328,70 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔓 解绑账号从所有API Keys
|
||||||
|
async unbindAccountFromAllKeys(accountId, accountType) {
|
||||||
|
try {
|
||||||
|
// 账号类型与字段的映射关系
|
||||||
|
const fieldMap = {
|
||||||
|
claude: 'claudeAccountId',
|
||||||
|
'claude-console': 'claudeConsoleAccountId',
|
||||||
|
gemini: 'geminiAccountId',
|
||||||
|
openai: 'openaiAccountId',
|
||||||
|
'openai-responses': 'openaiAccountId', // 特殊处理,带 responses: 前缀
|
||||||
|
azure_openai: 'azureOpenaiAccountId',
|
||||||
|
bedrock: 'bedrockAccountId',
|
||||||
|
ccr: null // CCR 账号没有对应的 API Key 字段
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = fieldMap[accountType]
|
||||||
|
if (!field) {
|
||||||
|
logger.info(`账号类型 ${accountType} 不需要解绑 API Key`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有API Keys
|
||||||
|
const allKeys = await this.getAllApiKeys()
|
||||||
|
|
||||||
|
// 筛选绑定到此账号的 API Keys
|
||||||
|
let boundKeys = []
|
||||||
|
if (accountType === 'openai-responses') {
|
||||||
|
// OpenAI-Responses 特殊处理:查找 openaiAccountId 字段中带 responses: 前缀的
|
||||||
|
boundKeys = allKeys.filter((key) => key.openaiAccountId === `responses:${accountId}`)
|
||||||
|
} else {
|
||||||
|
// 其他账号类型正常匹配
|
||||||
|
boundKeys = allKeys.filter((key) => key[field] === accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量解绑
|
||||||
|
for (const key of boundKeys) {
|
||||||
|
const updates = {}
|
||||||
|
if (accountType === 'openai-responses') {
|
||||||
|
updates.openaiAccountId = null
|
||||||
|
} else if (accountType === 'claude-console') {
|
||||||
|
updates.claudeConsoleAccountId = null
|
||||||
|
} else {
|
||||||
|
updates[field] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateApiKey(key.id, updates)
|
||||||
|
logger.info(
|
||||||
|
`✅ 自动解绑 API Key ${key.id} (${key.name}) 从 ${accountType} 账号 ${accountId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundKeys.length > 0) {
|
||||||
|
logger.success(
|
||||||
|
`🔓 成功解绑 ${boundKeys.length} 个 API Key 从 ${accountType} 账号 ${accountId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundKeys.length
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ 解绑 API Keys 失败 (${accountType} 账号 ${accountId}):`, error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🧹 清理过期的API Keys
|
// 🧹 清理过期的API Keys
|
||||||
async cleanupExpiredKeys() {
|
async cleanupExpiredKeys() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1598,7 +1598,7 @@ const editAccount = (account) => {
|
|||||||
// 删除账户
|
// 删除账户
|
||||||
const deleteAccount = async (account) => {
|
const deleteAccount = async (account) => {
|
||||||
// 检查是否有API Key绑定到此账号
|
// 检查是否有API Key绑定到此账号
|
||||||
const boundKeysCount = apiKeys.value.filter(
|
const boundKeys = apiKeys.value.filter(
|
||||||
(key) =>
|
(key) =>
|
||||||
key.claudeAccountId === account.id ||
|
key.claudeAccountId === account.id ||
|
||||||
key.claudeConsoleAccountId === account.id ||
|
key.claudeConsoleAccountId === account.id ||
|
||||||
@@ -1606,22 +1606,18 @@ const deleteAccount = async (account) => {
|
|||||||
key.openaiAccountId === account.id ||
|
key.openaiAccountId === account.id ||
|
||||||
key.azureOpenaiAccountId === account.id ||
|
key.azureOpenaiAccountId === account.id ||
|
||||||
key.openaiAccountId === `responses:${account.id}`
|
key.openaiAccountId === `responses:${account.id}`
|
||||||
).length
|
|
||||||
|
|
||||||
if (boundKeysCount > 0) {
|
|
||||||
showToast(
|
|
||||||
`无法删除此账号,有 ${boundKeysCount} 个API Key绑定到此账号,请先解绑所有API Key`,
|
|
||||||
'error'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmed = await showConfirm(
|
|
||||||
'删除账户',
|
|
||||||
`确定要删除账户 "${account.name}" 吗?\n\n此操作不可恢复。`,
|
|
||||||
'删除',
|
|
||||||
'取消'
|
|
||||||
)
|
)
|
||||||
|
const boundKeysCount = boundKeys.length
|
||||||
|
|
||||||
|
// 构建确认消息
|
||||||
|
let confirmMessage = `确定要删除账户 "${account.name}" 吗?`
|
||||||
|
if (boundKeysCount > 0) {
|
||||||
|
confirmMessage += `\n\n⚠️ 注意:此账号有 ${boundKeysCount} 个 API Key 绑定。`
|
||||||
|
confirmMessage += `\n删除后,这些 API Key 将自动切换为共享池模式。`
|
||||||
|
}
|
||||||
|
confirmMessage += '\n\n此操作不可恢复。'
|
||||||
|
|
||||||
|
const confirmed = await showConfirm('删除账户', confirmMessage, '删除', '取消')
|
||||||
|
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
@@ -1648,10 +1644,18 @@ const deleteAccount = async (account) => {
|
|||||||
const data = await apiClient.delete(endpoint)
|
const data = await apiClient.delete(endpoint)
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast('账户已删除', 'success')
|
// 根据解绑结果显示不同的消息
|
||||||
// 清空分组成员缓存,因为账户可能从分组中移除
|
let toastMessage = '账户已成功删除'
|
||||||
|
if (data.unboundKeys > 0) {
|
||||||
|
toastMessage += `,${data.unboundKeys} 个 API Key 已切换为共享池模式`
|
||||||
|
}
|
||||||
|
showToast(toastMessage, 'success')
|
||||||
|
|
||||||
|
// 清空相关缓存
|
||||||
groupMembersLoaded.value = false
|
groupMembersLoaded.value = false
|
||||||
|
apiKeysLoaded.value = false // 重新加载API Keys以反映解绑变化
|
||||||
loadAccounts()
|
loadAccounts()
|
||||||
|
loadApiKeys(true) // 强制重新加载API Keys
|
||||||
} else {
|
} else {
|
||||||
showToast(data.message || '删除失败', 'error')
|
showToast(data.message || '删除失败', 'error')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user