/** * Thing Extension - 后台 Service Worker * * 职责: * 1. 接管浏览器下载,转发到 aria2 * 2. 与本应用(可选)通过 JSON-RPC 通信 * 3. 提供右键菜单"使用 aria2 下载链接" * 4. 预留资源嗅探能力(webRequest 监听) * * 注:Service Worker 是短生命周期的,配置需持久化到 chrome.storage */ // ===== 默认配置 ===== const DEFAULT_CONFIG = { rpcUrl: 'http://127.0.0.1:6800/jsonrpc', rpcSecret: '', // 是否拦截浏览器原生下载 interceptDownload: true, // 文件大小阈值(字节),超过才转 aria2。0 = 全部转 minSize: 0, // 排除的域名(这些域名的下载走浏览器原生) excludeDomains: [], // 是否显示桌面通知 showNotifications: true } // ===== 配置读取 ===== async function getConfig() { const stored = await chrome.storage.local.get('config') return { ...DEFAULT_CONFIG, ...(stored.config || {}) } } async function saveConfig(config) { await chrome.storage.local.set({ config }) } // ===== aria2 JSON-RPC 调用 ===== async function aria2Call(method, params = []) { const config = await getConfig() const rpcParams = [] if (config.rpcSecret) { rpcParams.push(`token:${config.rpcSecret}`) } rpcParams.push(...params) const resp = await fetch(config.rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: Date.now().toString(), method, params: rpcParams }) }) if (!resp.ok) { throw new Error(`aria2 RPC HTTP ${resp.status}`) } const data = await resp.json() if (data.error) { throw new Error(`aria2 RPC error: ${data.error.message} (${data.error.code})`) } return data.result } /** * 添加 URI 下载任务到 aria2 * @returns {string} gid */ async function aria2AddUri(url, filename, referer, cookies) { const options = {} if (filename) options.out = filename if (referer) options.referer = referer if (cookies) options.header = [`Cookie: ${cookies}`] // User-Agent 用浏览器默认值更兼容 options['user-agent'] = navigator.userAgent return aria2Call('aria2.addUri', [[url], options]) } // ===== 下载拦截 ===== async function shouldIntercept(downloadItem) { const config = await getConfig() if (!config.interceptDownload) return false // 大小阈值 if (config.minSize > 0 && downloadItem.fileSize > 0 && downloadItem.fileSize < config.minSize) { return false } // 域名排除 try { const url = new URL(downloadItem.finalUrl || downloadItem.url) if (config.excludeDomains.some(d => url.hostname.includes(d))) { return false } } catch { /* ignore */ } return true } async function handleDownloadCreated(downloadItem) { if (!await shouldIntercept(downloadItem)) return // 立即取消浏览器原生下载 try { await chrome.downloads.cancel(downloadItem.id) await chrome.downloads.erase({ id: downloadItem.id }) } catch { /* ignore */ } const url = downloadItem.finalUrl || downloadItem.url const filename = downloadItem.filename || '' try { const gid = await aria2AddUri(url, filename, downloadItem.referrer, '') await notify('已添加到 aria2', `${filename || url}\nGID: ${gid}`) } catch (e) { await notify('aria2 添加失败', `${filename || url}\n${e.message}`) // 失败时把 URL 重新交给浏览器下载 try { await chrome.downloads.download({ url }) } catch { /* ignore */ } } } // ===== 通知 ===== async function notify(title, message) { const config = await getConfig() if (!config.showNotifications) return try { await chrome.notifications.create({ type: 'basic', iconUrl: 'icons/icon-128.png', title, message }) } catch { /* ignore */ } } // ===== 右键菜单 ===== chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: 'thing-download-link', title: '使用 aria2 下载此链接', contexts: ['link'] }) chrome.contextMenus.create({ id: 'thing-download-page', title: '使用 aria2 下载当前页面资源', contexts: ['page'] }) }) chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === 'thing-download-link') { const url = info.linkUrl const filename = url.split('/').pop()?.split('?')[0] || '' try { const gid = await aria2AddUri(url, filename, info.pageUrl, '') await notify('已添加到 aria2', `${filename || url}\nGID: ${gid}`) } catch (e) { await notify('aria2 添加失败', `${e.message}`) } } }) // ===== 下载事件监听 ===== chrome.downloads.onCreated.addListener(handleDownloadCreated) // ===== 资源嗅探(预留,仅日志,不拦截)===== // 后续可启用:监听页面媒体资源,提供"嗅探到的资源"列表 chrome.webRequest.onBeforeRequest.addListener( (details) => { // 预留:识别视频/音频流等可下载资源 // 当前不处理,仅保留权限和入口 return undefined }, { urls: [''] }, [] ) // ===== 来自 popup 的消息 ===== chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type === 'getConfig') { getConfig().then(sendResponse) return true } if (msg.type === 'saveConfig') { saveConfig(msg.config).then(() => sendResponse({ ok: true })).catch(e => sendResponse({ ok: false, error: e.message })) return true } if (msg.type === 'testConnection') { aria2Call('aria2.getVersion', []) .then(res => sendResponse({ ok: true, version: res.version })) .catch(e => sendResponse({ ok: false, error: e.message })) return true } })