193 lines
5.6 KiB
JavaScript
193 lines
5.6 KiB
JavaScript
/**
|
|
* 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: ['<all_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
|
|
}
|
|
})
|