feat:项目打包成Electron

This commit is contained in:
2026-06-14 18:10:58 +08:00
parent 7a0443ebcc
commit 8970b63800
20 changed files with 3895 additions and 11 deletions

View 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 模式,生产环境用本地文件模式**
### 模式 ADev 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 后端完全不需要动。
### 方案 2Electron 内嵌后端(未来优化)
将 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 使用完整 ChromiumWebAssembly + 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—— 正式发布时需要
- 多语言安装包定制 —— 当前使用系统语言