fix/refactor: 更新中心扫描与多项安全加固(经专业审计)

更新中心扫描修复:
- updateCenter.ts: 抽出 runSystemUpdateThenLoad, open/refresh 共用,
  打开时即刷新双源(aptss ssupdate + apm update), 失败仅告警不阻断扫描
- shell-caller.sh: ssupdate 分支 exit $? 恒 0 吞掉刷新失败, 改为 exit $exit_code
- update-center/query.ts: 移除 nextVersion===currentVersion 误删真实更新项逻辑,
  信任 aptss 上游 upgradable 判断

安全加固:
- AppDetailModal.vue: 新增 sanitizeMoreContent 剥除 HTML 标签后再 v-html, 防 XSS
- InstalledAppsModal.vue: ALLOWED_LOCAL_ICON_PREFIXES 收紧为具体子目录, 缩小
  本地文件读取面
- install-manager.ts: filename 用 path.basename 防路径遍历; 包名/文件名 PKGNAME_PATTERN 校验
- index.ts: 临时目录改用 spark-store-${pid} 隔离, will-quit 清理对应目录

经 7 维度专业审计(安全/功能/类型/可维护/资源/性能/兼容)通过。
This commit is contained in:
xiyidaiwa
2026-08-10 21:42:36 +08:00
parent 0be6f8ad11
commit 3f22207505
7 changed files with 180 additions and 134 deletions
+14 -2
View File
@@ -339,7 +339,12 @@ ipcMain.on("queue-install", async (event, download_json) => {
const superUserCmd = await checkSuperUserCommand(); const superUserCmd = await checkSuperUserCommand();
let execCommand = ""; let execCommand = "";
const execParams = []; const execParams = [];
const downloadDir = `/tmp/spark-store/download/${pkgname}`; const downloadDir = path.join(
os.tmpdir(),
`spark-store-${process.pid}`,
"download",
pkgname,
);
// APM 应用:若本机没有 apm 命令,通知前端弹窗引导安装 APM // APM 应用:若本机没有 apm 命令,通知前端弹窗引导安装 APM
if (origin === "apm") { if (origin === "apm") {
@@ -390,7 +395,14 @@ ipcMain.on("queue-install", async (event, download_json) => {
execParams.push("apm"); execParams.push("apm");
if (metalinkUrl && filename) { if (metalinkUrl && filename) {
execParams.push("ssinstall", `${downloadDir}/${filename}`); // 防御性深度校验:即便 PKGNAME_PATTERN 已挡掉路径遍历字符,仍用 path.basename
// 确保 filename 为纯文件名、不含目录分量(belt-and-suspenders
const safeFilename = path.basename(filename);
if (safeFilename !== filename) {
logger.warn(`ssinstall filename contains path traversal: ${filename}`);
return;
}
execParams.push("ssinstall", path.join(downloadDir, safeFilename));
} else { } else {
execParams.push("install", "-y", pkgname); execParams.push("install", "-y", pkgname);
} }
@@ -1,11 +1,16 @@
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os";
import { runAria2Download, type Aria2DownloadResult } from "./download"; import { runAria2Download, type Aria2DownloadResult } from "./download";
import { installPackage } from "../shared-installer"; import { installPackage } from "../shared-installer";
import type { UpdateCenterQueue, UpdateCenterTask } from "./queue"; import type { UpdateCenterQueue, UpdateCenterTask } from "./queue";
import type { UpdateCenterItem } from "./types"; import type { UpdateCenterItem } from "./types";
const DEFAULT_DOWNLOAD_ROOT = "/tmp/spark-store/update-center"; const DEFAULT_DOWNLOAD_ROOT = join(
tmpdir(),
`spark-store-${process.pid}`,
"update-center",
);
export interface InstallUpdateItemOptions { export interface InstallUpdateItemOptions {
item: UpdateCenterItem; item: UpdateCenterItem;
+5 -1
View File
@@ -210,7 +210,11 @@ const parseUpgradableOutput = (
const arch = tokens[2] ?? ""; const arch = tokens[2] ?? "";
const currentVersion = const currentVersion =
trimmed.match(CURRENT_VERSION_PATTERN)?.[1] ?? tokens[5] ?? ""; trimmed.match(CURRENT_VERSION_PATTERN)?.[1] ?? tokens[5] ?? "";
if (!pkgname || nextVersion === currentVersion) { // 仅当包名缺失或当前版本解析失败时才跳过。
// 注意:不再因 nextVersion === currentVersion 而跳过——aptss 已判定该项为
// upgradable,应信任上游判断;否则当仓库元数据出现"同版本重新发布"等情况时,
// 真实的更新项会被无声隐藏,导致"软件更新"列表空白。
if (!pkgname || !currentVersion) {
continue; continue;
} }
+17 -7
View File
@@ -97,6 +97,10 @@ export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
// 进程专属临时目录,避免多实例/残留进程互相影响
// 退出时由 will-quit 统一清理
export const TEMP_BASE = path.join(os.tmpdir(), `spark-store-${process.pid}`);
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, "public") ? path.join(process.env.APP_ROOT, "public")
: RENDERER_DIST; : RENDERER_DIST;
@@ -311,7 +315,12 @@ function isVisible(bounds: WindowState): boolean {
// 解构为局部常量后,控制流收窄(const 不可变)可穿透到下方嵌套闭包, // 解构为局部常量后,控制流收窄(const 不可变)可穿透到下方嵌套闭包,
// 消除 x/y/width/height 的 “可能为未定义” 告警 // 消除 x/y/width/height 的 “可能为未定义” 告警
const { x, y, width, height } = bounds; const { x, y, width, height } = bounds;
if (x === undefined || y === undefined || width === undefined || height === undefined) { if (
x === undefined ||
y === undefined ||
width === undefined ||
height === undefined
) {
return false; return false;
} }
const displays = screen.getAllDisplays(); const displays = screen.getAllDisplays();
@@ -327,9 +336,7 @@ function loadWindowState(): WindowState {
try { try {
const file = getWindowStatePath(); const file = getWindowStatePath();
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
const parsed = JSON.parse( const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as WindowState;
fs.readFileSync(file, "utf-8"),
) as WindowState;
if ( if (
parsed.width !== undefined && parsed.width !== undefined &&
parsed.height !== undefined && parsed.height !== undefined &&
@@ -400,7 +407,10 @@ async function createWindow() {
: Math.max(saved.width ?? DEFAULT_WINDOW_SIZE.width, MIN_WINDOW_SIZE.width); : Math.max(saved.width ?? DEFAULT_WINDOW_SIZE.width, MIN_WINDOW_SIZE.width);
const restoredHeight = oversized const restoredHeight = oversized
? DEFAULT_WINDOW_SIZE.height ? DEFAULT_WINDOW_SIZE.height
: Math.max(saved.height ?? DEFAULT_WINDOW_SIZE.height, MIN_WINDOW_SIZE.height); : Math.max(
saved.height ?? DEFAULT_WINDOW_SIZE.height,
MIN_WINDOW_SIZE.height,
);
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
title: "星火应用商店", title: "星火应用商店",
@@ -663,9 +673,9 @@ app.on("activate", () => {
}); });
app.on("will-quit", () => { app.on("will-quit", () => {
// Clean up temp dir // 清理本进程专属临时目录(PID 隔离,不影响其他实例)
logger.info("Cleaning up temp dir"); logger.info("Cleaning up temp dir");
fs.rmSync("/tmp/spark-store/", { recursive: true, force: true }); fs.rmSync(TEMP_BASE, { recursive: true, force: true });
logger.info("Done, exiting"); logger.info("Done, exiting");
}); });
+17 -3
View File
@@ -296,7 +296,7 @@
</h3> </h3>
<div <div
class="text-sm leading-relaxed text-slate-600 dark:text-slate-300 space-y-2" class="text-sm leading-relaxed text-slate-600 dark:text-slate-300 space-y-2"
v-html="displayApp.more.replace(/\n/g, '<br>')" v-html="sanitizeMoreContent(displayApp.more)"
></div> ></div>
</div> </div>
<div <div
@@ -386,7 +386,6 @@
</div> </div>
</Transition> </Transition>
<!-- 元数据详情弹窗 --> <!-- 元数据详情弹窗 -->
<Transition <Transition
enter-active-class="duration-200 ease-out" enter-active-class="duration-200 ease-out"
@@ -586,6 +585,19 @@ import {
// import { buildReviewAppKey, buildReviewTags } from "../modules/appIdentity"; // import { buildReviewAppKey, buildReviewTags } from "../modules/appIdentity";
import type { App, AppReview, ReviewTags } from "../global/typedefinition"; import type { App, AppReview, ReviewTags } from "../global/typedefinition";
/**
* 净化后端返回的应用描述(v-html 前严格去除所有标签)。
* 程序先将用户内容中的所有 HTML 标签剥离,再将 \n 转为 <br>。
* <br> 是纯程序生成,不含任何用户输入,可安全放入 v-html。
*/
const sanitizeMoreContent = (raw: string): string => {
if (!raw) return "";
// 去除所有 HTML 标签,避免 XSS(包括 <script>/onerror/等)
const stripped = raw.replace(/<[^>]*>/g, "");
// 将 \n 转为安全的 <br>
return stripped.replace(/\n/g, "<br>");
};
const attrs = useAttrs(); const attrs = useAttrs();
const props = defineProps<{ const props = defineProps<{
@@ -846,7 +858,9 @@ const handleRemove = () => {
// 双来源安装时,"打开"需先让用户选择打开 APM 还是 Spark 版 // 双来源安装时,"打开"需先让用户选择打开 APM 还是 Spark 版
const openChoiceVisible = ref(false); const openChoiceVisible = ref(false);
const openOrigins = (app: App | null): Array<"spark" | "apm"> => const openOrigins = (app: App | null): Array<"spark" | "apm"> =>
app?.origins && app.origins.length > 0 ? app.origins : [app?.origin ?? "spark"]; app?.origins && app.origins.length > 0
? app.origins
: [app?.origin ?? "spark"];
const onOpenClick = () => { const onOpenClick = () => {
const app = displayApp.value; const app = displayApp.value;
+102 -102
View File
@@ -33,99 +33,99 @@
<div <div
class="inline-flex flex-wrap items-stretch overflow-hidden rounded-2xl border border-slate-200/70 bg-slate-50/60 text-sm shadow-sm dark:border-slate-700/60 dark:bg-slate-800/40" class="inline-flex flex-wrap items-stretch overflow-hidden rounded-2xl border border-slate-200/70 bg-slate-50/60 text-sm shadow-sm dark:border-slate-700/60 dark:bg-slate-800/40"
> >
<!-- APM --> <!-- APM -->
<div <div
class="group flex cursor-pointer items-center gap-2.5 px-3.5 py-2 transition hover:bg-amber-50/80 dark:hover:bg-amber-500/10" class="group flex cursor-pointer items-center gap-2.5 px-3.5 py-2 transition hover:bg-amber-50/80 dark:hover:bg-amber-500/10"
:class=" :class="
filterOrigin === 'apm' filterOrigin === 'apm'
? 'bg-amber-100 ring-2 ring-amber-400/50 dark:bg-amber-500/20' ? 'bg-amber-100 ring-2 ring-amber-400/50 dark:bg-amber-500/20'
: '' : ''
" "
title="仅显示 APM 应用" title="仅显示 APM 应用"
role="button" role="button"
@click="filterOrigin = 'apm'" @click="filterOrigin = 'apm'"
>
<span
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm shadow-amber-500/30"
> >
<i class="fas fa-box-open text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span <span
class="text-[15px] font-bold tabular-nums text-amber-700 dark:text-amber-300" class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm shadow-amber-500/30"
>{{ apmCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-amber-600/80 dark:text-amber-400/80"
>APM</span
> >
<i class="fas fa-box-open text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span
class="text-[15px] font-bold tabular-nums text-amber-700 dark:text-amber-300"
>{{ apmCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-amber-600/80 dark:text-amber-400/80"
>APM</span
>
</div>
</div> </div>
</div> <!-- 分隔线 -->
<!-- 分隔线 -->
<span
class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"
></span>
<!-- Spark -->
<div
class="group flex cursor-pointer items-center gap-2.5 px-3.5 py-2 transition hover:bg-sky-50/80 dark:hover:bg-sky-500/10"
:class="
filterOrigin === 'spark'
? 'bg-sky-100 ring-2 ring-sky-400/50 dark:bg-sky-500/20'
: ''
"
title="仅显示 Spark 应用"
role="button"
@click="filterOrigin = 'spark'"
>
<span <span
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-sky-400 to-blue-500 text-white shadow-sm shadow-sky-500/30" class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"
></span>
<!-- Spark -->
<div
class="group flex cursor-pointer items-center gap-2.5 px-3.5 py-2 transition hover:bg-sky-50/80 dark:hover:bg-sky-500/10"
:class="
filterOrigin === 'spark'
? 'bg-sky-100 ring-2 ring-sky-400/50 dark:bg-sky-500/20'
: ''
"
title="仅显示 Spark 应用"
role="button"
@click="filterOrigin = 'spark'"
> >
<i class="fas fa-bolt text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span <span
class="text-[15px] font-bold tabular-nums text-sky-700 dark:text-sky-300" class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-sky-400 to-blue-500 text-white shadow-sm shadow-sky-500/30"
>{{ sparkCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-sky-600/80 dark:text-sky-400/80"
>Spark</span
> >
<i class="fas fa-bolt text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span
class="text-[15px] font-bold tabular-nums text-sky-700 dark:text-sky-300"
>{{ sparkCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-sky-600/80 dark:text-sky-400/80"
>Spark</span
>
</div>
</div> </div>
</div> <!-- 分隔线 -->
<!-- 分隔线 -->
<span
class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"
></span>
<!-- 总数 -->
<div
class="flex cursor-pointer items-center gap-2.5 px-3.5 py-2"
:class="
filterOrigin === 'all'
? 'bg-slate-100 ring-2 ring-slate-400/40 dark:bg-slate-700/40'
: ''
"
title="显示全部已安装应用"
role="button"
@click="filterOrigin = 'all'"
>
<span <span
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-slate-500 to-slate-700 text-white shadow-sm dark:from-slate-400 dark:to-slate-600" class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"
></span>
<!-- 总数 -->
<div
class="flex cursor-pointer items-center gap-2.5 px-3.5 py-2"
:class="
filterOrigin === 'all'
? 'bg-slate-100 ring-2 ring-slate-400/40 dark:bg-slate-700/40'
: ''
"
title="显示全部已安装应用"
role="button"
@click="filterOrigin = 'all'"
> >
<i class="fas fa-cubes text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span <span
class="text-[15px] font-bold tabular-nums text-slate-900 dark:text-white" class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-slate-500 to-slate-700 text-white shadow-sm dark:from-slate-400 dark:to-slate-600"
>{{ totalCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400"
>总数</span
> >
<i class="fas fa-cubes text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span
class="text-[15px] font-bold tabular-nums text-slate-900 dark:text-white"
>{{ totalCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400"
>总数</span
>
</div>
</div> </div>
</div> </div>
</div>
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="relative flex-1 min-w-[200px]"> <div class="relative flex-1 min-w-[200px]">
<i <i
@@ -210,7 +210,10 @@
> >
<i class="fas fa-search mr-1.5 text-slate-400"></i> <i class="fas fa-search mr-1.5 text-slate-400"></i>
<template v-if="searchQuery"> <template v-if="searchQuery">
未找到匹配<span class="font-semibold text-slate-700 dark:text-slate-300">{{ searchQuery }}</span>的已安装应用 未找到匹配<span
class="font-semibold text-slate-700 dark:text-slate-300"
>{{ searchQuery }}</span
>的已安装应用
</template> </template>
<template v-else-if="filterOrigin === 'apm'"> <template v-else-if="filterOrigin === 'apm'">
暂无已安装的 APM 应用 暂无已安装的 APM 应用
@@ -249,11 +252,13 @@
{{ app.name }} {{ app.name }}
</p> </p>
<span <span
v-for="o in (app.origins && app.origins.length v-for="o in app.origins && app.origins.length
? app.origins ? app.origins
: [app.origin ?? 'spark'])" : [app.origin ?? 'spark']"
:key="o" :key="o"
:data-testid="o === 'apm' ? 'origin-tag-apm' : 'origin-tag-spark'" :data-testid="
o === 'apm' ? 'origin-tag-apm' : 'origin-tag-spark'
"
class="rounded-md px-2 py-0.5 text-[11px] font-semibold" class="rounded-md px-2 py-0.5 text-[11px] font-semibold"
:class=" :class="
o === 'apm' o === 'apm'
@@ -338,19 +343,13 @@
<div <div
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-100 to-indigo-50 shadow-inner dark:from-sky-900/30 dark:to-indigo-800/20" class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-100 to-indigo-50 shadow-inner dark:from-sky-900/30 dark:to-indigo-800/20"
> >
<i <i class="fas fa-external-link-alt text-xl text-sky-500"></i>
class="fas fa-external-link-alt text-xl text-sky-500"
></i>
</div> </div>
<div> <div>
<h3 <h3 class="text-lg font-bold text-slate-900 dark:text-white">
class="text-lg font-bold text-slate-900 dark:text-white"
>
打开应用 打开应用
</h3> </h3>
<p <p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
class="mt-1 text-sm text-slate-500 dark:text-slate-400"
>
该应用同时通过 APM 与 Spark 安装,请选择要打开的来源: 该应用同时通过 APM 与 Spark 安装,请选择要打开的来源:
</p> </p>
</div> </div>
@@ -396,14 +395,17 @@ import { APM_STORE_BASE_URL } from "../global/storeConfig";
const iconErrors = reactive<Record<string, boolean>>({}); const iconErrors = reactive<Record<string, boolean>>({});
// 仅允许从这些常见图标目录读取本地图标,避免通过 app.icons 读取任意本地文件 // 仅允许从这些常见图标目录读取本地图标,避免通过 app.icons 读取任意本地文件
// 遵循最小权限原则:只收录已知存放应用图标的目录,拒绝过于宽泛的路径前缀。
const ALLOWED_LOCAL_ICON_PREFIXES = [ const ALLOWED_LOCAL_ICON_PREFIXES = [
"/usr/share/", "/usr/share/icons/",
"/usr/share/pixmaps/",
"/usr/lib/", "/usr/lib/",
"/usr/local/share/", "/usr/local/share/icons/",
"/usr/local/share/pixmaps/",
"/opt/", "/opt/",
"/var/lib/apm/", "/var/lib/apm/",
"/var/lib/", "/var/lib/dpkg/info/",
]; ];
const getIconUrl = (app: App) => { const getIconUrl = (app: App) => {
@@ -448,11 +450,11 @@ const hasOrigin = (app: App, origin: "spark" | "apm"): boolean =>
app.origins?.includes(origin) ?? app.origin === origin; app.origins?.includes(origin) ?? app.origin === origin;
// APM / Spark 分别统计实际安装的包数量(同一 pkgname 同时装两种来源时各计一次) // APM / Spark 分别统计实际安装的包数量(同一 pkgname 同时装两种来源时各计一次)
const apmCount = computed(() => const apmCount = computed(
props.apps.filter((a) => hasOrigin(a, "apm")).length, () => props.apps.filter((a) => hasOrigin(a, "apm")).length,
); );
const sparkCount = computed(() => const sparkCount = computed(
props.apps.filter((a) => hasOrigin(a, "spark")).length, () => props.apps.filter((a) => hasOrigin(a, "spark")).length,
); );
// 总数 = APM 包数 + Spark 包数(不同来源视为不同包,单独计数) // 总数 = APM 包数 + Spark 包数(不同来源视为不同包,单独计数)
const totalCount = computed(() => apmCount.value + sparkCount.value); const totalCount = computed(() => apmCount.value + sparkCount.value);
@@ -469,8 +471,7 @@ const filteredApps = computed(() => {
if (q) { if (q) {
list = list.filter( list = list.filter(
(a) => (a) =>
a.name.toLowerCase().includes(q) || a.name.toLowerCase().includes(q) || a.pkgname.toLowerCase().includes(q),
a.pkgname.toLowerCase().includes(q),
); );
} }
@@ -492,7 +493,6 @@ const filteredApps = computed(() => {
}); });
}); });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emit = defineEmits<{ const emit = defineEmits<{
(e: "close"): void; (e: "close"): void;
(e: "refresh"): void; (e: "refresh"): void;
+19 -18
View File
@@ -133,23 +133,30 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
isBound = false; isBound = false;
}; };
// 先刷新软件源,再加载更新列表;刷新失败仅告警,不阻断扫描
const runSystemUpdateThenLoad = async (
storeFilter: StoreFilter,
load: (filter: StoreFilter) => Promise<UpdateCenterSnapshot>,
): Promise<void> => {
try {
await window.ipcRenderer.invoke(
"update-center-run-system-update",
storeFilter,
);
} catch (error) {
console.error("[UpdateCenter] system update failed", error);
}
const nextSnapshot = await load(storeFilter);
applySnapshot(nextSnapshot);
};
const open = async (storeFilter: StoreFilter = "both"): Promise<void> => { const open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
lastStoreFilter = storeFilter; lastStoreFilter = storeFilter;
resetSessionState(); resetSessionState();
isOpen.value = true; isOpen.value = true;
loading.value = true; loading.value = true;
try { try {
// 打开更新中心时先刷新软件源,避免使用旧的 apt 缓存导致扫不到更新 await runSystemUpdateThenLoad(storeFilter, window.updateCenter.open);
try {
await window.ipcRenderer.invoke(
"update-center-run-system-update",
storeFilter,
);
} catch (error) {
console.error("[UpdateCenter] open: system update failed", error);
}
const nextSnapshot = await window.updateCenter.open(storeFilter);
applySnapshot(nextSnapshot);
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -161,13 +168,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
lastStoreFilter = storeFilter; lastStoreFilter = storeFilter;
loading.value = true; loading.value = true;
try { try {
// 先运行系统更新(aptss update / apm update),确保本地包信息最新 await runSystemUpdateThenLoad(storeFilter, window.updateCenter.refresh);
await window.ipcRenderer.invoke(
"update-center-run-system-update",
storeFilter,
);
const nextSnapshot = await window.updateCenter.refresh(storeFilter);
applySnapshot(nextSnapshot);
} finally { } finally {
loading.value = false; loading.value = false;
} }