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 改默认:
| 键 | 默认 | 说明 |
|---|---|---|
enabled | false | 总开关 |
appId / appSecret | '' | 凭证;留空读 QQBOT_APP_ID/QQBOT_APP_SECRET 环境变量(推荐,别把 secret 提交进 git) |
workspacePath | '' | QQ 用户绑定的工作区目录(与凭证同为配置);留空读 QQBOT_WORKSPACE;再空则会话无 cwd |
provider / model | '' | 每个 agent 的模型路由;留空由 agent/request 或其他配置决定 |
sandbox | false | true 走沙箱 API(测试期用);也可设 QQBOT_SANDBOX=true 环境变量 |
streamIntervalMs | 300 | 流式批量刷新的节流间隔 |
msgType | 2 | 整段发送的消息类型(文档「发送群聊消息」):0=纯文本,2=Markdown;流式仍固定 markdown |
turnTimeoutMs | 600000 | 单轮超时兜底(ms):超时经 dsh 公开的 Agent.cancel() 中断 runaway turn |
degradeQueueThreshold | 5 | 入站积压超过该值进入优雅降级(单聊流式降为整段发送) |
allowGlobalSendTo | false | 安全默认关闭:true 时注册全局工具 qq_targets/qq_send_to/qq_send_media_to(任何会话的 AI 都能向台账中已知 QQ 目标被动回复发起文本/富媒体消息);false 时不注册(仅保留 QQ 会话内的对话回复与撤回/富媒体/按钮能力) |
sendToMinIntervalMs | 3000 | 全局主动发送(qq_send_to)对同一目标的最小间隔(ms),防单目标轰炸 |
sendToMaxPerMinute | 20 | 全局主动发送全目标合计上限(条/分钟,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 默认挂载
standardagent 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_CREATE的author.username)——QQ 不存在按 openid 查昵称的 HTTP 接口,直接同步读取,不引入任何异步等待面。 - 优雅降级:目标 agent 入站积压超过阈值 → 单聊流式降为整段发送——主聊天不被非必要功能拖累,降级对用户基本无感。
- 超时保护与自动恢复:所有 QQ fetch 带超时(
AbortSignal.timeout);入站消息处理每任务 30s 上限(超时跳过并回执);网关半开连接探活(无入站超阈值强制重连);昵称等待 2.5s 上限;插件卸载清理全部定时器与流。
已实现 / 已知限制
- 单聊流式(
stream_messages),群聊自动降级为整段发送(msgType配置:0文本 /2Markdown)。 - 群聊接收
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(默认standard,config.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 直传或工作区内本地文件分片上传,fileType1=图片 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 规范)
- 注册即效应:WS 连接、事件监听、agent 全走
ctx.effect(),插件卸载(HMR)自动断开并dispose所有 agent,不留孤儿会话。 - 模型可见 ⟺ 已记录:QQ 消息走
followup(createUserMessage(...)),自然落进 session 日志;插件不另存对话。 - 稳定身份:
SessionId('qq:<type>:<id>')做会话身份,配sessionPersistence即可跨重启续聊。 - 带凭证 HTTP
redirect:'error':拒绝跟随重定向,防凭证转发(对齐 dshpackages/web的规矩)。