用户希望将现有的 Prompt Optimizer Web 应用改造为桌面端应用,其核心目标是利用 Electron 主进程代理 API 请求,从而彻底解决浏览器的 CORS 跨域问题。
- 技术栈统一: Electron 允许我们复用现有的 JavaScript/TypeScript 和 Vue 技术栈,无需引入 Rust (Tauri 方案) 等新技术,降低了团队的学习成本和开发门槛。
- 最小化代码侵入: 通过 Electron 的进程间通信(IPC)机制,我们可以实现一个无缝的 API 请求代理,仅需在 SDK 初始化时注入一个自定义的网络请求函数,对核心业务逻辑 (
packages/core) 的侵入极小。 - 生态成熟: Electron 拥有庞大而成熟的社区和生态系统,为未来的功能扩展(如自动更新、系统通知)提供了强有力的保障。
应用采用高层服务代理架构,职责清晰,维护性强。主进程作为后端服务提供者,渲染进程作为前端消费者。
┌─────────────────────────────────────────────────────────────┐
│ 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)只负责感知和行动。
在纯Web应用中,UI和Core生活在同一个世界里(单进程),可以直接通信。但在Electron中,主进程和渲染进程是两个完全隔离的操作系统进程,拥有各自独立的内存空间。
如果在UI层(渲染进程)直接调用 createModelManager(),会发生什么?
- 数据孤岛:会在渲染进程中创建一个全新的、空白的
ModelManager实例。它与主进程中那个拥有真实数据的实例互不相通,导致数据永远无法同步。 - 能力缺失:
core模块的部分功能(如未来要实现的文件读写)依赖于Node.js环境。渲染进程(基于Chromium)没有这些能力,调用相关功能将直接导致应用崩溃。
进程间通信(IPC)是连接这两个隔离世界的唯一桥梁。
ipcRenderer: 安装在渲染进程的"电话",专门用于向主进程"打电话"(发起请求)。ipcMain: 安装在主进程的"总机",专门用于"接电话"(处理请求)。
我们主要使用invoke/handle这种双向通信模式,它完美地模拟了"请求-响应"的异步流程。
直接让UI层去操作ipcRenderer.invoke('channel-name', ...)这种底层的"电话指令"是混乱且不安全的。为此,我们引入了代理模式 (Proxy Pattern)。
ElectronModelManagerProxy这类代理类的核心作用是**"假装"自己是真正的 ModelManager**,从而让UI层的代码可以像以前一样无缝调用,无需关心背后复杂的跨进程通信。
它的工作流程是一场精密的"拦截-转发-返回":
- UI调用:UI调用
modelManager.getModels()。 - Proxy拦截:实际上调用的是
ElectronModelManagerProxy实例的同名方法。 - Proxy转发:该方法不包含业务逻辑,只负责通过
preload.js暴露的electronAPI,最终调用ipcRenderer.invoke('model-getModels')。 - 主进程处理:
ipcMain.handle捕获请求,调用主进程中唯一的、真实的ModelManager实例,处理并返回数据。 - 数据返回:结果沿原路返回,最终交付给UI组件。
这个模式虽然在新增方法时需要在多个文件(main.js, preload.js, proxy.ts)中添加"样板代码",但这并非无意义的重复,而是为了换取单一数据源、安全的边界和优雅的类型安全抽象所付出的、性价比极高的代价。
- Windows 10/11, macOS, or Linux
- Node.js 18+
- pnpm 8+
# 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环境变量或手动安装。 - 手动安装命令:
# (路径可能因 pnpm 版本而异) cd node_modules/.pnpm/electron@<version>/node_modules/electron node install.js
3. API 调用失败
- 检查 API 密钥是否在桌面应用的 "模型管理" 页面中正确配置。
- 打开开发者工具 (
Ctrl+Shift+I) 查看渲染进程的Console。 - 关键: 由于核心 API 调用逻辑已移至主进程,请务必检查启动桌面应用的终端(命令行窗口)中的日志输出,那里会包含最直接的
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调用可能带来的所有潜在错误。