# APM 应用商店 - AI 编码指南
**仓库:** elysia-best/apm-app-store
**项目类型:** Electron + Vue 3 + Vite 桌面应用
**用途:** APM (AmberPM) 包管理器的桌面应用商店客户端
**许可证:** 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: true,
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** - `