第 2 章:Monorepo 与运行时拓扑 —— 四个世界如何接起来¶
2.1 仓库骨架¶
jan-main/
├── core/ # TypeScript 公共类型与扩展基类
├── web-app/ # React + Vite 产品界面
├── extensions/ # assistant / conversational / inference / RAG 等
├── src-tauri/
│ ├── src/core/ # Rust 应用服务与 Tauri commands
│ └── plugins/ # llama.cpp、hardware、MCP 辅助 plugin 等
├── mlx-server/ # Apple Silicon 的 Swift 服务器
├── docs/ # 产品文档站,不是运行时核心
└── scripts/ + Makefile # 构建、下载和开发入口
根 package.json 使用 Yarn workspaces,把 core、web-app、extensions/* 纳入同一个依赖图。Rust 则由 src-tauri/Cargo.toml 负责,插件以 path dependency 挂进主 crate。
2.2 启动顺序:从进程到界面¶
sequenceDiagram
participant OS as 操作系统
participant Main as src-tauri/src/main.rs
participant App as lib.rs::run
participant Tauri as Tauri Builder
participant Web as web-app/main.tsx
participant Root as RootLayout
OS->>Main: 启动 Jan
Main->>Main: 修正 PATH / 检查 lddtree helper
Main->>App: app_lib::run()
App->>Tauri: 注册 commands 与 plugins
Tauri->>Tauri: 创建 AppState、store、日志、MCP、更新器
Tauri->>Web: 加载 Vite bundle / devUrl
Web->>Web: 清理 pending webdata reset
Web->>Web: 动态 import routeTree 与 i18n
Web->>Root: RouterProvider
Root->>Root: ServiceHub / Theme / Extension / DataProvider
Rust 入口的关键点¶
main.rs 在调用 Tauri 之前先处理 PATH 和 lddtree helper。这不是装饰:Jan 会启动模型和工具子进程,如果 PATH 在入口处不稳定,后续子进程会出现“终端能运行、应用不能运行”的差异。
lib.rs::run 做三件大事:
- 用
invoke_commands_with_extras!汇总 filesystem、app、server、MCP、threads、downloads 和退出/主题命令。 - 注册 Tauri plugins:
tauri-plugin-llamacpp、vector DB、RAG、websearch、HTTP、store、updater 等。 - 用
AppState注入长生命周期资源,并在 setup 中初始化日志、配置迁移和 MCP 启动。
Web 入口的关键点¶
main.tsx 没有立刻 import 所有 store,而是先消费 Rust 写入的 take_pending_webdata_reset 标记,再动态加载 route tree 和 i18n。这解决了 Zustand persist 在模块 import 时同步 hydrate、导致 factory reset 清理太晚的竞态。
2.3 三条跨边界通道¶
| 通道 | 方向 | 适合传什么 |
|---|---|---|
invoke(command, args) |
WebView → Rust | 请求/响应式操作,文件、线程、服务器、密钥 |
Tauri emit/listen |
Rust ↔ WebView | 状态变化、下载进度、MCP 状态、退出提醒 |
| plugin guest API | WebView → Rust plugin | 模型加载、硬件、向量库、RAG |
Core 的 globalThis.core.api 和 Web 的 ServiceHub 对这三种通道做了二次封装。前端组件通常不直接写大量 invoke,而是调用 getServiceHub().threads()、.messages()、.models() 或 .mcp()。
2.4 为什么要有 ServiceHub¶
web-app/src/services/index.ts 的 PlatformServiceHub 把 app、assistants、core、dialog、events、hardware、MCP、messages、models、projects、providers、RAG、threads、updater、window 等服务组合起来。它把“平台差异”从 React 组件中拿走:
- Tauri 实现调用
@tauri-apps/api/core。 - Default 实现用于浏览器/测试,能力可以是 no-op 或显式抛错。
- 移动端/桌面端可以复用接口,但在后端选择不同实现。
这个抽象的核心不是依赖注入本身,而是让 store 也能拿到服务。useThreads、useMessages 是 Zustand store,不在 React render tree 内,但它们通过 getServiceHub() 持久化数据。
2.5 运行时拓扑的阅读陷阱¶
不要把 core 理解成 Rust core,也不要把 extensions/llamacpp-extension 理解成真正启动 llama-server 的地方。前者是 TypeScript 协议,后者是前端编排层;真正的模型进程管理在 Tauri plugin Rust 代码中。正确的追踪方式是: