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:
shaw
2025-09-10 15:41:52 +08:00
parent 1c3b74f45b
commit 08946c67ea
17 changed files with 3061 additions and 238 deletions

View File

@@ -603,6 +603,25 @@ class ClaudeAccountService {
updatedData.updatedAt = new Date().toISOString()
// 如果是手动修改调度状态,清除所有自动停止相关的字段
if (Object.prototype.hasOwnProperty.call(updates, 'schedulable')) {
// 清除所有自动停止的标记,防止自动恢复
delete updatedData.rateLimitAutoStopped
delete updatedData.fiveHourAutoStopped
delete updatedData.fiveHourStoppedAt
delete updatedData.tempErrorAutoStopped
// 兼容旧的标记(逐步迁移)
delete updatedData.autoStoppedAt
delete updatedData.stoppedReason
// 如果是手动启用调度,记录日志
if (updates.schedulable === true || updates.schedulable === 'true') {
logger.info(`✅ Manually enabled scheduling for account ${accountId}`)
} else {
logger.info(`⛔ Manually disabled scheduling for account ${accountId}`)
}
}
// 检查是否手动禁用了账号如果是则发送webhook通知
if (updates.isActive === 'false' && accountData.isActive === 'true') {
try {
@@ -1088,7 +1107,9 @@ class ClaudeAccountService {
updatedAccountData.rateLimitedAt = new Date().toISOString()
updatedAccountData.rateLimitStatus = 'limited'
// 限流时停止调度,与 OpenAI 账号保持一致
updatedAccountData.schedulable = false
updatedAccountData.schedulable = 'false'
// 使用独立的限流自动停止标记,避免与其他自动停止冲突
updatedAccountData.rateLimitAutoStopped = 'true'
// 如果提供了准确的限流重置时间戳来自API响应头
if (rateLimitResetTimestamp) {
@@ -1173,13 +1194,16 @@ class ClaudeAccountService {
delete accountData.rateLimitedAt
delete accountData.rateLimitStatus
delete accountData.rateLimitEndAt // 清除限流结束时间
// 恢复可调度状态,与 OpenAI 账号保持一致
accountData.schedulable = true
// 只恢复因限流而自动停止的账户
if (accountData.rateLimitAutoStopped === 'true' && accountData.schedulable === 'false') {
accountData.schedulable = 'true'
delete accountData.rateLimitAutoStopped
logger.info(`✅ Auto-resuming scheduling for account ${accountId} after rate limit cleared`)
}
await redis.setClaudeAccount(accountId, accountData)
logger.success(
`✅ Rate limit removed for account: ${accountData.name} (${accountId}), schedulable restored`
)
logger.success(`✅ Rate limit removed for account: ${accountData.name} (${accountId})`)
return { success: true }
} catch (error) {
@@ -1331,17 +1355,13 @@ class ClaudeAccountService {
}
// 如果账户因为5小时限制被自动停止现在恢复调度
if (
accountData.autoStoppedAt &&
accountData.schedulable === 'false' &&
accountData.stoppedReason === '5小时使用量接近限制自动停止调度'
) {
if (accountData.fiveHourAutoStopped === 'true' && accountData.schedulable === 'false') {
logger.info(
`✅ Auto-resuming scheduling for account ${accountData.name} (${accountId}) - new session window started`
)
accountData.schedulable = 'true'
delete accountData.stoppedReason
delete accountData.autoStoppedAt
delete accountData.fiveHourAutoStopped
delete accountData.fiveHourStoppedAt
// 发送Webhook通知
try {
@@ -1823,8 +1843,16 @@ class ClaudeAccountService {
updatedAccountData.status = 'created'
}
// 恢复可调度状态
// 恢复可调度状态(管理员手动重置时恢复调度是合理的)
updatedAccountData.schedulable = 'true'
// 清除所有自动停止相关的标记
delete updatedAccountData.rateLimitAutoStopped
delete updatedAccountData.fiveHourAutoStopped
delete updatedAccountData.fiveHourStoppedAt
delete updatedAccountData.tempErrorAutoStopped
// 兼容旧的标记
delete updatedAccountData.autoStoppedAt
delete updatedAccountData.stoppedReason
// 清除错误相关字段
delete updatedAccountData.errorMessage
@@ -1850,7 +1878,15 @@ class ClaudeAccountService {
'rateLimitEndAt',
'tempErrorAt',
'sessionWindowStart',
'sessionWindowEnd'
'sessionWindowEnd',
// 新的独立标记
'rateLimitAutoStopped',
'fiveHourAutoStopped',
'fiveHourStoppedAt',
'tempErrorAutoStopped',
// 兼容旧的标记
'autoStoppedAt',
'stoppedReason'
]
await redis.client.hdel(`claude:account:${accountId}`, ...fieldsToDelete)
@@ -1901,13 +1937,22 @@ class ClaudeAccountService {
// 如果临时错误状态超过指定时间,尝试重新激活
if (minutesSinceTempError > TEMP_ERROR_RECOVERY_MINUTES) {
account.status = 'active' // 恢复为 active 状态
account.schedulable = 'true' // 恢复为可调度
// 恢复因临时错误而自动停止的账户
if (account.tempErrorAutoStopped === 'true') {
account.schedulable = 'true' // 恢复为可调度
delete account.tempErrorAutoStopped
}
delete account.errorMessage
delete account.tempErrorAt
await redis.setClaudeAccount(account.id, account)
// 显式从 Redis 中删除这些字段(因为 HSET 不会删除现有字段)
await redis.client.hdel(`claude:account:${account.id}`, 'errorMessage', 'tempErrorAt')
await redis.client.hdel(
`claude:account:${account.id}`,
'errorMessage',
'tempErrorAt',
'tempErrorAutoStopped'
)
// 同时清除500错误计数
await this.clearInternalErrors(account.id)
@@ -1992,6 +2037,8 @@ class ClaudeAccountService {
updatedAccountData.schedulable = 'false' // 设置为不可调度
updatedAccountData.errorMessage = 'Account temporarily disabled due to consecutive 500 errors'
updatedAccountData.tempErrorAt = new Date().toISOString()
// 使用独立的临时错误自动停止标记
updatedAccountData.tempErrorAutoStopped = 'true'
// 保存更新后的账户数据
await redis.setClaudeAccount(accountId, updatedAccountData)
@@ -2010,7 +2057,11 @@ class ClaudeAccountService {
if (minutesSince >= 5) {
// 恢复账户
account.status = 'active'
account.schedulable = 'true'
// 只恢复因临时错误而自动停止的账户
if (account.tempErrorAutoStopped === 'true') {
account.schedulable = 'true'
delete account.tempErrorAutoStopped
}
delete account.errorMessage
delete account.tempErrorAt
@@ -2020,7 +2071,8 @@ class ClaudeAccountService {
await redis.client.hdel(
`claude:account:${accountId}`,
'errorMessage',
'tempErrorAt'
'tempErrorAt',
'tempErrorAutoStopped'
)
// 清除 500 错误计数
@@ -2108,8 +2160,9 @@ class ClaudeAccountService {
`⚠️ Account ${accountData.name} (${accountId}) approaching 5h limit, auto-stopping scheduling`
)
accountData.schedulable = 'false'
accountData.stoppedReason = '5小时使用量接近限制自动停止调度'
accountData.autoStoppedAt = new Date().toISOString()
// 使用独立的5小时限制自动停止标记
accountData.fiveHourAutoStopped = 'true'
accountData.fiveHourStoppedAt = new Date().toISOString()
// 发送Webhook通知
try {