From 38f9fd3b848d798e597ee613521576da77c039ae Mon Sep 17 00:00:00 2001 From: daidai Date: Sat, 28 Mar 2026 12:38:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E9=87=8D=E8=AF=95UI=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=85=BC=E5=AE=B9=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复 workers.vue 中"图片下载重试"UI (imgDlRetry, imgDlRetryMaxRetries) - 恢复批量代理弹窗提示文字 - 修复实例标识:有 id 用 id,没有用 name - download.js 参数名 maxRetries -> retries,与 adapter 调用一致 - routes.js 重试下载读取配置并传递 retries --- src/backend/utils/download.js | 14 +++---- src/server/api/admin/routes.js | 5 ++- webui/src/components/settings/workers.vue | 46 ++++++++++++++++++++--- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/backend/utils/download.js b/src/backend/utils/download.js index 49714c0..e7ab9c7 100644 --- a/src/backend/utils/download.js +++ b/src/backend/utils/download.js @@ -21,22 +21,22 @@ function isRetryableError(message) { * @param {import('playwright-core').Page} page - Playwright 页面对象 * @param {object} [options] - 可选配置 * @param {number} [options.timeout=60000] - 超时时间(毫秒) - * @param {number} [options.maxRetries=3] - 最大重试次数 + * @param {number} [options.retries=3] - 最大重试次数 * @param {number} [options.retryDelay=1000] - 重试延迟基数(毫秒) * @returns {Promise<{ image?: string, imageUrl?: string, error?: string }>} 下载结果(包含原始 URL) */ export async function useContextDownload(url, page, options = {}) { - const { timeout = 120000, maxRetries = 3, retryDelay = 1000 } = options; + const { timeout = 120000, retries = 3, retryDelay = 1000 } = options; - for (let attempt = 1; attempt <= maxRetries; attempt++) { + for (let attempt = 1; attempt <= retries; attempt++) { try { const response = await page.request.get(url, { timeout }); if (!response.ok()) { const status = response.status(); // 5xx 错误可重试 - if (status >= 500 && attempt < maxRetries) { - logger.warn('下载', `HTTP ${status},重试 ${attempt}/${maxRetries}...`); + if (status >= 500 && attempt < retries) { + logger.warn('下载', `HTTP ${status},重试 ${attempt}/${retries}...`); await new Promise(r => setTimeout(r, retryDelay * attempt)); continue; } @@ -50,8 +50,8 @@ export async function useContextDownload(url, page, options = {}) { return { image: `data:${mimeType};base64,${base64}`, imageUrl: url }; } catch (e) { - if (isRetryableError(e.message) && attempt < maxRetries) { - logger.warn('下载', `${e.message},重试 ${attempt}/${maxRetries}...`); + if (isRetryableError(e.message) && attempt < retries) { + logger.warn('下载', `${e.message},重试 ${attempt}/${retries}...`); await new Promise(r => setTimeout(r, retryDelay * attempt)); continue; } diff --git a/src/server/api/admin/routes.js b/src/server/api/admin/routes.js index 71d75e7..4cd3273 100644 --- a/src/server/api/admin/routes.js +++ b/src/server/api/admin/routes.js @@ -572,7 +572,10 @@ export function createAdminRouter(context) { const poolContext = queueManager?.getPoolContext?.(); const page = poolContext?.getFirstPage?.(); if (page) { - downloadFn = (url) => useContextDownload(url, page); + const imgDlCfg = config?.backend?.pool?.failover || {}; + downloadFn = (url) => useContextDownload(url, page, { + retries: imgDlCfg.imgDlRetry ? (imgDlCfg.imgDlRetryMaxRetries || 3) : 1 + }); } } catch { /* Pool 未初始化,使用后备方案 */ } diff --git a/webui/src/components/settings/workers.vue b/webui/src/components/settings/workers.vue index f297de7..9655492 100644 --- a/webui/src/components/settings/workers.vue +++ b/webui/src/components/settings/workers.vue @@ -87,6 +87,9 @@ const instanceData = computed({ set: (val) => { settingsStore.workerConfig = val; } }); +// 获取实例唯一标识(优先 id,没有则用 name) +const getInstanceKey = (inst) => inst.id || inst.name; + // 批量选择 const selectedRowKeys = ref([]); const rowSelection = computed(() => ({ @@ -121,7 +124,7 @@ const openBatchProxy = () => { const handleBatchProxySave = async () => { const newList = (instanceData.value || []).map(inst => { - if (!selectedRowKeys.value.includes(inst.name)) return inst; + if (!selectedRowKeys.value.includes(getInstanceKey(inst))) return inst; return { ...inst, proxy: batchProxyForm.value.proxy ? { @@ -152,7 +155,7 @@ const handleBatchDelete = () => { cancelText: '取消', async onOk() { const newList = (instanceData.value || []).filter( - inst => !selectedRowKeys.value.includes(inst.name) + inst => !selectedRowKeys.value.includes(getInstanceKey(inst)) ); const success = await settingsStore.saveWorkerConfig(newList); if (success) { @@ -225,7 +228,8 @@ const handleEdit = (record) => { // 删除实例 const handleDelete = async (record) => { - const newList = instanceData.value.filter(item => item.name !== record.name); + const key = getInstanceKey(record); + const newList = instanceData.value.filter(item => getInstanceKey(item) !== key); await settingsStore.saveWorkerConfig(newList); }; @@ -253,8 +257,9 @@ const handleSaveEdit = async () => { // 创建 newList.push(instanceToSave); } else { - // 更新 - 用原始 name 查找 - const index = newList.findIndex(item => item.name === editingInstance.value.name); + // 更新 - 用唯一标识查找 + const editingKey = getInstanceKey(editingInstance.value); + const index = newList.findIndex(item => getInstanceKey(item) === editingKey); if (index > -1) { newList[index] = instanceToSave; } @@ -374,6 +379,32 @@ const handleRemoveWorker = (index) => { + + + + + +
+
图片下载重试
+
+ 启用后,图片/视频下载失败时会自动重试下载(不重新生成) +
+ +
+
+ + +
+
下载重试次数
+
+ 图片下载失败时的最大重试次数,范围 1-10 +
+ +
+
+
@@ -408,7 +439,7 @@ const handleRemoveWorker = (index) => { + :row-selection="rowSelection" :row-key="record => record.id || record.name">