Skip to content

约定目录

在插件包根目录下放一个 commands/ 文件夹、往里丢一个 .ts 文件,命令就出现了——不用在任何地方注册。这组会被 Feature 发现机制自动扫描的目录就是约定目录:每个目录对应一个 Feature 包(feature provider),目录里的文件按命名规则映射为能力(capability)。发现流程:

几个要点。能力的完整 id 形如 owner\0feature\0localName\0 分隔),localName 由目录内的相对路径决定;同一 owner 下 localName(或同一文件来源)重复会抛 DiscoveryConflictError。目录不存在或没有匹配文件时该 Feature 静默跳过——插件只声明自己用到的目录就行。另外,target: server 的模块在 Node 侧加载执行,target: client(pages)则经构建产物在浏览器加载。

作者 import:依赖 zhin.js 的应用可从门面子路径导入 define*——zhin.js/commandzhin.js/middlewarezhin.js/handlerzhin.js/adapterzhin.js/component(以及主入口 zhin.jsdefinePlugin)。下表「Feature 包」是 platformFeatures 挂载的实现包名;Root 依赖 zhin.js / @zhin.js/core 时已自动继承。

zhin.features 与依赖声明@zhin.js/runtime ≥1.0.12):manifest 里引用的 Feature 包名必须出现在该插件的 dependencies / peerDependencies / optionalDependencies 之一,否则启动抛 PackageResolutionError。子插件对 Stable Feature(command / middleware / component / handler)应声明为 optional peerDependencies,由 Root 经 zhin.js 提供——不要pnpm adddependencies(避免重复安装实现包)。适配器的 @zhin.js/adapter、实验性 @zhin.js/tool 等仍按需装进 dependencies 或非 optional peer。

目录一览

目录文件形态递归targetFeature 包featureId默认导出
commands/.ts / .tsx,支持动态参数文件是(子目录拼层级)server@zhin.js/commandzhin.commanddefineCommand(...)
middlewares/.tsserver@zhin.js/middlewarezhin.middlewaredefineMiddleware(...)
handlers/.ts是(/ 分段;省略 event 时映为 . 事件名)server@zhin.js/handlerzhin.handlerdefineHandler(...)
components/.ts / .tsxserver@zhin.js/componentzhin.componentdefineComponent(...)
adapters/.tsserver@zhin.js/adapterzhin.adapterdefineAdapter(...)
tools/.tsserver@zhin.js/toolzhin.agent-tooldefineAgentTool(...)
agent/prompt-sections/.tsserver@zhin.js/prompt-sectionzhin.agent-prompt-sectiondefineAgentPromptSection(...)
skills/子目录 + SKILL.md一层server@zhin.js/skillzhin.skillMarkdown 文本
agents/*.agent.mdserver@zhin.js/agent-featurezhin.agentMarkdown 文本
mcp/.tsserver@zhin.js/mcp-featurezhin.mcpdefineMcp(...)
pages/.ts / .tsx,含 $nav / $footer 布局槽client@zhin.js/page / @zhin.js/layoutzhin.page / zhin.layout页面构件

命名规则

通用段规则:目录名、普通文件名去扩展名后,默认须匹配 ^[a-z0-9][a-z0-9-]*$(小写字母/数字开头、可含连字符)。不匹配的文件被跳过。

例外:commands/ 静态段还允许 Unicode 名(如 赞我.ts),规则与 isCapabilityLocalSegmentzhin.js)一致——ASCII kebab,或含非 ASCII 字母且无 ASCII 大写的 Unicode 标识;动态参数文件([name].ts 等)仍限 ASCII。tools/ 额外允许 ASCII snake(如 send_user_like.ts)。其它约定目录(middlewares / adapters / …)不放宽。

各目录的补充规则:

目录localName 推导示例
commands/子目录与文件名用 / 拼接;静态段可为 ASCII kebab 或 Unicode 名(如 赞我);动态参数文件用 Next.js 风格方括号声明形态并映射为 $name 段:[name].ts(x) 必需、[[name]].ts(x) 可选、[...name].ts(x) 捕获所有、[[...name]].ts(x) 可选捕获所有;类型与默认值在 defineCommand({ params }) 中声明commands/lottery-today.tslottery-todaycommands/赞我.ts赞我commands/lottery/[[game]].tslottery/$game
middlewares/相对路径去扩展名,/ 拼接middlewares/keyword-reply.tskeyword-reply
handlers/相对路径去扩展名,/ 拼接为 capability localName;省略 event 时把 / 映成 . 作为 Lifecycle 事件名handlers/message/receive.ts → localName message/receive → event message.receive
components/相对路径去扩展名,/ 拼接components/share-music.tsshare-music
adapters/同上adapters/napcat.tsnapcat
tools/文件名去扩展名(不递归子目录);ASCII kebab 或 snaketools/music-search.tsmusic-searchtools/send_user_like.tssend_user_like
agent/prompt-sections/相对路径去扩展名,/ 拼接agent/prompt-sections/project/rules.tsproject/rules
skills/子目录名即 localName,目录内必须含 SKILL.mdskills/memory-consolidate/SKILL.mdmemory-consolidate
agents/文件名去掉 .agent.md 后缀agents/planner.agent.mdplanner
mcp/文件名去扩展名(不递归)mcp/my-server.tsmy-server
pages/文件名去扩展名;$nav.tsx / $footer.tsx 是布局槽(同 slot 同时有 .ts.tsx 时以 .tsx 为准)pages/workroom.tsxworkroompages/$nav.tsxnav

命令动态参数文件的方括号语法写错会抛 CommandPathSyntaxError,提示 expected [name].ts(x), [[name]].ts(x), [...name].ts(x) or [[...name]].ts(x);有默认值时文件名必须用双方括号,且 params 中必须声明对应参数,否则同样报错。

各目录的最小形态

commands/ — defineCommand

ts
// plugins/utils/lottery/commands/lottery-today.ts
import { defineCommand } from 'zhin.js/command';

export default defineCommand<LotteryConfig>({
  description: 'Show today published recommendation report',
  async execute({ use }) {
    const { db } = use(lotteryRuntimeToken);
    // …返回字符串即回复
  },
});

middlewares/ — defineMiddleware

ts
// plugins/utils/group-suite/middlewares/keyword-reply.ts(节选)
import { defineMiddleware } from 'zhin.js/middleware';

export default defineMiddleware<Message, GroupSuiteConfig>({
  target: 'inbound',
  async handle(context, next) {
    const config = resolveGroupSuiteConfig(context.config);
    if (!config.keywordReply) {
      await next();
      return;
    }
    // …命中关键词则回复,否则 await next() 放行
  },
});

handlers/ — defineHandler

Lifecycle 事件名 注册监听器(无 next() 链)。目录路径用 / 作为 capability localName;省略 event 时把路径中的 / 映成 . 得到事件名(如 handlers/notice/receive.tsnotice.receive)。从 @zhin.js/core/feature/handler 导入时,Plugin.Lifecycle 已并入 HandlerEventMap,写 event: 'message.receive' 时参数类型可推断。

依赖 zhin.js / @zhin.js/core 的 Root 会经由 platformFeatures 挂载 @zhin.js/handler,无需再单独声明或安装。ImRuntime 会分发:

  • message.receive(消息入站,命令/中间件之前)
  • notice.receive / request.receive / system.receive(适配器经 sideEventGatewayToken 上报)

Handler 的 thisHandlerContext

  • this.interaction:用户输入、确认与选择(与命令 UserInteraction 同源;侧事件按场景通道合成)

事件上的 $endpoint 是不可变 identity。Handler 不暴露可保存的 live Endpoint;发送、 审批与交互必须走 generation-bound port,避免热切换后继续操作已退役资源。

middlewares/ 的分工:需要 await next() 的有序入/出站链用 middleware;只需在某事件上 fire-and-forget 处理用 handler。

ts
// handlers/message/receive.ts
import { defineHandler } from 'zhin.js/handler';

export default defineHandler({
  // 可省略:文件路径已推导出 message.receive
  event: 'message.receive',
  async handle(message) {
    await this.interaction?.ask({ type: 'text', title: '继续?' });
  },
});
ts
// handlers/request/receive.ts
import { defineHandler } from 'zhin.js/handler';

export default defineHandler({
  event: 'request.receive',
  async handle(req) {
    if (await this.interaction?.ask({ type: 'confirm', title: '同意该请求?' })) await req.$approve();
  },
});
ts
// handlers/system/receive.ts — 登录扫码等
import { defineHandler } from 'zhin.js/handler';

export default defineHandler({
  event: 'system.receive',
  async handle(ev) {
    if (ev.$sub_type !== 'qrcode') return;
    await this.interaction?.ask({ type: 'text', title: '扫码完成后回复 done' });
  },
});

也可在 setup 里用 addHandler(localName, defineHandler(...)),与目录发现进入同一 HandlerIndex

adapters/ — defineAdapter

ts
// plugins/adapters/napcat/adapters/napcat.ts(节选)
import { defineAdapter } from 'zhin.js/adapter';

export default defineAdapter<NapCatAdapterConfig>({
  capabilities: ['inbound', 'outbound'],
  create(context) {
    const config = resolveNapCatConfig(context.config);
    const gateway = context.use(outboundMessageToken);
    return new NapCatWsEndpoint({ id: context.id, gateway, config });
  },
});

capabilities 至少含 inbound / outbound 之一;create 返回的 Endpoint 生命周期见 WS/SSE 端点生命周期

tools/ — defineAgentTool

ts
// plugins/utils/music/tools/music-search.ts(节选)
import { defineAgentTool } from '@zhin.js/tool';

export default defineAgentTool<{ keyword: string; source?: MusicSource; limit?: number }>({
  description: '搜索音乐并返回结果列表',
  inputSchema: {
    type: 'object',
    properties: { keyword: { type: 'string', description: '搜索关键词' } },
    required: ['keyword'],
  },
  approval: 'never',
  execute: ({ keyword, source, limit }) => searchMusic(String(keyword), source, limit ?? 5),
});

skills/ 与 agents/ — Markdown

skills/<name>/SKILL.md 带 frontmatter(name / description / tools 白名单等),如 examples/full-bot/skills/memory-consolidate/SKILL.md

markdown
---
name: memory-consolidate
description: 回合末或 master 说「记住」时,将 1–3 条可检索事实写入 memory_entries
tools:
  - memory_upsert
  - memory_search
---

agents/<name>.agent.md 是 Agent 人格/指令文件,如 examples/multi-agent-room/agents/planner.agent.md

pages/ — Console 页面

pages/*.tsx 编译为浏览器产物,挂进 Remote Console;examples/full-bot/pages/workroom.tsx 是现成例子。$nav.tsx / $footer.tsx@zhin.js/layout 消费,注入导航与页脚。

仓库实例

想找生产级参照时,直接翻这些目录:commandsplugins/utils/lottery/commands/(含动态参数 lottery/[[game]].ts);middlewaresplugins/utils/group-suite/middlewares/plugins/games/*/middlewares/handlershandlers/message/receive.ts + defineHandler(见上文最小形态;仓库内示例可按需自加);componentsplugins/utils/music/components/share-music.tsadaptersplugins/adapters/napcat/adapters/napcat.tstoolsplugins/utils/music/tools/plugins/utils/group-suite/tools/skillsexamples/full-bot/skills/memory-consolidate/agentsexamples/multi-agent-room/agents/pagesexamples/full-bot/pages/workroom.tsx