下载非内核
This commit is contained in:
+162
-321
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user