简介:MoltbotOneBotv11协议插件项目面向需要在非官方环境中集成QQ通信能力的开发者与进阶用户,基于OneBot v11协议,通过NapCat、Lagrange等第三方客户端连接QQ,实现私聊与群聊中文字、图片、语音、视频及文件等多类型消息的收发处理,并附带自动解压能力,便于处理压缩包形式的传输内容。资源包共12个文件,以TypeScript源码为主,涵盖types、api、channel、runtime等核心模块,另含package.json、tsconfig.json等工程配置、README与说明文档及附赠资料,整体约67KB,结构紧凑,便于快速理解插件架构与接入方式。目前已有85人学习关注。对于希望将QQ通信嵌入自有开发流程或企业协作场景的读者,可借助源码与文档掌握协议对接、消息解析与配置方法,并在此基础上扩展更复杂的通讯功能,同时需留意第三方客户端带来的隐私与安全风险。
1. 从 Moltbot 到 OneBotv11:QQ 机器人消息链路到底怎么打通
很多人第一次接触 QQ 机器人,是拿现成框架跑个「收到消息回个复读」,但真到要接私聊、群聊、图片、语音、视频、文件全类型消息时,链路就开始玄学了。Moltbot 的 OneBotv11 协议插件项目,解决的就是这件事:它不直接和 QQ 通信,而是通过 NapCat、Lagrange 这类第三方客户端把 QQ 消息转成 OneBotv11 标准事件,再由插件做统一收发。换句话说,你写的业务逻辑只面向 OneBotv11 协议,底层换 NapCat 还是 Lagrange 基本不用改代码。这套方案适合想自建 QQ 机器人、又不想被单一客户端绑死的开发者,尤其是需要处理富媒体消息和自动解压文件包的场景。下面按「协议是什么 → 怎么接 → 参数怎么调 → 坑在哪」推一遍。
2. OneBotv11 协议与 NapCat/Lagrange 的分工:谁负责什么
2.1 三层结构:QQ 客户端、协议实现、业务插件
先把链路拆清楚,不然后面排错会找不到北。最底层是 QQ 客户端本身,它负责登录、维持在线、收发包;中间层是 NapCat 或 Lagrange,它们以无头或带界面的方式挂载 QQ,把 QQ 的私有协议翻译成 OneBotv11 标准事件,并通过正向 WebSocket 或反向 WebSocket 暴露出来;最上层才是 Moltbot 的 OneBotv11 插件,它只认协议字段,不关心底层是哪个客户端。
这种分层的价值在于:NapCat 更新了、Lagrange 换版本了,只要 OneBotv11 字段没变,插件代码不用动。常见做法是把连接配置抽成独立配置文件,客户端地址、端口、access_token 都放进去,换客户端只改配置。
OneBotv11 的核心字段其实不多,消息收发主要围绕post_type、message_type、message、raw_message、user_id、group_id这几个。理解它们,后面写处理逻辑就是查表。
2.2 正向与反向 WebSocket 怎么选
NapCat 和 Lagrange 都支持两种连接方向,选错了会出现「机器人收不到消息」或「发不出去」的经典翻车。
正向 WebSocket 是插件主动连客户端,配置里写客户端的ws://127.0.0.1:3001。适合插件和客户端在同一台机器、插件启动晚于客户端的场景。反向 WebSocket 是客户端主动连插件,插件要起一个 WS 服务端,配置里填插件的地址。适合插件部署在远程、客户端在本地,或者需要多客户端连同一个插件的场景。
我一般本地开发用正向,省得自己维护服务端;上线多实例时用反向,方便统一入口。下面是一个正向连接的最小配置示例,字段名按 OneBotv11 通用约定写:
{ "ws_url": "ws://127.0.0.1:3001", "access_token": "your_token_here", "reconnect_interval": 5000, "heartbeat_interval": 30000 }ws_url指向 NapCat 或 Lagrange 暴露的地址,端口以客户端实际配置为准,常见是 3001 或 6700。access_token必须和客户端侧一致,否则握手直接被拒。reconnect_interval是断线重连间隔,单位毫秒,设太小会疯狂重连刷日志,设太大断线后消息会丢一段。heartbeat_interval是心跳间隔,用来检测连接是否还活着,一般 30000 够用。
提示:access_token 不要留空,哪怕本地测试也建议设一个,否则同机其他程序可能误连。
2.3 消息事件的最小处理骨架
连接通了之后,第一件事是能打印出收到的原始事件,确认字段长什么样。不同客户端在富媒体字段上会有细微差异,先看原始数据再写解析,能省掉大量猜测。
import json import websockets async def on_message(ws): async for raw in ws: event = json.loads(raw) # 只处理消息事件,忽略心跳和元事件 if event.get("post_type") != "message": continue msg_type = event.get("message_type") # private 或 group user_id = event.get("user_id") group_id = event.get("group_id") message = event.get("message") # 消息段数组 print(f"[{msg_type}] user={user_id} group={group_id}") print(json.dumps(message, ensure_ascii=False))这段代码只做一件事:把post_type为message的事件挑出来,打印消息类型、发送者和消息段。message字段是数组,每个元素是一个消息段,形如{"type": "text", "data": {"text": "你好"}}。先跑通这一步,确认能收到私聊和群聊,再往下做业务。
3. 私聊群聊与富媒体消息的落地处理
3.1 文字、图片、语音、视频、文件的消息段解析
OneBotv11 把不同消息类型统一成消息段数组,这是它比早期 CQ 码更清晰的地方。常见类型和关键字段如下:
| 消息段 type | 关键 data 字段 | 说明 |
|---|---|---|
| text | text | 纯文本内容 |
| image | file, url | file 可能是文件名或路径,url 是下载地址 |
| record | file, url | 语音,格式多为 amr/silk |
| video | file, url | 视频 |
| file | file, name | 文件消息,name 是原始文件名 |
解析时不要假设file一定是本地路径,NapCat 和 Lagrange 在不同配置下可能给文件名、也可能给 URL。稳妥做法是优先用url下载,没有url再按file处理。
def parse_segments(message): result = {"text": "", "images": [], "records": [], "videos": [], "files": []} for seg in message: t = seg.get("type") data = seg.get("data", {}) if t == "text": result["text"] += data.get("text", "") elif t == "image": result["images"].append(data.get("url") or data.get("file")) elif t == "record": result["records"].append(data.get("url") or data.get("file")) elif t == "video": result["videos"].append(data.get("url") or data.get("file")) elif t == "file": result["files"].append({"name": data.get("name"), "src": data.get("url") or data.get("file")}) return resultparse_segments把消息段按类型归拢,文本拼接,媒体存列表。注意file段单独存了name,因为文件消息后续要按原名保存或解压,丢了名字会很麻烦。
3.2 发送私聊和群聊消息的两种调用
收消息靠事件,发消息靠 API 调用。OneBotv11 发送消息统一走send_msg,通过message_type区分私聊和群聊。
async def send_private(ws, user_id, text): payload = { "action": "send_msg", "params": {"message_type": "private", "user_id": user_id, "message": text}, "echo": "send_private" } await ws.send(json.dumps(payload)) async def send_group(ws, group_id, text): payload = { "action": "send_msg", "params": {"message_type": "group", "group_id": group_id, "message": text}, "echo": "send_group" } await ws.send(json.dumps(payload))echo字段是回执标识,客户端处理完会带同样的echo返回结果,方便你确认哪条消息发成功了。发图片或文件时,message换成消息段数组,例如[{"type": "image", "data": {"file": "file:///path/to/a.jpg"}}]。file支持file://、http://和 base64,具体支持哪种看客户端,NapCat 对本地路径支持较好,Lagrange 更推荐 URL。
注意:群聊发送频率过高会被风控,批量发送时加 1 到 3 秒随机间隔,别问我怎么知道的。
3.3 自动解压文件消息的实现思路
标题里提到「自动解」,落地时通常是收到file段后下载、判断类型、解压、再回传结果。这里有两个关键点:一是压缩包可能带密码,二是解压路径要防目录穿越。
import os import zipfile def safe_extract(zip_path, dest_dir): os.makedirs(dest_dir, exist_ok=True) with zipfile.ZipFile(zip_path) as zf: for member in zf.namelist(): # 阻止 ../ 穿越到目标目录之外 target = os.path.realpath(os.path.join(dest_dir, member)) if not target.startswith(os.path.realpath(dest_dir)): raise ValueError(f"非法路径: {member}") zf.extractall(dest_dir)safe_extract先遍历文件名做路径校验,再统一解压。dest_dir建议按user_id或group_id分目录,避免不同用户文件互相覆盖。带密码的包先尝试无密码,失败后回消息提示用户提供密码,不要硬编码密码列表去爆破。
4. 避坑与排查:连接、消息、风控三类高频问题
4.1 连不上或频繁掉线
现象:插件日志一直重连,或者连上几秒就断。原因通常是access_token不一致、端口被占用、或者客户端没开启对应 WS 服务。解决:先确认客户端侧 WS 服务已启用并记下端口,再用curl或简单 WS 客户端测连通性,token 两边逐字符比对。掉线频繁还要看heartbeat_interval是否小于客户端超时时间。
4.2 收到消息但字段缺失
现象:能收到事件,但message为空或图片没有url。原因是客户端配置里关闭了富媒体上报,或者消息段类型不在你解析范围内。解决:先打印原始 JSON,确认客户端实际发了什么;再对照客户端文档开启对应上报选项。不同客户端对file字段的填充策略不同,别照搬另一家的解析代码。
4.3 发消息没反应或报错
现象:send_msg发出去了,但没收到回执,或回执里status是failed。常见原因是user_id或group_id类型不对(有的客户端要字符串,有的要数字),或者机器人不在该群、被禁言。解决:统一用整数传 ID,先发一条纯文本测试,确认基础通道没问题再发富媒体。
4.4 文件解压相关翻车
现象:解压报错、文件乱码、或者解压出一堆无关文件。原因是压缩包编码不是 UTF-8、或者包含绝对路径。解决:解压前先读文件名列表,遇到乱码用cp437重新解码再试;路径校验必须做,别直接extractall。
4.5 风控与频率限制
现象:机器人突然不回消息,或账号被限制。原因是短时间大量发送、重复内容、被多人举报。解决:控制发送频率,内容做随机化,群发场景加队列和延迟。这块没有后悔药,只能提前设计限流。
5. 进阶:把消息处理做成可扩展的插件管线
5.1 用中间件拆分解析、路由、回复
当消息类型变多,把所有逻辑塞进一个on_message会很快失控。我一般拆成三段:解析层把原始事件转成统一内部结构,路由层按message_type和关键词决定交给哪个处理器,回复层只负责调send_msg。这样加新功能只需注册新处理器,不动主循环。
class Pipeline: def __init__(self): self.handlers = [] def register(self, handler): self.handlers.append(handler) async def dispatch(self, ctx): for h in self.handlers: if await h.match(ctx): await h.handle(ctx) returnmatch决定是否处理,handle执行具体逻辑。ctx里放解析后的文本、媒体列表、发送者信息。这样私聊和群聊可以共用同一批处理器,只在match里判断来源。
5.2 验证消息链路是否真的通了
别只看日志说「已连接」,要端到端验证。我习惯用三步:第一步,手动在 QQ 发一条私聊,确认插件打印出事件;第二步,让插件回一条固定文本,确认能发出去;第三步,发一张图片和一个压缩包,确认媒体下载和解压都正常。三步都过,才算链路真正打通。
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 收私聊 | 向机器人发「ping」 | 日志出现 private 事件 |
| 发私聊 | 触发回复逻辑 | QQ 收到文本 |
| 收群聊 | 群里 @机器人 | 日志出现 group 事件 |
| 富媒体 | 发图片和 zip | 图片可下载,zip 可解压 |
5.3 一个具体技巧:用 echo 做请求响应配对
OneBotv11 的echo字段常被忽略,但它能解决「发了消息不知道成没成」的问题。每次发 API 请求带一个唯一echo,在接收循环里匹配回执,就能做超时重试和失败告警。我一般用uuid4生成 echo,存一个待确认字典,收到回执就删,超时未删就重发或记录。这个习惯让我少踩了很多「以为发出去了其实没有」的坑。
这套方案值不值得做,取决于你是否需要长期维护一个多类型消息的 QQ 机器人。如果只是临时玩票,现成框架够用;如果要接业务、要换客户端不换代码、要处理文件和富媒体,Moltbot 的 OneBotv11 插件这条路是稳的。希望帮到你。
本文还有配套的精品资源,点击获取