/** * @fileoverview 错误归一化模块 * @description 统一处理页面级和 HTTP 级错误,提供可重试判定 */ import { logger } from '../../utils/logger.js'; import { ADAPTER_ERRORS } from '../../utils/constants.js'; // ========================================== // 可重试判定 // ========================================== /** * 判断错误是否可重试 * @param {string} errorMessage - 错误消息 * @returns {boolean} */ export function isRetryableError(errorMessage) { if (!errorMessage) return false; const retryablePatterns = [ // 网络错误 /network|net::|econnreset|econnrefused|etimedout/i, // 超时 /timeout|timed out/i, // 页面崩溃 /crashed|crash/i, // 5xx 服务端错误 /5\d{2}|internal server error|bad gateway|service unavailable/i, // 限流(可能是临时的) /rate limit|too many requests|429/i, ]; return retryablePatterns.some(pattern => pattern.test(errorMessage)); } // ========================================== // 页面错误归一化 // ========================================== /** * 统一处理页面级错误 * @param {Error} err - 原始错误 * @param {object} [meta={}] - 日志元数据 * @returns {{ error: string, code: string, retryable: boolean } | null} */ export function normalizePageError(err, meta = {}) { if (err.message === 'PAGE_CLOSED') { logger.error('适配器', '页面已关闭', meta); return { error: '页面已关闭,请勿在生图过程中刷新页面', code: ADAPTER_ERRORS.PAGE_CLOSED, retryable: true }; } if (err.message === 'PAGE_CRASHED') { logger.error('适配器', '页面崩溃', meta); return { error: '页面崩溃,请重试', code: ADAPTER_ERRORS.PAGE_CRASHED, retryable: true }; } if (err.message === 'PAGE_INVALID') { logger.error('适配器', '页面状态无效', meta); return { error: '页面状态无效,请重新初始化', code: ADAPTER_ERRORS.PAGE_INVALID, retryable: true }; } // API_TIMEOUT: waitApiResponse 内部转换后的超时错误 if (err.message?.startsWith('API_TIMEOUT:')) { const timeoutMsg = err.message.replace('API_TIMEOUT: ', ''); logger.error('适配器', timeoutMsg, meta); return { error: timeoutMsg, code: ADAPTER_ERRORS.TIMEOUT_ERROR, retryable: true }; } // 兼容原生 TimeoutError (其他地方抛出的) if (err.name === 'TimeoutError' || err.message?.includes('Timeout')) { logger.error('适配器', '请求超时', meta); return { error: '请求超时, 请检查网络或稍后重试', code: ADAPTER_ERRORS.TIMEOUT_ERROR, retryable: true }; } // PAGE_ERROR_DETECTED: waitApiResponse 页面 UI 中检测到的错误关键词 if (err.message?.startsWith('PAGE_ERROR_DETECTED:')) { const keyword = err.message.replace('PAGE_ERROR_DETECTED: ', ''); logger.error('适配器', `页面检测到错误: ${keyword}`, meta); return { error: `内容被阻止: ${keyword}`, code: ADAPTER_ERRORS.CONTENT_BLOCKED, retryable: false }; } // API_ERROR_DETECTED: waitApiResponse API 响应体中检测到的错误关键词 if (err.message?.startsWith('API_ERROR_DETECTED:')) { const keyword = err.message.replace('API_ERROR_DETECTED: ', ''); logger.error('适配器', `API 响应检测到错误: ${keyword}`, meta); return { error: `内容被阻止: ${keyword}`, code: ADAPTER_ERRORS.CONTENT_BLOCKED, retryable: false }; } return null; } // ========================================== // HTTP 错误归一化 // ========================================== /** * 统一处理 HTTP 响应错误 * @param {import('playwright-core').Response} response - HTTP 响应对象 * @param {string} [content=null] - 响应体内容(可选) * @returns {{ error: string, code: string, retryable: boolean } | null} */ export function normalizeHttpError(response, content = null) { const status = response.status(); // 429 限流检查 if (status === 429 || content?.includes('Too Many Requests')) { return { error: '触发限流/上游繁忙', code: ADAPTER_ERRORS.RATE_LIMITED, retryable: true }; } // reCAPTCHA 验证失败 if (content?.includes('recaptcha validation failed')) { return { error: '触发人机验证', code: ADAPTER_ERRORS.CAPTCHA_REQUIRED, retryable: false }; } // 5xx 服务端错误(可重试) if (status >= 500) { return { error: `上游服务器错误,HTTP错误码: ${status}`, code: ADAPTER_ERRORS.HTTP_ERROR, retryable: true }; } // 4xx 客户端错误(不可重试) if (status >= 400) { return { error: `请求错误,HTTP错误码: ${status}`, code: ADAPTER_ERRORS.HTTP_ERROR, retryable: false }; } return null; } // ========================================== // 通用错误归一化 // ========================================== /** * 标准化错误对象(通用) * @param {string} error - 错误消息 * @returns {{error: string, code: string, retryable: boolean}} */ export function normalizeError(error) { const retryable = isRetryableError(error); let code = ADAPTER_ERRORS.NETWORK_ERROR; if (/timeout/i.test(error)) { code = ADAPTER_ERRORS.TIMEOUT_ERROR; } else if (/crashed/i.test(error)) { code = ADAPTER_ERRORS.PAGE_CRASHED; } else if (/closed/i.test(error)) { code = ADAPTER_ERRORS.PAGE_CLOSED; } else if (/5\d{2}|internal server/i.test(error)) { code = ADAPTER_ERRORS.HTTP_ERROR; } else if (/rate limit|429/i.test(error)) { code = ADAPTER_ERRORS.RATE_LIMITED; } else if (/captcha|recaptcha/i.test(error)) { code = ADAPTER_ERRORS.CAPTCHA_REQUIRED; } return { error, code, retryable }; }