桌面客户端架构.md 27 KB

桌面客户端架构

本文引用的文件

  • ai-electron/package.json
  • ai-electron/electron/main.ts
  • ai-electron/electron/preload/index.ts
  • ai-electron/electron/preload/bridge.ts
  • ai-electron/electron/preload/lifecycle.ts
  • ai-electron/electron/config/windowOptions.ts
  • ai-electron/electron/config/config.default.ts
  • ai-electron/electron/controller/windowCtl.ts
  • ai-electron/frontend/src/main.ts
  • ai-electron/frontend/src/App.vue
  • ai-electron/frontend/vite.config.ts
  • ai-electron/frontend/src/core/settings/serverSetting.ts
  • ai-electron/frontend/src/core/utils/map/offlineMap.ts
  • ai-electron/frontend/src/ai/api/sse.ts
  • ai-electron/frontend/src/ai/store/chatStream.ts
  • ai-electron/frontend/src/ai/api/chatApi.ts
  • ai-electron/cmd/builder.json

目录

  1. 简介
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 关键组件详解
  6. 依赖关系分析
  7. 性能与体验特性
  8. 常见问题排查
  9. 从启动到发起 Agent 请求的最小路径
  10. 结论

简介

本项目是一个基于 Electron + ee-core 的桌面客户端,前端工程使用 Vite + Vue 3 + Ant Design Vue + TypeScript。主进程通过 ee-core.ElectronEgg 管理应用生命周期、窗口、托盘与安全服务;渲染进程负责主题、国际化、水印、授权、地图和 SSE Agent 对话。后端 ai-server 独立部署,客户端在运行时动态绑定其地址,并可选择加载本地 china.pmtiles 离线地图。

项目结构

桌面端代码主要位于 ai-electron 下,可划分为三层:

层级 目录 职责
主进程层 electron/main.ts、electron/preload/*、electron/config/*、electron/controller/*、electron/service/* 应用生命周期、窗口控制、系统托盘、安全策略、IPC 控制器
渲染进程层 frontend/src/* Vue 3 应用、路由、状态、UI、SSE Agent、离线地图、设置
构建与打包层 package.json、cmd/builder.json、frontend/vite.config.ts 开发脚本、Vite 配置、Electron Builder 产物
graph TB
    subgraph "主进程"
        M["main.ts<br/>注册生命周期与 preload"]
        L["preload/lifecycle.ts<br/>ready / windowReady / beforeClose"]
        P["preload/index.ts<br/>创建托盘与安全服务"]
        C["controller/windowCtl.ts<br/>跨窗口 IPC"]
        W["config/windowOptions.ts<br/>webPreferences"]
        D["config/config.default.ts<br/>默认窗口与静态资源路径"]
    end

    subgraph "渲染进程"
        FMain["frontend/src/main.ts<br/>启动引导、健康检查、路由守卫"]
        AppVue["App.vue<br/>主题/国际化/水印/授权单元"]
        ServerCfg["serverSetting.ts<br/>动态绑定 ai-server 地址"]
        OfflineMap["offlineMap.ts<br/>china.pmtiles 离线地图"]
        ChatStore["chatStream.ts<br/>会话与流式状态机"]
        Sse["sse.ts<br/>SSE 解析与 fetch 流读取"]
        ChatApi["chatApi.ts<br/>非流式聊天接口"]
    end

    M --> L
    M --> P
    M --> W
    M --> D
    M --> C

    FMain --> AppVue
    FMain --> ServerCfg
    FMain --> ChatStore
    ChatStore --> ChatApi
    ChatStore --> Sse
    AppVue --> ServerCfg
    AppVue --> OfflineMap

图示来源

  • ai-electron/electron/main.ts:1-19
  • ai-electron/electron/preload/lifecycle.ts:1-73
  • ai-electron/electron/preload/index.ts:1-16
  • ai-electron/electron/config/windowOptions.ts:1-41
  • ai-electron/electron/config/config.default.ts:1-50
  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/App.vue:1-85
  • ai-electron/frontend/src/core/settings/serverSetting.ts:1-200
  • ai-electron/frontend/src/core/utils/map/offlineMap.ts:1-200
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/frontend/src/ai/store/chatStream.ts:1-200
  • ai-electron/frontend/src/ai/api/chatApi.ts:1-80

章节来源

  • ai-electron/package.json:1-42
  • ai-electron/electron/main.ts:1-19
  • ai-electron/frontend/vite.config.ts:1-40

核心组件

本节聚焦主进程与渲染进程的职责边界。

主进程入口与生命周期

main.ts 是 Electron 入口,使用 ee-core.ElectronEgg 创建应用实例,注册 ready、electron-app-ready、window-ready、before-close 生命周期钩子,并注册 preload。它不直接管理业务逻辑,而是把能力交给生命周期类和预加载模块。

关键点:

  • 单例应用:new ElectronEgg() 后调用 app.run()。
  • 生命周期由 Lifecycle 类实现,集中处理窗口居中、显示策略、二次启动激活等。
  • preload 模块在窗口创建前执行,负责创建系统托盘和安全服务。

章节来源

  • ai-electron/electron/main.ts:1-19
  • ai-electron/electron/preload/lifecycle.ts:1-73
  • ai-electron/electron/preload/index.ts:1-16

预加载桥与 IPC 暴露

preload/index.ts 是框架级预加载入口,用于初始化托盘与安全服务;preload/bridge.ts 则在启用上下文隔离时通过 contextBridge.exposeInMainWorld('electron', ...) 向渲染进程暴露 ipcRenderer。当前窗口配置关闭了上下文隔离,因此渲染进程可直接访问 Node/Electron API,但 bridge 仍保留为扩展点。

安全模型要点:

  • 默认 contextIsolation: false、nodeIntegration: true,便于现有渲染代码直接使用 Electron API。
  • 若未来开启上下文隔离,应通过 contextBridge 暴露最小化 API,而不是直接把全部 Electron API 抛给渲染进程。
  • 跨窗口状态同步通过主进程 WindowCtl 统一转发,避免渲染进程直接枚举所有窗口。

章节来源

  • ai-electron/electron/preload/bridge.ts:1-10
  • ai-electron/electron/config/windowOptions.ts:10-20
  • ai-electron/electron/controller/windowCtl.ts:1-123

渲染进程启动引导

frontend/src/main.ts 是 Vue 应用的 bootstrap 入口,承担以下职责:

  • 初始化全局组件、国际化、路由、Pinia store、错误处理。
  • 判断是否缺少后端服务地址;若缺失则进入授权/服务地址页。
  • 对未绑定地址的场景执行最多 10 次、每次约 3 秒的后端健康检查。
  • 健康检查通过后执行授权校验与登录门禁。
  • 智能工作台窗口与普通主窗口的行为分支。

    flowchart TD
    Start["渲染进程启动"] --> Init["初始化 Store、路由、国际化、全局组件"]
    Init --> CheckServer{"是否已绑定 ai-server 地址?"}
    CheckServer -->|否| GoAuth["跳转到授权/服务地址页"]
    CheckServer -->|是| Probe["健康检查:最多 10 次 × 3 秒"]
    Probe --> Ready{"后端就绪?"}
    Ready -->|否| ShowReconnect["显示重连与服务地址操作"]
    Ready -->|是| AuthCheck["校验授权与登录态"]
    AuthCheck --> Route["计算最终路由并挂载 App"]
    

图示来源

  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/main.ts:201-399

章节来源

  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/main.ts:201-399

App.vue:主题、国际化、水印与授权单元

App.vue 是根组件,负责:

  • 通过 Ant Design Vue 的 StyleProvider 和 ConfigProvider 注入主题与语言。
  • 使用 useLocale 获取中文语言包。
  • 使用 useRootSetting 获取深色模式与主题色。
  • 使用 useWatermark 根据授权单元设置页面水印。
  • 首次挂载时尝试调用授权信息接口,将授权单元写入用户缓存并驱动水印。

注意:如果尚未绑定服务端地址,或 /sys/info 失败,会清空水印,避免展示过期或错误的授权单元。

章节来源

  • ai-electron/frontend/src/App.vue:1-85

架构总览

下图展示从 Electron 主进程到前端 AI Agent 请求的整体调用链。

sequenceDiagram
    participant EE as "ElectronEgg"
    participant Main as "main.ts"
    participant Life as "lifecycle.ts"
    participant Pre as "preload/index.ts"
    participant Win as "BrowserWindow"
    participant Vue as "frontend main.ts"
    participant Api as "serverSetting.ts"
    participant SSE as "sse.ts"
    participant Stream as "chatStream.ts"
    participant Server as "ai-server"

    EE->>Main: 创建应用
    Main->>Life: 注册 ready / electron-app-ready / window-ready / before-close
    Main->>Pre: 注册 preload
    Main->>EE: run()
    EE->>Win: 创建主窗口
    Pre->>Pre: 创建托盘与安全服务
    Life->>Win: 设置窗口大小与显示时机
    Vue->>Api: getApiOrigin()
    Vue->>Vue: waitForBackendReady()
    Vue->>Server: GET /js/a/sys/health
    Server-->>Vue: 健康响应
    Vue->>Vue: resolveStartupRoute()
    Vue->>Stream: 初始化会话与消息状态
    Stream->>SSE: streamChat(url, params, handlers)
    SSE->>Server: POST 事件流请求
    Server-->>SSE: text/event-stream
    SSE-->>Stream: intent / thinking / tool_call / token / done / error

图示来源

  • ai-electron/electron/main.ts:1-19
  • ai-electron/electron/preload/lifecycle.ts:1-73
  • ai-electron/electron/preload/index.ts:1-16
  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/core/settings/serverSetting.ts:1-200
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/frontend/src/ai/store/chatStream.ts:1-200

关键组件详解

主进程生命周期与窗口行为

Lifecycle 类封装了四个关键阶段:

  • ready:ee-core 应用已加载。
  • electronAppReady:处理双击图标时恢复已打开窗口,优先拉回智能工作台窗口。
  • windowReady:主窗口按屏幕工作区尺寸按比例居中,并根据配置延迟显示。
  • beforeClose:应用关闭前的清理钩子。

窗口共享配置集中在 sharedWebPreferences,包含 contextIsolation: false 和 nodeIntegration: true,确保主窗口与智能工作台窗口行为一致。

章节来源

  • ai-electron/electron/preload/lifecycle.ts:1-73
  • ai-electron/electron/config/windowOptions.ts:10-41
  • ai-electron/electron/config/config.default.ts:10-35

跨窗口 IPC 控制器

windowCtl.ts 提供统一的窗口管理 IPC:

  • openSmartWindow:打开或聚焦智能工作台窗口。
  • closeSmartWindow:关闭智能工作台窗口。
  • isSmartWindowOpen:查询智能窗口是否打开。
  • focusMainWindow:登录失效等场景下把主窗口拉到前台。
  • getWindowRole:让渲染进程知道自己属于主窗口还是智能工作台。
  • syncState:向除发送者外的所有窗口广播语义事件,通道名为 app:state。

该控制器返回 { success, data|message },失败时记录日志但不抛出到 Electron 层,便于渲染进程统一处理。

章节来源

  • ai-electron/electron/controller/windowCtl.ts:1-123

动态绑定 ai-server 地址

serverSetting.ts 是客户端与后端地址绑定的核心:

  • 地址保存在原始 localStorage 键 SERVER_BASE_URL__ 中,不参与 AES 快照,客户端升级后复用。
  • 最近成功绑定的地址保存在 SERVER_BASE_URL_HISTORY__,最多保留 5 条。
  • normalizeServerOrigin 只接受协议、主机和端口,拒绝携带 /js、/a 等路径。
  • getApiOrigin 优先取运行时绑定地址,其次取构建期环境变量。
  • buildHealthCheckUrl 构造 /js/a/sys/health 健康检查地址。
  • testServerConnection 使用原生 fetch 测试连接,超时、跨域、HTTP 非 200 都会给出明确错误信息。
  • reloadToAppRoot 切换地址后回到应用根路径并重新加载。

    flowchart TD
    Input["用户输入服务地址"] --> Normalize["normalizeServerOrigin 规范化"]
    Normalize --> Valid{"格式合法?"}
    Valid -->|否| Error["返回错误信息"]
    Valid -->|是| Save["保存到 SERVER_BASE_URL__"]
    Save --> Remember["加入最近使用历史"]
    Remember --> Health["testServerConnection 健康检查"]
    Health --> Ok{"连接成功?"}
    Ok -->|否| ShowError["提示网络、端口、跨域或服务问题"]
    Ok -->|是| Reload["reloadToAppRoot 回到根路径并重载"]
    

图示来源

  • ai-electron/frontend/src/core/settings/serverSetting.ts:1-200
  • ai-electron/frontend/src/core/settings/serverSetting.ts:201-248

章节来源

  • ai-electron/frontend/src/core/settings/serverSetting.ts:1-248

离线地图 china.pmtiles

offlineMap.ts 提供两种 PMTiles 数据源:

  • ElectronPmtilesSource:通过主进程 IPC 读取本机绝对路径下的 china.pmtiles,按需分块读取二进制范围。
  • HttpPmtilesSource:对内置静态资源 resource/map/china.pmtiles 使用 HTTP Range 请求。

离线地图目录保存在 OFFLINE_MAP_DIR__,支持 Windows 盘符、UNC 路径、POSIX 绝对路径和本地协议。样式 JSON 也可从本机路径读取,否则回退到内置样式。

classDiagram
    class ElectronPmtilesSource {
        +string filePath
        +getKey() string
        +getBytes(offset, length) ArrayBuffer
    }

    class HttpPmtilesSource {
        +string url
        +getKey() string
        +getBytes(offset, length) ArrayBuffer
    }

    class OfflineMap {
        +getOfflineMapDir() string
        +getOfflineMapPmtilesPath() string
        +resolveOfflinePmtilesSources() Array
        +readOfflineMapStyleText() Promise~string~
        +isValidOfflineMapPath(value) boolean
        +setOfflineMapDir(path) void
    }

    OfflineMap --> ElectronPmtilesSource : "本地路径时使用"
    OfflineMap --> HttpPmtilesSource : "内置静态资源时使用"

图示来源

  • ai-electron/frontend/src/core/utils/map/offlineMap.ts:1-200

章节来源

  • ai-electron/frontend/src/core/utils/map/offlineMap.ts:1-200

SSE Agent 对话流程

Agent 对话分为两层:

  • chatApi.ts:封装非流式聊天接口,如会话列表、消息列表、发送消息等。
  • sse.ts:通用 SSE 解析器与 streamSse 流式请求实现,支持 intent、thinking、tool_call、tool_input、tool_result、token、done、error 等事件。
  • chatStream.ts:AI 对话流式状态机,维护会话、消息、工具步骤、思考过程、意图与建议项,并做增量解析节流。

    sequenceDiagram
    participant UI as "AI 对话界面"
    participant Store as "chatStream.ts"
    participant Api as "chatApi.ts"
    participant SSE as "sse.ts"
    participant Server as "ai-server"
    
    UI->>Store: 发送消息
    Store->>Api: createSession / listMessages / sendMessage
    Store->>SSE: streamChat(url, params, handlers)
    SSE->>Server: POST 带 x-token 的事件流请求
    Server-->>SSE: intent / thinking / tool_call / token / done
    SSE-->>Store: 分发事件
    Store->>Store: 更新消息、工具步骤、思考文本、建议项
    Store-->>UI: 增量渲染
    

图示来源

  • ai-electron/frontend/src/ai/api/chatApi.ts:1-80
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/frontend/src/ai/store/chatStream.ts:1-200

章节来源

  • ai-electron/frontend/src/ai/api/chatApi.ts:1-80
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/frontend/src/ai/store/chatStream.ts:1-200

前端工程结构与 Vite 配置

前端工程位于 ai-electron/frontend,采用:

  • Vite 作为构建工具。
  • Vue 3 作为视图框架。
  • Ant Design Vue 作为 UI 组件库。
  • TypeScript 作为类型系统。
  • UnoCSS 与自定义 less 作为样式方案。

vite.config.ts 中:

  • 通过 loadEnv 加载环境配置。
  • 设置 base、define、plugins、server、esbuild、build、css、resolve.alias。
  • @ 指向 src,@build 指向 build。

章节来源

  • ai-electron/frontend/vite.config.ts:1-40

依赖关系分析

graph LR
    Package["package.json<br/>ee-core / electron-updater"]
    Main["electron/main.ts"]
    Lifecycle["electron/preload/lifecycle.ts"]
    Preload["electron/preload/index.ts"]
    Bridge["electron/preload/bridge.ts"]
    WindowOpts["electron/config/windowOptions.ts"]
    DefaultCfg["electron/config/config.default.ts"]
    WindowCtl["electron/controller/windowCtl.ts"]
    FrontMain["frontend/src/main.ts"]
    AppVue["frontend/src/App.vue"]
    ServerCfg["frontend/src/core/settings/serverSetting.ts"]
    OfflineMap["frontend/src/core/utils/map/offlineMap.ts"]
    SSE["frontend/src/ai/api/sse.ts"]
    ChatStore["frontend/src/ai/store/chatStream.ts"]
    ChatApi["frontend/src/ai/api/chatApi.ts"]

    Package --> Main
    Main --> Lifecycle
    Main --> Preload
    Main --> Bridge
    DefaultCfg --> WindowOpts
    Main --> WindowCtl
    FrontMain --> AppVue
    FrontMain --> ServerCfg
    AppVue --> ServerCfg
    AppVue --> OfflineMap
    ChatStore --> ChatApi
    ChatStore --> SSE

图示来源

  • ai-electron/package.json:1-42
  • ai-electron/electron/main.ts:1-19
  • ai-electron/electron/preload/lifecycle.ts:1-73
  • ai-electron/electron/preload/index.ts:1-16
  • ai-electron/electron/preload/bridge.ts:1-10
  • ai-electron/electron/config/windowOptions.ts:1-41
  • ai-electron/electron/config/config.default.ts:1-50
  • ai-electron/electron/controller/windowCtl.ts:1-123
  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/App.vue:1-85
  • ai-electron/frontend/src/core/settings/serverSetting.ts:1-200
  • ai-electron/frontend/src/core/utils/map/offlineMap.ts:1-200
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/frontend/src/ai/store/chatStream.ts:1-200
  • ai-electron/frontend/src/ai/api/chatApi.ts:1-80

章节来源

  • ai-electron/package.json:1-42

性能与体验特性

  • 启动健康检查:最多 10 次、每次约 3 秒探测后端,loading 页实时显示重试次数,避免长时间阻塞且给用户明确反馈。
  • 授权前置:授权校验优先于登录态检查,授权过期时直接跳转授权页并携带过期原因。
  • 登录门禁:无 token 时跳转授权/服务地址页,同时保留来源路由以便登录后回跳。
  • 流式输出:SSE 事件按真实顺序下发,前端维护时序链,避免长回答重复扫描导致的 O(n²) 卡顿。
  • 离线地图:本机路径走 IPC 分块读取,内置静态资源走 HTTP Range,减少后端压力。
  • 地址持久化:服务地址存储在原始 localStorage 键中,客户端升级后无需重复绑定。

[本节为通用优化说明,不直接分析具体代码片段]

常见问题排查

现象 可能原因 排查位置
启动后一直显示“后端服务连接失败” ai-server 未启动、地址绑定错误、跨域被拦截、防火墙阻止 frontend/src/main.ts 的健康检查逻辑
授权页显示服务不可达 绑定地址后首次健康检查失败 frontend/src/core/settings/serverSetting.ts 的 testServerConnection
地图不显示或瓦片空白 本机离线地图目录无效、china.pmtiles 不存在、Range 请求失败 frontend/src/core/utils/map/offlineMap.ts
Agent 没有流式输出 后端未返回 text/event-stream、token 失效、请求被代理或网关拦截 frontend/src/ai/api/sse.ts
登录后跳转异常 入口路由处理、hash/history 模式差异、来源路由丢失 frontend/src/main.ts 的路由解析与登录门禁
智能工作台窗口行为异常 窗口角色识别错误或 IPC 事件未正确转发 electron/controller/windowCtl.ts
打包后找不到静态资源 Vite base、Electron 静态资源路径、asar 打包影响 frontend/vite.config.ts、electron/config/config.default.ts

章节来源

  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/core/settings/serverSetting.ts:1-248
  • ai-electron/frontend/src/core/utils/map/offlineMap.ts:1-200
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/electron/controller/windowCtl.ts:1-123
  • ai-electron/frontend/vite.config.ts:1-40
  • ai-electron/electron/config/config.default.ts:1-50

从启动到发起 Agent 请求的最小路径

以下路径描述最小可运行流程,不包含完整 UI 交互细节:

  1. Electron 主进程启动:

    • 加载 main.ts。
    • 注册生命周期与 preload。
    • 调用 app.run() 创建主窗口。
  2. 预加载模块初始化:

    • 创建托盘与安全服务。
    • 可选通过 bridge.ts 暴露 ipcRenderer。
  3. 渲染进程引导:

    • 初始化 Vue、Store、路由、国际化。
    • 检测是否缺少 ai-server 地址;缺失则进入授权/服务地址页。
    • 若地址存在,执行最多 10 次健康检查。
  4. 授权与登录:

    • 校验授权,过期则跳转授权页。
    • 无 token 则跳转授权/登录入口。
  5. 发起 Agent 请求:

    • 用户在 AI 对话界面发送消息。
    • chatStream.ts 调用 chatApi.ts 创建或查询会话。
    • sse.ts 使用 fetch 发起事件流请求,携带 x-token。
    • 解析 intent、thinking、tool_call、token、done、error 等事件。
    • 更新流式消息并渲染到界面。

      flowchart TD
      A["Electron 主进程启动"] --> B["注册生命周期与 preload"]
      B --> C["创建主窗口"]
      C --> D["渲染进程 main.ts 初始化"]
      D --> E["检查 ai-server 地址"]
      E --> F["健康检查后端"]
      F --> G["授权与登录校验"]
      G --> H["AI 对话界面发送消息"]
      H --> I["chatStream.ts 调用 chatApi.ts"]
      I --> J["sse.ts 发起 SSE 流式请求"]
      J --> K["解析事件并更新消息"]
      

图示来源

  • ai-electron/electron/main.ts:1-19
  • ai-electron/electron/preload/index.ts:1-16
  • ai-electron/frontend/src/main.ts:1-200
  • ai-electron/frontend/src/ai/api/chatApi.ts:1-80
  • ai-electron/frontend/src/ai/api/sse.ts:1-200
  • ai-electron/frontend/src/ai/store/chatStream.ts:1-200

结论

本桌面客户端以 ee-core.ElectronEgg 为主进程骨架,通过生命周期钩子和预加载模块组织启动流程;渲染进程以 Vue 3 为核心,完成主题、国际化、授权、地图与 SSE Agent 对话。ai-server 地址在运行时绑定,离线地图通过 PMTiles 和本机路径读取;跨窗口状态通过主进程统一转发。整体架构清晰分离主进程与渲染进程职责,适合继续扩展安全加固、自动更新与更多桌面能力。

[本节为总结性内容,不直接分析具体文件]