Skip to content

dsh-qqbot-lite

Verified

dsh-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] ↩ 引用 某人:原文 被引用的消息

引用解析的两个来源(优先级从高到低):

  1. msg_elements[0] —— QQ 直接把被引用内容推过来,最可靠
  2. 本地流水按 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