docs/developer/desktop-developer-guide.md
用户希望将现有的 Prompt Optimizer Web 应用改造为桌面端应用,其核心目标是利用 Electron 主进程代理 API 请求,从而彻底解决浏览器的 CORS 跨域问题。
packages/core) 的侵入极小。应用采用高层服务代理架构,职责清晰,维护性强。主进程作为后端服务提供者,渲染进程作为前端消费者。
┌─────────────────────────────────────────────────────────────┐
│ 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 更新
为了深刻理解新架构的健壮性,必须理解其背后的核心理念:主进程是"大脑",渲染进程是"四肢"。所有的记忆、思考和决策(核心服务)都必须由"大脑"统一做出,而"四肢"(UI)只负责感知和行动。
core 模块?在纯Web应用中,UI和Core生活在同一个世界里(单进程),可以直接通信。但在Electron中,主进程和渲染进程是两个完全隔离的操作系统进程,拥有各自独立的内存空间。
如果在UI层(渲染进程)直接调用 createModelManager(),会发生什么?
ModelManager实例。它与主进程中那个拥有真实数据的实例互不相通,导致数据永远无法同步。core模块的部分功能(如未来要实现的文件读写)依赖于Node.js环境。渲染进程(基于Chromium)没有这些能力,调用相关功能将直接导致应用崩溃。ipcRenderer 与 ipcMain:两个世界的电话进程间通信(IPC)是连接这两个隔离世界的唯一桥梁。
ipcRenderer: 安装在渲染进程的"电话",专门用于向主进程"打电话"(发起请求)。ipcMain: 安装在主进程的"总机",专门用于"接电话"(处理请求)。我们主要使用invoke/handle这种双向通信模式,它完美地模拟了"请求-响应"的异步流程。
ElectronModelManagerProxy:优雅的"全权代理"直接让UI层去操作ipcRenderer.invoke('channel-name', ...)这种底层的"电话指令"是混乱且不安全的。为此,我们引入了代理模式 (Proxy Pattern)。
ElectronModelManagerProxy这类代理类的核心作用是**"假装"自己是真正的 ModelManager**,从而让UI层的代码可以像以前一样无缝调用,无需关心背后复杂的跨进程通信。
它的工作流程是一场精密的"拦截-转发-返回":
modelManager.getModels()。ElectronModelManagerProxy实例的同名方法。preload.js暴露的electronAPI,最终调用ipcRenderer.invoke('model-getModels')。ipcMain.handle捕获请求,调用主进程中唯一的、真实的ModelManager实例,处理并返回数据。这个模式虽然在新增方法时需要在多个文件(main.js, preload.js, proxy.ts)中添加"样板代码",但这并非无意义的重复,而是为了换取单一数据源、安全的边界和优雅的类型安全抽象所付出的、性价比极高的代价。
# 1. (首次) 在项目根目录安装所有依赖
pnpm install
# 2. 运行桌面应用开发模式
pnpm dev:desktop
此命令将同时启动 Vite 开发服务器(用于前端界面)和 Electron 应用实例,并开启热重载。
当前架构放弃了脆弱的底层 fetch 代理,转向更稳定、更易于维护的高层服务代理模型。
主进程 (main.js) 现在作为后端服务,直接消费 packages/core 的能力,完全复用其业务逻辑,避免了代码冗余。
// 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);
});
渲染进程与主进程之间的通信"契约",从不稳定的 fetch API 升级为我们自己定义的、稳定的 ILLMService 接口。
// 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 文件的形式持久化存储在用户本地磁盘上,充分利用桌面环境的优势。pnpm dev:desktop: 同时启动前端开发服务器和 Electron 应用,用于日常开发。pnpm build:web: 仅构建前端 Web 应用,产物输出到 packages/desktop/web-dist。pnpm build:desktop: 构建最终的可分发桌面应用程序(如 .exe 或 .dmg)。# 完整构建流程,将自动先构建 web 内容
pnpm build:desktop
# 构建完成后,可执行文件位于以下目录
# packages/desktop/dist/
打包配置位于 packages/desktop/package.json 的 build 字段中。
{
"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" // 应用图标
}
}
}
1. 应用启动失败或界面空白
pnpm install 已成功执行。pnpm build:web 是否成功执行,并且 packages/desktop/web-dist 目录已生成且内容不为空。pnpm store prune && pnpm install。2. Electron 安装不完整
electron_mirror 镜像配置;如本地网络需要,请在用户级 ~/.npmrc 或 shell 环境变量中按需配置,再重试安装。# (路径可能因 pnpm 版本而异)
cd node_modules/.pnpm/electron@<version>/node_modules/electron
node install.js
3. API 调用失败
Ctrl+Shift+I) 查看渲染进程的 Console。node-fetch 错误信息。当前手动维护多个文件的IPC"样板代码"是清晰和健壮的,但随着功能扩展,开发效率和一致性会成为挑战。未来,我们可以采用**代码生成 (Code Generation)**的方案来彻底解决这个问题。
我们唯一的、需要手动维护的文件,应该是服务的接口定义(例如 IModelManager)。我们将这个接口作为**"单一事实源" (Single Source of Truth)**。
core包的types.ts文件中维护IModelManager等接口。ts-morph等库编写一个Node.js脚本,该脚本能够读取并解析TypeScript接口的结构(方法名、参数、返回值等)。main.js中的ipcMain.handle、preload.js中的ipcRenderer调用,以及electron-proxy.ts中的代理方法。package.json中。未来新增/修改/删除一个接口方法时,开发者只需修改接口定义,然后运行一个命令(如pnpm generate:ipc),所有相关的IPC代码都会被自动、无误地更新。社区中成熟的tRPC框架也提供了类似的思路,其核心就是"零代码生成"的类型安全API层。我们可以借鉴其思想,甚至尝试将其集成到Electron的IPC机制中。
采用此方案后,我们的开发流程将变得极为高效和安全,彻底消除手动维护IPC调用可能带来的所有潜在错误。