Skip to content

definePlugin

声明一个插件不需要继承基类,也不用往任何注册表里手工挂条目——默认导出 definePlugin(...)(来自 @zhin.js/plugin-runtime)的返回值就够了。这份返回值叫 PluginDefinition:名字、元数据、依赖声明,外加一个 setup(context) 装配函数。

ts
import { definePlugin } from '@zhin.js/plugin-runtime';

export default definePlugin<MyConfig>({
  name: 'my-plugin',
  metadata: { displayName: 'My Plugin', icon: 'Blocks', order: 10 },
  requires: [databaseHostToken],
  async setup(context) {
    // 装配:注册命令之外的运行时资源(表、cron、Agent 工具、出站推送……)
    return () => { /* generation 结束时执行 */ };
  },
});

几个硬约束先记住。name 必填,必须匹配 ^[a-z][a-z0-9-]*$,否则 definePlugin 直接抛 TypeError;返回的 definition 被 Object.freeze,不可再改。setup 可同步、可 async,可选地返回一个 Dispose,在当前代(generation)结束时执行。

从零起步的完整教程见 编写第一个插件;插件模型概念见 插件模型

setup context 五个成员

setup 收到的 PluginSetupContext<TConfig> 只有五个成员,全部只读:

成员类型作用
pluginPluginInstanceView实例视图:id / instanceKey / parent / root / role'root' | 'child')。多实例部署时按 instanceKey 隔离
configConfigView<TConfig>配置视图,config.get() 返回只读配置。默认值来自插件包 schema.json,由 zhin.config.ymlplugin: 段(Root 自身)或 plugins.<key> 段覆盖
resourcesScope资源作用域:has(token) / use(token) 解析 Host token,provide(token, value, dispose?) 向子作用域发布资源
lifecycleDisposeStack代的回收栈:lifecycle.add(dispose) 登记的清理函数在代结束时按逆序执行
handoffGenerationHandoffRegistry代际交接注册表:handoff.add(participant) 参与热重载事务(见下文「代际交接」)

看一段真实装配就清楚了(examples/capabilities-bot/plugin.ts,节选):

ts
async setup(context) {
  const { instanceKey } = context.plugin;            // ① 实例视图
  const config = context.config.get();               // ② 配置视图
  if (context.resources.has(databaseHostToken)) {    // ③ 资源作用域
    const db = context.resources.use(databaseHostToken);
    db.define('showcase_counter', { /* … */ });
  }
  context.lifecycle.add(schedule.register({ /* … */ })); // ④ 生命周期回收
  return () => console.log('disposed');              // ⑤ setup 返回 Dispose
}

Scope 解析规则

resources 是一条父子链:use(token) 先查本作用域,未命中则向父作用域递归;整条链都没有则抛 Missing resource 错误。可选能力一律先 has(token)use(token),缺失时自行降级。

metadata 与 requires

metadata 的三个字段全部服务于 Remote Console 的插件卡片(/api/plugins):displayName 是展示名,icon 是图标名,order 是排序权重。都不影响运行时行为。

requires 声明硬依赖的 Host token 数组,缺失即拒绝启动——与 has() + 降级的软依赖路径互为补充:

ts
// 硬依赖:没有数据库就不启动
export default definePlugin({
  name: 'my-plugin',
  requires: [databaseHostToken],
  // …
});

Host token

Host token 是 Host 提供给插件的能力句柄,setup 里通过 context.resources 解析。CLI Host 启动时自动装配,未装配的 token 用 has() 判空降级。前六个从 @zhin.js/plugin-runtime 导出,httpHostToken@zhin.js/host-http 导出。

tokentoken id提供条件关键方法
databaseHostTokenzhin.database.host配置了 database:define(name, columns) 注册表;models.get(name) 取模型(select / insert / update / delete / count);start() 由 Host 在代激活时调用,插件不自己调
scheduleHostTokenzhin.schedule.host始终可用register(job) 注册 6 段 solar cron(秒 分 时 日 月 周),返回取消函数;list() 列出任务
outboundHostTokenzhin.outbound.host有可用 Adaptersend(input) 主动推送(返回平台消息 id 或 null);可选 addReaction / removeReaction / recall
agentToolsHostTokenzhin.agent-tools.host安装并启用 AI(Agent Host)register(tool) 注册 Agent 工具,返回注销函数
htmlRendererTokenzhin.html-renderer.host安装了 @zhin.js/html-rendererrender(html, { width, format, backgroundColor }) → PNG(Buffer)或 SVG(string);未安装时必须降级为纯文本
runtimeEventPublisherTokenzhin.runtime.event-publisherRoot 级,CLI console 装配publish(type, data) 向 Console SSE hub 广播事件(适配器用来推 endpoint:request / endpoint:notice 等)
httpHostTokenzhin.host.httpHTTP Host 启用route(method, path, handler, meta?) 注册 HTTP 路由;ws(path).onConnection(cb) 注册 WS 端点;listen() / close() 由 Host 管理

注意这两段典型用法的共同点:先 has() 守卫,再把返回的注销函数挂进 lifecycle——热重载时自动回收。

ts
// 定时任务:dispose 挂 lifecycle,热重载安全回收
if (config.heartbeatCron && context.resources.has(scheduleHostToken)) {
  const schedule = context.resources.use(scheduleHostToken);
  context.lifecycle.add(schedule.register({
    id: 'capabilities-bot/heartbeat',
    cron: config.heartbeatCron,
    description: 'Showcase heartbeat',
    execute: () => log('heartbeat ♥'),
  }));
}

// Agent 工具:装了 Agent Host 才存在,未装静默跳过
if (context.resources.has(agentToolsHostToken)) {
  const agentTools = context.resources.use(agentToolsHostToken);
  context.lifecycle.add(agentTools.register({
    name: 'showcase_greet',
    description: 'Return the configured greeting for a name',
    source: 'capabilities-bot',
    inputSchema: {
      type: 'object',
      properties: { name: { type: 'string' } },
      required: ['name'],
    },
    execute: (input) => `${config.greeting},${String(input.name ?? 'world')}!`,
  }));
}

AgentToolRegistration 的常用字段:name(模型可见的工具名)、descriptioninputSchema(zod 或 JSON Schema)、platforms / scopes / permissions(按消息维度限制可见性)、hidden(只按名调用、不进入工具目录)、approval'never' | 'always',与执行策略叠加)、source(来源标注)。

代际交接(handoff)

热重载是一次「代」事务:新一代装配完成后,旧代静默、新代激活、失败则回滚。context.handoff.add(participant) 注册参与者,可实现的钩子:

钩子时机
quiescePrevious(previous)旧代静默(如暂停接收新事件)
activateNext()新代激活(如启动 endpoint、启动 cron 后的首个动作)
deactivateNext()激活失败时回滚新代
resumePrevious()回滚后恢复旧代
openNext()事务提交后开放准入(如开始收消息)

capabilities-bot 用它解决启动时序竞争——endpoint 就绪后再推上线消息:

ts
context.handoff.add({
  activateNext: async () => {
    await outbound.send({ ...target, content: `${config.greeting},capabilities-bot 已上线` });
  },
});

真实示例

  • capabilities-botplugin.ts 一个 setup() 调动全部常用 Host 面(database / schedule / agent-tools / outbound / handoff),每一项都配 has() 降级,是本文所有代码片段的来源。
  • lotteryplugins/utils/lottery/plugin.ts):生产级插件——数据库优先用 databaseHostToken、缺省落内存实现;provide 自有 token 给命令复用;Agent 工具走 await import() 惰性加载,保证 IM-only 安装不引入 @zhin.js/agent;cron 每日流水线。

lottery 的装配骨架值得抄:

ts
// plugins/utils/lottery/plugin.ts(节选)
async setup(context) {
  const config = resolveLotteryConfig(context.config.get());
  const db = context.resources.has(databaseHostToken)
    ? context.resources.use(databaseHostToken)
    : createInMemoryLotteryDb();
  if (context.resources.has(databaseHostToken)) {
    defineLotteryTables(db);
  }
  context.resources.provide(lotteryRuntimeToken, { db });

  // Agent 工具惰性加载:IM-only 安装不含 @zhin.js/agent
  if (context.resources.has(agentToolsHostToken)) {
    const agentTools = context.resources.use(agentToolsHostToken);
    const { registerLotteryAgentTools } = await import('./agent/runtime-tools.js');
    context.lifecycle.add(registerLotteryAgentTools(agentTools));
  }
  // …cron 注册见 scheduleHostToken 一节
}

package.json zhin 字段

插件包(以及 feature 包)用 package.json 顶层的 zhin 字段声明清单,取代旧的 plugin.yml。解析与强校验在 @zhin.js/runtimepackages/im/runtime/src/manifest.ts),非法清单直接抛 ManifestValidationError

jsonc
{
  "zhin": {
    "protocol": 1,                    // 必填,目前恒为 1
    "type": "plugin",                 // 必填:"plugin" | "feature"
    "entry": "./plugin.ts",           // 必填:包相对路径,须 ./ 开头、不得 .. 逃逸
    "engine": "^1.0.0",               // 可选:对 Runtime engine 版本的 semver 要求,不满足拒绝加载
    "runtime": "trusted",             // 可选(仅 plugin):"trusted"(默认)| "isolated"
    "platformFeatures": true,         // 可选(仅 plugin):默认 true,Root 插件自动获得官方 Stable Features;设 false 退出
    "features": [                     // 可选(仅 plugin):依赖的 Feature 包清单,缺省 []
      { "package": "@zhin.js/command", "api": "^1.0.0", "optional": false }
    ],
    "plugins": [                      // 可选(仅 plugin):挂载的子插件实例清单,缺省 []
      { "package": "@zhin.js/adapter-icqq", "instanceKey": "icqq" }
    ]
  }
}

字段细则:

字段必填含义
protocol清单协议版本,当前必须为 1
typeplugin(可挂载子插件、可声明 features/runtime)或 feature(平台能力提供者)
entry插件/功能入口文件,包相对路径,./ 开头且不得含 ..
enginesemver range,对 Runtime 提供的 engine 版本做 satisfies 校验
runtime否(仅 plugin)isolated 表示在隔离运行时中执行:仅限子插件(Root 不可用)、不得挂载 Host Feature,且需要 isolation adapter 支持
platformFeatures否(仅 plugin)默认 true:Root 插件即使不声明也会获得官方 Stable Features(@zhin.js/adaptercommandcomponent
features否(仅 plugin)依赖的 Feature 包:package 支持 npm 包名或 ./ 相对路径(monorepo 本地);api 是对该 Feature 声明的 featureApi 版本的 semver 要求;optional 标记缺失可降级
plugins否(仅 plugin)挂载子插件实例:packagefeatures 的包名规则;instanceKey 必填,匹配 ^[a-z0-9][a-z0-9-]*$,是实例隔离与配置的键——同一包挂多个实例就靠不同 instanceKey,实例配置写在 app 的 zhin.config.ymlplugins.<instanceKey>

feature 包(type: "feature")的清单只有 protocol / type / entry / engine / featureApi 五个字段,其中 featureApi(可选)声明本 feature 实现的 API 版本,供消费方 features[].api 校验。

下一步