mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 为普通Claude账户添加529错误处理功能
- 添加可配置的529错误处理机制,通过CLAUDE_OVERLOAD_HANDLING_MINUTES环境变量控制 - 支持流式和非流式请求的529错误检测 - 自动标记过载账户并在指定时间后恢复 - 成功请求后自动清除过载状态 - 默认禁用,需手动配置启用(0表示禁用,>0表示过载持续分钟数) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,10 @@ CLAUDE_API_URL=https://api.anthropic.com/v1/messages
|
|||||||
CLAUDE_API_VERSION=2023-06-01
|
CLAUDE_API_VERSION=2023-06-01
|
||||||
CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14
|
CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14
|
||||||
|
|
||||||
|
# 🚫 529错误处理配置
|
||||||
|
# 启用529错误处理,0表示禁用,>0表示过载状态持续时间(分钟)
|
||||||
|
CLAUDE_OVERLOAD_HANDLING_MINUTES=0
|
||||||
|
|
||||||
# 🌐 代理配置
|
# 🌐 代理配置
|
||||||
DEFAULT_PROXY_TIMEOUT=600000
|
DEFAULT_PROXY_TIMEOUT=600000
|
||||||
MAX_PROXY_RETRIES=3
|
MAX_PROXY_RETRIES=3
|
||||||
|
|||||||
@@ -46,7 +46,14 @@ const config = {
|
|||||||
apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01',
|
apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01',
|
||||||
betaHeader:
|
betaHeader:
|
||||||
process.env.CLAUDE_BETA_HEADER ||
|
process.env.CLAUDE_BETA_HEADER ||
|
||||||
'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
|
'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14',
|
||||||
|
overloadHandling: {
|
||||||
|
enabled: (() => {
|
||||||
|
const minutes = parseInt(process.env.CLAUDE_OVERLOAD_HANDLING_MINUTES) || 0
|
||||||
|
// 验证配置值:限制在0-1440分钟(24小时)内
|
||||||
|
return Math.max(0, Math.min(minutes, 1440))
|
||||||
|
})()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ☁️ Bedrock API配置
|
// ☁️ Bedrock API配置
|
||||||
|
|||||||
@@ -2137,6 +2137,108 @@ class ClaudeAccountService {
|
|||||||
logger.error(`❌ Failed to update session window status for account ${accountId}:`, error)
|
logger.error(`❌ Failed to update session window status for account ${accountId}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚫 标记账号为过载状态(529错误)
|
||||||
|
async markAccountOverloaded(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
|
if (!accountData) {
|
||||||
|
throw new Error('Account not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置的过载处理时间(分钟)
|
||||||
|
const overloadMinutes = config.overloadHandling?.enabled || 0
|
||||||
|
|
||||||
|
if (overloadMinutes === 0) {
|
||||||
|
logger.info('⏭️ 529 error handling is disabled')
|
||||||
|
return { success: false, error: '529 error handling is disabled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const overloadKey = `account:overload:${accountId}`
|
||||||
|
const ttl = overloadMinutes * 60 // 转换为秒
|
||||||
|
|
||||||
|
await redis.setex(
|
||||||
|
overloadKey,
|
||||||
|
ttl,
|
||||||
|
JSON.stringify({
|
||||||
|
accountId,
|
||||||
|
accountName: accountData.name,
|
||||||
|
markedAt: new Date().toISOString(),
|
||||||
|
expiresAt: new Date(Date.now() + ttl * 1000).toISOString()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`🚫 Account ${accountData.name} (${accountId}) marked as overloaded for ${overloadMinutes} minutes`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在账号上记录最后一次529错误
|
||||||
|
const updates = {
|
||||||
|
lastOverloadAt: new Date().toISOString(),
|
||||||
|
errorMessage: `529错误 - 过载${overloadMinutes}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAccountData = { ...accountData, ...updates }
|
||||||
|
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||||
|
|
||||||
|
return { success: true, accountName: accountData.name, duration: overloadMinutes }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to mark account as overloaded: ${accountId}`, error)
|
||||||
|
// 不抛出错误,避免影响主请求流程
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 检查账号是否过载
|
||||||
|
async isAccountOverloaded(accountId) {
|
||||||
|
try {
|
||||||
|
// 如果529处理未启用,直接返回false
|
||||||
|
const overloadMinutes = config.overloadHandling?.enabled || 0
|
||||||
|
if (overloadMinutes === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const overloadKey = `account:overload:${accountId}`
|
||||||
|
const overloadData = await redis.get(overloadKey)
|
||||||
|
|
||||||
|
if (overloadData) {
|
||||||
|
// 账号处于过载状态
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账号未过载
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to check if account is overloaded: ${accountId}`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 移除账号的过载状态
|
||||||
|
async removeAccountOverload(accountId) {
|
||||||
|
try {
|
||||||
|
const accountData = await redis.getClaudeAccount(accountId)
|
||||||
|
if (!accountData) {
|
||||||
|
throw new Error('Account not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const overloadKey = `account:overload:${accountId}`
|
||||||
|
await redis.del(overloadKey)
|
||||||
|
|
||||||
|
logger.info(`✅ Account ${accountData.name} (${accountId}) overload status removed`)
|
||||||
|
|
||||||
|
// 清理账号上的错误信息
|
||||||
|
if (accountData.errorMessage && accountData.errorMessage.includes('529错误')) {
|
||||||
|
const updatedAccountData = { ...accountData }
|
||||||
|
delete updatedAccountData.errorMessage
|
||||||
|
delete updatedAccountData.lastOverloadAt
|
||||||
|
await redis.setClaudeAccount(accountId, updatedAccountData)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to remove overload status for account: ${accountId}`, error)
|
||||||
|
// 不抛出错误,移除过载状态失败不应该影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ClaudeAccountService()
|
module.exports = new ClaudeAccountService()
|
||||||
|
|||||||
@@ -208,6 +208,24 @@ class ClaudeRelayService {
|
|||||||
)
|
)
|
||||||
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||||
}
|
}
|
||||||
|
// 检查是否为529状态码(服务过载)
|
||||||
|
else if (response.statusCode === 529) {
|
||||||
|
logger.warn(`🚫 Overload error (529) detected for account ${accountId}`)
|
||||||
|
|
||||||
|
// 检查是否启用了529错误处理
|
||||||
|
if (config.claude.overloadHandling.enabled > 0) {
|
||||||
|
try {
|
||||||
|
await claudeAccountService.markAccountOverloaded(accountId)
|
||||||
|
logger.info(
|
||||||
|
`🚫 Account ${accountId} marked as overloaded for ${config.claude.overloadHandling.enabled} minutes`
|
||||||
|
)
|
||||||
|
} catch (overloadError) {
|
||||||
|
logger.error(`❌ Failed to mark account as overloaded: ${accountId}`, overloadError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`🚫 529 error handling is disabled, skipping account overload marking`)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 检查是否为5xx状态码
|
// 检查是否为5xx状态码
|
||||||
else if (response.statusCode >= 500 && response.statusCode < 600) {
|
else if (response.statusCode >= 500 && response.statusCode < 600) {
|
||||||
logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`)
|
logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`)
|
||||||
@@ -296,6 +314,19 @@ class ClaudeRelayService {
|
|||||||
await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType)
|
await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果请求成功,检查并移除过载状态
|
||||||
|
try {
|
||||||
|
const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId)
|
||||||
|
if (isOverloaded) {
|
||||||
|
await claudeAccountService.removeAccountOverload(accountId)
|
||||||
|
}
|
||||||
|
} catch (overloadError) {
|
||||||
|
logger.error(
|
||||||
|
`❌ Failed to check/remove overload status for account ${accountId}:`,
|
||||||
|
overloadError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 只有真实的 Claude Code 请求才更新 headers
|
// 只有真实的 Claude Code 请求才更新 headers
|
||||||
if (
|
if (
|
||||||
clientHeaders &&
|
clientHeaders &&
|
||||||
@@ -1002,6 +1033,27 @@ class ClaudeRelayService {
|
|||||||
`🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked`
|
`🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked`
|
||||||
)
|
)
|
||||||
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||||
|
} else if (res.statusCode === 529) {
|
||||||
|
logger.warn(`🚫 [Stream] Overload error (529) detected for account ${accountId}`)
|
||||||
|
|
||||||
|
// 检查是否启用了529错误处理
|
||||||
|
if (config.claude.overloadHandling.enabled > 0) {
|
||||||
|
try {
|
||||||
|
await claudeAccountService.markAccountOverloaded(accountId)
|
||||||
|
logger.info(
|
||||||
|
`🚫 [Stream] Account ${accountId} marked as overloaded for ${config.claude.overloadHandling.enabled} minutes`
|
||||||
|
)
|
||||||
|
} catch (overloadError) {
|
||||||
|
logger.error(
|
||||||
|
`❌ [Stream] Failed to mark account as overloaded: ${accountId}`,
|
||||||
|
overloadError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`🚫 [Stream] 529 error handling is disabled, skipping account overload marking`
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (res.statusCode >= 500 && res.statusCode < 600) {
|
} else if (res.statusCode >= 500 && res.statusCode < 600) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
`🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}`
|
||||||
@@ -1327,6 +1379,19 @@ class ClaudeRelayService {
|
|||||||
await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType)
|
await unifiedClaudeScheduler.removeAccountRateLimit(accountId, accountType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果流式请求成功,检查并移除过载状态
|
||||||
|
try {
|
||||||
|
const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId)
|
||||||
|
if (isOverloaded) {
|
||||||
|
await claudeAccountService.removeAccountOverload(accountId)
|
||||||
|
}
|
||||||
|
} catch (overloadError) {
|
||||||
|
logger.error(
|
||||||
|
`❌ [Stream] Failed to check/remove overload status for account ${accountId}:`,
|
||||||
|
overloadError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 只有真实的 Claude Code 请求才更新 headers(流式请求)
|
// 只有真实的 Claude Code 请求才更新 headers(流式请求)
|
||||||
if (
|
if (
|
||||||
clientHeaders &&
|
clientHeaders &&
|
||||||
|
|||||||
@@ -528,7 +528,10 @@ class UnifiedClaudeScheduler {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return !(await claudeAccountService.isAccountRateLimited(accountId))
|
// 检查是否限流或过载
|
||||||
|
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId)
|
||||||
|
const isOverloaded = await claudeAccountService.isAccountOverloaded(accountId)
|
||||||
|
return !isRateLimited && !isOverloaded
|
||||||
} else if (accountType === 'claude-console') {
|
} else if (accountType === 'claude-console') {
|
||||||
const account = await claudeConsoleAccountService.getAccount(accountId)
|
const account = await claudeConsoleAccountService.getAccount(accountId)
|
||||||
if (!account || !account.isActive) {
|
if (!account || !account.isActive) {
|
||||||
|
|||||||
Reference in New Issue
Block a user