mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
151 lines
5.7 KiB
JavaScript
151 lines
5.7 KiB
JavaScript
/**
|
|
* @fileoverview 错误归一化模块
|
|
* @description 统一处理页面级和 HTTP 级错误,提供可重试判定
|
|
*/
|
|
|
|
import { logger } from '../../utils/logger.js';
|
|
import { ADAPTER_ERRORS } from '../../server/errors.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 };
|
|
}
|