mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: 新增 OpenAI-Responses 账户管理功能和独立自动停止标记机制
## 功能新增 - 实现 OpenAI-Responses 账户服务(openaiResponsesAccountService.js) - 支持使用账户内置 API Key 进行请求转发 - 实现每日额度管理和重置机制 - 支持代理配置和优先级设置 - 实现 OpenAI-Responses 中继服务(openaiResponsesRelayService.js) - 处理请求转发和响应流处理 - 自动记录使用统计信息 - 支持流式和非流式响应 - 新增管理界面的 OpenAI-Responses 账户管理功能 - 完整的 CRUD 操作支持 - 实时额度监控和状态管理 - 支持手动重置限流和每日额度 ## 架构改进 - 引入独立的自动停止标记机制,区分不同原因的自动停止 - rateLimitAutoStopped: 限流自动停止 - fiveHourAutoStopped: 5小时限制自动停止 - tempErrorAutoStopped: 临时错误自动停止 - quotaAutoStopped: 额度耗尽自动停止 - 修复手动修改调度状态时自动恢复的问题 - 统一清理逻辑,防止状态冲突 ## 其他优化 - getAccountUsageStats 支持不同账户类型参数 - 统一调度器支持 OpenAI-Responses 账户类型 - WebHook 通知增强,支持新账户类型的事件 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const openaiAccountService = require('./openaiAccountService')
|
||||
const openaiResponsesAccountService = require('./openaiResponsesAccountService')
|
||||
const accountGroupService = require('./accountGroupService')
|
||||
const redis = require('../models/redis')
|
||||
const logger = require('../utils/logger')
|
||||
@@ -32,23 +33,53 @@ class UnifiedOpenAIScheduler {
|
||||
return await this.selectAccountFromGroup(groupId, sessionHash, requestedModel, apiKeyData)
|
||||
}
|
||||
|
||||
// 普通专属账户
|
||||
const boundAccount = await openaiAccountService.getAccount(apiKeyData.openaiAccountId)
|
||||
// 普通专属账户 - 根据前缀判断是 OpenAI 还是 OpenAI-Responses 类型
|
||||
let boundAccount = null
|
||||
let accountType = 'openai'
|
||||
|
||||
// 检查是否有 responses: 前缀(用于区分 OpenAI-Responses 账户)
|
||||
if (apiKeyData.openaiAccountId.startsWith('responses:')) {
|
||||
const accountId = apiKeyData.openaiAccountId.replace('responses:', '')
|
||||
boundAccount = await openaiResponsesAccountService.getAccount(accountId)
|
||||
accountType = 'openai-responses'
|
||||
} else {
|
||||
// 普通 OpenAI 账户
|
||||
boundAccount = await openaiAccountService.getAccount(apiKeyData.openaiAccountId)
|
||||
accountType = 'openai'
|
||||
}
|
||||
|
||||
if (
|
||||
boundAccount &&
|
||||
(boundAccount.isActive === true || boundAccount.isActive === 'true') &&
|
||||
boundAccount.status !== 'error'
|
||||
) {
|
||||
// 检查是否被限流
|
||||
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
||||
if (isRateLimited) {
|
||||
const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited`
|
||||
logger.warn(`⚠️ ${errorMsg}`)
|
||||
throw new Error(errorMsg)
|
||||
if (accountType === 'openai') {
|
||||
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
|
||||
if (isRateLimited) {
|
||||
const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited`
|
||||
logger.warn(`⚠️ ${errorMsg}`)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
} else if (
|
||||
accountType === 'openai-responses' &&
|
||||
boundAccount.rateLimitStatus === 'limited'
|
||||
) {
|
||||
// OpenAI-Responses 账户的限流检查
|
||||
const isRateLimitCleared = await openaiResponsesAccountService.checkAndClearRateLimit(
|
||||
boundAccount.id
|
||||
)
|
||||
if (!isRateLimitCleared) {
|
||||
const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited`
|
||||
logger.warn(`⚠️ ${errorMsg}`)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// 专属账户:可选的模型检查(只有明确配置了supportedModels且不为空才检查)
|
||||
// OpenAI-Responses 账户默认支持所有模型
|
||||
if (
|
||||
accountType === 'openai' &&
|
||||
requestedModel &&
|
||||
boundAccount.supportedModels &&
|
||||
boundAccount.supportedModels.length > 0
|
||||
@@ -62,13 +93,19 @@ class UnifiedOpenAIScheduler {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`🎯 Using bound dedicated OpenAI account: ${boundAccount.name} (${apiKeyData.openaiAccountId}) for API key ${apiKeyData.name}`
|
||||
`🎯 Using bound dedicated ${accountType} account: ${boundAccount.name} (${boundAccount.id}) for API key ${apiKeyData.name}`
|
||||
)
|
||||
// 更新账户的最后使用时间
|
||||
await openaiAccountService.recordUsage(apiKeyData.openaiAccountId, 0)
|
||||
if (accountType === 'openai') {
|
||||
await openaiAccountService.recordUsage(boundAccount.id, 0)
|
||||
} else {
|
||||
await openaiResponsesAccountService.updateAccount(boundAccount.id, {
|
||||
lastUsedAt: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
return {
|
||||
accountId: apiKeyData.openaiAccountId,
|
||||
accountType: 'openai'
|
||||
accountId: boundAccount.id,
|
||||
accountType
|
||||
}
|
||||
} else {
|
||||
// 专属账户不可用时直接报错,不降级到共享池
|
||||
@@ -230,6 +267,40 @@ class UnifiedOpenAIScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有 OpenAI-Responses 账户(共享池)
|
||||
const openaiResponsesAccounts = await openaiResponsesAccountService.getAllAccounts()
|
||||
for (const account of openaiResponsesAccounts) {
|
||||
if (
|
||||
(account.isActive === true || account.isActive === 'true') &&
|
||||
account.status !== 'error' &&
|
||||
account.status !== 'rateLimited' &&
|
||||
(account.accountType === 'shared' || !account.accountType) && // 兼容旧数据
|
||||
this._isSchedulable(account.schedulable)
|
||||
) {
|
||||
// 检查并清除过期的限流状态
|
||||
const isRateLimitCleared = await openaiResponsesAccountService.checkAndClearRateLimit(
|
||||
account.id
|
||||
)
|
||||
|
||||
// 如果仍然处于限流状态,跳过
|
||||
if (account.rateLimitStatus === 'limited' && !isRateLimitCleared) {
|
||||
logger.debug(`⏭️ Skipping OpenAI-Responses account ${account.name} - rate limited`)
|
||||
continue
|
||||
}
|
||||
|
||||
// OpenAI-Responses 账户默认支持所有模型
|
||||
// 因为它们是第三方兼容 API,模型支持由第三方决定
|
||||
|
||||
availableAccounts.push({
|
||||
...account,
|
||||
accountId: account.id,
|
||||
accountType: 'openai-responses',
|
||||
priority: parseInt(account.priority) || 50,
|
||||
lastUsedAt: account.lastUsedAt || '0'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return availableAccounts
|
||||
}
|
||||
|
||||
@@ -262,6 +333,24 @@ class UnifiedOpenAIScheduler {
|
||||
return false
|
||||
}
|
||||
return !(await this.isAccountRateLimited(accountId))
|
||||
} else if (accountType === 'openai-responses') {
|
||||
const account = await openaiResponsesAccountService.getAccount(accountId)
|
||||
if (
|
||||
!account ||
|
||||
(account.isActive !== true && account.isActive !== 'true') ||
|
||||
account.status === 'error'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// 检查是否可调度
|
||||
if (!this._isSchedulable(account.schedulable)) {
|
||||
logger.info(`🚫 OpenAI-Responses account ${accountId} is not schedulable`)
|
||||
return false
|
||||
}
|
||||
// 检查并清除过期的限流状态
|
||||
const isRateLimitCleared =
|
||||
await openaiResponsesAccountService.checkAndClearRateLimit(accountId)
|
||||
return account.rateLimitStatus !== 'limited' || isRateLimitCleared
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
@@ -307,6 +396,18 @@ class UnifiedOpenAIScheduler {
|
||||
try {
|
||||
if (accountType === 'openai') {
|
||||
await openaiAccountService.setAccountRateLimited(accountId, true, resetsInSeconds)
|
||||
} else if (accountType === 'openai-responses') {
|
||||
// 对于 OpenAI-Responses 账户,使用与普通 OpenAI 账户类似的处理方式
|
||||
const duration = resetsInSeconds ? Math.ceil(resetsInSeconds / 60) : null
|
||||
await openaiResponsesAccountService.markAccountRateLimited(accountId, duration)
|
||||
|
||||
// 同时更新调度状态,避免继续被调度
|
||||
await openaiResponsesAccountService.updateAccount(accountId, {
|
||||
schedulable: 'false',
|
||||
rateLimitResetAt: resetsInSeconds
|
||||
? new Date(Date.now() + resetsInSeconds * 1000).toISOString()
|
||||
: new Date(Date.now() + 3600000).toISOString() // 默认1小时
|
||||
})
|
||||
}
|
||||
|
||||
// 删除会话映射
|
||||
@@ -329,6 +430,17 @@ class UnifiedOpenAIScheduler {
|
||||
try {
|
||||
if (accountType === 'openai') {
|
||||
await openaiAccountService.setAccountRateLimited(accountId, false)
|
||||
} else if (accountType === 'openai-responses') {
|
||||
// 清除 OpenAI-Responses 账户的限流状态
|
||||
await openaiResponsesAccountService.updateAccount(accountId, {
|
||||
rateLimitedAt: '',
|
||||
rateLimitStatus: '',
|
||||
rateLimitResetAt: '',
|
||||
status: 'active',
|
||||
errorMessage: '',
|
||||
schedulable: 'true'
|
||||
})
|
||||
logger.info(`✅ Rate limit cleared for OpenAI-Responses account ${accountId}`)
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
|
||||
Reference in New Issue
Block a user