This commit is contained in:
SunSeekerX
2026-02-09 18:13:45 +08:00
parent 61e772fd95
commit a119cb1744
86 changed files with 3803 additions and 2618 deletions

View File

@@ -86,7 +86,7 @@ class Application {
// 💳 初始化账户余额查询服务Provider 注册)
try {
const accountBalanceService = require('./services/accountBalanceService')
const accountBalanceService = require('./services/account/accountBalanceService')
const { registerAllProviders } = require('./services/balanceProviders')
registerAllProviders(accountBalanceService)
logger.info('✅ 账户余额查询服务已初始化')
@@ -137,7 +137,7 @@ class Application {
// 🕐 初始化Claude账户会话窗口
logger.info('🕐 Initializing Claude account session windows...')
const claudeAccountService = require('./services/claudeAccountService')
const claudeAccountService = require('./services/account/claudeAccountService')
await claudeAccountService.initializeSessionWindows()
// 📊 初始化费用排序索引服务
@@ -639,9 +639,12 @@ class Application {
// 注册各个服务的缓存实例
const services = [
{ name: 'claudeAccount', service: require('./services/claudeAccountService') },
{ name: 'claudeConsole', service: require('./services/claudeConsoleAccountService') },
{ name: 'bedrockAccount', service: require('./services/bedrockAccountService') }
{ name: 'claudeAccount', service: require('./services/account/claudeAccountService') },
{
name: 'claudeConsole',
service: require('./services/account/claudeConsoleAccountService')
},
{ name: 'bedrockAccount', service: require('./services/account/bedrockAccountService') }
]
// 注册已加载的服务缓存
@@ -673,7 +676,7 @@ class Application {
logger.info('🧹 Starting scheduled cleanup...')
const apiKeyService = require('./services/apiKeyService')
const claudeAccountService = require('./services/claudeAccountService')
const claudeAccountService = require('./services/account/claudeAccountService')
const [expiredKeys, errorAccounts] = await Promise.all([
apiKeyService.cleanupExpiredKeys(),

View File

@@ -6,13 +6,13 @@
*/
const logger = require('../utils/logger')
const geminiAccountService = require('../services/geminiAccountService')
const geminiApiAccountService = require('../services/geminiApiAccountService')
const { sendGeminiRequest, getAvailableModels } = require('../services/geminiRelayService')
const { sendAntigravityRequest } = require('../services/antigravityRelayService')
const geminiAccountService = require('../services/account/geminiAccountService')
const geminiApiAccountService = require('../services/account/geminiApiAccountService')
const { sendGeminiRequest, getAvailableModels } = require('../services/relay/geminiRelayService')
const { sendAntigravityRequest } = require('../services/relay/antigravityRelayService')
const crypto = require('crypto')
const sessionHelper = require('../utils/sessionHelper')
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
const unifiedGeminiScheduler = require('../services/scheduler/unifiedGeminiScheduler')
const apiKeyService = require('../services/apiKeyService')
const redis = require('../models/redis')
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
@@ -20,6 +20,52 @@ const { parseSSELine } = require('../utils/sseParser')
const axios = require('axios')
const { getSafeMessage } = require('../utils/errorSanitizer')
const ProxyHelper = require('../utils/proxyHelper')
const upstreamErrorHelper = require('../utils/upstreamErrorHelper')
// 处理 Gemini 上游错误,标记账户为临时不可用
const handleGeminiUpstreamError = async (
errorStatus,
accountId,
accountType,
sessionHash,
headers,
disableAutoProtection = false
) => {
if (!accountId || !errorStatus) {
return
}
const autoProtectionDisabled = disableAutoProtection === true || disableAutoProtection === 'true'
try {
if (errorStatus === 429) {
if (!autoProtectionDisabled) {
const ttl = upstreamErrorHelper.parseRetryAfter(headers)
await upstreamErrorHelper.markTempUnavailable(accountId, accountType || 'gemini', 429, ttl)
// 同时设置 rate-limit 状态,保持与 /messages handler 一致
await unifiedGeminiScheduler
.markAccountRateLimited(accountId, accountType || 'gemini', sessionHash)
.catch((e) => logger.warn('Failed to mark account as rate limited:', e))
}
if (sessionHash) {
await unifiedGeminiScheduler._deleteSessionMapping(sessionHash)
}
return
}
if (errorStatus >= 500 || errorStatus === 401 || errorStatus === 403) {
if (!autoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(
accountId,
accountType || 'gemini',
errorStatus
)
}
}
if (sessionHash) {
await unifiedGeminiScheduler._deleteSessionMapping(sessionHash)
}
} catch (e) {
logger.warn('[UpstreamError] Failed to handle Gemini upstream error:', e)
}
}
// ============================================================================
// 工具函数
@@ -44,28 +90,65 @@ function buildGeminiApiUrl(baseUrl, model, action, apiKey, options = {}) {
// 移除末尾的斜杠(如果有)
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
// 检查是否为新格式(以 /models 结尾
const isNewFormat = normalizedBaseUrl.endsWith('/models')
// 模式 3: URL 模板(包含 {model} 占位符
const isTemplate = normalizedBaseUrl.includes('{model}')
// 模式 2: 以 /models 结尾
const isModelsFormat = normalizedBaseUrl.endsWith('/models')
// 模板校验: 有 {model} 但没有 {action} 且 {model} 后面没有 : 开头的固定 action
if (isTemplate && !listModels && !normalizedBaseUrl.includes('{action}')) {
const afterModel = normalizedBaseUrl.split('{model}')[1] || ''
if (!afterModel.startsWith(':')) {
const err = new Error(
`Gemini baseUrl 模板配置错误: 包含 {model} 但缺少 :{action} 或固定 action。` +
`当前: ${baseUrl},示例: https://proxy.com/v1beta/models/{model}:{action}`
)
err.statusCode = 400
throw err
}
}
let url
if (listModels) {
// 获取模型列表
if (isNewFormat) {
// 新格式: baseUrl 已包含 /v1beta/models直接添加查询参数
if (isTemplate) {
// 模板模式: 分离 path 和 query分别剔除含 {model}/{action} 的部分
const [pathPart, queryPart] = normalizedBaseUrl.split('?')
let cleanPath = pathPart.split('{model}')[0].replace(/\/+$/, '')
let cleanQuery = ''
if (queryPart) {
cleanQuery = queryPart
.split('&')
.filter((p) => !p.includes('{model}') && !p.includes('{action}'))
.join('&')
}
// 如果 {model} 在 query 里path 未变path 可能缺少 /models
if (cleanPath === pathPart.replace(/\/+$/, '') && !cleanPath.endsWith('/models')) {
const logger = require('../utils/logger')
logger.warn(
'Gemini 模板 {model} 在 query 中listModels 路径可能不正确,自动追加 /v1beta/models',
{ baseUrl }
)
cleanPath += '/v1beta/models'
}
const base = cleanQuery ? `${cleanPath}?${cleanQuery}` : cleanPath
const separator = base.includes('?') ? '&' : '?'
url = `${base}${separator}key=${apiKey}`
} else if (isModelsFormat) {
url = `${normalizedBaseUrl}?key=${apiKey}`
} else {
// 旧格式: 需要拼接 /v1beta/models
url = `${normalizedBaseUrl}/v1beta/models?key=${apiKey}`
}
} else {
// 模型操作 (generateContent, streamGenerateContent, countTokens)
const streamParam = stream ? '&alt=sse' : ''
if (isNewFormat) {
// 新格式: baseUrl 已包含 /v1beta/models直接拼接 /{model}:action
if (isTemplate) {
// 模板模式: 直接替换占位符({action} 可选,用户可硬编码 action
url = normalizedBaseUrl.replace('{model}', model).replace('{action}', action)
const separator = url.includes('?') ? '&' : '?'
url += `${separator}key=${apiKey}${streamParam}`
} else if (isModelsFormat) {
url = `${normalizedBaseUrl}/${model}:${action}?key=${apiKey}${streamParam}`
} else {
// 旧格式: 需要拼接 /v1beta/models/{model}:action
url = `${normalizedBaseUrl}/v1beta/models/${model}:${action}?key=${apiKey}${streamParam}`
}
}
@@ -664,6 +747,16 @@ async function handleMessages(req, res) {
}
}
// 处理其他上游错误5xx/401/403
await handleGeminiUpstreamError(
errorStatus,
accountId,
accountType,
sessionHash,
error.response?.headers,
account?.disableAutoProtection
)
// 返回错误响应
const status = errorStatus || 500
const errorResponse = {
@@ -1429,6 +1522,10 @@ async function handleCountTokens(req, res) {
* 处理 generateContent 请求v1internal 格式)
*/
async function handleGenerateContent(req, res) {
let accountId = null
let accountType = null
let sessionHash = null
try {
if (!ensureGeminiPermission(req, res)) {
return undefined
@@ -1437,7 +1534,7 @@ async function handleGenerateContent(req, res) {
const { project, user_prompt_id, request: requestData } = req.body
// 从路径参数或请求体中获取模型名
const model = req.body.model || req.params.modelName || 'gemini-2.5-flash'
const sessionHash = sessionHelper.generateSessionHash(req.body)
sessionHash = sessionHelper.generateSessionHash(req.body)
// 处理不同格式的请求
let actualRequestData = requestData
@@ -1478,7 +1575,7 @@ async function handleGenerateContent(req, res) {
sessionHash,
model
)
const { accountId, accountType } = schedulerResult
;({ accountId, accountType } = schedulerResult)
// v1internal 路由只支持 OAuth 账户,不支持 API Key 账户
if (accountType === 'gemini-api') {
@@ -1638,6 +1735,14 @@ async function handleGenerateContent(req, res) {
requestMethod: error.config?.method,
stack: error.stack
})
await handleGeminiUpstreamError(
error.response?.status,
accountId,
accountType,
sessionHash,
error.response?.headers,
account?.disableAutoProtection
)
res.status(500).json({
error: {
message: getSafeMessage(error) || 'Internal server error',
@@ -1653,6 +1758,9 @@ async function handleGenerateContent(req, res) {
*/
async function handleStreamGenerateContent(req, res) {
let abortController = null
let accountId = null
let accountType = null
let sessionHash = null
try {
if (!ensureGeminiPermission(req, res)) {
@@ -1662,7 +1770,7 @@ async function handleStreamGenerateContent(req, res) {
const { project, user_prompt_id, request: requestData } = req.body
// 从路径参数或请求体中获取模型名
const model = req.body.model || req.params.modelName || 'gemini-2.5-flash'
const sessionHash = sessionHelper.generateSessionHash(req.body)
sessionHash = sessionHelper.generateSessionHash(req.body)
// 处理不同格式的请求
let actualRequestData = requestData
@@ -1703,7 +1811,7 @@ async function handleStreamGenerateContent(req, res) {
sessionHash,
model
)
const { accountId, accountType } = schedulerResult
;({ accountId, accountType } = schedulerResult)
// v1internal 路由只支持 OAuth 账户,不支持 API Key 账户
if (accountType === 'gemini-api') {
@@ -1997,6 +2105,14 @@ async function handleStreamGenerateContent(req, res) {
requestMethod: error.config?.method,
stack: error.stack
})
await handleGeminiUpstreamError(
error.response?.status,
accountId,
accountType,
sessionHash,
error.response?.headers,
account?.disableAutoProtection
)
if (!res.headersSent) {
res.status(500).json({
@@ -2025,6 +2141,7 @@ async function handleStandardGenerateContent(req, res) {
let account = null
let sessionHash = null
let accountId = null
let accountType = null
let isApiAccount = false
try {
@@ -2102,8 +2219,7 @@ async function handleStandardGenerateContent(req, res) {
model,
{ allowApiAccounts: true }
)
;({ accountId } = schedulerResult)
const { accountType } = schedulerResult
;({ accountId, accountType } = schedulerResult)
isApiAccount = accountType === 'gemini-api'
const actualAccountId = accountId
@@ -2148,6 +2264,12 @@ async function handleStandardGenerateContent(req, res) {
// Gemini API 账户:直接使用 API Key 请求
const apiUrl = buildGeminiApiUrl(account.baseUrl, model, 'generateContent', account.apiKey)
logger.info('📤 Gemini upstream request', {
targetUrl: apiUrl.replace(/key=[^&]+/, 'key=***'),
model,
accountId: account.id
})
const axiosConfig = {
method: 'POST',
url: apiUrl,
@@ -2282,6 +2404,14 @@ async function handleStandardGenerateContent(req, res) {
responseData: error.response?.data,
stack: error.stack
})
await handleGeminiUpstreamError(
error.response?.status,
accountId,
accountType,
sessionHash,
error.response?.headers,
account?.disableAutoProtection
)
res.status(500).json({
error: {
@@ -2300,6 +2430,7 @@ async function handleStandardStreamGenerateContent(req, res) {
let account = null
let sessionHash = null
let accountId = null
let accountType = null
let isApiAccount = false
try {
@@ -2375,8 +2506,7 @@ async function handleStandardStreamGenerateContent(req, res) {
model,
{ allowApiAccounts: true }
)
;({ accountId } = schedulerResult)
const { accountType } = schedulerResult
;({ accountId, accountType } = schedulerResult)
isApiAccount = accountType === 'gemini-api'
const actualAccountId = accountId
@@ -2446,6 +2576,12 @@ async function handleStandardStreamGenerateContent(req, res) {
}
)
logger.info('📤 Gemini upstream request', {
targetUrl: apiUrl.replace(/key=[^&]+/, 'key=***'),
model,
accountId: actualAccountId
})
const axiosConfig = {
method: 'POST',
url: apiUrl,
@@ -2755,9 +2891,17 @@ async function handleStandardStreamGenerateContent(req, res) {
responseData: normalizedError.parsedBody || normalizedError.rawBody,
stack: error.stack
})
await handleGeminiUpstreamError(
normalizedError.status || error.response?.status,
accountId,
accountType,
sessionHash,
error.response?.headers,
account?.disableAutoProtection
)
if (!res.headersSent) {
const statusCode = normalizedError.status || 500
const statusCode = error.statusCode || normalizedError.status || 500
const responseBody = {
error: {
message: normalizedError.message,

View File

@@ -1451,6 +1451,7 @@ const authenticateAdmin = async (req, res, next) => {
}
const authDuration = Date.now() - startTime
req._authInfo = `${adminSession.username} ${authDuration}ms`
logger.security(`Admin authenticated: ${adminSession.username} in ${authDuration}ms`)
return next()
@@ -1593,6 +1594,7 @@ const authenticateUserOrAdmin = async (req, res, next) => {
req.userType = 'admin'
const authDuration = Date.now() - startTime
req._authInfo = `${adminSession.username} ${authDuration}ms`
logger.security(`Admin authenticated: ${adminSession.username} in ${authDuration}ms`)
return next()
}
@@ -1773,67 +1775,80 @@ const requestLogger = (req, res, next) => {
const userAgent = req.get('User-Agent') || 'unknown'
const referer = req.get('Referer') || 'none'
// 记录请求开始
// 请求开始 → debug 级别(减少正常请求的日志量)
const isDebugRoute = req.originalUrl.includes('event_logging')
if (req.originalUrl !== '/health') {
if (isDebugRoute) {
logger.debug(`▶️ [${requestId}] ${req.method} ${req.originalUrl} | IP: ${clientIP}`)
} else {
logger.info(`▶️ [${requestId}] ${req.method} ${req.originalUrl} | IP: ${clientIP}`)
}
logger.debug(`▶ [${requestId}] ${req.method} ${req.originalUrl}`, {
ip: clientIP,
body: req.body && Object.keys(req.body).length > 0 ? req.body : undefined
})
}
// 拦截 res.json() 捕获响应体
const originalJson = res.json.bind(res)
res.json = (body) => {
res._responseBody = body
return originalJson(body)
}
res.on('finish', () => {
if (req.originalUrl === '/health') {
return
}
const duration = Date.now() - start
const contentLength = res.get('Content-Length') || '0'
const status = res.statusCode
// 构建日志元数据
const logMetadata = {
requestId,
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration,
contentLength,
ip: clientIP,
userAgent,
referer
// 状态 emoji
const emoji = status >= 500 ? '❌' : status >= 400 ? '⚠️ ' : '🟢'
const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info'
// 主消息行
const msg = `${emoji} ${status} ${req.method} ${req.originalUrl} ${duration}ms ${contentLength}B`
// 构建树形 metadata
const meta = { requestId }
// 请求体(非 GET 且有内容时显示)
if (req.method !== 'GET' && req.body && Object.keys(req.body).length > 0) {
meta.req = req.body
}
// 根据状态码选择日志级别
if (res.statusCode >= 500) {
logger.error(
`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`,
logMetadata
)
} else if (res.statusCode >= 400) {
logger.warn(
`◀️ [${requestId}] ${req.method} ${req.originalUrl} | ${res.statusCode} | ${duration}ms | ${contentLength}B`,
logMetadata
)
} else if (req.originalUrl !== '/health') {
if (isDebugRoute) {
logger.debug(
`🟢 ${req.method} ${req.originalUrl} - ${res.statusCode} (${duration}ms)`,
logMetadata
)
} else {
logger.request(req.method, req.originalUrl, res.statusCode, duration, logMetadata)
}
// 查询参数GET 请求且有查询参数时单独显示)
const queryIdx = req.originalUrl.indexOf('?')
if (queryIdx > -1) {
meta.query = req.originalUrl.substring(queryIdx + 1)
}
// API Key相关日志
// 响应体
if (res._responseBody) {
meta.res = res._responseBody
}
// API Key 信息(合并到同一条日志)
if (req.apiKey) {
logger.api(
`📱 [${requestId}] Request from ${req.apiKey.name} (${req.apiKey.id}) | ${duration}ms`
)
meta.key = `${req.apiKey.name} (${req.apiKey.id})`
}
// 认证信息
if (req._authInfo) {
meta.auth = req._authInfo
}
// 完整信息写入文件
meta.ip = clientIP
meta.ua = userAgent
meta.referer = referer
if (isDebugRoute) {
logger.debug(msg, meta)
} else {
logger[level](msg, meta)
}
// 慢请求警告
if (duration > 5000) {
logger.warn(
`🐌 [${requestId}] Slow request detected: ${duration}ms for ${req.method} ${req.originalUrl}`
)
logger.warn(`🐌 Slow request: ${duration}ms ${req.method} ${req.originalUrl}`)
}
})

View File

@@ -1,7 +1,7 @@
const express = require('express')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
const accountBalanceService = require('../../services/accountBalanceService')
const accountBalanceService = require('../../services/account/accountBalanceService')
const balanceScriptService = require('../../services/balanceScriptService')
const { isBalanceScriptEnabled } = require('../../utils/featureFlags')

View File

@@ -1,10 +1,10 @@
const express = require('express')
const accountGroupService = require('../../services/accountGroupService')
const claudeAccountService = require('../../services/claudeAccountService')
const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService')
const geminiAccountService = require('../../services/geminiAccountService')
const openaiAccountService = require('../../services/openaiAccountService')
const droidAccountService = require('../../services/droidAccountService')
const claudeAccountService = require('../../services/account/claudeAccountService')
const claudeConsoleAccountService = require('../../services/account/claudeConsoleAccountService')
const geminiAccountService = require('../../services/account/geminiAccountService')
const openaiAccountService = require('../../services/account/openaiAccountService')
const droidAccountService = require('../../services/account/droidAccountService')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')

View File

@@ -1,5 +1,5 @@
const express = require('express')
const azureOpenaiAccountService = require('../../services/azureOpenaiAccountService')
const azureOpenaiAccountService = require('../../services/account/azureOpenaiAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')
@@ -433,13 +433,16 @@ router.post('/azure-openai-accounts/:accountId/test', authenticateAdmin, async (
}
// 构造测试请求
const { createOpenAITestPayload } = require('../../utils/testPayloadHelper')
const {
createChatCompletionsTestPayload,
extractErrorMessage
} = require('../../utils/testPayloadHelper')
const { getProxyAgent } = require('../../utils/proxyHelper')
const deploymentName = account.deploymentName || 'gpt-4o-mini'
const apiVersion = account.apiVersion || '2024-02-15-preview'
const apiUrl = `${account.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${apiVersion}`
const payload = createOpenAITestPayload(deploymentName)
const payload = createChatCompletionsTestPayload(deploymentName)
const requestConfig = {
headers: {
@@ -488,10 +491,23 @@ router.post('/azure-openai-accounts/:accountId/test', authenticateAdmin, async (
return res.status(500).json({
success: false,
error: 'Test failed',
message: error.response?.data?.error?.message || error.message,
message: extractErrorMessage(error.response?.data, error.message),
latency
})
}
})
// 重置 Azure OpenAI 账户状态
router.post('/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await azureOpenaiAccountService.resetAccountStatus(accountId)
logger.success(`Admin reset status for Azure OpenAI account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Azure OpenAI account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
module.exports = router

View File

@@ -5,7 +5,7 @@
const express = require('express')
const router = express.Router()
const bedrockAccountService = require('../../services/bedrockAccountService')
const bedrockAccountService = require('../../services/account/bedrockAccountService')
const apiKeyService = require('../../services/apiKeyService')
const accountGroupService = require('../../services/accountGroupService')
const redis = require('../../models/redis')
@@ -363,4 +363,17 @@ router.post('/:accountId/test', authenticateAdmin, async (req, res) => {
}
})
// 重置 Bedrock 账户状态
router.post('/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await bedrockAccountService.resetAccountStatus(accountId)
logger.success(`Admin reset status for Bedrock account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Bedrock account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
module.exports = router

View File

@@ -1,5 +1,5 @@
const express = require('express')
const ccrAccountService = require('../../services/ccrAccountService')
const ccrAccountService = require('../../services/account/ccrAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')
@@ -7,6 +7,7 @@ const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
const webhookNotifier = require('../../utils/webhookNotifier')
const { formatAccountExpiry, mapExpiryField } = require('./utils')
const { extractErrorMessage } = require('../../utils/testPayloadHelper')
const router = express.Router()
@@ -492,7 +493,7 @@ router.post('/:accountId/test', authenticateAdmin, async (req, res) => {
return res.status(500).json({
success: false,
error: 'Test failed',
message: error.response?.data?.error?.message || error.message,
message: extractErrorMessage(error.response?.data, error.message),
latency
})
}

View File

@@ -6,8 +6,8 @@
const express = require('express')
const router = express.Router()
const claudeAccountService = require('../../services/claudeAccountService')
const claudeRelayService = require('../../services/claudeRelayService')
const claudeAccountService = require('../../services/account/claudeAccountService')
const claudeRelayService = require('../../services/relay/claudeRelayService')
const accountGroupService = require('../../services/accountGroupService')
const accountTestSchedulerService = require('../../services/accountTestSchedulerService')
const apiKeyService = require('../../services/apiKeyService')

View File

@@ -6,8 +6,8 @@
const express = require('express')
const router = express.Router()
const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService')
const claudeConsoleRelayService = require('../../services/claudeConsoleRelayService')
const claudeConsoleAccountService = require('../../services/account/claudeConsoleAccountService')
const claudeConsoleRelayService = require('../../services/relay/claudeConsoleRelayService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')

View File

@@ -1,16 +1,17 @@
const express = require('express')
const apiKeyService = require('../../services/apiKeyService')
const claudeAccountService = require('../../services/claudeAccountService')
const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService')
const bedrockAccountService = require('../../services/bedrockAccountService')
const ccrAccountService = require('../../services/ccrAccountService')
const geminiAccountService = require('../../services/geminiAccountService')
const droidAccountService = require('../../services/droidAccountService')
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
const claudeAccountService = require('../../services/account/claudeAccountService')
const claudeConsoleAccountService = require('../../services/account/claudeConsoleAccountService')
const bedrockAccountService = require('../../services/account/bedrockAccountService')
const ccrAccountService = require('../../services/account/ccrAccountService')
const geminiAccountService = require('../../services/account/geminiAccountService')
const droidAccountService = require('../../services/account/droidAccountService')
const openaiResponsesAccountService = require('../../services/account/openaiResponsesAccountService')
const redis = require('../../models/redis')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
const CostCalculator = require('../../utils/costCalculator')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const config = require('../../../config/config')
const router = express.Router()
@@ -352,6 +353,17 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
}
})
// 获取所有临时不可用账户状态
router.get('/temp-unavailable', authenticateAdmin, async (req, res) => {
try {
const statuses = await upstreamErrorHelper.getAllTempUnavailable()
return res.json({ success: true, data: statuses })
} catch (error) {
logger.error('❌ Failed to get temp unavailable statuses:', error)
return res.status(500).json({ error: 'Failed to get temp unavailable statuses' })
}
})
// 获取使用统计
router.get('/usage-stats', authenticateAdmin, async (req, res) => {
try {

View File

@@ -1,6 +1,6 @@
const express = require('express')
const crypto = require('crypto')
const droidAccountService = require('../../services/droidAccountService')
const droidAccountService = require('../../services/account/droidAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')
@@ -13,6 +13,7 @@ const {
} = require('../../utils/workosOAuthHelper')
const webhookNotifier = require('../../utils/webhookNotifier')
const { formatAccountExpiry, mapExpiryField } = require('./utils')
const { extractErrorMessage } = require('../../utils/testPayloadHelper')
const router = express.Router()
@@ -683,10 +684,23 @@ router.post('/droid-accounts/:accountId/test', authenticateAdmin, async (req, re
return res.status(500).json({
success: false,
error: 'Test failed',
message: error.response?.data?.error?.message || error.message,
message: extractErrorMessage(error.response?.data, error.message),
latency
})
}
})
// 重置 Droid 账户状态
router.post('/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await droidAccountService.resetAccountStatus(accountId)
logger.success(`Admin reset status for Droid account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Droid account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
module.exports = router

View File

@@ -1,5 +1,5 @@
const express = require('express')
const geminiAccountService = require('../../services/geminiAccountService')
const geminiAccountService = require('../../services/account/geminiAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')
@@ -532,7 +532,10 @@ router.post('/:accountId/test', authenticateAdmin, async (req, res) => {
// 构造测试请求
const axios = require('axios')
const { createGeminiTestPayload } = require('../../utils/testPayloadHelper')
const {
createGeminiTestPayload,
extractErrorMessage
} = require('../../utils/testPayloadHelper')
const { getProxyAgent } = require('../../utils/proxyHelper')
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`
@@ -585,7 +588,7 @@ router.post('/:accountId/test', authenticateAdmin, async (req, res) => {
return res.status(500).json({
success: false,
error: 'Test failed',
message: error.response?.data?.error?.message || error.message,
message: extractErrorMessage(error.response?.data, error.message),
latency
})
}

View File

@@ -1,5 +1,5 @@
const express = require('express')
const geminiApiAccountService = require('../../services/geminiApiAccountService')
const geminiApiAccountService = require('../../services/account/geminiApiAccountService')
const apiKeyService = require('../../services/apiKeyService')
const accountGroupService = require('../../services/accountGroupService')
const redis = require('../../models/redis')

View File

@@ -6,7 +6,7 @@
const express = require('express')
const crypto = require('crypto')
const axios = require('axios')
const openaiAccountService = require('../../services/openaiAccountService')
const openaiAccountService = require('../../services/account/openaiAccountService')
const accountGroupService = require('../../services/accountGroupService')
const apiKeyService = require('../../services/apiKeyService')
const redis = require('../../models/redis')

View File

@@ -4,7 +4,8 @@
*/
const express = require('express')
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
const axios = require('axios')
const openaiResponsesAccountService = require('../../services/account/openaiResponsesAccountService')
const apiKeyService = require('../../services/apiKeyService')
const accountGroupService = require('../../services/accountGroupService')
const redis = require('../../models/redis')
@@ -12,6 +13,8 @@ const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
const webhookNotifier = require('../../utils/webhookNotifier')
const { formatAccountExpiry, mapExpiryField } = require('./utils')
const { createOpenAITestPayload, extractErrorMessage } = require('../../utils/testPayloadHelper')
const { getProxyAgent } = require('../../utils/proxyHelper')
const router = express.Router()
@@ -459,31 +462,25 @@ router.post('/openai-responses-accounts/:accountId/test', authenticateAdmin, asy
const startTime = Date.now()
try {
// 获取账户信息
// 获取账户信息apiKey 已自动解密)
const account = await openaiResponsesAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
// 获取解密后的 API Key
const apiKey = await openaiResponsesAccountService.getDecryptedApiKey(accountId)
if (!apiKey) {
if (!account.apiKey) {
return res.status(401).json({ error: 'API Key not found or decryption failed' })
}
// 构造测试请求
const axios = require('axios')
const { createOpenAITestPayload } = require('../../utils/testPayloadHelper')
const { getProxyAgent } = require('../../utils/proxyHelper')
const baseUrl = account.baseUrl || 'https://api.openai.com'
const apiUrl = `${baseUrl}/v1/chat/completions`
const payload = createOpenAITestPayload(model)
const baseUrl = account.baseApi || 'https://api.openai.com'
const apiUrl = `${baseUrl}/responses`
const payload = createOpenAITestPayload(model, { stream: false })
const requestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
Authorization: `Bearer ${account.apiKey}`
},
timeout: 30000
}
@@ -500,10 +497,19 @@ router.post('/openai-responses-accounts/:accountId/test', authenticateAdmin, asy
const response = await axios.post(apiUrl, payload, requestConfig)
const latency = Date.now() - startTime
// 提取响应文本
// 提取响应文本Responses API 格式)
let responseText = ''
if (response.data?.choices?.[0]?.message?.content) {
responseText = response.data.choices[0].message.content
const output = response.data?.output
if (Array.isArray(output)) {
for (const item of output) {
if (item.type === 'message' && Array.isArray(item.content)) {
for (const block of item.content) {
if (block.type === 'output_text' && block.text) {
responseText += block.text
}
}
}
}
}
logger.success(
@@ -527,7 +533,7 @@ router.post('/openai-responses-accounts/:accountId/test', authenticateAdmin, asy
return res.status(500).json({
success: false,
error: 'Test failed',
message: error.response?.data?.error?.message || error.message,
message: extractErrorMessage(error.response?.data, error.message),
latency
})
}

View File

@@ -8,10 +8,10 @@ const router = express.Router()
const { authenticateAdmin } = require('../../middleware/auth')
const redis = require('../../models/redis')
const claudeAccountService = require('../../services/claudeAccountService')
const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService')
const openaiAccountService = require('../../services/openaiAccountService')
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
const claudeAccountService = require('../../services/account/claudeAccountService')
const claudeConsoleAccountService = require('../../services/account/claudeConsoleAccountService')
const openaiAccountService = require('../../services/account/openaiAccountService')
const openaiResponsesAccountService = require('../../services/account/openaiResponsesAccountService')
const logger = require('../../utils/logger')
function toBool(value, defaultValue = false) {

View File

@@ -3,7 +3,7 @@ const fs = require('fs')
const path = require('path')
const axios = require('axios')
const claudeCodeHeadersService = require('../../services/claudeCodeHeadersService')
const claudeAccountService = require('../../services/claudeAccountService')
const claudeAccountService = require('../../services/account/claudeAccountService')
const redis = require('../../models/redis')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')
@@ -408,4 +408,44 @@ router.post('/claude-code-version/clear', authenticateAdmin, async (req, res) =>
}
})
// ==================== 模型价格管理 ====================
const pricingService = require('../../services/pricingService')
// 获取所有模型价格数据
router.get('/models/pricing', authenticateAdmin, async (req, res) => {
try {
const data = pricingService.pricingData
res.json({
success: true,
data: data || {}
})
} catch (error) {
logger.error('Failed to get model pricing:', error)
res.status(500).json({ error: 'Failed to get model pricing', message: error.message })
}
})
// 获取价格服务状态
router.get('/models/pricing/status', authenticateAdmin, async (req, res) => {
try {
const status = pricingService.getStatus()
res.json({ success: true, data: status })
} catch (error) {
logger.error('Failed to get pricing status:', error)
res.status(500).json({ error: 'Failed to get pricing status', message: error.message })
}
})
// 强制刷新价格数据
router.post('/models/pricing/refresh', authenticateAdmin, async (req, res) => {
try {
const result = await pricingService.forceUpdate()
res.json({ success: result.success, message: result.message })
} catch (error) {
logger.error('Failed to refresh pricing:', error)
res.status(500).json({ error: 'Failed to refresh pricing', message: error.message })
}
})
module.exports = router

View File

@@ -1,14 +1,14 @@
const express = require('express')
const apiKeyService = require('../../services/apiKeyService')
const ccrAccountService = require('../../services/ccrAccountService')
const claudeAccountService = require('../../services/claudeAccountService')
const claudeConsoleAccountService = require('../../services/claudeConsoleAccountService')
const geminiAccountService = require('../../services/geminiAccountService')
const geminiApiAccountService = require('../../services/geminiApiAccountService')
const openaiAccountService = require('../../services/openaiAccountService')
const openaiResponsesAccountService = require('../../services/openaiResponsesAccountService')
const droidAccountService = require('../../services/droidAccountService')
const bedrockAccountService = require('../../services/bedrockAccountService')
const ccrAccountService = require('../../services/account/ccrAccountService')
const claudeAccountService = require('../../services/account/claudeAccountService')
const claudeConsoleAccountService = require('../../services/account/claudeConsoleAccountService')
const geminiAccountService = require('../../services/account/geminiAccountService')
const geminiApiAccountService = require('../../services/account/geminiApiAccountService')
const openaiAccountService = require('../../services/account/openaiAccountService')
const openaiResponsesAccountService = require('../../services/account/openaiResponsesAccountService')
const droidAccountService = require('../../services/account/droidAccountService')
const bedrockAccountService = require('../../services/account/bedrockAccountService')
const redis = require('../../models/redis')
const { authenticateAdmin } = require('../../middleware/auth')
const logger = require('../../utils/logger')

View File

@@ -1,10 +1,10 @@
const express = require('express')
const claudeRelayService = require('../services/claudeRelayService')
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService')
const bedrockRelayService = require('../services/bedrockRelayService')
const ccrRelayService = require('../services/ccrRelayService')
const bedrockAccountService = require('../services/bedrockAccountService')
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
const claudeRelayService = require('../services/relay/claudeRelayService')
const claudeConsoleRelayService = require('../services/relay/claudeConsoleRelayService')
const bedrockRelayService = require('../services/relay/bedrockRelayService')
const ccrRelayService = require('../services/relay/ccrRelayService')
const bedrockAccountService = require('../services/account/bedrockAccountService')
const unifiedClaudeScheduler = require('../services/scheduler/unifiedClaudeScheduler')
const apiKeyService = require('../services/apiKeyService')
const { authenticateApiKey } = require('../middleware/auth')
const logger = require('../utils/logger')
@@ -12,8 +12,8 @@ const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelH
const sessionHelper = require('../utils/sessionHelper')
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
const claudeRelayConfigService = require('../services/claudeRelayConfigService')
const claudeAccountService = require('../services/claudeAccountService')
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService')
const claudeAccountService = require('../services/account/claudeAccountService')
const claudeConsoleAccountService = require('../services/account/claudeConsoleAccountService')
const {
isWarmupRequest,
buildMockWarmupResponse,
@@ -1289,8 +1289,8 @@ router.get('/v1/models', authenticateApiKey, async (req, res) => {
})
}
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
const geminiAccountService = require('../services/geminiAccountService')
const unifiedGeminiScheduler = require('../services/scheduler/unifiedGeminiScheduler')
const geminiAccountService = require('../services/account/geminiAccountService')
let accountSelection
try {

View File

@@ -3,10 +3,14 @@ const redis = require('../models/redis')
const logger = require('../utils/logger')
const apiKeyService = require('../services/apiKeyService')
const CostCalculator = require('../utils/costCalculator')
const claudeAccountService = require('../services/claudeAccountService')
const openaiAccountService = require('../services/openaiAccountService')
const claudeAccountService = require('../services/account/claudeAccountService')
const openaiAccountService = require('../services/account/openaiAccountService')
const serviceRatesService = require('../services/serviceRatesService')
const { createClaudeTestPayload } = require('../utils/testPayloadHelper')
const {
createClaudeTestPayload,
extractErrorMessage,
sanitizeErrorMsg
} = require('../utils/testPayloadHelper')
const modelsConfig = require('../../config/models')
const { getSafeMessage } = require('../utils/errorSanitizer')
@@ -25,7 +29,7 @@ router.get('/models', (req, res) => {
})
}
// 返回所有模型(按服务分组)
// 返回所有模型(按服务分组 + 平台维度
res.json({
success: true,
data: {
@@ -33,7 +37,8 @@ router.get('/models', (req, res) => {
gemini: modelsConfig.GEMINI_MODELS,
openai: modelsConfig.OPENAI_MODELS,
other: modelsConfig.OTHER_MODELS,
all: modelsConfig.getAllModels()
all: modelsConfig.getAllModels(),
platforms: modelsConfig.PLATFORM_TEST_MODELS
}
})
})
@@ -920,7 +925,8 @@ router.post('/api-key/test', async (req, res) => {
responseStream: res,
payload: createClaudeTestPayload(model, { stream: true, prompt, maxTokens }),
timeout: 60000,
extraHeaders: { 'x-api-key': apiKey }
extraHeaders: { 'x-api-key': apiKey },
sanitize: true
})
} catch (error) {
logger.error('❌ API Key test failed:', error)
@@ -1015,14 +1021,14 @@ router.post('/api-key/test-gemini', async (req, res) => {
let errorMsg = `API Error: ${response.status}`
try {
const json = JSON.parse(errorData)
errorMsg = json.message || json.error?.message || json.error || errorMsg
errorMsg = extractErrorMessage(json, errorMsg)
} catch {
if (errorData.length < 200) {
errorMsg = errorData || errorMsg
}
}
res.write(
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: errorMsg })}\n\n`
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: sanitizeErrorMsg(errorMsg) })}\n\n`
)
res.end()
})
@@ -1168,14 +1174,14 @@ router.post('/api-key/test-openai', async (req, res) => {
let errorMsg = `API Error: ${response.status}`
try {
const json = JSON.parse(errorData)
errorMsg = json.message || json.error?.message || json.error || errorMsg
errorMsg = extractErrorMessage(json, errorMsg)
} catch {
if (errorData.length < 200) {
errorMsg = errorData || errorMsg
}
}
res.write(
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: errorMsg })}\n\n`
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: sanitizeErrorMsg(errorMsg) })}\n\n`
)
res.end()
})

View File

@@ -2,10 +2,11 @@ const express = require('express')
const router = express.Router()
const logger = require('../utils/logger')
const { authenticateApiKey } = require('../middleware/auth')
const azureOpenaiAccountService = require('../services/azureOpenaiAccountService')
const azureOpenaiRelayService = require('../services/azureOpenaiRelayService')
const azureOpenaiAccountService = require('../services/account/azureOpenaiAccountService')
const azureOpenaiRelayService = require('../services/relay/azureOpenaiRelayService')
const apiKeyService = require('../services/apiKeyService')
const crypto = require('crypto')
const upstreamErrorHelper = require('../utils/upstreamErrorHelper')
// 支持的模型列表 - 基于真实的 Azure OpenAI 模型
const ALLOWED_MODELS = {
@@ -163,6 +164,16 @@ router.post('/chat/completions', authenticateApiKey, async (req, res) => {
let account = null
if (req.apiKey?.azureOpenaiAccountId) {
account = await azureOpenaiAccountService.getAccount(req.apiKey.azureOpenaiAccountId)
if (account) {
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
'azure-openai'
)
if (isTempUnavailable) {
logger.warn(`⏱️ Bound Azure OpenAI account temporarily unavailable, falling back to pool`)
account = null
}
}
if (!account) {
logger.warn(`Bound Azure OpenAI account not found: ${req.apiKey.azureOpenaiAccountId}`)
}
@@ -182,6 +193,24 @@ router.post('/chat/completions', authenticateApiKey, async (req, res) => {
endpoint: 'chat/completions'
})
// 检查上游响应状态码(仅对认证/限流/服务端错误暂停,不对 400/404 等客户端错误暂停)
const azureAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
const shouldPause =
account?.id &&
!azureAutoProtectionDisabled &&
(response.status === 401 ||
response.status === 403 ||
response.status === 429 ||
response.status >= 500)
if (shouldPause) {
const customTtl =
response.status === 429 ? upstreamErrorHelper.parseRetryAfter(response.headers) : null
await upstreamErrorHelper
.markTempUnavailable(account.id, 'azure-openai', response.status, customTtl)
.catch(() => {})
}
// 处理流式响应
if (req.body.stream) {
await azureOpenaiRelayService.handleStreamResponse(response, res, {
@@ -256,6 +285,16 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
let account = null
if (req.apiKey?.azureOpenaiAccountId) {
account = await azureOpenaiAccountService.getAccount(req.apiKey.azureOpenaiAccountId)
if (account) {
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
'azure-openai'
)
if (isTempUnavailable) {
logger.warn(`⏱️ Bound Azure OpenAI account temporarily unavailable, falling back to pool`)
account = null
}
}
if (!account) {
logger.warn(`Bound Azure OpenAI account not found: ${req.apiKey.azureOpenaiAccountId}`)
}
@@ -275,6 +314,24 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
endpoint: 'responses'
})
// 检查上游响应状态码(仅对认证/限流/服务端错误暂停,不对 400/404 等客户端错误暂停)
const azureAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
const shouldPause =
account?.id &&
!azureAutoProtectionDisabled &&
(response.status === 401 ||
response.status === 403 ||
response.status === 429 ||
response.status >= 500)
if (shouldPause) {
const customTtl =
response.status === 429 ? upstreamErrorHelper.parseRetryAfter(response.headers) : null
await upstreamErrorHelper
.markTempUnavailable(account.id, 'azure-openai', response.status, customTtl)
.catch(() => {})
}
// 处理流式响应
if (req.body.stream) {
await azureOpenaiRelayService.handleStreamResponse(response, res, {
@@ -348,6 +405,16 @@ router.post('/embeddings', authenticateApiKey, async (req, res) => {
let account = null
if (req.apiKey?.azureOpenaiAccountId) {
account = await azureOpenaiAccountService.getAccount(req.apiKey.azureOpenaiAccountId)
if (account) {
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
'azure-openai'
)
if (isTempUnavailable) {
logger.warn(`⏱️ Bound Azure OpenAI account temporarily unavailable, falling back to pool`)
account = null
}
}
if (!account) {
logger.warn(`Bound Azure OpenAI account not found: ${req.apiKey.azureOpenaiAccountId}`)
}
@@ -367,6 +434,24 @@ router.post('/embeddings', authenticateApiKey, async (req, res) => {
endpoint: 'embeddings'
})
// 检查上游响应状态码(仅对认证/限流/服务端错误暂停,不对 400/404 等客户端错误暂停)
const azureAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
const shouldPause =
account?.id &&
!azureAutoProtectionDisabled &&
(response.status === 401 ||
response.status === 403 ||
response.status === 429 ||
response.status >= 500)
if (shouldPause) {
const customTtl =
response.status === 429 ? upstreamErrorHelper.parseRetryAfter(response.headers) : null
await upstreamErrorHelper
.markTempUnavailable(account.id, 'azure-openai', response.status, customTtl)
.catch(() => {})
}
// 处理响应
const { usageData, actualModel } = azureOpenaiRelayService.handleNonStreamResponse(
response,

View File

@@ -1,7 +1,7 @@
const crypto = require('crypto')
const express = require('express')
const { authenticateApiKey } = require('../middleware/auth')
const droidRelayService = require('../services/droidRelayService')
const droidRelayService = require('../services/relay/droidRelayService')
const sessionHelper = require('../utils/sessionHelper')
const logger = require('../utils/logger')
const apiKeyService = require('../services/apiKeyService')

View File

@@ -7,11 +7,11 @@ const express = require('express')
const router = express.Router()
const logger = require('../utils/logger')
const { authenticateApiKey } = require('../middleware/auth')
const claudeRelayService = require('../services/claudeRelayService')
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService')
const claudeRelayService = require('../services/relay/claudeRelayService')
const claudeConsoleRelayService = require('../services/relay/claudeConsoleRelayService')
const openaiToClaude = require('../services/openaiToClaude')
const apiKeyService = require('../services/apiKeyService')
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
const unifiedClaudeScheduler = require('../services/scheduler/unifiedClaudeScheduler')
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
const { getSafeMessage } = require('../utils/errorSanitizer')
const sessionHelper = require('../utils/sessionHelper')

View File

@@ -2,9 +2,9 @@ const express = require('express')
const router = express.Router()
const logger = require('../utils/logger')
const { authenticateApiKey } = require('../middleware/auth')
const geminiAccountService = require('../services/geminiAccountService')
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
const { getAvailableModels } = require('../services/geminiRelayService')
const geminiAccountService = require('../services/account/geminiAccountService')
const unifiedGeminiScheduler = require('../services/scheduler/unifiedGeminiScheduler')
const { getAvailableModels } = require('../services/relay/geminiRelayService')
const crypto = require('crypto')
const apiKeyService = require('../services/apiKeyService')

View File

@@ -4,10 +4,10 @@ const router = express.Router()
const logger = require('../utils/logger')
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 unifiedOpenAIScheduler = require('../services/scheduler/unifiedOpenAIScheduler')
const openaiAccountService = require('../services/account/openaiAccountService')
const openaiResponsesAccountService = require('../services/account/openaiResponsesAccountService')
const openaiResponsesRelayService = require('../services/relay/openaiResponsesRelayService')
const apiKeyService = require('../services/apiKeyService')
const redis = require('../models/redis')
const crypto = require('crypto')

View File

@@ -1,8 +1,8 @@
const redis = require('../models/redis')
const balanceScriptService = require('./balanceScriptService')
const logger = require('../utils/logger')
const CostCalculator = require('../utils/costCalculator')
const { isBalanceScriptEnabled } = require('../utils/featureFlags')
const redis = require('../../models/redis')
const balanceScriptService = require('../balanceScriptService')
const logger = require('../../utils/logger')
const CostCalculator = require('../../utils/costCalculator')
const { isBalanceScriptEnabled } = require('../../utils/featureFlags')
class AccountBalanceService {
constructor(options = {}) {

View File

@@ -1,8 +1,9 @@
const redisClient = require('../models/redis')
const redisClient = require('../../models/redis')
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const config = require('../../config/config')
const logger = require('../utils/logger')
const config = require('../../../config/config')
const logger = require('../../utils/logger')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
// 加密相关常量
const ALGORITHM = 'aes-256-cbc'
@@ -138,6 +139,10 @@ async function createAccount(accountData) {
isActive: accountData.isActive !== false ? 'true' : 'false',
status: 'active',
schedulable: accountData.schedulable !== false ? 'true' : 'false',
disableAutoProtection:
accountData.disableAutoProtection === true || accountData.disableAutoProtection === 'true'
? 'true'
: 'false', // 关闭自动防护
createdAt: now,
updatedAt: now
}
@@ -230,6 +235,14 @@ async function updateAccount(accountId, updates) {
// 直接保存,不做任何调整
}
// 自动防护开关
if (updates.disableAutoProtection !== undefined) {
updates.disableAutoProtection =
updates.disableAutoProtection === true || updates.disableAutoProtection === 'true'
? 'true'
: 'false'
}
// 更新账户类型时处理共享账户集合
const client = redisClient.getClientSafe()
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
@@ -262,7 +275,7 @@ async function updateAccount(accountId, updates) {
// 删除账户
async function deleteAccount(accountId) {
// 首先从所有分组中移除此账户
const accountGroupService = require('./accountGroupService')
const accountGroupService = require('../accountGroupService')
await accountGroupService.removeAccountFromAllGroups(accountId)
const client = redisClient.getClientSafe()
@@ -380,8 +393,14 @@ async function selectAvailableAccount(sessionId = null) {
if (accountId) {
const account = await getAccount(accountId)
if (account && account.isActive === 'true' && account.schedulable === 'true') {
logger.debug(`Reusing Azure OpenAI account ${accountId} for session ${sessionId}`)
return account
const isTempUnavail = await upstreamErrorHelper.isTempUnavailable(accountId, 'azure-openai')
if (!isTempUnavail) {
logger.debug(`Reusing Azure OpenAI account ${accountId} for session ${sessionId}`)
return account
}
logger.warn(
`⏱️ Session-bound Azure OpenAI account ${accountId} temporarily unavailable, falling back to pool`
)
}
}
}
@@ -389,18 +408,30 @@ async function selectAvailableAccount(sessionId = null) {
// 获取所有共享账户
const sharedAccounts = await getSharedAccounts()
// 过滤出可用的账户
const availableAccounts = sharedAccounts.filter((acc) => {
// ✅ 检查账户订阅是否过期
// 过滤出可用的账户(异步过滤,包含临时不可用检查)
const availableAccounts = []
for (const acc of sharedAccounts) {
// 检查账户订阅是否过期
if (isSubscriptionExpired(acc)) {
logger.debug(
`⏰ Skipping expired Azure OpenAI account: ${acc.name}, expired at ${acc.subscriptionExpiresAt}`
)
return false
continue
}
return acc.isActive === 'true' && acc.schedulable === 'true'
})
if (acc.isActive !== 'true' || acc.schedulable !== 'true') {
continue
}
// 检查临时不可用状态
const isTempUnavail = await upstreamErrorHelper.isTempUnavailable(acc.id, 'azure-openai')
if (isTempUnavail) {
logger.debug(`⏱️ Skipping temporarily unavailable Azure OpenAI account: ${acc.name}`)
continue
}
availableAccounts.push(acc)
}
if (availableAccounts.length === 0) {
throw new Error('No available Azure OpenAI accounts')
@@ -515,6 +546,69 @@ async function migrateApiKeysForAzureSupport() {
return migratedCount
}
// 🔄 重置Azure OpenAI账户所有异常状态
async function resetAccountStatus(accountId) {
try {
const accountData = await getAccount(accountId)
if (!accountData) {
throw new Error('Account not found')
}
const client = redisClient.getClientSafe()
const accountKey = `azure_openai:account:${accountId}`
const updates = {
status: 'active',
errorMessage: '',
schedulable: 'true',
isActive: 'true'
}
const fieldsToDelete = [
'rateLimitedAt',
'rateLimitStatus',
'unauthorizedAt',
'unauthorizedCount',
'overloadedAt',
'overloadStatus',
'blockedAt',
'quotaStoppedAt'
]
await client.hset(accountKey, updates)
await client.hdel(accountKey, ...fieldsToDelete)
logger.success(`Reset all error status for Azure OpenAI account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'azure-openai').catch(() => {})
// 异步发送 Webhook 通知(忽略错误)
try {
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || accountId,
platform: 'azure-openai',
status: 'recovered',
errorCode: 'STATUS_RESET',
reason: 'Account status manually reset',
timestamp: new Date().toISOString()
})
} catch (webhookError) {
logger.warn(
'Failed to send webhook notification for Azure OpenAI status reset:',
webhookError
)
}
return { success: true, accountId }
} catch (error) {
logger.error(`❌ Failed to reset Azure OpenAI account status: ${accountId}`, error)
throw error
}
}
module.exports = {
createAccount,
getAccount,
@@ -528,6 +622,7 @@ module.exports = {
performHealthChecks,
toggleSchedulable,
migrateApiKeysForAzureSupport,
resetAccountStatus,
encrypt,
decrypt
}

View File

@@ -1,10 +1,11 @@
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const bedrockRelayService = require('./bedrockRelayService')
const LRUCache = require('../utils/lruCache')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const bedrockRelayService = require('../relay/bedrockRelayService')
const LRUCache = require('../../utils/lruCache')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class BedrockAccountService {
constructor() {
@@ -41,7 +42,8 @@ class BedrockAccountService {
accountType = 'shared', // 'dedicated' or 'shared'
priority = 50, // 调度优先级 (1-100数字越小优先级越高)
schedulable = true, // 是否可被调度
credentialType = 'access_key' // 'access_key', 'bearer_token'(默认为 access_key
credentialType = 'access_key', // 'access_key', 'bearer_token'(默认为 access_key
disableAutoProtection = false // 是否关闭自动防护429/401/400/529 不自动禁用)
} = options
const accountId = uuidv4()
@@ -64,7 +66,8 @@ class BedrockAccountService {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
type: 'bedrock' // 标识这是Bedrock账户
type: 'bedrock', // 标识这是Bedrock账户
disableAutoProtection // 关闭自动防护
}
// 加密存储AWS凭证
@@ -343,6 +346,11 @@ class BedrockAccountService {
account.subscriptionExpiresAt = updates.subscriptionExpiresAt
}
// 自动防护开关
if (updates.disableAutoProtection !== undefined) {
account.disableAutoProtection = updates.disableAutoProtection
}
account.updatedAt = new Date().toISOString()
await client.set(`bedrock_account:${accountId}`, JSON.stringify(account))
@@ -776,6 +784,66 @@ class BedrockAccountService {
return { success: false, error: error.message }
}
}
// 🔄 重置Bedrock账户所有异常状态
async resetAccountStatus(accountId) {
try {
const accountData = await this.getAccount(accountId)
if (!accountData) {
throw new Error('Account not found')
}
const client = redis.getClientSafe()
const accountKey = `bedrock:account:${accountId}`
const updates = {
status: 'active',
errorMessage: '',
schedulable: 'true',
isActive: 'true'
}
const fieldsToDelete = [
'rateLimitedAt',
'rateLimitStatus',
'unauthorizedAt',
'unauthorizedCount',
'overloadedAt',
'overloadStatus',
'blockedAt',
'quotaStoppedAt'
]
await client.hset(accountKey, updates)
await client.hdel(accountKey, ...fieldsToDelete)
logger.success(`Reset all error status for Bedrock account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'bedrock').catch(() => {})
// 异步发送 Webhook 通知(忽略错误)
try {
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || accountId,
platform: 'bedrock',
status: 'recovered',
errorCode: 'STATUS_RESET',
reason: 'Account status manually reset',
timestamp: new Date().toISOString()
})
} catch (webhookError) {
logger.warn('Failed to send webhook notification for Bedrock status reset:', webhookError)
}
return { success: true, accountId }
} catch (error) {
logger.error(`❌ Failed to reset Bedrock account status: ${accountId}`, error)
throw error
}
}
}
module.exports = new BedrockAccountService()

View File

@@ -1,8 +1,9 @@
const { v4: uuidv4 } = require('uuid')
const ProxyHelper = require('../utils/proxyHelper')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const { createEncryptor } = require('../utils/commonHelper')
const ProxyHelper = require('../../utils/proxyHelper')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const { createEncryptor } = require('../../utils/commonHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class CcrAccountService {
constructor() {
@@ -39,7 +40,8 @@ class CcrAccountService {
accountType = 'shared', // 'dedicated' or 'shared'
schedulable = true, // 是否可被调度
dailyQuota = 0, // 每日额度限制美元0表示不限制
quotaResetTime = '00:00' // 额度重置时间HH:mm格式
quotaResetTime = '00:00', // 额度重置时间HH:mm格式
disableAutoProtection = false // 是否关闭自动防护429/401/400/529 不自动禁用)
} = options
// 验证必填字段
@@ -86,7 +88,8 @@ class CcrAccountService {
// 使用与统计一致的时区日期,避免边界问题
lastResetDate: redis.getDateStringInTimezone(), // 最后重置日期(按配置时区)
quotaResetTime, // 额度重置时间
quotaStoppedAt: '' // 因额度停用的时间
quotaStoppedAt: '', // 因额度停用的时间
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护
}
const client = redis.getClientSafe()
@@ -175,7 +178,8 @@ class CcrAccountService {
dailyUsage: parseFloat(accountData.dailyUsage || '0'),
lastResetDate: accountData.lastResetDate || '',
quotaResetTime: accountData.quotaResetTime || '00:00',
quotaStoppedAt: accountData.quotaStoppedAt || null
quotaStoppedAt: accountData.quotaStoppedAt || null,
disableAutoProtection: accountData.disableAutoProtection === 'true'
})
}
}
@@ -221,6 +225,7 @@ class CcrAccountService {
}
accountData.isActive = accountData.isActive === 'true'
accountData.schedulable = accountData.schedulable !== 'false' // 默认为true
accountData.disableAutoProtection = accountData.disableAutoProtection === 'true'
if (accountData.proxy) {
accountData.proxy = JSON.parse(accountData.proxy)
@@ -299,6 +304,11 @@ class CcrAccountService {
updatedData.subscriptionExpiresAt = updates.subscriptionExpiresAt
}
// 自动防护开关
if (updates.disableAutoProtection !== undefined) {
updatedData.disableAutoProtection = updates.disableAutoProtection.toString()
}
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updatedData)
// 处理共享账户集合变更
@@ -691,7 +701,7 @@ class CcrAccountService {
// 发送 Webhook 通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,
@@ -858,9 +868,12 @@ class CcrAccountService {
logger.success(`Reset all error status for CCR account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'ccr').catch(() => {})
// 异步发送 Webhook 通知(忽略错误)
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || accountId,

View File

@@ -1,22 +1,23 @@
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const ProxyHelper = require('../utils/proxyHelper')
const ProxyHelper = require('../../utils/proxyHelper')
const axios = require('axios')
const redis = require('../models/redis')
const config = require('../../config/config')
const logger = require('../utils/logger')
const { maskToken } = require('../utils/tokenMask')
const redis = require('../../models/redis')
const config = require('../../../config/config')
const logger = require('../../utils/logger')
const { maskToken } = require('../../utils/tokenMask')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const {
logRefreshStart,
logRefreshSuccess,
logRefreshError,
logTokenUsage,
logRefreshSkipped
} = require('../utils/tokenRefreshLogger')
const tokenRefreshService = require('./tokenRefreshService')
const LRUCache = require('../utils/lruCache')
const { formatDateWithTimezone, getISOStringWithTimezone } = require('../utils/dateHelper')
const { isOpus45OrNewer } = require('../utils/modelHelper')
} = require('../../utils/tokenRefreshLogger')
const tokenRefreshService = require('../tokenRefreshService')
const LRUCache = require('../../utils/lruCache')
const { formatDateWithTimezone, getISOStringWithTimezone } = require('../../utils/dateHelper')
const { isOpus45OrNewer } = require('../../utils/modelHelper')
/**
* Check if account is Pro (not Max)
@@ -401,7 +402,7 @@ class ClaudeAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name,
@@ -787,7 +788,7 @@ class ClaudeAccountService {
// 检查是否手动禁用了账号如果是则发送webhook通知
if (updates.isActive === 'false' && accountData.isActive === 'true') {
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: updatedData.name || 'Unknown Account',
@@ -831,7 +832,7 @@ class ClaudeAccountService {
async deleteAccount(accountId) {
try {
// 首先从所有分组中移除此账户
const accountGroupService = require('./accountGroupService')
const accountGroupService = require('../accountGroupService')
await accountGroupService.removeAccountFromAllGroups(accountId)
const result = await redis.deleteClaudeAccount(accountId)
@@ -1387,7 +1388,7 @@ class ClaudeAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || 'Claude Account',
@@ -1742,7 +1743,7 @@ class ClaudeAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || 'Claude Account',
@@ -2386,7 +2387,7 @@ class ClaudeAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name,
@@ -2500,6 +2501,13 @@ class ClaudeAccountService {
const serverErrorKey = `claude_account:${accountId}:5xx_errors`
await redis.client.del(serverErrorKey)
// 清除过载状态
const overloadKey = `account:overload:${accountId}`
await redis.client.del(overloadKey)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'claude-official').catch(() => {})
logger.info(
`✅ Successfully reset all error states for account ${accountData.name} (${accountId})`
)
@@ -2704,7 +2712,7 @@ class ClaudeAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name,
@@ -2795,7 +2803,7 @@ class ClaudeAccountService {
if (canSendWarning) {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || 'Claude Account',

View File

@@ -1,10 +1,11 @@
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const ProxyHelper = require('../utils/proxyHelper')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const LRUCache = require('../utils/lruCache')
const ProxyHelper = require('../../utils/proxyHelper')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const LRUCache = require('../../utils/lruCache')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class ClaudeConsoleAccountService {
constructor() {
@@ -414,7 +415,7 @@ class ClaudeConsoleAccountService {
// 检查是否手动禁用了账号如果是则发送webhook通知
if (updates.isActive === false && existingAccount.isActive === true) {
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: updatedData.name || existingAccount.name || 'Unknown Account',
@@ -512,8 +513,8 @@ class ClaudeConsoleAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const { getISOStringWithTimezone } = require('../utils/dateHelper')
const webhookNotifier = require('../../utils/webhookNotifier')
const { getISOStringWithTimezone } = require('../../utils/dateHelper')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || 'Claude Console Account',
@@ -726,7 +727,7 @@ class ClaudeConsoleAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || 'Claude Console Account',
@@ -793,7 +794,7 @@ class ClaudeConsoleAccountService {
// 发送Webhook通知包含完整错误详情
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || 'Claude Console Account',
@@ -947,7 +948,7 @@ class ClaudeConsoleAccountService {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || 'Claude Console Account',
@@ -1040,7 +1041,7 @@ class ClaudeConsoleAccountService {
// 发送Webhook通知
if (accountData && Object.keys(accountData).length > 0) {
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || 'Unknown Account',
@@ -1329,7 +1330,7 @@ class ClaudeConsoleAccountService {
// 发送webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || 'Unknown Account',
@@ -1479,9 +1480,12 @@ class ClaudeConsoleAccountService {
logger.success(`Reset all error status for Claude Console account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'claude-console').catch(() => {})
// 发送 Webhook 通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || accountId,

View File

@@ -1,11 +1,12 @@
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const axios = require('axios')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const { maskToken } = require('../utils/tokenMask')
const ProxyHelper = require('../utils/proxyHelper')
const { createEncryptor, isTruthy } = require('../utils/commonHelper')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const { maskToken } = require('../../utils/tokenMask')
const ProxyHelper = require('../../utils/proxyHelper')
const { createEncryptor, isTruthy } = require('../../utils/commonHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
/**
* Droid 账户管理服务
@@ -476,7 +477,8 @@ class DroidAccountService {
authenticationMethod = '',
expiresIn = null,
apiKeys = [],
userAgent = '' // 自定义 User-Agent
userAgent = '', // 自定义 User-Agent
disableAutoProtection = false // 是否关闭自动防护429/401/400/529 不自动禁用)
} = options
const accountId = uuidv4()
@@ -753,7 +755,8 @@ class DroidAccountService {
apiKeys: hasApiKeys ? JSON.stringify(apiKeyEntries) : '',
apiKeyCount: hasApiKeys ? String(apiKeyEntries.length) : '0',
apiKeyStrategy: hasApiKeys ? 'random_sticky' : '',
userAgent: userAgent || '' // 自定义 User-Agent
userAgent: userAgent || '', // 自定义 User-Agent
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护
}
await redis.setDroidAccount(accountId, accountData)
@@ -1494,6 +1497,66 @@ class DroidAccountService {
logger.warn(`⚠️ Failed to update lastUsedAt for Droid account ${accountId}:`, error)
}
}
// 🔄 重置Droid账户所有异常状态
async resetAccountStatus(accountId) {
try {
const accountData = await this.getAccount(accountId)
if (!accountData) {
throw new Error('Account not found')
}
const client = redis.getClientSafe()
const accountKey = `droid:account:${accountId}`
const updates = {
status: 'active',
errorMessage: '',
schedulable: 'true',
isActive: 'true'
}
const fieldsToDelete = [
'rateLimitedAt',
'rateLimitStatus',
'unauthorizedAt',
'unauthorizedCount',
'overloadedAt',
'overloadStatus',
'blockedAt',
'quotaStoppedAt'
]
await client.hset(accountKey, updates)
await client.hdel(accountKey, ...fieldsToDelete)
logger.success(`Reset all error status for Droid account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'droid').catch(() => {})
// 异步发送 Webhook 通知(忽略错误)
try {
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: accountData.name || accountId,
platform: 'droid',
status: 'recovered',
errorCode: 'STATUS_RESET',
reason: 'Account status manually reset',
timestamp: new Date().toISOString()
})
} catch (webhookError) {
logger.warn('Failed to send webhook notification for Droid status reset:', webhookError)
}
return { success: true, accountId }
} catch (error) {
logger.error(`❌ Failed to reset Droid account status: ${accountId}`, error)
throw error
}
}
}
// 导出单例

View File

@@ -1,21 +1,22 @@
const redisClient = require('../models/redis')
const redisClient = require('../../models/redis')
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const https = require('https')
const logger = require('../utils/logger')
const logger = require('../../utils/logger')
const { OAuth2Client } = require('google-auth-library')
const { maskToken } = require('../utils/tokenMask')
const ProxyHelper = require('../utils/proxyHelper')
const { maskToken } = require('../../utils/tokenMask')
const ProxyHelper = require('../../utils/proxyHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const {
logRefreshStart,
logRefreshSuccess,
logRefreshError,
logTokenUsage,
logRefreshSkipped
} = require('../utils/tokenRefreshLogger')
const tokenRefreshService = require('./tokenRefreshService')
const { createEncryptor } = require('../utils/commonHelper')
const antigravityClient = require('./antigravityClient')
} = require('../../utils/tokenRefreshLogger')
const tokenRefreshService = require('../tokenRefreshService')
const { createEncryptor } = require('../../utils/commonHelper')
const antigravityClient = require('../antigravityClient')
// Gemini 账户键前缀
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:'
@@ -134,7 +135,7 @@ async function fetchAvailableModelsAntigravity(
getAntigravityModelAlias,
getAntigravityModelMetadata,
normalizeAntigravityModelInput
} = require('../utils/antigravityModel')
} = require('../../utils/antigravityModel')
const pushModel = (modelId) => {
if (!modelId || seen.has(modelId)) {
@@ -523,6 +524,12 @@ async function createAccount(accountData) {
// 支持的模型列表(可选)
supportedModels: accountData.supportedModels || [], // 空数组表示支持所有模型
// 自动防护开关
disableAutoProtection:
accountData.disableAutoProtection === true || accountData.disableAutoProtection === 'true'
? 'true'
: 'false',
// 时间戳
createdAt: now,
updatedAt: now,
@@ -666,6 +673,14 @@ async function updateAccount(accountId, updates) {
// 直接保存,不做任何调整
}
// 自动防护开关
if (updates.disableAutoProtection !== undefined) {
updates.disableAutoProtection =
updates.disableAutoProtection === true || updates.disableAutoProtection === 'true'
? 'true'
: 'false'
}
// 如果通过 geminiOauth 更新,也要检查是否新增了 refresh token
if (updates.geminiOauth && !oldRefreshToken) {
const oauthData =
@@ -692,7 +707,7 @@ async function updateAccount(accountId, updates) {
// 检查是否手动禁用了账号如果是则发送webhook通知
if (updates.isActive === 'false' && existingAccount.isActive !== 'false') {
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: updates.name || existingAccount.name || 'Unknown Account',
@@ -1076,7 +1091,7 @@ async function refreshAccountToken(accountId) {
// 发送Webhook通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name,
@@ -1843,9 +1858,12 @@ async function resetAccountStatus(accountId) {
await updateAccount(accountId, updates)
logger.info(`✅ Reset all error status for Gemini account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'gemini').catch(() => {})
// 发送 Webhook 通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,

View File

@@ -1,9 +1,10 @@
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const LRUCache = require('../utils/lruCache')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const LRUCache = require('../../utils/lruCache')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class GeminiApiAccountService {
constructor() {
@@ -44,7 +45,8 @@ class GeminiApiAccountService {
accountType = 'shared', // 'dedicated' or 'shared'
schedulable = true, // 是否可被调度
supportedModels = [], // 支持的模型列表
rateLimitDuration = 60 // 限流时间(分钟)
rateLimitDuration = 60, // 限流时间(分钟)
disableAutoProtection = false
} = options
// 验证必填字段
@@ -79,7 +81,11 @@ class GeminiApiAccountService {
// 限流相关
rateLimitedAt: '',
rateLimitStatus: '',
rateLimitDuration: rateLimitDuration.toString()
rateLimitDuration: rateLimitDuration.toString(),
// 自动防护开关
disableAutoProtection:
disableAutoProtection === true || disableAutoProtection === 'true' ? 'true' : 'false'
}
// 保存到 Redis
@@ -154,6 +160,14 @@ class GeminiApiAccountService {
: updates.baseUrl
}
// 处理 disableAutoProtection 布尔值转字符串
if (updates.disableAutoProtection !== undefined) {
updates.disableAutoProtection =
updates.disableAutoProtection === true || updates.disableAutoProtection === 'true'
? 'true'
: 'false'
}
// 更新 Redis
const client = redis.getClientSafe()
const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
@@ -363,7 +377,7 @@ class GeminiApiAccountService {
)
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,
@@ -456,9 +470,12 @@ class GeminiApiAccountService {
await this.updateAccount(accountId, updates)
logger.info(`✅ Reset all error status for Gemini-API account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'gemini-api').catch(() => {})
// 发送 Webhook 通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,

View File

@@ -1,19 +1,20 @@
const redisClient = require('../models/redis')
const redisClient = require('../../models/redis')
const { v4: uuidv4 } = require('uuid')
const axios = require('axios')
const ProxyHelper = require('../utils/proxyHelper')
const config = require('../../config/config')
const logger = require('../utils/logger')
// const { maskToken } = require('../utils/tokenMask')
const ProxyHelper = require('../../utils/proxyHelper')
const config = require('../../../config/config')
const logger = require('../../utils/logger')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
// const { maskToken } = require('../../utils/tokenMask')
const {
logRefreshStart,
logRefreshSuccess,
logRefreshError,
logTokenUsage,
logRefreshSkipped
} = require('../utils/tokenRefreshLogger')
const tokenRefreshService = require('./tokenRefreshService')
const { createEncryptor } = require('../utils/commonHelper')
} = require('../../utils/tokenRefreshLogger')
const tokenRefreshService = require('../tokenRefreshService')
const { createEncryptor } = require('../../utils/commonHelper')
// 使用 commonHelper 的加密器
const encryptor = createEncryptor('openai-account-salt')
@@ -405,7 +406,7 @@ async function refreshAccountToken(accountId) {
// 发送 Webhook 通知(如果启用)
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account?.name || accountName,
@@ -496,6 +497,11 @@ async function createAccount(accountData) {
isActive: accountData.isActive !== false ? 'true' : 'false',
status: 'active',
schedulable: accountData.schedulable !== false ? 'true' : 'false',
// 自动防护开关
disableAutoProtection:
accountData.disableAutoProtection === true || accountData.disableAutoProtection === 'true'
? 'true'
: 'false',
lastRefresh: now,
createdAt: now,
updatedAt: now
@@ -605,6 +611,14 @@ async function updateAccount(accountId, updates) {
// 直接保存,不做任何调整
}
// 处理 disableAutoProtection 布尔值转字符串
if (updates.disableAutoProtection !== undefined) {
updates.disableAutoProtection =
updates.disableAutoProtection === true || updates.disableAutoProtection === 'true'
? 'true'
: 'false'
}
// 更新账户类型时处理共享账户集合
const client = redisClient.getClientSafe()
if (updates.accountType && updates.accountType !== existingAccount.accountType) {
@@ -961,7 +975,7 @@ async function setAccountRateLimited(accountId, isLimited, resetsInSeconds = nul
if (isLimited) {
try {
const account = await getAccount(accountId)
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,
@@ -1005,7 +1019,7 @@ async function markAccountUnauthorized(accountId, reason = 'OpenAI账号认证
)
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,
@@ -1045,9 +1059,12 @@ async function resetAccountStatus(accountId) {
await updateAccount(accountId, updates)
logger.info(`✅ Reset all error status for OpenAI account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'openai').catch(() => {})
// 发送 Webhook 通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,

View File

@@ -1,9 +1,10 @@
const { v4: uuidv4 } = require('uuid')
const crypto = require('crypto')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const LRUCache = require('../utils/lruCache')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const LRUCache = require('../../utils/lruCache')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class OpenAIResponsesAccountService {
constructor() {
@@ -49,7 +50,8 @@ class OpenAIResponsesAccountService {
schedulable = true, // 是否可被调度
dailyQuota = 0, // 每日额度限制美元0表示不限制
quotaResetTime = '00:00', // 额度重置时间HH:mm格式
rateLimitDuration = 60 // 限流时间(分钟)
rateLimitDuration = 60, // 限流时间(分钟)
disableAutoProtection = false // 是否关闭自动防护429/401/400/529 不自动禁用)
} = options
// 验证必填字段
@@ -93,7 +95,8 @@ class OpenAIResponsesAccountService {
dailyUsage: '0',
lastResetDate: redis.getDateStringInTimezone(),
quotaResetTime,
quotaStoppedAt: ''
quotaStoppedAt: '',
disableAutoProtection: disableAutoProtection.toString() // 关闭自动防护
}
// 保存到 Redis
@@ -162,6 +165,11 @@ class OpenAIResponsesAccountService {
// 直接保存,不做任何调整
}
// 自动防护开关
if (updates.disableAutoProtection !== undefined) {
updates.disableAutoProtection = updates.disableAutoProtection.toString()
}
// 更新 Redis
const client = redis.getClientSafe()
const key = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
@@ -310,7 +318,7 @@ class OpenAIResponsesAccountService {
)
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,
@@ -475,9 +483,12 @@ class OpenAIResponsesAccountService {
await this.updateAccount(accountId, updates)
logger.info(`✅ Reset all error status for OpenAI-Responses account ${accountId}`)
// 清除临时不可用状态
await upstreamErrorHelper.clearTempUnavailable(accountId, 'openai-responses').catch(() => {})
// 发送 Webhook 通知
try {
const webhookNotifier = require('../utils/webhookNotifier')
const webhookNotifier = require('../../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || accountId,

View File

@@ -49,26 +49,26 @@ class AccountNameCacheService {
const newGroupCache = new Map()
// 延迟加载服务,避免循环依赖
const claudeAccountService = require('./claudeAccountService')
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
const geminiAccountService = require('./geminiAccountService')
const openaiAccountService = require('./openaiAccountService')
const azureOpenaiAccountService = require('./azureOpenaiAccountService')
const bedrockAccountService = require('./bedrockAccountService')
const droidAccountService = require('./droidAccountService')
const ccrAccountService = require('./ccrAccountService')
const claudeAccountService = require('./account/claudeAccountService')
const claudeConsoleAccountService = require('./account/claudeConsoleAccountService')
const geminiAccountService = require('./account/geminiAccountService')
const openaiAccountService = require('./account/openaiAccountService')
const azureOpenaiAccountService = require('./account/azureOpenaiAccountService')
const bedrockAccountService = require('./account/bedrockAccountService')
const droidAccountService = require('./account/droidAccountService')
const ccrAccountService = require('./account/ccrAccountService')
const accountGroupService = require('./accountGroupService')
// 可选服务(可能不存在)
let geminiApiAccountService = null
let openaiResponsesAccountService = null
try {
geminiApiAccountService = require('./geminiApiAccountService')
geminiApiAccountService = require('./account/geminiApiAccountService')
} catch (e) {
// 服务不存在,忽略
}
try {
openaiResponsesAccountService = require('./openaiResponsesAccountService')
openaiResponsesAccountService = require('./account/openaiResponsesAccountService')
} catch (e) {
// 服务不存在,忽略
}

View File

@@ -269,7 +269,7 @@ class AccountTestSchedulerService {
* @private
*/
async _testClaudeAccount(accountId, model) {
const claudeRelayService = require('./claudeRelayService')
const claudeRelayService = require('./relay/claudeRelayService')
return await claudeRelayService.testAccountConnectionSync(accountId, model)
}

View File

@@ -34,8 +34,8 @@ const fs = require('fs')
const path = require('path')
const logger = require('../utils/logger')
const { getProjectRoot } = require('../utils/projectPaths')
const geminiAccountService = require('./geminiAccountService')
const unifiedGeminiScheduler = require('./unifiedGeminiScheduler')
const geminiAccountService = require('./account/geminiAccountService')
const unifiedGeminiScheduler = require('./scheduler/unifiedGeminiScheduler')
const sessionHelper = require('../utils/sessionHelper')
const signatureCache = require('../utils/signatureCache')
const apiKeyService = require('./apiKeyService')

View File

@@ -280,16 +280,16 @@ class ClaudeRelayConfigService {
let accountService
switch (accountType) {
case 'claude-official':
accountService = require('./claudeAccountService')
accountService = require('./account/claudeAccountService')
break
case 'claude-console':
accountService = require('./claudeConsoleAccountService')
accountService = require('./account/claudeConsoleAccountService')
break
case 'bedrock':
accountService = require('./bedrockAccountService')
accountService = require('./account/bedrockAccountService')
break
case 'ccr':
accountService = require('./ccrAccountService')
accountService = require('./account/ccrAccountService')
break
default:
logger.warn(`Unknown account type for validation: ${accountType}`)

View File

@@ -4,10 +4,10 @@
*/
const logger = require('../utils/logger')
const openaiAccountService = require('./openaiAccountService')
const claudeAccountService = require('./claudeAccountService')
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
const unifiedOpenAIScheduler = require('./unifiedOpenAIScheduler')
const openaiAccountService = require('./account/openaiAccountService')
const claudeAccountService = require('./account/claudeAccountService')
const claudeConsoleAccountService = require('./account/claudeConsoleAccountService')
const unifiedOpenAIScheduler = require('./scheduler/unifiedOpenAIScheduler')
const webhookService = require('./webhookService')
class RateLimitCleanupService {

View File

@@ -1,7 +1,7 @@
const apiKeyService = require('./apiKeyService')
const apiKeyService = require('../apiKeyService')
const { convertMessagesToGemini, convertGeminiResponse } = require('./geminiRelayService')
const { normalizeAntigravityModelInput } = require('../utils/antigravityModel')
const antigravityClient = require('./antigravityClient')
const { normalizeAntigravityModelInput } = require('../../utils/antigravityModel')
const antigravityClient = require('../antigravityClient')
function buildRequestData({ messages, model, temperature, maxTokens, sessionId }) {
const requestedModel = normalizeAntigravityModelInput(model)

View File

@@ -1,7 +1,8 @@
const axios = require('axios')
const ProxyHelper = require('../utils/proxyHelper')
const logger = require('../utils/logger')
const config = require('../../config/config')
const ProxyHelper = require('../../utils/proxyHelper')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
// 转换模型名称(去掉 azure/ 前缀)
function normalizeModelName(model) {
@@ -212,6 +213,16 @@ async function handleAzureOpenAIRequest({
logger.error('Azure OpenAI Request Failed', errorDetails)
}
// 网络错误标记临时不可用
const azureAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (account?.id && !azureAutoProtectionDisabled) {
const statusCode = error.response?.status || 503
await upstreamErrorHelper
.markTempUnavailable(account.id, 'azure-openai', statusCode)
.catch(() => {})
}
throw error
}
}

View File

@@ -4,9 +4,10 @@ const {
InvokeModelWithResponseStreamCommand
} = require('@aws-sdk/client-bedrock-runtime')
const { fromEnv } = require('@aws-sdk/credential-providers')
const logger = require('../utils/logger')
const config = require('../../config/config')
const userMessageQueueService = require('./userMessageQueueService')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const userMessageQueueService = require('../userMessageQueueService')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class BedrockRelayService {
constructor() {
@@ -188,7 +189,7 @@ class BedrockRelayService {
}
} catch (error) {
logger.error('❌ Bedrock非流式请求失败:', error)
throw this._handleBedrockError(error)
throw this._handleBedrockError(error, accountId, bedrockAccount)
} finally {
// 📬 释放用户消息队列锁(兜底,正常情况下已在请求发送后提前释放)
if (queueLockAcquired && queueRequestId && accountId) {
@@ -376,10 +377,12 @@ class BedrockRelayService {
}
res.write('event: error\n')
res.write(`data: ${JSON.stringify({ error: this._handleBedrockError(error).message })}\n\n`)
res.write(
`data: ${JSON.stringify({ error: this._handleBedrockError(error, accountId, bedrockAccount).message })}\n\n`
)
res.end()
throw this._handleBedrockError(error)
throw this._handleBedrockError(error, accountId, bedrockAccount)
} finally {
// 📬 释放用户消息队列锁(兜底,正常情况下已在请求发送后提前释放)
if (queueLockAcquired && queueRequestId && accountId) {
@@ -647,7 +650,25 @@ class BedrockRelayService {
}
// 处理Bedrock错误
_handleBedrockError(error) {
_handleBedrockError(error, accountId = null, bedrockAccount = null) {
const autoProtectionDisabled =
bedrockAccount?.disableAutoProtection === true ||
bedrockAccount?.disableAutoProtection === 'true'
if (accountId && !autoProtectionDisabled) {
if (error.name === 'ThrottlingException') {
upstreamErrorHelper.markTempUnavailable(accountId, 'bedrock', 429).catch(() => {})
} else if (error.name === 'AccessDeniedException') {
upstreamErrorHelper.markTempUnavailable(accountId, 'bedrock', 403).catch(() => {})
} else if (
error.name === 'ServiceUnavailableException' ||
error.name === 'InternalServerException'
) {
upstreamErrorHelper.markTempUnavailable(accountId, 'bedrock', 500).catch(() => {})
} else if (error.name === 'ModelNotReadyException') {
upstreamErrorHelper.markTempUnavailable(accountId, 'bedrock', 503).catch(() => {})
}
}
const errorMessage = error.message || 'Unknown Bedrock error'
if (error.name === 'ValidationException') {

View File

@@ -1,10 +1,11 @@
const axios = require('axios')
const ccrAccountService = require('./ccrAccountService')
const logger = require('../utils/logger')
const config = require('../../config/config')
const { parseVendorPrefixedModel } = require('../utils/modelHelper')
const userMessageQueueService = require('./userMessageQueueService')
const { isStreamWritable } = require('../utils/streamHelper')
const ccrAccountService = require('../account/ccrAccountService')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const { parseVendorPrefixedModel } = require('../../utils/modelHelper')
const userMessageQueueService = require('../userMessageQueueService')
const { isStreamWritable } = require('../../utils/streamHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class CcrRelayService {
constructor() {
@@ -261,7 +262,11 @@ class CcrRelayService {
// 检查错误状态并相应处理
if (response.status === 401) {
logger.warn(`🚫 Unauthorized error detected for CCR account ${accountId}`)
await ccrAccountService.markAccountUnauthorized(accountId)
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!autoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(accountId, 'ccr', 401).catch(() => {})
}
} else if (response.status === 429) {
logger.warn(`🚫 Rate limit detected for CCR account ${accountId}`)
// 收到429先检查是否因为超过了手动配置的每日额度
@@ -270,9 +275,35 @@ class CcrRelayService {
})
await ccrAccountService.markAccountRateLimited(accountId)
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!autoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(
accountId,
'ccr',
429,
upstreamErrorHelper.parseRetryAfter(response.headers)
)
.catch(() => {})
}
} else if (response.status === 529) {
logger.warn(`🚫 Overload error detected for CCR account ${accountId}`)
await ccrAccountService.markAccountOverloaded(accountId)
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!autoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(accountId, 'ccr', 529).catch(() => {})
}
} else if (response.status >= 500) {
logger.warn(`🔥 Server error (${response.status}) detected for CCR account ${accountId}`)
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!autoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(accountId, 'ccr', response.status)
.catch(() => {})
}
} else if (response.status === 200 || response.status === 201) {
// 如果请求成功,检查并移除错误状态
const isRateLimited = await ccrAccountService.isAccountRateLimited(accountId)
@@ -310,6 +341,15 @@ class CcrRelayService {
error.message
)
// 网络错误标记临时不可用
if (accountId && !error.response) {
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!autoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(accountId, 'ccr', 503).catch(() => {})
}
}
throw error
} finally {
// 📬 释放用户消息队列锁(兜底,正常情况下已在请求发送后提前释放)
@@ -489,6 +529,14 @@ class CcrRelayService {
)
} else {
logger.error(`❌ CCR stream relay failed (Account: ${account?.name || accountId}):`, error)
// 网络错误标记临时不可用
if (accountId && !error.response) {
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!autoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(accountId, 'ccr', 503).catch(() => {})
}
}
}
throw error
} finally {
@@ -596,16 +644,40 @@ class CcrRelayService {
`❌ CCR API returned error status: ${response.status} | Account: ${account?.name || accountId}`
)
const autoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (response.status === 401) {
ccrAccountService.markAccountUnauthorized(accountId)
if (!autoProtectionDisabled) {
upstreamErrorHelper.markTempUnavailable(accountId, 'ccr', 401).catch(() => {})
}
} else if (response.status === 429) {
ccrAccountService.markAccountRateLimited(accountId)
if (!autoProtectionDisabled) {
upstreamErrorHelper
.markTempUnavailable(
accountId,
'ccr',
429,
upstreamErrorHelper.parseRetryAfter(response.headers)
)
.catch(() => {})
}
// 检查是否因为超过每日额度
ccrAccountService.checkQuotaUsage(accountId).catch((err) => {
logger.error('❌ Failed to check quota after 429 error:', err)
})
} else if (response.status === 529) {
ccrAccountService.markAccountOverloaded(accountId)
if (!autoProtectionDisabled) {
upstreamErrorHelper.markTempUnavailable(accountId, 'ccr', 529).catch(() => {})
}
} else if (response.status >= 500) {
if (!autoProtectionDisabled) {
upstreamErrorHelper
.markTempUnavailable(accountId, 'ccr', response.status)
.catch(() => {})
}
}
// 设置错误响应的状态码和响应头
@@ -885,7 +957,7 @@ class CcrRelayService {
// ⏰ 更新账户最后使用时间
async _updateLastUsedTime(accountId) {
try {
const redis = require('../models/redis')
const redis = require('../../models/redis')
const client = redis.getClientSafe()
await client.hset(`ccr_account:${accountId}`, 'lastUsedAt', new Date().toISOString())
} catch (error) {

View File

@@ -1,17 +1,18 @@
const axios = require('axios')
const { v4: uuidv4 } = require('uuid')
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const config = require('../../config/config')
const claudeConsoleAccountService = require('../account/claudeConsoleAccountService')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const {
sanitizeUpstreamError,
sanitizeErrorMessage,
isAccountDisabledError
} = require('../utils/errorSanitizer')
const userMessageQueueService = require('./userMessageQueueService')
const { isStreamWritable } = require('../utils/streamHelper')
const { filterForClaude } = require('../utils/headerFilter')
} = require('../../utils/errorSanitizer')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const userMessageQueueService = require('../userMessageQueueService')
const { isStreamWritable } = require('../../utils/streamHelper')
const { filterForClaude } = require('../../utils/headerFilter')
class ClaudeConsoleRelayService {
constructor() {
@@ -334,7 +335,9 @@ class ClaudeConsoleRelayService {
`🚫 Unauthorized error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
)
if (!autoProtectionDisabled) {
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
await upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', 401)
.catch(() => {})
}
} else if (accountDisabledError) {
logger.error(
@@ -357,6 +360,14 @@ class ClaudeConsoleRelayService {
if (!autoProtectionDisabled) {
await claudeConsoleAccountService.markAccountRateLimited(accountId)
await upstreamErrorHelper
.markTempUnavailable(
accountId,
'claude-console',
429,
upstreamErrorHelper.parseRetryAfter(response.headers)
)
.catch(() => {})
}
} else if (response.status === 529) {
logger.warn(
@@ -364,6 +375,18 @@ class ClaudeConsoleRelayService {
)
if (!autoProtectionDisabled) {
await claudeConsoleAccountService.markAccountOverloaded(accountId)
await upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', 529)
.catch(() => {})
}
} else if (response.status >= 500) {
logger.warn(
`🔥 Server error (${response.status}) detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
)
if (!autoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', response.status)
.catch(() => {})
}
} else if (response.status === 200 || response.status === 201) {
// 如果请求成功,检查并移除错误状态
@@ -831,7 +854,9 @@ class ClaudeConsoleRelayService {
`🚫 [Stream] Unauthorized error detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
)
if (!autoProtectionDisabled) {
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
await upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', 401)
.catch(() => {})
}
} else if (accountDisabledError) {
logger.error(
@@ -854,6 +879,14 @@ class ClaudeConsoleRelayService {
})
if (!autoProtectionDisabled) {
await claudeConsoleAccountService.markAccountRateLimited(accountId)
await upstreamErrorHelper
.markTempUnavailable(
accountId,
'claude-console',
429,
upstreamErrorHelper.parseRetryAfter(response.headers)
)
.catch(() => {})
}
} else if (response.status === 529) {
logger.warn(
@@ -861,6 +894,18 @@ class ClaudeConsoleRelayService {
)
if (!autoProtectionDisabled) {
await claudeConsoleAccountService.markAccountOverloaded(accountId)
await upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', 529)
.catch(() => {})
}
} else if (response.status >= 500) {
logger.warn(
`🔥 [Stream] Server error (${response.status}) detected for Claude Console account ${accountId}${autoProtectionDisabled ? ' (auto-protection disabled, skipping status change)' : ''}`
)
if (!autoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', response.status)
.catch(() => {})
}
}
@@ -1246,16 +1291,37 @@ class ClaudeConsoleRelayService {
// 检查错误状态
if (error.response) {
const catchAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (error.response.status === 401) {
claudeConsoleAccountService.markAccountUnauthorized(accountId)
if (!catchAutoProtectionDisabled) {
upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', 401)
.catch(() => {})
}
} else if (error.response.status === 429) {
claudeConsoleAccountService.markAccountRateLimited(accountId)
// 检查是否因为超过每日额度
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
logger.error('❌ Failed to check quota after 429 error:', err)
})
if (!catchAutoProtectionDisabled) {
claudeConsoleAccountService.markAccountRateLimited(accountId)
// 检查是否因为超过每日额度
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
logger.error('❌ Failed to check quota after 429 error:', err)
})
upstreamErrorHelper
.markTempUnavailable(
accountId,
'claude-console',
429,
upstreamErrorHelper.parseRetryAfter(error.response.headers)
)
.catch(() => {})
}
} else if (error.response.status === 529) {
claudeConsoleAccountService.markAccountOverloaded(accountId)
if (!catchAutoProtectionDisabled) {
claudeConsoleAccountService.markAccountOverloaded(accountId)
upstreamErrorHelper
.markTempUnavailable(accountId, 'claude-console', 529)
.catch(() => {})
}
}
}
@@ -1311,7 +1377,7 @@ class ClaudeConsoleRelayService {
// 🕐 更新最后使用时间
async _updateLastUsedTime(accountId) {
try {
const client = require('../models/redis').getClientSafe()
const client = require('../../models/redis').getClientSafe()
const accountKey = `claude_console_account:${accountId}`
const exists = await client.exists(accountKey)
@@ -1390,7 +1456,7 @@ class ClaudeConsoleRelayService {
// 🧪 测试账号连接供Admin API使用
async testAccountConnection(accountId, responseStream) {
const { sendStreamTestRequest } = require('../utils/testPayloadHelper')
const { sendStreamTestRequest } = require('../../utils/testPayloadHelper')
try {
const account = await claudeConsoleAccountService.getAccount(accountId)

View File

@@ -1,26 +1,27 @@
const https = require('https')
const zlib = require('zlib')
const path = require('path')
const ProxyHelper = require('../utils/proxyHelper')
const { filterForClaude } = require('../utils/headerFilter')
const claudeAccountService = require('./claudeAccountService')
const unifiedClaudeScheduler = require('./unifiedClaudeScheduler')
const sessionHelper = require('../utils/sessionHelper')
const logger = require('../utils/logger')
const config = require('../../config/config')
const claudeCodeHeadersService = require('./claudeCodeHeadersService')
const redis = require('../models/redis')
const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator')
const { formatDateWithTimezone } = require('../utils/dateHelper')
const requestIdentityService = require('./requestIdentityService')
const { createClaudeTestPayload } = require('../utils/testPayloadHelper')
const userMessageQueueService = require('./userMessageQueueService')
const { isStreamWritable } = require('../utils/streamHelper')
const ProxyHelper = require('../../utils/proxyHelper')
const { filterForClaude } = require('../../utils/headerFilter')
const claudeAccountService = require('../account/claudeAccountService')
const unifiedClaudeScheduler = require('../scheduler/unifiedClaudeScheduler')
const sessionHelper = require('../../utils/sessionHelper')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const claudeCodeHeadersService = require('../claudeCodeHeadersService')
const redis = require('../../models/redis')
const ClaudeCodeValidator = require('../../validators/clients/claudeCodeValidator')
const { formatDateWithTimezone } = require('../../utils/dateHelper')
const requestIdentityService = require('../requestIdentityService')
const { createClaudeTestPayload } = require('../../utils/testPayloadHelper')
const userMessageQueueService = require('../userMessageQueueService')
const { isStreamWritable } = require('../../utils/streamHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const {
getHttpsAgentForStream,
getHttpsAgentForNonStream,
getPricingData
} = require('../utils/performanceOptimizer')
} = require('../../utils/performanceOptimizer')
// structuredClone polyfill for Node < 17
const safeClone =
@@ -693,22 +694,26 @@ class ClaudeRelayService {
if (errorCount >= 1) {
logger.error(
`❌ Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized`
)
await unifiedClaudeScheduler.markAccountUnauthorized(
accountId,
accountType,
sessionHash
`❌ Account ${accountId} encountered 401 error (${errorCount} errors), temporarily pausing`
)
}
await upstreamErrorHelper.markTempUnavailable(accountId, accountType, 401).catch(() => {})
// 清除粘性会话,让后续请求路由到其他账户
if (sessionHash) {
await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {})
}
}
// 检查是否为403状态码禁止访问
// 注意如果进行了重试retryCount > 0这里的 403 是重试后最终的结果
else if (response.statusCode === 403) {
logger.error(
`🚫 Forbidden error (403) detected for account ${accountId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}, marking as blocked`
`🚫 Forbidden error (403) detected for account ${accountId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}, temporarily pausing`
)
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
await upstreamErrorHelper.markTempUnavailable(accountId, accountType, 403).catch(() => {})
// 清除粘性会话,让后续请求路由到其他账户
if (sessionHash) {
await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {})
}
}
// 检查是否返回组织被禁用错误400状态码
else if (organizationDisabledError) {
@@ -734,6 +739,7 @@ class ClaudeRelayService {
} else {
logger.info(`🚫 529 error handling is disabled, skipping account overload marking`)
}
await upstreamErrorHelper.markTempUnavailable(accountId, accountType, 529).catch(() => {})
}
// 检查是否为5xx状态码
else if (response.statusCode >= 500 && response.statusCode < 600) {
@@ -819,6 +825,14 @@ class ClaudeRelayService {
sessionHash,
rateLimitResetTimestamp
)
await upstreamErrorHelper
.markTempUnavailable(
accountId,
accountType,
429,
upstreamErrorHelper.parseRetryAfter(response.headers)
)
.catch(() => {})
if (dedicatedRateLimitMessage) {
return {
@@ -1935,6 +1949,14 @@ class ClaudeRelayService {
sessionHash,
rateLimitResetTimestamp
)
await upstreamErrorHelper
.markTempUnavailable(
accountId,
accountType,
429,
upstreamErrorHelper.parseRetryAfter(res.headers)
)
.catch(() => {})
logger.warn(`🚫 [Stream] Rate limit detected for account ${accountId}, status 429`)
if (isDedicatedOfficialAccount) {
@@ -2032,21 +2054,29 @@ class ClaudeRelayService {
if (errorCount >= 1) {
logger.error(
`❌ [Stream] Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized`
)
await unifiedClaudeScheduler.markAccountUnauthorized(
accountId,
accountType,
sessionHash
`❌ [Stream] Account ${accountId} encountered 401 error (${errorCount} errors), temporarily pausing`
)
}
await upstreamErrorHelper
.markTempUnavailable(accountId, accountType, 401)
.catch(() => {})
// 清除粘性会话,让后续请求路由到其他账户
if (sessionHash) {
await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {})
}
} else if (res.statusCode === 403) {
// 403 处理:走到这里说明重试已用尽或不适用重试,直接标记 blocked
// 注意:重试逻辑已在 handleErrorResponse 外部提前处理
logger.error(
`🚫 [Stream] Forbidden error (403) detected for account ${accountId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}, marking as blocked`
`🚫 [Stream] Forbidden error (403) detected for account ${accountId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}, temporarily pausing`
)
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
await upstreamErrorHelper
.markTempUnavailable(accountId, accountType, 403)
.catch(() => {})
// 清除粘性会话,让后续请求路由到其他账户
if (sessionHash) {
await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {})
}
} else if (res.statusCode === 529) {
logger.warn(`🚫 [Stream] Overload error (529) detected for account ${accountId}`)
@@ -2068,6 +2098,9 @@ class ClaudeRelayService {
`🚫 [Stream] 529 error handling is disabled, skipping account overload marking`
)
}
await upstreamErrorHelper
.markTempUnavailable(accountId, accountType, 529)
.catch(() => {})
} else if (res.statusCode >= 500 && res.statusCode < 600) {
logger.warn(
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
@@ -2506,6 +2539,14 @@ class ClaudeRelayService {
sessionHash,
rateLimitResetTimestamp
)
await upstreamErrorHelper
.markTempUnavailable(
accountId,
accountType,
429,
upstreamErrorHelper.parseRetryAfter(res.headers)
)
.catch(() => {})
}
} else if (res.statusCode === 200) {
// 请求成功清除401和500错误计数

View File

@@ -1,13 +1,14 @@
const https = require('https')
const axios = require('axios')
const ProxyHelper = require('../utils/proxyHelper')
const droidScheduler = require('./droidScheduler')
const droidAccountService = require('./droidAccountService')
const apiKeyService = require('./apiKeyService')
const redis = require('../models/redis')
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
const logger = require('../utils/logger')
const runtimeAddon = require('../utils/runtimeAddon')
const ProxyHelper = require('../../utils/proxyHelper')
const droidScheduler = require('../scheduler/droidScheduler')
const droidAccountService = require('../account/droidAccountService')
const apiKeyService = require('../apiKeyService')
const redis = require('../../models/redis')
const { updateRateLimitCounters } = require('../../utils/rateLimitHelper')
const logger = require('../../utils/logger')
const runtimeAddon = require('../../utils/runtimeAddon')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const SYSTEM_PROMPT = 'You are Droid, an AI software engineering agent built by Factory.'
const RUNTIME_EVENT_FMT_PAYLOAD = 'fmtPayload'
@@ -346,6 +347,21 @@ class DroidRelayService {
}
const status = error?.response?.status
const droidAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
// 5xx 错误
if (status >= 500 && account?.id && !droidAutoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(account.id, 'droid', status).catch(() => {})
} else if (
!status &&
account?.id &&
error.message !== 'Client disconnected' &&
!droidAutoProtectionDisabled
) {
// 网络错误(非客户端断开),临时不可用
await upstreamErrorHelper.markTempUnavailable(account.id, 'droid', 503).catch(() => {})
}
if (status >= 400 && status < 500) {
try {
await this._handleUpstreamClientError(status, {
@@ -518,6 +534,15 @@ class DroidRelayService {
logger.info('✅ res.end() reached')
const body = Buffer.concat(chunks).toString()
logger.error(`❌ Factory.ai error response body: ${body || '(empty)'}`)
if (res.statusCode >= 500) {
const streamAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!streamAutoProtectionDisabled) {
upstreamErrorHelper
.markTempUnavailable(account.id, 'droid', res.statusCode)
.catch(() => {})
}
}
if (res.statusCode >= 400 && res.statusCode < 500) {
this._handleUpstreamClientError(res.statusCode, {
account,
@@ -1380,7 +1405,11 @@ class DroidRelayService {
return
}
await this._stopDroidAccountScheduling(accountId, statusCode, '凭证不可用')
const clientErrorAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!clientErrorAutoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(accountId, 'droid', statusCode)
}
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
}

View File

@@ -1,8 +1,8 @@
const axios = require('axios')
const ProxyHelper = require('../utils/proxyHelper')
const logger = require('../utils/logger')
const config = require('../../config/config')
const apiKeyService = require('./apiKeyService')
const ProxyHelper = require('../../utils/proxyHelper')
const logger = require('../../utils/logger')
const config = require('../../../config/config')
const apiKeyService = require('../apiKeyService')
// Gemini API 配置
const GEMINI_API_BASE = 'https://cloudcode.googleapis.com/v1'

View File

@@ -1,13 +1,14 @@
const axios = require('axios')
const ProxyHelper = require('../utils/proxyHelper')
const logger = require('../utils/logger')
const { filterForOpenAI } = require('../utils/headerFilter')
const openaiResponsesAccountService = require('./openaiResponsesAccountService')
const apiKeyService = require('./apiKeyService')
const unifiedOpenAIScheduler = require('./unifiedOpenAIScheduler')
const config = require('../../config/config')
const ProxyHelper = require('../../utils/proxyHelper')
const logger = require('../../utils/logger')
const { filterForOpenAI } = require('../../utils/headerFilter')
const openaiResponsesAccountService = require('../account/openaiResponsesAccountService')
const apiKeyService = require('../apiKeyService')
const unifiedOpenAIScheduler = require('../scheduler/unifiedOpenAIScheduler')
const config = require('../../../config/config')
const crypto = require('crypto')
const LRUCache = require('../utils/lruCache')
const LRUCache = require('../../utils/lruCache')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
// lastUsedAt 更新节流(每账户 60 秒内最多更新一次,使用 LRU 防止内存泄漏)
const lastUsedAtThrottle = new LRUCache(1000) // 最多缓存 1000 个账户
@@ -160,6 +161,19 @@ class OpenAIResponsesRelayService {
sessionHash
)
const oaiAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!oaiAutoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(
account.id,
'openai-responses',
429,
resetsInSeconds || upstreamErrorHelper.parseRetryAfter(response.headers)
)
.catch(() => {})
}
// 返回错误响应(使用处理后的数据,避免循环引用)
const errorResponse = errorData || {
error: {
@@ -218,31 +232,23 @@ class OpenAIResponsesRelayService {
})
if (response.status === 401) {
let reason = 'OpenAI Responses账号认证失败401错误'
if (errorData) {
if (typeof errorData === 'string' && errorData.trim()) {
reason = `OpenAI Responses账号认证失败401错误${errorData.trim()}`
} else if (
errorData.error &&
typeof errorData.error.message === 'string' &&
errorData.error.message.trim()
) {
reason = `OpenAI Responses账号认证失败401错误${errorData.error.message.trim()}`
} else if (typeof errorData.message === 'string' && errorData.message.trim()) {
reason = `OpenAI Responses账号认证失败401错误${errorData.message.trim()}`
}
}
logger.warn(`🚫 OpenAI Responses账号认证失败401错误for account ${account?.id}`)
try {
await unifiedOpenAIScheduler.markAccountUnauthorized(
account.id,
'openai-responses',
sessionHash,
reason
)
// 仅临时暂停,不永久禁用
const oaiAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!oaiAutoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(account.id, 'openai-responses', 401)
.catch(() => {})
}
if (sessionHash) {
await unifiedOpenAIScheduler._deleteSessionMapping(sessionHash).catch(() => {})
}
} catch (markError) {
logger.error(
'❌ Failed to mark OpenAI-Responses account unauthorized after 401:',
'❌ Failed to mark OpenAI-Responses account temporarily unavailable after 401:',
markError
)
}
@@ -272,11 +278,36 @@ class OpenAIResponsesRelayService {
return res.status(401).json(unauthorizedResponse)
}
// 处理 5xx 上游错误
if (response.status >= 500 && account?.id) {
try {
const oaiAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!oaiAutoProtectionDisabled) {
await upstreamErrorHelper.markTempUnavailable(
account.id,
'openai-responses',
response.status
)
}
if (sessionHash) {
await unifiedOpenAIScheduler._deleteSessionMapping(sessionHash).catch(() => {})
}
} catch (markError) {
logger.warn(
'Failed to mark OpenAI-Responses account temporarily unavailable:',
markError
)
}
}
// 清理监听器
req.removeListener('close', handleClientDisconnect)
res.removeListener('close', handleClientDisconnect)
return res.status(response.status).json(errorData)
return res
.status(response.status)
.json(upstreamErrorHelper.sanitizeErrorForClient(errorData))
}
// 更新最后使用时间(节流)
@@ -314,10 +345,15 @@ class OpenAIResponsesRelayService {
// 检查是否是网络错误
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
await openaiResponsesAccountService.updateAccount(account.id, {
status: 'error',
errorMessage: `Connection error: ${error.code}`
})
if (account?.id) {
const oaiAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!oaiAutoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(account.id, 'openai-responses', 503)
.catch(() => {})
}
}
}
// 如果已经发送了响应头,直接结束
@@ -352,31 +388,25 @@ class OpenAIResponsesRelayService {
}
if (status === 401) {
let reason = 'OpenAI Responses账号认证失败401错误'
if (errorData) {
if (typeof errorData === 'string' && errorData.trim()) {
reason = `OpenAI Responses账号认证失败401错误${errorData.trim()}`
} else if (
errorData.error &&
typeof errorData.error.message === 'string' &&
errorData.error.message.trim()
) {
reason = `OpenAI Responses账号认证失败401错误${errorData.error.message.trim()}`
} else if (typeof errorData.message === 'string' && errorData.message.trim()) {
reason = `OpenAI Responses账号认证失败401错误${errorData.message.trim()}`
}
}
logger.warn(
`🚫 OpenAI Responses账号认证失败401错误for account ${account?.id} (catch handler)`
)
try {
await unifiedOpenAIScheduler.markAccountUnauthorized(
account.id,
'openai-responses',
sessionHash,
reason
)
// 仅临时暂停,不永久禁用
const oaiAutoProtectionDisabled =
account?.disableAutoProtection === true || account?.disableAutoProtection === 'true'
if (!oaiAutoProtectionDisabled) {
await upstreamErrorHelper
.markTempUnavailable(account.id, 'openai-responses', 401)
.catch(() => {})
}
if (sessionHash) {
await unifiedOpenAIScheduler._deleteSessionMapping(sessionHash).catch(() => {})
}
} catch (markError) {
logger.error(
'❌ Failed to mark OpenAI-Responses account unauthorized in catch handler:',
'❌ Failed to mark OpenAI-Responses account temporarily unavailable in catch handler:',
markError
)
}
@@ -402,7 +432,7 @@ class OpenAIResponsesRelayService {
return res.status(401).json(unauthorizedResponse)
}
return res.status(status).json(errorData)
return res.status(status).json(upstreamErrorHelper.sanitizeErrorForClient(errorData))
}
// 其他错误
@@ -571,7 +601,7 @@ class OpenAIResponsesRelayService {
// 更新账户使用额度(如果设置了额度限制)
if (parseFloat(account.dailyQuota) > 0) {
// 使用CostCalculator正确计算费用考虑缓存token的不同价格
const CostCalculator = require('../utils/costCalculator')
const CostCalculator = require('../../utils/costCalculator')
const costInfo = CostCalculator.calculateCost(
{
input_tokens: actualInputTokens, // 实际输入(不含缓存)
@@ -700,7 +730,7 @@ class OpenAIResponsesRelayService {
// 更新账户使用额度(如果设置了额度限制)
if (parseFloat(account.dailyQuota) > 0) {
// 使用CostCalculator正确计算费用考虑缓存token的不同价格
const CostCalculator = require('../utils/costCalculator')
const CostCalculator = require('../../utils/costCalculator')
const costInfo = CostCalculator.calculateCost(
{
input_tokens: actualInputTokens, // 实际输入(不含缓存)

View File

@@ -1,13 +1,14 @@
const droidAccountService = require('./droidAccountService')
const accountGroupService = require('./accountGroupService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const droidAccountService = require('../account/droidAccountService')
const accountGroupService = require('../accountGroupService')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const {
isTruthy,
isAccountHealthy,
sortAccountsByPriority,
normalizeEndpointType
} = require('../utils/commonHelper')
} = require('../../utils/commonHelper')
class DroidScheduler {
constructor() {
@@ -57,9 +58,21 @@ class DroidScheduler {
})
)
return accounts.filter(
(account) => account && isAccountHealthy(account) && this._isAccountSchedulable(account)
)
const result = []
for (const account of accounts) {
if (!account || !isAccountHealthy(account) || !this._isAccountSchedulable(account)) {
continue
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(account.id, 'droid')
if (isTempUnavailable) {
logger.debug(
`⏭️ Skipping Droid group member ${account.name || account.id} - temporarily unavailable`
)
continue
}
result.push(account)
}
return result
}
async _ensureLastUsedUpdated(accountId) {
@@ -99,8 +112,15 @@ class DroidScheduler {
} else {
const account = await droidAccountService.getAccount(binding)
if (account) {
candidates = [account]
isDedicatedBinding = true
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(account.id, 'droid')
if (isTempUnavailable) {
logger.warn(
`⏱️ Bound Droid account ${account.name || account.id} temporarily unavailable, falling back to pool`
)
} else {
candidates = [account]
isDedicatedBinding = true
}
}
}
}
@@ -109,13 +129,26 @@ class DroidScheduler {
candidates = await droidAccountService.getSchedulableAccounts(normalizedEndpoint)
}
const filtered = candidates.filter(
const syncFiltered = candidates.filter(
(account) =>
account &&
isAccountHealthy(account) &&
this._isAccountSchedulable(account) &&
this._matchesEndpoint(account, normalizedEndpoint)
)
const filteredResults = await Promise.all(
syncFiltered.map(async (account) => {
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(account.id, 'droid')
if (isTempUnavailable) {
logger.debug(
`⏭️ Skipping Droid account ${account.name || account.id} - temporarily unavailable`
)
return null
}
return account
})
)
const filtered = filteredResults.filter(Boolean)
if (filtered.length === 0) {
throw new Error(

View File

@@ -1,12 +1,13 @@
const claudeAccountService = require('./claudeAccountService')
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
const bedrockAccountService = require('./bedrockAccountService')
const ccrAccountService = require('./ccrAccountService')
const accountGroupService = require('./accountGroupService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../utils/modelHelper')
const { isSchedulable, sortAccountsByPriority } = require('../utils/commonHelper')
const claudeAccountService = require('../account/claudeAccountService')
const claudeConsoleAccountService = require('../account/claudeConsoleAccountService')
const bedrockAccountService = require('../account/bedrockAccountService')
const ccrAccountService = require('../account/ccrAccountService')
const accountGroupService = require('../accountGroupService')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const { parseVendorPrefixedModel, isOpus45OrNewer } = require('../../utils/modelHelper')
const { isSchedulable, sortAccountsByPriority } = require('../../utils/commonHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
/**
* Check if account is Pro (not Max)
@@ -1175,7 +1176,7 @@ class UnifiedClaudeScheduler {
const client = redis.getClientSafe()
const mappingData = JSON.stringify({ accountId, accountType })
// 依据配置设置TTL小时
const appConfig = require('../../config/config')
const appConfig = require('../../../config/config')
const ttlHours = appConfig.session?.stickyTtlHours || 1
const ttlSeconds = Math.max(1, Math.floor(ttlHours * 60 * 60))
await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, ttlSeconds, mappingData)
@@ -1224,7 +1225,7 @@ class UnifiedClaudeScheduler {
return true
}
const appConfig = require('../../config/config')
const appConfig = require('../../../config/config')
const ttlHours = appConfig.session?.stickyTtlHours || 1
const renewalThresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0
@@ -1261,15 +1262,10 @@ class UnifiedClaudeScheduler {
ttlSeconds = 300
) {
try {
const client = redis.getClientSafe()
const key = `temp_unavailable:${accountType}:${accountId}`
await client.setex(key, ttlSeconds, '1')
await upstreamErrorHelper.markTempUnavailable(accountId, accountType, 500, ttlSeconds)
if (sessionHash) {
await this._deleteSessionMapping(sessionHash)
}
logger.warn(
`⏱️ Account ${accountId} (${accountType}) marked temporarily unavailable for ${ttlSeconds}s`
)
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark account temporarily unavailable: ${accountId}`, error)
@@ -1279,14 +1275,7 @@ class UnifiedClaudeScheduler {
// 🔍 检查账户是否临时不可用
async isAccountTemporarilyUnavailable(accountId, accountType) {
try {
const client = redis.getClientSafe()
const key = `temp_unavailable:${accountType}:${accountId}`
return (await client.exists(key)) === 1
} catch (error) {
logger.error(`❌ Failed to check temp unavailable status: ${accountId}`, error)
return false
}
return upstreamErrorHelper.isTempUnavailable(accountId, accountType)
}
// 🚫 标记账户为限流状态

View File

@@ -1,9 +1,10 @@
const geminiAccountService = require('./geminiAccountService')
const geminiApiAccountService = require('./geminiApiAccountService')
const accountGroupService = require('./accountGroupService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const { isSchedulable, isActive, sortAccountsByPriority } = require('../utils/commonHelper')
const geminiAccountService = require('../account/geminiAccountService')
const geminiApiAccountService = require('../account/geminiApiAccountService')
const accountGroupService = require('../accountGroupService')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const { isSchedulable, isActive, sortAccountsByPriority } = require('../../utils/commonHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
const OAUTH_PROVIDER_GEMINI_CLI = 'gemini-cli'
const OAUTH_PROVIDER_ANTIGRAVITY = 'antigravity'
@@ -241,8 +242,17 @@ class UnifiedGeminiScheduler {
const accountId = apiKeyData.geminiAccountId.replace('api:', '')
const boundAccount = await geminiApiAccountService.getAccount(accountId)
if (boundAccount && isActive(boundAccount.isActive) && boundAccount.status !== 'error') {
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
accountId,
'gemini-api'
)
if (isTempUnavailable) {
logger.warn(
`⏱️ Bound Gemini-API account ${boundAccount.name} (${accountId}) temporarily unavailable, falling back to pool`
)
}
const isRateLimited = await this.isAccountRateLimited(accountId)
if (!isRateLimited) {
if (!isRateLimited && !isTempUnavailable) {
// 检查模型支持
if (
requestedModel &&
@@ -298,8 +308,17 @@ class UnifiedGeminiScheduler {
) {
return availableAccounts
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
boundAccount.id,
'gemini'
)
if (isTempUnavailable) {
logger.warn(
`⏱️ Bound Gemini account ${boundAccount.name} (${boundAccount.id}) temporarily unavailable, falling back to pool`
)
}
const isRateLimited = await this.isAccountRateLimited(boundAccount.id)
if (!isRateLimited) {
if (!isRateLimited && !isTempUnavailable) {
// 检查模型支持
if (
requestedModel &&
@@ -364,6 +383,13 @@ class UnifiedGeminiScheduler {
continue
}
// 检查临时不可用
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(account.id, 'gemini')
if (isTempUnavailable) {
logger.debug(`⏭️ Skipping Gemini account ${account.name} - temporarily unavailable`)
continue
}
// 检查模型支持
if (requestedModel && account.supportedModels && account.supportedModels.length > 0) {
// 处理可能带有 models/ 前缀的模型名
@@ -417,6 +443,16 @@ class UnifiedGeminiScheduler {
}
}
// 检查临时不可用
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
'gemini-api'
)
if (isTempUnavailable) {
logger.debug(`⏭️ Skipping Gemini-API account ${account.name} - temporarily unavailable`)
continue
}
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id)
if (!isRateLimited) {
@@ -451,6 +487,14 @@ class UnifiedGeminiScheduler {
logger.info(`🚫 Gemini account ${accountId} is not schedulable`)
return false
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
accountId,
accountType
)
if (isTempUnavailable) {
logger.info(`⏱️ Gemini account ${accountId} is temporarily unavailable`)
return false
}
return !(await this.isAccountRateLimited(accountId))
} else if (accountType === 'gemini-api') {
const account = await geminiApiAccountService.getAccount(accountId)
@@ -462,6 +506,14 @@ class UnifiedGeminiScheduler {
logger.info(`🚫 Gemini-API account ${accountId} is not schedulable`)
return false
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
accountId,
accountType
)
if (isTempUnavailable) {
logger.info(`⏱️ Gemini account ${accountId} is temporarily unavailable`)
return false
}
return !(await this.isAccountRateLimited(accountId))
}
return false
@@ -494,7 +546,7 @@ class UnifiedGeminiScheduler {
const client = redis.getClientSafe()
const mappingData = JSON.stringify({ accountId, accountType })
// 依据配置设置TTL小时
const appConfig = require('../../config/config')
const appConfig = require('../../../config/config')
const ttlHours = appConfig.session?.stickyTtlHours || 1
const ttlSeconds = Math.max(1, Math.floor(ttlHours * 60 * 60))
const key = this._getSessionMappingKey(sessionHash, oauthProvider)
@@ -535,7 +587,7 @@ class UnifiedGeminiScheduler {
return true
}
const appConfig = require('../../config/config')
const appConfig = require('../../../config/config')
const ttlHours = appConfig.session?.stickyTtlHours || 1
const renewalThresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0
if (!renewalThresholdMinutes) {
@@ -749,6 +801,14 @@ class UnifiedGeminiScheduler {
// 检查是否被限流
const isRateLimited = await this.isAccountRateLimited(account.id, accountType)
if (!isRateLimited) {
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
accountType
)
if (isTempUnavailable) {
logger.debug(`⏭️ Skipping group member ${account.name} - temporarily unavailable`)
continue
}
availableAccounts.push({
...account,
accountId: account.id,

View File

@@ -1,9 +1,10 @@
const openaiAccountService = require('./openaiAccountService')
const openaiResponsesAccountService = require('./openaiResponsesAccountService')
const accountGroupService = require('./accountGroupService')
const redis = require('../models/redis')
const logger = require('../utils/logger')
const { isSchedulable, sortAccountsByPriority } = require('../utils/commonHelper')
const openaiAccountService = require('../account/openaiAccountService')
const openaiResponsesAccountService = require('../account/openaiResponsesAccountService')
const accountGroupService = require('../accountGroupService')
const redis = require('../../models/redis')
const logger = require('../../utils/logger')
const { isSchedulable, sortAccountsByPriority } = require('../../utils/commonHelper')
const upstreamErrorHelper = require('../../utils/upstreamErrorHelper')
class UnifiedOpenAIScheduler {
constructor() {
@@ -153,91 +154,102 @@ class UnifiedOpenAIScheduler {
boundAccount.status !== 'unauthorized'
if (isActiveBoundAccount) {
if (accountType === 'openai') {
const readiness = await this._ensureAccountReadyForScheduling(
boundAccount,
boundAccount.id,
{ sanitized: false }
)
if (!readiness.canUse) {
const isRateLimited = readiness.reason === 'rate_limited'
const errorMsg = isRateLimited
? `Dedicated account ${boundAccount.name} is currently rate limited`
: `Dedicated account ${boundAccount.name} is not schedulable`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = isRateLimited ? 429 : 403
throw error
}
} else {
const hasRateLimitFlag = this._isRateLimited(boundAccount.rateLimitStatus)
if (hasRateLimitFlag) {
const isRateLimitCleared = await openaiResponsesAccountService.checkAndClearRateLimit(
boundAccount.id
)
if (!isRateLimitCleared) {
const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 429 // Too Many Requests - 限流
throw error
}
// 限流已解除,刷新账户最新状态,确保后续调度信息准确
boundAccount = await openaiResponsesAccountService.getAccount(boundAccount.id)
if (!boundAccount) {
const errorMsg = `Dedicated account ${apiKeyData.openaiAccountId} not found after rate limit reset`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 404
throw error
}
}
if (!isSchedulable(boundAccount.schedulable)) {
const errorMsg = `Dedicated account ${boundAccount.name} is not schedulable`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 403 // Forbidden - 调度被禁止
throw error
}
// ⏰ 检查 OpenAI-Responses 专属账户订阅是否过期
if (openaiResponsesAccountService.isSubscriptionExpired(boundAccount)) {
const errorMsg = `Dedicated account ${boundAccount.name} subscription has expired`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 403 // Forbidden - 订阅已过期
throw error
}
}
// 专属账户可选的模型检查只有明确配置了supportedModels且不为空才检查
// OpenAI-Responses 账户默认支持所有模型
if (
accountType === 'openai' &&
requestedModel &&
boundAccount.supportedModels &&
boundAccount.supportedModels.length > 0
) {
const modelSupported = boundAccount.supportedModels.includes(requestedModel)
if (!modelSupported) {
const errorMsg = `Dedicated account ${boundAccount.name} does not support model ${requestedModel}`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 400 // Bad Request - 请求参数错误
throw error
}
}
logger.info(
`🎯 Using bound dedicated ${accountType} account: ${boundAccount.name} (${boundAccount.id}) for API key ${apiKeyData.name}`
)
// 更新账户的最后使用时间
await this.updateAccountLastUsed(boundAccount.id, accountType)
return {
accountId: boundAccount.id,
// 检查是否临时不可用
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
boundAccount.id,
accountType
)
if (isTempUnavailable) {
logger.warn(
`⏱️ Bound ${accountType} account ${boundAccount.name} temporarily unavailable, falling back to pool`
)
// 不 throw让代码继续走到共享池选择
} else {
if (accountType === 'openai') {
const readiness = await this._ensureAccountReadyForScheduling(
boundAccount,
boundAccount.id,
{ sanitized: false }
)
if (!readiness.canUse) {
const isRateLimited = readiness.reason === 'rate_limited'
const errorMsg = isRateLimited
? `Dedicated account ${boundAccount.name} is currently rate limited`
: `Dedicated account ${boundAccount.name} is not schedulable`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = isRateLimited ? 429 : 403
throw error
}
} else {
const hasRateLimitFlag = this._isRateLimited(boundAccount.rateLimitStatus)
if (hasRateLimitFlag) {
const isRateLimitCleared =
await openaiResponsesAccountService.checkAndClearRateLimit(boundAccount.id)
if (!isRateLimitCleared) {
const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 429 // Too Many Requests - 限流
throw error
}
// 限流已解除,刷新账户最新状态,确保后续调度信息准确
boundAccount = await openaiResponsesAccountService.getAccount(boundAccount.id)
if (!boundAccount) {
const errorMsg = `Dedicated account ${apiKeyData.openaiAccountId} not found after rate limit reset`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 404
throw error
}
}
if (!isSchedulable(boundAccount.schedulable)) {
const errorMsg = `Dedicated account ${boundAccount.name} is not schedulable`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 403 // Forbidden - 调度被禁止
throw error
}
// ⏰ 检查 OpenAI-Responses 专属账户订阅是否过期
if (openaiResponsesAccountService.isSubscriptionExpired(boundAccount)) {
const errorMsg = `Dedicated account ${boundAccount.name} subscription has expired`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 403 // Forbidden - 订阅已过期
throw error
}
}
// 专属账户可选的模型检查只有明确配置了supportedModels且不为空才检查
// OpenAI-Responses 账户默认支持所有模型
if (
accountType === 'openai' &&
requestedModel &&
boundAccount.supportedModels &&
boundAccount.supportedModels.length > 0
) {
const modelSupported = boundAccount.supportedModels.includes(requestedModel)
if (!modelSupported) {
const errorMsg = `Dedicated account ${boundAccount.name} does not support model ${requestedModel}`
logger.warn(`⚠️ ${errorMsg}`)
const error = new Error(errorMsg)
error.statusCode = 400 // Bad Request - 请求参数错误
throw error
}
}
logger.info(
`🎯 Using bound dedicated ${accountType} account: ${boundAccount.name} (${boundAccount.id}) for API key ${apiKeyData.name}`
)
// 更新账户的最后使用时间
await this.updateAccountLastUsed(boundAccount.id, accountType)
return {
accountId: boundAccount.id,
accountType
}
}
} else {
// 专属账户不可用时直接报错,不降级到共享池
@@ -370,6 +382,12 @@ class UnifiedOpenAIScheduler {
continue
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(accountId, 'openai')
if (isTempUnavailable) {
logger.debug(`⏭️ Skipping openai account ${account.name} - temporarily unavailable`)
continue
}
// 检查token是否过期并自动刷新
const isExpired = openaiAccountService.isTokenExpired(account)
if (isExpired) {
@@ -465,6 +483,17 @@ class UnifiedOpenAIScheduler {
}
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
'openai-responses'
)
if (isTempUnavailable) {
logger.debug(
`⏭️ Skipping openai-responses account ${account.name} - temporarily unavailable`
)
continue
}
// ⏰ 检查订阅是否过期
if (openaiResponsesAccountService.isSubscriptionExpired(account)) {
logger.debug(
@@ -517,6 +546,15 @@ class UnifiedOpenAIScheduler {
return false
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
accountId,
accountType
)
if (isTempUnavailable) {
logger.info(`⏱️ OpenAI account ${accountId} (${accountType}) is temporarily unavailable`)
return false
}
return true
} else if (accountType === 'openai-responses') {
const account = await openaiResponsesAccountService.getAccount(accountId)
@@ -541,7 +579,20 @@ class UnifiedOpenAIScheduler {
// 检查并清除过期的限流状态
const isRateLimitCleared =
await openaiResponsesAccountService.checkAndClearRateLimit(accountId)
return !this._isRateLimited(account.rateLimitStatus) || isRateLimitCleared
if (this._isRateLimited(account.rateLimitStatus) && !isRateLimitCleared) {
return false
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
accountId,
accountType
)
if (isTempUnavailable) {
logger.info(`⏱️ OpenAI account ${accountId} (${accountType}) is temporarily unavailable`)
return false
}
return true
}
return false
} catch (error) {
@@ -572,7 +623,7 @@ class UnifiedOpenAIScheduler {
const client = redis.getClientSafe()
const mappingData = JSON.stringify({ accountId, accountType })
// 依据配置设置TTL小时
const appConfig = require('../../config/config')
const appConfig = require('../../../config/config')
const ttlHours = appConfig.session?.stickyTtlHours || 1
const ttlSeconds = Math.max(1, Math.floor(ttlHours * 60 * 60))
await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, ttlSeconds, mappingData)
@@ -598,7 +649,7 @@ class UnifiedOpenAIScheduler {
return true
}
const appConfig = require('../../config/config')
const appConfig = require('../../../config/config')
const ttlHours = appConfig.session?.stickyTtlHours || 1
const renewalThresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0
if (!renewalThresholdMinutes) {
@@ -849,6 +900,17 @@ class UnifiedOpenAIScheduler {
continue
}
const isTempUnavailable = await upstreamErrorHelper.isTempUnavailable(
account.id,
accountType
)
if (isTempUnavailable) {
logger.debug(
`⏭️ Skipping group member ${accountType} account ${account.name} - temporarily unavailable`
)
continue
}
// 检查token是否过期仅对 OpenAI OAuth 账户检查)
if (accountType === 'openai') {
const isExpired = openaiAccountService.isTokenExpired(account)

View File

@@ -7,13 +7,11 @@ const fs = require('fs')
const os = require('os')
// 安全的 JSON 序列化函数,处理循环引用和特殊字符
const safeStringify = (obj, maxDepth = 3, fullDepth = false) => {
const safeStringify = (obj, maxDepth = Infinity) => {
const seen = new WeakSet()
// 如果是fullDepth模式增加深度限制
const actualMaxDepth = fullDepth ? 10 : maxDepth
const replacer = (key, value, depth = 0) => {
if (depth > actualMaxDepth) {
if (depth > maxDepth) {
return '[Max Depth Reached]'
}
@@ -21,18 +19,13 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => {
if (typeof value === 'string') {
try {
// 移除或转义可能导致JSON解析错误的字符
let cleanValue = value
const cleanValue = value
// eslint-disable-next-line no-control-regex
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') // 移除控制字符
.replace(/[\uD800-\uDFFF]/g, '') // 移除孤立的代理对字符
// eslint-disable-next-line no-control-regex
.replace(/\u0000/g, '') // 移除NUL字节
// 如果字符串过长,截断并添加省略号
if (cleanValue.length > 1000) {
cleanValue = `${cleanValue.substring(0, 997)}...`
}
return cleanValue
} catch (error) {
return '[Invalid String Data]'
@@ -77,7 +70,37 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => {
try {
const processed = replacer('', obj)
return JSON.stringify(processed)
const result = JSON.stringify(processed)
// 体积保护: 超过 50KB 时对大字段做截断,保留顶层结构
if (result.length > 50000 && processed && typeof processed === 'object') {
const truncated = { ...processed, _truncated: true, _totalChars: result.length }
// 第一轮: 截断单个大字段
for (const [k, v] of Object.entries(truncated)) {
if (k.startsWith('_')) {
continue
}
const fieldStr = typeof v === 'string' ? v : JSON.stringify(v)
if (fieldStr && fieldStr.length > 10000) {
truncated[k] = `${fieldStr.substring(0, 10000)}...[truncated]`
}
}
// 第二轮: 如果总长度仍超 50KB逐字段缩减到 2KB
let secondResult = JSON.stringify(truncated)
if (secondResult.length > 50000) {
for (const [k, v] of Object.entries(truncated)) {
if (k.startsWith('_')) {
continue
}
const fieldStr = typeof v === 'string' ? v : JSON.stringify(v)
if (fieldStr && fieldStr.length > 2000) {
truncated[k] = `${fieldStr.substring(0, 2000)}...[truncated]`
}
}
secondResult = JSON.stringify(truncated)
}
return secondResult
}
return result
} catch (error) {
// 如果JSON.stringify仍然失败使用更保守的方法
try {
@@ -93,50 +116,64 @@ const safeStringify = (obj, maxDepth = 3, fullDepth = false) => {
}
}
// 📝 增强的日志格式
const createLogFormat = (colorize = false) => {
const formats = [
// 控制台不显示的 metadata 字段(已在 message 中或低价值)
const CONSOLE_SKIP_KEYS = new Set(['type', 'level', 'message', 'timestamp', 'stack'])
// 控制台格式: 树形展示 metadata
const createConsoleFormat = () =>
winston.format.combine(
winston.format.timestamp({ format: () => formatDateWithTimezone(new Date(), false) }),
winston.format.errors({ stack: true })
// 移除 winston.format.metadata() 来避免自动包装
]
if (colorize) {
formats.push(winston.format.colorize())
}
formats.push(
winston.format.errors({ stack: true }),
winston.format.colorize(),
winston.format.printf(({ level, message, timestamp, stack, ...rest }) => {
const emoji = {
error: '',
warn: '⚠️ ',
info: ' ',
debug: '🐛',
verbose: '📝'
// 时间戳只取时分秒
const shortTime = timestamp ? timestamp.split(' ').pop() : ''
let logMessage = `${shortTime} ${message}`
// 收集要显示的 metadata
const entries = Object.entries(rest).filter(([k]) => !CONSOLE_SKIP_KEYS.has(k))
if (entries.length > 0) {
const indent = ' '.repeat(shortTime.length + 1)
entries.forEach(([key, value], i) => {
const isLast = i === entries.length - 1
const branch = isLast ? '└─' : '├─'
const displayValue =
value !== null && typeof value === 'object' ? safeStringify(value) : String(value)
logMessage += `\n${indent}${branch} ${key}: ${displayValue}`
})
}
let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`
// 直接处理额外数据不需要metadata包装
const additionalData = { ...rest }
delete additionalData.level
delete additionalData.message
delete additionalData.timestamp
delete additionalData.stack
if (Object.keys(additionalData).length > 0) {
logMessage += ` | ${safeStringify(additionalData)}`
if (stack) {
logMessage += `\n${stack}`
}
return stack ? `${logMessage}\n${stack}` : logMessage
return logMessage
})
)
return winston.format.combine(...formats)
}
// 文件格式: NDJSON完整结构化数据
const createFileFormat = () =>
winston.format.combine(
winston.format.timestamp({ format: () => formatDateWithTimezone(new Date(), false) }),
winston.format.errors({ stack: true }),
winston.format.printf(({ level, message, timestamp, stack, ...rest }) => {
const entry = { ts: timestamp, lvl: level, msg: message }
// 合并所有 metadata
for (const [k, v] of Object.entries(rest)) {
if (k !== 'level' && k !== 'message' && k !== 'timestamp' && k !== 'stack') {
entry[k] = v
}
}
if (stack) {
entry.stack = stack
}
return safeStringify(entry)
})
)
const logFormat = createLogFormat(false)
const consoleFormat = createLogFormat(true)
const fileFormat = createFileFormat()
const consoleFormat = createConsoleFormat()
const isTestEnv = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID
// 📁 确保日志目录存在并设置权限
@@ -153,7 +190,7 @@ const createRotateTransport = (filename, level = null) => {
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles,
auditFile: path.join(config.logging.dirname, `.${filename.replace('%DATE%', 'audit')}.json`),
format: logFormat
format: fileFormat
})
if (level) {
@@ -184,7 +221,7 @@ const errorFileTransport = createRotateTransport('claude-relay-error-%DATE%.log'
// 🔒 创建专门的安全日志记录器
const securityLogger = winston.createLogger({
level: 'warn',
format: logFormat,
format: fileFormat,
transports: [createRotateTransport('claude-relay-security-%DATE%.log', 'warn')],
silent: false
})
@@ -207,7 +244,7 @@ const authDetailLogger = winston.createLogger({
// 🌟 增强的 Winston logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || config.logging.level,
format: logFormat,
format: fileFormat,
transports: [
// 📄 文件输出
dailyRotateFileTransport,
@@ -225,7 +262,7 @@ const logger = winston.createLogger({
exceptionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'exceptions.log'),
format: logFormat,
format: fileFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
@@ -238,7 +275,7 @@ const logger = winston.createLogger({
rejectionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'rejections.log'),
format: logFormat,
format: fileFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),

View File

@@ -1,4 +1,11 @@
const crypto = require('crypto')
const { mapToErrorCode } = require('./errorSanitizer')
// 将原始错误信息映射为安全的标准错误码消息
const sanitizeErrorMsg = (msg) => {
const mapped = mapToErrorCode({ message: msg }, { logOriginal: false })
return `[${mapped.code}] ${mapped.message}`
}
/**
* 生成随机十六进制字符串
@@ -92,7 +99,8 @@ async function sendStreamTestRequest(options) {
payload = createClaudeTestPayload('claude-sonnet-4-5-20250929', { stream: true }),
proxyAgent = null,
timeout = 30000,
extraHeaders = {}
extraHeaders = {},
sanitize = false
} = options
const sendSSE = (type, data = {}) => {
@@ -166,17 +174,17 @@ async function sendStreamTestRequest(options) {
let errorMsg = `API Error: ${response.status}`
try {
const json = JSON.parse(errorData)
errorMsg = json.message || json.error?.message || json.error || errorMsg
errorMsg = extractErrorMessage(json, errorMsg)
} catch {
if (errorData.length < 200) {
errorMsg = errorData || errorMsg
}
}
endTest(false, errorMsg)
endTest(false, sanitize ? sanitizeErrorMsg(errorMsg) : errorMsg)
resolve()
})
response.data.on('error', (err) => {
endTest(false, err.message)
endTest(false, sanitize ? sanitizeErrorMsg(err.message) : err.message)
resolve()
})
})
@@ -270,7 +278,7 @@ function createGeminiTestPayload(_model = 'gemini-2.5-pro', options = {}) {
* @returns {object} 测试请求体
*/
function createOpenAITestPayload(model = 'gpt-5', options = {}) {
const { prompt = 'hi', maxTokens = 100 } = options
const { prompt = 'hi', maxTokens = 100, stream = true } = options
return {
model,
input: [
@@ -280,15 +288,77 @@ function createOpenAITestPayload(model = 'gpt-5', options = {}) {
}
],
max_output_tokens: maxTokens,
stream: true
stream
}
}
/**
* 生成 Chat Completions 测试请求体(用于 Azure OpenAI 等 Chat Completions 端点)
* @param {string} model - 模型名称
* @param {object} options - 可选配置
* @param {string} options.prompt - 自定义提示词(默认 'hi'
* @param {number} options.maxTokens - 最大输出 token默认 100
* @returns {object} 测试请求体
*/
function createChatCompletionsTestPayload(model = 'gpt-4o-mini', options = {}) {
const { prompt = 'hi', maxTokens = 100 } = options
return {
model,
messages: [
{
role: 'user',
content: prompt
}
],
max_tokens: maxTokens
}
}
/**
* 从各种格式的错误响应中提取可读错误信息
* 支持格式: {message}, {error:{message}}, {msg:{error:{message}}}, {error:"string"} 等
* @param {object} json - 解析后的 JSON 错误响应
* @param {string} fallback - 提取失败时的回退信息
* @returns {string} 错误信息
*/
function extractErrorMessage(json, fallback) {
if (!json || typeof json !== 'object') {
return fallback
}
// 直接 message
if (json.message && typeof json.message === 'string') {
return json.message
}
// {error: {message: "..."}}
if (json.error?.message) {
return json.error.message
}
// {msg: {error: {message: "..."}}} (relay 包装格式)
if (json.msg?.error?.message) {
return json.msg.error.message
}
if (json.msg?.message) {
return json.msg.message
}
// {error: "string"}
if (typeof json.error === 'string') {
return json.error
}
// {msg: "string"}
if (typeof json.msg === 'string') {
return json.msg
}
return fallback
}
module.exports = {
randomHex,
generateSessionString,
createClaudeTestPayload,
createGeminiTestPayload,
createOpenAITestPayload,
createChatCompletionsTestPayload,
extractErrorMessage,
sanitizeErrorMsg,
sendStreamTestRequest
}

View File

@@ -0,0 +1,255 @@
const logger = require('./logger')
const TEMP_UNAVAILABLE_PREFIX = 'temp_unavailable'
// 默认 TTL
const DEFAULT_TTL = {
server_error: 300, // 5xx: 5分钟
overload: 600, // 529: 10分钟
auth_error: 1800, // 401/403: 30分钟
timeout: 300, // 504/网络超时: 5分钟
rate_limit: 300 // 429: 5分钟优先使用响应头解析值
}
// 延迟加载配置,避免循环依赖
let _configCache = null
const getConfig = () => {
if (!_configCache) {
try {
_configCache = require('../../config/config')
} catch {
_configCache = {}
}
}
return _configCache
}
const getTtlConfig = () => {
const config = getConfig()
return {
server_error: config.upstreamError?.serverErrorTtlSeconds ?? DEFAULT_TTL.server_error,
overload: config.upstreamError?.overloadTtlSeconds ?? DEFAULT_TTL.overload,
auth_error: config.upstreamError?.authErrorTtlSeconds ?? DEFAULT_TTL.auth_error,
timeout: config.upstreamError?.timeoutTtlSeconds ?? DEFAULT_TTL.timeout,
rate_limit: DEFAULT_TTL.rate_limit
}
}
// 延迟加载 redis避免循环依赖
let _redis = null
const getRedis = () => {
if (!_redis) {
_redis = require('../models/redis')
}
return _redis
}
// 根据 HTTP 状态码分类错误类型
const classifyError = (statusCode) => {
if (statusCode === 529) {
return 'overload'
}
if (statusCode === 504) {
return 'timeout'
}
if (statusCode === 401 || statusCode === 403) {
return 'auth_error'
}
if (statusCode === 429) {
return 'rate_limit'
}
if (statusCode >= 500) {
return 'server_error'
}
return null
}
// 解析 429 响应头中的重置时间(返回秒数)
const parseRetryAfter = (headers) => {
if (!headers) {
return null
}
// 标准 Retry-After 头(秒数或 HTTP 日期)
const retryAfter = headers['retry-after']
if (retryAfter) {
const seconds = parseInt(retryAfter, 10)
if (!isNaN(seconds) && seconds > 0) {
return seconds
}
const date = new Date(retryAfter)
if (!isNaN(date.getTime())) {
const diff = Math.ceil((date.getTime() - Date.now()) / 1000)
if (diff > 0) {
return diff
}
}
}
// Anthropic 限流重置头ISO 时间)
const anthropicReset = headers['anthropic-ratelimit-unified-reset']
if (anthropicReset) {
const date = new Date(anthropicReset)
if (!isNaN(date.getTime())) {
const diff = Math.ceil((date.getTime() - Date.now()) / 1000)
if (diff > 0) {
return diff
}
}
}
// OpenAI/Codex 限流重置头
const xReset = headers['x-ratelimit-reset-requests'] || headers['x-codex-ratelimit-reset']
if (xReset) {
const seconds = parseInt(xReset, 10)
if (!isNaN(seconds) && seconds > 0) {
return seconds
}
}
return null
}
// 标记账户为临时不可用
const markTempUnavailable = async (accountId, accountType, statusCode, customTtl = null) => {
try {
const errorType = classifyError(statusCode)
if (!errorType) {
return { success: false, reason: 'not_a_pausable_error' }
}
const ttlConfig = getTtlConfig()
const ttlSeconds = customTtl ?? ttlConfig[errorType]
const redis = getRedis()
const client = redis.getClientSafe()
const key = `${TEMP_UNAVAILABLE_PREFIX}:${accountType}:${accountId}`
await client.setex(
key,
ttlSeconds,
JSON.stringify({
statusCode,
errorType,
markedAt: new Date().toISOString()
})
)
logger.warn(
`⏱️ [UpstreamError] Account ${accountId} (${accountType}) marked temporarily unavailable for ${ttlSeconds}s (${statusCode} ${errorType})`
)
return { success: true, ttlSeconds, errorType }
} catch (error) {
logger.error(
`❌ [UpstreamError] Failed to mark account ${accountId} temporarily unavailable:`,
error
)
return { success: false }
}
}
// 检查账户是否临时不可用
const isTempUnavailable = async (accountId, accountType) => {
try {
const redis = getRedis()
const client = redis.getClientSafe()
const key = `${TEMP_UNAVAILABLE_PREFIX}:${accountType}:${accountId}`
return (await client.exists(key)) === 1
} catch (error) {
logger.error(
`❌ [UpstreamError] Failed to check temp unavailable status for ${accountId}:`,
error
)
return false
}
}
// 清除临时不可用状态
const clearTempUnavailable = async (accountId, accountType) => {
try {
const redis = getRedis()
const client = redis.getClientSafe()
const key = `${TEMP_UNAVAILABLE_PREFIX}:${accountType}:${accountId}`
await client.del(key)
} catch (error) {
logger.error(`❌ [UpstreamError] Failed to clear temp unavailable for ${accountId}:`, error)
}
}
// 批量查询所有临时不可用状态(用于前端展示)
const getAllTempUnavailable = async () => {
try {
const redis = getRedis()
const client = redis.getClientSafe()
const pattern = `${TEMP_UNAVAILABLE_PREFIX}:*`
const keys = await client.keys(pattern)
if (!keys.length) {
return {}
}
const pipeline = client.pipeline()
for (const key of keys) {
pipeline.get(key)
pipeline.ttl(key)
}
const results = await pipeline.exec()
const statuses = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// key format: temp_unavailable:{accountType}:{accountId}
const parts = key.split(':')
const accountType = parts[1]
const accountId = parts.slice(2).join(':')
const [getErr, value] = results[i * 2]
const [ttlErr, ttl] = results[i * 2 + 1]
if (getErr || ttlErr || !value) {
continue
}
try {
const data = JSON.parse(value)
const compositeKey = `${accountType}:${accountId}`
statuses[compositeKey] = {
accountId,
accountType,
statusCode: data.statusCode,
errorType: data.errorType,
markedAt: data.markedAt,
ttl: ttl > 0 ? ttl : 0
}
} catch {
// ignore parse errors
}
}
return statuses
} catch (error) {
logger.error('❌ [UpstreamError] Failed to get all temp unavailable statuses:', error)
return {}
}
}
// 清洗上游错误数据,去除内部路由标识(如 [codex/codex]
const sanitizeErrorForClient = (errorData) => {
if (!errorData || typeof errorData !== 'object') {
return errorData
}
try {
const str = JSON.stringify(errorData)
const cleaned = str.replace(/ \[[^\]\/]+\/[^\]]+\]/g, '')
return JSON.parse(cleaned)
} catch {
return errorData
}
}
module.exports = {
markTempUnavailable,
isTempUnavailable,
clearTempUnavailable,
getAllTempUnavailable,
classifyError,
parseRetryAfter,
sanitizeErrorForClient,
TEMP_UNAVAILABLE_PREFIX
}