Skip to content

插件开发、测试与发布

本指南覆盖 Zhin.js 插件的完整生命周期:创建 → 开发 → 测试 → 构建 → 发布

前置知识:阅读本页前建议先了解 核心概念插件系统

创建插件

方式一:CLI 脚手架(推荐)

在你的 Zhin.js 项目根目录执行:

bash
npx zhin new my-plugin

系统会进入交互式向导,依次选择:

? 选择插件类型
  > normal    (普通插件)
    service   (服务插件,提供 Context)
    adapter   (适配器插件,对接聊天平台)

? 输入插件描述: 我的第一个插件

生成的目录结构:

src/plugins/my-plugin/
├── src/
│   └── index.ts        # 插件入口
├── package.json        # 包信息
├── tsconfig.json       # TypeScript 配置
└── README.md           # 插件说明

方式二:手动创建

src/plugins/ 目录下创建一个 .ts 文件即可:

typescript
// src/plugins/hello.ts
import { usePlugin, MessageCommand } from 'zhin.js'

const { addCommand } = usePlugin()

addCommand(
  new MessageCommand('hello')
    .desc('打个招呼')
    .action(() => '你好!')
)

然后在配置文件中启用:

yaml
# zhin.config.yml
plugins:
  - hello

单文件 vs 目录插件

  • 单文件插件:适合简单功能,直接在 src/plugins/ 下创建 .ts 文件
  • 目录插件:适合复杂功能或需要发布到 npm,使用 npx zhin new 创建

插件 API

通过 usePlugin() 获取插件实例,它提供以下核心 API:

typescript
import { usePlugin, MessageCommand, Cron } from 'zhin.js'

const {
  // 命令
  addCommand,      // 添加消息命令
  // AI 工具
  addTool,         // 添加 AI 可调用的工具
  // 中间件
  addMiddleware,   // 添加消息中间件
  // 定时任务
  addCron,         // 添加定时任务
  // 组件
  addComponent,    // 添加消息组件
  // 数据库
  defineModel,     // 定义数据模型
  // 配置
  addConfig,       // 注册插件配置项
  // 依赖注入
  provide,         // 注册服务
  inject,          // 注入已有服务
  useContext,       // 等待服务就绪后执行
  // 生命周期
  onMounted,       // 插件挂载完成
  onDispose,       // 插件卸载时
  // 工具
  logger,          // 日志
} = usePlugin()

详见 插件系统API 参考

开发示例

基础命令插件

typescript
import { usePlugin, MessageCommand } from 'zhin.js'

const { addCommand } = usePlugin()

// 简单命令
addCommand(
  new MessageCommand('ping')
    .desc('检查机器人是否在线')
    .action(() => 'pong!')
)

// 带参数的命令
addCommand(
  new MessageCommand('echo <message:string>')
    .desc('回显消息')
    .action((_, result) => `你说:${result.params.message}`)
)

// 带可选参数的命令
addCommand(
  new MessageCommand('greet [name:string]')
    .desc('问候某人')
    .action((_, result) => {
      const name = result.params.name || '世界'
      return `你好,${name}!`
    })
)

使用数据库

typescript
import { usePlugin, MessageCommand } from 'zhin.js'

// 1. 声明模型类型
declare module 'zhin.js' {
  interface Models {
    notes: { id: number; text: string; createdAt: string }
  }
}

const { defineModel, addCommand, useContext } = usePlugin()

// 2. 定义数据模型
defineModel('notes', {
  id: { type: 'integer', primary: true },
  text: { type: 'string' },
  createdAt: { type: 'string', default: () => new Date().toISOString() }
})

// 3. 等待数据库就绪后注册命令
useContext('database', (db) => {
  const notes = db.models.get('notes')

  addCommand(
    new MessageCommand('note <text:string>')
      .desc('添加笔记')
      .action(async (_, result) => {
        await notes.insert({ text: result.params.text })
        return '✅ 笔记已保存'
      })
  )

  addCommand(
    new MessageCommand('notes')
      .desc('查看所有笔记')
      .action(async () => {
        const list = await notes.select()
        if (!list.length) return '暂无笔记'
        return list.map((n, i) => `${i + 1}. ${n.text}`).join('\n')
      })
  )
})

注册 AI 工具

typescript
import { usePlugin } from 'zhin.js'

const { addTool } = usePlugin()

addTool({
  name: 'roll_dice',
  description: '掷骰子,返回 1~6 的随机数',
  parameters: {},
  execute: async () => {
    return String(Math.ceil(Math.random() * 6))
  }
})

addTool({
  name: 'get_time',
  description: '获取当前时间',
  parameters: {},
  execute: async () => {
    return new Date().toLocaleString('zh-CN')
  }
})

服务插件(提供 Context)

typescript
import { usePlugin } from 'zhin.js'

const { provide } = usePlugin()

// 提供一个计数器服务
provide({
  name: 'counter',
  description: '全局计数器服务',
  value: {
    counts: new Map<string, number>(),
    increment(key: string) {
      const val = (this.counts.get(key) || 0) + 1
      this.counts.set(key, val)
      return val
    },
    get(key: string) {
      return this.counts.get(key) || 0
    }
  }
})

// 声明类型,让其他插件获得类型提示
declare module 'zhin.js' {
  namespace Plugin {
    interface Contexts {
      counter: {
        counts: Map<string, number>
        increment(key: string): number
        get(key: string): number
      }
    }
  }
}

其他插件即可注入使用:

typescript
const { useContext } = usePlugin()

useContext('counter', (counter) => {
  const count = counter.increment('visits')
  console.log(`第 ${count} 次访问`)
})

测试插件

使用 Sandbox 适配器实时测试

最快的测试方式是在开发模式下使用内置的 Sandbox 适配器:

bash
pnpm dev

启动后,直接在终端输入消息进行测试:

> ping
机器人: pong!

> echo 测试消息
机器人: 你说:测试消息

代码修改保存后会自动热重载,无需重启。

编写单元测试

Zhin.js 使用 Vitest 作为测试框架。在插件目录下创建测试文件:

typescript
// src/plugins/my-plugin/__tests__/index.test.ts
import { describe, it, expect } from 'vitest'
import { MessageCommand } from 'zhin.js'

describe('my-plugin', () => {
  it('should create ping command', () => {
    const cmd = new MessageCommand('ping')
      .desc('检查在线')
      .action(() => 'pong!')

    expect(cmd).toBeDefined()
  })

  it('should handle echo command with params', () => {
    const cmd = new MessageCommand('echo <message:string>')
      .desc('回显消息')
      .action((_, result) => `你说:${result.params.message}`)

    expect(cmd).toBeDefined()
  })
})

运行测试:

bash
# 运行所有测试
pnpm test

# 监听模式(修改后自动重新测试)
pnpm test:watch

# 生成覆盖率报告
pnpm test:coverage

使用 Web 控制台调试

启动开发模式后访问 http://localhost:8086,在 Web 控制台中可以:

  • 查看插件加载状态
  • 查看 Feature 注册情况(命令、工具、定时任务等)
  • 实时查看日志输出
  • 监控内存使用和消息统计

构建插件

将插件构建为可发布的 npm 包:

bash
# 在插件目录下构建
npx zhin build

# 清理后重新构建
npx zhin build --clean

# 生产模式构建(跳过 sourcemap)
npx zhin build --production

构建完成后,lib/ 目录包含编译后的 JavaScript 文件。

发布插件

1. 完善 package.json

确保 package.json 包含必要的信息:

json
{
  "name": "@zhin.js/plugin-my-plugin",
  "version": "1.0.0",
  "description": "我的 Zhin.js 插件",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "files": [
    "lib",
    "README.md"
  ],
  "keywords": [
    "zhin",
    "zhin-plugin",
    "chatbot"
  ],
  "peerDependencies": {
    "zhin.js": ">=2.0.0"
  }
}

命名规范

  • 官方插件:@zhin.js/plugin-<name>
  • 社区插件:zhin-plugin-<name> 或你自己的 scope(如 @yourname/zhin-plugin-<name>

带有 zhinzhin-plugin 关键词的包会在 npx zhin search 中被发现。

2. 编写 README

为你的插件编写清晰的 README,至少包括:

  • 插件功能描述
  • 安装方法
  • 配置说明
  • 使用示例
  • 可用命令列表

3. 发布到 npm

bash
# 发布插件
npx zhin pub

# 或使用标准 npm 命令
npm publish

# 带标签发布(如测试版)
npx zhin pub --tag beta

# 模拟发布(不实际上传)
npx zhin pub --dry-run

4. 验证发布

发布成功后,其他用户可以通过以下方式使用你的插件:

bash
# 搜索插件
npx zhin search my-plugin

# 查看插件信息
npx zhin info zhin-plugin-my-plugin

# 安装插件
npx zhin install zhin-plugin-my-plugin

插件开发最佳实践

✅ 推荐做法

  • 使用 TypeScript — 获得完整的类型提示和编译时检查
  • 声明类型扩展 — 让其他插件获得类型提示(declare module 'zhin.js'
  • 使用 useContext — 确保依赖的服务就绪后再使用
  • 返回清理函数 — 在 onDispose 中清理定时器、连接等资源
  • 使用 logger — 通过插件自带的 logger 输出日志,便于调试
  • 添加 addConfig — 让用户可以通过配置文件自定义插件行为

❌ 避免做法

  • 不要使用 any 类型 — 保持类型安全
  • 不要泄露资源 — 插件卸载时确保清理所有资源
  • 不要硬编码配置 — 使用 addConfig 或环境变量
  • 不要阻塞主线程 — 耗时操作使用 async/await

完整插件模板

typescript
import { usePlugin, MessageCommand } from 'zhin.js'

// 类型扩展
declare module 'zhin.js' {
  interface Models {
    my_data: { id: number; value: string }
  }
}

const {
  addCommand,
  addTool,
  addConfig,
  defineModel,
  useContext,
  onMounted,
  onDispose,
  logger
} = usePlugin()

// 注册配置
addConfig('my-plugin', {
  prefix: '!',
  maxResults: 10
})

// 定义数据模型
defineModel('my_data', {
  id: { type: 'integer', primary: true },
  value: { type: 'string' }
})

// 注册 AI 工具
addTool({
  name: 'my_tool',
  description: '我的工具',
  parameters: {},
  execute: async () => '工具执行结果'
})

// 生命周期
onMounted(() => {
  logger.info('插件已启动')
})

onDispose(() => {
  logger.info('插件已卸载')
})

// 使用数据库
useContext('database', (db) => {
  const model = db.models.get('my_data')

  addCommand(
    new MessageCommand('mycommand <value:string>')
      .desc('我的命令')
      .action(async (_, result) => {
        await model.insert({ value: result.params.value })
        return '✅ 完成'
      })
  )
})

常见问题

插件没有加载

  1. 确认插件文件在 src/plugins/ 目录下
  2. 确认插件已添加到 zhin.config.ymlplugins 列表中
  3. 查看终端日志是否有加载错误

热重载不生效

确保使用开发模式启动(pnpm dev),而不是生产模式(pnpm start)。

发布后用户搜索不到

确保 package.jsonkeywords 中包含 zhinzhin-plugin

类型提示不完整

使用 declare module 'zhin.js' 扩展类型声明,详见上方示例。

下一步

基于 MIT 许可发布