兼容性
约 1345 字大约 4 分钟
2026-09-15
目标是云崽(TRSS-Yunzai)插件零改动可用。做法不是重写插件,而是把云崽的对外契约原样复刻一层。
三层结构
| 层 | 位置 | 说明 |
|---|---|---|
| 内核 | src/ → lib/core/ | 纯 TS,不依赖全局变量,插件通过扩展点接入 |
| 兼容层 | lib/**(手写 JS) | 物理路径 shim、全局变量、e 对象、Bot 语义 |
| 插件 | plugins/** | 两套写法共用同一注册表与分发流程 |
插件里写死的相对路径(../../lib/plugins/plugin.js、../../lib/config/config.js 等)无法重定向, 所以这些文件在 lib/ 下真实存在,内容只是转发到内核。
支持清单
插件基类 pluginname / dsc / event / priority / rule / task / handler / namespace, 方法 reply / conKey / setContext / getContext / finish / awaitContext / resolveContext / renderImg。
rulereg(字符串或正则,加载时预编译)、fnc(方法名)、event(三段通配,如 message.group.normal)、 permission(master / owner / admin)、log: false。
task cron 表达式(5 段或 6 段,node-schedule),fnc 支持方法名与函数。
钩子accept(e)(每条事件都跑,在 rule 之前)、init()(加载时一次,返回 "return" 跳过整个插件)。
事件对象 emsg / raw_message / message / img / at / atBot / user_id / group_id / self_id / isGroup / isPrivate / isMaster / sender, 方法 reply / getReply / recall / logText, 对象 e.group / e.friend / e.member / e.bot, 运行时 e.runtime(render / handler / cfg / common)。
全局与可导入路径
| 形式 | 内容 |
|---|---|
| 全局变量 | Bot / logger / redis / segment / Renderer / plugin / start_type |
lib/config/config.js | cfg(getGroup / getConfig / getdefSet / getYaml / getOther / master / masterQQ / uin / package) |
lib/plugins/*.js | plugin / loader / handler / runtime / config(makeConfig) |
| 其它 | lib/common/common.js、lib/puppeteer/puppeteer.js、lib/util.js、lib/renderer/* |
消息计数 Redis 键与云崽一致:Yz:count:<receive|send>:msg:{total|bot:ID|user:ID|group:ID}:{day|month|year|total}。
适配器契约Bot.adapter.push({ id, name, path, load() })、Bot[uin] = bot、Bot.em('message.group.normal', e)(逐级派发)、 Bot.wsf[path]、Bot.express。
自带 Web 服务的插件与热重载 插件在加载期通过 Bot.express / Bot.wsf 注册的路由与 WebSocket 路径会按插件记账, 卸载 / 重载时自动清理 —— hl-icqq-web、hl-douyin-plugin 这类自带面板的插件反复重载不会出现 路由重复注册。两点约定:
- 整图重载(
apps/、lib/深层文件也换新)依赖 Node 22.15+;更老的 Node 上目录型插件 改深层文件要#重启 - 插件在模块顶层建的资源(定时器、自己
listen的独立服务、浏览器、数据库句柄)要插件自己 交接,框架只负责Bot.express路由、Bot.wsf路径与onUnload(),见 模块级资源要自己交接
事件总线Bot 是 EventEmitter(JiuLiBot extends EventEmitter),on / once / emit / off / addListener / removeListener / removeAllListeners / listenerCount / eventNames 全部可用; Bot.on('message') 拿到的是未分发的原始事件流(与云崽一致)。 详见事件对象与权限。
内置更新插件的类接口system/update.js 导出 Update 类(含 getPlugin / runUpdate / getLog / isUp / restart / quiet 等云崽同名成员)。生态插件自己的更新指令(如 #椰奶更新、#小飞更新)会 new Update() 后直接调用这些方法,改动时不要改名。
plugins/other/ 别名 云崽的内置插件在 plugins/other/,JiuLi 并进了 plugins/system/,因此框架提供 plugins/other/{update,restart,sendLog,version}.js 转发(导出 default 与具名), 云崽插件里的 import ... from '../../other/update.js' 不用改。加载器会跳过该目录, 不会把同一个插件注册两次。
框架版本号package.json 的 version 固定为 3.1.3(V3 家族)。很多云崽插件用它判断走 V2 还是 V3 分支:
const yunzaiVersion = JSON.parse(fs.readFileSync('package.json')).version
const isV3 = yunzaiVersion[0] * 1 > 2 // 0.1.0 → false → 走旧分支就会报错插件从文件读、从 cfg.package 读,拿到的都是 3.1.3;框架名仍然是 JiuLi。
框架标识
生态插件常按框架名做分支或展示(yenai / genshin / xiaofei 的状态页、miao 的 isMiao)。
- JiuLi 的对外标识固定为
JiuLi,不能自定义 - 只有
jiuli.yaml → compat.spoof_yunzai: true(插件伪装模式)才对外展示trss-yunzai
| 插件 | 读取方式 | 默认显示 |
|---|---|---|
| yenai-plugin / genshin / xiaofei-plugin | cfg.package.name | JiuLi |
| miao-plugin | 直接读真实 package.json | Yunzai-Bot(它自己的硬编码默认名) |
| hl-icqq-web 状态面板 | 源码里写死的字符串 | 需要插件自己改 |
插件里写死的名字框架改不了;开启伪装模式后显示名与分支判断会一起变成 TRSS-Yunzai, 因为插件只有一个名字来源,没法既显示 JiuLi 又走 TRSS 分支。
兼容层之外
已实机验证的生态插件:miao-plugin、genshin、yenai-plugin、xiaofei-plugin、HMeme、 TRSS-Plugin、Yunzai-QQBot-Plugin、Yunzai-ICQQ-Plugin、hl-icqq-web。
已知限制
- 深层内部状态(如
loader.load_time以外的字段)不保证与云崽一致,只保证插件常用面 - 依赖
e.runtime.MysInfo / NoteUser的插件需要plugins/genshin存在(JiuLi 会动态加载) plugins/miao-plugin的#miao/#miao.models别名已保留在根package.json#importsplugins/system、plugins/other在云崽里是插件不是内核,缺失时把目录复制过来即可
性能适配注意:云崽插件在 import 阶段做同步外部调用(尤其是 Windows 上调 systeminformation 查 WMI) 会拖慢整个启动,因为插件是并行导入、主线程是共享的。已对移植的插件做过惰性化改造, 差异记录在仓库的 docs/ported-plugin-patches.md。详见启动慢的排查。
