云崽风格插件
约 1079 字大约 4 分钟
2026-09-15
从 TRSS-Yunzai / Yunzai 迁移过来的插件不需要改代码。这一页是写法的速查。
基本结构
export class MyPlugin extends plugin {
constructor() {
super({
name: '功能名',
dsc: '功能描述',
event: 'message', // 默认 message;通知类事件写 notice.group.poke 这种
priority: 5000, // 数字越小越先执行
rule: [
{ reg: '^#指令$', fnc: 'main' },
{ reg: '^#管理$', fnc: 'admin', permission: 'master' },
{ reg: '^#静默$', fnc: 'quiet', log: false },
],
})
}
async main(e) {
await e.reply('收到')
return true // 返回 false 表示继续匹配后面的规则
}
}plugin 是全局变量,也可以用 import plugin from '../../lib/plugins/plugin.js'。
rule
| 字段 | 说明 |
|---|---|
reg | 字符串或正则,加载时预编译 |
fnc | 要调用的方法名 |
event | 限定事件,支持三段通配(message.group.normal) |
permission | master / owner / admin,不满足时框架直接回复权限提示 |
log | false 时不打印「中 ▶ / 完成」日志 |
方法收到的第一个参数是事件对象 e。
task(定时任务)
super({
name: '定时',
task: [
{ name: '每日任务', cron: '0 8 * * *', fnc: 'daily' },
{ name: '每分钟', cron: '*/1 * * * *', fnc: () => logger.mark('tick') },
],
})
async daily() {
// 定时任务里没有 this.e,不要调用 this.reply
await Bot.sendGroupMsg(Bot.uin[0], '123456', '早上好')
}cron 支持 5 段或 6 段(node-schedule 语法)。
生命周期
async init() {
// 插件加载时执行一次
// 返回字符串 "return" 可以跳过整个插件的加载
}
async accept(e) {
// 每条事件都会调用,在 rule 之前
// 返回 "return" 终止分发;返回真值结束 accept 阶段,继续走 rule
}上下文会话
async start() {
this.setContext('waiting')
return this.reply('请发送内容')
}
async waiting() {
this.finish('waiting')
return this.reply(`收到:${this.e.msg}`)
}Promise 形式:
async ask() {
const next = await this.awaitContext() // 超时返回 false
if (next) await this.reply(`收到:${next.msg}`)
}会话按「插件名 + bot + 群/用户」隔离,默认 120 秒超时, 完整签名 setContext(type, isGroup, time, timeout)。
回复与其它常用方法
await this.reply('文本') // 等价于 e.reply
await this.reply('文本', true) // 引用回复
await this.reply(img, true, { recallMsg: 10 }) // 10 秒后撤回
this.conKey('aaa') // 按关键词匹配(用于 accept 分支)
await this.renderImg('my-plugin', 'help/index', data) // 渲染后直接发送热重载与模块级副作用
云崽插件很常见的写法是在模块顶层就把东西建起来:setInterval、fs.watch、打开数据库、 启动浏览器、Bot.on(...) 挂监听。JiuLi 的热重载会把整张模块图重新求值,这些顶层代码会再跑 一遍 —— 旧那一份没人收,于是每重载一次就多一份:定时任务跑两遍、文件变更处理两次、浏览器 进程留在系统里。这一条与插件写法无关,旧插件同样适用:
// plugins/my-plugin/index.js(目录型入口)
if (typeof globalThis.__myPluginHandoff === 'function') {
try { await globalThis.__myPluginHandoff() } catch (e) { logger.warn('交还资源失败', e) }
}
startWatcher()
globalThis.__myPluginHandoff = () => stopWatcher()用 globalThis 而不是模块级变量:模块级变量在重新求值后已经清零,新的一份看不见旧的握着什么。 挂在实例上的资源写 onUnload() 即可(框架在卸载 / 重载前调用)。
老插件还有一类更容易踩的写法:循环 import + 顶层副作用。模块顶层就用环上另一个模块的导出 (如 model/db/BaseModel.js 顶层用从 #index 来的 Data,而 #index 与 base.js、gsCfg.js 互成环),冷启动顺序恰好是对的,重载改变了求值顺序就撞 TDZ:
ReferenceError: Cannot access 'Data' before initialization
at plugins/xxx/model/db/BaseModel.js?jiuli_reload=2:5:1框架会认出这类失败并自动重启恢复,但要根治得把顶层副作用挪进函数、或拆掉环 —— 详见重载后模块初始化失败。
完整约定(含 Bot.express 面板路由与独立 Web 服务)见 插件开发 → 模块级资源要自己交接。
常见坑
this.e只在 rule / accept 调用期间有值,定时任务与init()里是空的rule命中但方法返回false时会继续匹配后续规则,需要独占就返回true或不返回- 上下文会话的函数名要与
setContext的 type 一致,否则永远不会被调用 - 指令被别的插件抢走时先看
priority:数字相同时按加载顺序先匹配先赢(重载不会改变这个 顺序);要稳定压过别人就把数字写小一点
