Files
claude-relay-service/src/routes/admin/bedrockAccounts.js
2025-11-27 22:16:45 +08:00

372 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Admin Routes - Bedrock Accounts Management
* AWS Bedrock 账户管理路由
*/
const express = require('express')
const router = express.Router()
const bedrockAccountService = require('../../services/bedrockAccountService')
const apiKeyService = require('../../services/apiKeyService')
const accountGroupService = require('../../services/accountGroupService')
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')
// ☁️ Bedrock 账户管理
// 获取所有Bedrock账户
router.get('/', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
const result = await bedrockAccountService.getAllAccounts()
if (!result.success) {
return res
.status(500)
.json({ error: 'Failed to get Bedrock accounts', message: result.error })
}
let accounts = result.data
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'bedrock') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id, 'openai')
const groupInfos = await accountGroupService.getAccountGroups(account.id)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
groupInfos,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
}
} catch (statsError) {
logger.warn(
`⚠️ Failed to get usage stats for Bedrock account ${account.id}:`,
statsError.message
)
try {
const groupInfos = await accountGroupService.getAccountGroups(account.id)
const formattedAccount = formatAccountExpiry(account)
return {
...formattedAccount,
groupInfos,
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
} catch (groupError) {
logger.warn(
`⚠️ Failed to get group info for 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 Bedrock accounts:', error)
return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message })
}
})
// 创建新的Bedrock账户
router.post('/', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
region,
awsCredentials,
defaultModel,
priority,
accountType,
credentialType
} = req.body
if (!name) {
return res.status(400).json({ error: 'Name is 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'].includes(accountType)) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared" or "dedicated"' })
}
// 验证credentialType的有效性
if (credentialType && !['default', 'access_key', 'bearer_token'].includes(credentialType)) {
return res.status(400).json({
error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"'
})
}
const result = await bedrockAccountService.createAccount({
name,
description: description || '',
region: region || 'us-east-1',
awsCredentials,
defaultModel,
priority: priority || 50,
accountType: accountType || 'shared',
credentialType: credentialType || 'default'
})
if (!result.success) {
return res
.status(500)
.json({ error: 'Failed to create Bedrock account', message: result.error })
}
logger.success(`☁️ Admin created Bedrock account: ${name}`)
const formattedAccount = formatAccountExpiry(result.data)
return res.json({ success: true, data: formattedAccount })
} catch (error) {
logger.error('❌ Failed to create Bedrock account:', error)
return res
.status(500)
.json({ error: 'Failed to create Bedrock account', message: error.message })
}
})
// 更新Bedrock账户
router.put('/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const updates = req.body
// ✅ 【新增】映射字段名:前端的 expiresAt -> 后端的 subscriptionExpiresAt
const mappedUpdates = mapExpiryField(updates, 'Bedrock', 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'].includes(mappedUpdates.accountType)) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared" or "dedicated"' })
}
// 验证credentialType的有效性
if (
mappedUpdates.credentialType &&
!['default', 'access_key', 'bearer_token'].includes(mappedUpdates.credentialType)
) {
return res.status(400).json({
error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"'
})
}
const result = await bedrockAccountService.updateAccount(accountId, mappedUpdates)
if (!result.success) {
return res
.status(500)
.json({ error: 'Failed to update Bedrock account', message: result.error })
}
logger.success(`📝 Admin updated Bedrock account: ${accountId}`)
return res.json({ success: true, message: 'Bedrock account updated successfully' })
} catch (error) {
logger.error('❌ Failed to update Bedrock account:', error)
return res
.status(500)
.json({ error: 'Failed to update Bedrock account', message: error.message })
}
})
// 删除Bedrock账户
router.delete('/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'bedrock')
const result = await bedrockAccountService.deleteAccount(accountId)
if (!result.success) {
return res
.status(500)
.json({ error: 'Failed to delete Bedrock account', message: result.error })
}
let message = 'Bedrock账号已成功删除'
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) {
logger.error('❌ Failed to delete Bedrock account:', error)
return res
.status(500)
.json({ error: 'Failed to delete Bedrock account', message: error.message })
}
})
// 切换Bedrock账户状态
router.put('/:accountId/toggle', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const accountResult = await bedrockAccountService.getAccount(accountId)
if (!accountResult.success) {
return res.status(404).json({ error: 'Account not found' })
}
const newStatus = !accountResult.data.isActive
const updateResult = await bedrockAccountService.updateAccount(accountId, {
isActive: newStatus
})
if (!updateResult.success) {
return res
.status(500)
.json({ error: 'Failed to toggle account status', message: updateResult.error })
}
logger.success(
`🔄 Admin toggled Bedrock account status: ${accountId} -> ${
newStatus ? 'active' : 'inactive'
}`
)
return res.json({ success: true, isActive: newStatus })
} catch (error) {
logger.error('❌ Failed to toggle Bedrock account status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle account status', message: error.message })
}
})
// 切换Bedrock账户调度状态
router.put('/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const accountResult = await bedrockAccountService.getAccount(accountId)
if (!accountResult.success) {
return res.status(404).json({ error: 'Account not found' })
}
const newSchedulable = !accountResult.data.schedulable
const updateResult = await bedrockAccountService.updateAccount(accountId, {
schedulable: newSchedulable
})
if (!updateResult.success) {
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: updateResult.error })
}
// 如果账号被禁用发送webhook通知
if (!newSchedulable) {
await webhookNotifier.sendAccountAnomalyNotification({
accountId: accountResult.data.id,
accountName: accountResult.data.name || 'Bedrock Account',
platform: 'bedrock',
status: 'disabled',
errorCode: 'BEDROCK_MANUALLY_DISABLED',
reason: '账号已被管理员手动禁用调度',
timestamp: new Date().toISOString()
})
}
logger.success(
`🔄 Admin toggled Bedrock account schedulable status: ${accountId} -> ${
newSchedulable ? 'schedulable' : 'not schedulable'
}`
)
return res.json({ success: true, schedulable: newSchedulable })
} catch (error) {
logger.error('❌ Failed to toggle Bedrock account schedulable status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: error.message })
}
})
// 测试Bedrock账户连接
router.post('/:accountId/test', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await bedrockAccountService.testAccount(accountId)
if (!result.success) {
return res.status(500).json({ error: 'Account test failed', message: result.error })
}
logger.success(`🧪 Admin tested Bedrock account: ${accountId} - ${result.data.status}`)
return res.json({ success: true, data: result.data })
} catch (error) {
logger.error('❌ Failed to test Bedrock account:', error)
return res.status(500).json({ error: 'Failed to test Bedrock account', message: error.message })
}
})
module.exports = router