复用下载中心(1/2)

This commit is contained in:
2026-04-12 17:53:16 +08:00
parent 6fcfa438d9
commit e2f59b3cdf
9 changed files with 532 additions and 306 deletions

View File

@@ -0,0 +1,368 @@
/**
* 共享的安装/下载逻辑
* 被 install-manager.ts 和 update-center 共同使用
*/
import { spawn, ChildProcess } from "node:child_process";
import { createWriteStream } from "node:fs";
import * as fs from "node:fs";
import * as path from "node:path";
import axios from "axios";
import pino from "pino";
const logger = pino({ name: "shared-installer" });
export const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
export interface DownloadOptions {
pkgname: string;
metalinkUrl: string;
filename: string;
downloadDir: string;
onLog?: (msg: string) => void;
onProgress?: (progress: number) => void;
onStatus?: (status: string) => void;
signal?: AbortSignal;
}
export interface DownloadResult {
filePath: string;
downloadDir: string;
}
/**
* 下载 metalink 文件并使用 aria2c 下载 deb 包
* 与 install-manager.ts 中的下载逻辑保持一致
*/
export const downloadPackage = async ({
pkgname,
metalinkUrl,
filename,
downloadDir,
onLog,
onProgress,
onStatus,
signal,
}: DownloadOptions): Promise<DownloadResult> => {
// 1. 创建下载目录
try {
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir, { recursive: true });
}
} catch (err) {
logger.error(`无法创建目录 ${downloadDir}: ${err}`);
throw err;
}
const metalinkPath = path.join(downloadDir, `${filename}.metalink`);
onLog?.(`正在获取 Metalink 文件: ${metalinkUrl}`);
// 2. 下载 metalink 文件
const response = await axios.get(metalinkUrl, {
baseURL: "https://erotica.spark-app.store",
responseType: "stream",
});
const writer = createWriteStream(metalinkPath);
response.data.pipe(writer);
await new Promise<void>((resolve, reject) => {
writer.on("finish", resolve);
writer.on("error", reject);
});
onLog?.("Metalink 文件下载完成");
// 3. 清理下载目录中的旧文件(保留 .metalink 文件)
const existingFiles = fs.readdirSync(downloadDir);
for (const file of existingFiles) {
if (file.endsWith(".metalink")) continue;
const filePath = path.join(downloadDir, file);
try {
fs.unlinkSync(filePath);
onLog?.(`已清理旧文件: ${file}`);
} catch (err) {
logger.warn(`清理文件失败 ${filePath}: ${err}`);
}
}
// 4. 使用 aria2c 下载 deb 文件
const aria2Args = [
`--dir=${downloadDir}`,
"--allow-overwrite=true",
"--summary-interval=1",
"--connect-timeout=10",
"--timeout=15",
"--max-tries=3",
"--retry-wait=5",
"--max-concurrent-downloads=4",
"--min-split-size=1M",
"--lowest-speed-limit=1K",
"--auto-file-renaming=false",
"-M",
metalinkPath,
];
onStatus?.("downloading");
// 下载重试逻辑每次超时时间递增最多3次
const timeoutList = [3000, 5000, 15000];
let retryCount = 0;
let downloadSuccess = false;
while (retryCount < timeoutList.length && !downloadSuccess) {
const currentTimeout = timeoutList[retryCount];
if (retryCount > 0) {
onLog?.(`${retryCount} 次重试下载...`);
onProgress?.(0);
// 重试前清理旧文件
const retryFiles = fs.readdirSync(downloadDir);
for (const file of retryFiles) {
if (file.endsWith(".metalink")) continue;
const filePath = path.join(downloadDir, file);
try {
fs.unlinkSync(filePath);
} catch (cleanErr) {
logger.warn(`重试清理文件失败 ${filePath}: ${cleanErr}`);
}
}
}
try {
await new Promise<void>((resolve, reject) => {
onLog?.(`启动下载: aria2c ${aria2Args.join(" ")}`);
const child = spawn("aria2c", aria2Args);
let lastProgressTime = Date.now();
let lastProgress = 0;
const progressCheckInterval = 1000;
// 设置超时检测定时器
const timeoutChecker = setInterval(() => {
const now = Date.now();
// 只在进度为0时检查超时
if (lastProgress === 0 && now - lastProgressTime > currentTimeout) {
clearInterval(timeoutChecker);
child.kill();
reject(new Error(`下载卡在0%超过 ${currentTimeout / 1000}`));
}
}, progressCheckInterval);
child.stdout.on("data", (data) => {
const str = data.toString();
// Match ( 12%) or (12%)
const match = str.match(/[0-9]+(\.[0-9]+)?%/g);
if (match) {
const p = parseFloat(match.at(-1)) / 100;
if (p > lastProgress) {
lastProgress = p;
lastProgressTime = Date.now();
}
onProgress?.(p);
}
});
child.stderr.on("data", (d) => onLog?.(`aria2c: ${d}`));
// 处理取消信号
const abortHandler = () => {
clearInterval(timeoutChecker);
child.kill();
reject(new Error("下载已取消"));
};
signal?.addEventListener("abort", abortHandler, { once: true });
child.on("close", (code) => {
clearInterval(timeoutChecker);
signal?.removeEventListener("abort", abortHandler);
if (code === 0) {
onProgress?.(1);
resolve();
} else {
reject(new Error(`Aria2c exited with code ${code}`));
}
});
child.on("error", (err) => {
clearInterval(timeoutChecker);
signal?.removeEventListener("abort", abortHandler);
reject(err);
});
});
// 检查是否已取消
if (signal?.aborted) {
throw new Error("下载已取消");
}
downloadSuccess = true;
} catch (err) {
retryCount++;
if (retryCount >= timeoutList.length) {
throw new Error(`下载失败,已重试 ${timeoutList.length} 次: ${err}`);
}
onLog?.(`下载失败,准备重试 (${retryCount}/${timeoutList.length})`);
// 等待2秒后重试
await new Promise((r) => setTimeout(r, 2000));
}
}
const filePath = path.join(downloadDir, filename);
return { filePath, downloadDir };
};
export interface InstallOptions {
pkgname: string;
filePath: string;
origin: "spark" | "apm";
superUserCmd?: string;
onLog?: (msg: string) => void;
signal?: AbortSignal;
}
/**
* 安装已下载的包
* 与 install-manager.ts 中的安装逻辑保持一致
*/
export const installPackage = async ({
pkgname,
filePath,
origin,
superUserCmd,
onLog,
signal,
}: InstallOptions): Promise<void> => {
// 构建安装命令
let execCommand = "";
const execParams: string[] = [];
if (origin === "spark") {
execCommand = superUserCmd || SHELL_CALLER_PATH;
if (superUserCmd) execParams.push(SHELL_CALLER_PATH);
execParams.push(
"ssinstall",
filePath,
"--delete-after-install",
"--no-create-desktop-entry",
"--native"
);
} else {
// APM
execCommand = superUserCmd || SHELL_CALLER_PATH;
if (superUserCmd) {
execParams.push(SHELL_CALLER_PATH);
}
execParams.push("apm", "ssinstall", filePath);
}
const cmdString = `${execCommand} ${execParams.join(" ")}`;
onLog?.(`执行安装: ${cmdString}`);
logger.info(`启动安装: ${cmdString}`);
return new Promise<void>((resolve, reject) => {
const child = spawn(execCommand, execParams, {
shell: false,
env: process.env,
});
let stdout = "";
let stderr = "";
let logBuffer = "";
let logBufferTimer: NodeJS.Timeout | null = null;
const LOG_FLUSH_MS = 100;
const flushLogBuffer = () => {
if (logBuffer.length > 0) {
onLog?.(logBuffer);
logBuffer = "";
}
logBufferTimer = null;
};
const bufferedSendLog = (message: string) => {
logBuffer += message;
if (!logBufferTimer) {
logBufferTimer = setTimeout(flushLogBuffer, LOG_FLUSH_MS);
}
};
// 处理取消信号
const abortHandler = () => {
child.kill();
reject(new Error("安装已取消"));
};
signal?.addEventListener("abort", abortHandler, { once: true });
child.stdout?.on("data", (data) => {
stdout += data.toString();
bufferedSendLog(data.toString());
});
child.stderr?.on("data", (data) => {
stderr += data.toString();
bufferedSendLog(data.toString());
});
child.on("error", (err) => {
signal?.removeEventListener("abort", abortHandler);
if (logBufferTimer) clearTimeout(logBufferTimer);
flushLogBuffer();
reject(err);
});
child.on("close", (code) => {
signal?.removeEventListener("abort", abortHandler);
if (logBufferTimer) clearTimeout(logBufferTimer);
flushLogBuffer();
if (code === 0) {
resolve();
} else {
reject(new Error(`安装失败,退出码: ${code}`));
}
});
});
};
/**
* 检查是否有 apm 命令
*/
export const checkApmAvailable = async (): Promise<boolean> => {
return new Promise((resolve) => {
const child = spawn("which", ["apm"]);
let stdout = "";
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.on("close", (code) => {
resolve(code === 0 && stdout.trim().length > 0);
});
child.on("error", () => {
resolve(false);
});
});
};
/**
* 检查提权命令
*/
export const checkSuperUserCommand = async (): Promise<string> => {
return new Promise((resolve) => {
const child = spawn("which", ["/usr/bin/pkexec"]);
let stdout = "";
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.on("close", (code) => {
if (code === 0) {
resolve(stdout.trim());
} else {
resolve("");
}
});
child.on("error", () => {
resolve("");
});
});
};

View File

@@ -1,7 +1,5 @@
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { spawn } from "node:child_process";
import { downloadPackage, type DownloadResult } from "../shared-installer";
import type { UpdateCenterItem } from "./types";
export interface Aria2DownloadResult {
@@ -16,8 +14,6 @@ export interface RunAria2DownloadOptions {
signal?: AbortSignal;
}
const PROGRESS_PATTERN = /(\d{1,3}(?:\.\d+)?)%/;
export const runAria2Download = async ({
item,
downloadDir,
@@ -29,68 +25,18 @@ export const runAria2Download = async ({
throw new Error(`Missing download metadata for ${item.pkgname}`);
}
await mkdir(downloadDir, { recursive: true });
const filePath = join(downloadDir, item.fileName);
// Use .metalink URL for download (same as Qt version)
// 使用与商店安装相同的下载逻辑
const metalinkUrl = `${item.downloadUrl}.metalink`;
await new Promise<void>((resolve, reject) => {
const child = spawn("aria2c", [
"--dir",
downloadDir,
"--out",
item.fileName,
"--enable-rpc=false",
"--console-log-level=warn",
"--summary-interval=1",
"--allow-overwrite=true",
"--connect-timeout=30",
"--max-tries=3",
const result = await downloadPackage({
pkgname: item.pkgname,
metalinkUrl,
]);
const abortDownload = () => {
child.kill();
reject(new Error(`Update task cancelled: ${item.pkgname}`));
};
if (signal?.aborted) {
abortDownload();
return;
}
signal?.addEventListener("abort", abortDownload, { once: true });
const handleOutput = (chunk: Buffer) => {
const message = chunk.toString().trim();
if (!message) {
return;
}
onLog?.(message);
const progressMatch = message.match(PROGRESS_PATTERN);
if (progressMatch) {
onProgress?.(Number(progressMatch[1]));
}
};
child.stdout?.on("data", handleOutput);
child.stderr?.on("data", handleOutput);
child.on("error", reject);
child.on("close", (code) => {
signal?.removeEventListener("abort", abortDownload);
if (code === 0) {
resolve();
return;
}
reject(new Error(`aria2c exited with code ${code ?? -1}`));
});
filename: item.fileName,
downloadDir,
onLog,
onProgress,
signal,
});
onProgress?.(100);
return { filePath };
return { filePath: result.filePath };
};

View File

@@ -155,20 +155,30 @@ const loadAptssItemMetadata = async (
| { item: UpdateCenterItem; warning?: undefined }
| { item: null; warning: string }
> => {
console.log(`[DEBUG] Loading APTSS metadata for ${item.pkgname}`);
const printUrisCommand = getAptssPrintUrisCommand(item.pkgname);
console.log(`[DEBUG] APTSS command: ${printUrisCommand.command} ${printUrisCommand.args.join(' ')}`);
const metadataResult = await runCommand(
printUrisCommand.command,
printUrisCommand.args,
);
console.log(`[DEBUG] APTSS metadata result code: ${metadataResult.code}`);
console.log(`[DEBUG] APTSS metadata stdout: ${metadataResult.stdout.substring(0, 500)}`);
console.log(`[DEBUG] APTSS metadata stderr: ${metadataResult.stderr.substring(0, 500)}`);
const commandError = getCommandError(
`aptss metadata query for ${item.pkgname}`,
metadataResult,
);
if (commandError) {
console.log(`[DEBUG] APTSS metadata error: ${commandError}`);
return { item: null, warning: commandError };
}
const metadata = parsePrintUrisOutput(metadataResult.stdout);
console.log(`[DEBUG] APTSS parsed metadata:`, metadata);
if (!metadata) {
return {
item: null,
@@ -424,14 +434,8 @@ export const initializeUpdateCenter = (): UpdateCenterService => {
return updateCenterService;
}
const superUserCmdProvider = async (): Promise<string> => {
const installManager = await import("../install-manager.js");
return installManager.checkSuperUserCommand();
};
updateCenterService = createUpdateCenterService({
loadItems: loadUpdateCenterItems,
superUserCmdProvider,
});
registerUpdateCenterIpc(ipcMain, updateCenterService);

View File

@@ -1,19 +1,12 @@
import { spawn } from "node:child_process";
import { join } from "node:path";
import { runAria2Download, type Aria2DownloadResult } from "./download";
import { installPackage } from "../shared-installer";
import type { UpdateCenterQueue, UpdateCenterTask } from "./queue";
import type { UpdateCenterItem } from "./types";
const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
const SSINSTALL_PATH = "/usr/bin/ssinstall";
const DEFAULT_DOWNLOAD_ROOT = "/tmp/spark-store/update-center";
export interface UpdateCommand {
execCommand: string;
execParams: string[];
}
export interface InstallUpdateItemOptions {
item: UpdateCenterItem;
filePath?: string;
@@ -55,73 +48,10 @@ export interface CreateTaskRunnerOptions extends TaskRunnerDependencies {
superUserCmd?: string;
}
const runCommand = async (
execCommand: string,
execParams: string[],
onLog?: (message: string) => void,
signal?: AbortSignal,
): Promise<void> => {
await new Promise<void>((resolve, reject) => {
const child = spawn(execCommand, execParams, {
shell: false,
env: process.env,
});
const handleOutput = (chunk: Buffer) => {
const message = chunk.toString().trim();
if (message) {
onLog?.(message);
}
};
const abortCommand = () => {
child.kill();
reject(new Error(`Update task cancelled: ${execParams.join(" ")}`));
};
if (signal?.aborted) {
abortCommand();
return;
}
signal?.addEventListener("abort", abortCommand, { once: true });
child.stdout?.on("data", handleOutput);
child.stderr?.on("data", handleOutput);
child.on("error", reject);
child.on("close", (code) => {
signal?.removeEventListener("abort", abortCommand);
if (code === 0) {
resolve();
return;
}
reject(new Error(`${execCommand} exited with code ${code ?? -1}`));
});
});
};
const buildPrivilegedCommand = (
command: string,
args: string[],
superUserCmd?: string,
): UpdateCommand => {
if (superUserCmd) {
return {
execCommand: superUserCmd,
execParams: [command, ...args],
};
}
return {
execCommand: command,
execParams: args,
};
};
// Removed buildLegacySparkUpgradeCommand - all updates now require downloading the deb package first
// to avoid aptss install popup. Use ssinstall with downloaded deb file instead.
/**
* 安装更新项
* 使用与商店安装相同的逻辑
*/
export const installUpdateItem = async ({
item,
filePath,
@@ -135,33 +65,17 @@ export const installUpdateItem = async ({
);
}
if (item.source === "apm") {
const installCommand = buildPrivilegedCommand(
SHELL_CALLER_PATH,
["apm", "ssinstall", filePath],
superUserCmd,
);
await runCommand(
installCommand.execCommand,
installCommand.execParams,
onLog,
signal,
);
return;
}
// 使用与商店安装相同的安装逻辑
const origin = item.source === "apm" ? "apm" : "spark";
// APTSS (Spark Store) packages use ssinstall
const installCommand = buildPrivilegedCommand(
SSINSTALL_PATH,
[filePath, "--delete-after-install", "--no-create-desktop-entry", "--native"],
await installPackage({
pkgname: item.pkgname,
filePath,
origin,
superUserCmd,
);
await runCommand(
installCommand.execCommand,
installCommand.execParams,
onLog,
signal,
);
});
};
export const createTaskRunner = (
@@ -194,11 +108,7 @@ export const createTaskRunner = (
return {
cancelActiveTask: () => {
if (!activeAbortController || activeAbortController.signal.aborted) {
return;
}
activeAbortController.abort();
activeAbortController?.abort();
},
runNextTask: async () => {
if (inFlightTask) {

View File

@@ -270,7 +270,9 @@ export const parsePrintUrisOutput = (
return null;
}
const [, downloadUrl, fileName, size, sha512] = match;
const [, rawDownloadUrl, fileName, size, sha512] = match;
// Clean up the URL: remove backticks and extra spaces
const downloadUrl = rawDownloadUrl.replace(/[`'"]/g, "").trim();
return {
downloadUrl,
fileName,

View File

@@ -88,8 +88,8 @@ export const createUpdateCenterQueue = (): UpdateCenterQueue => {
let refreshing = false;
let nextTaskId = 1;
const getTask = (taskId: number): UpdateCenterTask | undefined =>
tasks.find((task) => task.id === taskId);
const getTaskIndex = (taskId: number): number =>
tasks.findIndex((task) => task.id === taskId);
return {
setItems: (nextItems) => {
@@ -117,40 +117,59 @@ export const createUpdateCenterQueue = (): UpdateCenterQueue => {
return task;
},
markActiveTask: (taskId, status) => {
const task = getTask(taskId);
if (!task) {
const taskIndex = getTaskIndex(taskId);
if (taskIndex === -1) {
return;
}
task.status = status;
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
tasks = tasks.map((task, index) =>
index === taskIndex ? { ...task, status } : task,
);
},
updateTaskProgress: (taskId, progress) => {
const task = getTask(taskId);
if (!task) {
const taskIndex = getTaskIndex(taskId);
if (taskIndex === -1) {
return;
}
task.progress = clampProgress(progress);
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
tasks = tasks.map((task, index) =>
index === taskIndex
? { ...task, progress: clampProgress(progress) }
: task,
);
},
appendTaskLog: (taskId, message, time = Date.now()) => {
const task = getTask(taskId);
if (!task) {
const taskIndex = getTaskIndex(taskId);
if (taskIndex === -1) {
return;
}
task.logs = [...task.logs, { time, message }];
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
tasks = tasks.map((task, index) =>
index === taskIndex
? { ...task, logs: [...task.logs, { time, message }] }
: task,
);
},
finishTask: (taskId, status, error) => {
const task = getTask(taskId);
if (!task) {
const taskIndex = getTaskIndex(taskId);
if (taskIndex === -1) {
return;
}
task.status = status;
task.error = error;
if (status === "completed") {
task.progress = 100;
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
tasks = tasks.map((task, index) =>
index === taskIndex
? {
...task,
status,
error,
progress: status === "completed" ? 100 : task.progress,
}
: task,
);
},
getNextQueuedTask: () => tasks.find((task) => task.status === "queued"),
getSnapshot: () => createSnapshot(items, tasks, warnings, refreshing),

View File

@@ -1,3 +1,4 @@
import { BrowserWindow, ipcMain } from "electron";
import {
LEGACY_IGNORE_CONFIG_PATH,
applyIgnoredEntries,
@@ -5,7 +6,6 @@ import {
loadIgnoredEntries,
saveIgnoredEntries,
} from "./ignore-config";
import { createTaskRunner, type UpdateCenterTaskRunner } from "./install";
import {
createUpdateCenterQueue,
type UpdateCenterQueue,
@@ -79,11 +79,6 @@ export interface CreateUpdateCenterServiceOptions {
loadItems: () => Promise<UpdateCenterItem[] | UpdateCenterLoadedItems>;
loadIgnoredEntries?: () => Promise<Set<string>>;
saveIgnoredEntries?: (entries: ReadonlySet<string>) => Promise<void>;
createTaskRunner?: (
queue: UpdateCenterQueue,
superUserCmd?: string,
) => UpdateCenterTaskRunner;
superUserCmdProvider?: () => Promise<string>;
}
const getTaskKey = (
@@ -112,19 +107,9 @@ const toState = (
migrationTarget: item.migrationTarget,
aptssVersion: item.aptssVersion,
})),
tasks: snapshot.tasks.map((task) => ({
taskKey: getTaskKey(task.item),
packageName: task.pkgname,
source: task.item.source,
localIcon: task.item.localIcon,
remoteIcon: task.item.remoteIcon,
status: task.status,
progress: task.progress,
logs: task.logs.map((log) => ({ ...log })),
errorMessage: task.error ?? "",
})),
tasks: [], // 不再展示任务日志
warnings: [...snapshot.warnings],
hasRunningTasks: snapshot.hasRunningTasks,
hasRunningTasks: false, // 任务不在更新中心执行
});
const normalizeLoadedItems = (
@@ -152,12 +137,8 @@ export const createUpdateCenterService = (
options.saveIgnoredEntries ??
((entries: ReadonlySet<string>) =>
saveIgnoredEntries(LEGACY_IGNORE_CONFIG_PATH, entries));
const createRunner =
options.createTaskRunner ??
((currentQueue: UpdateCenterQueue, superUserCmd?: string) =>
createTaskRunner(currentQueue, { superUserCmd }));
let processingPromise: Promise<void> | null = null;
let activeRunner: UpdateCenterTaskRunner | null = null;
let nextUpdateTaskId = 1;
const applyWarning = (message: string): void => {
queue.finishRefresh([message]);
@@ -167,9 +148,9 @@ export const createUpdateCenterService = (
const emit = (): UpdateCenterServiceState => {
const snapshot = getState();
for (const listener of listeners) {
listeners.forEach((listener) => {
listener(snapshot);
}
});
return snapshot;
};
@@ -192,47 +173,6 @@ export const createUpdateCenterService = (
}
};
const failQueuedTasks = (message: string): void => {
for (const task of queue.getSnapshot().tasks) {
if (task.status === "queued") {
queue.appendTaskLog(task.id, message);
queue.finishTask(task.id, "failed", message);
}
}
};
const ensureProcessing = async (): Promise<void> => {
if (processingPromise) {
return processingPromise;
}
processingPromise = (async () => {
let superUserCmd = "";
try {
superUserCmd = (await options.superUserCmdProvider?.()) ?? "";
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
failQueuedTasks(message);
applyWarning(message);
emit();
return;
}
activeRunner = createRunner(queue, superUserCmd);
while (queue.getNextQueuedTask()) {
await activeRunner.runNextTask();
emit();
}
})().finally(() => {
processingPromise = null;
activeRunner = null;
});
return processingPromise;
};
return {
open: refresh,
refresh,
@@ -250,47 +190,63 @@ export const createUpdateCenterService = (
},
async start(taskKeys) {
const snapshot = queue.getSnapshot();
const existingTaskKeys = new Set(
snapshot.tasks
.filter(
(task) =>
!["completed", "failed", "cancelled"].includes(task.status),
)
.map((task) => getTaskKey(task.item)),
);
const selectedItems = snapshot.items.filter(
(item) =>
taskKeys.includes(getTaskKey(item)) &&
!item.ignored &&
!existingTaskKeys.has(getTaskKey(item)),
!item.ignored,
);
if (selectedItems.length === 0) {
return;
}
for (const item of selectedItems) {
queue.enqueueItem(item);
}
emit();
// 获取主窗口的 webContents
const mainWindow = BrowserWindow.getAllWindows()[0];
const webContents = mainWindow?.webContents;
await ensureProcessing();
},
async cancel(taskKey) {
const task = queue
.getSnapshot()
.tasks.find((entry) => getTaskKey(entry.item) === taskKey);
if (!task) {
if (!webContents) {
console.error("No main window found");
return;
}
queue.finishTask(task.id, "cancelled", "Cancelled");
if (["downloading", "installing"].includes(task.status)) {
activeRunner?.cancelActiveTask();
// 获取当前 items
let currentItems = snapshot.items;
for (const item of selectedItems) {
const updateTaskId = nextUpdateTaskId++;
// 构建 metalink URL
const metalinkUrl = item.downloadUrl
? `${item.downloadUrl}.metalink`
: undefined;
// 发送到主下载队列
const installTaskData = {
id: updateTaskId,
pkgname: item.pkgname,
metalinkUrl,
filename: item.fileName,
upgradeOnly: true,
origin: item.source === "apm" ? "apm" : "spark",
retry: false,
};
// 通过 IPC 发送到主下载队列
webContents.send("queue-install", JSON.stringify(installTaskData));
// 从更新中心的 items 中移除该应用(不再显示在更新列表中)
currentItems = currentItems.filter((i) => getTaskKey(i) !== getTaskKey(item));
}
// 更新队列中的 items
queue.setItems(currentItems);
emit();
},
async cancel(taskKey) {
// 取消功能不再需要通过更新中心,直接忽略
console.log("Cancel not needed for task:", taskKey);
},
getState,
subscribe(listener) {
listeners.add(listener);

View File

@@ -36,16 +36,13 @@
</p>
</div>
<div
class="grid min-h-0 flex-1 gap-0 lg:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]"
>
<div class="min-h-0 flex-1">
<UpdateCenterList
:items="store.filteredItems.value"
:tasks="store.snapshot.value.tasks"
:selected-task-keys="store.selectedTaskKeys.value"
@toggle-selection="emit('toggle-selection', $event)"
/>
<UpdateCenterLogPanel :tasks="store.snapshot.value.tasks" />
</div>
<UpdateCenterMigrationConfirm
@@ -53,11 +50,6 @@
@close="emit('dismiss-migration-confirm')"
@confirm="emit('confirm-migration-start')"
/>
<UpdateCenterCloseConfirm
:show="store.showCloseConfirm.value"
@close="emit('dismiss-close-confirm')"
@confirm="emit('confirm-close')"
/>
</div>
</div>
</Transition>
@@ -68,9 +60,7 @@ import { computed } from "vue";
import type { UpdateCenterStore } from "@/modules/updateCenter";
import UpdateCenterCloseConfirm from "./update-center/UpdateCenterCloseConfirm.vue";
import UpdateCenterList from "./update-center/UpdateCenterList.vue";
import UpdateCenterLogPanel from "./update-center/UpdateCenterLogPanel.vue";
import UpdateCenterMigrationConfirm from "./update-center/UpdateCenterMigrationConfirm.vue";
import UpdateCenterToolbar from "./update-center/UpdateCenterToolbar.vue";
@@ -80,8 +70,6 @@ const emit = defineEmits<{
(e: "request-start-selected"): void;
(e: "confirm-migration-start"): void;
(e: "dismiss-migration-confirm"): void;
(e: "confirm-close"): void;
(e: "dismiss-close-confirm"): void;
}>();
const props = defineProps<{

View File

@@ -3,7 +3,10 @@ import { computed, ref, type ComputedRef, type Ref } from "vue";
import type {
UpdateCenterItem,
UpdateCenterSnapshot,
DownloadItem,
} from "@/global/typedefinition";
import { downloads } from "@/global/downloadStatus";
import { APM_STORE_BASE_URL } from "@/global/storeConfig";
const EMPTY_SNAPSHOT: UpdateCenterSnapshot = {
items: [],
@@ -143,21 +146,51 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
};
const startSelected = async (): Promise<void> => {
const taskKeys = getSelectedItems().map((item) => item.taskKey);
const selectedItems = getSelectedItems();
const taskKeys = selectedItems.map((item) => item.taskKey);
if (taskKeys.length === 0) {
return;
}
// 在前端创建下载项,这样用户能在下载列表中看到更新任务
const arch = window.apm_store.arch || "amd64";
let downloadIdCounter = downloads.value.length > 0 ? Math.max(...downloads.value.map(d => d.id)) + 1 : 1;
selectedItems.forEach((item) => {
// 检查任务是否已存在
if (!downloads.value.find(d => d.pkgname === item.packageName && d.origin === (item.source === "apm" ? "apm" : "spark"))) {
const finalArch = item.source === "apm" ? `${arch}-apm` : `${arch}-store`;
const download: DownloadItem = {
id: downloadIdCounter++,
name: item.displayName,
pkgname: item.packageName,
version: item.newVersion,
icon: `${APM_STORE_BASE_URL}/${finalArch}/unknown/${item.packageName}/icon.png`,
origin: item.source === "apm" ? "apm" : "spark",
status: "queued",
progress: 0,
downloadedSize: 0,
totalSize: item.size || 0,
speed: 0,
timeRemaining: 0,
startTime: Date.now(),
logs: [{ time: Date.now(), message: "开始更新..." }],
source: "Update Center",
retry: false,
upgradeOnly: true,
filename: item.fileName,
metalinkUrl: item.downloadUrl ? `${item.downloadUrl}.metalink` : undefined,
};
downloads.value.push(download);
}
});
await window.updateCenter.start(taskKeys);
};
const requestClose = (): void => {
if (snapshot.value.hasRunningTasks) {
showCloseConfirm.value = true;
return;
}
// 直接关闭,不需要确认,因为任务在主下载队列中执行
closeNow();
};