mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-25 03:22:30 +00:00
263 lines
7.7 KiB
JavaScript
263 lines
7.7 KiB
JavaScript
/**
|
||
* 错误消息清理工具 - 白名单错误码制
|
||
* 所有错误映射到预定义的标准错误码,原始消息只记日志不返回前端
|
||
*/
|
||
|
||
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' }
|
||
]
|
||
|
||
/**
|
||
* 根据原始错误匹配标准错误码
|
||
* @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 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
|
||
})
|
||
}
|
||
|
||
// 匹配错误码
|
||
let matchedCode = 'E015' // 默认:内部服务器错误
|
||
|
||
// 先按 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'
|
||
}
|
||
}
|
||
|
||
// 再按消息内容精确匹配(可能覆盖状态码匹配)
|
||
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
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 提取原始错误消息
|
||
*/
|
||
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 ''
|
||
}
|
||
|
||
/**
|
||
* 创建安全的错误响应对象
|
||
* @param {Error|string|object} error - 原始错误
|
||
* @param {object} options - 选项
|
||
* @returns {{ error: { code: string, message: string }, status: number }}
|
||
*/
|
||
function createSafeErrorResponse(error, options = {}) {
|
||
const mapped = mapToErrorCode(error, options)
|
||
return {
|
||
error: {
|
||
code: mapped.code,
|
||
message: mapped.message
|
||
},
|
||
status: mapped.status
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建安全的 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 (
|
||
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,
|
||
isAccountDisabledError
|
||
}
|