From 1ff14e38cb3d2feaadba4eb6ae4a66b2c42bdeb0 Mon Sep 17 00:00:00 2001 From: iaineng Date: Thu, 4 Sep 2025 00:27:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Claude=E8=B4=A6?= =?UTF-8?q?=E6=88=B7403=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C?= =?UTF-8?q?=E5=B0=81=E7=A6=81=E7=8A=B6=E6=80=81=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增Claude账户403错误自动检测和处理机制 - 区分Claude账户401未授权和403封禁两种错误状态 - 支持非流式和流式请求中的401/403错误处理 - 优化Claude账户错误处理代码,减少重复逻辑 - 支持前端显示不同的Claude账户错误状态和颜色 - 完善Claude账户异常Webhook通知错误码区分 --- src/services/claudeAccountService.js | 54 +++++++++++++++++++++----- src/services/claudeRelayService.js | 35 ++++++++++++++++- src/services/unifiedClaudeScheduler.js | 26 +++++++++++++ src/utils/webhookNotifier.js | 1 + 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 08606550..1c37262b 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -1695,9 +1695,31 @@ class ClaudeAccountService { } } - // 🚫 标记账户为未授权状态(401错误) - async markAccountUnauthorized(accountId, sessionHash = null) { + // 🚫 通用的账户错误标记方法 + async markAccountError(accountId, errorType, sessionHash = null) { + const ERROR_CONFIG = { + unauthorized: { + status: 'unauthorized', + errorMessage: 'Account unauthorized (401 errors detected)', + timestampField: 'unauthorizedAt', + errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED', + logMessage: 'unauthorized' + }, + blocked: { + status: 'blocked', + errorMessage: 'Account blocked (403 error detected - account may be suspended by Claude)', + timestampField: 'blockedAt', + errorCode: 'CLAUDE_OAUTH_BLOCKED', + logMessage: 'blocked' + } + } + try { + const errorConfig = ERROR_CONFIG[errorType] + if (!errorConfig) { + throw new Error(`Unsupported error type: ${errorType}`) + } + const accountData = await redis.getClaudeAccount(accountId) if (!accountData || Object.keys(accountData).length === 0) { throw new Error('Account not found') @@ -1705,10 +1727,10 @@ class ClaudeAccountService { // 更新账户状态 const updatedAccountData = { ...accountData } - updatedAccountData.status = 'unauthorized' + updatedAccountData.status = errorConfig.status updatedAccountData.schedulable = 'false' // 设置为不可调度 - updatedAccountData.errorMessage = 'Account unauthorized (401 errors detected)' - updatedAccountData.unauthorizedAt = new Date().toISOString() + updatedAccountData.errorMessage = errorConfig.errorMessage + updatedAccountData[errorConfig.timestampField] = new Date().toISOString() // 保存更新后的账户数据 await redis.setClaudeAccount(accountId, updatedAccountData) @@ -1720,7 +1742,7 @@ class ClaudeAccountService { } logger.warn( - `⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling` + `⚠️ Account ${accountData.name} (${accountId}) marked as ${errorConfig.logMessage} and disabled for scheduling` ) // 发送Webhook通知 @@ -1730,9 +1752,10 @@ class ClaudeAccountService { accountId, accountName: accountData.name, platform: 'claude-oauth', - status: 'unauthorized', - errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED', - reason: 'Account unauthorized (401 errors detected)' + status: errorConfig.status, + errorCode: errorConfig.errorCode, + reason: errorConfig.errorMessage, + timestamp: getISOStringWithTimezone(new Date()) }) } catch (webhookError) { logger.error('Failed to send webhook notification:', webhookError) @@ -1740,11 +1763,21 @@ class ClaudeAccountService { return { success: true } } catch (error) { - logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error) + logger.error(`❌ Failed to mark account ${accountId} as ${errorType}:`, error) throw error } } + // 🚫 标记账户为未授权状态(401错误) + async markAccountUnauthorized(accountId, sessionHash = null) { + return this.markAccountError(accountId, 'unauthorized', sessionHash) + } + + // 🚫 标记账户为被封锁状态(403错误) + async markAccountBlocked(accountId, sessionHash = null) { + return this.markAccountError(accountId, 'blocked', sessionHash) + } + // 🔄 重置账户所有异常状态 async resetAccountStatus(accountId) { try { @@ -1769,6 +1802,7 @@ class ClaudeAccountService { // 清除错误相关字段 delete updatedAccountData.errorMessage delete updatedAccountData.unauthorizedAt + delete updatedAccountData.blockedAt delete updatedAccountData.rateLimitedAt delete updatedAccountData.rateLimitStatus delete updatedAccountData.rateLimitEndAt diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 4a0f48da..19677023 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -198,6 +198,13 @@ class ClaudeRelayService { ) } } + // 检查是否为403状态码(禁止访问) + else if (response.statusCode === 403) { + logger.error( + `🚫 Forbidden error (403) detected for account ${accountId}, marking as blocked` + ) + await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash) + } // 检查是否为5xx状态码 else if (response.statusCode >= 500 && response.statusCode < 600) { logger.warn(`🔥 Server error (${response.statusCode}) detected for account ${accountId}`) @@ -953,8 +960,32 @@ class ClaudeRelayService { if (res.statusCode !== 200) { // 将错误处理逻辑封装在一个异步函数中 const handleErrorResponse = async () => { - // 增加对5xx错误的处理 - if (res.statusCode >= 500 && res.statusCode < 600) { + if (res.statusCode === 401) { + logger.warn(`🔐 [Stream] Unauthorized error (401) detected for account ${accountId}`) + + await this.recordUnauthorizedError(accountId) + + const errorCount = await this.getUnauthorizedErrorCount(accountId) + logger.info( + `🔐 [Stream] Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes` + ) + + if (errorCount >= 1) { + logger.error( + `❌ [Stream] Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized` + ) + await unifiedClaudeScheduler.markAccountUnauthorized( + accountId, + accountType, + sessionHash + ) + } + } else if (res.statusCode === 403) { + logger.error( + `🚫 [Stream] Forbidden error (403) detected for account ${accountId}, marking as blocked` + ) + await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash) + } else if (res.statusCode >= 500 && res.statusCode < 600) { logger.warn( `🔥 [Stream] Server error (${res.statusCode}) detected for account ${accountId}` ) diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index c83676a2..34e61a81 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -636,6 +636,32 @@ class UnifiedClaudeScheduler { } } + // 🚫 标记账户为被封锁状态(403错误) + async markAccountBlocked(accountId, accountType, sessionHash = null) { + try { + // 只处理claude-official类型的账户,不处理claude-console和gemini + if (accountType === 'claude-official') { + await claudeAccountService.markAccountBlocked(accountId, sessionHash) + + // 删除会话映射 + if (sessionHash) { + await this._deleteSessionMapping(sessionHash) + } + + logger.warn(`🚫 Account ${accountId} marked as blocked due to 403 error`) + } else { + logger.info( + `ℹ️ Skipping blocked marking for non-Claude OAuth account: ${accountId} (${accountType})` + ) + } + + return { success: true } + } catch (error) { + logger.error(`❌ Failed to mark account as blocked: ${accountId} (${accountType})`, error) + throw error + } + } + // 🚫 标记Claude Console账户为封锁状态(模型不支持) async blockConsoleAccount(accountId, reason) { try { diff --git a/src/utils/webhookNotifier.js b/src/utils/webhookNotifier.js index e5002c29..dccbb878 100644 --- a/src/utils/webhookNotifier.js +++ b/src/utils/webhookNotifier.js @@ -68,6 +68,7 @@ class WebhookNotifier { const errorCodes = { 'claude-oauth': { unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED', + blocked: 'CLAUDE_OAUTH_BLOCKED', error: 'CLAUDE_OAUTH_ERROR', disabled: 'CLAUDE_OAUTH_MANUALLY_DISABLED' },