dsh-qqbot-lite
Verifieddsh-qqbot-lite · v1.1.9 · MIT · Web UI
DSH 的轻量 QQ 机器人插件(零运行时依赖):WebSocket 收群聊/私聊消息接入 dsh 会话,并提供 qq_message / qq_group / qq_bot 三个工具供 AI 主动调用(发消息、发文件、读消息、按需下载附件、群管理、查状态)。自带「设置 → QQ 机器人」页面,支持手机 QQ 扫码绑定,无需手抄凭据。
Install
dsh plugin add dsh-qqbot-lite Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
dsh-qqbot-lite
QQ 机器人插件(自研轻量实现,零运行时依赖)。
让 AI 能主动调用工具收发 QQ 消息:WebSocket 收消息接入 dsh 会话,
并提供一组 qq_* 工具供模型自主调用。自带「设置 → QQ 机器人」页面,
手机 QQ 扫码即可绑定,不用手抄凭据。
为什么自研
| 方案 | 依赖体积 | 问题 |
|---|---|---|
@zaofan/dsh-qqbot |
~150MB | 带 onnxruntime-node(113MB)、sharp、transformers,安装时踩过 ERR_PNPM_IGNORED_BUILDS |
dsh-qqbot-ui(官方 0.5.0 重打包) |
4 个依赖 | 可用,但仍依赖腾讯 SDK |
| 本插件 | 0 个依赖 | 只用 Node 内置能力 |
零依赖是怎么做到的:
- HTTP → Node 内置
fetch(>= 18) - WebSocket → Node 内置
WebSocket(>= 22,已实测可用) - 加密/压缩 →
node:crypto/node:zlib - 不 import 任何
@deepseek-ai/*包(link:安装时解析不到宿主的 peerDependencies, 会直接ERR_MODULE_NOT_FOUND;本插件改用ctx.get()拿服务 + 动态 import 兜底)
提供的工具
工具按领域合并,用 action 参数分派具体操作——每个工具的描述都要进系统提示,
工具越多上下文越贵,所以只暴露 3 个:
| 工具 | action | 作用 |
|---|---|---|
qq_message |
send |
向指定群/私聊发文本(被动优先,超限自动转主动;长文自动分条) |
send_file |
上传并发送本地文件(图片/视频/语音/普通文件) | |
read |
主动拉取消息,返回【新消息】+【历史消息】两区,供 AI 自行判断要不要回 | |
download |
按需下载超过闸门的附件(参数:message_id、index) | |
recall |
撤回自己发的消息(QQ 限制:2 分钟内) | |
qq_group |
info |
群信息(群号、成员数、群主) |
members |
群成员列表(⚠️ 多数机器人无此权限,返回 11253) | |
restrict_get / restrict_set |
读/写禁言设置 | |
join_requests |
待审批入群申请列表 | |
approve_join |
批准/拒绝入群申请 | |
remove_members |
批量踢人(可选拉黑) | |
qq_bot |
info |
机器人自身身份(名字、id、AppID、已知会话数) |
targets |
列出已知会话,供 AI 寻址 | |
status |
网关连接状态(是否正在收消息) |
关键设计:回复当前会话不需要工具——AI 的正文输出由出站层
(监听 session/event 的 assistant/message)自动转成 QQ 消息。
工具只补「正文做不到的事」:发到别的会话、发媒体、读群聊、群管理、查状态。
为什么要 read
插件的唤醒门控只在被 @ 时叫醒 AI(避免刷屏),但这样 AI 被叫醒时
看不到群里刚才还聊了什么。所以每条消息(含没 @ 机器人的闲聊)都写进
$DSH_HOME/qqbot/messages.json,AI 可以主动调 qq_message + action=read 来读:
【新消息】2 条
08:19 しい: 肥鱼[表情: 呲牙]
08:20 恸: 可以的
↩ 引用 某人:这个软件那可以刷视频吗?
【历史消息】1 条
08:19 Elyisum: [图片: flower.jpg 800x600] 康康我的花
读取即推进游标——下次再读,这批「新消息」就变成「历史消息」。
mark_read: false 可只预览不消费。AI 自己发的消息也会记进去(显示为 我:),
因为 QQ 不会把机器人自己的消息推回来。
文本解析:为什么不能直接把 content 喂给 AI
QQ 的 content 里塞着 AI 读不懂的原始标记,直接喂会造成理解偏差。
lib/text-parsing.js 负责把它们变成人话(规则对齐官方 SDK 的
protocol/utils/text-parsing.js):
| 原始形态 | 解析后 | 说明 |
|---|---|---|
<faceType=1,faceId="13",ext="eyJ0ZXh0Ijoi5ZGy54mZIn0="> |
[表情: 呲牙] |
ext 是 base64 的 JSON,取 text 字段 |
<faceType=6,faceId="0",ext="...">(ext 无 text) |
[表情包] |
自定义大表情,真实画面在图片附件里 |
[图片] |
[图片: a.jpg 1239x1391] |
带文件名和尺寸 |
| 48×48 的小图 | [表情包: x.jpg 48x48] |
按尺寸判定为表情包 |
attachments[].asr_refer_text |
[语音: 你好啊] |
QQ 自带语音识别 |
msg_elements[0] |
↩ 引用 某人:原文 |
被引用的消息 |
引用解析的两个来源(优先级从高到低):
msg_elements[0]—— QQ 直接把被引用内容推过来,最可靠- 本地流水按
ref_msg_idx查找 —— QQ 没推时回退到自己的缓存
两个都拿不到时会留下 ↩ 引用了一条消息(原文未能取到) 这个明确标记,
而不是静默丢弃 —— 让 AI 知道「这是条引用」,好过让它误以为这是句独立的话。
附件存储
群里每条消息的附件都会下载(不只被 @ 的)—— AI 主动回看群聊时,
图片和文件得已经在本地,否则它只看到一句 [图片],不知道画的是什么。
两道闸门:
| 闸门 | 默认值 | 行为 |
|---|---|---|
| 单文件 | 10MB | 超过就不下,流水里留 URL,标 [图片未下载: 超过 10MB 闸门;可按需下载] |
| 总量 | 500MB | media 目录超限时,按修改时间删最旧的,删到 90% 水位 |
按需下载:超过单文件闸门的附件,AI 可以用
qq_message + action=download(参数 message_id、index)单独拉取 ——
只在真要看的图上花代价。
⚠️ QQ 的附件 URL 带
rkey,会过期。过期后按需下载会失败, 只能让用户重发。所以默认策略是"能下就下",而不是"用时再下"。
清理策略的细节:
- 删到 90% 水位而非刚好等于上限,避免每下载一张图都触发一次全目录扫描
- 至少保留 1 个文件,防止单个超大文件导致目录被清空
寻址方式
AI 记不住 32 位 openid,所以 target 支持:
- 群名 / 用户名(模糊匹配,同名时返回候选让 AI 选,不猜)
- 完整 openid
group:<openid>/c2c:<openid>显式前缀
地址簿在收到消息时自动登记,持久化到 $DSH_HOME/qqbot/targets.json。
被动回复窗口(最关键的坑)
| 场景 | 被动回复有效期 | 单条消息可回次数 |
|---|---|---|
| 群聊 | 5 分钟 | 5 次 |
| 单聊 | 60 分钟 | 4 次 |
- 带
msg_id= 被动回复(不耗主动配额,但受时效/次数限制) - 不带
msg_id= 主动推送(无时效,但用户可关闭 + 频控 + 每日 1000 条上限)
本插件策略:被动优先;窗口过期或次数用尽自动降级为主动推送,
并在工具返回值里如实标注走了哪种模式。同一 msg_id 的 msg_seq 自动递增。
安装
dsh plugin --profile web add dsh-qqbot-lite
装完重启 dsh 才会加载(dsh plugin add 只改磁盘,不影响已运行的进程)。
重启后打开 设置 → QQ 机器人,点「扫码绑定」,用手机 QQ 扫二维码即可
——不用去 QQ 开放平台手抄凭据。凭据会自动写进 profile 的
cordis.patch.yml,再重启一次生效(插件在启动时读配置)。
也可以完全跳过界面,手动填配置(见下节)。
扫码配对
插件自带一个「设置 → QQ 机器人」页面,走的是 QQ 官方的扫码绑定流程:
本地生成 32 字节随机 key
↓
POST q.qq.com/lite/create_bind_task { key } → task_id
↓
二维码内容 = q.qq.com/qqbot/openclaw/connect.html?task_id=...
↓
手机 QQ 扫码确认
↓
轮询 POST q.qq.com/lite/poll_bind_result { task_id }
→ status=2(COMPLETED) 时返回 bot_appid + bot_encrypt_secret
↓
appSecret = AES-256-GCM 解密(key 就是本地那个随机 key)
↓
凭据写入 profile 的 cordis.patch.yml
这套协议只用 Node 内置能力复现(node:crypto + fetch),不依赖腾讯的
@tencent-connect/qqbot-connector。二维码编码算法内置在
lib/vendor/qrcode/(MIT,见各文件头部版权声明),因此本插件对外仍是零依赖。
宿主侧只暴露三个接口,绝不下发 appSecret:
| 接口 | 作用 |
|---|---|
GET /qqbot-lite/status |
配置状态、网关连接状态、扫码进度 |
POST /qqbot-lite/qr/start |
开一个扫码会话(未过期则复用),返回二维码 SVG |
POST /qqbot-lite/qr/cancel |
中止当前扫码会话 |
配置
- id: dsh-qqbot-lite
config:
appId: "你的AppID"
appSecret: "你的AppSecret"
preset: qqbot # 可选:收到消息时创建会话所用的 agent preset
botOpenid: "..." # 开了全量消息后必填:机器人自己的 openid(用于识别被 @)
maxAttachmentBytes: 10485760 # 可选:单附件下载上限,默认 10MB
maxMediaBytes: 524288000 # 可选:media 目录总量上限,默认 500MB
debug: false # 可选:调试日志
intents: 33554432 # 可选:覆盖 intents(默认 1<<25 群+单聊)
baseUrl: "" # 可选:覆盖 API 基址
dshHome: "" # 可选:覆盖 $DSH_HOME
凭据解析顺序:config > 环境变量 QQBOT_APPID / QQBOT_SECRET。
缺凭据不会阻塞启动:插件照常激活(配置树里可见),只是不连网关。
架构
lib/
index.js 插件入口:串起各层,注册工具,绑定生命周期
api.js REST 层:token 管理(提前 5 分钟刷新)+ 网关地址缓存 + 错误人话化
gateway.js WebSocket:HELLO/IDENTIFY/RESUME/心跳/重连退避/关闭码处置
inbound.js 入站:QQ 事件 → dsh agent 会话(sessionId 确定性派生,可 resume)
sender.js 发送层:被动优先,超限自动降级主动
reply-window.js 被动回复窗口:按官方规则计数与过期
store.js 地址簿:openid ↔ 人类可读名称
message-log.js 消息流水:每条消息都存(含未 @ 的),带已读游标供 AI 回看
text-parsing.js 入站文本解析:表情 base64 解码 / 引用还原 / 附件摘要
attachment.js 附件存储:全部下载 + 大小闸门 + 空间配额 + SSRF 防护
tools.js AI 可调用的工具定义(3 个工具,action 分派)
会话映射
sessionKey = `qqbot:${appId}:${scope}:${peerId}`
sessionId = SHA-256(sessionKey) 派生的 UUID
确定性派生 → 重启后能自动 resume 回同一个会话,历史不丢。
消息体组装(对齐真实运行记录)
群聊:[昵称 (openid)] 正文 (@you)
单聊:正文(不加前缀)
附件:- Image: /path/to/x.jpg (在发送者标签之前)
语音:- ASR: <QQ 自带的识别文本> ← 不用自己跑 STT
安全
- 附件下载有 SSRF 防护:只允许 HTTPS,阻断指向私有网段 (10/8、172.16/12、192.168/16、127/8、169.254/16、224/4)的 URL。 附件 URL 来自外部消息,不校验就可能被用来探测内网。
- 附件单文件上限 10MB,超时 120 秒;media 目录上限 500MB(可配)。
- 工具名只用
[a-zA-Z0-9_-](出现@等字符会让 LLM API 整轮拒绝)。
已知限制
- 未实现:互动按钮、定时任务、表情包库、单聊流式
- 群成员列表 / 批量踢人接口对本机器人多数无权限(
11253),需平台授权 - 群全量消息(
GROUP_MESSAGE_CREATE)需在 QQ 开放平台开启「接收所有消息」 - 开启全量消息后,@ 消息也走
GROUP_MESSAGE_CREATE,必须配botOpenid才能识别被 @(否则会被唤醒门控拦掉) intents带无权限位会被网关直接拒连,默认只用1<<25
参考文档
调研成果在 /root/dsh/qqbot-api-docs/:
README.md— 索引与关键结论01-收消息链路调研.md— 时序、op 码表、事件类型、消息体、踩坑清单02-API接口清单.md— REST 端点(带实测状态)、SDK API、错误码映射03-真实运行记录还原.md— 从本机历史日志还原的真实行为与工具 schema