从零写一个 Hermes Agent 的 QQ 插件,顺便开源了
想在自己的服务器上跑一个 Hermes Agent,然后通过 QQ 控制它。听起来一件事,结果从头到尾折腾了一整天——最后不是装完了,而是自己写了一个开源插件。
这篇文章记录整个过程:从选型、部署、踩坑,到被逼着造轮子,最后发布到 GitHub。
📌 背景:Hermes 官方不支持 QQ
Hermes Agent 是 Nous Research 出的开源自主智能体,能持久记忆、自动创建技能、跑定时任务,功能很顶。但它的消息网关只支持 Telegram、Discord、Slack 那一票国外平台——没有 QQ。
INFO
关于 OneBot 扫描事件的坑,我之前踩过一次,可以看这篇:关于onebot扫描事件
先说结论,QQ 接入有三条路:
| 方案 | 思路 | 问题 |
|---|---|---|
| 官方 QQ Bot API | 腾讯官方机器人 | 消息审核、主动推送限制多,不自由 |
| 现成开源桥接 | NapCat + 桥接项目 | 要么 star 个位数不敢用,要么只解决入站 |
| 自己写插件 | 以真插件形式接进 Hermes | 工作量大,但架构最干净 |
我最后选了第三条。中间的过程比较曲折,往下看。
🛠️ 部署:三个容器的连环坑
服务器是国内的,国际出口基本残废。记录几个印象深刻的坑:
GitHub 拉不动。一开始想源码构建 Hermes 镜像,git clone 卡 15 分钟不动。测了一圈镜像站,git clone 全被 403 或超时。最后发现——官方根本就有 Docker 镜像 nousresearch/hermes-agent,白折腾了。
TIP
遇到"下载不动"先怀疑人生,再怀疑方案。也许根本不需要下载这玩意。
Docker Hub 也拉不动。docker pull 停在 Pulling fs layer 一动不动。解法是镜像站前缀直拉:
docker pull docker.1ms.run/mlikiowa/napcat-docker:latest
docker tag docker.1ms.run/mlikiowa/napcat-docker:latest mlikiowa/napcat-docker:latest
同一个镜像换几个站轮着重试,断点续传几次就下来了。
最终架构是三个容器:
QQ ⇄ NapCat(OneBot v11) ⇄ AstrBot ⇄ Hermes Agent ⇄ GLM
AstrBot(3.9 万 star)当对话前端,Hermes 当大脑,GLM Coding Plan 出算力。跑通的那一刻还挺爽的。
🤔 但是:Hermes 在 QQ 里是个"残疾人"
跑起来才发现问题:AstrBot 只是把 QQ 消息转成 API 请求发给 Hermes,Hermes 返回的只能是文本。
它不知道 QQ 的存在。不能发文件、不能发图片、不能主动推送消息——一个能执行命令的强力 agent,在 QQ 里只能动嘴不能动手。
去 NapCat 社区转了一圈:官方有个 PR 给 Hermes 加 NapCat 支持,挂了三个月没人理,review 意见没人改;第三方项目要么 patch 源码(升级就炸),要么 star 只有十几个。
行吧,自己写。
🧐 开发:对着官方文档逐行核对
Hermes 的插件系统是正经设计过的:放 ~/.hermes/plugins/ 目录,实现 BasePlatformAdapter 的三个方法(connect / disconnect / send),通过 register(ctx) 注册,就能获得和 Telegram/Discord 同级的网关待遇——完全不用改源码。
我找了官方 IRC 插件当参考,照着写了:
hermes-napcat/
├── plugin.yaml # 插件清单
├── __init__.py # register(ctx) 注册入口
├── onebot.py # OneBot v11 WS 客户端(重连、API 调用)
├── adapter.py # 平台适配器(消息收发、白名单)
├── tools.py # 7 个 QQ 工具
└── schemas.py # 工具的 JSON Schema
设计上花心思的点:
- 双重白名单取交集:群白名单(插件层过滤,名单外的群连配对提示都不发)∩ 用户白名单(网关层),群里只有指定的人能指挥
- 按需可见:平时完全不读群聊(零 token 消耗、零隐私记录),你问"群里聊了什么"它才调
get_group_msg_history拉记录——"我让他看见他才看见" - 零额外依赖:只用 Hermes 自带的 aiohttp
🐛 实测:五个 bug 的连续剧
代码写完只是开始。上服务器实测,bug 接连有冒出来,一个比一个隐蔽:
| # | Bug | 教训 |
|---|---|---|
| 1 | connect() 多了个 is_reconnect 参数不认识 | 接口签名要看真实调用,文档不一定全 |
| 2 | 工具 handler 写成 async def,返回的是协程对象 | 文档说了 handler 必须是同步函数,我没仔细读 |
| 3 | 消息里的 [CQ:at,qq=xxx] 原样透传给模型 | 收消息要解析 CQ 码,发消息也要把 CQ 转回结构化段 |
| 4 | 闭包里 message = cq_to_segments(message) 直接 UnboundLocalError | 赋值让 Python 判定局部变量,右侧读时还没赋值 |
| 5 | 发文件报 NapCat 错误 1200「识别URL失败」 | Hermes 和 NapCat 是两个容器,路径根本不可达,最后用 base64 内联解决 |
DANGER
最痛的领悟:只测函数不测调用链,等于没测。bug 2 和 bug 4 都是"函数单元是对的、串起来就炸",后来我把测试升级成全链路(同步入口 → 线程桥 → 事件循环 → mock 客户端)才锁住。
有意思的是 bug 3 的发端问题——是 Hermes 自己读了我的插件源码定位到的,它指出了 send() 里没做 CQ 转换。agent 帮我 debug 我写的、用来驱动它的插件,套娃了属于是。
✅ 结果:v0.2.0 已开源
最终版本 9 个 commit,五组回归测试全绿,发布了 v0.2.0:
功能一览:
- QQ 私聊 / 群聊对话(群聊 @ 门控 + 双重白名单)
- 7 个工具:主动发消息、发文件(base64 跨容器投递)、群列表、按需拉群聊记录、成员查询、禁言、踢人
- 断线自动重连(指数退避 + 幂等启动防双连接)
- cron 定时任务结果直接推送到 QQ
现在的完整架构:
入站(对话):QQ → NapCat → AstrBot → Hermes
出站(动作):Hermes → napcat 工具集 → NapCat → QQ
AstrBot 管人设和插件生态,hermes-napcat 给 agent 一双能主动干活的手。
📝 总结
- 先找官方镜像再源码构建,能省半天
- 插件开发前,把参考插件的真实调用方式核一遍,比读十遍文档有用
- 交付前跑全链路测试,函数级测试会漏掉协程、闭包、线程桥这类"串联才炸"的问题
- 跨容器传文件,路径都是虚的,base64 内联才是通用解
- 生态缺什么就写什么,写完就是你的开源项目
从"想通过 QQ 聊天"到"给这个生态填了个空位",一天时间。挺值的。
