mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user