mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 新增Claude Console账户临时封禁处理和错误消息清理
- 新增 CLAUDE_CONSOLE_BLOCKED_HANDLING_MINUTES 配置项,自动处理账户临时禁用的 400 错误(如 "organization has been disabled"、"too many active sessions" 等)。 - 添加 errorSanitizer 工具模块,自动清理上游错误响应中的供应商特定信息(URL、供应商名称等),避免泄露中转服务商信息。 - 统一调度器现在会主动检查并恢复已过期的封禁账户,确保账户在临时封禁时长结束后可以立即重新使用。
This commit is contained in:
162
src/utils/errorSanitizer.js
Normal file
162
src/utils/errorSanitizer.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 错误消息清理工具
|
||||
* 用于移除上游错误中的供应商特定信息(如 URL、引用等)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 清理错误消息中的 URL 和供应商引用
|
||||
* @param {string} message - 原始错误消息
|
||||
* @returns {string} - 清理后的消息
|
||||
*/
|
||||
function sanitizeErrorMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
return message
|
||||
}
|
||||
|
||||
// 移除 URL(http:// 或 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
|
||||
}
|
||||
Reference in New Issue
Block a user