Skip to content

Latest commit

 

History

History
132 lines (91 loc) · 6.27 KB

File metadata and controls

132 lines (91 loc) · 6.27 KB

02 Electron 启动与平台抽象

一、启动顺序为什么重要

Electron 应用不是一个进程里加载一切。Chatbox 至少有三个运行上下文:

sequenceDiagram
  participant OS as 操作系统
  participant Main as Electron Main
  participant Preload as Preload
  participant Renderer as React Renderer
  OS->>Main: 启动 electron
  Main->>Main: pdfjs globals / legacy migration
  Main->>Main: 创建窗口、注册 IPC、初始化服务
  Main->>Preload: 为 BrowserWindow 注入 preload
  Preload->>Renderer: 暴露 window.electronAPI
  Renderer->>Renderer: migration / settings / stores
  Renderer->>Main: 通过 IPC 读取配置、文件和平台能力
  Renderer->>Renderer: RouterProvider 渲染 UI
Loading

src/main/main.ts 的最顶部,两个 import 有意放在 Electron app 之前:

  • pdfjs-globals:为 pdfjs-dist 可能在顶层访问的 DOMMatrixPath2DImageData 提供兼容对象。
  • legacy-database-migration:处理 Electron 数据目录行为变化,避免旧数据库被错误删除。

这不是风格问题,而是模块顶层副作用的时序约束:如果依赖在 import 时就读取全局对象,等 app.whenReady() 后再补救已经晚了。

二、Main 初始化了什么

main.ts 的职责可以按生命周期分成四组:

时机 工作 典型文件
模块加载 polyfill、迁移、错误捕获、容器/GPU flags main.tspdfjs-globals.ts
app ready 前 Linux runtime flags、协议注册、单实例准备 main.tsdeeplinks.ts
app ready 后 窗口、菜单、托盘、IPC handlers、服务初始化 main.tsmenu.ts
运行中 更新、快捷键、窗口事件、深链、退出清理 app-updater.tswindow_state.ts

Main 把知识库和 Session Attachment RAG 初始化做成异步 promise,并在启动早期触发但不阻塞所有窗口逻辑。这种方式兼顾了“尽早准备”和“某个索引服务失败不让整个 UI 永久卡住”。

三、Preload 的安全形状

Preload 中的 electronHandler 是 Renderer 看到的唯一桌面能力入口。它包括:

  • invoke:通用的 typed IPC 请求入口。
  • 文件路径、主题、窗口焦点/显示/最大化事件。
  • updater 状态监听。
  • MCP stdio transport 事件。
  • 内置 Skills 更新通知和导航通知。

createListener(channel) 把 Electron 的 (event, ...args) 监听器转成业务回调,并返回取消订阅函数。这个细节很重要:React effect 可以在卸载时解除监听,避免重复订阅和窗口生命周期泄漏。

为什么不是直接暴露 ipcRenderer

直接暴露 ipcRenderer 会让页面代码可以任意构造 channel 和参数,扩大信任边界。当前做法把:

  1. 可调用的 channel 集中在 preload。
  2. 输入和输出类型 集中在 shared/electron-types 与 Platform 实现。
  3. 权限判断 留在 Main handler,而不是信任 UI 自己声明“我有权限”。

四、Platform 是跨平台的“依赖注入器”

src/renderer/platform/index.ts 根据构建目标和运行环境选择:

if (process.env.NODE_ENV === 'test') return new TestPlatform()
if (CHATBOX_BUILD_TARGET === 'mobile_app') return new MobilePlatform()
if (typeof window !== 'undefined' && window.electronAPI) return new DesktopPlatform(window.electronAPI)
return new WebPlatform()

业务代码拿到的是 Platform 接口,而不是 if (isElectron) 到处散落。接口同时承载:

  • Storage:key/value、blob、枚举和清理。
  • 系统:主题、版本、设备名、窗口、自动启动、深链。
  • 文件:本地解析、读取、写入、搜索、目录选择。
  • Agent:Sandbox、产物导出、HTML 预览。
  • 领域服务:Knowledge Base、Session Attachment RAG、Image Generation Storage、Session Meta Storage。

这使得同一个 Session Store 可以在 Web 测试里注入 TestPlatform,也可以在移动端走 Capacitor、桌面端走 IPC。

五、IPC 调用的一条完整路径

sandboxRead 为例:

flowchart LR
  Tool[Renderer Tool\nread_file]
  Platform[DesktopPlatform.sandboxRead]
  Bridge[window.electronAPI.invoke]
  Handler[main/sandbox/ipc-handlers.ts]
  Manager[main/sandbox/manager.ts]
  FS[受限文件系统]
  Tool --> Platform --> Bridge --> Handler --> Manager --> FS
  FS --> Manager --> Handler --> Bridge --> Platform --> Tool
Loading

这里有三个可替换点:

  • Tool 不知道它运行在 Electron 还是 Web。
  • Platform 不知道具体 Sandbox 如何实现,只把参数映射成 IPC。
  • Manager 负责真实路径校验、工作目录、超时和输出截断。

六、错误处理的两层策略

Main 使用 process.on('uncaughtException') 兜底,记录 Sentry 上下文后 flush 并退出;Renderer 在 index.tsx 中通过 ErrorBoundary 保护 UI,并在初始化、迁移和生成链路上用领域错误上报。

这不是“所有错误都吞掉”:

  • 可以恢复的错误变成消息上的 errorCodeerrorerrorExtra
  • 不可恢复的进程错误仍然终止,避免应用处于半初始化状态。
  • 预期错误(配额、取消、用户拒绝)和异常错误使用不同的 Sentry 分类。

七、设计取舍

取舍 1:Main 不是业务层

把 prompt、ToolSet、Session 状态留在 Renderer,减少 IPC 往返和序列化,也让 Web 版本可以复用。代价是 Renderer 拥有很多业务复杂度,因此需要 Shared 类型和清晰的 Store 模块边界。

取舍 2:Platform 接口较宽

Platform 同时包含 UI、存储、Sandbox、RAG 等能力,看起来不像最小接口;但它把三套平台实现的差异集中到一个面上,避免业务层到处探测能力。通过可选方法和 scope 门控,Web/Mobile 可以拒绝桌面能力而不破坏类型契约。

取舍 3:配置与会话分离

Main 的 electron-store 管理配置和周期备份;会话走 Renderer Storage。这样配置容易备份和恢复,会话则可以容纳大量消息而不把 config.json 变成巨型文件。

小结

Chatbox 的运行时边界由“Main 可信系统层、Preload 窄桥、Renderer 产品层、Platform 跨平台门面、Shared 领域契约”共同构成。下一章把注意力转向最重要的领域对象:Session、Message、Thread 和 Fork。