Files
prompt-optimizer/docs/developer/desktop-developer-guide.md

256 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prompt Optimizer 桌面应用开发者指南
## 1. 项目背景与目标
用户希望将现有的 Prompt Optimizer Web 应用改造为桌面端应用,其核心目标是**利用 Electron 主进程代理 API 请求,从而彻底解决浏览器的 CORS 跨域问题**。
### 技术选型:为何选择 Electron
- **技术栈统一**: Electron 允许我们复用现有的 JavaScript/TypeScript 和 Vue 技术栈,无需引入 Rust (Tauri 方案) 等新技术,降低了团队的学习成本和开发门槛。
- **最小化代码侵入**: 通过 Electron 的进程间通信IPC机制我们可以实现一个无缝的 API 请求代理,仅需在 SDK 初始化时注入一个自定义的网络请求函数,对核心业务逻辑 (`packages/core`) 的侵入极小。
- **生态成熟**: Electron 拥有庞大而成熟的社区和生态系统,为未来的功能扩展(如自动更新、系统通知)提供了强有力的保障。
## 2. 架构设计
应用采用**高层服务代理**架构,职责清晰,维护性强。主进程作为后端服务提供者,渲染进程作为前端消费者。
### 整体架构图
```
┌─────────────────────────────────────────────────────────────┐
│ Electron 桌面应用 │
├─────────────────────────────────────────────────────────────┤
│ 主进程 (main.js) - 服务端 │
│ - 窗口管理 │
│ - **直接消费 @prompt-optimizer/core 包** │
│ - **实例化并持有核心服务 (LLMService, ModelManager)** │
│ - **作为后端,通过 IPC 提供高层服务接口 (如 testConnection)** │
├─────────────────────────────────────────────────────────────┤
│ 预加载脚本 (preload.js) - 安全桥梁 │
│ - 将主进程的高层服务接口 (`llm.testConnection`) │
│ - 安全地暴露给渲染进程 (`window.electronAPI.llm.*`) │
├─────────────────────────────────────────────────────────────┤
│ 渲染进程 (Vue 应用) - 纯前端消费者 │
│ - UI 界面与用户交互 │
│ - **通过 `core` 包中的代理对象 (`ElectronLLMProxy`)** │
│ - **调用 `window.electronAPI.llm.testConnection()`** │
│ - **不直接处理网络请求,只调用定义好的服务接口** │
└─────────────────────────────────────────────────────────────┘
```
### 服务调用数据流
```
1. 用户在UI上操作触发 Vue 组件中的方法
2. Vue 组件调用 `core` 包中面向 Electron 的代理服务 (`ElectronLLMProxy`)
3. 代理服务调用预加载脚本暴露的 `window.electronAPI.llm.testConnection()` (IPC 调用)
4. 预加载脚本通过 `ipcRenderer` 将请求发送给主进程
5. 主进程的 `ipcMain` 监听器捕获请求,直接调用**主进程中持有的真实 LLMService 实例**
6. LLMService 实例在 Node.js 环境中,使用 `node-fetch` 发起真实的 API 请求
7. 最终结果 (JSON 数据,非 Response 对象) 沿原路返回:主进程 → 预加载脚本 → 代理服务 → Vue 组件 → UI 更新
```
### 核心架构详解:代理模式与进程间通信 (IPC)
为了深刻理解新架构的健壮性,必须理解其背后的核心理念:**主进程是"大脑",渲染进程是"四肢"**。所有的记忆、思考和决策(核心服务)都必须由"大脑"统一做出,而"四肢"UI只负责感知和行动。
#### 1. 为何不能在UI层直接调用 `core` 模块?
在纯Web应用中UI和Core生活在同一个世界里单进程可以直接通信。但在Electron中主进程和渲染进程是两个**完全隔离的操作系统进程**,拥有各自独立的内存空间。
如果在UI层渲染进程直接调用 `createModelManager()`,会发生什么?
- **数据孤岛**:会在渲染进程中创建一个**全新的、空白的**`ModelManager`实例。它与主进程中那个拥有真实数据的实例**互不相通**,导致数据永远无法同步。
- **能力缺失**`core`模块的部分功能如未来要实现的文件读写依赖于Node.js环境。渲染进程基于Chromium没有这些能力调用相关功能将直接导致**应用崩溃**。
#### 2. `ipcRenderer` 与 `ipcMain`:两个世界的电话
进程间通信IPC是连接这两个隔离世界的唯一桥梁。
- **`ipcRenderer`**: 安装在**渲染进程**的"电话",专门用于向主进程"打电话"(发起请求)。
- **`ipcMain`**: 安装在**主进程**的"总机",专门用于"接电话"(处理请求)。
我们主要使用`invoke`/`handle`这种**双向通信**模式,它完美地模拟了"请求-响应"的异步流程。
#### 3. `ElectronModelManagerProxy`:优雅的"全权代理"
直接让UI层去操作`ipcRenderer.invoke('channel-name', ...)`这种底层的"电话指令"是混乱且不安全的。为此,我们引入了**代理模式 (Proxy Pattern)**。
`ElectronModelManagerProxy`这类代理类的核心作用是**"假装"自己是真正的 `ModelManager`**从而让UI层的代码可以像以前一样无缝调用无需关心背后复杂的跨进程通信。
它的工作流程是一场精密的"拦截-转发-返回"
1. **UI调用**UI调用`modelManager.getModels()`
2. **Proxy拦截**:实际上调用的是`ElectronModelManagerProxy`实例的同名方法。
3. **Proxy转发**:该方法不包含业务逻辑,只负责通过`preload.js`暴露的`electronAPI`,最终调用`ipcRenderer.invoke('model-getModels')`
4. **主进程处理**`ipcMain.handle`捕获请求,调用**主进程中唯一的、真实的`ModelManager`实例**,处理并返回数据。
5. **数据返回**结果沿原路返回最终交付给UI组件。
这个模式虽然在新增方法时需要在多个文件(`main.js`, `preload.js`, `proxy.ts`)中添加"样板代码",但这并非无意义的重复,而是为了换取**单一数据源、安全的边界和优雅的类型安全抽象**所付出的、性价比极高的代价。
## 3. 快速启动 (开发模式)
### 系统要求
- Windows 10/11, macOS, or Linux
- Node.js 18+
- pnpm 8+
### 启动步骤
```bash
# 1. (首次) 在项目根目录安装所有依赖
pnpm install
# 2. 运行桌面应用开发模式
pnpm dev:desktop
```
此命令将同时启动 Vite 开发服务器(用于前端界面)和 Electron 应用实例,并开启热重载。
## 4. 核心技术实现
当前架构放弃了脆弱的底层 `fetch` 代理,转向更稳定、更易于维护的**高层服务代理模型**。
### 服务消费模型
主进程 (`main.js`) 现在作为后端服务,直接消费 `packages/core` 的能力,完全复用其业务逻辑,避免了代码冗余。
```javascript
// main.js - 主进程直接导入并使用 core 包
const {
createLLMService,
createModelManager,
// ... 其他服务
} = require('@prompt-optimizer/core');
// 在主进程启动时实例化服务
let llmService;
app.whenReady().then(() => {
// 此处需要一个适合 Node.js 的存储方案 (见下文)
const modelManager = createModelManager(/* ... */);
// 创建一个在 Node.js 环境中运行的真实 LLMService 实例
llmService = createLLMService(modelManager);
// 将服务实例传递给 IPC 设置函数
setupIPC(llmService);
});
```
### 高层 IPC 接口
渲染进程与主进程之间的通信"契约",从不稳定的 `fetch` API 升级为我们自己定义的、稳定的 `ILLMService` 接口。
```javascript
// main.js - 提供服务接口
function setupIPC(llmService) {
ipcMain.handle('llm-testConnection', async (event, provider) => {
try {
await llmService.testConnection(provider);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// ... 其他接口的实现
}
// preload.js - 暴露服务接口
contextBridge.exposeInMainWorld('electronAPI', {
llm: {
testConnection: (provider) => ipcRenderer.invoke('llm-testConnection', provider),
// ... 其他接口的暴露
}
});
```
### 存储策略
由于渲染进程的 `IndexedDB` 在主进程 (Node.js) 中不可用,我们为桌面端设计了分阶段的存储方案:
- **第一阶段 (当前实现):** 采用一个临时的**内存存储**方案。这使得新架构可以快速运行起来,但应用关闭后数据会丢失。
- **第二阶段 (未来计划):** 实现一个**文件存储 (`FileStorageProvider`)**,将模型、模板等数据以 JSON 文件的形式持久化存储在用户本地磁盘上,充分利用桌面环境的优势。
## 5. 构建与部署
### 开发脚本
- `pnpm dev:desktop`: 同时启动前端开发服务器和 Electron 应用,用于日常开发。
- `pnpm build:web`: 仅构建前端 Web 应用,产物输出到 `packages/desktop/web-dist`
- `pnpm build:desktop`: 构建最终的可分发桌面应用程序(如 `.exe``.dmg`)。
### 生产版本构建流程
```bash
# 完整构建流程,将自动先构建 web 内容
pnpm build:desktop
# 构建完成后,可执行文件位于以下目录
# packages/desktop/dist/
```
### Electron Builder 配置
打包配置位于 `packages/desktop/package.json``build` 字段中。
```json
{
"build": {
"appId": "com.promptoptimizer.desktop",
"productName": "Prompt Optimizer",
"directories": { "output": "dist" },
"files": [
"main.js",
"preload.js",
"web-dist/**/*", // 将构建好的前端应用打包进去
"node_modules/**/*"
],
"win": {
"target": "nsis", // Windows 安装包格式
"icon": "icon.ico" // 应用图标
}
}
}
```
## 6. 故障排除
**1. 应用启动失败或界面空白**
- 确保 `pnpm install` 已成功执行。
- 确认 `pnpm build:web` 是否成功执行,并且 `packages/desktop/web-dist` 目录已生成且内容不为空。
- 尝试清理并重新安装: `pnpm store prune && pnpm install`
**2. Electron 安装不完整**
- 这通常是网络问题。仓库默认不再提交 `electron_mirror` 镜像配置;如本地网络需要,请在用户级 `~/.npmrc` 或 shell 环境变量中按需配置,再重试安装。
- 手动安装命令:
```bash
# (路径可能因 pnpm 版本而异)
cd node_modules/.pnpm/electron@<version>/node_modules/electron
node install.js
```
**3. API 调用失败**
- 检查 API 密钥是否在桌面应用的 "模型管理" 页面中正确配置。
- 打开开发者工具 (`Ctrl+Shift+I`) 查看渲染进程的 `Console`。
- **关键:** 由于核心 API 调用逻辑已移至主进程,请务必**检查启动桌面应用的终端(命令行窗口)中的日志输出**,那里会包含最直接的 `node-fetch` 错误信息。
- 确认网络连接正常。
## 7. 未来架构改进方向
当前手动维护多个文件的IPC"样板代码"是清晰和健壮的,但随着功能扩展,开发效率和一致性会成为挑战。未来,我们可以采用**代码生成 (Code Generation)**的方案来彻底解决这个问题。
### 核心理念
我们唯一的、需要手动维护的文件,应该是服务的**接口定义**(例如 `IModelManager`)。我们将这个接口作为**"单一事实源" (Single Source of Truth)**。
### 自动化工作流
1. **定义蓝图**: 在`core`包的`types.ts`文件中维护`IModelManager`等接口。
2. **编写生成器脚本**: 使用`ts-morph`等库编写一个Node.js脚本该脚本能够读取并解析TypeScript接口的结构方法名、参数、返回值等
3. **自动生成样板代码**: 脚本遍历接口中的每个方法,并根据预设模板,自动生成`main.js`中的`ipcMain.handle`、`preload.js`中的`ipcRenderer`调用,以及`electron-proxy.ts`中的代理方法。
4. **一键更新**: 将此脚本集成到`package.json`中。未来新增/修改/删除一个接口方法时,开发者只需修改接口定义,然后运行一个命令(如`pnpm generate:ipc`所有相关的IPC代码都会被自动、无误地更新。
### 备选方案
社区中成熟的`tRPC`框架也提供了类似的思路,其核心就是"零代码生成"的类型安全API层。我们可以借鉴其思想甚至尝试将其集成到Electron的IPC机制中。
采用此方案后我们的开发流程将变得极为高效和安全彻底消除手动维护IPC调用可能带来的所有潜在错误。