From f03789759069f7f229427bb64e9b15edb0735592 Mon Sep 17 00:00:00 2001 From: meishibiezb <750783119@qq.com> Date: Tue, 5 May 2026 02:47:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/background.ts | 85 +++++++++++++++++++++++++++++++++++++++ entrypoints/popup/main.ts | 38 ++++++++--------- wxt.config.ts | 4 +- 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/entrypoints/background.ts b/entrypoints/background.ts index f96fa48..32d0b08 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,3 +1,88 @@ export default defineBackground(() => { console.log('Hello background!', { id: browser.runtime.id }); + + // 监听下载请求 + browser.runtime.onMessage.addListener(async (msg) => { + if (msg.type === 'DOWNLOAD_PINS') { + const pins: PinData[] = msg.pins; + const concurrency: number = msg.concurrency || 3; + const result = await startBatchDownload(pins, concurrency); + // 通知 popup 下载完成 + browser.runtime.sendMessage({ + type: 'DOWNLOAD_DONE', + success: result.success, + failed: result.failed, + total: result.total, + }); + } + }); }); + +// 定义单个下载任务 (封装成 Promise) +async function downloadOne(pin: PinData): Promise { + return new Promise(async (resolve) => { + // 处理文件名非法字符(清理文件名) + const safeName = (pin.title || 'image').replace(/[\n\r\t]/g, ' ').replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ').trim().slice(0, 100); + const filename = `Pins/${safeName}.jpg`; + + browser.downloads.download({ + url: pin.imgSrc, + filename: filename, + headers: [{ name: 'Referer', value: 'https://huaban.com/' }], + conflictAction: 'uniquify' + }, (id) => { + if (!id) { + console.warn(`跳过: ${pin.title} (可能 URL 无效)`); + return resolve(); // 即使失败也要 resolve,否则会阻塞队列 + } + + // 监听下载状态,只有当下载真正完成(或中断)时才释放资源 + const listener = (delta: Browser.downloads.DownloadDelta) => { + if (delta.id !== id) return; + + if (delta.state && delta.state.current === 'complete') { + browser.downloads.onChanged.removeListener(listener); + resolve(); + } + // 处理中断或取消的情况,防止队列卡死 + else if (delta.state && delta.state.current === 'interrupted') { + browser.downloads.onChanged.removeListener(listener); + console.warn(`下载中断: ${pin.title}`, JSON.stringify(delta)); + resolve(); + } + }; + browser.downloads.onChanged.addListener(listener); + }); + }); +} + +// 并发调度器 +export async function startBatchDownload(pins: PinData[], limit: number = 3) { + let success = 0; + let failed = 0; + + const executing: Promise[] = []; // 正在运行的任务池 + + for (const pin of pins) { + // 创建任务 + const task = downloadOne(pin); + + // 加入任务池 + const wrapper = task.finally(() => { + const idx = executing.indexOf(wrapper); + if (idx !== -1) executing.splice(idx, 1); + }); + executing.push(wrapper); + + // 关键逻辑:如果池子满了(达到 limit),等待任意一个任务完成 + if (executing.length >= limit) { + await Promise.race(executing); + } + } + + // 等待剩余任务全部完成 + await Promise.all(executing); + console.log("所有下载任务已处理完毕"); + + return { success, failed, total: pins.length }; +} \ No newline at end of file diff --git a/entrypoints/popup/main.ts b/entrypoints/popup/main.ts index 93a824d..f8c9b43 100644 --- a/entrypoints/popup/main.ts +++ b/entrypoints/popup/main.ts @@ -1,5 +1,6 @@ import './style.css'; import type { PinData } from '../../components/pin-collector'; +import { startBatchDownload } from '../background'; document.querySelector('#app')!.innerHTML = ` -
- -
@@ -24,19 +20,18 @@ document.querySelector('#app')!.innerHTML = ` document.getElementById('collectBtn')!.addEventListener('click', async () => { showStatus('收集中...', 'info'); try { - const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); - if (!tab?.id) { - showStatus('无法获取当前标签页', 'error'); - return; - } - const response = await browser.tabs.sendMessage(tab.id, { - type: 'COLLECT_PINS', - }); - if (response?.success) { - // await browser.storage.local.set({ collectedPins: response.pins }); - // renderPins(response.pins as PinData[]); - await loadAndRender(); - showStatus(`✅ 已收集 ${response.count} 个 Pin`, 'success'); + await loadAndRender(); + + // 下载 + const result = (await browser.storage.local.get('collectedPins')) as { + collectedPins?: PinData[]; + }; + const pins: PinData[] = result.collectedPins || []; + if (pins.length > 0) { + showStatus(`开始下载 ${pins.length} 张...`, 'info'); + browser.runtime.sendMessage({ type: 'DOWNLOAD_PINS', pins, concurrency: 3 }); + } else { + showStatus('没有可下载的图片', 'error'); } } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); @@ -126,4 +121,11 @@ browser.runtime.onMessage.addListener(async (msg) => { `✅ 加载完成 · 共 ${msg.totalItems} 条`; } } + if (msg.type === 'DOWNLOAD_DONE') { + showStatus( + `✅ 下载完成: ${msg.success}/${msg.total}` + + (msg.failed > 0 ? ` (${msg.failed} 失败)` : ''), + 'success' + ); + } }); \ No newline at end of file diff --git a/wxt.config.ts b/wxt.config.ts index 3b4619f..3b7e7d0 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -2,8 +2,8 @@ import { defineConfig } from 'wxt'; // See https://wxt.dev/api/config.html export default defineConfig({ - browser: "firefox", - manifest: { + browser: "firefox", + manifest: { permissions: ['storage', 'downloads'], }, });