跳转至

第 4 章:Core 与扩展系统 —— 共享协议,不是插件沙箱

4.1 Core 的三层含义

core/src 不是一个完整应用,而是一组“跨边界契约”:

  1. types/:Thread、Message、Model、Assistant、MCP、Inference 等领域类型。
  2. browser/:给 WebView 使用的 filesystem、events、logger、ModelManager 和 extension 基类。
  3. index.ts:统一导出类型和 browser 模块,并声明 globalThis.core

这套设计让 extension 可以在不依赖 React 的情况下操作能力,同时让 Web UI 只依赖抽象接口。

4.2 BaseExtension 的生命周期

stateDiagram-v2
  [*] --> Discovered: getActiveExtensions
  Discovered --> Activated: ExtensionManager.activateExtension
  Activated --> Loaded: onLoad()
  Loaded --> Loaded: registerModels / registerSettings
  Loaded --> Unloaded: onUnload()
  Unloaded --> [*]

BaseExtension 统一提供:

  • name/productName/url/active/version/description 元数据。
  • type():返回 ExtensionTypeEnum
  • onLoad()/onUnload():生命周期钩子。
  • compatibility():平台/版本兼容性声明。
  • registerModels():写入共享 ModelManager
  • registerSettings/getSettings/updateSettings():以 localStorage 的 extension name 为 key 保存设置。

注意 onLoad 是抽象方法,但 ExtensionManager 使用 Promise.allSettled 并逐个报告失败:一个扩展加载失败不应阻塞所有扩展。这是良好的产品级容错,但也意味着加载顺序不能被默认视为严格顺序。

4.3 类型驱动的能力插座

class MyEngine extends AIEngine {
  readonly provider = 'my-provider'
  type() { return ExtensionTypeEnum.Engine }
  async chat(options: chatOptions) { /* ... */ }
}

Core 没有把 llama.cpp、Anthropic 或 MCP 写死在 InferenceExtension 里,而是用接口表达能力:

抽象 解决的问题
AssistantExtension 助手的增删查与默认助手
ConversationalExtension Thread/Message 的持久化协议
InferenceExtension 旧式 inference 请求入口
AIEngine Provider、模型加载、chat completion 和 import
MCPExtension 工具发现与 MCP server 管理
RAGExtension / VectorDBExtension 附件检索与向量存储

真实实现位于 extensions/*/src/index.ts。例如 conversational-extension 只是把 Core 方法映射到 window.core.api;它不重复实现文件存储,存储责任在 Rust command。

4.4 EngineManager:从 Provider 名称到引擎实例

EngineManagerMap<string, AIEngine> 注册引擎,key 是 engine.providerAIEngine.onLoad() 调用 registerEngine(),因此引擎扩展加载后即可通过 provider 查询。

这让模型选择有两段映射:

UI provider = llamacpp
  → ModelFactory / ExtensionManager.getEngine('llamacpp')
  → llamacpp-extension
  → tauri-plugin-llamacpp guest API

4.5 “可扩展”并不等于“任意第三方安全执行”

当前 Tauri 实现的 getActiveExtensions() 返回 bundled extensions;安装/卸载在 Tauri 路径上多数是 no-op 或由预置资源决定。ExtensionManager.activateExtension 仍保留动态 import 的能力,但真正的安全边界由应用打包、Tauri capability 和 Rust command 决定。

因此更准确的表述是:Jan 有插件化的内部架构,且为外部扩展保留了协议;它不是一个自动提供沙箱隔离的插件市场。这一区分对安全评估很重要。

4.6 为什么 Core 需要 globalThis.core

浏览器侧的 core.tsevents.tsfs.ts 使用 globalThis.core.api。这是一种 bootstrap 后注入的桥接方式:

  • Core 包不直接依赖 Tauri API,能在浏览器/测试中被加载。
  • Tauri 具体实现可替换。
  • extension 不需要知道当前运行在 desktop、mobile 还是 mock 环境。

代价是类型和运行时初始化是两个时间点。若组件在 ServiceHub/Core 尚未初始化前访问 bridge,就会得到 undefined 或显式错误,所以 ServiceHubProvider 必须位于根布局的早期。

4.7 设计取舍

这个扩展系统选的是“共享内存中的协议和注册表”,不是进程级微服务:调用简单、延迟低、类型共享自然;但扩展崩溃、权限隔离、版本兼容和热更新都更难。对于桌面单体产品,这是合理的起点;如果未来要让第三方扩展不可信执行,必须再加进程/权限边界。