跳转至

第 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,把 coreweb-appextensions/* 纳入同一个依赖图。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 做三件大事:

  1. invoke_commands_with_extras! 汇总 filesystem、app、server、MCP、threads、downloads 和退出/主题命令。
  2. 注册 Tauri plugins:tauri-plugin-llamacpp、vector DB、RAG、websearch、HTTP、store、updater 等。
  3. 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.tsPlatformServiceHub 把 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 也能拿到服务。useThreadsuseMessages 是 Zustand store,不在 React render tree 内,但它们通过 getServiceHub() 持久化数据。

2.5 运行时拓扑的阅读陷阱

不要把 core 理解成 Rust core,也不要把 extensions/llamacpp-extension 理解成真正启动 llama-server 的地方。前者是 TypeScript 协议,后者是前端编排层;真正的模型进程管理在 Tauri plugin Rust 代码中。正确的追踪方式是:

llamacpp-extension/src/index.ts
  → @janhq/tauri-plugin-llamacpp-api
  → src-tauri/plugins/tauri-plugin-llamacpp/src/commands.rs
  → router.rs / llama-server subprocess