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"> @@ -609,6 +640,9 @@ const handleRemoveWorker = (index) => { + + 将对选中的 {{ selectedRowKeys.length }} 个实例统一设置代理 +