dsh-plugin-qqbot

August 14, 2026 · View on GitHub

QQ Bot 传输适配器插件:让 dsh 通过 QQ 机器人聊天直接交互。参考 homeAI 的 platform/ 接入模式,映射到 dsh 的「外部协议驱动」扩展点(对照 docs/cookbook/extension-cookbook.md 与官方 dsh-acp)。

开发必读AGENTS.md 记录了 QQ API v2 官方文档来源、插件接口 ↔ 文档映射与变更核对清单——改任何接口/事件前先读它。

架构

QQ 服务器 ◄─WS(收)/HTTP(发)─► 本插件
                              ├─ src/config.ts          配置 Schema + 环境变量读取
                              ├─ src/types.ts           全部纯类型(QQTarget/QQSendOptions/…)
                              ├─ src/qqbot-client.ts    OpenAPI 发送/上传/撤回/互动(fetch, redirect:'error')
                              ├─ src/qqbot-gateway.ts   WebSocket 接收 → 归一化消息/互动事件
                              ├─ src/sessions.ts        openid/群 → SessionId 双向映射
                              ├─ src/state.ts           目标 → 当前会话 id 持久状态
                              ├─ src/targets.ts         目标台账(群/C2C + 最近 msg_id,$DSH_HOME)
                              ├─ src/stream.ts          节流流式(token 与 QQ HTTP 解耦)
                              ├─ src/inbound.ts         入站消息/互动 → followup(含 ensureAgent、串行锁)
                              ├─ src/outbound.ts        出站 assistant/chunk → QQ(含 turn 看门狗)
                              ├─ src/tools.ts           会话级工具(qq_recall/media/buttons/targets/send_to)
                              └─ src/index.ts           装配入口(Config/生命周期 effect)
                                       │ followup / create / cancel

                              ctx.agents → agent-loop(主循环,dsh 内置,无需自写)

环境与依赖

  • Node 22.19+ / 24(使用原生全局 WebSocket,无需 ws 依赖)。
  • 依赖 @deepseek-ai/cordis 等为 peerDependency(与宿主 dsh 共享同一实例);devDependencies 里同样声明,用于构建时类型解析。

构建

pnpm install        # 安装 devDeps(含 @deepseek-ai/dsh 用于本地集成测试)
pnpm run build      # tsc → lib/(ESM,相对导入的 .ts 自动改写为 .js)

prepare 脚本同样执行构建,因此 git 直装(拿源码)后会自动产出 lib/

接入 dsh(三种方式)

# 本地路径
dsh plugin --profile web add D:/code/dsh-plugin-qqbot
# Git(需 prepare 构建;只信任的仓库)
dsh plugin --profile web add github:you/dsh-plugin-qqbot#<commit>
# npm(去掉 package.json 的 private 后发布)
dsh plugin --profile web add dsh-plugin-qqbot

验证进树:

dsh --profile web --dump-config    # 应看到 "# == dsh-plugin-qqbot" 层与 qqbot 行

配置

在 profile 的 cordis.patch.yml 覆盖 qqbot 行(后层按行覆盖,需重述全部键),或直接在 cordis.patch.yml 改默认:

默认说明
enabledfalse总开关
appId / appSecret''凭证;留空读 QQBOT_APP_ID/QQBOT_APP_SECRET 环境变量(推荐,别把 secret 提交进 git)
workspacePath''QQ 用户绑定的工作区目录(与凭证同为配置);留空读 QQBOT_WORKSPACE;再空则会话无 cwd
provider / model''每个 agent 的模型路由;留空由 agent/request 或其他配置决定
sandboxfalsetrue 走沙箱 API(测试期用);也可设 QQBOT_SANDBOX=true 环境变量
streamIntervalMs300流式批量刷新的节流间隔
msgType2整段发送的消息类型(文档「发送群聊消息」):0=纯文本,2=Markdown;流式仍固定 markdown
turnTimeoutMs600000单轮超时兜底(ms):超时经 dsh 公开的 Agent.cancel() 中断 runaway turn
degradeQueueThreshold5入站积压超过该值进入优雅降级(单聊流式降为整段发送)
allowGlobalSendTofalse安全默认关闭true 时注册全局工具 qq_targets/qq_send_to/qq_send_media_to(任何会话的 AI 都能向台账中已知 QQ 目标被动回复发起文本/富媒体消息);false 时不注册(仅保留 QQ 会话内的对话回复与撤回/富媒体/按钮能力)
sendToMinIntervalMs3000全局主动发送(qq_send_to)对同一目标的最小间隔(ms),防单目标轰炸
sendToMaxPerMinute20全局主动发送全目标合计上限(条/分钟,60s 滑动窗口),防多目标聚合轰炸;0 = 不限
blockedWords内置最小集合出站内容违禁词(命中则拒绝发送到 QQ):qq_send_to 与模型回复(流式/整段)均过滤;内置默认覆盖明确非法/违规交易类词(低误伤),按业务在配置中扩展,不需要可配 []
preset''QQ 会话 agent 挂载的 agent preset(空串 = 部署默认,通常为 standard);挂载后 QQ 会话获得与 Web 会话一致的完整工具集(web_search/pwsh/read/fs/subagent 等);宿主无 agent-presets 服务(如最小 profile)时忽略

安全边界

  • 敏感能力默认关闭:全局主动发送(allowGlobalSendTo)默认 false,需在 cordis.patch.yml 显式开启;开启后任何会话的 AI 都可调用(这正是"任意会话告诉 AI 去发消息"的场景),请仅在你信任的部署开启。
  • QQ 会话工具集与 Web 会话一致:QQ 会话 agent 默认挂载 standard agent preset,因此同样拥有 web_search/pwsh/read/fs 等完整工具(用户要求"全部加回来"——不限制工具,安全上只保证插件自身不可被篡改:出站违禁词、脱敏、realpath、限流等全部保留)。QQ 入站消息可含 prompt injection,恶意指令诱导 agent 执行命令/读文件的风险由部署侧沙箱(dsh-sandbox)与模型注入防护承担——插件侧不额外禁用工具,但所有出站仍过违禁词/脱敏/限流。
  • 被动回复窗口天然限流qq_send_to 依赖台账最近 msg_id(单聊 60 分钟 / 群聊 5 分钟),每条最多回复 4/5 次(PASSIVE_REPLY_LIMIT),窗口过期即拒绝——大幅限制滥用面。
  • 发送节流(两级)qq_send_to 对同一目标有 sendToMinIntervalMs 最小间隔(防单目标轰炸)+ 全目标 sendToMaxPerMinute 滑动窗口上限(防多目标聚合轰炸)。
  • 出站违禁词过滤config.blockedWords(内置默认最小集合 + 配置扩展)命中即拒绝发送——覆盖 qq_send_to/qq_send_buttons 主动发送、模型回复(流式/整段)与 turn 错误补发全部出站路径,命中记审计日志(词 + 调用者 + 目标)。
  • 敏感信息脱敏:错误回执(入站处理失败、turn 错误补发)发送前经 redactSensitive 脱敏——本地路径替换为 [路径]、控制字符压平、限长 500,防宿主内部路径泄露给 QQ 用户。
  • 网关重连退避:WebSocket 重连指数退避(5s → 60s 上限),防持续失败时的重连风暴/日志刷屏。
  • 审计日志:每次全局主动发送记录调用会话、目标与内容长度(不落正文)。
  • 文件外传防护(本地文件上传加固)qq_send_media(filePath) 仅允许会话工作区内文件,且用 realpath 解析符号链接/junction 后校验(工具层 + client 层双重校验,收敛 TOCTOU 窗口);上传前 stat 检查大小上限(200MB,先于读入内存)fileName 消毒(路径分隔符/控制字符移除);atUsers 仅接受 QQ openid 合法字符(防标签注入);fileType 限定 1-4;url 仅接受 http(s)。
  • 持久化写盘防抖:台账/状态 JSON 高频更新合并为一次写(500ms 防抖 + 卸载 flush()),避免同步 IO 阻塞事件循环(群刷屏 DoS 面)。
  • 工具输出消毒:台账昵称/群成员名压平控制字符并限长,防恶意昵称经换行伪造消息污染模型上下文。
  • 凭证不落日志:appId/appSecret/access_token 不打印;业务请求统一 HTTPS + redirect:'error'(含分片 PUT)。
  • 稳定性保障:所有 QQ fetch 带超时(15s 默认、分片 PUT 60s);入站处理每任务 30s 上限;turn 看门狗(turnTimeoutMs 超时 Agent.cancel());分片上传重试预算(upload_config);持久化 fail soft;插件卸载自动清理定时器/流/会话。
  • 已知限制(部署侧):① 工作区内文件(含 .env 若放工作区)可被模型读取并外传——请让 QQ 会话工作区专用、不含敏感文件;② QQ 入站消息可含 prompt injection(恶意指令诱导 agent 执行命令/读文件)——QQ 会话现在与 Web 会话工具集一致(含 pwsh/read 等),缓解依赖部署侧(agent 沙箱、最小权限)与模型注入防护,插件只做文件读取限制与模型侧警示;③ allowGlobalSendTo 开启后任何会话的模型都能看到台账联系人信息。

稳定性设计(适配 dsh 语义,不触碰循环内部)

  • 卡死兜底分层:dsh 自身已覆盖模型流空闲看门狗(streamIdleTimeoutMs)与工具调用 deadline(dsh-tool-call-timeout-policy);本插件补总 turn 时长兜底——观察持久 session/event(turn/start、turn/end),超时调用公开的 Agent.cancel(),对应 agent-loop "No built-in turn budget" 文档给出的实现方式(turnTimeoutMs 默认 10 分钟,测试可调小)。
  • 群成员昵称来自 WS 载荷GROUP_AT_MESSAGE_CREATE / GROUP_MESSAGE_CREATEauthor.username)——QQ 不存在按 openid 查昵称的 HTTP 接口,直接同步读取,不引入任何异步等待面。
  • 优雅降级:目标 agent 入站积压超过阈值 → 单聊流式降为整段发送——主聊天不被非必要功能拖累,降级对用户基本无感。
  • 超时保护与自动恢复:所有 QQ fetch 带超时(AbortSignal.timeout);入站消息处理每任务 30s 上限(超时跳过并回执);网关半开连接探活(无入站超阈值强制重连);昵称等待 2.5s 上限;插件卸载清理全部定时器与流。

已实现 / 已知限制

  • 单聊流式(stream_messages),群聊自动降级为整段发送(msgType 配置:0 文本 / 2 Markdown)。
  • 群聊接收 GROUP_AT_MESSAGE_CREATE(@消息)与 GROUP_MESSAGE_CREATE(群消息全量模式,需机器人开启「接收所有消息」);被动回复自动携带最新 msg_id;群成员前缀 [群员 xxx,昵称(角色)],角色取 author.member_role(管理员/群主标注)。
  • 入站消息归一化(renderInboundText,文档「单聊消息事件」):引用消息(103)带被引用内容、ARK 卡片(3)带标题/描述/来源/链接、图片/文件附件带标记与 URL、语音带 ASR 参考文本——agent 能看到用户发的完整上下文。
  • 被动回复按文档限次:单聊每条消息最多 4 次、群聊 5 次(QQBotClient.sendMessage 超限跳过并告警)。
  • 消息撤回(单聊+群聊):发送接口返回消息 id 并记入撤回台账,会话级 qq_recall 工具在 2 分钟窗口内撤回最近一条(DELETE /v2/{users|groups}/{openid}/messages/{message_id},10 QPS)——模型在用户要求撤回时自动调用,仅 QQ 会话可见该工具;群管理员还可撤普通群成员消息(按事件 d.id,待向模型暴露入站 msg_id 后支持指定 id)。
  • 富媒体发送(msg_type=7):会话级 qq_send_media 工具——公网 URL 直传,或工作区内本地文件分片上传upload_prepare → 按 upload_config 并发 + 重试逐片 PUT+finish → 合并,单聊/群聊端点按场景选择,10 QPS),获取 file_info 后发送图片/视频/语音/文件;file_info 有 ttl,过期需重新上传。
  • QQ 会话 agent 装配:QQ 会话由插件 ctx.agents.create/resume 创建,与 Web 会话一样挂载 agent preset(默认 standardconfig.preset 可指定),因此拥有与 Web 会话一致的完整工具集(web_search/pwsh/read/fs/subagent 等),不再只有 qq_recall/qq_send_media/qq_send_buttons 三个受限工具;宿主无 agent-presets 服务时跳过挂载(最小工具集)。会话级 QQ 工具(撤回/富媒体/按钮)仍只对 QQ 会话可见。
  • QQ 的 HITL(按钮互动):会话级 qq_send_buttons 工具发带内嵌键盘按钮的消息(type=1 回调按钮);用户点击触发 INTERACTION_CREATE——插件立即 PUT /interactions/{id} 回应(防客户端 loading),把按钮 data 作为 [按钮点击] <data> 消息进入 agent 继续处理;消息反馈(点赞/点踩)、清空会话、用户/群授权事件([授权] …)也会进入 agent。
  • 文本交互标签(文档「文本交互」):@某人 <qqbot-at-user>、回车/参数指令(仅单聊)、@全部成员/跳转子频道/表情(频道)等标签原样透传;每个 QQ 会话注入 qq:interaction-capabilities 系统提示段告知模型可用格式。
  • 目标台账 + 主动被动回复发起(全局工具,需 allowGlobalSendTo: true 开启):入站目标(群/C2C)与最近 msg_id、最近昵称、**群成员映射(member_openid→昵称)**持久化到 $DSH_HOME/dsh-plugin-qqbot-targets.json全局工具 qq_targets(列台账、有效期与群成员)+ qq_send_to(携带最近 msg_id、递增 msg_seq、atUsers 群内 @,被动回复方式、不用主动消息)+ qq_send_media_to(同一被动回复方式主动发送富媒体:URL 直传或工作区内本地文件分片上传,fileType 1=图片 2=视频 3=语音 4=文件)——任意 dsh 会话里告诉 AI「去群里向 xx 用户发消息/发张图」,AI 即可主动向指定群(含 @ 群成员)或指定用户发起文本或富媒体;msg_id 过期或目标未发过消息则拒绝并提示;三者共享每目标节流与全局限额。
  • 无自有命令系统:所有 QQ 消息都作为普通用户消息进入 agent(commandPrefix 等命令路由已移除)。
  • 未实现:频道收发、srv_send_msg、互动类型 15/16/20(故事集/切换模型/群授权状态变更,仅日志)、引用回复(message_reference)、主动消息/互动召回(is_wakeup)、HITL 到 dsh ask_user/approval 的桥接、reasoning 分气泡、会话切换命令。

设计要点(对齐 dsh 规范)

  1. 注册即效应:WS 连接、事件监听、agent 全走 ctx.effect(),插件卸载(HMR)自动断开并 dispose 所有 agent,不留孤儿会话。
  2. 模型可见 ⟺ 已记录:QQ 消息走 followup(createUserMessage(...)),自然落进 session 日志;插件不另存对话。
  3. 稳定身份SessionId('qq:<type>:<id>') 做会话身份,配 sessionPersistence 即可跨重启续聊。
  4. 带凭证 HTTP redirect:'error':拒绝跟随重定向,防凭证转发(对齐 dsh packages/web 的规矩)。