下载非内核

This commit is contained in:
zhongluofeng
2026-07-22 18:26:26 +08:00
parent 1e31ee8da9
commit f7d3c13f35
24 changed files with 2609 additions and 2532 deletions
@@ -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 Downloaderaria2,嗅探网页资源。",
"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">
+8 -8
View File
@@ -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', '未连接')
}