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

@@ -6,6 +6,8 @@ const config = require('../../config/config')
const { authenticateApiKey } = require('../middleware/auth')
const unifiedOpenAIScheduler = require('../services/unifiedOpenAIScheduler')
const openaiAccountService = require('../services/openaiAccountService')
const openaiResponsesAccountService = require('../services/openaiResponsesAccountService')
const openaiResponsesRelayService = require('../services/openaiResponsesRelayService')
const apiKeyService = require('../services/apiKeyService')
const crypto = require('crypto')
const ProxyHelper = require('../utils/proxyHelper')
@@ -34,51 +36,81 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel =
throw new Error('No available OpenAI account found')
}
// 获取账户详情
let account = await openaiAccountService.getAccount(result.accountId)
if (!account || !account.accessToken) {
throw new Error(`OpenAI account ${result.accountId} has no valid accessToken`)
}
// 根据账户类型获取账户详情
let account,
accessToken,
proxy = null
// 检查 token 是否过期并自动刷新(双重保护)
if (openaiAccountService.isTokenExpired(account)) {
if (account.refreshToken) {
logger.info(`🔄 Token expired, auto-refreshing for account ${account.name} (fallback)`)
if (result.accountType === 'openai-responses') {
// 处理 OpenAI-Responses 账户
account = await openaiResponsesAccountService.getAccount(result.accountId)
if (!account || !account.apiKey) {
throw new Error(`OpenAI-Responses account ${result.accountId} has no valid apiKey`)
}
// OpenAI-Responses 账户不需要 accessToken直接返回账户信息
accessToken = null // OpenAI-Responses 使用账户内的 apiKey
// 解析代理配置
if (account.proxy) {
try {
await openaiAccountService.refreshAccountToken(result.accountId)
// 重新获取更新后的账户
account = await openaiAccountService.getAccount(result.accountId)
logger.info(`✅ Token refreshed successfully in route handler`)
} catch (refreshError) {
logger.error(`Failed to refresh token for ${account.name}:`, refreshError)
throw new Error(`Token expired and refresh failed: ${refreshError.message}`)
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
} catch (e) {
logger.warn('Failed to parse proxy configuration:', e)
}
} else {
throw new Error(`Token expired and no refresh token available for account ${account.name}`)
}
}
// 解密 accessTokenaccount.accessToken 是加密的)
const accessToken = openaiAccountService.decrypt(account.accessToken)
if (!accessToken) {
throw new Error('Failed to decrypt OpenAI accessToken')
}
// 解析代理配置
let proxy = null
if (account.proxy) {
try {
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
} catch (e) {
logger.warn('Failed to parse proxy configuration:', e)
logger.info(`Selected OpenAI-Responses account: ${account.name} (${result.accountId})`)
} else {
// 处理普通 OpenAI 账户
account = await openaiAccountService.getAccount(result.accountId)
if (!account || !account.accessToken) {
throw new Error(`OpenAI account ${result.accountId} has no valid accessToken`)
}
// 检查 token 是否过期并自动刷新(双重保护)
if (openaiAccountService.isTokenExpired(account)) {
if (account.refreshToken) {
logger.info(`🔄 Token expired, auto-refreshing for account ${account.name} (fallback)`)
try {
await openaiAccountService.refreshAccountToken(result.accountId)
// 重新获取更新后的账户
account = await openaiAccountService.getAccount(result.accountId)
logger.info(`✅ Token refreshed successfully in route handler`)
} catch (refreshError) {
logger.error(`Failed to refresh token for ${account.name}:`, refreshError)
throw new Error(`Token expired and refresh failed: ${refreshError.message}`)
}
} else {
throw new Error(
`Token expired and no refresh token available for account ${account.name}`
)
}
}
// 解密 accessTokenaccount.accessToken 是加密的)
accessToken = openaiAccountService.decrypt(account.accessToken)
if (!accessToken) {
throw new Error('Failed to decrypt OpenAI accessToken')
}
// 解析代理配置
if (account.proxy) {
try {
proxy = typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
} catch (e) {
logger.warn('Failed to parse proxy configuration:', e)
}
}
logger.info(`Selected OpenAI account: ${account.name} (${result.accountId})`)
}
logger.info(`Selected OpenAI account: ${account.name} (${result.accountId})`)
return {
accessToken,
accountId: result.accountId,
accountName: account.name,
accountType: result.accountType,
proxy,
account
}
@@ -151,9 +183,16 @@ const handleResponses = async (req, res) => {
accessToken,
accountId,
accountName: _accountName,
accountType,
proxy,
account
} = await getOpenAIAuthToken(apiKeyData, sessionId, requestedModel)
// 如果是 OpenAI-Responses 账户,使用专门的中继服务处理
if (accountType === 'openai-responses') {
logger.info(`🔀 Using OpenAI-Responses relay service for account: ${account.name}`)
return await openaiResponsesRelayService.handleRequest(req, res, account, apiKeyData)
}
// 基于白名单构造上游所需的请求头,确保键为小写且值受控
const incoming = req.headers || {}