本章先建立地图,再进入实现。核心问题是:Chatbox 是桌面应用、Web 应用、移动应用,还是一个 AI Agent Harness?答案是:它用一套共享的领域模型,把这四种身份叠在一起。
Chatbox Community Edition 的公开定位是多模型桌面客户端,但源码显示它至少包含六条能力线:
| 能力线 | 解决的问题 | 主要代码区域 |
|---|---|---|
| 对话 | 创建会话、发送消息、流式渲染、继续/重新生成 | renderer/stores/session/、routes/session/ |
| 模型 | 用统一接口接入不同 API 协议和供应商 | shared/providers/、shared/models/ |
| Agent | 让模型调用搜索、文件、代码、MCP、Skills | renderer/packages/model-calls/、stores/session/tools-builder.ts |
| 数据 | 会话、设置、图片、blob、知识库持久化 | renderer/storage/、main/store-node.ts |
| 平台 | Desktop/Web/Mobile 共用业务逻辑 | renderer/platform/、src/main/、Capacitor |
| 交付 | 自动更新、安装包、签名、CI、错误上报 | main/app-updater.ts、.github/、Sentry |
因此,“发送一条消息”不是一个 React onClick 直接 fetch,而是一条跨层流水线:UI 产生领域动作,Session Store 组装上下文,Provider 把统一模型请求翻译成具体协议,AI SDK 把响应变成流,Stream Processor 把流重新组织为可持久化的 Message content parts。
flowchart TB
UI[React UI / TanStack Router]
Store[Zustand Session Store\n会话 CRUD / 生成编排]
Agent[Agent Harness\nContext + Tools + Sandbox]
Model[ModelInterface\n统一 chatStream]
Provider[Provider Registry\nOpenAI / Claude / Gemini / ...]
Stream[Stream Chunk Processor\ntext / reasoning / tool]
Persist[Platform + Storage\nIndexedDB / SQLite / Electron IPC]
Main[Electron Main\n窗口 / 文件 / 沙箱 / OAuth / 更新]
UI --> Store
Store --> Agent
Agent --> Model
Model --> Provider
Provider -->|HTTP / OAuth / API Key| Remote[模型服务]
Remote --> Provider --> Model
Model --> Stream --> Store --> UI
Store --> Persist
Persist --> Main
Main --> Persist
这张图有两个容易被忽略的事实:
- Renderer 才是 AI 业务编排中心。Main 负责操作系统能力和安全边界,不负责决定 prompt、模型、工具组合。
- Storage 不是单一数据库。配置、会话、图片和大 blob 使用不同存储介质,再由 Platform 接口统一给业务层使用。
src/main/main.ts 创建 BrowserWindow、注册 IPC、处理深链、快捷键、托盘、更新、OAuth 回调和本地文件能力。它还初始化知识库、Session Attachment RAG、MCP stdio transport、Skills 和 Sandbox 的 handlers。
Main 的原则是:能直接触碰系统的事情留在这里。例如读取用户真实文件、运行受控命令、创建本地预览服务、访问 app.getPath('userData'),都不应该从 Renderer 直接做。
src/preload/index.ts 使用 contextBridge.exposeInMainWorld('electronAPI', electronHandler) 暴露有限 API。它不把完整 ipcRenderer 放到网页里,而是把 invoke、监听器、窗口事件、更新事件、MCP 传输事件封装成类型化对象。
Renderer 负责页面、路由、状态、会话、模型调用、工具构建、流式更新、设置和跨平台业务逻辑。这里的 platform 实例把 Desktop、Web、Mobile 的差异收敛到统一接口。
src/shared/types/、models/、providers/、context/、services/ 里放的是可以被多个运行时复用的模型、消息、设置、Provider、上下文转换和错误定义。Shared 不是“杂物目录”,而是防止 Desktop、Web、Mobile 各自发明一套消息语义的边界。
Chatbox 的 Agent 能力不是一个独立服务器上的 workflow,而是运行在用户本机的客户端里。这样做带来三种产品能力:
- 文件和代码可以在本地工作目录上执行,数据不必先上传云端。
- 用户可以配置自己的 API Host、API Key、OAuth 和本地模型。
- UI 可以把工具调用、审批、暂停、继续和产物预览做成同一个交互闭环。
代价也很清楚:必须处理 Electron 安全、跨平台路径、原生模块、权限边界、崩溃恢复和长时间流式状态。这也是后续章节反复出现“边界”和“状态”的原因。
| 取舍 | 选择 | 结果 |
|---|---|---|
| 模型接口 | 自有 ModelInterface + AI SDK |
上层不依赖单一供应商;底层可以适配不同协议 |
| 工具来源 | 运行时按能力构建 ToolSet | 普通聊天不被 Agent 工具污染;弱模型可以被门控 |
| 会话结构 | Session + Thread + Message Fork | 支持历史线程与重新生成分支,而不把历史压扁成线性日志 |
| 平台差异 | Platform 接口 |
业务逻辑可共享;实现细节隔离在 desktop/web/mobile |
| 长结果 | blob 外置 + 消息短预览 | 避免单条消息和上下文被工具输出撑爆 |
| 高风险操作 | 持久化 pause + 显式审批 | 重启后仍能恢复,不把安全判断放在临时 Promise 里 |
不要从最厚的 React 组件开始。推荐按下面的链路走:
src/renderer/stores/session/orchestration.ts:一次生成的总编排。src/renderer/stores/session/agent-harness.ts:系统 prompt 和 ToolSet 的构建。src/shared/models/types.ts:统一模型协议。src/shared/providers/index.ts:Provider 到 Model 的实例化。src/renderer/stores/session/stream-chunk-processor.ts:流事件到消息部件的归一化。src/shared/types/session.ts:最终被保存和渲染的状态形状。
这一条路径就是 Chatbox 的“脊柱”。设置页、MCP、RAG、Sandbox 都是接在这条脊柱上的侧枝。
Chatbox 的核心不是某个组件,而是一个跨平台的客户端 Agent Runtime:Renderer 负责决策和状态,Provider 负责模型协议适配,Main 负责系统能力,Shared 负责领域契约,Storage 负责让长生命周期状态可恢复。下一章从启动过程和 IPC 开始,解释这些边界在程序启动时如何实际接上。