From ca79e08c815cd05a6a2f8cb96b9e595ffc579651 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Sep 2025 14:17:38 +0000 Subject: [PATCH 1/2] chore: sync VERSION file with release v1.1.134 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 64dbc165..eef0324e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.133 +1.1.134 From 908e323db0ffeaf92cf0aca851ff468db26cdf53 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Tue, 9 Sep 2025 00:46:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E4=B8=BA=E6=99=AE=E9=80=9AClaude?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E6=B7=BB=E5=8A=A0529=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加可配置的529错误处理机制,通过CLAUDE_OVERLOAD_HANDLING_MINUTES环境变量控制 - 支持流式和非流式请求的529错误检测 - 自动标记过载账户并在指定时间后恢复 - 成功请求后自动清除过载状态 - 默认禁用,需手动配置启用(0表示禁用,>0表示过载持续分钟数) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .env.example | 4 + config/config.example.js | 9 ++- src/services/claudeAccountService.js | 102 +++++++++++++++++++++++++ src/services/claudeRelayService.js | 65 ++++++++++++++++ src/services/unifiedClaudeScheduler.js | 5 +- 5 files changed, 183 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index f3791b06..fa640f15 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,10 @@ CLAUDE_API_URL=https://api.anthropic.com/v1/messages 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 +# 🚫 529错误处理配置 +# 启用529错误处理,0表示禁用,>0表示过载状态持续时间(分钟) +CLAUDE_OVERLOAD_HANDLING_MINUTES=0 + # 🌐 代理配置 DEFAULT_PROXY_TIMEOUT=600000 MAX_PROXY_RETRIES=3 diff --git a/config/config.example.js b/config/config.example.js index 5fae5fde..3f8d71d7 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -46,7 +46,14 @@ const config = { apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01', betaHeader: 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配置 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 336e8828..9219a7f5 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -2137,6 +2137,108 @@ class ClaudeAccountService { 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() diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index f4e03334..050da9a2 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -208,6 +208,24 @@ class ClaudeRelayService { ) 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状态码 else if (response.statusCode >= 500 && response.statusCode < 600) { logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`) @@ -296,6 +314,19 @@ class ClaudeRelayService { 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 if ( clientHeaders && @@ -1002,6 +1033,27 @@ class ClaudeRelayService { `🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked` ) 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) { logger.warn( `🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}` @@ -1327,6 +1379,19 @@ class ClaudeRelayService { 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(流式请求) if ( clientHeaders && diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 7bceb040..5af5d726 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -528,7 +528,10 @@ class UnifiedClaudeScheduler { 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') { const account = await claudeConsoleAccountService.getAccount(accountId) if (!account || !account.isActive) {