diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index 8bccad9f..ecfdf8b5 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -1,4 +1,4 @@ -import { ipcMain, WebContents } from "electron"; +import { BrowserWindow, dialog, ipcMain, WebContents } from "electron"; import { spawn, ChildProcess, exec } from "node:child_process"; import { promisify } from "node:util"; import fs from "node:fs"; @@ -101,6 +101,33 @@ const parseInstalledList = (output: string) => { return apps; }; +/** 检测本机是否已安装 apm 命令 */ +const checkApmAvailable = async (): Promise => { + const { code, stdout } = await runCommandCapture("which", ["apm"]); + const found = code === 0 && stdout.trim().length > 0; + if (!found) logger.info("未检测到 apm 命令"); + return found; +}; + +/** 提权执行 shell-caller aptss install apm 安装 APM,安装后需用户重启电脑 */ +const runInstallApm = async (superUserCmd: string): Promise => { + const execCommand = superUserCmd || SHELL_CALLER_PATH; + const execParams = superUserCmd + ? [SHELL_CALLER_PATH, "aptss", "install", "apm"] + : [SHELL_CALLER_PATH, "aptss", "install", "apm"]; + logger.info(`执行安装 APM: ${execCommand} ${execParams.join(" ")}`); + const { code, stdout, stderr } = await runCommandCapture( + execCommand, + execParams, + ); + if (code !== 0) { + logger.error({ code, stdout, stderr }, "安装 APM 失败"); + return false; + } + logger.info("安装 APM 完成"); + return true; +}; + const parseUpgradableList = (output: string) => { const apps: Array<{ pkgname: string; @@ -175,6 +202,63 @@ ipcMain.on("queue-install", async (event, download_json) => { const execParams = []; const downloadDir = `/tmp/spark-store/download/${pkgname}`; + // APM 应用:若本机没有 apm 命令,弹窗提示并可选提权安装 APM(安装后需重启电脑) + if (origin === "apm") { + const hasApm = await checkApmAvailable(); + if (!hasApm) { + const win = BrowserWindow.fromWebContents(webContents); + const { response } = await dialog.showMessageBox(win ?? undefined, { + type: "question", + title: "需要安装 APM", + message: "此应用需要使用 APM 安装。", + detail: + "APM是星火应用商店的容器包管理器,安装APM后方可安装此应用,是否确认安装?", + buttons: ["确认", "取消"], + defaultId: 0, + cancelId: 1, + }); + if (response !== 0) { + webContents.send("install-complete", { + id, + success: false, + time: Date.now(), + exitCode: -1, + message: JSON.stringify({ + message: "用户取消安装 APM,无法继续安装此应用", + stdout: "", + stderr: "", + }), + }); + return; + } + const installApmOk = await runInstallApm(superUserCmd); + if (!installApmOk) { + webContents.send("install-complete", { + id, + success: false, + time: Date.now(), + exitCode: -1, + message: JSON.stringify({ + message: "安装 APM 失败,请检查网络或权限后重试", + stdout: "", + stderr: "", + }), + }); + return; + } else { + // 安装APM成功,提示用户已安装成功,需要重启后方可展示应用 + await dialog.showMessageBox(win ?? undefined, { + type: "info", + title: "APM 安装成功", + message: "恭喜您,APM 已成功安装", + detail: "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。", + buttons: ["确定"], + defaultId: 0, + }); + } + } + } + if (origin === "spark") { // Spark Store logic if (upgradeOnly) { @@ -586,31 +670,6 @@ ipcMain.handle("list-upgradable", async () => { return { success: true, apps }; }); -ipcMain.handle("list-installed", async () => { - const superUserCmd = await checkSuperUserCommand(); - const execCommand = - superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; - const execParams = - superUserCmd.length > 0 - ? [SHELL_CALLER_PATH, "aptss", "list", "--installed"] - : ["aptss", "list", "--installed"]; - - const { code, stdout, stderr } = await runCommandCapture( - execCommand, - execParams, - ); - if (code !== 0) { - logger.error(`list-installed failed: ${stderr || stdout}`); - return { - success: false, - message: stderr || stdout || `list-installed failed with code ${code}`, - apps: [], - }; - } - - const apps = parseInstalledList(stdout); - return { success: true, apps }; -}); // eslint-disable-next-line @typescript-eslint/no-explicit-any ipcMain.handle("uninstall-installed", async (_event, payload: any) => { diff --git a/electron/main/index.ts b/electron/main/index.ts index 88849693..a6bced11 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -3,6 +3,7 @@ import { BrowserWindow, ipcMain, Menu, + nativeImage, shell, Tray, nativeTheme, @@ -234,36 +235,45 @@ app.on("will-quit", () => { logger.info("Done, exiting"); }); -// 设置托盘 -// 获取图标路径 -function getIconPath() { - let iconPath = ""; - const iconName = - process.platform === "win32" ? "amber-pm-logo.ico" : "amber-pm-logo.png"; // 图标文件名,linux下需要png格式,不然会不显示 - // 判断是否在打包模式 - if (app.isPackaged) { - // 打包模式 - iconPath = path.join(process.resourcesPath, "icons", iconName); // 路径根据自身情况调整 - } else { - // 开发模式 - const projectRoot = path.join(__dirname, "../.."); // __dirname 指向 dist-electron/main,但资源在项目根目录,所以..指向上一级 - iconPath = path.join(projectRoot, "icons", iconName); - } +// 设置托盘:系统中应用名称为 spark-store,图标优先 spark-store,其次 spark-store.svg,再次替代图标 +const ICONS_DIR = app.isPackaged + ? path.join(process.resourcesPath, "icons") + : path.join(__dirname, "../..", "icons"); - // 检查文件是否存在 - if (fs.existsSync(iconPath)) { - logger.info("图标文件存在:" + iconPath); - return iconPath; - } else { - logger.error("图标文件不存在:" + iconPath); - // 返回一个默认图标路径或null - return null; - } +function resolveIconPath(filename: string): string { + return path.join(ICONS_DIR, filename); } -let tray = null; +/** 按优先级返回托盘图标路径:spark-store(.png|.ico) → amber-pm-logo.png。托盘不支持 SVG,故不尝试 spark-store.svg */ +function getTrayIconPath(): string | null { + const ext = process.platform === "win32" ? ".ico" : ".png"; + const candidates = [ + `spark-store${ext}` + ]; + for (const name of candidates) { + const iconPath = resolveIconPath(name); + if (fs.existsSync(iconPath)) { + logger.info("托盘图标使用: " + iconPath); + return iconPath; + } + } + logger.warn("未找到托盘图标,将使用替代图标。查找目录: " + ICONS_DIR); + return null; +} + +/** 16x16 透明 PNG,用作托盘无图标时的替代 */ +const FALLBACK_TRAY_PNG = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg=="; + +function getTrayImage(): string | ReturnType { + const iconPath = getTrayIconPath(); + if (iconPath) return iconPath; + return nativeImage.createFromDataURL(FALLBACK_TRAY_PNG); +} + +let tray: Tray | null = null; app.whenReady().then(() => { - tray = new Tray(getIconPath()); + tray = new Tray(getTrayImage()); const contextMenu = Menu.buildFromTemplate([ { label: "显示主界面", diff --git a/extras/shell-caller.sh b/extras/shell-caller.sh index 38aac794..59468bb7 100755 --- a/extras/shell-caller.sh +++ b/extras/shell-caller.sh @@ -55,10 +55,39 @@ case "$command_type" in echo "操作已取消" exit 0 fi + elif [[ "$2" == "install" ]]; then + packages="${@:3}" + # 确认框通用参数 + title="确认安装" + text="正在准备安装: $packages\n\n若这是您下达的安装指令,请选择确认继续安装" + + # 优先尝试 garma,其次 zenity + if command -v garma &> /dev/null; then + garma --question --title="$title" --text="$text" \ + --ok-label="确认安装" --cancel-label="取消" --width=400 + confirmed=$? + elif command -v zenity &> /dev/null; then + zenity --question --title="$title" --text="$text" \ + --ok-label="确认安装" --cancel-label="取消" --width=400 + confirmed=$? + else + echo "错误:未找到 garma 或 zenity,无法显示确认对话框。安装操作已拒绝。" + exit 1 + fi + + # 根据确认结果执行 + if [[ $confirmed -eq 0 ]]; then + /usr/bin/aptss "${@:2}" -y 2>&1 + exit_code=$? + else + echo "操作已取消" + exit 0 + fi + else - # 非 remove 命令,直接执行 aptss - /usr/bin/aptss "${@:2}" 2>&1 - exit_code=$? + # 非 remove 命令,拒绝执行 + echo "拒绝执行 aptss 白名单外的指令" + exit 1 fi ;; diff --git a/extras/spark-store b/extras/spark-store new file mode 100755 index 00000000..d5bc45ae --- /dev/null +++ b/extras/spark-store @@ -0,0 +1,28 @@ +#!/bin/bash + +# 基础参数,始终添加 --no-sandbox +ARGS="--no-sandbox" + +# 检查是否在容器中运行 +# 方法1: 检查 root 路径 +ROOT_PATH=$(readlink -f /proc/self/root) +if [ "$ROOT_PATH" != "/" ]; then + echo "检测到容器环境 (root path: $ROOT_PATH)" + ARGS="$ARGS --no-apm" +fi + +# 方法2: 检查 IS_ACE_ENV 环境变量 +if [ "$IS_ACE_ENV" = "1" ]; then + echo "检测到 ACE 容器环境" + ARGS="$ARGS --no-apm" +fi + +# 检查是否为 arm64 且为 wayland session +ARCH=$(uname -m) +if [ "$ARCH" = "aarch64" ] && [ "$XDG_SESSION_TYPE" = "wayland" ]; then + echo "检测到 arm64 架构和 Wayland 会话" + ARGS="$ARGS --disable-gpu" +fi + +# 执行程序 +/opt/spark-store/bin/spark-store $ARGS "$@" \ No newline at end of file