JiuLi 原生插件
约 805 字大约 3 分钟
2026-09-15
原生写法保留了云崽的全部能力(rule / task / handler / accept / init),另外多了四项:
| 能力 | 用途 |
|---|---|
beforeMessage / afterMessage | 分发前后统一拦截与收尾(审计、限流、统计) |
onLoad / onUnload | 生命周期,卸载时清理定时器 / 连接 |
useConfig | 插件配置,首次用默认值生成 config/<插件名>.yaml |
this.render / this.dataDir / this.resDir | 渲染与私有目录,不用自己拼路径 |
类形态
import { JiuLiPlugin } from 'jiuli'
export default class Demo extends JiuLiPlugin {
constructor() {
super({
name: '演示',
dsc: '类型提示更完整',
event: 'message',
priority: 100,
rule: [
{ reg: '^#演示$', fnc: 'demo' },
{ reg: '^#演示渲染$', fnc: 'renderDemo' },
],
task: [{ name: '每日', cron: '0 3 * * *', fnc: 'daily' }],
})
}
async demo(e) {
await this.reply(`你好,${e.sender?.nickname || e.user_id}`)
return true
}
async renderDemo() {
const img = await this.render('help/index', { title: '标题' }) // 失败返回 false
return this.reply(img || '渲染失败')
}
async daily() {
const fs = await import('node:fs/promises')
await fs.writeFile(`${this.dataDir}/last.txt`, new Date().toISOString())
}
}函数式 DSL
import { definePlugin, command, task, accept, handler, logger } from 'jiuli'
export default definePlugin({
name: '演示',
dsc: '函数式写法',
priority: 100,
apps: [
// 正则 → 处理函数,第二个参数是匹配结果
command(/^#演示$/, async (e, match) => e.reply('你好'), { name: '演示' }),
// 选项:permission / event / log / name
command(/^#管理$/, async e => e.reply('ok'), { permission: 'master' }),
command(/^#群限定$/, async e => e.reply('群消息'), { event: 'message.group' }),
// 定时任务(没有 this.e)
task('0 8 * * *', () => logger.mark('早安'), { name: '早安' }),
// 通知 / 请求事件
accept('notice.group.poke', async e => e.reply('别戳了')),
// 跨插件 handler:e.runtime.handler.call('demo.hello', e, args)
handler('demo.hello', (e, args) => `hello ${args ?? ''}`),
],
})分发钩子
import { definePlugin, command, beforeMessage, afterMessage } from 'jiuli'
export default definePlugin({
name: '审计',
apps: [
beforeMessage(async e => {
if (e.msg === '#禁止') {
await e.reply('这条指令被拦截了')
return false // 拦截:后续插件与规则都不执行
}
}),
afterMessage(e => {
logger.mark(`[审计] ${e.user_id}: ${e.msg}`)
}),
command(/^#正常$/, async e => e.reply('ok')),
],
})多个插件的钩子会按优先级依次执行。类形态需要在 hooks 里显式声明方法名:
export default class Audit extends JiuLiPlugin {
constructor() {
super({ name: '审计', hooks: { beforeMessage: 'onBefore', afterMessage: 'onAfter' } })
}
async onBefore(e) { /* 返回 false 可拦截 */ }
async onAfter(e) { /* 收尾 */ }
}钩子必须显式声明(DSL 的
beforeMessage(...)或hooks: {...}), 不会因为插件里恰好有同名方法就被当成钩子调用。
生命周期与配置
export default class Demo extends JiuLiPlugin {
async onLoad() {
// 加载完成:建目录、起定时器、预热缓存
this.timer = setInterval(() => {}, 60_000)
}
async onUnload() {
// 卸载 / 热重载前清理资源,避免重复注册
clearInterval(this.timer)
}
async init() {
const { config, configSave } = await this.useConfig({ enable: true, tips: '你好' })
config.tips = '新值'
await configSave()
}
}useConfig 生成的配置落在 config/<插件名>.yaml。旧插件继续用 init(),两者是同一个钩子 (基类把 init() 转发到 onLoad())。
实例属性
| 属性 | 说明 |
|---|---|
this.e | 当前事件(只在 rule / accept 期间有值) |
this.dataDir | 插件私有数据目录 data/plugins/<插件名>/ |
this.resDir | 插件资源目录 plugins/<插件名>/resources/ |
this.render(tpl, data) | 渲染成可直接回复的图片,失败返回 false |
this.renderBase64(tpl, data) | 返回 base64:// 字符串 |
this.reply(msg, quote?, opts?) | 回复当前事件 |
从框架导入什么
import {
JiuLiPlugin, definePlugin, command, task, accept, handler,
beforeMessage, afterMessage, makeConfig,
logger, cfg, metrics, Runtime, getBot, renderService, common,
} from 'jiuli'jiuli 是根包的导出(exports["."]),插件目录里有自己的 package.json 时, 记得在里面加 "jiuli": "link:../..",否则这个 import 解析不到。
