mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-11-11 23:11:03 +08:00
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:
@@ -202,6 +202,12 @@ const refreshInstalledSyncCandidates = async (
|
||||
}
|
||||
|
||||
const refreshedApps: App[] = [];
|
||||
// 只记录「查询成功」的来源:list-installed 在来源被启动参数禁用或对应
|
||||
// 包管理器不可用时会返回 success:false(而非 reject)。若仍把它算作已刷新,
|
||||
// mergeInstalledApps 会按「该来源已刷新」丢弃本地已知记录,随后
|
||||
// syncInstalledAppsToAccount 会把这份不完整列表 PUT/me/app-list 覆盖到账号,
|
||||
// 造成云端应用列表静默丢数据。
|
||||
const refreshedOrigins: Array<"spark" | "apm"> = [];
|
||||
await Promise.all(
|
||||
origins.map(async (origin) => {
|
||||
const pkgnameList =
|
||||
@@ -216,6 +222,7 @@ const refreshInstalledSyncCandidates = async (
|
||||
});
|
||||
if (!result?.success) return;
|
||||
|
||||
refreshedOrigins.push(origin);
|
||||
const appList = Array.isArray(result?.apps) ? result.apps : [];
|
||||
for (const rawApp of appList) {
|
||||
// 运行时类型守卫:避免后端字段缺失时下游访问 undefined
|
||||
@@ -233,7 +240,7 @@ const refreshInstalledSyncCandidates = async (
|
||||
syncCandidateApps.value = mergeInstalledApps(
|
||||
syncCandidateApps.value,
|
||||
refreshedApps,
|
||||
origins,
|
||||
refreshedOrigins,
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -75,6 +75,11 @@ export {
|
||||
currentApp,
|
||||
currentAppSparkInstalled,
|
||||
currentAppApmInstalled,
|
||||
// APM 安装引导弹窗开关必须与 modules/processInstall 写入的 ref 同源,
|
||||
// 故同样从 storeConfig 再导出。历史上此处重复 `ref(false)` 声明过一份,
|
||||
// 导致 processInstall 置位 true 后 App.vue 读到的仍是另一实例,
|
||||
// APM 未安装时点安装什么都不会发生(弹窗静默不显示)。
|
||||
showApmInstallDialog,
|
||||
} from "../global/storeConfig";
|
||||
export const showModal = ref(false);
|
||||
export const showPreview = ref(false);
|
||||
@@ -139,7 +144,6 @@ export const showUninstallModal = ref(false);
|
||||
export const uninstallTargetApp: Ref<App | null> = ref(null);
|
||||
export const showAboutModal = ref(false);
|
||||
export const showSettingsModal = ref(false);
|
||||
export const showApmInstallDialog = ref(false);
|
||||
|
||||
// 可用来源 computed(供 storeFilter 相关判断复用)
|
||||
export const availableSources = computed(() => ({
|
||||
|
||||
@@ -34,12 +34,24 @@ import {
|
||||
getEffectiveStoreFilter,
|
||||
} from "../modules/storeFilter";
|
||||
import { cloudItemKey, cloudPackageKey } from "../modules/appListSync";
|
||||
import { removeDownloadItem } from "../global/downloadStatus";
|
||||
|
||||
// 运行时类型守卫(重构前在 App.vue 中校验完整字段,拆分时被削弱成只查
|
||||
// pkgname/origin,导致字段残缺的行会带着 undefined 流入已安装列表与云端
|
||||
// 同步条目)。主进程 list-installed 始终按下方结构回传,逐字段校验即可。
|
||||
const isInstalledAppInfo = (value: unknown): value is InstalledAppInfo => {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
return typeof v.pkgname === "string" && typeof v.origin === "string";
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const v = value as Partial<InstalledAppInfo>;
|
||||
return (
|
||||
typeof v.pkgname === "string" &&
|
||||
typeof v.name === "string" &&
|
||||
typeof v.version === "string" &&
|
||||
typeof v.arch === "string" &&
|
||||
(v.origin === "spark" || v.origin === "apm") &&
|
||||
typeof v.flags === "string" &&
|
||||
typeof v.isDependency === "boolean" &&
|
||||
// icon 为可选字段,需显式校验类型,避免非字符串值进入下游 app.icon || ""
|
||||
(typeof v.icon === "string" || v.icon === undefined)
|
||||
);
|
||||
};
|
||||
|
||||
const LIST_INSTALLED_TIMEOUT_MS = 15000;
|
||||
@@ -269,6 +281,7 @@ const refreshFavoriteInstalledApps = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
const refreshedApps: App[] = [];
|
||||
const refreshedOrigins: Array<"spark" | "apm"> = [];
|
||||
await Promise.all(
|
||||
origins.map(async (origin) => {
|
||||
const pkgnameList =
|
||||
@@ -283,6 +296,10 @@ const refreshFavoriteInstalledApps = async (): Promise<void> => {
|
||||
});
|
||||
if (!result?.success) return;
|
||||
|
||||
// 仅「查询成功」的来源才算已刷新:来源禁用/包管理器缺失时返回
|
||||
// success:false,此时若仍从列表里剔除该来源的已知应用,会让已安装
|
||||
// 列表凭空少掉一整类应用(与云端同步丢数据同源)。
|
||||
refreshedOrigins.push(origin);
|
||||
const appList = Array.isArray(result?.apps) ? result.apps : [];
|
||||
for (const rawApp of appList) {
|
||||
// 运行时类型守卫:避免后端字段缺失时下游访问 undefined
|
||||
@@ -299,7 +316,7 @@ const refreshFavoriteInstalledApps = async (): Promise<void> => {
|
||||
installedApps.value = [
|
||||
...installedApps.value.filter(
|
||||
(app) =>
|
||||
!origins.includes(app.origin) &&
|
||||
!refreshedOrigins.includes(app.origin) &&
|
||||
!refreshedKeys.has(`${app.origin}:${app.pkgname}`),
|
||||
),
|
||||
...refreshedApps,
|
||||
@@ -309,7 +326,9 @@ const refreshFavoriteInstalledApps = async (): Promise<void> => {
|
||||
const requestUninstall = (app: App) => {
|
||||
uninstallTargetApp.value = app;
|
||||
showUninstallModal.value = true;
|
||||
removeDownloadItem(app.pkgname);
|
||||
// 注意:不要在此处移除下载记录。卸载尚未确认/完成,提前移除会导致
|
||||
// 用户取消卸载也丢失记录;实际移除由 remove-complete 成功后统一处理
|
||||
// (见 App.vue handleRemoveComplete),与重构前的语义一致。
|
||||
};
|
||||
|
||||
const openInstalledModal = () => {
|
||||
|
||||
Reference in New Issue
Block a user