From ce7df1228136ccda72d3c664ffe4d099e0e24e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E6=A0=8B=E6=A2=81?= Date: Wed, 18 Feb 2026 18:09:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AF=86=E5=88=AB=20403=20permission=5F?= =?UTF-8?q?error=20=E4=B8=BA=E8=B4=A6=E5=8F=B7=E5=B0=81=E7=A6=81=E7=8A=B6?= =?UTF-8?q?=E6=80=81=EF=BC=8C=E9=81=BF=E5=85=8D=2030=20=E5=88=86=E9=92=9F?= =?UTF-8?q?=E6=97=A0=E9=99=90=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude 封禁账号后返回 HTTP 403 + permission_error: "OAuth authentication is currently not allowed for this organization." 原有的 _isOrganizationDisabledError 只检测 HTTP 400,无法识别该错误。 且 else if 分支中通用 403 在 organizationDisabledError 之前, 即使修改函数也会被截断。 修复内容: 1. _isOrganizationDisabledError 兼容 403 + permission_error 场景 2. 非流式路径:将 organizationDisabledError 检测提前到通用 403 之前 3. 流式路径:在 403 分支内部优先判断是否为封禁性质的 403 Closes #990 --- src/services/relay/claudeRelayService.js | 58 ++++++++++++++++-------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/services/relay/claudeRelayService.js b/src/services/relay/claudeRelayService.js index 24f2bd90..757fb09f 100644 --- a/src/services/relay/claudeRelayService.js +++ b/src/services/relay/claudeRelayService.js @@ -132,16 +132,23 @@ class ClaudeRelayService { return '' } - // 🚫 检查是否为组织被禁用错误 + // 🚫 检查是否为组织被禁用/封禁错误 + // 支持两种场景: + // 1. HTTP 400 + "this organization has been disabled"(原有) + // 2. HTTP 403 + "OAuth authentication is currently not allowed for this organization"(封禁后新返回格式) _isOrganizationDisabledError(statusCode, body) { - if (statusCode !== 400) { + if (statusCode !== 400 && statusCode !== 403) { return false } const message = this._extractErrorMessage(body) if (!message) { return false } - return message.toLowerCase().includes('this organization has been disabled') + const lowerMessage = message.toLowerCase() + return ( + lowerMessage.includes('this organization has been disabled') || + lowerMessage.includes('oauth authentication is currently not allowed') + ) } // 🔍 判断是否是真实的 Claude Code 请求 @@ -703,7 +710,15 @@ class ClaudeRelayService { await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {}) } } - // 检查是否为403状态码(禁止访问) + // 检查是否为组织被禁用/封禁错误(400 或 403) + // 必须在通用 403 处理之前检测,否则会被截断 + else if (organizationDisabledError) { + logger.error( + `🚫 Organization disabled/banned error (${response.statusCode}) detected for account ${accountId}, marking as blocked` + ) + await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash) + } + // 检查是否为403状态码(禁止访问,非封禁类) // 注意:如果进行了重试,retryCount > 0;这里的 403 是重试后最终的结果 else if (response.statusCode === 403) { logger.error( @@ -715,13 +730,6 @@ class ClaudeRelayService { await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {}) } } - // 检查是否返回组织被禁用错误(400状态码) - else if (organizationDisabledError) { - logger.error( - `🚫 Organization disabled error (400) detected for account ${accountId}, marking as blocked` - ) - await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash) - } // 检查是否为529状态码(服务过载) else if (response.statusCode === 529) { logger.warn(`🚫 Overload error (529) detected for account ${accountId}`) @@ -2186,14 +2194,28 @@ class ClaudeRelayService { await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {}) } } else if (res.statusCode === 403) { - // 403 处理:走到这里说明重试已用尽或不适用重试,直接标记 blocked + // 403 处理:先检查是否为封禁性质的 403(组织被禁用/OAuth 被禁止) // 注意:重试逻辑已在 handleErrorResponse 外部提前处理 - logger.error( - `🚫 [Stream] Forbidden error (403) detected for account ${accountId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}, temporarily pausing` - ) - await upstreamErrorHelper - .markTempUnavailable(accountId, accountType, 403) - .catch(() => {}) + if (this._isOrganizationDisabledError(res.statusCode, errorData)) { + logger.error( + `🚫 [Stream] Organization disabled/banned error (403) detected for account ${accountId}, marking as blocked` + ) + await unifiedClaudeScheduler + .markAccountBlocked(accountId, accountType, sessionHash) + .catch((markError) => { + logger.error( + `❌ [Stream] Failed to mark account ${accountId} as blocked:`, + markError + ) + }) + } else { + logger.error( + `🚫 [Stream] Forbidden error (403) detected for account ${accountId}${retryCount > 0 ? ` after ${retryCount} retries` : ''}, temporarily pausing` + ) + await upstreamErrorHelper + .markTempUnavailable(accountId, accountType, 403) + .catch(() => {}) + } // 清除粘性会话,让后续请求路由到其他账户 if (sessionHash) { await unifiedClaudeScheduler.clearSessionMapping(sessionHash).catch(() => {})