常见问题
约 5350 字大约 18 分钟
2026-09-15
启动很慢
先看日志里这行:
[ Plugin ] 插件 102 个 · 规则 356 条 · … · 用时 13秒680
[ Plugin ] 启动关键路径:genshin 14732ms · xiaofei-plugin 12582ms · … · 并行导入,耗时含公共依赖解析插件是并行导入的,这里的耗时是「开始加载 → 该插件就绪」,最后就绪的几个就是拖慢启动的那几个, 不代表插件本身有问题。实测结论(Windows / 102 个插件 / Node 25):
| 情况 | 耗时 |
|---|---|
| 只加载框架与内置插件 | 约 0.24 秒 |
| 单个重插件(如 genshin) | 约 2.2 秒 |
| 全部生态插件 | 8 ~ 14 秒 |
耗时构成(--cpu-prof 采样):
| 项 | 占比 |
|---|---|
Node 模块解析(internalModuleStat / lstat / getPackageScopeConfig) | 约 68% |
子进程(spawn / spawnSync) | 约 2% |
| 插件自身 JS | 约 1% |
所以慢的是文件系统调用:本机 statSync 单次约 107µs,1000+ 个模块文件每个要探测上百次候选路径。 串行 13.61s ≈ 并行 13.68s,与并发策略无关;V8 编译缓存实测也没有收益。
能做的优化:
- 别让插件在
import阶段做同步外部调用。典型是systeminformation——Windows 上用 PowerShell 查 WMI,si.graphics()单次约 10 秒、si.cpu()/si.osInfo()各约 4.6 秒。yenai-plugin 原本在 5 个模块顶层调用它们, 光 import 就占 10 秒,实测启动 23.5s → 13.7s。 - 不要复制插件的
node_modules,交给根目录pnpm install,避免重复依赖副本。 - 机器很忙时调大
config/config/bot.yaml → plugin_load_timeout(默认 60 秒)。
服务器上启动报 Cannot find module '.../lib/core/start.js'
lib/core/ 是编译产物,不入库,所以新克隆的机器上还没有它 —— 需要先编译内核:
node jiuli b # 只编译内核
node jiuli i # 装依赖 + 编译 + 生成配置(首次部署推荐)直接用 node jiuli m(tmux 常驻)或 node jiuli s(pm2)也可以:它们会自动补齐 「依赖 / 编译 / 配置」这三步,无需手工构建。
端口被占用
默认 1721。改 config/config/server.yaml → port(url 一起改),然后重启。 端口被占用时启动会直接报错,启动日志里的 [ Server ] 启动 HTTP 服务器 … 一行就是实际监听地址。
启动卡在「扫描插件目录 plugins/ …」不动
现象:日志停在扫描那一行,之后没有任何输出(也可能几分钟后才继续)。
原因:某个插件的 import 一直不返回,而且它会阻塞后面插件的导入。常见来源:
- 插件顶层有
await在等一个永远不会来的东西(网络请求、Bot.once('online')之类) - 文件被混淆/压缩过(javascript-obfuscator 等),混淆器自带的反调试循环在 Node 里会卡死
- 文件写了一半(语法不完整通常直接报错,不会卡住)
框架会自我止损,日志直接点名:
[ Plugin ] 等待 1 个插件就绪(已等 5 秒):example/xxx.js
[ Plugin ] 插件加载超时(60 秒)example/xxx.js
该插件顶层可能在做重活或卡住了,并且会阻塞后续插件的导入
本轮已加载 38 个插件,跳过剩余 58 个;修好后发 #重载 或重启即可
排查:临时把该文件改名(如加 .disabled 后缀)或移走,再看是否恢复正常排查步骤:
- 按提示把那个文件改名(
xxx.js→xxx.js.disabled)或移走,重启 - 启动恢复正常 → 就是它,去修或删掉那个文件(不要用混淆过的代码)
- 想更快暴露:把
config/config/bot.yaml → plugin_load_timeout调小(如 15 秒)
相关配置:
config/config/jiuli.yaml → perf.load_concurrency(默认 4):插件导入并发数。 插件很多、内存小的机器建议 1~2;不要为了快调很大,会加剧 Node 加载器的锁竞争- 被跳过的插件稍后若自己加载完,框架会自动把它补进分发(日志:
xxx 迟到就绪,已加入分发)
插件加载报错怎么看
插件加载失败不会阻塞启动:出问题的插件会被跳过,其余插件照常加载,启动流程照常走完。 日志里每个失败插件都是两行,第一行说「哪个文件 · 什么原因」,第二行给下一步:
[ Plugin ] 插件加载错误 example/戳一戳.js · 缺少依赖包 axios(plugins/example/戳一戳.js 引入)
安装:cd plugins/example && pnpm add axios 或 项目根目录 pnpm i
[ Plugin ] 插件加载错误 example/戳一戳.js · 找不到文件 plugins/hl-plugin/model/HL.js
由 plugins/example/戳一戳.js 引入 —— 该文件不存在:插件没放全,或相对路径写错
[ Plugin ] 插件加载错误 example/测试1.js · 语法错误 Unexpected end of input
检查该文件是否写完整(括号 / 引号是否闭合),可执行 node --check 自查| 报错 | 含义 | 处理 |
|---|---|---|
缺少依赖包 xxx | 插件要的 npm 包没装 | cd plugins/<插件> && pnpm add xxx,或根目录 pnpm i |
找不到文件 ... | 插件里 import 的本地文件不存在 | 插件没放全(少了 model/ 之类),或路径写错 |
语法错误 ... | 文件写坏了(少括号/引号,或没写完) | 按提示行修好,node --check <文件> 自查 |
插件加载超时(N 秒) | 插件顶层在等网络 / 连服务 | 检查该插件;仍要跑就调大 bot.yaml → plugin_load_timeout |
Could not locate the bindings file ... node_sqlite3.node | 原生模块没编译出来(见下一节) | git pull --ff-only && pnpm install |
缺依赖时会额外汇总一段「有 N 个依赖没装」,列出包名与涉及插件。 完整 Node 堆栈不再刷屏,需要时把 config/config/bot.yaml → log_level 调成 debug 即可看到。
插件运行时报错(指令执行中抛异常)
同样只给一行原因 + 插件内的出错位置,不是整段 Node 堆栈:
[ Plugin ] 执行出错 状态统计 <system/status.js> · xxx is not a function
plugins/system/status.js:42:18
插件自身的问题:本次指令未完成,其他插件与启动都不受影响(完整堆栈:log_level: debug)- 出错位置是插件自己代码里的那一行(中文文件名也能正常显示)
- 只管这一条指令:其他插件照常响应,框架也不需要重启
accept/beforeMessage钩子出错、定时任务出错([ Task ])、onUnload出错、未捕获异常([ Crash ]/[ Reject ],会标注「疑似来自 …」)都是这个格式
修好之后:热重载会自动重新加载;file_watch 关掉时发 #重载 或 #重载 <关键词> 也能加载 (失败过的插件不会被当成"空文件"跳过)。
插件报 Could not locate the bindings file(sqlite3 / canvas 编译失败)
[ JiuLi ] [hl-plugin] 加载 .../plugins/hl-plugin/apps/look.pro.js 失败:
Could not locate the bindings file. Tried:
→ .../node_modules/.pnpm/[email protected]_.../node_modules/sqlite3/build/Release/node_sqlite3.node
→ .../sqlite3/lib/binding/napi-v6-linux-x64/node_sqlite3.node ...这不是插件写错了,是它的原生模块没编译出来。常见两条原因叠在一起:
- 插件自己的
package.json声明的是真sqlite3(hl-plugin、hl-icqq-web都写了sqlite3: ^5.1.6→ 装到 5.1.7),而[email protected]没有 Node 22+/24 的预编译包, 于是退化成 node-gyp 源码编译; sqlite3自带的[email protected]里那份 gyp 还在from distutils.version import StrictVersion, 而 Python 3.12+ 已经删掉distutils(Debian 13 / Ubuntu 24 都是 3.12+),于是:ModuleNotFoundError: No module named 'distutils'→ configure 直接失败,node_sqlite3.node永远不生成。
修法(框架已内置,更新代码即可):
cd <你的框架目录>
git pull --ff-only
pnpm installpnpm-workspace.yaml 里已把 sqlite3 全量覆盖成 NAPI 分支 (overrides: { sqlite3: npm:@karinjs/sqlite3@^0.4.3 },框架自己的数据库早就用的它)—— 自带 linux/mac/win 预编译、与 Node 版本无关,不需要 gcc / python。装完重启一次即可。
- pnpm 12 起只读
pnpm-workspace.yaml,package.json里的pnpm.overrides会被忽略并打印The "pnpm" field in package.json is no longer read by pnpm的告警;pnpm 9/10 也读工作区文件; pnpm 8 及更早只读package.json的pnpm字段。框架两处都写了同样的覆盖,所以任何版本 都能装上,告警可以忽略。 canvas@2(老插件用)同样没有 Node 24 预编译,会走源码编译;装齐系统库就能编过:apt install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev build-essential。 用canvas@3的插件走预编译,不受影响(hl-plugin就是 v3)。- 想确认真实原因:在报错包里跑
node node_modules/.bin/node-pre-gyp install --fallback-to-build,它会打印是下载失败还是编译失败。
this.e 是 undefined
定时任务(task.fnc)与 init() / onLoad() 里没有事件对象,不要调用 this.reply。 需要主动发消息用:
await Bot.sendGroupMsg(bot_id, group_id, '内容')
await Bot.sendFriendMsg(bot_id, user_id, '内容')改了插件没生效
热重载覆盖:新增 js、修改 js(含 index.js 与 apps/、lib/ 深层文件)、删除 js、新建插件目录, 存盘后约 2 秒生效。按这个顺序检查:
config/config/bot.yaml → file_watch是不是false- 刚启动的十几秒内:监听是边扫描边铺开的,日志里出现「热重载已开启」之后才算铺到深层 目录;在它之前改文件不会触发(真实
plugins/上千个文件,Windows 上这一趟要十几秒) - 只有
.js会触发重载:改resources/模板、插件自己的config/*.yaml都不会 (配置由插件自己读,模板由渲染层管缓存);node_modules/、.git/、temp/整个被忽略 config/、data/、resources/、temp/里的.js要看内容:纯数据(export const a = false这种)不重载,只有带插件结构(插件类 /rule/apps/task/fnc…)的才重载。 这是为了避免"插件加载时写回自己的配置文件 → 又触发重载"的自激循环(实测几十秒打爆内存); 确实要让私有配置生效,发#重载 <插件名>强制刷新- 删掉的是嵌套文件(
apps/x.js、lib/y.js)时插件不会被卸载,只会重载一次; 插件目录(入口index.js)被删才算插件删除 - 插件目录只能有一层:
plugins/<插件名>/(有index.js)或plugins/<目录>/*.js(没有index.js,每个 js 一个插件)。plugins/example/子目录/index.js这种"扁平目录里再套目录" 不会被加载,改它也不会触发重载 - 手动重载:
#重载(补漏)/#重载 关键词(强制刷新) - Node 版本:整图重载要 Node 22.15+(
#版本可看)。更老的 Node 上目录型插件 (index.js+apps/+lib/)只重载入口文件,改apps/、lib/里的代码要#重启 - 改了内核(
src/)需要pnpm build重新编译
日志说「已暂停它的重载监听」
某个插件在窗口内自激重载达到 perf.reload_limit(默认 5 次 / 60 秒)时的熔断:只暂停 这个插件的热重载,其它插件照常;本次运行有效。自激指的是"文件是插件自己在加载时写出来的", 典型就是它在模块顶层把自己目录下的 .js(配置 / 数据)写回去 —— 写文件触发重载、重载又写一遍, 每轮都会永久占住一张模块图(ESM 缓存不释放旧图),几十秒就能把内存打爆(真实事故)。
处理:把写文件挪到运行期(指令 / 定时任务里),或改存成 .json / .yaml;改完发 #重载 <插件名> 恢复监听并强制刷新,内存已经涨上去就直接 #重启。想放宽或关掉熔断: jiuli.yaml → perf.reload_limit / perf.reload_window(reload_limit: 0 关闭)。 人工改文件不计入熔断,开发时连着存十几次代码不会被暂停。
热重载之后行为「串味」了
重载本身出问题时是几个症状,成因不同,都能从日志上看出来:
| 症状 | 成因 | 现在 |
|---|---|---|
重载 A 之后 #xx 被 B 抢走(如重载 miao 后 #更新面板 变成系统更新插件响应) | 分发是「priority 升序、同优先级按加载顺序先匹配先赢」,而重载会把插件挪到同优先级队伍的末尾 | 已修:重载保持首次加载时的顺序 |
| 面板路由 / WebSocket 路径出现两遍 | 重载时按「插件」记账清理旧路由,而插件是并行导入的,记账可能认成了别的插件 | 已修:归属改用异步上下文,并行也不会认错 |
重载后该插件指令全没了、日志里一堆 Cannot access 'x' before initialization | 重载会改变整张模块图的求值顺序;插件把循环 import + 顶层副作用混在一起时,新顺序下顶层访问撞上 TDZ(冷启动顺序恰好是好的)。失败还可能被插件自己吞掉,于是「指令没了但进程还在线」 | 已修:认出这类失败后自动重启恢复(两分钟内只重启一次,reload_restart_on_error: false 可关);根治要插件侧拆环或把顶层副作用挪进函数,见插件开发 → 重载后模块初始化失败 |
| 定时任务 / 文件监听 / 浏览器各多一份 | 插件在模块顶层建的资源,重载后新的一份建了、旧的一份没人收 | 框架侧只能保证清理自己管的(express 路由、Bot.wsf、onUnload);插件自己的模块级资源要插件作者交接,见模块级资源要自己交接 |
对自己写的插件,最后一行是最常见的坑;对第三方插件,升级到最新代码即可。
重载后插件自带 Web 服务报 EADDRINUSE / 端口被占用
框架只清理插件注册在 Bot.express 上的路由与 Bot.wsf 里的 WebSocket 路径。 插件自己 http.createServer(...).listen(端口) 起的独立服务不在记账范围内,要插件自己在 交还函数里 close()(hl-icqq-web 就是这种形态)。插件没做交接时会一直占着端口,重载后 监听失败;临时办法是把 file_watch 关掉只手动重启,或找到该插件补一段交接。
#状态 这类指令没有
云崽的 plugins/system、plugins/other 是插件,不是内核功能。JiuLi 自带了一套等价实现 (在 plugins/system/),如果被你删过,从仓库拿回来即可。
Redis 连不上
日志里会出现降级提示。本机地址连不上时可自动用内存实现(jiuli.yaml → redis_fallback: true), 插件常用命令都能跑,但重启后数据丢失。要持久化就装个 Redis:
# Debian / Ubuntu
apt-get install -y redis-server && systemctl enable --now redis-server
# CentOS / RHEL
yum install -y redis && systemctl enable --now redis
# 验证
redis-cli ping # 期望 PONG框架在连不上时会给出上面的装法提示;连续重连失败且允许降级时会自动切到内存实现。
出现大量 Redis Client Error 刷屏
这是插件自己建的连接在报错(典型是照抄了 node-redis 官方示例的 console.error('Redis Client Error', err)),并且它常常在模块顶层等待连接 —— Redis 一挂,这类插件就会一边刷屏一边卡在加载阶段(日志里能看到 「等待 N 个插件就绪:<插件名>」)。
处理顺序:
- 先把 Redis 起来(上面的命令),刷屏和卡加载会一起消失
- 起不来又不需要持久化 →
jiuli.yaml → redis_fallback: true,用内存实现顶着 - 插件自身的
console.error刷屏框架管不了,需要插件自己加错误处理(或改用全局redis,这样框架的降级/节流对它也生效)
框架自身的 Redis 报错已做节流(每 10 秒最多一条并带累计次数),且不再谎报「已连接」。
图片渲染不可用(找不到浏览器)
日志特征:
[ Render ] Chromium 启动失败 · Error: Could not find Chrome (ver. 153.0.8010.36)
[ Render ] Chromium 不可用,图片渲染功能已停用之后依赖图片的插件(状态图、帮助图……)会降级成文字。
原因是 puppeteer 只认它自己记录的那个版本号对应的缓存目录。早期 .puppeteerrc.cjs 会把一个空的 executablePath 传给 puppeteer,让它以为你已指定浏览器,于是安装时跳过下载, 服务器上就一直没有浏览器。
框架现在按下面的顺序找浏览器,命中即用:
renderers/puppeteer/config.yaml → chromiumPath(或jiuli.yaml → bot.chromium_path)- puppeteer 自带的缓存目录
- 系统已装的浏览器(
chromium/google-chrome/microsoft-edge等) - 自动安装:Linux 且 root 时优先用包管理器装
chromium(顺带装中文字体), 否则用npx puppeteer browsers install chrome
第 4 步约 150MB,只做一次;不想让它自己装就设环境变量 JIULI_NO_AUTO_INSTALL=1。
手工装也可以:
# Debian / Ubuntu
apt-get install -y chromium fonts-noto-cjk
# CentOS / RHEL
yum install -y chromium
# 装完把路径写进配置,省得每次都去找
# renderers/puppeteer/config.yaml → chromiumPath: /usr/bin/chromium中文字体是另一个坑:少了 fonts-noto-cjk 时截图里的中文会变成方块,而且不会报错。 emoji 同理,缺 fonts-noto-color-emoji 时图里的 👑 💬 这类字符也会变成方块 (框架启动浏览器时会自检一次并在日志里提醒)。
验证直接发一次 #渲染测试(或 #渲染状态 看后端是否就绪):
#渲染测试成功时出一张卡片图;启动日志里 程序 /usr/bin/chromium 就是实际用到的浏览器。
出现 Puppeteer old Headless deprecation warning
这是提示不是报错,图照常出。原因是某个插件自己 puppeteer.launch() 时没写 headless, 而它自带的 puppeteer 版本偏老(21 及以下)—— 老版本默认走旧无头模式,就打印这条提示:
puppeteer.launch({ args: [...] }) // 老版本这里会提示
puppeteer.launch({ headless: 'new', args: [...] }) // 写明用新的无头模式改插件就行,注意插件更新后可能被覆盖。框架自己的渲染后端一直用 "new" (renderers/puppeteer/config.yaml → headless),不受影响。 用根目录那份 puppeteer(25+)的插件写 headless: true 也不会提示 —— 新版本里它就是新无头模式。
插件提示 Command failed: ffmpeg …
插件做视频 / 语音 / 图片合成时普遍要调外部命令 ffmpeg,系统没装就会只报一句 Command failed: ffmpeg -y -i …,从这行看不出是缺命令。
框架启动后会自检这类外部命令,并在 Linux + root 下自动安装(apt / dnf / yum), 日志里是 [ Deps ] 开头;装不了(非 root / 非 Linux)会给出手动命令:
# Debian / Ubuntu
apt-get install -y ffmpeg
# CentOS / RHEL
yum install -y ffmpeg
# Windows
winget install Gyan.FFmpeg发 #诊断 也能看到这一项(【系统命令】ffmpeg ✓)。不想让框架自动装东西,设环境变量 JIULI_NO_AUTO_INSTALL=1。
一直刷 被动回复失败(code(40034128))
这是 QQ 官方机器人适配器(Yunzai-QQBot-Plugin)的兜底逻辑,不是故障:
// 插件源码:被动回复被 QQ 拒了,就删掉 msg_id/event_id 改用主动消息重发
if (['22007', '40034128', '40034105'].includes(code)) {
logger.warn(`被动回复失败(code(${code})),正在尝试通过主动消息发送`)原因是被动回复有次数/时效限制(官方规则:同一个 msg_id 通常 5 分钟内最多回 5 条)。 一条消息如果被拆成好几条回复(比如一次扫码流程回了 6 条),超出的部分就只能降级成主动消息 —— 消息本身照常送达,只是每降级一条就打一行 WARN。注意主动消息有每日额度,长期靠降级会耗额度, 把多段回复合并成一条能同时解决这两件事。
框架侧已经做了重复日志折叠(config/config/bot.yaml → log_dedupe):同一条内容在 10 秒内重复时, 终端只留一条 + 折叠计数,logs/jiuli.<日期>.log 里每一条都还在,所以不会再把聊天/终端刷掉。
框架名显示成 Yunzai
JiuLi 的对外标识固定为 JiuLi。插件里写死的框架名框架改不了(例如 miao-plugin 自己硬编码的 Yunzai-Bot)。 如果某个插件只在 TRSS 分支下正常,可以开插件伪装模式:
# config/config/jiuli.yaml
compat:
spoof_yunzai: true开启后对外显示 trss-yunzai,插件识别到的名字也会一起变。
Windows 上要注意
- 渲染需要本机有 Chrome 或 Edge;没有就装一个,或
bot.yaml → chromium_path指定路径 data/建议放在本地磁盘,放机械盘或网络盘会明显拖慢启动- Windows Defender 的实时扫描会放大文件系统开销,给项目目录加排除项能快一些
systeminformation相关的插件首次执行会明显变慢(PowerShell 查 WMI),见上文
git pull 报冲突 / 更新后版本号没变
用 pnpm add xxx 装的插件依赖写在根 package.json 里,它就成了本地修改 —— git pull 会因为「本地改动会被覆盖」而中止(error: Your local changes ... Aborting)。
两个办法:
# 1) 拉取时自动暂存再还原(推荐)
git -c rebase.autoStash=true pull --rebase --autostash// 2) 更彻底:把插件依赖挪到插件自己的 package.json
// plugins/example/package.json(该目录已声明 "jiuli": "link:../..")
// 然后根 package.json 保持与仓库一致,pull 不再冲突如果 pull 中途产生冲突(文件里出现 <<<<<<<),会导致 package.json 解析失败、 框架起不来(ERR_INVALID_PACKAGE_CONFIG)。处理:
git status # 看 UU 标记的文件
# 手工删掉 <<<<<<< / ======= / >>>>>>> 三类标记,保留正确内容
node -e "JSON.parse(require('fs').readFileSync('package.json','utf8'))" # 确认 JSON 有效
git add package.json && git commit -m "resolve"不要删这些目录
data/、logs/、temp/、config/config/ 是运行状态(数据、日志、主人与群配置)。 框架对「目录被清理」做了自愈(写文件前先确保目录存在),但运行中的实例仍可能短暂报错。 需要重置配置就删单个 config/config/*.yaml。
测试会不会动到我的数据
不会。测试脚本用 JIULI_CONFIG_DIR=test/.tmp-* 和隔离的插件目录,不会影响正在运行的实例。 测试脚本属于开发用,不在仓库里(开发者本地保留 test/),普通部署不需要它们。
