feat:项目打包成Electron
This commit is contained in:
205
docs/10-Electron桌面端设计计划.md
Normal file
205
docs/10-Electron桌面端设计计划.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# CamTalk Electron 桌面端设计计划
|
||||
|
||||
## 背景与动机
|
||||
|
||||
CamTalk 当前以 Docker 方式部署在 HTTP 服务器上(`http://8.161.227.145:9000`),浏览器的安全上下文策略导致 `navigator.mediaDevices` 为 `undefined`,摄像头和麦克风无法使用。通过 Electron 包装为桌面应用后,页面以 `file://` 协议加载,Chromium 将其视为安全上下文,`getUserMedia` 可直接正常工作,无需 HTTPS。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
Electron 桌面应用
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Main Process (Node.js) │
|
||||
│ ┌──────────────────────────────────────────┐│
|
||||
│ │ BrowserWindow 管理 ││
|
||||
│ │ 系统托盘 / 应用菜单 ││
|
||||
│ │ 摄像头 & 麦克风权限自动授予(session API) ││
|
||||
│ │ 应用生命周期管理 ││
|
||||
│ └──────────────────────────────────────────┘│
|
||||
│ ↕ contextBridge │
|
||||
│ Renderer Process (Chromium) │
|
||||
│ ┌──────────────────────────────────────────┐│
|
||||
│ │ 现有 React 前端(几乎零改动) ││
|
||||
│ │ - CameraManager / MicManager 正常工作 ││
|
||||
│ │ - VAD (ONNX Runtime Web) 正常工作 ││
|
||||
│ │ - WebSocket 连接 → Go 后端 ││
|
||||
│ └──────────────────────────────────────────┘│
|
||||
│ ↕ WebSocket (ws://) │
|
||||
│ Go 后端 (已有,无改动) │
|
||||
│ ┌──────────────────────────────────────────┐│
|
||||
│ │ :8080 网关服务 ││
|
||||
│ │ 可以是本地进程 / 远程服务器 ││
|
||||
│ └──────────────────────────────────────────┘│
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 前端改动评估
|
||||
|
||||
结论:前端代码几乎不需要改动。原因如下:
|
||||
|
||||
| 模块 | 当前实现 | Electron 下 | 需要改动? |
|
||||
|------|---------|------------|----------|
|
||||
| CameraManager | `navigator.mediaDevices.getUserMedia()` | `file://` 下可用 | 否 |
|
||||
| MicManager | `navigator.mediaDevices.getUserMedia()` | `file://` 下可用 | 否 |
|
||||
| VAD (@ricky0123/vad-web) | ONNX Runtime Web + AudioWorklet | Electron Chromium 完整支持 | 否 |
|
||||
| WebSocket | 根据 `window.location` 自动推导 URL | 需确保指向正确的后端地址 | 微调 |
|
||||
| 配置存储 (localStorage) | 浏览器 localStorage | Electron 原生支持 | 否 |
|
||||
|
||||
**唯一需要关注的点**:WebSocket URL 的推导逻辑。当前 `websocket.ts` 里是基于 `window.location.host` 推导的,Electron 下 `window.location` 是 `file://` 协议,host 为空。解决方案:在 Electron 的 Main Process 中注入一个环境变量 `VITE_WS_URL`,或者在 Main Process 中通过 preload 脚本暴露后端地址。
|
||||
|
||||
## Electron 壳子结构
|
||||
|
||||
```
|
||||
desktop/ # 新建 Electron 项目目录
|
||||
├── package.json # Electron + 构建依赖
|
||||
├── electron-builder.yml # 打包配置(可选,后续)
|
||||
├── main/
|
||||
│ ├── index.ts # Main Process 入口
|
||||
│ ├── window.ts # BrowserWindow 创建与配置
|
||||
│ ├── permissions.ts # 摄像头/麦克风权限自动授予
|
||||
│ └── tray.ts # 系统托盘(可选)
|
||||
├── preload/
|
||||
│ └── index.ts # preload 脚本,通过 contextBridge 暴露配置
|
||||
└── assets/
|
||||
├── icon.png # 应用图标
|
||||
└── icon.icns # macOS 图标
|
||||
```
|
||||
|
||||
### main/index.ts — 核心职责
|
||||
|
||||
- 创建 `BrowserWindow`,配置 `webPreferences`:
|
||||
- `preload`: 指向 preload 脚本路径
|
||||
- `contextIsolation: true`
|
||||
- `nodeIntegration: false`(安全最佳实践)
|
||||
- 加载前端内容(两种模式,见下文"加载策略")
|
||||
- 注册 `session.defaultSession.setPermissionRequestHandler`,自动授予 `media` 权限
|
||||
- 处理应用生命周期(ready / window-all-closed / activate)
|
||||
|
||||
### main/permissions.ts — 权限自动授予
|
||||
|
||||
Electron 默认不会像浏览器那样弹出权限请求弹窗。需要在 Main Process 中显式处理:
|
||||
|
||||
```typescript
|
||||
// 伪代码示意
|
||||
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
// 自动授予摄像头、麦克风、通知等权限
|
||||
const allowedPermissions = ['media', 'mediaKeySystem', 'notifications'];
|
||||
callback(allowedPermissions.includes(permission));
|
||||
});
|
||||
|
||||
session.defaultSession.setPermissionCheckHandler((webContents, permission) => {
|
||||
return true; // 始终返回已授权
|
||||
});
|
||||
```
|
||||
|
||||
### preload/index.ts — 安全桥接
|
||||
|
||||
通过 `contextBridge` 向渲染进程暴露必要的原生能力:
|
||||
|
||||
```typescript
|
||||
// 伪代码示意
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 后端地址配置
|
||||
getBackendUrl: () => 'ws://localhost:8080/ws', // 或从配置文件读取
|
||||
// 应用版本
|
||||
getAppVersion: () => app.getVersion(),
|
||||
// 平台信息
|
||||
platform: process.platform,
|
||||
});
|
||||
```
|
||||
|
||||
## 加载策略
|
||||
|
||||
两种模式各有利弊,推荐**开发阶段用 dev server 模式,生产环境用本地文件模式**:
|
||||
|
||||
### 模式 A:Dev Server 模式(开发调试用)
|
||||
|
||||
```typescript
|
||||
// main/index.ts
|
||||
win.loadURL('http://localhost:5173'); // 连接 Vite 开发服务器
|
||||
```
|
||||
|
||||
优点:热更新、开发体验好,和现有前端开发流程完全一致。
|
||||
缺点:需要先启动 `npm run dev`。
|
||||
|
||||
### 模式 B:本地文件模式(生产环境用)
|
||||
|
||||
```typescript
|
||||
// main/index.ts
|
||||
win.loadFile(path.join(__dirname, '../frontend-dist/index.html'));
|
||||
```
|
||||
|
||||
前端执行 `npm run build` 后,将 `frontend/dist/` 目录的产物复制到 Electron 项目中,Electron 通过 `file://` 协议加载。
|
||||
|
||||
优点:不依赖任何服务器,双击应用即可运行。
|
||||
缺点:需要构建步骤。
|
||||
|
||||
### WebSocket 连接适配
|
||||
|
||||
当前 `websocket.ts` 的 URL 推导逻辑:
|
||||
|
||||
```typescript
|
||||
const WS_URL =
|
||||
import.meta.env.VITE_WS_URL ||
|
||||
`${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws`;
|
||||
```
|
||||
|
||||
Electron 下 `window.location.host` 为空字符串,会导致推导出 `ws:///ws` 这样的无效地址。解决方案有两个:
|
||||
|
||||
1. **推荐:通过 preload 注入**。在 `window.electronAPI.getBackendUrl()` 中获取,修改 `websocket.ts` 增加一行 Electron 检测:
|
||||
```typescript
|
||||
const WS_URL =
|
||||
(window as any).electronAPI?.getBackendUrl?.() ||
|
||||
import.meta.env.VITE_WS_URL ||
|
||||
`${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws`;
|
||||
```
|
||||
|
||||
2. **备选:.env 文件**。在 Electron 项目中设置 `VITE_WS_URL=ws://localhost:8080/ws`,构建时 Vite 会将其内联到代码中。
|
||||
|
||||
## Go 后端的运行方式
|
||||
|
||||
有两种选择,当前阶段推荐方案 1:
|
||||
|
||||
### 方案 1:后端独立运行(推荐,当前阶段)
|
||||
|
||||
用户需要自己先在本地或服务器上启动 Go 后端,Electron 应用连接到指定地址。配置方式:
|
||||
|
||||
- 第一次启动时弹出设置窗口,让用户输入后端地址(如 `ws://localhost:8080/ws` 或 `ws://8.161.227.145:8080/ws`)
|
||||
- 保存到本地配置文件(`electron-store` 或简单 JSON 文件)
|
||||
- 后续启动自动读取
|
||||
|
||||
这种方案改动最小,Go 后端完全不需要动。
|
||||
|
||||
### 方案 2:Electron 内嵌后端(未来优化)
|
||||
|
||||
将 Go 编译为二进制文件,打包进 Electron 应用,Main Process 启动时作为子进程拉起。用户体验更好(双击即用),但增加打包复杂度。当前阶段不建议。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
| 阶段 | 内容 | 预估工作量 |
|
||||
|------|------|----------|
|
||||
| 1. 初始化 | 在项目根目录新建 `desktop/` 目录,初始化 Electron + TypeScript 项目 | 0.5h |
|
||||
| 2. Main Process | 编写窗口创建、权限授予、应用生命周期代码 | 1-2h |
|
||||
| 3. Preload 脚本 | 编写 contextBridge,暴露后端地址等配置 | 0.5h |
|
||||
| 4. 前端适配 | 修改 `websocket.ts` 增加 Electron 环境检测(仅 1 个文件) | 0.5h |
|
||||
| 5. 开发联调 | Dev Server 模式联调,验证摄像头/麦克风/WebSocket 正常工作 | 1h |
|
||||
| 6. 本地文件模式 | 配置生产构建流程,build → loadFile 验证 | 1h |
|
||||
| 7. 打包分发(可选) | electron-builder 配置 macOS/Windows 安装包 | 1-2h |
|
||||
|
||||
总计约 5-7 小时。
|
||||
|
||||
## 风险点与应对
|
||||
|
||||
| 风险 | 影响 | 应对策略 |
|
||||
|------|------|---------|
|
||||
| ONNX Runtime Web 在 Electron 中的兼容性 | VAD 可能不工作 | Electron 使用完整 Chromium,WebAssembly + AudioWorklet 均支持,风险低。如有问题可在 Main Process 设置 `app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer')` |
|
||||
| `@ricky0123/vad-web` 的 AudioWorklet 加载路径 | `file://` 下静态资源路径可能不对 | 确保 `public/` 下的 VAD 模型文件在构建后正确复制。必要时通过 preload 动态注入 worklet 脚本路径 |
|
||||
| Electron 安全策略限制 media 权限 | 摄像头/麦克风仍不可用 | 通过 `session.setPermissionRequestHandler` 显式授予,这是成熟方案 |
|
||||
| 远程后端网络不通 | WebSocket 连不上 | Electron 不受 CORS 限制(可在 `webPreferences` 中关闭 `webSecurity` 或设置 CORS headers),但网络连通性需要用户自行保证 |
|
||||
|
||||
## 不在本次范围内
|
||||
|
||||
- Go 后端内嵌打包(方案 2)—— 后续优化
|
||||
- 自动更新机制(electron-updater)—— 后续优化
|
||||
- 代码签名与公证(macOS notarize)—— 正式发布时需要
|
||||
- 多语言安装包定制 —— 当前使用系统语言
|
||||
Reference in New Issue
Block a user