mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-27 01:47:46 +00:00
1
This commit is contained in:
@@ -1,162 +1,228 @@
|
||||
/**
|
||||
* 错误消息清理工具
|
||||
* 用于移除上游错误中的供应商特定信息(如 URL、引用等)
|
||||
* 错误消息清理工具 - 白名单错误码制
|
||||
* 所有错误映射到预定义的标准错误码,原始消息只记日志不返回前端
|
||||
*/
|
||||
|
||||
const logger = require('./logger')
|
||||
|
||||
// 标准错误码定义
|
||||
const ERROR_CODES = {
|
||||
E001: { message: 'Service temporarily unavailable', status: 503 },
|
||||
E002: { message: 'Network connection failed', status: 502 },
|
||||
E003: { message: 'Authentication failed', status: 401 },
|
||||
E004: { message: 'Rate limit exceeded', status: 429 },
|
||||
E005: { message: 'Invalid request', status: 400 },
|
||||
E006: { message: 'Model not available', status: 503 },
|
||||
E007: { message: 'Upstream service error', status: 502 },
|
||||
E008: { message: 'Request timeout', status: 504 },
|
||||
E009: { message: 'Permission denied', status: 403 },
|
||||
E010: { message: 'Resource not found', status: 404 },
|
||||
E011: { message: 'Account temporarily unavailable', status: 503 },
|
||||
E012: { message: 'Server overloaded', status: 529 },
|
||||
E013: { message: 'Invalid API key', status: 401 },
|
||||
E014: { message: 'Quota exceeded', status: 429 },
|
||||
E015: { message: 'Internal server error', status: 500 }
|
||||
}
|
||||
|
||||
// 错误特征匹配规则(按优先级排序)
|
||||
const ERROR_MATCHERS = [
|
||||
// 网络层错误
|
||||
{ pattern: /ENOTFOUND|DNS|getaddrinfo/i, code: 'E002' },
|
||||
{ pattern: /ECONNREFUSED|ECONNRESET|connection refused/i, code: 'E002' },
|
||||
{ pattern: /ETIMEDOUT|timeout/i, code: 'E008' },
|
||||
{ pattern: /ECONNABORTED|aborted/i, code: 'E002' },
|
||||
|
||||
// 认证错误
|
||||
{ pattern: /unauthorized|invalid.*token|token.*invalid|invalid.*key/i, code: 'E003' },
|
||||
{ pattern: /invalid.*api.*key|api.*key.*invalid/i, code: 'E013' },
|
||||
{ pattern: /authentication|auth.*fail/i, code: 'E003' },
|
||||
|
||||
// 权限错误
|
||||
{ pattern: /forbidden|permission.*denied|access.*denied/i, code: 'E009' },
|
||||
{ pattern: /does not have.*permission/i, code: 'E009' },
|
||||
|
||||
// 限流错误
|
||||
{ pattern: /rate.*limit|too many requests|429/i, code: 'E004' },
|
||||
{ pattern: /quota.*exceeded|usage.*limit/i, code: 'E014' },
|
||||
|
||||
// 过载错误
|
||||
{ pattern: /overloaded|529|capacity/i, code: 'E012' },
|
||||
|
||||
// 账户错误
|
||||
{ pattern: /account.*disabled|organization.*disabled/i, code: 'E011' },
|
||||
{ pattern: /too many active sessions/i, code: 'E011' },
|
||||
|
||||
// 模型错误
|
||||
{ pattern: /model.*not.*found|model.*unavailable|unsupported.*model/i, code: 'E006' },
|
||||
|
||||
// 请求错误
|
||||
{ pattern: /bad.*request|invalid.*request|malformed/i, code: 'E005' },
|
||||
{ pattern: /not.*found|404/i, code: 'E010' },
|
||||
|
||||
// 上游错误
|
||||
{ pattern: /upstream|502|bad.*gateway/i, code: 'E007' },
|
||||
{ pattern: /503|service.*unavailable/i, code: 'E001' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 清理错误消息中的 URL 和供应商引用
|
||||
* @param {string} message - 原始错误消息
|
||||
* @returns {string} - 清理后的消息
|
||||
* 根据原始错误匹配标准错误码
|
||||
* @param {Error|string|object} error - 原始错误
|
||||
* @param {object} options - 选项
|
||||
* @param {string} options.context - 错误上下文(用于日志)
|
||||
* @param {boolean} options.logOriginal - 是否记录原始错误(默认true)
|
||||
* @returns {{ code: string, message: string, status: number }}
|
||||
*/
|
||||
function sanitizeErrorMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
return message
|
||||
function mapToErrorCode(error, options = {}) {
|
||||
const { context = 'unknown', logOriginal = true } = options
|
||||
|
||||
// 提取原始错误信息
|
||||
const originalMessage = extractOriginalMessage(error)
|
||||
const errorCode = error?.code || error?.response?.status
|
||||
const statusCode = error?.response?.status || error?.status || error?.statusCode
|
||||
|
||||
// 记录原始错误到日志(供调试)
|
||||
if (logOriginal && originalMessage) {
|
||||
logger.debug(`[ErrorSanitizer] Original error (${context}):`, {
|
||||
message: originalMessage,
|
||||
code: errorCode,
|
||||
status: statusCode
|
||||
})
|
||||
}
|
||||
|
||||
// 移除 URL(http:// 或 https://)
|
||||
let cleaned = message.replace(/https?:\/\/[^\s]+/gi, '')
|
||||
// 匹配错误码
|
||||
let matchedCode = 'E015' // 默认:内部服务器错误
|
||||
|
||||
// 移除常见的供应商引用模式
|
||||
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(/yes.vg\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'
|
||||
// 先按 HTTP 状态码快速匹配
|
||||
if (statusCode) {
|
||||
if (statusCode === 401) matchedCode = 'E003'
|
||||
else if (statusCode === 403) matchedCode = 'E009'
|
||||
else if (statusCode === 404) matchedCode = 'E010'
|
||||
else if (statusCode === 429) matchedCode = 'E004'
|
||||
else if (statusCode === 502) matchedCode = 'E007'
|
||||
else if (statusCode === 503) matchedCode = 'E001'
|
||||
else if (statusCode === 504) matchedCode = 'E008'
|
||||
else if (statusCode === 529) matchedCode = 'E012'
|
||||
}
|
||||
|
||||
return cleaned
|
||||
// 再按消息内容精确匹配(可能覆盖状态码匹配)
|
||||
if (originalMessage) {
|
||||
for (const matcher of ERROR_MATCHERS) {
|
||||
if (matcher.pattern.test(originalMessage)) {
|
||||
matchedCode = matcher.code
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按错误 code 匹配(网络错误)
|
||||
if (errorCode) {
|
||||
const codeStr = String(errorCode).toUpperCase()
|
||||
if (codeStr === 'ENOTFOUND' || codeStr === 'EAI_AGAIN') matchedCode = 'E002'
|
||||
else if (codeStr === 'ECONNREFUSED' || codeStr === 'ECONNRESET') matchedCode = 'E002'
|
||||
else if (codeStr === 'ETIMEDOUT' || codeStr === 'ESOCKETTIMEDOUT') matchedCode = 'E008'
|
||||
else if (codeStr === 'ECONNABORTED') matchedCode = 'E002'
|
||||
}
|
||||
|
||||
const result = ERROR_CODES[matchedCode]
|
||||
return {
|
||||
code: matchedCode,
|
||||
message: result.message,
|
||||
status: result.status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归清理对象中的所有错误消息字段
|
||||
* @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) {
|
||||
// 清理所有字符串字段,不仅仅是 message
|
||||
if (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
|
||||
}
|
||||
}
|
||||
|
||||
function extractOriginalMessage(error) {
|
||||
if (!error) return ''
|
||||
if (typeof error === 'string') return error
|
||||
if (error.message) return error.message
|
||||
if (error.response?.data?.error?.message) return error.response.data.error.message
|
||||
if (error.response?.data?.error) return String(error.response.data.error)
|
||||
if (error.response?.data?.message) return error.response.data.message
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为账户被禁用或不可用的 400 错误
|
||||
* @param {number} statusCode - HTTP 状态码
|
||||
* @param {*} body - 响应体
|
||||
* @returns {boolean} - 是否为账户禁用错误
|
||||
* 创建安全的错误响应对象
|
||||
* @param {Error|string|object} error - 原始错误
|
||||
* @param {object} options - 选项
|
||||
* @returns {{ error: { code: string, message: string }, status: number }}
|
||||
*/
|
||||
function isAccountDisabledError(statusCode, body) {
|
||||
if (statusCode !== 400) {
|
||||
return false
|
||||
function createSafeErrorResponse(error, options = {}) {
|
||||
const mapped = mapToErrorCode(error, options)
|
||||
return {
|
||||
error: {
|
||||
code: mapped.code,
|
||||
message: mapped.message
|
||||
},
|
||||
status: mapped.status
|
||||
}
|
||||
}
|
||||
|
||||
const message = extractErrorMessage(body)
|
||||
if (!message) {
|
||||
return false
|
||||
}
|
||||
// 将消息全部转换为小写,进行模糊匹配(避免大小写问题)
|
||||
const lowerMessage = message.toLowerCase()
|
||||
// 检测常见的账户禁用/不可用模式
|
||||
/**
|
||||
* 创建安全的 SSE 错误事件
|
||||
* @param {Error|string|object} error - 原始错误
|
||||
* @param {object} options - 选项
|
||||
* @returns {string} - SSE 格式的错误事件
|
||||
*/
|
||||
function createSafeSSEError(error, options = {}) {
|
||||
const mapped = mapToErrorCode(error, options)
|
||||
return `event: error\ndata: ${JSON.stringify({
|
||||
error: mapped.message,
|
||||
code: mapped.code,
|
||||
timestamp: new Date().toISOString()
|
||||
})}\n\n`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安全的错误消息(用于替换 error.message)
|
||||
* @param {Error|string|object} error - 原始错误
|
||||
* @param {object} options - 选项
|
||||
* @returns {string}
|
||||
*/
|
||||
function getSafeMessage(error, options = {}) {
|
||||
return mapToErrorCode(error, options).message
|
||||
}
|
||||
|
||||
// 兼容旧接口
|
||||
function sanitizeErrorMessage(message) {
|
||||
if (!message) return 'Service temporarily unavailable'
|
||||
return mapToErrorCode({ message }, { logOriginal: false }).message
|
||||
}
|
||||
|
||||
function sanitizeUpstreamError(errorData) {
|
||||
return createSafeErrorResponse(errorData, { logOriginal: false })
|
||||
}
|
||||
|
||||
function extractErrorMessage(body) {
|
||||
return extractOriginalMessage(body)
|
||||
}
|
||||
|
||||
function isAccountDisabledError(statusCode, body) {
|
||||
if (statusCode !== 400) return false
|
||||
const message = extractOriginalMessage(body)
|
||||
if (!message) return false
|
||||
const lower = 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')
|
||||
lower.includes('organization has been disabled') ||
|
||||
lower.includes('account has been disabled') ||
|
||||
lower.includes('account is disabled') ||
|
||||
lower.includes('no account supporting') ||
|
||||
lower.includes('account not found') ||
|
||||
lower.includes('invalid account') ||
|
||||
lower.includes('too many active sessions')
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ERROR_CODES,
|
||||
mapToErrorCode,
|
||||
createSafeErrorResponse,
|
||||
createSafeSSEError,
|
||||
getSafeMessage,
|
||||
// 兼容旧接口
|
||||
sanitizeErrorMessage,
|
||||
sanitizeUpstreamError,
|
||||
extractErrorMessage,
|
||||
|
||||
Reference in New Issue
Block a user