From e2f59b3cdf32c765db73b8328b3217efeda6e8d5 Mon Sep 17 00:00:00 2001
From: shenmo
Date: Sun, 12 Apr 2026 17:53:16 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A4=8D=E7=94=A8=E4=B8=8B=E8=BD=BD=E4=B8=AD?=
=?UTF-8?q?=E5=BF=83(1/2)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/main/backend/shared-installer.ts | 368 ++++++++++++++++++
.../main/backend/update-center/download.ts | 78 +---
electron/main/backend/update-center/index.ts | 16 +-
.../main/backend/update-center/install.ts | 118 +-----
electron/main/backend/update-center/query.ts | 4 +-
electron/main/backend/update-center/queue.ts | 55 ++-
.../main/backend/update-center/service.ts | 140 +++----
src/components/UpdateCenterModal.vue | 14 +-
src/modules/updateCenter.ts | 45 ++-
9 files changed, 532 insertions(+), 306 deletions(-)
create mode 100644 electron/main/backend/shared-installer.ts
diff --git a/electron/main/backend/shared-installer.ts b/electron/main/backend/shared-installer.ts
new file mode 100644
index 00000000..d1f31c2a
--- /dev/null
+++ b/electron/main/backend/shared-installer.ts
@@ -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 => {
+ // 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((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((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 => {
+ // 构建安装命令
+ 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((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 => {
+ 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 => {
+ 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("");
+ });
+ });
+};
diff --git a/electron/main/backend/update-center/download.ts b/electron/main/backend/update-center/download.ts
index f6e030c4..b9be8326 100644
--- a/electron/main/backend/update-center/download.ts
+++ b/electron/main/backend/update-center/download.ts
@@ -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((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",
- 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}`));
- });
+
+ const result = await downloadPackage({
+ pkgname: item.pkgname,
+ metalinkUrl,
+ filename: item.fileName,
+ downloadDir,
+ onLog,
+ onProgress,
+ signal,
});
- onProgress?.(100);
-
- return { filePath };
+ return { filePath: result.filePath };
};
diff --git a/electron/main/backend/update-center/index.ts b/electron/main/backend/update-center/index.ts
index 76c100e6..c8280c96 100644
--- a/electron/main/backend/update-center/index.ts
+++ b/electron/main/backend/update-center/index.ts
@@ -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 => {
- const installManager = await import("../install-manager.js");
- return installManager.checkSuperUserCommand();
- };
-
updateCenterService = createUpdateCenterService({
loadItems: loadUpdateCenterItems,
- superUserCmdProvider,
});
registerUpdateCenterIpc(ipcMain, updateCenterService);
diff --git a/electron/main/backend/update-center/install.ts b/electron/main/backend/update-center/install.ts
index 937e5620..5660b541 100644
--- a/electron/main/backend/update-center/install.ts
+++ b/electron/main/backend/update-center/install.ts
@@ -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 => {
- await new Promise((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;
- }
-
- // APTSS (Spark Store) packages use ssinstall
- const installCommand = buildPrivilegedCommand(
- SSINSTALL_PATH,
- [filePath, "--delete-after-install", "--no-create-desktop-entry", "--native"],
+ // 使用与商店安装相同的安装逻辑
+ const origin = item.source === "apm" ? "apm" : "spark";
+
+ 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) {
diff --git a/electron/main/backend/update-center/query.ts b/electron/main/backend/update-center/query.ts
index 74ce1e4c..1cb49422 100644
--- a/electron/main/backend/update-center/query.ts
+++ b/electron/main/backend/update-center/query.ts
@@ -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,
diff --git a/electron/main/backend/update-center/queue.ts b/electron/main/backend/update-center/queue.ts
index cc2b21e1..7d492f2e 100644
--- a/electron/main/backend/update-center/queue.ts
+++ b/electron/main/backend/update-center/queue.ts
@@ -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),
diff --git a/electron/main/backend/update-center/service.ts b/electron/main/backend/update-center/service.ts
index f6fc2213..1856aadd 100644
--- a/electron/main/backend/update-center/service.ts
+++ b/electron/main/backend/update-center/service.ts
@@ -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;
loadIgnoredEntries?: () => Promise>;
saveIgnoredEntries?: (entries: ReadonlySet) => Promise;
- createTaskRunner?: (
- queue: UpdateCenterQueue,
- superUserCmd?: string,
- ) => UpdateCenterTaskRunner;
- superUserCmdProvider?: () => Promise;
}
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) =>
saveIgnoredEntries(LEGACY_IGNORE_CONFIG_PATH, entries));
- const createRunner =
- options.createTaskRunner ??
- ((currentQueue: UpdateCenterQueue, superUserCmd?: string) =>
- createTaskRunner(currentQueue, { superUserCmd }));
- let processingPromise: Promise | 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 => {
- 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);
diff --git a/src/components/UpdateCenterModal.vue b/src/components/UpdateCenterModal.vue
index 3c22bbc3..76ce5a2f 100644
--- a/src/components/UpdateCenterModal.vue
+++ b/src/components/UpdateCenterModal.vue
@@ -36,16 +36,13 @@
-
@@ -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<{
diff --git a/src/modules/updateCenter.ts b/src/modules/updateCenter.ts
index 7a1ad5e9..14e0b3a4 100644
--- a/src/modules/updateCenter.ts
+++ b/src/modules/updateCenter.ts
@@ -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 => {
- 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();
};