跳转至

第 9 章:WebView UI、ServiceHub 与状态 —— React 只是最上层

9.1 RootLayout 是应用组装点

routes/__root.tsx 把 ServiceHub、Theme、Interface、Toaster、Translation、Extension、Data、GlobalEventHandler、Sidebar、Dialog 和 updater 组装成根布局。它同时区分普通聊天路由与 logs/system monitor/local API server 的专用 layout。

ServiceHubProvider
  ├─ ThemeProvider / InterfaceProvider
  ├─ TranslationProvider
  │   ├─ ExtensionProvider
  │   │   ├─ DataProvider
  │   │   └─ GlobalEventHandler
  │   └─ ToolApproval / Attachment / Error / OOM dialogs
  └─ AppLayout or LogsLayout

这不是单纯的 Provider 叠加;它编码了初始化顺序。DataProvider 需要 ServiceHub,ExtensionProvider 需要 Core,根布局中的全局 dialog 需要事件和状态已经存在。

9.2 Zustand store 的分工

Store/Hook 负责什么
useThreads thread map、当前 thread、标题、favorite、搜索、删除清理
useMessages 每个 thread 的 ThreadMessage[],乐观写入和持久化
useChatSessions AI SDK Chat/transport、status、session title
useAppState 模型 loading、MCP/RAG tool names、live token stats、thread transient state
useModelProvider provider/model 选择和配置
useInterfaceSettings UI 设置、主题、窗口、快捷键

一个重要边界是:持久领域实体和生成中的临时状态分开useMessages 负责可恢复消息;useChatSessions 负责当前 AI SDK 会话;useAppState 负责 UI 需要的瞬时指标。

9.3 ServiceHub 让 store 也能访问后端

React 组件可以用 useServiceHub(),但 Zustand store 用不了 React hook,所以通过 getServiceHub() 读取全局服务。ServiceHubProvider mount 时执行 initializeServiceHub() 并写入 store。

// 组件内
const hub = useServiceHub()
await hub.threads().listThreads()

// Zustand action 内
getServiceHub().messages().modifyMessage(message)

这种设计避免了把 invoke 散落到每个 store,同时让测试可以替换 ServiceHub。风险是初始化是运行时前置条件,任何在 provider 之前创建并执行的 store action 都可能失败。

9.4 Chat 页面是“状态机视图”

threads/$threadId.tsx 同时观察:

  • 当前 thread 是否存在。
  • message 是否正在 streaming/submitted。
  • 模型是否正在加载。
  • 是否有工具审批等待。
  • 是否有 context overflow、OOM 或 backend error。
  • 是否需要继续一条被中断的 assistant 输出。

因此 UI 不是 messages.map(render),而是一个状态机的投影:同一条 assistant message 可能从空壳、streaming、tool call、tool result、complete、error、stopped 依次变化。

9.5 数据流和事件流

flowchart LR
  Input[ChatInput] --> UIStore[Zustand session state]
  UIStore --> Transport[CustomChatTransport]
  Transport --> Backend[AI SDK / Tauri / local server]
  Backend --> Chunks[stream chunks]
  Chunks --> UIStore
  Backend --> Events[Tauri events]
  Events --> Global[GlobalEventHandler]
  UIStore --> Persist[ServiceHub messages / threads]
  Persist --> Files[JSONL / SQLite]

事件流用于跨组件的全局状态:下载进度、MCP 连接变化、模型 busy、OOM、更新器;请求流用于一条聊天的局部状态。把两者分开,是避免所有东西都塞进全局 event bus 的关键。

9.6 i18n 和平台差异也是架构

main.tsx 动态加载 i18n,locales 按语言拆分 chat、common、settings、providers、tools 等 namespace。根布局根据 IS_WINDOWS/IS_LINUX/IS_TAURI 渲染窗口拖拽和 resize grips;移动端在启动时调整 viewport 和 safe-area。

这些代码说明 Jan 的平台适配不是只在 Rust:窗口交互、键盘快捷键、safe-area、文件拖拽、默认浏览器行为都由 WebView 负责。

9.7 UI 架构的代价

  • 路由页面、transport、hook、store 互相了解,阅读成本高。
  • 同一业务状态可能同时存在 store、AI SDK Chat、ThreadMessage 和 Rust backend state 中。
  • 乐观更新提高速度,但需要处理持久化失败与应用重启后的 reconcile。
  • 通过 ServiceHub 隔离平台后,能力发现和初始化更间接;新贡献者要先找到服务实现再读组件。