From 72e664b16c20da34d830c754c967c42e65bb7a70 Mon Sep 17 00:00:00 2001 From: daidai Date: Fri, 27 Mar 2026 19:45:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=BE=E7=89=87=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 isRetryableError() 判断网络错误是否可重试 - 默认启用重试 (maxRetries=3) - 5xx 错误自动重试 - 网络超时、断开等错误智能重试 - 增加重试延迟(指数退避) - 返回 imageUrl 便于失败后重试 - 超时延长到 120s --- src/backend/utils/download.js | 42 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/backend/utils/download.js b/src/backend/utils/download.js index 5021aa4..49714c0 100644 --- a/src/backend/utils/download.js +++ b/src/backend/utils/download.js @@ -3,6 +3,17 @@ * @description 图片下载与 Base64 转换 */ +import { logger } from '../../utils/logger.js'; + +/** + * 判断错误是否可重试 + * @param {string} message - 错误消息 + * @returns {boolean} + */ +function isRetryableError(message) { + return /timeout|network|econnreset|econnrefused|etimedout|disconnected|tls|socket/i.test(message); +} + /** * 使用页面上下文下载图片并转换为 Base64 * 自动继承页面的 Cookie 和 Session,解决鉴权问题 @@ -10,19 +21,26 @@ * @param {import('playwright-core').Page} page - Playwright 页面对象 * @param {object} [options] - 可选配置 * @param {number} [options.timeout=60000] - 超时时间(毫秒) - * @param {number} [options.retries=0] - 下载失败时的重试次数 - * @returns {Promise<{ image?: string, error?: string }>} 下载结果 + * @param {number} [options.maxRetries=3] - 最大重试次数 + * @param {number} [options.retryDelay=1000] - 重试延迟基数(毫秒) + * @returns {Promise<{ image?: string, imageUrl?: string, error?: string }>} 下载结果(包含原始 URL) */ export async function useContextDownload(url, page, options = {}) { - const { timeout = 60000, retries = 0 } = options; + const { timeout = 120000, maxRetries = 3, retryDelay = 1000 } = options; - for (let attempt = 0; attempt <= retries; attempt++) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await page.request.get(url, { timeout }); if (!response.ok()) { - if (attempt < retries) continue; - return { error: `下载失败: HTTP ${response.status()}` }; + const status = response.status(); + // 5xx 错误可重试 + if (status >= 500 && attempt < maxRetries) { + logger.warn('下载', `HTTP ${status},重试 ${attempt}/${maxRetries}...`); + await new Promise(r => setTimeout(r, retryDelay * attempt)); + continue; + } + return { error: `下载失败: HTTP ${status}`, imageUrl: url }; } const buffer = await response.body(); @@ -30,10 +48,16 @@ export async function useContextDownload(url, page, options = {}) { const contentType = response.headers()['content-type'] || 'image/png'; const mimeType = contentType.split(';')[0].trim(); - return { image: `data:${mimeType};base64,${base64}` }; + return { image: `data:${mimeType};base64,${base64}`, imageUrl: url }; } catch (e) { - if (attempt < retries) continue; - return { error: `已获取结果,但图片下载时遇到错误: ${e.message}` }; + if (isRetryableError(e.message) && attempt < maxRetries) { + logger.warn('下载', `${e.message},重试 ${attempt}/${maxRetries}...`); + await new Promise(r => setTimeout(r, retryDelay * attempt)); + continue; + } + return { error: `已获取结果,但图片下载时遇到错误: ${e.message}`, imageUrl: url }; } } + + return { error: '下载失败: 已达最大重试次数', imageUrl: url }; }