diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index d2dad6e3..fe528356 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -699,31 +699,26 @@ ipcMain.handle("check-installed", async (_event, payload: any) => { return isInstalled; } - const checkScript = "/opt/spark-store/extras/check-is-installed"; + // Spark: 使用 dpkg-query 检查安装状态 + const { code, stdout } = await runCommandCapture("dpkg-query", [ + "-W", + "-f=${Package}\\t${Status}\\n", + pkgname, + ]); - // 首先尝试使用内置脚本 - if (fs.existsSync(checkScript)) { - const child = spawn(checkScript, [pkgname], { - shell: false, - env: process.env, - }); - - await new Promise((resolve) => { - child.on("error", (err) => { - logger.error(`check-installed 脚本执行失败: ${err?.message || err}`); - resolve(); - }); - - child.on("close", (code) => { - if (code === 0) { + if (code === 0) { + const line = stdout.trim(); + if (line) { + const parts = line.split("\t"); + if (parts.length >= 2) { + const status = parts[1].trim(); + // 检查状态是否为 "install ok installed" + if (status === "install ok installed") { isInstalled = true; - logger.info(`应用已安装 (脚本检测): ${pkgname}`); + logger.info(`应用已安装 (dpkg检测): ${pkgname}`); } - resolve(); - }); - }); - - if (isInstalled) return true; + } + } } return isInstalled; @@ -793,7 +788,11 @@ ipcMain.on("remove-installed", async (_event, payload) => { ipcMain.handle( "list-installed", - async (_event, origin: "apm" | "spark" = "apm") => { + async ( + _event, + payload: { origin: "apm" | "spark"; pkgnameList?: string[] }, + ) => { + const { origin, pkgnameList } = payload; const apmBasePath = "/var/lib/apm/apm/files/ace-env/var/lib/apm"; try { @@ -809,9 +808,54 @@ ipcMain.handle( }> = []; if (origin === "spark") { + // 如果提供了包名列表,只检查这些包的安装状态(优化版) + if (pkgnameList && pkgnameList.length > 0) { + logger.info( + `使用优化模式检查 ${pkgnameList.length} 个 Spark 包的安装状态`, + ); + + // 批量查询这些包的状态 + // 注意:dpkg-query 在部分包不存在时也会返回非零码,但已找到的包会输出到 stdout + const { stdout, stderr } = await runCommandCapture("dpkg-query", [ + "-W", + "-f=${Package}\\t${Version}\\t${Architecture}\\t${Status}\\n", + ...pkgnameList, + ]); + + // 即使没有错误,也可能有警告信息输出到 stderr + if (stderr) { + logger.debug(`dpkg-query warnings: ${stderr}`); + } + + const lines = stdout.split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + const parts = trimmed.split("\t"); + if (parts.length >= 4) { + const status = parts[3].trim(); + // 只保留状态为 "install ok installed" 的包 + if (status === "install ok installed") { + installedApps.push({ + pkgname: parts[0], + name: parts[0], + version: parts[1], + arch: parts[2], + flags: "[installed]", + origin: "spark", + isDependency: false, + }); + } + } + } + return { success: true, apps: installedApps }; + } + + // 回退到全量扫描模式(未提供包名列表时) + logger.info("使用全量扫描模式获取所有 Spark 已安装包"); const { code, stdout } = await runCommandCapture("dpkg-query", [ "-W", - "-f=${Package} ${Version} ${Architecture}\\n", + "-f=${Package}\\t${Version}\\t${Architecture}\\t${Status}\\n", ]); if (code !== 0) { @@ -827,17 +871,21 @@ ipcMain.handle( for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - const parts = trimmed.split(" "); - if (parts.length >= 3) { - installedApps.push({ - pkgname: parts[0], - name: parts[0], - version: parts[1], - arch: parts[2], - flags: "[installed]", - origin: "spark", - isDependency: false, - }); + const parts = trimmed.split("\t"); + if (parts.length >= 4) { + const status = parts[3].trim(); + // 只保留状态为 "install ok installed" 的包 + if (status === "install ok installed") { + installedApps.push({ + pkgname: parts[0], + name: parts[0], + version: parts[1], + arch: parts[2], + flags: "[installed]", + origin: "spark", + isDependency: false, + }); + } } } return { success: true, apps: installedApps }; diff --git a/electron/main/backend/update-center/index.ts b/electron/main/backend/update-center/index.ts index f5e91a4a..d41143a4 100644 --- a/electron/main/backend/update-center/index.ts +++ b/electron/main/backend/update-center/index.ts @@ -49,6 +49,21 @@ interface RemoteCategoryAppEntry { const REMOTE_STORE_BASE_URL = "https://erotica.spark-app.store"; const categoryCache = new Map>(); +const isAptssAvailable = async (): Promise => { + return await new Promise((resolve) => { + const child = spawn("command", ["-v", "aptss"], { + shell: false, + env: process.env, + }); + child.on("close", (code) => { + resolve(code === 0); + }); + child.on("error", () => { + resolve(false); + }); + }); +}; + const APTSS_LIST_UPGRADABLE_COMMAND = { command: "bash", args: [ @@ -352,49 +367,63 @@ const enrichItemIcons = (items: UpdateCenterItem[]): UpdateCenterItem[] => { export const loadUpdateCenterItems = async ( runCommand: UpdateCenterCommandRunner = runCommandCapture, ): Promise => { + const aptssAvailable = await isAptssAvailable(); + const [aptssResult, apmResult, aptssInstalledResult, apmInstalledResult] = await Promise.all([ - runCommand( - APTSS_LIST_UPGRADABLE_COMMAND.command, - APTSS_LIST_UPGRADABLE_COMMAND.args, - ), + aptssAvailable + ? runCommand( + APTSS_LIST_UPGRADABLE_COMMAND.command, + APTSS_LIST_UPGRADABLE_COMMAND.args, + ) + : Promise.resolve({ code: 0, stdout: "", stderr: "" }), runCommand("apm", ["list", "--upgradable"]), - runCommand( - DPKG_QUERY_INSTALLED_COMMAND.command, - DPKG_QUERY_INSTALLED_COMMAND.args, - ), + aptssAvailable + ? runCommand( + DPKG_QUERY_INSTALLED_COMMAND.command, + DPKG_QUERY_INSTALLED_COMMAND.args, + ) + : Promise.resolve({ code: 0, stdout: "", stderr: "" }), runCommand("apm", ["list", "--installed"]), ]); const warnings = [ - getCommandError("aptss upgradable query", aptssResult), + aptssAvailable + ? getCommandError("aptss upgradable query", aptssResult) + : null, getCommandError("apm upgradable query", apmResult), - getCommandError("dpkg installed query", aptssInstalledResult), + aptssAvailable + ? getCommandError("dpkg installed query", aptssInstalledResult) + : null, getCommandError("apm installed query", apmInstalledResult), ].filter((message): message is string => message !== null); const aptssItems = - aptssResult.code === 0 + aptssAvailable && aptssResult.code === 0 ? parseAptssUpgradableOutput(aptssResult.stdout) : []; const apmItems = apmResult.code === 0 ? parseApmUpgradableOutput(apmResult.stdout) : []; - if (aptssResult.code !== 0 && apmResult.code !== 0) { + if (apmResult.code !== 0) { throw new Error(warnings.join("; ")); } const installedSources = buildInstalledSourceMap( - aptssInstalledResult.code === 0 ? aptssInstalledResult.stdout : "", + aptssAvailable && aptssInstalledResult.code === 0 + ? aptssInstalledResult.stdout + : "", apmInstalledResult.code === 0 ? apmInstalledResult.stdout : "", ); const [categorizedAptssItems, categorizedApmItems] = await Promise.all([ - enrichItemCategories(aptssItems), + aptssAvailable ? enrichItemCategories(aptssItems) : Promise.resolve([]), enrichItemCategories(apmItems), ]); const [enrichedAptssItems, enrichedApmItems] = await Promise.all([ - enrichAptssItems(categorizedAptssItems, runCommand), + aptssAvailable + ? enrichAptssItems(categorizedAptssItems, runCommand) + : Promise.resolve({ items: [], warnings: [] }), enrichApmItems(categorizedApmItems, runCommand), ]); diff --git a/src/App.vue b/src/App.vue index 2d3f2f68..b15f4806 100644 --- a/src/App.vue +++ b/src/App.vue @@ -816,7 +816,19 @@ const refreshInstalledApps = async () => { installedError.value = ""; try { const origin = activeInstalledOrigin.value; - const result = await window.ipcRenderer.invoke("list-installed", origin); + + // Spark 优化:只检查远端商店目录中的应用,避免全量扫描 + let pkgnameList: string[] | undefined; + if (origin === "spark") { + pkgnameList = apps.value + .filter((a) => a.origin === "spark") + .map((a) => a.pkgname); + } + + const result = await window.ipcRenderer.invoke("list-installed", { + origin, + pkgnameList, + }); if (!result?.success) { installedApps.value = []; installedError.value = result?.message || "读取已安装应用失败"; @@ -835,15 +847,6 @@ const refreshInstalledApps = async () => { continue; } - // 二次确认:使用 check-installed 验证包是否真正安装 - const isReallyInstalled = await window.ipcRenderer.invoke( - "check-installed", - { pkgname: app.pkgname, origin: app.origin }, - ); - if (!isReallyInstalled) { - continue; - } - if (appInfo) { appInfo.flags = app.flags; appInfo.arch = app.arch;