This commit is contained in:
SunSeekerX
2026-01-21 11:55:28 +08:00
149 changed files with 15035 additions and 4017 deletions

View File

@@ -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
})
}
// 移除 URLhttp:// 或 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,