第 4 章:Core 与扩展系统 —— 共享协议,不是插件沙箱¶
4.1 Core 的三层含义¶
core/src 不是一个完整应用,而是一组“跨边界契约”:
types/:Thread、Message、Model、Assistant、MCP、Inference 等领域类型。browser/:给 WebView 使用的 filesystem、events、logger、ModelManager 和 extension 基类。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 名称到引擎实例¶
EngineManager 用 Map<string, AIEngine> 注册引擎,key 是 engine.provider。AIEngine.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.ts、events.ts、fs.ts 使用 globalThis.core.api。这是一种 bootstrap 后注入的桥接方式:
- Core 包不直接依赖 Tauri API,能在浏览器/测试中被加载。
- Tauri 具体实现可替换。
- extension 不需要知道当前运行在 desktop、mobile 还是 mock 环境。
代价是类型和运行时初始化是两个时间点。若组件在 ServiceHub/Core 尚未初始化前访问 bridge,就会得到 undefined 或显式错误,所以 ServiceHubProvider 必须位于根布局的早期。
4.7 设计取舍¶
这个扩展系统选的是“共享内存中的协议和注册表”,不是进程级微服务:调用简单、延迟低、类型共享自然;但扩展崩溃、权限隔离、版本兼容和热更新都更难。对于桌面单体产品,这是合理的起点;如果未来要让第三方扩展不可信执行,必须再加进程/权限边界。