下载非内核

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
+162 -321
View File
@@ -8,285 +8,141 @@ const logger = createLogger('downloader')
// ===== 与 Rust 端对应的数据结构(camelCase =====
export type TaskStatus = 'queued' | 'active' | 'paused' | 'complete' | 'error'
export interface Segment {
index: number
start: number
end: number
completed: number
}
export interface DownloadTask {
id: string
url: string
filename: string
dir: string
status: TaskStatus
totalSize: number
completedSize: number
speed: number
supportsResume: boolean
segments: Segment[]
error: string | null
createdAt: number
headers: Record<string, string>
}
export interface DownloaderSettings {
rpcPort: number
rpcSecret: string
downloadDir: string
maxConcurrent: number
maxConnectionPerServer: number
split: number
maxConnections: number
continueDownload: boolean
autoStart: boolean
speedLimit: number
kernelMirrors: string[]
}
/** 内核安装进度事件载荷,对应 Rust 端 InstallProgress */
export interface InstallProgress {
/** downloading | extracting | replacing | done | error */
stage: string
percent: number
downloadedBytes: number
totalBytes: number | null
message: string
}
export interface KernelInfo {
path: string
exists: boolean
version: string | null
}
export interface KernelUpdateInfo {
currentVersion: string | null
latestVersion: string
downloadUrl: string
hasUpdate: boolean
globalSpeedLimit: number
extensionPort: number
extensionSecret: string
deleteFilesOnRemove: boolean
}
export interface DownloaderStatus {
running: boolean
pid: number | null
restartCount: number
}
export interface RpcInfo {
rpcUrl: string
rpcSecret: string
extensionPath: string | null
export interface ExtensionInfo {
url: string
port: number
secret: string
hasSecret: boolean
}
/** aria2 任务文件信息 */
export interface Aria2File {
path: string
length: number
completedLength: number
selected: boolean
/** 下载进度事件载荷 */
interface ProgressPayload {
id: string
completedSize: number
totalSize: number
speed: number
status: TaskStatus
}
/** aria2 任务状态(tellActive/tellWaiting/tellStopped 返回项) */
export interface Aria2Task {
gid: string
status: 'active' | 'waiting' | 'paused' | 'complete' | 'removed' | 'error' | string
totalLength: string
completedLength: string
downloadSpeed: string
uploadSpeed: string
connections: string
dir: string
files?: Aria2File[]
bittorrent?: { info?: { name?: string } } | null
errorCode?: string
errorMessage?: string
}
/** 全局统计 */
export interface GlobalStat {
downloadSpeed: string
uploadSpeed: string
numActive: string
numWaiting: string
numStopped: string
numStoppedTotal: string
}
export interface Aria2Version {
version: string
enabledFeatures?: string[]
/** 下载完成事件载荷 */
interface CompletePayload {
id: string
filename: string
status: TaskStatus
error: string | null
}
export const useDownloaderStore = defineStore('downloader', () => {
const kernel = ref<KernelInfo | null>(null)
const status = ref<DownloaderStatus>({ running: false, pid: null, restartCount: 0 })
const version = ref<string>('')
const tasks = ref<DownloadTask[]>([])
const status = ref<DownloaderStatus>({ running: false })
const settings = ref<DownloaderSettings | null>(null)
const rpcInfo = ref<RpcInfo | null>(null)
const extensionInfo = ref<ExtensionInfo | null>(null)
const activeTasks = ref<Aria2Task[]>([])
const waitingTasks = ref<Aria2Task[]>([])
const stoppedTasks = ref<Aria2Task[]>([])
const globalStat = ref<GlobalStat | null>(null)
// ===== 任务历史持久化(localStorage =====
// 即使 aria2 未启动,也能展示最近一次的任务快照
const HISTORY_KEY = 'thing.downloader.taskHistory'
const HISTORY_MAX = 200 // 最多保留 200 条历史记录
/** 将当前任务快照保存到 localStorage(合并 active+waiting+stopped,按 gid 去重) */
const persistHistory = () => {
try {
const map = new Map<string, Aria2Task>()
// 先读已有历史,作为基底
const raw = localStorage.getItem(HISTORY_KEY)
if (raw) {
const existing: Aria2Task[] = JSON.parse(raw)
for (const t of existing) map.set(t.gid, t)
}
// 用最新任务覆盖(active/waiting/stopped 都是最新的)
for (const t of activeTasks.value) map.set(t.gid, t)
for (const t of waitingTasks.value) map.set(t.gid, t)
for (const t of stoppedTasks.value) map.set(t.gid, t)
// 限制条数:优先保留 stopped(已完成/错误),其次 waiting,最后 active
const all = Array.from(map.values())
const priority = { complete: 0, error: 0, removed: 1, active: 2, waiting: 2, paused: 2 } as Record<string, number>
all.sort((a, b) => (priority[a.status] ?? 3) - (priority[b.status] ?? 3))
const trimmed = all.slice(0, HISTORY_MAX)
localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmed))
} catch (e) {
logger.error('保存任务历史失败: ' + e)
}
}
/** 从 localStorage 加载任务历史,填充到 stoppedTasks(作为历史展示) */
const loadHistory = () => {
try {
const raw = localStorage.getItem(HISTORY_KEY)
if (!raw) return
const history: Aria2Task[] = JSON.parse(raw)
if (!Array.isArray(history)) return
// 仅在没有实时任务时填充(避免覆盖实时数据)
if (stoppedTasks.value.length === 0) {
stoppedTasks.value = history
}
} catch (e) {
logger.error('加载任务历史失败: ' + e)
}
}
// ===== 内核安装进度 =====
const installing = ref(false)
const installProgress = ref<InstallProgress | null>(null)
// 事件监听器
let progressUnlisten: UnlistenFn | null = null
let completeUnlisten: UnlistenFn | null = null
let addedUnlisten: UnlistenFn | null = null
/** 内核信息(同时尝试从 resource 提取到 cores/ */
const refreshKernel = async () => {
// ===== 任务列表 =====
const refreshTasks = async () => {
try {
kernel.value = await invoke<KernelInfo>('downloader_kernel_info')
tasks.value = await invoke<DownloadTask[]>('downloader_get_tasks')
} catch (e) {
logger.error('获取内核信息失败: ' + e)
logger.error('获取任务列表失败: ' + e)
}
return kernel.value
return tasks.value
}
/** 刷新进程状态 */
const refreshStatus = async () => {
try {
status.value = await invoke<DownloaderStatus>('downloader_status')
} catch (e) {
logger.error('获取进程状态失败: ' + e)
}
return status.value
}
const start = async () => {
await invoke('downloader_start')
await refreshStatus()
}
const stop = async () => {
await invoke('downloader_stop')
await refreshStatus()
}
const restart = async () => {
await invoke('downloader_restart')
await refreshStatus()
}
/** 等待 aria2 RPC 就绪(轮询 version 接口,最多等 10 秒) */
const waitForApi = async (timeoutMs = 10000): Promise<boolean> => {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
await invoke<Aria2Version>('downloader_version')
return true
} catch {
await new Promise((r) => setTimeout(r, 500))
}
}
return false
}
/** 获取 aria2 版本(仅运行时可用) */
const refreshVersion = async () => {
try {
const v = await invoke<Aria2Version>('downloader_version')
version.value = v.version
} catch {
version.value = ''
/** 根据进度事件更新单个任务(不触发完整刷新) */
const updateTaskProgress = (payload: ProgressPayload) => {
const task = tasks.value.find((t) => t.id === payload.id)
if (task) {
task.completedSize = payload.completedSize
task.totalSize = payload.totalSize
task.speed = payload.speed
task.status = payload.status
}
}
// ---------- 任务列表 ----------
const refreshActive = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_active')
activeTasks.value = res ?? []
} catch (e) {
logger.error('获取活跃任务失败: ' + e)
}
return activeTasks.value
/** 下载完成事件处理 */
const handleTaskComplete = async (_payload: CompletePayload) => {
// 刷新任务列表以获取最终状态
await refreshTasks()
}
const refreshWaiting = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_waiting')
waitingTasks.value = res ?? []
} catch (e) {
logger.error('获取等待任务失败: ' + e)
}
return waitingTasks.value
// ===== 任务操作 =====
const addTask = async (
url: string,
filename?: string,
dir?: string,
headers?: Record<string, string>
): Promise<string> => {
const id = await invoke<string>('downloader_add_task', {
url,
filename: filename || null,
dir: dir || null,
headers: headers || null
})
await refreshTasks()
return id
}
const refreshStopped = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_stopped')
stoppedTasks.value = res ?? []
} catch (e) {
logger.error('获取已完成任务失败: ' + e)
}
return stoppedTasks.value
const pauseTask = async (id: string) => {
await invoke('downloader_pause_task', { id })
await refreshTasks()
}
/** 刷新全部任务(active + waiting + stopped */
const refreshAllTasks = async () => {
await Promise.all([refreshActive(), refreshWaiting(), refreshStopped()])
// 刷新后持久化历史快照
persistHistory()
const resumeTask = async (id: string) => {
await invoke('downloader_resume_task', { id })
await refreshTasks()
}
const refreshGlobalStat = async () => {
try {
globalStat.value = await invoke<GlobalStat>('downloader_get_global_stat')
} catch (e) {
logger.error('获取全局统计失败: ' + e)
}
return globalStat.value
const removeTask = async (id: string, deleteFiles = false) => {
await invoke('downloader_remove_task', { id, deleteFiles })
await refreshTasks()
}
// ---------- 任务操作 ----------
const addUri = async (uris: string[], options?: Record<string, unknown>) => {
const opts = options ? (JSON.parse(JSON.stringify(options)) as unknown) : undefined
return await invoke<string>('downloader_add_uri', { uris, options: opts })
}
const pauseTask = async (gid: string) => {
await invoke('downloader_pause', { gid })
}
const unpauseTask = async (gid: string) => {
await invoke('downloader_unpause', { gid })
}
const removeTask = async (gid: string) => {
await invoke('downloader_remove', { gid })
}
const changeGlobalOption = async (options: Record<string, string>) => {
await invoke('downloader_change_global_option', { options })
}
// ---------- 设置 ----------
// ===== 设置 =====
const loadSettings = async () => {
settings.value = await invoke<DownloaderSettings>('downloader_get_settings')
return settings.value
@@ -297,107 +153,92 @@ export const useDownloaderStore = defineStore('downloader', () => {
settings.value = s
}
// ---------- RPC 信息 ----------
const loadRpcInfo = async () => {
rpcInfo.value = await invoke<RpcInfo>('downloader_get_rpc_info')
return rpcInfo.value
}
// ---------- 内核更新 / 安装 ----------
const checkKernelUpdate = async (): Promise<KernelUpdateInfo> => {
return await invoke<KernelUpdateInfo>('downloader_check_kernel_update')
}
const updateKernel = async (mirrorPrefix: string = '') => {
await invoke('downloader_update_kernel', { mirrorPrefix })
await refreshKernel()
}
/**
* 首次安装内核:调用后端 install_kernel,监听 downloader-kernel-install-progress 事件更新进度
* @param mirrorPrefix 镜像源前缀(空串=GitHub 直连)
*/
const installKernel = async (mirrorPrefix: string = ''): Promise<void> => {
if (installing.value) return
installing.value = true
installProgress.value = {
stage: 'downloading',
percent: 0,
downloadedBytes: 0,
totalBytes: null,
message: '准备开始下载...'
// ===== 状态 =====
const refreshStatus = async () => {
try {
status.value = await invoke<DownloaderStatus>('downloader_status')
} catch (e) {
logger.error('获取状态失败: ' + e)
}
return status.value
}
// ===== 扩展信息 =====
const loadExtensionInfo = async () => {
extensionInfo.value = await invoke<ExtensionInfo>('downloader_get_extension_info')
return extensionInfo.value
}
// ===== 事件监听 =====
const startEventListeners = async () => {
if (progressUnlisten && completeUnlisten && addedUnlisten) return
if (!progressUnlisten) {
progressUnlisten = await listen<InstallProgress>('downloader-kernel-install-progress', (e) => {
installProgress.value = e.payload
progressUnlisten = await listen<ProgressPayload>('download-progress', (e) => {
updateTaskProgress(e.payload)
})
}
try {
await invoke('downloader_install_kernel', { mirrorPrefix })
await refreshKernel()
} catch (e) {
logger.error('内核安装失败: ' + e)
throw e
} finally {
installing.value = false
if (progressUnlisten) {
progressUnlisten()
progressUnlisten = null
}
if (!completeUnlisten) {
completeUnlisten = await listen<CompletePayload>('download-complete', (e) => {
handleTaskComplete(e.payload)
})
}
if (!addedUnlisten) {
addedUnlisten = await listen<{ id: string }>('download-added', () => {
// 新任务加入(可能是扩展通过 HTTP API 添加的),刷新任务列表
refreshTasks()
})
}
}
const clearInstallProgress = () => {
installProgress.value = null
const stopEventListeners = () => {
if (progressUnlisten) {
progressUnlisten()
progressUnlisten = null
}
if (completeUnlisten) {
completeUnlisten()
completeUnlisten = null
}
if (addedUnlisten) {
addedUnlisten()
addedUnlisten = null
}
}
/** 用系统资源管理器打开目录(Rust 端绕过 opener scope 限制) */
// ===== 初始化 =====
const init = async () => {
await Promise.all([refreshStatus(), loadSettings(), loadExtensionInfo(), refreshTasks()])
await startEventListeners()
}
// ===== 工具函数 =====
const openDir = (path: string) => invoke<void>('downloader_open_dir', { path })
return {
// state
kernel,
tasks,
status,
version,
settings,
rpcInfo,
activeTasks,
waitingTasks,
stoppedTasks,
globalStat,
installing,
installProgress,
// kernel & process
refreshKernel,
refreshStatus,
start,
stop,
restart,
waitForApi,
refreshVersion,
extensionInfo,
// tasks
refreshActive,
refreshWaiting,
refreshStopped,
refreshAllTasks,
refreshGlobalStat,
addUri,
refreshTasks,
addTask,
pauseTask,
unpauseTask,
resumeTask,
removeTask,
changeGlobalOption,
// settings
loadSettings,
saveSettings,
// rpc info
loadRpcInfo,
// kernel update / install
checkKernelUpdate,
updateKernel,
installKernel,
clearInstallProgress,
openDir,
loadHistory,
persistHistory
// status
refreshStatus,
// extension
loadExtensionInfo,
// events
startEventListeners,
stopEventListeners,
// init
init,
// utils
openDir
}
})
+4
View File
@@ -98,6 +98,9 @@ export const useProxyStore = defineStore('proxy', () => {
const settings = ref<ProxySettings | null>(null)
const systemProxy = ref(false)
/** 是否已完成首次加载(避免初始 null/false 导致闪烁误导状态) */
const initialized = ref(false)
// ===== 内核安装进度 =====
const installing = ref(false)
const installProgress = ref<InstallProgress | null>(null)
@@ -325,6 +328,7 @@ export const useProxyStore = defineStore('proxy', () => {
proxies,
settings,
systemProxy,
initialized,
installing,
installProgress,
// kernel & process