Jan Assistant 扩展开发指南:用 TypeScript 构建、打包并测试你自己的 Jan 扩展
【免费下载链接】janJan is an open source alternative to ChatGPT that runs 100% offline on your computer.项目地址: https://gitcode.com/GitHub_Trending/ja/jan
本文以 Jan 仓库中的 assistant-extension 模板 为核心,结合 core 包中的扩展基类、扩展源码 与 单元测试,完整讲解如何用 TypeScript 创建、打包、安装和演进一个 Jan 扩展:从package.json元数据定义、rolldown 构建产物到file://虚拟文件系统中的助手数据迁移机制,读者可据此独立开发自己的 Jan 扩展。
一、模板定位:assistant-extension 在 Jan 仓库中的角色
Jan 是运行在本地的开源 AI 聊天应用,其功能通过“扩展(Extension)”机制进行模块化拆分:助手管理、对话编排、推理后端、模型下载、RAG、向量库等能力都实现为独立扩展,由@janhq/core包提供统一的事件、文件系统和类型系统。
extensions/assistant-extension 目录既是 Jan 内置的默认 AI 助手实现,也被官方 README 明确定位为一个可直接 fork 使用的扩展脚手架模板。README 开篇即说明:
Use this template to bootstrap the creation of a TypeScript Jan extension.
因此,围绕该目录学习,既能掌握“如何写一个新扩展”,又能顺带读懂 Jan 默认助手(默认系统提示词、默认采样参数、助手持久化与数据迁移)的完整实现。
二、创建你自己的扩展:模板使用与初始环境搭建
README 给出了标准的模板使用流程:
- 点击仓库顶部的 Use this template 按钮;
- 选择 Create a new repository;
- 为新的仓库选择 owner 与名称;
- 点击 Create repository;
- 克隆你的新仓库到本地。
环境要求
模板 README 明确要求一个较新的 Node.js 环境:20.x 或更高版本;如果在使用nodenv/nvm这类版本管理器,可以在仓库根目录按package.json中指定的版本安装对应 Node。
需要说明的一个仓库事实是:当前 package.json 中声明了"packageManager": "yarn@4.5.3",且依赖使用了workspace:*协议("@janhq/core": "workspace:*"),这意味着在 Jan monorepo 内部开发时应使用 Yarn 4 工作区;而在 fork 出的独立模板仓库中按 README 使用npm install同样可行(独立仓库中@janhq/core会解析为 npm 发布版本)。
依赖安装、打包与产物检查
README 描述的三步工作流,以及当前仓库中对应的真实脚本:
# 1. 安装依赖 npm install # 2. 打包 TypeScript(README 中的命令;当前仓库脚本名为 build) npm run bundle # 对应当前 package.json 中的 "build": "rolldown -c rolldown.config.mjs" # 3. 检查产物:扩展目录中会出现 .tgz 文件对照当前 package.json 的scripts,可确认模板命令与仓库实际脚本的对应关系:
{ "build": "rolldown -c rolldown.config.mjs", "build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install", "test": "vitest run" }即:build完成 rolldown 打包;build:publish在清理旧产物后执行build,再用npm pack生成.tgz安装包并复制到仓库的pre-install目录(随 Jan 应用预装);test运行 vitest 测试。README 中提到的 “tgz 产物”即由npm pack产生。
三、扩展元数据:package.json 字段逐项解读
README 指出:package.json定义了扩展的名称、主入口、描述和版本等元数据,fork 模板后必须更新其中的name与description。以当前模板文件为参照,各关键字段含义如下:
| 字段 | 当前值 | 作用 |
|---|---|---|
name | @janhq/assistant-extension | 扩展包名,是 Jan 识别扩展的唯一标识 |
productName | Jan Assistant | 展示在产品界面中的名称 |
version | 1.0.2 | 扩展版本号 |
main | dist/index.js | 扩展主入口(打包产物路径) |
node | dist/node/index.js | 节点侧入口(如存在独立后端逻辑) |
author/license | Jan <service@jan.ai>/AGPL-3.0 | 作者与协议信息 |
dependencies | @janhq/core | 唯一运行时依赖:Jan 扩展核心包 |
files | dist/*,package.json,README.md | 发布进.tgz包的文件白名单 |
installConfig.hoistingLimits | workspaces | 在 monorepo 中避免依赖被提升(hoisting)到工作区外 |
这些字段并非摆设:rolldown 构建配置会直接读取它们(见下一节),core 包的BaseExtension构造参数name / productName / url / active / description / version(见 extension.ts)也与元数据一一对应。
四、构建管线:从 src/index.ts 到 dist/index.js
rolldown.config.mjs 完整展示了扩展的打包逻辑,值得逐行理解:
import { defineConfig } from 'rolldown' import pkgJson from './package.json' with { type: 'json' } export default defineConfig([ { input: 'src/index.ts', output: { format: 'esm', file: 'dist/index.js', // 即 package.json 中的 main 字段 }, platform: 'browser', define: { NODE: JSON.stringify(`${pkgJson.name}/${pkgJson.node}`), VERSION: JSON.stringify(pkgJson.version), }, } ])要点:
- 入口是
src/index.ts,输出为ESM 格式单文件dist/index.js,正好落在package.json的main声明位置; platform: 'browser'表明扩展代码运行在浏览器/前端运行时环境中;define在编译期注入了两个全局常量NODE与VERSION,其值来自package.json的name/node/version字段。这与 src/@types/global.d.ts 中的声明相呼应:
declare const NODE: string declare const VERSION: string也就是说,扩展代码在运行时可以直接读取自身的包名与版本号,无需额外配置。
tsconfig.json 则规定了编译口径:target: es2016、module: ES6、declaration: true(声明文件输出到dist/types)、sourceMap: true,与 ESM 打包目标保持一致。
五、扩展代码骨架:继承 AssistantExtension 与生命周期
模板 README 对扩展代码有两点核心提示:大部分 Jan 扩展函数都是异步处理的,扩展函数会返回Promise<any>;事件订阅的典型写法如下(摘自 README):
import { events, MessageEvent, MessageRequest } from '@janhq/core' function onStart(): Promise<any> { return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => this.inference(data) ) }在 core 包中,扩展体系以抽象类层次组织:
- BaseExtension:所有扩展的基类,定义了
name、url、active、description、version等属性,以及两个必须实现的生命周期钩子onLoad()/onUnload(),还提供了registerModels、registerSettings等通用能力; - AssistantExtension:助手类型扩展的抽象中间层,声明
type()返回ExtensionTypeEnum.Assistant,并要求实现三个抽象方法:
export abstract class AssistantExtension extends BaseExtension implements AssistantInterface { type(): ExtensionTypeEnum | undefined { return ExtensionTypeEnum.Assistant } abstract createAssistant(assistant: Assistant): Promise<void> abstract deleteAssistant(assistant: Assistant): Promise<void> abstract getAssistants(): Promise<Assistant[]> }Assistant的数据形状定义在 core/src/types/assistant/assistantEntity.ts:包含avatar、id、object、created_at、name、description、model、instructions、tools、file_ids、metadata等字段,并配有逐字段注释,是编写助手相关扩展时最核心的类型契约。
模板 README 指向的 Jan Extension Core 模块文档,在本仓库中即 core/README.md。
六、默认助手实现解析:onLoad、持久化与种子数据
extensions/assistant-extension/src/index.ts 中的JanAssistantExtension是模板的参考实现,onLoad()(L17-L39)展示了扩展加载时应当完成的标准初始化序列:
async onLoad() { if (!(await fs.existsSync('file://assistants'))) { await fs.mkdir('file://assistants') } // Run migrations if needed await this.runMigrations() const assistants = await this.readAssistantsFromDisk() if (assistants.length === 0) { const assistantWithParams = { ...this.defaultAssistant, parameters: { temperature: 0.7, top_k: 20, top_p: 0.8, repeat_penalty: 1.12, }, } await this.createAssistant(assistantWithParams as Assistant) } }这里体现了 Jan 扩展编程模型的三个关键特征:
- 虚拟文件系统:一切持久化都通过
file://前缀路径进行(如file://assistants、file://assistants/<id>/assistant.json),由@janhq/core导出的fs模块统一抽象,屏蔽了不同平台的真实磁盘差异。这是一个写自定义扩展时必须记住的约定——不要直接使用 Node 的fs模块。 - 幂等初始化:先确保目录存在,再执行迁移,最后仅在磁盘为空时写入种子数据,避免覆盖用户已自定义的助手(对应测试用例 “does not overwrite an existing persisted assistant on load”)。
- 种子参数:默认助手
Jan(id: 'jan'、avatar: '👋'、model: '*'表示适配所有已安装模型)附带默认采样参数temperature: 0.7 / top_k: 20 / top_p: 0.8 / repeat_penalty: 1.12,其instructions是一段要求“按用户语言回复、逐步推理、作为专业工具调用者分析信息缺口”的系统提示词,并带有{{current_date}}日期占位符;tools中默认挂了一个禁用状态的retrieval工具,附带top_k: 2、chunk_size: 1024、chunk_overlap: 64的 RAG 检索配置与检索提示词模板(L333-L351)。
CRUD 方法本身也非常短小,是“最小可运行扩展”的范例:
createAssistant(L281-L292):确保file://assistants/<id>/目录存在后,把助手序列化为缩进 JSON 写入assistant.json;deleteAssistant(L294-L303):存在即删除assistant.json,不存在则为空操作(no-op);getAssistants(L275-L279):优先读取磁盘数据;磁盘为空时回退到内置的defaultAssistant,保证上层调用总能拿到至少一个可用助手。私有方法readAssistantsFromDisk还会跳过缺少assistant.json的目录以及 JSON 解析失败的损坏文件,只记录错误而不中断整体加载。
七、数据迁移机制:版本化 .migration_version 与三级迁移
JanAssistantExtension内置了一套值得借鉴的轻量数据迁移方案(L41-L95):
- 迁移版本记录在
file://assistants/.migration_version文件中,当前版本常量CURRENT_MIGRATION_VERSION = 3; getCurrentMigrationVersion()读取该文件,文件缺失或内容无法解析(parseInt得到NaN)时一律按版本 0处理,从而保证迁移一定会补跑;runMigrations()按currentVersion < N的条件逐档执行迁移,每完成一档立即写回版本号:
| 版本 | 迁移内容 |
|---|---|
| v1 | 将旧版指令前缀You are a helpful AI assistant.改写为You are Jan, a helpful AI assistant.,并保留后续自定义内容(用startsWith+ 字符串截取实现) |
| v2 | 将旧前缀助手整体改写为新版默认指令(含工具调用分析流程、{{current_date}}占位符),并补齐默认采样参数 |
| v3 | 仅当助手指令与 v2 写入的默认文本逐字完全一致时,剥离身份前缀段落,恢复为纯默认指令;用户自定义提示词不受影响 |
迁移逻辑刻意保守:每一档都先做字符串精确匹配再改写,且失败时只logger.error而不抛出,确保单个助手损坏不会阻塞整个扩展启动。这种“版本号文件 + 条件式补跑 + 精确匹配保护用户数据”的模式,可直接移植到任何需要持久化状态演进的 Jan 扩展中。
八、测试实践:用内存文件系统验证扩展逻辑
模板自带 src/index.test.ts,展示了官方推荐的扩展测试方式:用 vitest 对@janhq/core的fs进行 mock,以两个内存容器模拟虚拟文件系统——
let files: Map<string, string> // 路径 -> 文件内容 let dirs: Set<string> // 已存在的目录existsSync / mkdir / writeFileSync / readFileSync / rm / readdirSync全部落到Map/Set上(L12-L45),使得测试完全不依赖真实磁盘。测试覆盖的断言点恰好对应第六、七节的所有行为:
getAssistants:目录不存在、目录为空时均回退默认助手(id: 'jan');能并行读取多个助手;跳过无assistant.json的孤儿目录与 JSON 损坏的条目;createAssistant:自动建目录并写入格式化 JSON(断言输出含\n缩进);目录已存在时不再调用mkdir;deleteAssistant:存在时删除文件,不存在时fs.rm根本不被调用;onLoad:自动创建file://assistants目录、写入迁移版本'3'、种子助手携带正确的默认参数、且不覆盖已持久化的自定义助手;- 迁移:v1 精确替换前缀且保留尾部自定义文本(
'You are a helpful AI assistant. Be concise.'→'You are Jan, a helpful AI assistant. Be concise.');v2 写入参数、v3 剥掉身份前缀;已经是版本 3 时不重复执行迁移;版本文件内容为'garbage'时按 0 处理并补跑。
运行方式即npm test(对应"test": "vitest run"),测试环境由 vitest.config.ts 与 src/test/setup.ts 配置。
九、动手清单:从模板到自定义扩展
综合 README 与源码,把模板改造为自己的扩展可以按以下清单执行:
- 改名与元数据:更新 package.json 的
name、productName、description、author,并提升version; - 替换源码:
src/是扩展的心脏,README 明确允许整体替换。自定义扩展类应继承 core 中与你目标能力对应的抽象基类(如AssistantExtension),实现其全部抽象方法,并在onLoad()中完成初始化、onUnload()中做清理; - 遵循异步约定:所有扩展函数按异步风格编写,返回
Promise;需要响应消息等应用事件时,使用@janhq/core的events.on(...)订阅; - 持久化走 file:// 协议:使用 core 导出的
fs与joinPath管理数据目录,避免直接操作宿主磁盘; - 构建与验证:执行
npm install后运行build(即 README 所述的打包步骤),确认dist/index.js生成;用npm pack(或仓库内的build:publish)生成.tgz产物; - 回归测试:参照 src/index.test.ts 的内存 fs mock 手法为你的存储与迁移逻辑补测试,再执行
npm test。
以上流程均以当前仓库的实际文件为准:模板文档见 extensions/assistant-extension/README.md,构建与类型配置见 rolldown.config.mjs、tsconfig.json,扩展契约见 core/src/browser/extension.ts 与 core/src/browser/extensions/assistant.ts,助手类型契约见 core/src/types/assistant/assistantEntity.ts。掌握这套“元数据 + 打包 + 生命周期 + 虚拟文件系统持久化 + 版本化迁移”的组合模式,即可在 Jan 生态中开发并分发自己的功能扩展。
【免费下载链接】janJan is an open source alternative to ChatGPT that runs 100% offline on your computer.项目地址: https://gitcode.com/GitHub_Trending/ja/jan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考