feat: 实现OpenAI账户管理和统一调度系统

- 新增 OpenAI 账户管理服务,支持多账户轮询和负载均衡
- 实现统一的 OpenAI API 调度器,智能选择最优账户
- 优化成本计算器,支持更精确的 token 计算
- 更新模型定价数据,包含最新的 OpenAI 模型价格
- 增强 API Key 管理,支持更灵活的配额控制
- 改进管理界面,添加教程视图和账户分组管理
- 优化限流配置组件,提供更直观的用户体验

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-11 13:58:43 +08:00
parent f22a38d24a
commit f462684f97
22 changed files with 6163 additions and 3134 deletions

View File

@@ -4,6 +4,7 @@ const claudeAccountService = require('../services/claudeAccountService')
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService')
const bedrockAccountService = require('../services/bedrockAccountService')
const geminiAccountService = require('../services/geminiAccountService')
const openaiAccountService = require('../services/openaiAccountService')
const accountGroupService = require('../services/accountGroupService')
const redis = require('../models/redis')
const { authenticateAdmin } = require('../middleware/auth')
@@ -388,6 +389,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
permissions,
concurrencyLimit,
rateLimitWindow,
@@ -483,6 +485,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
permissions,
concurrencyLimit,
rateLimitWindow,
@@ -515,6 +518,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
permissions,
concurrencyLimit,
rateLimitWindow,
@@ -557,6 +561,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => {
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
permissions,
concurrencyLimit,
rateLimitWindow,
@@ -626,6 +631,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
claudeAccountId,
claudeConsoleAccountId,
geminiAccountId,
openaiAccountId,
permissions,
enableModelRestriction,
restrictedModels,
@@ -684,12 +690,17 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
updates.geminiAccountId = geminiAccountId || ''
}
if (openaiAccountId !== undefined) {
// 空字符串表示解绑null或空字符串都设置为空字符串
updates.openaiAccountId = openaiAccountId || ''
}
if (permissions !== undefined) {
// 验证权限值
if (!['claude', 'gemini', 'all'].includes(permissions)) {
if (!['claude', 'gemini', 'openai', 'all'].includes(permissions)) {
return res
.status(400)
.json({ error: 'Invalid permissions value. Must be claude, gemini, or all' })
.json({ error: 'Invalid permissions value. Must be claude, gemini, openai, or all' })
}
updates.permissions = permissions
}
@@ -894,6 +905,11 @@ router.get('/account-groups/:groupId/members', authenticateAdmin, async (req, re
account = await geminiAccountService.getAccount(memberId)
}
// 如果还找不到尝试OpenAI账户
if (!account) {
account = await openaiAccountService.getAccount(memberId)
}
if (account) {
members.push(account)
}
@@ -2396,6 +2412,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeConsoleAccounts,
geminiAccounts,
bedrockAccountsResult,
openaiAccounts,
todayStats,
systemAverages,
realtimeMetrics
@@ -2406,6 +2423,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeConsoleAccountService.getAllAccounts(),
geminiAccountService.getAllAccounts(),
bedrockAccountService.getAllAccounts(),
redis.getAllOpenAIAccounts(),
redis.getTodayStats(),
redis.getSystemAverages(),
redis.getRealtimeSystemMetrics()
@@ -2543,6 +2561,39 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length
// OpenAI账户统计
// 注意OpenAI账户的isActive和schedulable是字符串类型默认值为'true'
const normalOpenAIAccounts = openaiAccounts.filter(
(acc) =>
(acc.isActive === 'true' ||
acc.isActive === true ||
(!acc.isActive && acc.isActive !== 'false' && acc.isActive !== false)) &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== 'false' &&
acc.schedulable !== false && // 包括'true'、true和undefined
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
const abnormalOpenAIAccounts = openaiAccounts.filter(
(acc) =>
acc.isActive === 'false' ||
acc.isActive === false ||
acc.status === 'blocked' ||
acc.status === 'unauthorized'
).length
const pausedOpenAIAccounts = openaiAccounts.filter(
(acc) =>
(acc.schedulable === 'false' || acc.schedulable === false) &&
(acc.isActive === 'true' ||
acc.isActive === true ||
(!acc.isActive && acc.isActive !== 'false' && acc.isActive !== false)) &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length
const rateLimitedOpenAIAccounts = openaiAccounts.filter(
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length
const dashboard = {
overview: {
totalApiKeys: apiKeys.length,
@@ -2552,27 +2603,32 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeAccounts.length +
claudeConsoleAccounts.length +
geminiAccounts.length +
bedrockAccounts.length,
bedrockAccounts.length +
openaiAccounts.length,
normalAccounts:
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts,
normalBedrockAccounts +
normalOpenAIAccounts,
abnormalAccounts:
abnormalClaudeAccounts +
abnormalClaudeConsoleAccounts +
abnormalGeminiAccounts +
abnormalBedrockAccounts,
abnormalBedrockAccounts +
abnormalOpenAIAccounts,
pausedAccounts:
pausedClaudeAccounts +
pausedClaudeConsoleAccounts +
pausedGeminiAccounts +
pausedBedrockAccounts,
pausedBedrockAccounts +
pausedOpenAIAccounts,
rateLimitedAccounts:
rateLimitedClaudeAccounts +
rateLimitedClaudeConsoleAccounts +
rateLimitedGeminiAccounts +
rateLimitedBedrockAccounts,
rateLimitedBedrockAccounts +
rateLimitedOpenAIAccounts,
// 各平台详细统计
accountsByPlatform: {
claude: {
@@ -2602,6 +2658,13 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
abnormal: abnormalBedrockAccounts,
paused: pausedBedrockAccounts,
rateLimited: rateLimitedBedrockAccounts
},
openai: {
total: openaiAccounts.length,
normal: normalOpenAIAccounts,
abnormal: abnormalOpenAIAccounts,
paused: pausedOpenAIAccounts,
rateLimited: rateLimitedOpenAIAccounts
}
},
// 保留旧字段以兼容
@@ -2609,7 +2672,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts,
normalBedrockAccounts +
normalOpenAIAccounts,
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts,
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
@@ -4513,7 +4577,7 @@ router.post('/openai-accounts/exchange-code', authenticateAdmin, async (req, res
// 获取所有 OpenAI 账户
router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
try {
const accounts = await redis.getAllOpenAIAccounts()
const accounts = await openaiAccountService.getAllAccounts()
logger.info(`获取 OpenAI 账户列表: ${accounts.length} 个账户`)
@@ -4553,60 +4617,41 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
message: '账户名称不能为空'
})
}
const id = uuidv4()
// 创建账户数据
const accountData = {
id,
name,
description: description || '',
platform: 'openai',
accountType: accountType || 'shared',
groupId: groupId || null,
dedicatedApiKeys: dedicatedApiKeys || [],
priority: priority || 50,
rateLimitDuration: rateLimitDuration || 60,
enabled: true,
idToken: claudeAccountService._encryptSensitiveData(openaiOauth.idToken),
accessToken: claudeAccountService._encryptSensitiveData(openaiOauth.accessToken),
refreshToken: claudeAccountService._encryptSensitiveData(openaiOauth.refreshToken),
accountId: accountInfo?.accountId || '',
expiresAt: (Math.floor(Date.now() / 1000) + openaiOauth.expires_in) * 1000,
chatgptUserId: accountInfo?.chatgptUserId || '',
organizationId: accountInfo?.organizationId || '',
organizationRole: accountInfo?.organizationRole || '',
organizationTitle: accountInfo?.organizationTitle || '',
planType: accountInfo?.planType || '',
email: claudeAccountService._encryptSensitiveData(accountInfo?.email || ''),
emailVerified: accountInfo?.emailVerified || false,
openaiOauth: openaiOauth || {},
accountInfo: accountInfo || {},
proxy: proxy?.enabled
? {
type: proxy.type,
host: proxy.host,
port: proxy.port,
username: proxy.username || null,
password: proxy.password || null
}
: null,
isActive: true,
status: 'active',
lastRefresh: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
schedulable: true
}
// 存储代理配置(如果提供)
if (proxy?.enabled) {
accountData.proxy = {
type: proxy.type,
host: proxy.host,
port: proxy.port,
username: proxy.username || null,
password: proxy.password || null
}
// 创建账户
const createdAccount = await openaiAccountService.createAccount(accountData)
// 如果是分组类型,添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(createdAccount.id, groupId, 'openai')
}
// 保存到 Redis
const accountId = await redis.setOpenAiAccount(id, accountData)
logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${accountId})`)
logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${createdAccount.id})`)
return res.json({
success: true,
data: {
id: accountId,
...accountData
}
data: createdAccount
})
} catch (error) {
logger.error('创建 OpenAI 账户失败:', error)
@@ -4619,19 +4664,100 @@ router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
})
// 更新 OpenAI 账户
router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) =>
//TODO:
res.json({
success: true
})
)
router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const updates = req.body
// 验证accountType的有效性
if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
return res
.status(400)
.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' })
}
// 获取账户当前信息以处理分组变更
const currentAccount = await openaiAccountService.getAccount(id)
if (!currentAccount) {
return res.status(404).json({ error: 'Account not found' })
}
// 处理分组的变更
if (updates.accountType !== undefined) {
// 如果之前是分组类型,需要从原分组中移除
if (currentAccount.accountType === 'group') {
const oldGroup = await accountGroupService.getAccountGroup(id)
if (oldGroup) {
await accountGroupService.removeAccountFromGroup(id, oldGroup.id)
}
}
// 如果新类型是分组,添加到新分组
if (updates.accountType === 'group' && updates.groupId) {
await accountGroupService.addAccountToGroup(id, updates.groupId, 'openai')
}
}
// 准备更新数据
const updateData = { ...updates }
// 处理敏感数据加密
if (updates.openaiOauth) {
updateData.openaiOauth = updates.openaiOauth
if (updates.openaiOauth.idToken) {
updateData.idToken = updates.openaiOauth.idToken
}
if (updates.openaiOauth.accessToken) {
updateData.accessToken = updates.openaiOauth.accessToken
}
if (updates.openaiOauth.refreshToken) {
updateData.refreshToken = updates.openaiOauth.refreshToken
}
if (updates.openaiOauth.expires_in) {
updateData.expiresAt = new Date(
Date.now() + updates.openaiOauth.expires_in * 1000
).toISOString()
}
}
// 更新账户信息
if (updates.accountInfo) {
updateData.accountId = updates.accountInfo.accountId || currentAccount.accountId
updateData.chatgptUserId = updates.accountInfo.chatgptUserId || currentAccount.chatgptUserId
updateData.organizationId =
updates.accountInfo.organizationId || currentAccount.organizationId
updateData.organizationRole =
updates.accountInfo.organizationRole || currentAccount.organizationRole
updateData.organizationTitle =
updates.accountInfo.organizationTitle || currentAccount.organizationTitle
updateData.planType = updates.accountInfo.planType || currentAccount.planType
updateData.email = updates.accountInfo.email || currentAccount.email
updateData.emailVerified =
updates.accountInfo.emailVerified !== undefined
? updates.accountInfo.emailVerified
: currentAccount.emailVerified
}
const updatedAccount = await openaiAccountService.updateAccount(id, updateData)
logger.success(`📝 Admin updated OpenAI account: ${id}`)
return res.json({ success: true, data: updatedAccount })
} catch (error) {
logger.error('❌ Failed to update OpenAI account:', error)
return res.status(500).json({ error: 'Failed to update account', message: error.message })
}
})
// 删除 OpenAI 账户
router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await redis.getOpenAiAccount(id)
const account = await openaiAccountService.getAccount(id)
if (!account) {
return res.status(404).json({
success: false,
@@ -4639,7 +4765,15 @@ router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
})
}
await redis.deleteOpenAiAccount(id)
// 如果账户在分组中,从分组中移除
if (account.accountType === 'group') {
const group = await accountGroupService.getAccountGroup(id)
if (group) {
await accountGroupService.removeAccountFromGroup(id, group.id)
}
}
await openaiAccountService.deleteAccount(id)
logger.success(`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id})`)
@@ -4695,4 +4829,30 @@ router.put('/openai-accounts/:id/toggle', authenticateAdmin, async (req, res) =>
}
})
// 切换 OpenAI 账户调度状态
router.put(
'/openai-accounts/:accountId/toggle-schedulable',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
const result = await openaiAccountService.toggleSchedulable(accountId)
return res.json({
success: result.success,
schedulable: result.schedulable,
message: result.schedulable ? '已启用调度' : '已禁用调度'
})
} catch (error) {
logger.error('切换 OpenAI 账户调度状态失败:', error)
return res.status(500).json({
success: false,
message: '切换调度状态失败',
error: error.message
})
}
}
)
module.exports = router