LingYuYo/dsh-qq-guard

dsh-qq-guard

🤖 100% 由 AI 编写完成。把 QQ 接入 DeepSeek Harness 的桥接插件。提供执行期安全门禁、外部插件沙箱、模型无痕热切、看门狗失败降级与完整 Web UI。零外部 npm 依赖。

待分类ai-generateddeepseekqq-bot

dsh-qq-bridge

把 QQ 接进 DeepSeek Harness:QQ 私聊/群消息变成 DSH 会话里的用户消息,agent 的回复发回 QQ。

每个 QQ 会话对应一个独立的 DSH 会话,统一归组到「QQ 聊天」工作区。QQ 会话固定用 qq-chat preset,由本仓库的 @local/dsh-qq-guard 插件提供执行期工具门禁(deny-by-default),确保 QQ 那头的人拿不到本机执行能力。

QQ ──► NapCat (OneBot v11 正向 WS) ──► qq-bridge ──► DSH Web API
                                        ▲                  │
                                        └── agent 回复 ─────┘

版本演进

当前版本:v0.5.1(2026-10-24)

本版做了什么:修 v0.5.0 的真机盲点——老 QQ 会话拿不到新工具(模型说「没有发图工具」)。

  • 根因:DSH 会把每个会话首次 `request/header` 冻成工具基线,之后新增的工具只以

tool-addition 增量下发。v0.5.0 之前建的 QQ 会话,基线里没有 qq_send_image;在 opencode-go/deepseek 这条路由上模型始终看不到它(真机验证:同一句话在新建 qq-chat 会话里模型会正常调用 qq_send_image,在老会话里连显式点名都拒绝)。重启 DSH 不够—— 工具基线随会话历史持久化。

  • 修复:guard 的 /config 新增 toolEpoch(工具能力版本);桥接给每条会话记下建会话时的

epoch,发现不一致就在下一条消息时把该会话的 DSH 上下文自动重建一次(旧会话归档、不删历史), 新工具随之进入基线。启动时会同步拉一次热配置,避免重启后第一条消息仍落在旧基线。

  • 顺带:人设签名纳入 features(设置页刚打开技能时【能力】段会立刻重推);系统提示词新增

【工具可用性(以本段为准)】权威块,压掉自定义人设/历史里「这里没有发语音/发图工具」这类过时说法; qq_send_image 描述明确「对方要图就直接调用,别回答没有发图工具」。

  • 开关:不想自动重置上下文的话,在 qq-bridge/config.json 写

{ "capability": { "autoRefreshOnUpgrade": false } },之后需要自己「清空上下文」一次。

  • 回归:离线自测 204 条 bridge 断言 + guard 全量全绿(新增 epoch 重建 / 权威能力段 / 启动同步 9 条)。

上一版(v0.5.0,2026-10-24):四项 P2 收口 —— 发图片、外部插件真实生效、桥接性能、UI 手感。

  • 发图片 / 表情包:新增 qq_send_image 工具(官方 defineTool 编译参数,url / file 二选一),

桥接用 OneBot image 段发送并等回执再回答模型(下载/拒收的失败原因原样抛给模型)。 设置页「技能与插件」新增 sk-image(老配置里没有也显示,默认开),系统提示词同步引导模型使用。

  • 外部插件真实生效(P2):启用「自包含」外部插件后沙箱进程常驻;桥接收消息时把

message / before-context 事件转发进沙箱执行插件 hook(可改写文本、丢弃消息、追加外部上下文); 插件 registerTool() 注册的工具自动代理成 qq_ext_ 暴露给 QQ 会话(沙箱单飞 + 10s 超时)。 失败一律 best-effort,不影响聊天;GET /dsh-qq-guard/external/runtime 可看常驻状态 / 已加载 hook 与工具。

  • 桥接性能:模型路由 / 人设下发按签名去重(配置没变不再每条消息 POST)、会话存在性正结果缓存 30s、

图片 ingress 后台预取(并发 2 / LRU 32 / TTL 60s,复用 in-flight Promise)、TTS 语音指令脱离主串行链。

  • UI 手感:开关与保存改为乐观更新(点击立刻生效,写盘失败回滚并报错)、输入框改失焦 / 回车保存、

2–3 秒轮询全部下沉到叶子组件(切 Tab / 轮询不再带着整个面板重渲染)、耗时按钮加纯 CSS spinner。

  • 回归:离线自测扩到 260+ 条(新增发图 / hook / 性能 / 乐观更新断言),全绿。

上一版(v0.4.0,正式版,2026-10-24):把 QQ 桥接从「能跑但一堆静默失效」推到「人设、语音、搜索三条主链路都真机打通」。

  • 人设真的生效了(两轮才查到底):
  • 第一轮的 P0 —— guard 的 preset 作用域判定用错 API,applyGuard() 第一步就 return,

人设 section / qq_send_voice / restrict / 执行期白名单四件事全没跑(当时 QQ 会话 既没有系统人设、也没有执行期门禁)。改成 composedPreset + restrict 探针两层判定、 兜底 fail-closed,并新增「真宿主 + 真实 mountPreset」的回归测试。

  • 第二轮 —— 系统提示词里确实是我们的 complete 人设(6554 字实锤),provider 也不覆盖

system(直连 API 验证)。真正盖掉它的是 dsh-mnemon:把 USER.md / MEMORY.md 快照当 user 消息插进每一步。现在 guard 在 agent 创建之后用 {prepend:true} 挂到 agent.ctx 上,把这类消息摘掉。这也是隐私修复:此前群友对话能让模型读到主人的长期记忆。 (顺序陷阱有真 cordis 对照实验:装载时 prepend / push 都会泄漏,只有「agent 创建后 prepend」干净。)

  • 语音真机打通:微软 Edge 朗读接口已被端拒(握手 403 / WS 1006,上游改用 Sec-MS-GEC v2)。

换成免 Key 的 baidu provider(默认),edge 失败自动降级;qq_send_voice 改成等桥接回执, 失败直接抛错给模型。真机回执:已发送语音(baidu,96KB,via=base64,retcode=0) → QQ 收到语音条。

  • 网页搜索可配:根因不是白名单,是缺 DEEPSEEK_API_KEY。「网页搜索」齿轮里可填

Key / baseUrl / model,经 /config 热同步写进 DSH 的 web-search-deepseek,2 秒生效。

  • 单群独立响应档位:每个群可单独配 0–3 档(落盘 sessions.json 的 activityLevel,

null = 跟随全局),私聊不受影响。

  • 技能与插件齿轮接真实 Config:发语音(后端 / 音色 / Key)、防抖、频控、网页搜索都能在卡片里改;

未实装条目明确标「开发中」并给出原因。

  • 其它:qq_send_voice 用官方 defineTool() 编译参数、web_search 补挂载、

人设不再每 2 秒重复推送、Host 会话上报白名单补齐 persona/activityLevel。

上一版(v0.3.1-alpha,同日早些时候):guard 在 preset 内整体失活(P0)、qq_send_voice 非法 schema、web_search 未挂载、人设每 2 秒重复推送、单群独立响应档位、技能配置弹窗。

更早(v0.3.0-alpha):

  • 多模态与交互
  • 群聊 @ 文本格式修复:入站把 at 段转成 @昵称 (拿不到昵称退回 @QQ号 ),并保证与正文之间有一个空格。
  • 图片理解实装:下载 QQ 图片转 Base64(或直接使用 URL),按 DSH 多模态格式投递;带模型能力探测与纯文本降级。
  • TTS 发语音工具注册(WIP):qq_send_voice + Edge TTS / OpenAI 兼容 TTS,经 NapCat record 段发送。
  • 配置自由度
  • 打字延时分档阈值可自定义:短句上限 / 中等上限 / 长句触发三个字数边界(默认 10 / 14 / 15)与三档毫秒基准都能在设置页改。
  • 人设支持自定义命名:选中自定义人设后可直接改名字,保存写回 personaPresets。
  • 活跃设置防抖 / 限频实装:防抖窗口、每分钟上限、超限静默、指令禁言全部按真实配置生效;并受「防抖合并 / 频率控制」插件开关控制。

上一版(v0.2.0-beta.1):

  • 模型无痕热切换:agent/request 瀑布里按会话覆写本次请求的 provider/model,不碰 session/selectModel、不改 DSH 全局默认;POST /dsh-qq-guard/model 为桥接下发的入口。
  • 静默看门狗 + 失败降级:llm/stream 上看门狗把「模型挂着不吐字」翻成终态错误,agent/request-error 在同一轮内切到备用模型;GET /dsh-qq-guard/system-events 暴露最近 20 条底层事件。
  • 模型自我认知:人设里动态注入【底层模型】路由行,QQ 里问「你用的什么模型」能如实回答。
  • NapCat 日志可见:stdout/stderr 接环形缓冲,面板可看「起不来」的真实原因。
  • 一键启动接真实路由:左下角卡片按 NapCat → 桥接顺序拉起,状态灯读真实进程。
  • 高级参数:replyTimeoutMs / modelWatchdogMs / antiDebounceMs 进配置与设置页,桥接热读。

更早(v0.1.1-beta.1):

  • 外部插件兼容层:只读扫描 QQ Agent 格式(plugin.json / skill.json),支持动态配置表单,默认全部关闭。
  • 沙箱执行基础链路(P1):ctx.subprocess 独立进程 + --permission + module.registerHooks 拦截 node:fs / node:child_process / 越界引用,支持对自包含模块执行 setup() 冒烟测试。
  • 人设系统重构:概览卡片 + 独立编辑弹窗,支持机器人名字、参与度、管理员附加规则、系统提示词覆盖,桥接层拼接逻辑同步更新。
  • UI 布局收尾:左下角一键启动卡片、记忆整理面板、外部插件卡片描述裁剪与过滤。

已知问题 (Known Issues)

2026-10-24 的四个真机 Bug 已全部修复,根因与验证见
plugins/dsh-qq-guard/README.md 的「2026-10-24 修复记录」一节。
下面保留现状待办,都是「离线验不到、必须真机跑一次」的项目。
  • 语音链路已真机跑通(2026-10-24 16:5x):已发送语音(baidu,96KB,via=base64,retcode=0),

NapCat 收 mp3 base64 并转成 QQ 语音条。若哪天换 edge/openai 后端失败,失败原因会经回执 回到模型与日志(不会静默)。

  • `web_search` 要 DeepSeek API Key:已在「技能与插件 → 网页搜索」加了配置入口

(写入 DSH 的 web-search-deepseek),没配 Key 时工具会如实报 WEB_PROVIDER_CREDENTIAL_MISSING,别把它当「插件没接上」。

  • 个别站点 web_fetch 连不上是网络层(如 news.ycombinator.com TCP 超时)。

DSH 的代理策略只读启动环境变量,要用本机代理得 HTTPS_PROXY=… 重启 DSH。

  • 人设的上下文过滤要重启 DSH 才生效(Host 侧改动):第二轮修法是通过

agent/created 给每个 agent 挂监听,插件热重载不会重挂已存在的 agent, 必须重启一次让新会话走新代码。验证看 QQ 会话是否还会冒出 USER.md 的内容。

  • 桥接进程改完要重启一次:TTS provider、回执 detail、单群档位都是桥接侧改动,

进程不重启还是旧逻辑(设置页改完能落盘,但旧进程不认新字段)。

仓库结构

plugins/dsh-qq-guard/     DSH 插件(Host + 浏览器半身)
  ├─ index.js             qq-chat preset 的工具门禁 + 设置页数据路由 + 状态路由
  ├─ client.js            设置页 UI(分片节奏 / 群聊 @ / 标点处理 / 进程管理)
  ├─ cordis.patch.yml     bundle patch:插入 qq-chat preset + 顶层 settings 行
  ├─ napcat.mjs           NapCat 进程管理(启动 / 停止 / 三层在线判定)
  ├─ bridge-runner.mjs    桥接进程管理(autoStart / 单实例 / 崩溃重启上限)
  ├─ sandbox/             外部插件沙箱执行链路
  │   ├─ protocol.mjs     Host ↔ 子进程的 NDJSON 协议(零依赖、两侧共用)
  │   ├─ manager.mjs      Host 侧:拉起子进程 / 超时 / 熔断 / testLoad + invoke
  │   └─ child.mjs        子进程入口:--permission + registerHooks 拦危险内置模块
  └─ test-*.mjs           离线回归(假 ctx / 假 subprocess,不真起进程)

qq-bridge/                独立桥接进程(零 npm 依赖)
  ├─ start.mjs            入口:读配置 → 起桥接 → 打印状态
  ├─ src/onebot.mjs       OneBot v11 正向 WS 客户端(文本分片 / 语音 / 图片)
  ├─ src/dsh.mjs          DSH Web API 客户端(离线铸会话 Cookie)
  ├─ src/bridge.mjs       主逻辑:白名单 / 串行投递 / 回复审计 / 图片预取
  ├─ src/external-hooks.mjs  外部插件 hook 客户端(message / before-context,best-effort)
  ├─ src/runtime-config.mjs  从插件轮询热配置
  └─ test-offline.mjs     离线自测(不连 QQ、不启 NapCat、不碰 DSH 会话)

两份代码要一起用:插件负责门禁和设置页,桥接负责收发消息。

两个组件各自做什么

plugins/dsh-qq-guard

  1. 工具门禁(role: guard,装在 qq-chat preset 内)
  • tools.restrict:把危险工具从该作用域的工具清单里隐藏;
  • tools.guard:执行期白名单兜底,任何不在白名单内的调用一律拒绝。
  • 必须装在 preset 子行:顶层 tools.guard 是全局的,会把浏览器里的正常会话一起锁死。这个坑在代码注释里写了来龙去脉。
  1. 设置页(role: settings,装在 profile 顶层):分片节奏、气泡长度、群聊 @ 位置、标点处理、白名单、人设、外部插件、NapCat / 桥接进程管理。
  2. HTTP 路由:/dsh-qq-guard/config(桥接轮询热更新)、/dsh-qq-guard/status(状态上报)、/dsh-qq-guard/napcat、/dsh-qq-guard/bridge、/dsh-qq-guard/external-plugins/test(沙箱冒烟测试)、/dsh-qq-guard/external/hook(桥接转发的插件 hook,best-effort)、/dsh-qq-guard/external/runtime(常驻沙箱快照)。
  3. 沙箱:外部插件一律在独立子进程里加载执行,绝不 import 进 DSH 主进程。P2 起启用的自包含插件会常驻,hook 调用与 registerTool() 的工具执行都只在子进程内发生(单飞 + 超时熔断)。

qq-bridge

  • 零 npm 依赖,只用 Node 内置 WebSocket / fetch(Node ≥ 22)。
  • 鉴权不靠抓日志:从 ~/.dsh/.credentials.yaml 里持久化的 client-connection/browser-session 签名密钥离线铸造会话 Cookie。权限等同浏览器会话,所以白名单不是可选项。
  • 安全边界:白名单(fail-closed)+ 回复审计(本机路径 / 凭据特征 / sk- Key / 桥接令牌 → 整条不发)+ 同会话串行投递。
  • 单实例锁:两个桥接同时连 NapCat 同一条 WS 会重复收消息,所以抢锁失败直接让位。
  • 发图片 / 表情包:qq_send_image 入队 → 桥接用 OneBot image 段发送(http(s) URL 由 NapCat 下载,也接受 base64:// / file:/// / 绝对路径),等回执才算成功,失败原因回给模型。
  • 外部插件 hook:message / before-context 事件转发给 Host 常驻沙箱执行(可改写文本 / 丢弃消息 / 追加外部上下文),HTTP 失败或超时一律按原样放行。
  • 性能:模型路由与人设下发按签名去重、会话存在性正结果缓存 30s、图片 ingress 预取(并发 2 / LRU 32 / TTL 60s)不阻塞主队列、语音指令脱离主串行链。

安装

前提:Node ≥ 22、运行中的 NapCat(开好正向 WS 服务端)、运行中的 DSH 桌面端。

# 1) 插件:以 link 方式装进 profile(路径换成你放本仓库的位置)
dsh plugin --profile desktop add "link:<本仓库>/plugins/dsh-qq-guard"

# 2) 重启 DSH(bundle 层要重启才组装)

# 3) 桥接:复制配置再改
cp qq-bridge/config.example.json qq-bridge/config.json
node qq-bridge/start.mjs --dry-run   # 只校验配置 + DSH 鉴权,不连 QQ
node qq-bridge/start.mjs             # 正式启动

qq-chat preset 由 cordis.patch.yml 自动插入,不需要手改 profile yaml。

配置要点

qq-bridge/config.json:

字段 · 说明 dsh.sessionCwd · QQ 会话落在哪个工作区目录(必填) onebot.wsUrl / onebot.accessToken · NapCat 正向 WS 地址与令牌,两边必须一致 allow.private / allow.groups · 白名单。留空 = 谁都不理(刻意的默认值) deny · 黑名单,优先级高于白名单

插件设置页的优先级高于 config.json:桥接每条消息现读热配置,改设置不用重启。