fix(install): 修复 APM 未安装时点安装静默无反应,并清理同类接线断裂

主问题:showApmInstallDialog 在 useAppState.ts 被重复声明为独立 ref,
而 processInstall.ts 置位的是 storeConfig.ts 中的另一份实例,导致 APM
未安装时点「安装」既不弹引导弹窗也不创建下载任务(界面毫无反应)。

修复:
- useAppState 改为从 storeConfig 再导出同一实例(消除双实例)
- processInstall 在 install-complete 兜底识别「未安装 APM」失败原因
- 新增回归测试 ApmInstallDialog.test.ts(回退修复后 3/3 失败)

同类「接线断裂」问题全量排查修复(源自 d7bdcf36 拆分 composables):
- 应用下载排行永远为空:loadRanking 在 apps 未填充时空跑且不重试
- 详情页卸载后按钮状态不刷新(丢失的 checkAppInstalled 分支)
- 重试已取消任务复用 id,被旧任务 finally 删除而永久卡在下载中
- remove-installed 的 spawn 缺 error 监听(可致主进程崩溃)且不消费 stderr
- 云端应用列表在来源查询失败时静默丢数据(已安装列表同源问题)
- 忽略更新写入失败无反馈;下载统计上报缺 catch
- isInstalledAppInfo 运行时校验被削弱

清理重构遗留孤儿代码:
- 删除无引用的 AppDetailPage/AccountQuickMenu/TopActions/UpdateCenterCloseConfirm
- 恢复 handleRemoveComplete 语义与 onUnmounted 的 abort/unbind
- 删除孤儿 IPC:list-upgradable/show-apm-install-dialog/uninstall-installed
- 删除孤儿渲染端代码:handleUpgrade/handleRemove/useDownloadItemStatus
  及已无生产者的 queue-install 回灌监听器

验证:vue-tsc 0 error、vite build 通过、单元测试无新增失败
(基线 40 失败 → 35 失败)。

Signed-off-by: shenmo <jifengshenmo@outlook.com>
This commit is contained in:
2026-10-11 12:34:10 +08:00
parent 48a6e10263
commit 56a7e11622
16 changed files with 415 additions and 1056 deletions
+16 -66
View File
@@ -1,13 +1,13 @@
import pino from "pino";
import {
APM_STORE_BASE_URL,
APM_STORE_STATS_BASE_URL,
currentApp,
currentAppSparkInstalled,
currentAppApmInstalled,
showApmInstallDialog,
} from "../global/storeConfig";
import { APM_STORE_BASE_URL } from "../global/storeConfig";
import { downloads, getNextDownloadId } from "../global/downloadStatus";
import {
@@ -99,6 +99,11 @@ export const handleInstall = async (
)
.then((response) => {
logger.info("下载次数统计已发送,状态:", response.data);
})
// 统计上报失败不应影响安装,但也必须消费掉 rejection:
// 否则网络异常/超时会变成未处理的 promise rejection(渲染端没有全局兜底)。
.catch((error: unknown) => {
logger.warn(`下载次数统计发送失败(忽略): ${String(error)}`);
});
return download;
};
@@ -110,64 +115,6 @@ export const handleRetry = (download_: DownloadItem) => {
window.ipcRenderer.send("queue-install", JSON.stringify(download_));
};
export const handleUpgrade = async (app: App) => {
if (!app.pkgname) return;
// APM 应用:在创建下载任务前检查 APM 是否可用
if (app.origin === "apm") {
const hasApm = await window.ipcRenderer.invoke("check-apm-available");
if (!hasApm) {
showApmInstallDialog.value = true;
return;
}
}
if (
downloads.value.find(
(d) => d.pkgname === app.pkgname && d.origin === app.origin,
)
) {
logger.info(`任务已存在,忽略重复添加: ${app.pkgname} (${app.origin})`);
return;
}
const arch = window.apm_store.arch || "amd64";
const finalArch = app.origin === "spark" ? `${arch}-store` : `${arch}-apm`;
const downloadId = getNextDownloadId();
const download: DownloadItem = {
id: downloadId,
name: app.name,
pkgname: app.pkgname,
version: app.version,
icon: `${APM_STORE_BASE_URL}/${finalArch}/${app.category}/${app.pkgname}/icon.png`,
status: "queued",
progress: 0,
downloadedSize: 0,
totalSize: 0,
speed: 0,
timeRemaining: 0,
startTime: Date.now(),
logs: [{ time: Date.now(), message: "开始更新..." }],
source: "APM Update",
retry: false,
upgradeOnly: true,
origin: app.origin,
};
downloads.value.push(download);
window.ipcRenderer.send("queue-install", JSON.stringify(download));
};
export const handleRemove = (appObj?: App) => {
const targetApp = appObj || currentApp.value;
if (!targetApp?.pkgname) return;
window.ipcRenderer.send("remove-installed", {
pkgname: targetApp.pkgname,
origin: targetApp.origin,
});
};
window.ipcRenderer.on("remove-complete", (_event, log: DownloadResult) => {
if (log.success) {
if (log.origin === "spark") {
@@ -221,7 +168,9 @@ window.ipcRenderer.on("install-complete", (_event, log: DownloadResult) => {
if (downloadObj) {
// 更新中心来源的任务用「更新」语义,其余(普通安装)用「安装」语义,
// 避免把安装结果误报为「下载完成」。
const isUpdate = (downloadObj.source ?? "").toLowerCase().includes("update");
const isUpdate = (downloadObj.source ?? "")
.toLowerCase()
.includes("update");
const doneLabel = isUpdate ? "更新完成" : "安装完成";
const failVerb = isUpdate ? "更新失败" : "安装失败";
@@ -242,12 +191,13 @@ window.ipcRenderer.on("install-complete", (_event, log: DownloadResult) => {
time: Date.now(),
message: `${failVerb}: ${reason}`,
});
// 兜底:主进程检测到未安装 APM 时会直接失败并转发 trigger-apm-install-dialog。
// 渲染端这里再识别一次该失败原因,确保「未安装 APM」永远不会静默失败——
// 即使主进程事件在弹窗挂载前丢失,也能就地弹出 APM 安装引导。
if (downloadObj.origin === "apm" && reason.includes("未安装 APM")) {
showApmInstallDialog.value = true;
}
}
}
});
window.ipcRenderer.on("queue-install", (_event, payload: unknown) => {
const serializedPayload =
typeof payload === "string" ? payload : JSON.stringify(payload);
window.ipcRenderer.send("queue-install", serializedPayload);
});