下载非内核
This commit is contained in:
@@ -2,17 +2,17 @@
|
||||
* Thing Extension - 后台 Service Worker
|
||||
*
|
||||
* 职责:
|
||||
* 1. 接管浏览器下载,转发到 aria2
|
||||
* 1. 接管浏览器下载,转发到 Thing 下载引擎
|
||||
* 2. 资源嗅探:识别视频/音频/图片/压缩包/安装包等,存储到内存供 popup 查看
|
||||
* 3. 右键菜单"使用 aria2 下载"
|
||||
* 3. 右键菜单"使用 Thing 下载"
|
||||
*
|
||||
* 注:Service Worker 是短生命周期的,配置需持久化到 chrome.storage
|
||||
*/
|
||||
|
||||
// ===== 默认配置 =====
|
||||
const DEFAULT_CONFIG = {
|
||||
rpcUrl: 'http://127.0.0.1:6800/jsonrpc',
|
||||
rpcSecret: '',
|
||||
serverUrl: 'http://127.0.0.1:16800',
|
||||
secret: '',
|
||||
interceptDownload: true,
|
||||
minSize: 0,
|
||||
excludeDomains: [],
|
||||
@@ -301,48 +301,61 @@ async function saveConfig(config) {
|
||||
await chrome.storage.local.set({ config })
|
||||
}
|
||||
|
||||
// ===== aria2 JSON-RPC 调用 =====
|
||||
async function aria2Call(method, params = []) {
|
||||
// ===== Thing 下载引擎 REST API 调用 =====
|
||||
async function apiRequest(path, options = {}) {
|
||||
const config = await getConfig()
|
||||
const rpcParams = []
|
||||
if (config.rpcSecret) {
|
||||
rpcParams.push(`token:${config.rpcSecret}`)
|
||||
const url = config.serverUrl.replace(/\/$/, '') + path
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
if (config.secret) {
|
||||
headers['Authorization'] = `Bearer ${config.secret}`
|
||||
}
|
||||
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
|
||||
})
|
||||
const resp = await fetch(url, {
|
||||
...options,
|
||||
headers: { ...headers, ...options.headers }
|
||||
})
|
||||
if (!resp.ok) {
|
||||
throw new Error(`aria2 RPC HTTP ${resp.status}`)
|
||||
const text = await resp.text().catch(() => '')
|
||||
throw new Error(`API HTTP ${resp.status}: ${text || resp.statusText}`)
|
||||
}
|
||||
const data = await resp.json()
|
||||
if (data.error) {
|
||||
throw new Error(`aria2 RPC error: ${data.error.message} (${data.error.code})`)
|
||||
}
|
||||
return data.result
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 URI 下载任务到 aria2
|
||||
* 测试连接(GET /health)
|
||||
*/
|
||||
async function aria2AddUri(url, filename, referer, cookies, headers) {
|
||||
const options = {}
|
||||
if (filename) options.out = filename
|
||||
if (referer) options.referer = referer
|
||||
const headerList = []
|
||||
if (cookies) headerList.push(`Cookie: ${cookies}`)
|
||||
if (headers && headers.length) headerList.push(...headers)
|
||||
if (headerList.length) options.header = headerList
|
||||
options['user-agent'] = navigator.userAgent
|
||||
return aria2Call('aria2.addUri', [[url], options])
|
||||
async function testConnection() {
|
||||
const config = await getConfig()
|
||||
const url = config.serverUrl.replace(/\/$/, '') + '/health'
|
||||
const resp = await fetch(url)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加下载任务(POST /api/downloads)
|
||||
*/
|
||||
async function addDownload(url, filename, referer, cookies, headers) {
|
||||
const body = { url }
|
||||
if (filename) body.filename = filename
|
||||
// 构造请求头
|
||||
const headerMap = {}
|
||||
headerMap['User-Agent'] = navigator.userAgent
|
||||
if (referer) headerMap['Referer'] = referer
|
||||
if (cookies) headerMap['Cookie'] = cookies
|
||||
if (headers && headers.length) {
|
||||
for (const h of headers) {
|
||||
const idx = h.indexOf(':')
|
||||
if (idx > 0) {
|
||||
headerMap[h.slice(0, idx).trim()] = h.slice(idx + 1).trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
body.headers = headerMap
|
||||
const result = await apiRequest('/api/downloads', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
return result.id
|
||||
}
|
||||
|
||||
// ===== 下载拦截 =====
|
||||
@@ -373,10 +386,10 @@ async function handleDownloadCreated(downloadItem) {
|
||||
const filename = downloadItem.filename || ''
|
||||
|
||||
try {
|
||||
const gid = await aria2AddUri(url, filename, downloadItem.referrer, '')
|
||||
await notify('已添加到 aria2', `${filename || url}\nGID: ${gid}`)
|
||||
const id = await addDownload(url, filename, downloadItem.referrer, '')
|
||||
await notify('已添加到 Thing', `${filename || url}`)
|
||||
} catch (e) {
|
||||
await notify('aria2 添加失败', `${filename || url}\n${e.message}`)
|
||||
await notify('Thing 添加失败', `${filename || url}\n${e.message}`)
|
||||
try { await chrome.downloads.download({ url }) } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
@@ -399,7 +412,7 @@ async function notify(title, message) {
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'thing-download-link',
|
||||
title: '使用 aria2 下载此链接',
|
||||
title: '使用 Thing 下载此链接',
|
||||
contexts: ['link']
|
||||
})
|
||||
chrome.contextMenus.create({
|
||||
@@ -414,10 +427,10 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
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}`)
|
||||
await addDownload(url, filename, info.pageUrl, '')
|
||||
await notify('已添加到 Thing', `${filename || url}`)
|
||||
} catch (e) {
|
||||
await notify('aria2 添加失败', `${e.message}`)
|
||||
await notify('Thing 添加失败', `${e.message}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -603,8 +616,8 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
return true
|
||||
}
|
||||
if (msg.type === 'testConnection') {
|
||||
aria2Call('aria2.getVersion', [])
|
||||
.then(res => sendResponse({ ok: true, version: res.version }))
|
||||
testConnection()
|
||||
.then(() => sendResponse({ ok: true }))
|
||||
.catch(e => sendResponse({ ok: false, error: e.message }))
|
||||
return true
|
||||
}
|
||||
@@ -629,8 +642,8 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
}
|
||||
if (msg.type === 'downloadSniffed') {
|
||||
const { url, filename, referer } = msg
|
||||
aria2AddUri(url, filename, referer || '', '')
|
||||
.then(gid => sendResponse({ ok: true, gid }))
|
||||
addDownload(url, filename, referer || '', '')
|
||||
.then(id => sendResponse({ ok: true, id }))
|
||||
.catch(e => sendResponse({ ok: false, error: e.message }))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Thing Extension",
|
||||
"version": "0.2.0",
|
||||
"description": "发送浏览器下载到Thing Downloader(aria2),嗅探网页资源。",
|
||||
"description": "发送浏览器下载到 Thing 下载引擎,嗅探网页资源。",
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<img src="icons/icon-48.png" alt="Thing" class="logo" />
|
||||
<div class="title">
|
||||
<h1>Thing Extension</h1>
|
||||
<span class="subtitle">aria2 下载接管 · 资源嗅探</span>
|
||||
<span class="subtitle">Thing 下载引擎 · 资源嗅探</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -68,13 +68,13 @@
|
||||
|
||||
<form id="configForm">
|
||||
<label class="field">
|
||||
<span>RPC 地址</span>
|
||||
<input type="text" id="rpcUrl" placeholder="http://127.0.0.1:6800/jsonrpc" />
|
||||
<span>API 地址</span>
|
||||
<input type="text" id="serverUrl" placeholder="http://127.0.0.1:16800" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>RPC 密钥</span>
|
||||
<input type="password" id="rpcSecret" placeholder="未设置时留空" />
|
||||
<span>认证密钥</span>
|
||||
<input type="password" id="secret" placeholder="未设置时留空" />
|
||||
</label>
|
||||
|
||||
<label class="checkbox">
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
const $ = (id) => document.getElementById(id)
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
rpcUrl: 'http://127.0.0.1:6800/jsonrpc',
|
||||
rpcSecret: '',
|
||||
serverUrl: 'http://127.0.0.1:16800',
|
||||
secret: '',
|
||||
interceptDownload: true,
|
||||
minSize: 0,
|
||||
excludeDomains: [],
|
||||
@@ -53,8 +53,8 @@ async function loadConfig() {
|
||||
}
|
||||
|
||||
function fillForm(config) {
|
||||
$('rpcUrl').value = config.rpcUrl || DEFAULT_CONFIG.rpcUrl
|
||||
$('rpcSecret').value = config.rpcSecret || ''
|
||||
$('serverUrl').value = config.serverUrl || DEFAULT_CONFIG.serverUrl
|
||||
$('secret').value = config.secret || ''
|
||||
$('interceptDownload').checked = config.interceptDownload !== false
|
||||
$('sniffEnabled').checked = config.sniffEnabled !== false
|
||||
$('showNotifications').checked = config.showNotifications !== false
|
||||
@@ -65,8 +65,8 @@ function fillForm(config) {
|
||||
|
||||
function readForm() {
|
||||
return {
|
||||
rpcUrl: $('rpcUrl').value.trim() || DEFAULT_CONFIG.rpcUrl,
|
||||
rpcSecret: $('rpcSecret').value.trim(),
|
||||
serverUrl: $('serverUrl').value.trim() || DEFAULT_CONFIG.serverUrl,
|
||||
secret: $('secret').value.trim(),
|
||||
interceptDownload: $('interceptDownload').checked,
|
||||
sniffEnabled: $('sniffEnabled').checked,
|
||||
showNotifications: $('showNotifications').checked,
|
||||
@@ -97,7 +97,7 @@ $('testBtn').addEventListener('click', async () => {
|
||||
await send('saveConfig', { config })
|
||||
const res = await send('testConnection', {})
|
||||
if (res && res.ok) {
|
||||
setStatus('ok', `已连接 · aria2 ${res.version}`)
|
||||
setStatus('ok', '已连接 · Thing 下载引擎')
|
||||
} else {
|
||||
setStatus('fail', '连接失败:' + (res?.error || '未知错误'))
|
||||
}
|
||||
@@ -312,7 +312,7 @@ document.querySelectorAll('.sniff-tab').forEach(btn => {
|
||||
setStatus('', '检测中...')
|
||||
const res = await send('testConnection', {})
|
||||
if (res && res.ok) {
|
||||
setStatus('ok', `已连接 · aria2 ${res.version}`)
|
||||
setStatus('ok', '已连接 · Thing 下载引擎')
|
||||
} else {
|
||||
setStatus('fail', '未连接')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user