# 星火应用商店 - AI 编码指南 **仓库:** spark-store-project/spark-store **项目类型:** Electron + Vue 3 + Vite + Qt 桌面应用 **用途:** 星火应用商店的桌面应用商店客户端 **许可证:** GPL-3.0 --- 如果你是 AI 编码代理,在此仓库工作时,请遵循以下指南: ## 🏗️ 项目架构概览 ### 技术栈 - **前端框架:** Vue 3 with Composition API (` ``` ### Props 和 Events 模式 ```typescript // Props 定义 const props = defineProps<{ app: App | null; show: boolean; }>(); // Emits 定义 const emit = defineEmits<{ close: []; install: []; remove: []; }>(); // 使用 emit("install"); ``` ### Vue 中的 IPC 事件监听 **始终在 `onMounted` 中使用以便正确清理:** ```typescript onMounted(() => { window.ipcRenderer.on( "install-complete", (_event: IpcRendererEvent, result: DownloadResult) => { // 处理事件 }, ); window.ipcRenderer.on( "install-log", (_event: IpcRendererEvent, log: InstallLog) => { // 处理日志 }, ); }); ``` --- ## 🔧 主进程模式 ### 生成 APM 命令 ```typescript import { spawn } from "node:child_process"; // 检查权限提升 const superUserCmd = await checkSuperUserCommand(); const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; const execParams = superUserCmd.length > 0 ? [SHELL_CALLER_PATH, "apm", "install", "-y", pkgname] : ["apm", "install", "-y", pkgname]; // 生成进程 const child = spawn(execCommand, execParams, { shell: false, env: process.env, }); // 流式输出 child.stdout.on("data", (data) => { webContents.send("install-log", { id, time: Date.now(), message: data.toString(), }); }); // 处理完成 child.on("close", (code) => { const success = code === 0; webContents.send("install-complete", { id, success, exitCode: code /* ... */, }); }); ``` ### 解析 APM 输出 **APM 输出是基于文本的特定格式:** ```typescript // 已安装包格式: "pkgname/repo,version arch [flags]" // 示例: "code/stable,1.108.2 amd64 [installed]" const parseInstalledList = (output: string) => { const apps: InstalledAppInfo[] = []; const lines = output.split("\n"); for (const line of lines) { const match = line .trim() .match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/); if (match) { apps.push({ pkgname: match[1], version: match[2], arch: match[3], flags: match[4], raw: line.trim(), }); } } return apps; }; ``` --- ## 🌐 API 集成 ### 基础配置 ````typescript // src/global/storeConfig.ts export const APM_STORE_BASE_URL = 'https://erotica.spark-app.store'; // URL 结构: // /{arch}/{category}/applist.json - 应用列表 // /{arch}/{category}/{pkgname}/icon.png - 应用图标 // /{arch}/{category}/{pkgname}/screen_N.png - 截图 (1-5) // /{arch}/categories.json - 分类映射 ### 首页 (主页) 数据 商店可能会在 `{arch}` 下提供一个特殊的 `home` 目录用于本地化首页。预期有两个 JSON 文件: - `homelinks.json` — 用于构建首页的轮播或链接块。每个条目示例: ```json { "name": "交流反馈", "more": "前往论坛交流讨论", "imgUrl": "/home/links/bbs.png", "type": "_blank", "url": "https://bbs.spark-app.store/" } ```` - `homelist.json` — 描述若干推荐应用列表,每项引用一个 JSON 列表(`jsonUrl`): ```json [ { "name": "装机必备", "type": "appList", "jsonUrl": "/home/lists/NecessaryforInstallation.json" } ] ``` 应用使用的解析规则: - 通过前缀解析 `imgUrl`: `${APM_STORE_BASE_URL}/{arch}${imgUrl}`。 - `type: _blank` → 使用系统浏览器打开链接;`type: _self` → 在当前页面打开。 - 对于 `homelist.json` 中 `type: "appList"` 的条目,获取引用的 `jsonUrl` 并将每个项映射到 UI 使用的应用形状: - `Name` → `name` - `Pkgname` → `pkgname` - `Category` → `category` - `More` → `more` 实现位置: - 渲染进程: `src/App.vue` 在选择 `home` 分类时加载并规范化 `homelinks.json` 和 `homelist.json`,并将数据暴露给新的 `HomeView` 组件。 - 组件: `src/components/HomeView.vue` 渲染链接卡片和推荐应用部分(复用 `AppCard.vue`)。 注意事项: - `home` 目录路径是: 配置的 `APM_STORE_BASE_URL` 下的 `/{arch}/home/`。 - 缺失或部分无效的文件会被优雅地处理 — 个别失败不会阻止显示其他首页部分。 ```` ### Axios 使用 ```typescript const axiosInstance = axios.create({ baseURL: APM_STORE_BASE_URL, timeout: 1000, // 注意: 非常短的超时时间! }); // 按分类加载应用 const response = await axiosInstance.get( `/${window.apm_store.arch}/${category}/applist.json` ); ```` **开发代理 (vite.config.ts):** ```typescript server: { proxy: { '/local_amd64-store': { target: 'https://erotica.spark-app.store', changeOrigin: true, rewrite: (path) => path.replace(/^\/local_amd64-store/, ''), } } } ``` --- ## 🎯 Deep Link 协议 (SPK URI Scheme) **URL Scheme:** `spk://` ### 支持的 SPK URI 格式 格式: `spk://search/{pkgname}` **示例:** - `spk://search/code` - 搜索并打开 "code" 应用 - `spk://search/steam` - 搜索并打开 "steam" 应用 - `spk://search/store.spark-app.hmcl` - 搜索并打开 "HMCL" 游戏 ### 实现模式 ```typescript // electron/main/deeplink.ts - 解析命令行并路由 export function handleCommandLine(commandLine: string[]) { const deeplinkUrl = commandLine.find((arg) => arg.startsWith("spk://")); if (!deeplinkUrl) return; try { const url = new URL(deeplinkUrl); const action = url.hostname; // 'search' if (action === "search") { // 格式: spk://search/pkgname // url.pathname 将是 '/pkgname' const pkgname = url.pathname.split("/").filter(Boolean)[0]; if (pkgname) { listeners.emit("search", { pkgname }); } } } catch (error) { logger.error({ err: error }, "Error parsing SPK URI"); } } // src/App.vue - 在渲染进程中处理 window.ipcRenderer.on( "deep-link-search", (_event: IpcRendererEvent, data: { pkgname: string }) => { // 使用 pkgname 触发搜索 searchQuery.value = data.pkgname; }, ); ``` --- ## 🛡️ 安全考虑 ### 权限提升 **始终检查 `pkexec` 的可用性:** ```typescript const checkSuperUserCommand = async (): Promise => { if (process.getuid && process.getuid() !== 0) { const { stdout } = await execAsync("which /usr/bin/pkexec"); return stdout.trim().length > 0 ? "/usr/bin/pkexec" : ""; } return ""; }; ``` ### 上下文隔离 **当前状态:** 上下文隔离已 **启用** (Electron 默认行为)。 **通过 Preload 安全地暴露 IPC:** ```typescript // electron/preload/index.ts contextBridge.exposeInMainWorld("ipcRenderer", { on: (...args) => ipcRenderer.on(...args), send: (...args) => ipcRenderer.send(...args), invoke: (...args) => ipcRenderer.invoke(...args), }); ``` **⚠️ 不要启用 nodeIntegration 或禁用 contextIsolation!** --- ## 🎨 UI/UX 模式 ### Tailwind CSS 使用 **暗色模式支持:** ```vue
``` **主题切换:** ```typescript const isDarkTheme = ref(false); watch(isDarkTheme, (newVal) => { localStorage.setItem("theme", newVal ? "dark" : "light"); document.documentElement.classList.toggle("dark", newVal); }); ``` ### 模态框模式 ```vue ``` ### 加载状态 ```typescript const loading = ref(true); // 在模板中
Loading...
{{ apps.length }} apps
``` --- ## 🧪 测试与质量 ### ESLint 配置 ```typescript // eslint.config.ts export default defineConfig([ globalIgnores([ "**/3rdparty/**", "**/node_modules/**", "**/dist/**", "**/dist-electron/**", ]), tseslint.configs.recommended, pluginVue.configs["flat/essential"], eslintConfigPrettier, eslintPluginPrettierRecommended, ]); ``` ### TypeScript 配置 ```json { "compilerOptions": { "strict": true, // 严格模式已启用 "noEmit": true, // 不输出 (Vite 处理构建) "module": "ESNext", "target": "ESNext", "jsx": "preserve", // Vue JSX "resolveJsonModule": true // 导入 JSON 文件 } } ``` ### 代码质量命令 ```bash npm run lint # 运行 ESLint npm run lint:fix # 自动修复问题 npm run format # 使用 Prettier 格式化 ``` --- ## 🚀 构建与开发 ### 开发模式 ```bash npm run dev # 启动开发服务器 (Vite + Electron) ``` **开发服务器:** `http://127.0.0.1:3344/` (来自 package.json) ### 生产构建 ```bash npm run build # 构建所有 (deb + rpm) npm run build:deb # 仅构建 Debian 包 npm run build:rpm # 仅构建 RPM 包 ``` **构建输出:** - `dist-electron/` - 编译的 Electron 代码 - `dist/` - 编译的渲染器资源 - 打包的应用在项目根目录 ### 构建配置 **electron-builder.yml:** - App ID: `cn.eu.org.simplelinux.apmstore` - Linux 目标: deb, rpm - 包含 extras/ 目录在资源中 - 自动更新禁用 (Linux 包管理器处理更新) --- ## 📦 重要文件说明 ### 1. electron/main/backend/install-manager.ts **用途:** 核心包管理逻辑 **主要职责:** - 任务队列管理 - APM 命令生成 - 进度报告 - 已安装/可升级列表解析 **关键函数:** - `processNextInQueue()` - 任务处理器 - `parseInstalledList()` - 解析 APM 输出 - `checkSuperUserCommand()` - 权限提升 ### 2. src/App.vue **用途:** 根组件 **主要职责:** - 应用状态管理 - 分类/应用加载 - 模态框协调 - Deep Link 处理 ### 3. src/global/downloadStatus.ts **用途:** 下载队列状态 **关键特性:** - 响应式下载列表 - 下载项 CRUD 操作 - UI 更新的变化监听器 ### 4. electron/preload/index.ts **用途:** 渲染进程-主进程桥梁 **关键特性:** - IPC API 暴露 - 架构检测 - 加载动画 ### 5. vite.config.ts **用途:** 构建配置 **关键特性:** - Electron 插件设置 - 开发服务器代理 - Tailwind 集成 --- ## 🐛 常见陷阱与解决方案 ### 1. 重复任务处理 **问题:** 用户多次点击安装 **解决方案:** ```typescript if (tasks.has(id) && !download.retry) { logger.warn("Task already exists, ignoring duplicate"); return; } ``` ### 2. 窗口关闭行为 **问题:** 任务运行时关闭窗口 **解决方案:** ```typescript win.on("close", (event) => { event.preventDefault(); if (tasks.size > 0) { win.hide(); // 隐藏而不是关闭 win.setSkipTaskbar(true); } else { win.destroy(); // 没有任务时允许关闭 } }); ``` ### 3. 应用数据规范化 **问题:** API 返回 PascalCase,应用使用 camelCase **解决方案:** ```typescript const normalizedApp: App = { name: appJson.Name, pkgname: appJson.Pkgname, version: appJson.Version, // ... 映射所有字段 }; ``` ### 4. 截图加载 **问题:** 并非所有应用都有 5 张截图 **解决方案:** ```typescript for (let i = 1; i <= 5; i++) { const img = new Image(); img.src = screenshotUrl; img.onload = () => screenshots.value.push(screenshotUrl); // 没有 onerror 处理器 - 静默跳过缺失的图片 } ``` --- ## 📚 日志最佳实践 ### Pino Logger 使用 ```typescript import pino from "pino"; const logger = pino({ name: "module-name" }); // 级别: trace, debug, info, warn, error, fatal logger.info("Application started"); logger.error({ err }, "Failed to load apps"); logger.warn(`Package ${pkgname} not found`); ``` ### 日志位置 **开发:** 控制台使用 `pino-pretty` **生产:** 结构化 JSON 到 stdout --- ## 🔄 状态管理 ### 全局状态 (src/global/storeConfig.ts) ```typescript export const currentApp = ref(null); export const currentAppIsInstalled = ref(false); ``` **使用模式:** ```typescript import { currentApp, currentAppIsInstalled } from "@/global/storeConfig"; // 设置当前应用 currentApp.value = selectedApp; // 检查安装状态 window.ipcRenderer .invoke("check-installed", app.pkgname) .then((isInstalled: boolean) => { currentAppIsInstalled.value = isInstalled; }); ``` ### 下载队列 (src/global/downloadStatus.ts) ```typescript export const downloads = ref([]); // 添加下载 downloads.value.push(newDownload); // 移除下载 export const removeDownloadItem = (pkgname: string) => { const index = downloads.value.findIndex((d) => d.pkgname === pkgname); if (index !== -1) downloads.value.splice(index, 1); }; // 监听变化 export const watchDownloadsChange = (callback: () => void) => { watch(downloads, callback, { deep: true }); }; ``` --- ## 🎯 贡献指南 ### 添加新功能时 1. **首先添加 TypeScript 类型** (src/global/typedefinition.ts) 2. **更新 IPC 处理器** (如果需要主进程-渲染进程通信) 3. **遵循现有组件模式** (props, emits, setup) 4. **使用实际的 APM 命令测试** (不要在开发中使用 mock) 5. **完成任务时更新 README TODO 列表** ### 代码风格 - **使用 TypeScript 严格模式** - 没有 `any` 类型,除非使用 `eslint-disable` - **避免直接使用 eslint-disable** - 如果你真的不知道类型,使用 `undefined` 代替 - **优先使用 Composition API** - `