Administrator
发布于 2026-10-22 / 0 阅读
0
0

阿狐:一个会自己从地上爬起来的 QQ 机器人

起因

一个很小的需求:群里有人 @ 它,它就回一句话。

需求小,但它长成了别的东西。


它长什么样

小号 QQ
  ↓ OneBot v11 正向 WS (127.0.0.1:3001)
NapCat(便携版 QQ,快速登录,不用扫码)
  ↓
bot.mjs(零依赖 Node 24 脚本)
  ↓
GLM-4.7-Flash

最终它能做的事:

能力说明
群里唤醒@ 它,或直接说「阿狐」「小狐」
私聊必回私聊不设限,长文本不截断
分人人设每个 user_id 有独立档案,行为和语气都对得上
滚动摘要记忆超过 12 轮时,把最早的 6 轮压进摘要再丢掉原文
记忆落盘每轮应答后立刻写到 X 盘,拔盘自动降级为纯内存
掉线私聊掉线恢复只私聊主人,群里的其他人一个字不提
自己爬起来看门狗每 30 秒探活,死了就拉起来

「分人人设」是花时间最多的部分。机器人对每个人应该像不同的人——对主人是一种语气,对朋友是另一种。这个后来演变成了每个用户一份独立档案(年龄、生日、家乡、性格、说话方式)。


第一天:能跑

第一天是顺利的。搭好 NapCat、接上 OneBot、脚本连上模型,群里 @ 它就能回话。

顺利的部分只持续了一天。


第二天:它不理人了

群里有人来问「阿狐今天怎么不说话了」。

去翻日志,发现两千条重连记录,全是:

连接失败,5 秒后重连
连接失败,5 秒后重连
连接失败,5 秒后重连

连了 15 分钟,全失败。但 QQ 那边一点提示都没有——客户端显示的好好的,消息也发得出去。

问题出在 ws://127.0.0.1:3001 这个端口根本没在监听。NapCat 进程活着,QQ 进程活着,但 OneBot 的 WS 服务死了。程序层面完全看不出来,只有端口是死的。

这就是「假活」——进程在,服务没了。

而当时的看门狗逻辑是「端口不通可能是正在启动,等一下」,于是永远不动手。

解决

加了一条升级策略:端口挂超过 3 分钟、且进程还在 → 判定假活 → 强杀掉 NapCat 和 QQ,重新快速登录拉起。

修完当天晚上就触发了:

19:41 机器人对 3001 干重连了 70 次(67 分钟)

看门狗终于动手了。


第三天:看门狗自己崩了

第二天刚修好的看门狗,第三天 20:04 首次触发升级策略——直接把整个看门狗进程带崩了。

崩的原因有两个,都是很典型的 Node 坑:

坑 1:目录排序取最后一个

选 NapCat 启动脚本的时候,代码是这么写的:

const versions = fs.readdirSync(versionsDir);
const latest = versions[versions.length - 1];   // ❌

versions 目录里混着一个 setting.json 文件。按字典序排,setting.json 排在 9.9.33-52230 后面——于是取到了 versions\setting.json\resources\... 这种鬼路径。

修法是"只认目录,而且必须真的包含 _silent_start.bat",返回前用 existsSync 校验。

坑 2:spawn 的失败是异步的

try {
  spawn('cmd.exe', [...]);
} catch (e) {
  log('启动失败');
}   // ❌ 这个 catch 永远抓不到东西

spawn 的失败是异步 error 事件抛的,try/catch 接不住。必须挂 child.on('error', ...)。

修完之后,结果是:NapCat 被强杀了,看门狗也崩了,没人拉起。停机 140 分钟。


那天晚上 22:20

重开看门狗。这次它自己跑完了全流程:

22:20 重启看门狗 → 它发现 NapCat/QQ 全没了 → 按正确路径拉起 → 32 秒后端口恢复 → 机器人重连 → 私聊报「掉线 140 分钟」

无人值守自愈,验证通过。


还有一个更隐蔽的 bug

重连逻辑里有这么一段:

ws.onerror = () => { ws.close(); };   // ❌

这里的 ws 是模块级变量。如果旧连接的 error 事件在新连接已经建立之后才触发,这个 close() 关掉的就是那个新的、健康的连接。

结果就是:明明连上了,程序却以为断了,然后又去连——并行重连循环互相踩,日志刷得满天飞。

改成用局部变量捕获 socket,并且在 onclose 里加一句"我还是当前连接吗"的校验。

重连退避也从固定 5 秒改成了指数退避:

3s → 6s → 12s → 24s → 48s → 封顶 60s

26 分钟的断线原本能刷 305 行噪音,现在安静了。


交学费清单

这些东西,没有一个是"看文档能看出来的":

  1. 官方 launcher.bat 不能用——它靠注册表找 QQ,会找到版本不匹配的 C:\QQNT。必须用 _silent_start.bat 直连便携版路径
  2. %cd% 在解析期就展开了——cd 还没执行,路径全错。bat 里要用 %~dp0
  3. bat 和 vbs 里不能写 UTF-8 中文注释——cmd 按 GBK 解析会直接炸,纯 ASCII
  4. 杀进程要按路径过滤——taskkill /IM QQ.exe 会把我自己的 QQ 一起杀掉
  5. PowerShell 过滤条件会匹配到查询命令自己——命令行里含 bot.mjs 字样 → 自己杀自己,报 4294967295
  6. 手写 JSON 带 UTF-8 BOM,NapCat 解析失败会静默回退默认——token 悄悄变了,谁都不知道
  7. spawn 的错误是异步的,try/catch 抓不到
  8. readdirSync 排序取最后一个——目录里有同名文件就取到鬼路径

数据

修完之后统计了一次日志(两天,1459 行,16 个进程实例混写):

指标数值
应答成功率53.8%(107/199)
失败原因1305 过载 55 次 / 1302 限流 19 次 / 超时 18 次
延迟 p5018.8s
延迟 p9051.1s
最长单次312s

53.8% 这个数字不是代码问题。 免费档模型的并发池子就那么挤,实测 4 次里中 3 次过载。

但它也逼出了一堆工程细节:

  • 全局串行队列(免费档并发低,乱并发只会互相饿死)
  • 1302/1305/超时的分级退避:3/6/10/15 秒
  • 75 秒总时限,超了兜底回一句「大脑暂时过载」

安全

这个项目最要紧的一条纪律,写在备忘录里了:

绝对不能把 3001 / 6099 暴露到公网。

  • 3001 是 OneBot 控制口,没有 token,谁连上谁就能用这个号发消息
  • 6099 是 NapCat 管理面板

而接入网站这件事,结论也是一样的:

要接就在本机另起一个带鉴权的 HTTP 接口,网站后端走内网或隧道调它,别让公网直连这两个端口。

顺便一提,NapCat 的 webui.json 里那两个 host / accessControlMode 字段其实是不生效的——改完重启,端口照样监听 ::,非白名单 IP 照样放行。真正的防线是 Windows 防火墙(默认入站全拦)。这一点花了不少时间才确认:不要去折腾那两个字段,也不要手滑给 QQ 点"允许访问"。


后记

现在它在后台跑着,每天开机自启,自己看自己,自己爬起来。

老实说,做这个项目的过程里,我学到的东西比"接个 API 让它回话"多得多:

  • 进程活着不等于服务活着。 端口探活是唯一可信的信号
  • 自愈逻辑要能自愈。 看门狗自己崩掉,等于没有看门狗
  • 先确认要拉起的东西真的能起来,再去杀掉现在这个。 顺序反了,故障会被放大

第三条是那天晚上的教训——那次是先杀了 NapCat,拉起又因为路径 bug 失败,一次 3 分钟的卡死被放大成了 140 分钟全黑。杀的动作成功了、拉起的动作失败了,损失比原本大得多。

现在这个顺序改过来了:先解析出替代启动路径、确认文件真的存在,再动手杀。


评论