首页
/ Jan Assistant 扩展开发指南:用 TypeScript 构建、打包并测试你自己的 Jan 扩展

Jan Assistant 扩展开发指南:用 TypeScript 构建、打包并测试你自己的 Jan 扩展

2026-09-05 23:49:02作者:胡易黎Nicole

本文以 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 给出了标准的模板使用流程:

  1. 点击仓库顶部的 Use this template 按钮;
  2. 选择 Create a new repository;
  3. 为新的仓库选择 owner 与名称;
  4. 点击 Create repository;
  5. 克隆你的新仓库到本地。

环境要求

模板 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.jsonscripts,可确认模板命令与仓库实际脚本的对应关系:

{
  "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 模板后必须更新其中的 namedescription。以当前模板文件为参照,各关键字段含义如下:

字段 当前值 作用
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.jsonmain 声明位置;
  • platform: 'browser' 表明扩展代码运行在浏览器/前端运行时环境中;
  • define 在编译期注入了两个全局常量 NODEVERSION,其值来自 package.jsonname/node/version 字段。这与 src/@types/global.d.ts 中的声明相呼应:
declare const NODE: string
declare const VERSION: string

也就是说,扩展代码在运行时可以直接读取自身的包名与版本号,无需额外配置。

tsconfig.json 则规定了编译口径:target: es2016module: ES6declaration: 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:所有扩展的基类,定义了 nameurlactivedescriptionversion 等属性,以及两个必须实现的生命周期钩子 onLoad() / onUnload(),还提供了 registerModelsregisterSettings 等通用能力;
  • 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:包含 avataridobjectcreated_atnamedescriptionmodelinstructionstoolsfile_idsmetadata 等字段,并配有逐字段注释,是编写助手相关扩展时最核心的类型契约。

模板 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 扩展编程模型的三个关键特征:

  1. 虚拟文件系统:一切持久化都通过 file:// 前缀路径进行(如 file://assistantsfile://assistants/<id>/assistant.json),由 @janhq/core 导出的 fs 模块统一抽象,屏蔽了不同平台的真实磁盘差异。这是一个写自定义扩展时必须记住的约定——不要直接使用 Node 的 fs 模块。
  2. 幂等初始化:先确保目录存在,再执行迁移,最后仅在磁盘为空时写入种子数据,避免覆盖用户已自定义的助手(对应测试用例 “does not overwrite an existing persisted assistant on load”)。
  3. 种子参数:默认助手 Janid: 'jan'avatar: '👋'model: '*' 表示适配所有已安装模型)附带默认采样参数 temperature: 0.7 / top_k: 20 / top_p: 0.8 / repeat_penalty: 1.12,其 instructions 是一段要求“按用户语言回复、逐步推理、作为专业工具调用者分析信息缺口”的系统提示词,并带有 {{current_date}} 日期占位符;tools 中默认挂了一个 禁用状态retrieval 工具,附带 top_k: 2chunk_size: 1024chunk_overlap: 64 的 RAG 检索配置与检索提示词模板(L333-L351)。

CRUD 方法本身也非常短小,是“最小可运行扩展”的范例:

  • createAssistantL281-L292):确保 file://assistants/<id>/ 目录存在后,把助手序列化为缩进 JSON 写入 assistant.json
  • deleteAssistantL294-L303):存在即删除 assistant.json,不存在则为空操作(no-op);
  • getAssistantsL275-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/corefs 进行 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.tssrc/test/setup.ts 配置。

九、动手清单:从模板到自定义扩展

综合 README 与源码,把模板改造为自己的扩展可以按以下清单执行:

  1. 改名与元数据:更新 package.jsonnameproductNamedescriptionauthor,并提升 version
  2. 替换源码src/ 是扩展的心脏,README 明确允许整体替换。自定义扩展类应继承 core 中与你目标能力对应的抽象基类(如 AssistantExtension),实现其全部抽象方法,并在 onLoad() 中完成初始化、onUnload() 中做清理;
  3. 遵循异步约定:所有扩展函数按异步风格编写,返回 Promise;需要响应消息等应用事件时,使用 @janhq/coreevents.on(...) 订阅;
  4. 持久化走 file:// 协议:使用 core 导出的 fsjoinPath 管理数据目录,避免直接操作宿主磁盘;
  5. 构建与验证:执行 npm install 后运行 build(即 README 所述的打包步骤),确认 dist/index.js 生成;用 npm pack(或仓库内的 build:publish)生成 .tgz 产物;
  6. 回归测试:参照 src/index.test.ts 的内存 fs mock 手法为你的存储与迁移逻辑补测试,再执行 npm test

以上流程均以当前仓库的实际文件为准:模板文档见 extensions/assistant-extension/README.md,构建与类型配置见 rolldown.config.mjstsconfig.json,扩展契约见 core/src/browser/extension.tscore/src/browser/extensions/assistant.ts,助手类型契约见 core/src/types/assistant/assistantEntity.ts。掌握这套“元数据 + 打包 + 生命周期 + 虚拟文件系统持久化 + 版本化迁移”的组合模式,即可在 Jan 生态中开发并分发自己的功能扩展。

登录后查看全文
热门项目推荐
相关项目推荐