插件开发
HotCat Bot 启动时自动扫描 plugins/ 目录并加载所有插件,同时监听目录变化实现热插拔。
快速开始
在 plugins/<name>/ 下创建 index.ts:
import { PluginBase, PluginMeta } from 'hotcat-bot-qq/plugin'
import { BotApi } from 'hotcat-bot-qq/botApi'
import { BotClient } from 'hotcat-bot-qq/botClient'
import { Message } from 'hotcat-bot-qq/message'
export class HelloPlugin extends PluginBase {
static meta = {
name: 'hello',
version: '1.0.0',
description: '示例插件',
author: 'your-name',
}
static create(api: BotApi, bot: BotClient) {
return new HelloPlugin(api, bot)
}
async load() {
this.bot.event.message.onGroupMessage(this.onGroupMsg)
}
async unload() {
this.bot.event.message.offGroupMessage(this.onGroupMsg)
}
private onGroupMsg = async (_bot: BotClient, event: any) => {
if (event.raw_message === '/hello') {
await this.api.sendGroupMessage(event.group_id,
Message.reply(event.message_id),
Message.text('你好!')
)
}
}
}启动 bot,插件自动加载。无需在 bot.ts 中手动导入。
插件规范
必须导出
| 要求 | 说明 |
|---|---|
class 继承 PluginBase | 所有插件类必须 extends PluginBase |
static meta | 声明插件元数据,自动扫描时读取 |
static create() | 接收 api bot 两个参数 |
load() | 异步方法,注册事件 / 启动定时 |
unload() | 异步方法,移除事件 / 取消定时 |
可用属性
| 属性 | 类型 | 说明 |
|---|---|---|
this.api | BotApi | 所有 napcat API |
this.bot | BotClient | bot 实例,包含 event/scheduler/plugin |
this.meta | PluginMeta | 插件元数据 { name, version, description? } |
PluginMeta
interface PluginMeta {
name: string
version: string
description?: string
author?: string
}导入规范
统一从 hotcat-bot-qq/xxx 导入,按需取用:
import { PluginBase, PluginMeta } from 'hotcat-bot-qq/plugin'
import { BotApi } from 'hotcat-bot-qq/botApi'
import { BotClient } from 'hotcat-bot-qq/botClient'
import { Message } from 'hotcat-bot-qq/message'项目通过
tsconfig.json的paths将hotcat-bot-qq映射到本地根目录,开发时无需发布 npm 即可直接使用。
卸载清理
为什么要清理
热重载 bot.plugin.reload(name) 的内部流程是 unload() → load()。如果 unload() 没有清理干净,旧的事件监听器、定时器会继续残留,load() 再注册一次,导致同一事件触发多次回调——消息重复回复、定时任务叠加执行。
需要清理的资源
| 资源 | load 中创建 | unload 中释放 |
|---|---|---|
| 消息事件 | this.bot.event.message.onGroupMessage(fn) | this.bot.event.message.offGroupMessage(fn) |
| 定时任务 | this.bot.scheduler.cron(...) | this.bot.scheduler.cancel(id) |
| 定时任务 | this.bot.scheduler.every(...) | this.bot.scheduler.cancel(id) |
| 外部连接 | connect() / open() | disconnect() / close() |
完整示例
export class MyPlugin extends PluginBase {
private timerId = 0
async load() {
// 1. 注册事件
this.bot.event.message.onGroupMessage(this.onGroupMsg)
// 2. 启动定时 —— 保存 id 以便取消
this.timerId = this.bot.scheduler.every('1h', this.onTick)
}
async unload() {
// 1. 移除事件 —— 使用对应的 off 方法
this.bot.event.message.offGroupMessage(this.onGroupMsg)
// 2. 取消定时
this.bot.scheduler.cancel(this.timerId)
}
// ⚠️ 必须用箭头函数属性,保证 this 绑定且引用唯一
private onGroupMsg = async (event: any) => { ... }
private onTick = () => { ... }
}为什么用箭头函数属性(
private fn = () => {})而不是方法(private fn() {})?因为off()需要完全相同的函数引用才能移除。箭头函数属性在类实例化时绑定到this,且引用不会变化。
完整示例:每日签到
每天 0:00 遍历所有群聊发送签到消息。
import { PluginBase } from 'hotcat-bot-qq/plugin'
import { BotApi } from 'hotcat-bot-qq/botApi'
import { BotClient } from 'hotcat-bot-qq/botClient'
import { Message } from 'hotcat-bot-qq/message'
export class SignPlugin extends PluginBase {
static meta = {
name: 'sign',
version: '1.0.0',
description: '每日 0 点群签到',
author: 'your-name',
}
private timerId = 0
static create(api: BotApi, bot: BotClient) {
return new SignPlugin(api, bot)
}
async load() {
// 每天 0:00 执行
this.timerId = this.bot.scheduler.cron('0 0 * * *', this.doSign)
}
async unload() {
this.bot.scheduler.cancel(this.timerId)
}
private doSign = async () => {
const groups = await this.api.getGroupList()
for (const g of groups) {
try {
await this.api.sendGroupSign(g.group_id)
} catch {}
}
}
}时间偏移
cron('0 0 * * *') 基于 bot 所在机器的系统时钟。如果机器时间与 QQ 服务器时间偏差较大,签到可能失败。建议将 cron 设为 '0 0 0 * * *' 增加秒级延迟、或在 cron 回调中加一个随机延时(如 setTimeout(fn, Math.random() * 30000)),避免瞬时集中请求。
插件管理
Bot 启动时已自动 scan 并 watch,以下为手动控制场景。
启动时自动加载所有插件(默认)
无需任何代码,bot.start() 内部会执行 scan('./plugins') 和 watch('./plugins')。
手动注册并加载
bot.plugin.register('hello', HelloPlugin, {
name: 'hello',
version: '1.0.0',
})
await bot.plugin.load('hello')条件加载
const isDev = process.env.NODE_ENV !== 'production'
if (isDev) {
bot.plugin.register('debug', DebugPlugin, { name: 'debug', version: '1.0.0' })
await bot.plugin.load('debug')
}获取插件实例调用自定义方法
const p = bot.plugin.get('sign') as SignPlugin
if (p) {
p.doSign()
}重载所有已加载插件
for (const name of bot.plugin.loaded()) {
await bot.plugin.reload(name)
}卸载全部插件
for (const name of bot.plugin.loaded()) {
await bot.plugin.unload(name)
}通过消息指令控制插件
bot.event.message.onGroupMessage(async (bot, event) => {
if (event.raw_message === '/reload test') {
await bot.plugin.reload('test')
await bot.api.sendGroupMessage(event.group_id, Message.text('已重载'))
}
if (event.raw_message === '/plugins') {
const list = bot.plugin.list().join(', ')
await bot.api.sendGroupMessage(event.group_id, Message.text(`已注册: ${list}`))
}
})加载错误处理
scan 和 load 失败只输出错误日志,不影响其他插件:
[system]: 插件 "sign" 已注册
[system]: 插件 "sign" 已加载
[error]: 加载插件 "broken" 失败: index.ts 未导出符合规范的类
[system]: 插件 "hello" 已加载TypeScript 编译为 JS
框架支持 .ts 和 .js 插件。开发调试阶段直接跑 .ts,发布或分发时编译为 .js,原因:
- 纯 Node.js 用户无法直接执行
.ts,需要 JS 入口 - 编译后启动更快,省去运行时类型检查
- npm 包分发时只有
.js能直接被require/import
Node.js 用户:tsc 编译
# 单独编译
tsc --module ESNext --target ES2020 --declaration --outDir dist plugins/hello/index.ts或项目级 plugins/hello/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": ".",
"declaration": true,
"skipLibCheck": true
},
"include": ["index.ts"]
}cd plugins/hello && tsc
# 产物: index.js + index.d.ts(同目录),直接可被 scan 发现Bun 用户:bun build
bun build plugins/hello/index.ts --outdir plugins/hello --format esm
bun build会打包所有依赖,产物体积较大但运行更快,适合发布场景。开发调试直接用index.ts即可。
package.json 构建脚本
{
"name": "hotcat-plugin-hello",
"scripts": {
"build": "tsc",
"build:bun": "bun build index.ts --outdir . --format esm"
},
"files": ["index.ts", "index.js", "index.d.ts"]
}发布建议
线上分发带 .js,files 中同时保留 index.ts,用户拿到包无论 Node 还是 Bun 都能直接用。
目录结构
普通用户开发目录:
my-bot/
├── bot.ts
├── package.json
├── node_modules/
│ └── hotcat-bot-qq/
└── plugins/
├── hotCatPlugin/ # 默认插件,启动自动复制
│ └── index.ts
├── hello/ # 自行开发的源码插件
│ └── index.ts
├── sign/ # npm 安装的已编译插件
│ ├── index.ts # 源码(可选)
│ ├── index.js # 编译产物(入口,被 scan 发现)
│ └── index.d.ts
└── utils/
└── helper.ts # 不会被加载(无 index.ts / index.js)两文件同时存在时 index.js 优先,确保 Node.js 用户能正常加载。构建产物输出到同目录,不要用 dist/ 子目录。