Skip to content

命令 ​

要改反馈文案或整个菜单布局,见定制内置展示。

命令是用户输入 /hello、/remind 10 喝水 这类消息时执行的函数。本页从最简单的命令开始,逐步加上参数、选项、权限和富文本回复。

本页的 remind 是命令开发示例。需要直接使用持久提醒及补问流程,请看不启用 AI,也能设置提醒。

最简单的命令 ​

在插件根目录创建 commands/hello/index.ts:

ts
// commands/hello/index.ts
import { defineCommand } from 'zhin.js/command';

export default defineCommand({
  description: '打个招呼',
  execute: () => 'Hello from zhin.',
});

保存后发送 /hello,Bot 回复 Hello from zhin.。

  • 文件路径就是命令名,只有 index.ts(或 index.tsx)是命令入口,同目录的其他文件可作为辅助模块。
  • execute 的返回值就是回复。
  • description 会出现在命令列表和帮助里,建议都写上。
单文件写法

不想建目录时,可以在 plugin.ts 里注册同一个定义,效果完全相同:

ts
import { definePlugin } from 'zhin.js';
import { defineCommand } from 'zhin.js/command';

export default definePlugin({
  name: 'my-bot',
  setup({ addCommand }) {
    addCommand('hello', defineCommand({
      description: '打个招呼',
      execute: () => 'Hello from one file.',
    }));
  },
});

两种写法共用同一套命令表和冲突检测。命令变多后移到 commands/ 目录,修改单个命令时只会重载这一个文件。

常见需求 ​

接收参数 ​

在 params 里按顺序声明参数,execute 收到的 params 会自动带上正确的类型:

ts
// commands/remind/index.ts  →  /remind 10 喝水
import { defineCommand } from 'zhin.js/command';

export default defineCommand({
  description: '创建提醒',
  params: {
    minutes: { type: 'integer', min: 1, max: 1440, description: '分钟数' },
    note: { type: 'text', default: '休息一下', description: '提醒内容' },
  },
  execute({ params }) {
    // params.minutes: number,params.note: string
    return `好的,${params.minutes} 分钟后提醒你:${params.note}`;
  },
});
  • 用户输入 /remind 10 喝水:minutes 是 10,note 是 喝水。
  • 只输入 /remind 10:note 使用默认值 休息一下。
  • 输入 /remind abc:Bot 自动回复参数错误提示,execute 不会执行。

常用的约束:

写法作用
default: …未输入时的默认值
optional: true可以不输入,此时值为 undefined
min / max数值大小或文本长度的范围
choices: [...]只允许这些值
pattern用正则检查文本
validate(value, session)自定义校验,返回 true、false 或错误提示
rest: true收集剩余的所有输入为数组,必须是最后一个参数

全部参数类型见 参考 · 参数类型。

加选项 ​

选项是 --count 2、-q 这类可以出现在任意位置的开关:

ts
export default defineCommand({
  description: '创建提醒',
  params: {
    minutes: { type: 'integer', min: 1 },
  },
  options: {
    count: { type: 'integer', alias: 'c', default: 1, min: 1, max: 10 },
    quiet: { type: 'boolean', default: false },
  },
  execute({ params, options }) {
    // options.count: number,options.quiet: boolean
    if (options.quiet) return;                  // 不回复
    return `提醒 ${options.count} 次`;
  },
});

--count 2、--count=2、-c 2 三种写法等价;--quiet 表示 true,--quiet=false 表示 false。

自动生成的帮助 ​

只要声明了 params 或 options,用户发送 /remind --help 就会看到根据声明生成的帮助,其中会显示当前平台的命令前缀。

起别名和快捷方式 ​

ts
export default defineCommand({
  description: '点赞',
  alias: ['zan'],                         // /zan 等价于本命令
  shortcut: { '赞满': { count: 10 } },     // 用户整句发"赞满"时,相当于传入 count=10
  params: { count: { type: 'integer', default: 1 } },
  execute: ({ params }) => `点赞 ${params.count} 次`,
});

别名可以是多个词(如 'gh issue')。快捷方式要求消息去掉首尾空白后与键完全相同才会触发。别名、快捷方式与正式命令名之间不能重名,冲突会在启动时报错。

限制谁能使用 ​

用 permit 声明规则,不满足时 Bot 拒绝执行:

ts
export default defineCommand({
  description: '清空数据',
  permit: ['role(master)'],              // 仅 master 可用
  execute: () => '已清空',
});
规则含义
role(master) / role(trusted)发送者角色
adapter(icqq)只在某个平台上可用
group() / private() / channel()限制会话类型,空值或 * 不限该类型的会话 ID
group(123) / private(…) / channel(…) / user(…)比较相应会话或发送者 ID;不限制完整账号、父容器或话题

数组中的多条规则同时满足才通过;一条规则内用逗号分隔表示满足其一即可。没有权限的用户也看不到这个命令的帮助。

内置角色判断只让 master 隐含 trusted,不会额外推导 user、admin 或 owner。角色来自当前真实消息;适配器配置可以指定谁是 master:

yaml
configVersion: 2
# zhin.config.yml
plugins:
  qq: "@zhin.js/adapter-qq"
qq:
  master: '10001' # 整个适配器的 master
  endpoints:
    - name: main
      appid: ${QQ_APPID}
      master: '10001' # 也可以给单个账号单独设置

需要逐项理解规则时,可在Console 命令权限说明中查看内置条件或填写假设身份。它不执行平台 checker,也不授予权限;adapter(...) 与会话 ID 规则不能当作完整会话白名单。

在中间件或事件处理中需要同样的判断时,使用 checkPermissions:

ts
import { checkPermissions } from 'zhin.js';

if (!(await checkPermissions(context, ['group(team)', 'role(trusted)']))) return;

它复用同一套权限规则,但不替代 AI 工具的审批或执行安全策略。

限制重复执行与提示耗时任务 ​

给昂贵命令单独声明 execution,未声明的命令保持原行为。以下是可直接放入 commands/slow/index.ts 的演示:

ts
import { setTimeout as delay } from 'node:timers/promises';
import { defineCommand } from 'zhin.js/command';

export default defineCommand({
  description: '演示有冷却和并发限制的耗时任务',
  execution: { cooldownMs: 10_000, maxConcurrent: 1, progressAfterMs: 1_500 },
  async execute({ signal }) {
    await delay(3_000, undefined, { signal });
    return '演示任务已完成';
  },
});

由真实用户在同一完整会话发送 slow,约 1.5 秒后可收到一次处理中提示,业务结束后收到结果。按账号实际前缀与插件命名空间调整命令;这是本地延时演示,不调用业务服务。短任务结束前会取消尚未触发的提示;开始最终回复前等待已开始的提示,避免最终结果先到、处理中提示后到。

额度按指令 capability、真实发送者与完整会话隔离,包括 Endpoint、会话类型和 ID、父容器与话题。别名与快捷方式共用额度;它不是整个插件或所有用户共享的并发上限。

  • cooldownMs:接纳业务执行时开始计时,失败和取消仍消耗这次冷却;帮助、参数无效、权限拒绝与准入拒绝不消耗。上限一天。
  • maxConcurrent:占用持续到该次业务、渲染、出站中间件及最终发送实际结束。热更新或退役发出取消信号不会提前释放仍在运行的任务;不同运行代沿用同一额度。上限 1000。
  • progressAfterMs:默认不发提示,显式设置后每次处理对该指令最多提示一次。提示仅在业务等待阶段启动,经原会话的统一回复链发送;失败或结果不确定不重试。上限一小时。

三个值均为正整数,execution 至少声明一项。冷却拒绝包含记录时的剩余时间;并发已满无法预知何时结束,不伪造等待倒计时。诊断状态为 limited,与 denied 权限拒绝及平台投递失败分开,也不会转交 Agent 再执行。

启用策略需要 Core 的真实入站操作和完整身份;脱离该操作的直接 CommandIndex 调用不能伪造来源绕过检查。额外回复仍应 await input.$reply(...),任意未来定时器里启动的工作不属于已经结束的处理。冷却不替代幂等、权限或投递对账,不能用于自动重试 unknown,也不会为不支持取消的操作生成取消按钮。

回复图片、卡片或多条消息 ​

execute 可以返回:

返回值效果
字符串文本回复
JSX(<card>、<html><Card>...</Card></html>、<button onClick> 等)直接按平台能力发送富消息、图片卡片或交互按钮
component('status-card', props)渲染一个组件,支持图片的平台会显示为图片
raw({ type: 'html', data: { html, width } })原样发送的平台消息段
由以上值组成的数组多段消息
undefined不回复(例如你已经用 input.$reply() 自己回复过了)

把命令文件保存为 .tsx(如 commands/poll/index.tsx),就可以直接返回 JSX,包括平台原生 <card>、可视化图片卡片 <html><Card>...</Card></html>,以及带回调的 <button onClick>(支持原生按钮的平台渲染为按钮,纯文本平台自动降级为编号回复):

tsx
// commands/poll/index.tsx
import { defineCommand } from 'zhin.js/command';
import { Card, CardHeader, Badge } from '@zhin.js/components';

export default defineCommand({
  description: '发起发布确认投票',
  execute: () => (
    <>
      <card title="版本发布确认" content="是否立即发布 v1.1.0?" />
      <html>
        <Card>
          <CardHeader title={<strong>发布检查单</strong>} badge={<Badge>就绪</Badge>} />
          <p>构建与单元测试均已通过。</p>
        </Card>
      </html>
      <button onClick={async (clickMsg) => `已由 ${clickMsg.sender?.name ?? clickMsg.sender?.id} 确认!`}>
        确认
      </button>
    </>
  ),
});

也可以按名字调用已注册的组件:

ts
// commands/card/index.ts
import { defineCommand } from 'zhin.js/command';
import { component } from 'zhin.js/component';

export default defineCommand({
  description: '状态卡片',
  execute: () => component('status-card', { title: 'my-bot', lines: [] }),
});

多轮交互追问(context.interaction) ​

命令执行到一半需要向用户追问确认、文本、数字或单选/多选(type: 'confirm' | 'select' | 'text' | 'number',单选也可搭配 interaction.select() / pick 风格选项)时,直接使用 execute 参数里的 interaction:

ts
// commands/deploy/index.ts
import { defineCommand } from 'zhin.js/command';

export default defineCommand({
  description: '交互式发布向导',
  async execute({ interaction }) {
    if (!interaction) return '当前环境不支持多轮交互';

    const env = await interaction.ask({
      type: 'select',
      title: '请选择目标环境:',
      options: [
        { label: '预发环境 (staging)', value: 'staging' },
        { label: '生产环境 (prod)', value: 'prod' },
      ],
    });

    const replicas = await interaction.ask({
      type: 'number',
      title: '请输入实例副本数(1-10):',
      min: 1,
      max: 10,
       integer: true,
      default: 2,
    });

    const notes = await interaction.ask({
      type: 'text',
      title: '请输入本次变更说明:',
      default: '常规维护更新',
    });

    const confirmed = await interaction.ask({
      type: 'confirm',
      title: `确认发布到 ${env}(${replicas} 个副本,说明:${notes})?`,
      default: false,
    });

    return confirmed ? `已开始发布到 ${env}(${replicas} 副本)` : '已取消发布';
  },
});

知道是谁、在哪里发的 ​

ts
export default defineCommand({
  description: '谁在哪',
  execute: ({ adapter, endpoint, scene, sender }) =>
    `${sender?.name ?? sender?.id} @ ${scene?.type}:${scene?.id} via ${adapter}/${endpoint} roles=${sender?.role.join(',')}`,
});

sender 是发送者(含角色),scene 是群 / 私聊 / 频道,adapter 和 endpoint 是消息来自的平台和账号。全部字段见 参考 · execute 参数。

使用插件里的服务 ​

在 plugin.ts 的 setup 里用 context.resources.provide(token, value) 提供的服务,命令里用 use(token) 取出:

ts
export default defineCommand({
  description: '查看今日推荐',
  async execute({ use }) {
    const { db } = use(lotteryRuntimeToken);
    // ……查询并返回
  },
});

找不到对应服务时 use 会抛错。参见 definePlugin · 把资源提供给自己的命令和工具。

给 config 加类型 ​

约定目录里的命令不知道自己属于哪个插件,config 默认是 unknown。用 defineCommand.withConfig 先固定配置类型,再定义命令:

ts
import { defineCommand } from 'zhin.js/command';
import type { EchoConfig } from '../../../src/index.js'; // 与 schema.json 保持一致

export default defineCommand.withConfig<EchoConfig>()({
  params: { text: { type: 'string' } },
  execute({ params, config }) {
    return `${config.prefix ?? ''}${params.text}`; // params.text: string,config: EchoConfig
  },
});

defineCommand.withConfig<EchoConfig>() 返回的函数可以复用:在 src/ 里写一次 export const defineEchoCommand = defineCommand.withConfig<EchoConfig>(),各个命令文件直接调用它即可。

旧写法 defineCommand<EchoConfig>({...}) 仍然有效,但 TypeScript 不支持只写一部分类型参数、其余自动推断,params 会退化成 Record<string, CommandParameterValue>。需要 params / options 推断时请用 withConfig。

子命令 ​

目录嵌套就是子命令,目录段之间用空格连接:

文件命令
commands/hello/index.tshello
commands/qq/endpoint/list/index.tsqq endpoint list
commands/qq/endpoint/add/[[name]]/index.tsqq endpoint add [name]

插件名不会自动加到命令前面。多个插件的命令重名时,可以给其中一个插件配置 commandNamespace: admin,它的所有命令都会变成 admin xxx。

如果只是个别命令和别的插件重复,可以在该插件实例的配置里用 disabledCommands 把它们去掉,其余命令保持原名:

yaml
plugins:
  60s: "@zhin.js/plugin-60s"
60s:
  disabledCommands: [fanyi, weather]   # 写插件内的命令路径,不含 commandNamespace 和参数

写上级路径(如 netease)会连同 netease login、netease status 等子命令一起去掉。被去掉的命令不进路由表,也不占用路由,别的插件可以使用同名命令;没有匹配到任何命令的条目会被忽略。

用目录声明参数 ​

参数也可以写成路径的最后一段,风格与 Next.js 相同。类型和默认值仍然在 params 里声明:

目录含义帮助中显示params 声明
[name]/index.ts必填参数<name>params: { name: { type: 'string' } }
[[name]]/index.ts可选参数[name]params: { name: { type: 'string', default: '' } }
[...name]/index.ts收集剩余所有输入<...name>params: { name: { type: 'text' } },值为数组
[[...name]]/index.ts可选的收集剩余输入[...name]同上,未输入时为空数组

一般更推荐在 commands/remind/index.ts 里声明多个参数,而不是为每个参数建一层目录。目录参数适合"命令名之后紧跟一个对象名"的场景,例如 qq endpoint remove <name>。规则细节见 参考 · 目录参数规则。

接收图片、@ 等非文本参数 ​

参数类型可以是 image、mention 等消息段:

ts
// commands/upload/[asset]/index.ts  →  /upload [图片] 说明文字
import { defineCommand } from 'zhin.js/command';

export default defineCommand({
  params: {
    asset: { type: 'image', description: '要上传的图片' },
  },
  execute: ({ params, args, segments }) => ({
    uploaded: params.asset,          // 图片的 MediaRef
    captionWords: args,              // 剩余文字,按空白切分
    remainingSegments: segments,     // 剩余内容,保留图片、@ 等结构
  }),
});

框架直接在消息段上匹配,不会先把图片、@ 转成文字再解析。这类参数不支持默认值。

设置命令前缀 ​

默认没有前缀:任何消息都会尝试匹配命令。给某个平台连接配置 commandPrefix 后,只有以它开头的消息才会被当作命令,其余消息会进入普通聊天或 AI:

yaml
configVersion: 2
plugins:
  qq: "@zhin.js/adapter-qq"
qq:
  commandPrefix: '/' # 只有 "/qq endpoint list" 这样的消息才会触发命令
  endpoints:
    - name: main
      commandPrefix: '' # 可为单个账号单独覆盖

npm create zhin-app 生成的 Sandbox 已设置为 /。

查看已加载的指令菜单 ​

启用 Command Feature 后,内置菜单开箱即响应 菜单、帮助、help、menu(以及带前缀的 /help、/menu 或 @Bot 菜单),不需要另装帮助插件。当命令总数 ≤ 8 时,首页会自动扁平化展示根命令,方便一眼看完;超过 8 条时按插件分层分页展示。当前账号配置了 commandPrefix: '/' 时,也可以加 / 发送:

消息(无前缀时)返回内容
菜单 / 帮助 / help / menu根插件的指令、说明和直接子插件列表(总数 ≤ 8 时自动展开根指令)
菜单 rss实例键为 rss 的插件所拥有的指令和直接子插件
菜单 foo.bar嵌套插件 foo 下的 bar 的指令和子插件
菜单 --page 2根菜单的第 2 页
菜单 rss --page 2rss 实例菜单的第 2 页
remind --help某条命令的参数、选项和用法;不会执行该命令

这里的插件名是实例键,不是 npm 包名;先发 菜单,再按返回的子插件列表继续查看。

可以在项目根 zhin.config.yml 的顶层自定义菜单主关键词(与 plugins 同级):

yaml
menuKeyword: 帮助

menuKeyword: false 或 menuKeyword: '' 关闭内置菜单。

安装 @zhin.js/html-renderer 后,菜单通过统一发送链输出 HTML 样式卡片:图片平台渲染为菜单图,原生 HTML 平台遵循自身声明的策略。渲染失败、平台要求纯文本或不能接收生成的图片时,保留原菜单文本;未安装渲染器时仍返回文本。插件浏览、命令前缀和自定义关键词保持一致。

每页最多 30 项,指令与子插件合计。菜单显示总数、当前页和可以直接发送的上一页/下一页消息,沿用当前前缀和关键词。文本降级也只返回同一页,不会一次发送所有分页。页码必须为正整数;超出范围时提示实际总页数。图片中的长名称和说明最多显示两行,完整的指令声明仍可通过 --help 查看。

当端点同时声明支持原生 Markdown 和原生按钮时,多页菜单还会附上 Markdown 页码和“上一页 / 下一页”按钮:第一页禁用上一页,最后一页禁用下一页,只有一页时不显示翻页控件。点击后通过统一回复链发送目标页,保留自定义关键词、前缀和插件路径;回调只在原会话和当前 generation 内有效,过期或旧代按钮不能调用旧处理器。缺少任意一项能力的端点继续使用文本导航。Telegram 的禁用按钮映射需要 Bot API 10.3+,见 DisabledButton。

默认背景插画、装饰和图标均内置,不需要下载外部素材。项目根配置可自定义展示:

yaml
menuStyle:
  title: 知音 · 指令指南
  subtitle: 在日常里,发现一点不一样。
  accentColor: '#e7c993'
  # 可选:换成自己的背景图;省略则使用内置山月插画。
  backgroundImage: https://example.com/my-menu.webp

backgroundImage 支持 HTTP(S) 图片地址或 PNG/JPEG/WebP 的 base64 data URL,不支持文件路径或可执行 CSS。远程背景需要 Bot 所在机器能访问,其可用性与渲染器本身独立;离线使用自定义图片时可填写 data URL。accentColor 接受六位十六进制颜色,不合法的可选值使用默认样式。这些配置只改变图片展示,不改变文本菜单。

平台类型与单次 Session ​

在项目统一声明入口加载已安装的适配器类型,业务命令不需要引入 Adapter 类。例如项目 tsconfig.json 的 compilerOptions.types 可包含 "node"、"@zhin.js/adapter-icqq";保留原有类型条目。仅安装 npm 包并不会让 TypeScript 自动扫描它。

ts
import { defineCommand } from 'zhin.js/command';

export default defineCommand({
  adapter: 'icqq',
  params: { text: { type: 'text' } },
  options: { silent: { type: 'boolean', default: false } },
  async execute({ session, params, options }) {
    if (!session) return;
    session.adapter;  // IcqqAdapter
    session.endpoint; // IcqqEndpoint,继承实际 ICQQ Client
    session.scene;    // 当前群、私聊或频道;使用群字段前仍需收窄 kind
    session.sender;   // platformRoles 与 authorizationRoles 分开
    session.event;    // 同一次 SDK 回调的原生对象
    if (!options.silent) await session.reply(params.text);
  },
});

params、options、withConfig 与命令工厂保留各自的类型推断。没有实际 IM 输入的直接调用中,session 为 undefined。规范化 message.receive Handler 和入站 Middleware 使用同一 Session;未形成规范消息的原生事件不伪造 Session。

Session 是当前操作的只读视图。账号停止或重连会取消它的 signal;之后框架回复不会发送到替代连接。直接保存 SDK 引用并调用原生方法不经过框架租约,插件作者仍需遵守统一发送链路。

输入有误时会怎样 ​

情况Bot 的行为
消息不是任何命令不响应,交给后续中间件、普通聊天或 AI
命令名正确,但参数缺失或类型错误自动回复错误原因和帮助
没有权限自动回复拒绝提示,不显示帮助
执行成功回复 execute 的返回值

只有能确定是某个命令时才会提示参数错误。例如 remind nope 会提示;而 reminder 3 或普通聊天不会被当作命令。权限检查先于参数校验。

参考 ​

参数类型 ​

类别type得到的值
文本string / word / text字符串;text 会读取剩余的连续文本,保留引号和换行
数值number / integer / float数字;integer 要求整数,float 要求小数
布尔booleantrue / false
消息段mention / image / face / reply / forward / dice / rps对应消息段的字段

参数声明规则 ​

  • 参数按声明顺序读取输入。必填参数不能放在可选参数之后;rest: true 必须是最后一个。
  • text 会读取连续文本,应放在其他普通参数之后。
  • 默认值可以是动态函数;动态默认值和快捷方式的预填值同样会被校验。
  • 非法的静态默认值、参数顺序错误、重复的选项别名在加载时报错;动态默认值在派发时校验。
  • 选项可以出现在参数之间;负数不会被误认为选项;-- 之后不再解析选项。声明了 params 或 options 时,未知或重复的选项会报校验失败。
  • 没有声明参数的命令,可以从 args(剩余文字)和 segments(剩余消息段)自行解析输入。

真实示例:二维码生成 使用文本、图片尺寸与默认值;代码运行 使用目录参数声明语言,并提供代码和超时选项。

execute 参数 ​

execute(context) 收到一个只读对象:

字段说明
params声明的参数,类型由声明推导
options声明的选项;未设置且无默认值时为 undefined
args匹配命令名之后剩余的词(按空白切分)
segments剩余的消息段,保留图片、@ 等非文本内容
config本插件的配置
input触发命令的消息;由 Host 或程序调用时可能不存在
session绑定的单次平台操作视图(含 adapter、endpoint、scene、sender、event、reply)
$client声明 adapter 后,对应平台的强类型原生 Endpoint 客户端
interaction多轮交互追问接口(ask / sequence),见多轮交互追问
adapter适配器实例 id,如 root/icqq
endpoint账号 / 连接名
scene{ id, type, name? }:群、私聊或频道
sender{ id, name?, role: string[] }:发送者及其角色
has(token)检查可选的插件服务或 Host token 是否可用(在调用 use(token) 前使用)
use(token)取出插件提供的服务,缺失时抛错
locale当前消息或会话的语言区域(如 'zh-CN'、'en')
formatCommand(localPath)按当前账号的 commandPrefix 和插件 commandNamespace 格式化命令展示名
signal当前操作或 generation 的 AbortSignal,热重载或账号断开时触发取消
owner命令所属的插件
generation当前版本号(每次热重载递增)

单元测试命令 ​

用 @zhin.js/testing 的 createFakeMessage 和 runCommand 只需 10 行即可测试一条命令——runCommand 会根据命令声明自动解析位置参数 args 并填充 options 的默认值:

ts
import { createFakeMessage, createFakePlugin, runCommand } from '@zhin.js/testing';
import remind from '../commands/remind/index.js';

it('parses positional args and fills option defaults', async () => {
  const fake = createFakePlugin();
  const input = createFakeMessage({ sender: { id: 'u1', name: 'Alice' } });
  const reply = await runCommand(fake, remind, { input, args: ['10', '喝水'] });
  expect(reply).toBe('好的,10 分钟后提醒你:喝水');
});

完整测试指南见 插件测试。

input 不一定是消息(可能来自 Host 调用),读取发送者前要做类型判断;非消息来源时 $reply 不做任何事。input 的类型默认为 CommandMessage,与运行时的 Message 结构兼容(因分层限制,@zhin.js/command 不能直接依赖 @zhin.js/core)。

alias / permit / shortcut ​

字段行为
alias替换命令名的全部静态部分;目录参数接在后面
permit内置规则:adapter|group|private|channel|user|role(...)。失败返回 denied;没有鉴权主体时同样拒绝,程序化的 CommandIndex.execute 也不能绕过
shortcut{ 触发文本: 预填的 params },去掉首尾空白后完全相同才触发

冲突按完整的词序列判断:b 和 b list 可以共存。

目录参数规则 ​

  • 每条命令只能有一个目录参数,且必须是路径的最后一段。commands/[note]/index.ts 匹配顶层单词输入;commands/remind/[note]/index.ts 形成 remind <note>。
  • 参数有 default 时目录名必须用双方括号 [[name]];目录声明了参数但 params 里没有对应声明,都会抛 CommandPathSyntaxError。
  • TypeScript 读不到目录形态:使用无默认值的 [[id]] 时声明 params.id.optional: true;使用 [...items] 时声明 params.items.rest: true。目录参数应放在 params 的第一项。
  • 收集剩余输入时,元素的粒度由类型决定:text 按消息段收集(纯文本整体为一个元素);word / string 按空白切词;数值和布尔按词切分后逐个转换;mention / image 等按消息段收集。
  • 静态目录名使用小写 kebab-case(hello/、lottery-today/),也可以使用含非 ASCII 字符、无 ASCII 大写的名字(如 赞我/);参数目录只能用 ASCII。
  • 静态优先:list/index.ts 总是优先于 [name]/index.ts;动态路由之间,静态段多的优先。
  • 加上 commandNamespace 后仍然相同的路由会在启动时报错,可以给冲突的插件配置不同的命名空间。

深入 ​

一条命令是怎么被执行的 ​

匹配按固定优先级进行:静态命令先于动态命令,动态命令中路径更具体的优先。命中后,剩余文字按空白切分进入 args,完整的剩余消息段保留在 segments。例如 qq endpoint remove mybot 命中 qq endpoint remove <name>,args 为空,params.name === 'mybot'。

commandPrefix 按消息所属的适配器实例读取(默认 '');实例配置了 endpoints 时,以消息来源账号的 commandPrefix 为准。实现见 packages/im/core/src/plugin-runtime/im/message-dispatcher.ts。

程序化调用 CommandIndex.dispatch({ input, source, snapshot }) 时返回 status: unmatched | invalid | denied | executed:unmatched 的 matched 为 false;invalid 带有 issues: { field, message }[] 和 help;denied 表示权限拒绝;只有 executed 有业务返回值。Host 可以传入真实的 PermissionSubject 进行鉴权。

适配器的 endpoint 管理命令 ​

给适配器生成 list / add / remove 命令

createEndpointCommands(spec, defineCommand) 生成 <adapter> endpoint list / add / remove,与 Console 使用同一所属 Adapter 和持久化配置。平台特有的扫码或登录命令另行声明,不把它们藏在通用 bindFlow 回调中。

  • list 读取所属 Adapter 的账号配置与运行状态,不维护第二份 Endpoint Map。
  • add <id> <key=value...> 只保存配置,账号停用;之后在 Console 手动启动。
  • remove <id> 取消目标 incarnation 并关闭连接,历史保留;同名重建也获得新身份。
  • 首次修改前由管理员在 Console 核对已有账号清单并确认迁移,聊天命令不能代替确认。
  • 后续变更加密持久化,不写启动配置或 .env,不重启 Root。没有运行目录或数据库能力时拒绝写入,不回退文件。
  • add/remove 保留 master 权限规则;revision 冲突需刷新,提交结果未知需先读取权威状态核实,不能自动重试。
ts
// src/telegram-endpoint-commands.ts
import { createEndpointCommands } from 'zhin.js/adapter';
import { defineCommand } from 'zhin.js/command';

export const telegramEndpointCommands = createEndpointCommands({
  adapterKey: 'telegram',
  adapterDisplayName: 'Telegram',
  fields: [{ key: 'token', required: true, env: true,
    description: 'Telegram bot token' }],
}, defineCommand);

// commands/telegram/endpoint/list/index.ts
export default telegramEndpointCommands.list;
// add/[id]/index.ts exports .add; remove/[id]/index.ts exports .remove

adapterKey 是当前 owner 内注册的 Adapter 名称,框架从真实 command operation 的 owner、generation 和 AdapterIndex 解析目录,不按显示名猜账号。平台只注册实际 AdapterRegistry 类型,不需要 RuntimeState token 或额外 Client token。

fields 是与平台 schema 对齐的闭合字段列表。嵌套字段可用明确声明的 dotpath,例如 smtp.port;type 支持 string | number | boolean,省略为 string,不允许任意对象合并。env: true 或 secret: true 表示敏感字段,管理响应遮蔽其值,由 Host 私有存储保存,不是写 .env 的开关。add 从 args 读取 key=value,按第一个 = 切分。

Master 检查由通用 isEndpointOperator 在真实入口执行;平台额外业务权限不能替代或扩大该门禁。

下一步 ​