Jan @janhq/core 核心库详解:事件系统、扩展生命周期与从零构建自定义扩展
本文围绕 Jan 仓库中的 core/README.md 展开,系统讲解 TypeScript 核心库 @janhq/core 的三大职责——与核心 API 通信、注册应用扩展、导出类型定义,并结合 core/src 下的源码剖析事件系统(events.on/off/emit)、BaseExtension 生命周期与设置持久化机制,最后完整复现"下载模板 → 编写 onLoad() → yarn build → 手动安装 .tgz"的扩展开发全流程,帮助读者掌握为 Jan 编写自定义扩展所需的完整技术栈。
一、@janhq/core 的定位与包结构
按照 core/README.md 的定义:
This module includes functions for communicating with core APIs, registering app extensions, and exporting type definitions.
即 @janhq/core 提供三类能力:调用 Jan 核心 API 的桥接函数、扩展注册所需的基础类与类型、全套 TypeScript 类型定义。从 core/package.json 可以看到该包的关键元信息:
- 包名
@janhq/core,入口dist/index.js,类型声明位于dist/types/index.d.ts; - 运行时依赖仅有
rxjs与ulidx(工具 ID 生成),react@19.0.0作为 peerDependency,说明该库面向前端/扩展运行时设计; - 许可协议为
AGPL-3.0,包管理器锁定为yarn@4.5.3。
构建链在 core/package.json 的 scripts 中定义:
"prebuild": "rimraf dist",
"build": "tsc -p . && rolldown -c rolldown.config.mjs"
即先用 tsc 产出类型声明,再由 core/rolldown.config.mjs 以 ESM 格式打包 src/index.ts 为 dist/index.js,并注入 VERSION 等构建时常量。测试基于 Vitest(yarn test、yarn test:watch、yarn test:coverage)。
模块导出结构
库的顶层入口 core/src/index.ts 只做两件事:
export * from './types' // 导出所有类型定义
export * from './browser' // 导出浏览器/扩展运行时模块
declare global {
var core: any | undefined // 宿主应用注入的全局对象
}
注意 declare global 声明的 globalThis.core:这是 Jan 宿主应用(web-app / Tauri 前端)注入的全局对象,扩展运行期通过它访问真正的事件总线与 API 实现。这也是为什么 README 中将 import * as core from '@janhq/core' 标注为 "Web / extension runtime"——该库本身只是桥接层,能力由宿主应用提供。
core/src/browser/index.ts 进一步细分了七大模块:
| 模块 | 导出文件 | 职责 |
|---|---|---|
| Core | ./core |
核心 API 桥接 |
| Events | ./events |
事件订阅/发布 |
| Filesystem | ./fs |
文件系统操作 |
| Extension | ./extension |
BaseExtension 基类 |
| Extensions | ./extensions |
内置抽象扩展(assistant、conversational、inference、MCP、RAG、VectorDB、engines) |
| Models | ./models |
ModelManager 与模型类型 |
| Logger | ./logger |
共享日志器 |
这与 core/CONTRIBUTING.md 中列出的关键目录(/src/browser 核心 API、/src/browser/extensions 内置扩展、/src/types 类型定义、/src/test 测试工具)一一对应,其阐述的四条设计原则也值得记住:平台无关(浏览器/Node 均可运行)、基于扩展(新能力 = 新扩展)、全面类型化(TypeScript 强制)、事件驱动(组件间通过事件通信)。
二、事件系统:扩展与应用的解耦通道
README 扩展示例的第一行核心逻辑就是:
core.events.on(MessageEvent.OnMessageSent, (data) => MyExtension.inference(data, this))
事件系统的实现在 core/src/browser/events.ts,结构非常精简:
const on: (eventName: string, handler: Function) => void = (eventName, handler) => {
globalThis.core?.events?.on(eventName, handler)
}
const off: (eventName: string, handler: Function) => void = (eventName, handler) => {
globalThis.core?.events?.off(eventName, handler)
}
const emit: (eventName: string, object: any) => void = (eventName, object) => {
globalThis.core?.events?.emit(eventName, object)
}
export const events = { on, off, emit }
三个方法全部委托给 globalThis.core.events,即真正的 EventBus 由宿主应用持有,@janhq/core 只是类型安全的薄封装。core/CONTRIBUTING.md 给出了通用用法示例:
// Emit events
events.emit('model:loaded', { modelId: 'llama-3' })
// Listen for events
events.on('model:loaded', (data) => {
console.log('Model loaded:', data.modelId)
})
内置事件名枚举
消息相关的标准事件名定义在 core/src/types/message/messageEvent.ts:
export enum MessageEvent {
/** The `OnMessageSent` event is emitted when a message is sent. */
OnMessageSent = 'OnMessageSent',
/** The `OnMessageResponse` event is emitted when a message is received. */
OnMessageResponse = 'OnMessageResponse',
/** The `OnMessageUpdate` event is emitted when a message is updated. */
OnMessageUpdate = 'OnMessageUpdate',
}
配合 core/src/types/message/messageRequestType.ts 中的 MessageRequestType(Thread / Assistant / Summary),扩展就能按消息来源区分处理逻辑。MessageRequestData、ThreadContent、ThreadMessage、ContentType 等事件载荷类型则统一由 core/src/types 导出,这正是 README 中"导出类型定义"职责的具体体现。
三、扩展系统:BaseExtension 与生命周期
3.1 BaseExtension 基类
所有扩展必须继承 core/src/browser/extension.ts 中的抽象类:
export abstract class BaseExtension implements ExtensionType {
protected settingFolderName = 'settings'
protected settingFileName = 'settings.json'
name: string // 扩展名
productName?: string
url: string // 扩展加载地址
active // 是否激活
description
version
constructor(url, name, productName?, active?, description?, version?) { ... }
type(): ExtensionTypeEnum | undefined { return undefined } // 子类可覆写
abstract onLoad(): void // 扩展加载时的初始化逻辑
abstract onUnload(): void // 扩展卸载时的清理逻辑
compatibility(): Compatibility | undefined { return undefined }
}
几个关键点:
onLoad()/onUnload()是抽象方法,子类必须实现——初始化逻辑放在onLoad,清理逻辑放在onUnload,这与 README 中"在onLoad()方法里添加你的代码"的指引完全一致;type()返回扩展类型,应用据此判断该扩展是否扩展了某个已知能力。可用类型枚举定义在同文件 ExtensionTypeEnum:
export enum ExtensionTypeEnum {
Assistant = 'assistant',
Conversational = 'conversational',
Inference = 'inference',
Model = 'model',
SystemMonitoring = 'systemMonitoring',
MCP = 'mcp',
HuggingFace = 'huggingFace',
Engine = 'engine',
Hardware = 'hardware',
RAG = 'rag',
VectorDB = 'vectorDB',
}
compatibility()用于环境兼容性检查,返回Compatibility(platform: string[]与version: string),从源码结构看,宿主在加载扩展时会用它判断当前平台与版本是否匹配。
3.2 模型注册与设置持久化
BaseExtension 还内建了模型注册和设置管理能力,对第三方扩展非常实用:
- 模型注册(extension.ts#L104-L108):
registerModels(models: Model[])将模型逐个注册进内存中共享的ModelManager.instance(),本地推理类扩展(如仓库中的 llamacpp-extension)即依赖该机制上报模型列表; - 设置注册(extension.ts#L115-L156):
registerSettings(settings: SettingComponentProps[])会先读取localStorage中同名的旧设置,合并保留用户已设置的值、options 与 recommended 字段,再写回localStorage(key 为扩展名)。这解释了为什么扩展重命名(name字段变化)后设置会"丢失"——存储 key 绑定在name上; - 设置读取与更新:
getSetting<T>(key, defaultValue)(extension.ts#L164-L169)按 key 取值并支持默认值;updateSettings(componentProps)(extension.ts#L207-L232)更新后会对每一项触发onSettingUpdate(key, value)回调,子类覆写该回调即可响应设置变化; - 前置安装钩子:
install(): Promise<void>默认空实现,扩展可覆写用于下载依赖等安装期准备。
3.3 内置抽象扩展
core/src/browser/extensions/index.ts 导出了若干面向特定领域的抽象扩展,自定义扩展可继承它们来"扩展一个已知能力"而非从零实现:
ConversationalExtension(conversational.ts):实现线程与消息的增删查改,抽象方法包括listThreads、createThread、createMessage、listMessages、modifyMessage、getThreadAssistant等;InferenceExtension(inference.ts):只要求实现inference(data: MessageRequest): Promise<ThreadMessage>,是自定义推理逻辑的挂载点;AssistantExtension(assistant.ts):要求实现createAssistant/deleteAssistant/getAssistants;- 此外还有
MCPExtension(工具与服务器通信)、RAGExtension、VectorDBExtension以及engines(AI 引擎基类)。
例如仓库内的 extensions/llamacpp-extension 就是一个推理引擎扩展的完整实例,可作为学习参考。
3.4 文件系统桥接
扩展读写数据依赖 core/src/browser/fs.ts 导出的 fs 对象,它把一系列操作转发到 globalThis.core.api:
export const fs = {
writeFileSync, readFileSync, existsSync, readdirSync,
mkdir, rm, mv, unlinkSync, appendFileSync, copyFile,
fileStat, writeBlob, getGgufFiles,
}
这些函数与 Node 的 fs API 命名一致,但路径约定上支持 file:// 协议(指向 Jan 数据目录)。从 extensions/assistant-extension/src/index.ts 的实现可以看到真实用法:onLoad() 中先 fs.existsSync('file://assistants') 检查目录、不存在则 fs.mkdir,再执行数据迁移、读取已有 assistant 列表,为空时创建带默认采样参数(temperature: 0.7、top_k: 20、top_p: 0.8、repeat_penalty: 1.12)的默认 assistant。这展示了 onLoad + fs + 默认值兜底的典型初始化模式。
四、从零构建一个 Jan 扩展(README 全流程复现)
以下内容完整继承 core/README.md 的 "Build an Extension" 章节,并结合仓库补充细节。
第 1 步:获取扩展模板
README 建议下载官方扩展模板(extension-template 仓库)作为起点。也可以直接参考本仓库 extensions/ 目录下的 7 个内置扩展(assistant、conversational、download、llamacpp、mlx、rag、vector-db)的目录结构:每个扩展都是一个独立包,含 src/index.ts、package.json、rolldown.config.mjs、tsconfig.json、vitest.config.ts 等文件。
第 2 步:修改源码
- 在编辑器中打开
index.ts; - 把示例扩展类
SampleExtension重命名为你期望的扩展名(注意:name同时是设置持久化的 localStorage key,改名会影响已有设置的读取,见 3.2 节); - 引入 core 包:
import * as core from '@janhq/core'
- 在
onLoad()方法中添加你的代码。README 给出的完整示例是监听消息事件并提供自定义推理逻辑:
// Example of listening to app events and providing customized inference logic:
import * as core from '@janhq/core'
export default class MyExtension extends BaseExtension {
// On extension load
onLoad() {
core.events.on(MessageEvent.OnMessageSent, (data) => MyExtension.inference(data, this))
}
// Customized inference logic
private static inference(incomingMessage: MessageRequestData) {
// Prepare customized message content
const content: ThreadContent = {
type: ContentType.Text,
text: {
value: "I'm Jan Assistant!",
annotations: [],
},
}
// Modify message and send out
const outGoingMessage: ThreadMessage = {
...incomingMessage,
content,
}
}
}
结合前文源码可以读出该示例的每个符号来源:BaseExtension、MessageEvent、MessageRequestData、ThreadContent、ThreadMessage、ContentType 均从 @janhq/core 导出;core.events.on 实际注册到宿主的全局事件总线。若要监听模型加载、线程变更等其他事件,可参考 core/src/types 下 message、thread、model、assistant 等子目录导出的事件枚举,并用 core.events.off 在 onUnload() 中注销监听器,与 onLoad/onUnload 的生命周期约定保持一致。
第 3 步:构建扩展
- 进入扩展目录;
- 安装依赖:
yarn install
- 编译源码:
yarn build
README 特别说明:该命令会保持在前台持续运行,并在你每次修改源码后自动重新构建(watch 模式),因此开发期间终端会一直挂着,Ctrl+C 即可退出。构建产物中会生成 .tgz 安装包。
- 在 Jan 应用中手动安装:进入 Settings > Extension > Manual Installation,选择生成的
.tgz文件完成安装。安装成功后,该扩展会在应用启动时被实例化并触发其onLoad()。
五、本地开发与测试 @janhq/core 本身
如果需要直接开发核心库(而非基于它写扩展),core/CONTRIBUTING.md 给出了一组命令:
# Build the SDK
yarn build
# Run tests
yarn test
# Watch mode
yarn test:watch
测试用例与源文件同目录(如 core/src/browser/extension.test.ts、core/src/browser/events.test.ts),采用 Vitest 的 describe/it/expect 风格,例如:
describe('MyFeature', () => {
it('should do something', () => {
const result = doSomething()
expect(result).toBe('expected')
})
})
开发规范(同文件 "Best Practices" 一节)要求:保持简单、全面使用 TypeScript(禁用 any)、为关键功能写测试、遵循既有模式、并在 index 文件中导出新增模块。
六、适用前提与限制
- 运行环境:
@janhq/core的设计前提是存在宿主注入的globalThis.core(见 core/src/index.ts 的全局声明与 events.ts、fs.ts 的委托实现)。脱离 Jan 宿主应用单独import该库时,events/fs调用会得到undefined,类型定义与基类本身仍可使用; - 设置存储位置:扩展设置持久化在
localStorage(key 为扩展name),而非扩展目录下的settings.json——后者是settingFolderName/settingFileName约定的默认名称,从源码结构看主要用于扩展自身的默认设置文件; - 版本能力:本文以当前仓库
@janhq/core0.1.10(见 core/package.json)的实际代码为准,事件名、扩展类型枚举与BaseExtension方法签名均以上述源码文件为权威依据。
小结
@janhq/core 是 Jan 扩展生态的地基:events 提供宿主与应用间的事件总线、BaseExtension 定义了 onLoad/onUnload 生命周期与设置持久化、fs 提供 file:// 数据目录访问、types 目录输出全部类型定义。掌握 README 中的四步流程(模板 → 改 onLoad → yarn build watch 构建 → 手动安装 .tgz)后,即可参照 extensions/ 下的内置扩展为 Jan 编写自己的 assistant、推理或 RAG 扩展。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00