feat: 新增Claude Console账户临时封禁处理和错误消息清理

- 新增 CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES 配置项,自动处理账户临时禁用的 400 错误(如 "organization has been disabled"、"too many active sessions" 等)。
  - 添加 errorSanitizer 工具模块,自动清理上游错误响应中的供应商特定信息(URL、供应商名称等),避免泄露中转服务商信息。
  - 统一调度器现在会主动检查并恢复已过期的封禁账户,确保账户在临时封禁时长结束后可以立即重新使用。
This commit is contained in:
sususu
2025-10-17 15:27:47 +08:00
parent f6eb077d82
commit b0917b75a4
6 changed files with 548 additions and 59 deletions

View File

@@ -37,6 +37,17 @@ CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-20
# 启用529错误处理0表示禁用>0表示过载状态持续时间分钟
CLAUDE_OVERLOAD_HANDLING_MINUTES=0
# 400错误处理0表示禁用>0表示临时禁用时间分钟
# 只有匹配特定错误模式的 400 才会触发临时禁用
# - organization has been disabled
# - account has been disabled
# - account is disabled
# - no account supporting
# - account not found
# - invalid account
# - Too many active sessions
CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES=10
# 🌐 代理配置
DEFAULT_PROXY_TIMEOUT=600000
MAX_PROXY_RETRIES=3

View File

@@ -11,6 +11,7 @@ const logger = require('../utils/logger')
const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
const sessionHelper = require('../utils/sessionHelper')
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
const { sanitizeUpstreamError } = require('../utils/errorSanitizer')
const router = express.Router()
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
@@ -947,7 +948,13 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) =>
// 尝试解析并返回JSON响应
try {
const jsonData = JSON.parse(response.body)
res.json(jsonData)
// 对于非 2xx 响应,清理供应商特定信息
if (response.statusCode < 200 || response.statusCode >= 300) {
const sanitizedData = sanitizeUpstreamError(jsonData)
res.json(sanitizedData)
} else {
res.json(jsonData)
}
} catch (parseError) {
res.send(response.body)
}

View File

@@ -36,6 +36,20 @@ class ClaudeConsoleAccountService {
)
}
_getBlockedHandlingMinutes() {
const raw = process.env.CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES
if (raw === undefined || raw === null || raw === '') {
return 0
}
const parsed = Number.parseInt(raw, 10)
if (!Number.isFinite(parsed) || parsed <= 0) {
return 0
}
return parsed
}
// 🏢 创建Claude Console账户
async createAccount(options = {}) {
const {
@@ -690,6 +704,183 @@ class ClaudeConsoleAccountService {
}
}
// 🚫 标记账号为临时封禁状态400错误 - 账户临时禁用)
async markConsoleAccountBlocked(accountId, errorDetails = '') {
try {
const client = redis.getClientSafe()
const account = await this.getAccount(accountId)
if (!account) {
throw new Error('Account not found')
}
const blockedMinutes = this._getBlockedHandlingMinutes()
if (blockedMinutes <= 0) {
logger.info(
` CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES 未设置或为0跳过账户封禁${account.name} (${accountId})`
)
if (account.blockedStatus === 'blocked') {
try {
await this.removeAccountBlocked(accountId)
} catch (cleanupError) {
logger.warn(`⚠️ 尝试移除账户封禁状态失败:${accountId}`, cleanupError)
}
}
return { success: false, skipped: true }
}
const updates = {
blockedAt: new Date().toISOString(),
blockedStatus: 'blocked',
isActive: 'false', // 禁用账户与429保持一致
schedulable: 'false', // 停止调度与429保持一致
status: 'account_blocked', // 设置状态与429保持一致
errorMessage: '账户临时被禁用400错误',
// 使用独立的封禁自动停止标记
blockedAutoStopped: 'true'
}
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
// 发送Webhook通知包含完整错误详情
try {
const webhookNotifier = require('../utils/webhookNotifier')
await webhookNotifier.sendAccountAnomalyNotification({
accountId,
accountName: account.name || 'Claude Console Account',
platform: 'claude-console',
status: 'error',
errorCode: 'CLAUDE_CONSOLE_BLOCKED',
reason: `账户临时被禁用400错误。账户将在 ${blockedMinutes} 分钟后自动恢复。`,
errorDetails: errorDetails || '无错误详情',
timestamp: new Date().toISOString()
})
} catch (webhookError) {
logger.error('Failed to send blocked webhook notification:', webhookError)
}
logger.warn(`🚫 Claude Console account temporarily blocked: ${account.name} (${accountId})`)
return { success: true }
} catch (error) {
logger.error(`❌ Failed to mark Claude Console account as blocked: ${accountId}`, error)
throw error
}
}
// ✅ 移除账号的临时封禁状态
async removeAccountBlocked(accountId) {
try {
const client = redis.getClientSafe()
const accountKey = `${this.ACCOUNT_KEY_PREFIX}${accountId}`
// 获取账户当前状态和额度信息
const [currentStatus, quotaStoppedAt] = await client.hmget(
accountKey,
'status',
'quotaStoppedAt'
)
// 删除封禁相关字段
await client.hdel(accountKey, 'blockedAt', 'blockedStatus')
// 根据不同情况决定是否恢复账户
if (currentStatus === 'account_blocked') {
if (quotaStoppedAt) {
// 还有额度限制改为quota_exceeded状态
await client.hset(accountKey, {
status: 'quota_exceeded'
// isActive保持false
})
logger.info(
`⚠️ Blocked status removed but quota exceeded remains for account: ${accountId}`
)
} else {
// 没有额度限制,完全恢复
const accountData = await client.hgetall(accountKey)
const updateData = {
isActive: 'true',
status: 'active',
errorMessage: ''
}
const hadAutoStop = accountData.blockedAutoStopped === 'true'
// 只恢复因封禁而自动停止的账户
if (hadAutoStop && accountData.schedulable === 'false') {
updateData.schedulable = 'true' // 恢复调度
logger.info(
`✅ Auto-resuming scheduling for Claude Console account ${accountId} after blocked status cleared`
)
}
if (hadAutoStop) {
await client.hdel(accountKey, 'blockedAutoStopped')
}
await client.hset(accountKey, updateData)
logger.success(`✅ Blocked status removed and account re-enabled: ${accountId}`)
}
} else {
if (await client.hdel(accountKey, 'blockedAutoStopped')) {
logger.info(
` Removed stale auto-stop flag for Claude Console account ${accountId} during blocked status recovery`
)
}
logger.success(`✅ Blocked status removed for Claude Console account: ${accountId}`)
}
return { success: true }
} catch (error) {
logger.error(
`❌ Failed to remove blocked status for Claude Console account: ${accountId}`,
error
)
throw error
}
}
// 🔍 检查账号是否处于临时封禁状态
async isAccountBlocked(accountId) {
try {
const account = await this.getAccount(accountId)
if (!account) {
return false
}
if (account.blockedStatus === 'blocked' && account.blockedAt) {
const blockedDuration = this._getBlockedHandlingMinutes()
if (blockedDuration <= 0) {
await this.removeAccountBlocked(accountId)
return false
}
const blockedAt = new Date(account.blockedAt)
const now = new Date()
const minutesSinceBlocked = (now - blockedAt) / (1000 * 60)
// 禁用时长过后自动恢复
if (minutesSinceBlocked >= blockedDuration) {
await this.removeAccountBlocked(accountId)
return false
}
return true
}
return false
} catch (error) {
logger.error(
`❌ Failed to check blocked status for Claude Console account: ${accountId}`,
error
)
return false
}
}
// 🚫 标记账号为过载状态529错误
async markAccountOverloaded(accountId) {
try {

View File

@@ -2,6 +2,11 @@ const axios = require('axios')
const claudeConsoleAccountService = require('./claudeConsoleAccountService')
const logger = require('../utils/logger')
const config = require('../../config/config')
const {
sanitizeUpstreamError,
sanitizeErrorMessage,
isAccountDisabledError
} = require('../utils/errorSanitizer')
class ClaudeConsoleRelayService {
constructor() {
@@ -172,14 +177,49 @@ class ClaudeConsoleRelayService {
logger.debug(
`[DEBUG] Response data length: ${response.data ? (typeof response.data === 'string' ? response.data.length : JSON.stringify(response.data).length) : 0}`
)
logger.debug(
`[DEBUG] Response data preview: ${typeof response.data === 'string' ? response.data.substring(0, 200) : JSON.stringify(response.data).substring(0, 200)}`
)
// 对于错误响应,记录原始错误和清理后的预览
if (response.status < 200 || response.status >= 300) {
// 记录原始错误响应(包含供应商信息,用于调试)
const rawData =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
logger.error(
`📝 Upstream error response from ${account?.name || accountId}: ${rawData.substring(0, 500)}`
)
// 记录清理后的数据到error
try {
const responseData =
typeof response.data === 'string' ? JSON.parse(response.data) : response.data
const sanitizedData = sanitizeUpstreamError(responseData)
logger.error(`🧹 [SANITIZED] Error response to client: ${JSON.stringify(sanitizedData)}`)
} catch (e) {
const rawText =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
const sanitizedText = sanitizeErrorMessage(rawText)
logger.error(`🧹 [SANITIZED] Error response to client: ${sanitizedText}`)
}
} else {
logger.debug(
`[DEBUG] Response data preview: ${typeof response.data === 'string' ? response.data.substring(0, 200) : JSON.stringify(response.data).substring(0, 200)}`
)
}
// 检查是否为账户禁用/不可用的 400 错误
const accountDisabledError = isAccountDisabledError(response.status, response.data)
// 检查错误状态并相应处理
if (response.status === 401) {
logger.warn(`🚫 Unauthorized error detected for Claude Console account ${accountId}`)
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
} else if (accountDisabledError) {
logger.error(
`🚫 Account disabled error (400) detected for Claude Console account ${accountId}, marking as blocked`
)
// 传入完整的错误详情到 webhook
const errorDetails =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
await claudeConsoleAccountService.markConsoleAccountBlocked(accountId, errorDetails)
} else if (response.status === 429) {
logger.warn(`🚫 Rate limit detected for Claude Console account ${accountId}`)
// 收到429先检查是否因为超过了手动配置的每日额度
@@ -206,9 +246,30 @@ class ClaudeConsoleRelayService {
// 更新最后使用时间
await this._updateLastUsedTime(accountId)
const responseBody =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
logger.debug(`[DEBUG] Final response body to return: ${responseBody}`)
// 准备响应体并清理错误信息(如果是错误响应)
let responseBody
if (response.status < 200 || response.status >= 300) {
// 错误响应,清理供应商信息
try {
const responseData =
typeof response.data === 'string' ? JSON.parse(response.data) : response.data
const sanitizedData = sanitizeUpstreamError(responseData)
responseBody = JSON.stringify(sanitizedData)
logger.debug(`🧹 Sanitized error response`)
} catch (parseError) {
// 如果无法解析为JSON尝试清理文本
const rawText =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
responseBody = sanitizeErrorMessage(rawText)
logger.debug(`🧹 Sanitized error text`)
}
} else {
// 成功响应,不需要清理
responseBody =
typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
}
logger.debug(`[DEBUG] Final response body to return: ${responseBody.substring(0, 200)}...`)
return {
statusCode: response.status,
@@ -388,44 +449,83 @@ class ClaudeConsoleRelayService {
`❌ Claude Console API returned error status: ${response.status} | Account: ${account?.name || accountId}`
)
if (response.status === 401) {
claudeConsoleAccountService.markAccountUnauthorized(accountId)
} else if (response.status === 429) {
claudeConsoleAccountService.markAccountRateLimited(accountId)
// 检查是否因为超过每日额度
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
logger.error('❌ Failed to check quota after 429 error:', err)
})
} else if (response.status === 529) {
claudeConsoleAccountService.markAccountOverloaded(accountId)
}
// 收集错误数据用于检测
let errorDataForCheck = ''
const errorChunks = []
// 设置错误响应的状态码和响应头
if (!responseStream.headersSent) {
const errorHeaders = {
'Content-Type': response.headers['content-type'] || 'application/json',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
}
// 避免 Transfer-Encoding 冲突,让 Express 自动处理
delete errorHeaders['Transfer-Encoding']
delete errorHeaders['Content-Length']
responseStream.writeHead(response.status, errorHeaders)
}
// 直接透传错误数据,不进行包装
response.data.on('data', (chunk) => {
if (!responseStream.destroyed) {
responseStream.write(chunk)
}
errorChunks.push(chunk)
errorDataForCheck += chunk.toString()
})
response.data.on('end', () => {
if (!responseStream.destroyed) {
responseStream.end()
response.data.on('end', async () => {
// 记录原始错误消息到日志(方便调试,包含供应商信息)
logger.error(
`📝 [Stream] Upstream error response from ${account?.name || accountId}: ${errorDataForCheck.substring(0, 500)}`
)
// 检查是否为账户禁用错误
const accountDisabledError = isAccountDisabledError(
response.status,
errorDataForCheck
)
if (response.status === 401) {
await claudeConsoleAccountService.markAccountUnauthorized(accountId)
} else if (accountDisabledError) {
logger.error(
`🚫 [Stream] Account disabled error (400) detected for Claude Console account ${accountId}, marking as blocked`
)
// 传入完整的错误详情到 webhook
await claudeConsoleAccountService.markConsoleAccountBlocked(
accountId,
errorDataForCheck
)
} else if (response.status === 429) {
await claudeConsoleAccountService.markAccountRateLimited(accountId)
// 检查是否因为超过每日额度
claudeConsoleAccountService.checkQuotaUsage(accountId).catch((err) => {
logger.error('❌ Failed to check quota after 429 error:', err)
})
} else if (response.status === 529) {
await claudeConsoleAccountService.markAccountOverloaded(accountId)
}
// 设置响应头
if (!responseStream.headersSent) {
responseStream.writeHead(response.status, {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
})
}
// 清理并发送错误响应
try {
const fullErrorData = Buffer.concat(errorChunks).toString()
const errorJson = JSON.parse(fullErrorData)
const sanitizedError = sanitizeUpstreamError(errorJson)
// 记录清理后的错误消息(发送给客户端的,完整记录)
logger.error(
`🧹 [Stream] [SANITIZED] Error response to client: ${JSON.stringify(sanitizedError)}`
)
if (!responseStream.destroyed) {
responseStream.write(JSON.stringify(sanitizedError))
responseStream.end()
}
} catch (parseError) {
const sanitizedText = sanitizeErrorMessage(errorDataForCheck)
logger.error(`🧹 [Stream] [SANITIZED] Error response to client: ${sanitizedText}`)
if (!responseStream.destroyed) {
responseStream.write(sanitizedText)
responseStream.end()
}
}
resolve() // 不抛出异常,正常完成流处理
})
return
}

View File

@@ -527,68 +527,86 @@ class UnifiedClaudeScheduler {
logger.info(`📋 Found ${consoleAccounts.length} total Claude Console accounts`)
for (const account of consoleAccounts) {
// 主动检查封禁状态并尝试恢复(在过滤之前执行,确保可以恢复被封禁的账户)
const wasBlocked = await claudeConsoleAccountService.isAccountBlocked(account.id)
// 如果账户之前被封禁但现在已恢复,重新获取最新状态
let currentAccount = account
if (wasBlocked === false && account.status === 'account_blocked') {
// 可能刚刚被恢复,重新获取账户状态
const freshAccount = await claudeConsoleAccountService.getAccount(account.id)
if (freshAccount) {
currentAccount = freshAccount
logger.info(`🔄 Account ${account.name} was recovered from blocked status`)
}
}
logger.info(
`🔍 Checking Claude Console account: ${account.name} - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
`🔍 Checking Claude Console account: ${currentAccount.name} - isActive: ${currentAccount.isActive}, status: ${currentAccount.status}, accountType: ${currentAccount.accountType}, schedulable: ${currentAccount.schedulable}`
)
// 注意getAllAccounts返回的isActive是布尔值
// 注意getAllAccounts返回的isActive是布尔值getAccount返回的也是布尔值
if (
account.isActive === true &&
account.status === 'active' &&
account.accountType === 'shared' &&
this._isSchedulable(account.schedulable)
currentAccount.isActive === true &&
currentAccount.status === 'active' &&
currentAccount.accountType === 'shared' &&
this._isSchedulable(currentAccount.schedulable)
) {
// 检查是否可调度
// 检查模型支持
if (!this._isModelSupportedByAccount(account, 'claude-console', requestedModel)) {
if (!this._isModelSupportedByAccount(currentAccount, 'claude-console', requestedModel)) {
continue
}
// 检查订阅是否过期
if (claudeConsoleAccountService.isSubscriptionExpired(account)) {
if (claudeConsoleAccountService.isSubscriptionExpired(currentAccount)) {
logger.debug(
`⏰ Claude Console account ${account.name} (${account.id}) expired at ${account.subscriptionExpiresAt}`
`⏰ Claude Console account ${currentAccount.name} (${currentAccount.id}) expired at ${currentAccount.subscriptionExpiresAt}`
)
continue
}
// 主动触发一次额度检查,确保状态即时生效
try {
await claudeConsoleAccountService.checkQuotaUsage(account.id)
await claudeConsoleAccountService.checkQuotaUsage(currentAccount.id)
} catch (e) {
logger.warn(
`Failed to check quota for Claude Console account ${account.name}: ${e.message}`
`Failed to check quota for Claude Console account ${currentAccount.name}: ${e.message}`
)
// 继续处理该账号
}
// 检查是否被限流
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(account.id)
const isQuotaExceeded = await claudeConsoleAccountService.isAccountQuotaExceeded(account.id)
const isRateLimited = await claudeConsoleAccountService.isAccountRateLimited(
currentAccount.id
)
const isQuotaExceeded = await claudeConsoleAccountService.isAccountQuotaExceeded(
currentAccount.id
)
if (!isRateLimited && !isQuotaExceeded) {
availableAccounts.push({
...account,
accountId: account.id,
...currentAccount,
accountId: currentAccount.id,
accountType: 'claude-console',
priority: parseInt(account.priority) || 50,
lastUsedAt: account.lastUsedAt || '0'
priority: parseInt(currentAccount.priority) || 50,
lastUsedAt: currentAccount.lastUsedAt || '0'
})
logger.info(
`✅ Added Claude Console account to available pool: ${account.name} (priority: ${account.priority})`
`✅ Added Claude Console account to available pool: ${currentAccount.name} (priority: ${currentAccount.priority})`
)
} else {
if (isRateLimited) {
logger.warn(`⚠️ Claude Console account ${account.name} is rate limited`)
logger.warn(`⚠️ Claude Console account ${currentAccount.name} is rate limited`)
}
if (isQuotaExceeded) {
logger.warn(`💰 Claude Console account ${account.name} quota exceeded`)
logger.warn(`💰 Claude Console account ${currentAccount.name} quota exceeded`)
}
}
} else {
logger.info(
`❌ Claude Console account ${account.name} not eligible - isActive: ${account.isActive}, status: ${account.status}, accountType: ${account.accountType}, schedulable: ${account.schedulable}`
`❌ Claude Console account ${currentAccount.name} not eligible - isActive: ${currentAccount.isActive}, status: ${currentAccount.status}, accountType: ${currentAccount.accountType}, schedulable: ${currentAccount.schedulable}`
)
}
}

162
src/utils/errorSanitizer.js Normal file
View File

@@ -0,0 +1,162 @@
/**
* 错误消息清理工具
* 用于移除上游错误中的供应商特定信息(如 URL、引用等
*/
/**
* 清理错误消息中的 URL 和供应商引用
* @param {string} message - 原始错误消息
* @returns {string} - 清理后的消息
*/
function sanitizeErrorMessage(message) {
if (typeof message !== 'string') {
return message
}
// 移除 URLhttp:// 或 https://
let cleaned = message.replace(/https?:\/\/[^\s]+/gi, '')
// 移除常见的供应商引用模式
cleaned = cleaned.replace(/For more (?:details|information|help)[,\s]*/gi, '')
cleaned = cleaned.replace(/(?:please\s+)?visit\s+\S*/gi, '') // 移除 "visit xxx"
cleaned = cleaned.replace(/(?:see|check)\s+(?:our|the)\s+\S*/gi, '') // 移除 "see our xxx"
cleaned = cleaned.replace(/(?:contact|reach)\s+(?:us|support)\s+at\s+\S*/gi, '') // 移除联系信息
// 移除供应商特定关键词(包括整个单词)
cleaned = cleaned.replace(/88code\S*/gi, '')
cleaned = cleaned.replace(/duck\S*/gi, '')
cleaned = cleaned.replace(/packy\S*/gi, '')
cleaned = cleaned.replace(/ikun\S*/gi, '')
cleaned = cleaned.replace(/privnode\S*/gi, '')
cleaned = cleaned.replace(/yescode\S*/gi, '')
cleaned = cleaned.replace(/share\S*/gi, '')
cleaned = cleaned.replace(/yhlxj\S*/gi, '')
cleaned = cleaned.replace(/gac\S*/gi, '')
cleaned = cleaned.replace(/driod\S*/gi, '')
cleaned = cleaned.replace(/\s+/g, ' ').trim()
// 如果消息被清理得太短或为空,返回通用消息
if (cleaned.length < 5) {
return 'The requested model is currently unavailable'
}
return cleaned
}
/**
* 递归清理对象中的所有错误消息字段
* @param {Object} errorData - 原始错误数据对象
* @returns {Object} - 清理后的错误数据
*/
function sanitizeUpstreamError(errorData) {
if (!errorData || typeof errorData !== 'object') {
return errorData
}
// 深拷贝避免修改原始对象
const sanitized = JSON.parse(JSON.stringify(errorData))
// 递归清理嵌套的错误对象
const sanitizeObject = (obj) => {
if (!obj || typeof obj !== 'object') {
return obj
}
for (const key in obj) {
if (key === 'message' && typeof obj[key] === 'string') {
obj[key] = sanitizeErrorMessage(obj[key])
} else if (typeof obj[key] === 'object') {
sanitizeObject(obj[key])
}
}
return obj
}
return sanitizeObject(sanitized)
}
/**
* 提取错误消息(支持多种错误格式)
* @param {*} body - 错误响应体(字符串或对象)
* @returns {string} - 提取的错误消息
*/
function extractErrorMessage(body) {
if (!body) {
return ''
}
// 处理字符串类型
if (typeof body === 'string') {
const trimmed = body.trim()
if (!trimmed) {
return ''
}
try {
const parsed = JSON.parse(trimmed)
return extractErrorMessage(parsed)
} catch (error) {
return trimmed
}
}
// 处理对象类型
if (typeof body === 'object') {
// 常见错误格式: { error: "message" }
if (typeof body.error === 'string') {
return body.error
}
// 嵌套错误格式: { error: { message: "..." } }
if (body.error && typeof body.error === 'object') {
if (typeof body.error.message === 'string') {
return body.error.message
}
if (typeof body.error.error === 'string') {
return body.error.error
}
}
// 直接消息格式: { message: "..." }
if (typeof body.message === 'string') {
return body.message
}
}
return ''
}
/**
* 检测是否为账户被禁用或不可用的 400 错误
* @param {number} statusCode - HTTP 状态码
* @param {*} body - 响应体
* @returns {boolean} - 是否为账户禁用错误
*/
function isAccountDisabledError(statusCode, body) {
if (statusCode !== 400) {
return false
}
const message = extractErrorMessage(body)
if (!message) {
return false
}
// 将消息全部转换为小写,进行模糊匹配(避免大小写问题)
const lowerMessage = message.toLowerCase()
// 检测常见的账户禁用/不可用模式
return (
lowerMessage.includes('organization has been disabled') ||
lowerMessage.includes('account has been disabled') ||
lowerMessage.includes('account is disabled') ||
lowerMessage.includes('no account supporting') ||
lowerMessage.includes('account not found') ||
lowerMessage.includes('invalid account') ||
lowerMessage.includes('too many active sessions')
)
}
module.exports = {
sanitizeErrorMessage,
sanitizeUpstreamError,
extractErrorMessage,
isAccountDisabledError
}