npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

koishi-plugin-qq-command-registrar

v1.0.0

Published

为 QQ 官方机器人自动注册/更新指令面板与全局自定义菜单的 Koishi 插件

Readme

koishi-plugin-qq-command-registrar

自动把当前启用的 Koishi 指令注册成 QQ 官方机器人的指令面板(/v2/panels)和全局自定义菜单(/v2/menu),并做查重处理 —— 内容没变就不会重复下发。

  • 自动识别当前激活的指令,以及它们的参数要求(<必填> / [选填] / ...可变)和说明文案
  • 配置里的「注册指令 / 注册分类」是选单,选项直接来自当前已激活的指令列表
  • 「生效场景」是多选勾选框(单聊 / 群聊 / 文字子频道 / 频道私信)
  • 两层查重:本地内容未变时一个请求都不发;即使插件重启,也会先查询线上现状再比对,只更新有差异的面板
  • 面板用 remark 打标(koishi-cmd-registrar:<场景>:<序号>:<内容指纹>),只管理自己创建的面板,不会碰别人的
  • 按官方 QPM 限流(面板写入 10 QPM、菜单写入 5 QPM 等),自动串行、自动重试「操作进行中」

⚠️ 前置要求

  1. 必须安装 @koishijs/plugin-adapter-qq(平台名 qq),并在其中填好机器人的 AppID 与 Secret。 本插件通过该适配器获取 HTTP 客户端与 access token;没有它插件不会工作。

    npm i @koishijs/plugin-adapter-qq
  2. 机器人需要开通指令面板 / 自定义菜单相关接口权限(这两个接口 2026-08-12 才上线)。

  3. 生产环境必须配置 IP 白名单:官方对新机器人强制开启 IP 白名单且不可关闭,只有白名单内的出口 IP 才能调用 OpenAPI。 漏配时会返回 11298 接口访问源 IP 不在白名单内(插件会把这条错误码翻译成中文提示)。沙箱环境不受影响。

  4. 需要 HTTP 服务。插件已在 package.json 的 koishi.service.required 与代码里的 inject.required 中声明对 http 的依赖。

安装

npm i koishi-plugin-qq-command-registrar

在 koishi.yml 中启用:

plugins:
  qq-command-registrar:
    enabled: true          # 打开后才会进行初次注册
    panel:
      enabled: true
      commands: []         # 留空 = 全部已激活指令
      scopes: ['单聊', '群聊']
    menu:
      enabled: true
      categories: []

AppID / Secret 不需要重复填写 —— 插件会自动读取 adapter-qq 的配置。

配置

配置只有四个区块。

连接

| 字段 | 默认值 | 说明 | | --- | --- | --- | | enabled | false | 注册开关。打开即开始初次注册,之后自动增量同步 | | checkMode | false | 检查模式(演练):只查询线上现状并在日志里打印差异,不发送任何写请求 | | appid | 空(自动) | 机器人 AppID。留空自动读取 adapter-qq;填写后只管理 AppID 匹配的机器人 | | secret | 空(自动) | 机器人密钥(隐藏内容)。留空自动读取 adapter-qq;仅在适配器换不到 token 时作为兜底 |

指令面板

| 字段 | 默认值 | 说明 | | --- | --- | --- | | enabled | true | 是否管理指令面板 | | commands | 空(全部) | 选单,选项来自当前已激活的指令列表。留空表示全部 | | scopes | 单聊 + 群聊 | 多选勾选框:单聊 / 群聊 / 文字子频道 / 频道私信 | | overrides | 空 | 手工覆盖指令的注册内容,见下节 |

四种场景都以「全量生效」的方式注册。一个机器人最多 20 个面板,每个面板最多 20 个元素;指令多了会自动拆成多个面板。

写 YAML 时 scopes 也可以用数组写法:['单聊', '群聊'],与勾选等价。

指令覆盖 overrides

指令的内容、参数、说明默认全部自动识别,并且会随指令变化热重载(插件重启、指令增删改都会刷新)。 overrides 用来在需要时手工改写,只影响注册出去的菜单/面板,不改变指令本身的行为。

| 字段 | 说明 | | --- | --- | | command | 选单,要覆盖哪条指令 | | content | 面板元素名 / 菜单按钮文字。留空用自动识别结果 | | description | 展示用的说明。留空用指令自身的说明;填空串则清空(描述里就只剩参数用法) | | arguments | 覆盖参数列表。留空用指令自身声明的参数 | | arguments[].name | 参数名 | | arguments[].required | 必填显示为 <参数>,选填显示为 [参数] | | arguments[].variadic | 可变参数,显示为 ... | | arguments[].type | 参数种类,如 number / user;string 是默认种类,不展示 |

panel:
  overrides:
    - command: echo
      content: 复读            # 面板按钮文字
      description: 重复        # 描述里显示什么
      arguments:
        - { name: 内容, required: true }
        - { name: 次数, required: false, type: number }

注册出去就成了:面板元素名 复读、说明 重复 <内容> [次数:number];菜单按钮文字也是 复读, 但 send_message 仍然是真实指令名,保证点了能触发。

注意面板元素名同时是点击后填入输入框的内容,把它改成非指令名会导致点击后触发不了指令。 说明与参数要一起显示时受 30 字符(约 15 个汉字)上限约束,放不下会只保留说明 —— 把说明改短即可让参数露出来。 覆盖项里写了、但当前不存在的指令名会被忽略,并在日志里提示。

控制台上选单的每个选项都会带上自动识别到的「说明 · 用法」,同时 debug 日志里会逐条打印识别结果,照着填即可:

已注册指令选单,共 3 个选项
  识别到指令 echo:复读机|参数:content(必填), times
  识别到指令 roll:掷骰子|参数:max:number

菜单面板

| 字段 | 默认值 | 说明 | | --- | --- | --- | | enabled | true | 是否管理全局菜单(仅单聊窗口可见) | | categories | 空 | 注册分类列表。留空则把所有指令平铺成一级按钮(最多 10 个) |

categories 每一项:

| 字段 | 说明 | | --- | --- | | name | 一级按钮名称,最多 5 个汉字 | | commands | 选单,选项来自当前已激活的指令列表,最多 5 个 |

menu:
  categories:
    - name: 娱乐
      commands: [roll, echo, jrrp]
    - name: 管理
      commands: [mute, kick]

选单在控制台里渲染成下拉框;直接写 YAML 时也可以填 * 通配符(如 cmd*)。

其他

| 字段 | 默认值 | 说明 | | --- | --- | --- | | logLevel | info | 日志打印等级:silent / error / info / debug |

合并延迟、请求间隔、QPM 限流、重试次数、指令前缀、prune、onlyAdmin 等内部参数没有暴露成配置项,使用内置默认值。需要调整时可以覆盖导出的 RUNTIME 常量,或构造 new Registrar(ctx, config, { delay: 0 })。

初次注册与检查模式

enabled 就是「注册按钮」:

  1. 装好并配置好 adapter-qq 后,插件启动时会在日志里打印识别到的机器人(AppID / 环境)。
  2. 打开 连接 → enabled —— 这就是初次注册。
  3. 之后保持打开即可:指令增删改、或配置本身被修改,都会自动增量同步。

控制台保存配置会重新加载插件,插件对已经在线的机器人会主动补一次同步,所以「改完配置立刻就生效」。

不确定会改什么时,先打开 checkMode:插件只查询线上现状,把「将新增 / 将更新 / 将删除」打进日志,不发送任何写请求。确认无误再关掉它。

指令到菜单的映射

| Koishi | QQ 面板元素 | QQ 菜单按钮 | | --- | --- | --- | | name | name(点击后填入输入框) | 一级 name(≤10)/ 子项 name(≤14) | | description | desc(≤30) | — | | arguments | 拼进 desc,如 echo <内容> [次数] | — | | — | type: 'command' | type: 'send_message',send_message = 指令名 |

长度按「1 个汉字 = 2 个字符」计算,与官方口径一致。

面板按钮是把文本填进输入框的,所以填入的内容会跟随 Commander 的前缀配置(Koishi 默认无前缀,即裸指令名)。

注意:面板元素的 name 既是标签也是点击后填入输入框的内容,所以指令名超过 14 字符宽时面板会整个跳过这条指令(截断会填出一条不存在的指令),并在日志里提醒。菜单项不受影响 —— 它的 name 只是标签,send_message 才是填入的文本,标签会截断、指令名保持完整。

查重是怎么做的

  1. 本地查重:把本次要下发的全部内容算一个指纹。如果和上次成功下发的一模一样,直接返回,一个 HTTP 请求都不发。
  2. 线上查重:即使指纹不同(比如插件刚重启),也会先 GET /v2/panels(按场景)和 GET /v2/menu 拉取现状,逐面板比对 items 与 remark:
    • 完全一致 → 不发写请求
    • 有差异 → 只 PUT 那一个面板
    • 数量变少 → DELETE 多出来的
  3. 只有全部成功后才记录指纹;中途失败的话下次会重新尝试。

取消勾选某个场景后,插件仍会查询该场景,把之前留下的面板回收掉。

比对时会先做归一化(补齐 only_admin: false、link: '' 等默认值),避免平台补全的默认值被误判成「有变更」。

日志

日志前缀统一是 qq-command-registrar,等级由「其他 → 日志打印等级」控制。

info(默认)—— 注册情况

识别到 QQ 机器人 bot-1:AppID=102000001,密钥=已读取,环境=正式
开始同步(启动就绪):待注册指令 3 个、面板 2 个、菜单 3 项
同步完成(启动就绪):新增 2 个面板;更新全局菜单

debug —— 排查细节

已注册指令选单,共 3 个选项
场景 c2c:线上共 1 个面板,其中 1 个由本插件管理
场景 group:线上共 1 个面板,其中 1 个由本插件管理(未勾选,只做清理)
  + 新增面板 panel-1:场景 c2c,3 个元素
  ~ 更新面板 panel-3:内容有变化
  - 删除多余面板 panel-1
→ POST /v2/panels?scope=c2c {...}
← POST /v2/panels ok
接口 panelWrite 已达 10 QPM,等待 4820ms

Secret 只会显示「已读取 / 未读取」,不会写进日志。

接口对接情况

| 用途 | 接口 | 我们发的请求 | | --- | --- | --- | | 查面板列表 | GET /v2/panels | scope(必填,按场景逐个查)、limit=50、翻页用 cursor | | 查面板详情 | GET /v2/panels/{panel_id} | 列表没返回 items 时才补 | | 建面板 | POST /v2/panels | { scope, target_type: 'all', panel: { items, remark } } | | 改面板 | PUT /v2/panels/{panel_id} | { panel: { items, remark } } | | 删面板 | DELETE /v2/panels/{panel_id} | 无 body | | 查菜单 | GET /v2/menu | 无参数 | | 改菜单 | PUT /v2/menu | { menu: { items } },整体覆盖 |

  • 面板元素只发官方定义过的字段:name / desc / type(only_admin 仅在开启时带上)。
  • 长度与数量都按官方口径(1 个汉字 = 2 字符)在本地先截断或拆分,不把超限数据发出去。
  • POST /v2/panels 只返回 { panel_id }(不含面板内容),所以新建后会靠下一轮同步或详情接口补齐状态;插件对「对象」和「裸字符串」两种响应都做了兼容。
  • panel.version 我们不带:官方文档把它列为可选,且没有说明它是乐观锁;唯一与并发相关的错误码是 40030009 操作进行中,并未与 version 挂钩。既然语义未定义,带上反而可能引入失败。
  • PUT /v2/menu 没有请求侧 version:version 只在响应里返回,官方未定义条件写入语义,所以不存在「版本不匹配被拒」的问题。

测试里有一组官方文档一致性用例,直接断言我们发出去的路径、查询参数名、请求体字段名与嵌套结构,改动时能立刻发现不合规。

安全

| 项目 | 处理方式 | | --- | --- | | 鉴权头 | 复用适配器的 bot.http,自带 Authorization: QQBot <access_token> 与 X-Union-Appid,不用手工拼 | | token 获取 | 优先 bot.getAccessToken()(适配器自带缓存与自动续期;在有效期内重复请求返回同一个 token) | | token 兜底 | 适配器取不到时,用 AppID / Secret 打 POST /app/getAppAccessToken 换取,缓存到过期前 60 秒(官方正是在最后 60 秒才签发新 token,旧 token 在此期间仍然有效),并把新 token 装回 bot.http | | Secret 展示 | 配置项声明为 role('secret'),控制台里打码 | | Secret 日志 | 请求体写日志前统一走 redactForLog() 脱敏;secret / clientSecret / access_token 等字段一律变 ***。日志里只会出现「密钥=已读取 / 未读取」 | | 传输 | endpoint 来自适配器,默认 https://api.bot.qq.com/;沙箱会自动加 sandbox. 前缀(换 token 也走同一个 endpoint) | | 频率限制 | 按官方 QPM 分档限流(面板读 30 / 面板写 10 / target 60 / 菜单读 30 / 菜单写 5),滑动窗口,且所有请求串行 | | 内容安全 | 官方在提交时做内容安全检查,没有客户端预检接口;返回 40030020 时插件带中文提示抛出,不会静默重试 | | 排障 | 错误里保留官方的 trace_id(响应体字段或 X-Tps-trace-ID 响应头),便于找官方支持 |

关于错误码,两点实测结论:

  • 官方确实会用真实的 HTTP 状态码(401 / 404 / 429 / 5xx),但也会在响应体里放 err_code(0 表示成功),而且 换 token 的接口反过来:失败也返回 HTTP 200,错误码在 code 字段里。插件两套都读(err_code ?? code),两种失败形态都能识别。
  • 面板/菜单接口用 4003xxxx,通用错误用另一组。插件内置了中文释义,11298(IP 白名单)、100016(AppID/Secret 错误)、 11253(未开通接口权限)这些最常踩的坑会直接打印人话,不用再去查文档。

配置里的 secret 如果手工填写,会以明文存在 koishi.yml 中 —— 这是 Koishi 配置的固有行为(adapter-qq 也一样)。留空即可,插件会自动从适配器读取,不必把密钥再抄一遍。

补充一句事实边界:官方明确要求的是「不要在应用前端使用访问凭证,请在服务端发起 API 访问」;至于「不要打印密钥到日志」, 官方文档里并没有明文规定,属于通用安全实践。插件按实践做了脱敏,但请不要把它当成官方合规要求。

另外:X-Union-Appid 这个头不在官方的接口调用指南里,但主流 SDK(simbot / botpy / StarBot 等)都会带, 且新的 access_token 鉴权方式实际上要求它。插件从适配器的 bot.http 继承该头,所以不要绕开 bot.http 自己发请求。

控制台页面

装上 @koishijs/plugin-console 后,控制台侧边栏会多出一个 「QQ 指令注册」 页面,用来直接在网页上看注册情况,不用翻终端:

  • 当前状态:注册开关、检查模式、指令前缀、日志等级、识别到的机器人(AppID / 是否沙箱)
  • 最近的同步记录:每次同步的时间、触发原因、结果摘要、失败时的完整报错,以及那次涉及的指令/面板/菜单数量。 连「内容与上次一致,已跳过」也会记一笔,方便确认「为什么什么都没发生」
  • 自动识别到的指令:指令名、说明、用法,也就是插件实际读到的东西,照着它填「指令覆盖」

页面数据走控制台自己的 RPC(ctx.console.addListener + 客户端 send),不额外开任何 HTTP 端口,也就不存在绕过控制台鉴权的问题。

@koishijs/plugin-console 是可选依赖:没装时这一段会静默跳过(用 ctx.using(['console'], ...) 声明,而不是 if (ctx.console)),插件其余功能完全不受影响。

指令

| 指令 | 说明 | | --- | --- | | qq-command-registrar / qq-menu | 手动同步一次(开关关闭时也可用) | | qq-menu -f | 忽略查重结果,强制重新下发 |

注意事项

  • 全局自定义菜单只在 C2C 单聊窗口底部展示,群聊里看不到。
  • 一个机器人最多 20 个指令面板,每个面板最多 20 个元素。指令多了会自动拆成多个面板;超过 20 个面板时会在日志里告警。
  • 这套接口是 2026-08-12 新上的,需要较新的 QQ 客户端(手机版优先),灰度期间可能配置成功但客户端不显示。
  • 插件自身的 qq-command-registrar 指令名超过 14 字符宽,不会进入指令面板,但仍会出现在全局菜单里。
  • 官方没有说明 PUT /v2/menu 传空 items 是否合法,所以指令清空时插件不会去清空线上菜单,只告警一次。

开发

npm install
npm run build     # 后端 tsc -> lib/,控制台前端 -> dist/
npm test

npm run build 会跑两步:

  1. tsc -b:后端代码输出到 lib/
  2. node scripts/build-client.cjs:控制台前端从 client/index.ts 打包到 dist/ (直接调用 @koishijs/client 官方导出的 build(),外置依赖 vue / @koishijs/client 等由控制台宿主提供,和官方插件的构建方式一致)

发布时必须带上 dist/。这是控制台扩展最常见的坑:dist 没构建或没发到 npm,开发环境看着正常,装到生产环境后页面就不出现。本项目的 package.json 的 files 已包含 lib 和 dist。

测试使用 test/mock-server.js —— 一个严格复刻官方文档约束的假上游,不需要连接真实服务器就能验证生成的请求是否合规(GET /v2/panels 必须带 scope、列表接口不返回 openid、各处长度上限、QPM、HTTP 200 + err_code 等)。控制台扩展也有测试:没装 console 时静默跳过、装了之后注册入口与状态接口、状态接口返回的同步记录是否正确。