dsh-qq-notify
QQ ↔ DeepSeek Harness 双向桥插件
- Stars
- 1
- Language
- JavaScript
- Created
- Aug 15, 2026
- Updated
- Sep 17, 2026
Introduction
dsh-qq-notify
DeepSeek Harness 的 QQ 双向桥插件:通过 QQ 官方机器人,把 QQ 私聊接入 DeepSeek Harness。
- QQ → Harness:QQ 私聊可直接驱动 harness 会话,QQ 收到模型的回复。
- 接入已有工作区/会话:按工作区列出所有既有会话,选中后直接驱动它(继承历史、上下文、工作目录),通过 dsh 的消息队列(inbox)投递,不打断 web 端正在进行的会话。
- Harness → QQ:会话回复、回合完成通知、权限申请等推送到 QQ。
- 多会话:可新建、选择、退出会话。
- 上下线通知:由独立守护进程监控 DSH 进程,下线/恢复时发 QQ 通知(见
watchdog一节)。
版本与兼容性
| dsh 版本 | 状态 |
|---|---|
| ≤ 0.1.0-rc.5(旧 Windows 环境) | ✅ 兼容(旧 session.events 读取路径保留) |
| 0.1.5-rc.1 / 0.1.6-alpha.1(当前 WSL 环境) | ✅ 已修复并实测通过 |
0.3.0(2026-09-17)修了什么——dsh 升级后「完成通知静默失效」的根因:
session.events属性在新版 dsh 被删除(改为session.snapshotEvents()/session.ownEvents())。 旧代码在turn/end时遍历session.events生成摘要 → 抛TypeError: session.events is not iterable→ 被 dsh 的 session 观察者通道 try/catch 吞掉 → 通知一条都发不出来且日志无痕。 现在统一走sessionEventsOf()兼容层,两代 dsh 都能读。userQuestions.registerProvider从来不存在(新旧版都没有),QQ 接手ask_user的实际缝隙是 agent 作用域 waterfall 事件user-questions/request;已改为监听该事件(askUserBridge开关不变)。- 两个
session/event监听器 + 两个 waterfall 监听器全部加guarded()包装:监听器内异常写进qq-notify.log,不再静默消失。 - 新增
dryRun、环境变量兜底、启动留痕日志,便于「通知没来」时一眼定位。
0.3.1(2026-09-17)修了什么:
- 选会话时不再出现子代理会话:
/ws→ 工作区明细里的会话列表现在只列顶层会话。 子代理会话是某个任务的内部执行体(网页侧边栏也不平铺展示,只挂在父会话目录里), 之前会混进列表,选中后你的每句话都会被打进那个子代理里。 三处一起挡:列表过滤、选中时按 live 会话复核、以及「之前已接入的会话若被判定为子代理则自动断开回菜单」。 - 工作区导航改用 cwd 锚定(不再只按编号),列表变动时不会翻到别的工作区。
- 会话标题显示(0.3.0 起)与本修复的回归测试补齐:
tools/session-picker-e2e-test.mjs(32 项)。
细节与证据见 docs/dsh-compat-0.1.6.md。
安装
把本目录作为 bundle 装进 DSH 的 web profile:
# 在 DSH 源码根目录
pnpm dsh plugin --profile web add file:C:/Users/<你的用户名>/<dsh-qq-notify 所在路径>
插件的 package.json 声明了 dsh.bundle.patch,安装后会自动挂载 cordis.patch.yml 层的 qq-notify 插件行。
没有网络/github 不通时,等价的本地装配(当前 WSL 环境就是这么装的):
# 1) profile node_modules 下建 junction(Windows 用 mklink /J)
ln -s /mnt/e/dsh-qq-notify ~/.dsh/profiles/web/node_modules/dsh-qq-notify
# 2) profile package.json:dependencies 加 "dsh-qq-notify": "link:/mnt/e/dsh-qq-notify",
# dsh.profile.bundles 数组加 "dsh-qq-notify"
# 3) profile cordis.patch.yml 里填上面的 config 块
# 4) 重启 dsh web
依赖
@deepseek-ai/schemasteryws(QQ 网关 WebSocket)
均随 profile 的 pnpm install 自动安装。
配置
在 web profile 的 cordis.patch.yml 里为 qq-notify 填配置:
- id: qq-notify
name: 'dsh-qq-notify'
config:
appId: '你的 QQ 机器人 AppID'
appSecret: '你的 QQ 机器人 AppSecret'
sandbox: true
autoCapture: true
openid: '你的 C2C user_openid' # 私聊机器人一次后可自动捕获填回
ownerOpenid: '' # 留空=任意私聊都可驱动;填 openid=仅自己
bridge: true
provider: 'deepseek-official'
model: 'deepseek-v4-flash'
cwd: 'C:/Users/你的用户名/deepseek-harness'
outbox: true
outboxIntervalMs: 2000
notifyEvents:
- completed
- error
# —— 通知降噪(默认值即推荐值,一般不用写)——
notifySubagents: false # 子代理不单独推;折进顶层会话统计
notifyOnlyQuiet: true # 会话闲下来才推:连续回合/子代理来回合并成一条
notifyQuietMs: 20000 # 「闲下来」判定窗口:20s 内没有新活动才推
notifyStaleMs: 300000 # 兜底:持续忙碌也至少每 5 分钟汇报一次进度
approvalBridge: true # 被 QQ 驱动的会话,审批由 QQ 应答(false = 仍交给浏览器)
# 可选:dryRun: true 只把要推送的内容写进 qq-notify.log,不真发(验证通知链路用)
- 沙箱模式(
sandbox: true)时用沙箱网关;机器人上架后可改false。 openid官方 API 需要平台的 user_openid(不是 QQ 号);开启autoCapture后,你私聊机器人一次,插件会自动捕获并写入$DSH_HOME/qq-notify.openid.json。- 凭据也可以走环境变量(与
qq-watch.js同一套变量名,用于不方便写 patch 的场景):QQ_APP_ID/QQ_APP_SECRET/QQ_OPENID/QQ_SANDBOX。缺 appId/appSecret 时插件不启用,只记一行日志。 notifyEvents只认completed/error;显式给空数组即可关掉主动通知(不给该字段才用默认值)。- 提问/审批的优先级:
askUserBridge默认 false(提问仍由浏览器 UI 应答);approvalBridge默认 true(被 QQ 驱动的会话改由 QQ 应答审批,因为驱动来源就是 QQ)。
备注:本环境的
agent-default-model用的是deepseek-official/deepseek-v4-flash。provider/model必须显式填写(留空会让deployment:persona的{{model}}无值而报错)。
用法(QQ 私聊)
会话管理(只保留三个核心操作)
| 操作 | 命令 |
|---|---|
| 选择会话 | 菜单里回复数字;或 /use 会话名 |
| 退出会话 | /exit |
| 新建会话 | /new 会话名 |
辅助导航:/menu(回菜单)、/list(会话列表)、/ws(进入已有工作区列表)、/help。
接入已有工作区/会话
/ws(或主菜单回W+数字)→ 列出所有已有工作区(按 cwd)。- 回复数字选择工作区 → 列出该工作区下的会话(显示会话标题 + 短 id)。
- 回复数字选择会话 → 进入「驱动该已有会话」模式。
- 直接发消息 → 通过 dsh inbox 队列投递到该会话,QQ 收到回复。
已有工作区会话只读选定后驱动,不提供新建/改名/删除;QQ 新建会话只对 QQ 自建会话生效。 列表里只有顶层会话:子代理会话(某个任务的内部执行体)不会出现,也选不到—— 免得你的消息被打进一个已经跑完/正在跑的子任务里。
权限申请
harness 需要授权时(被 QQ 驱动过的会话),会推送到 QQ:
🔐 需要你的授权
工具:<toolName>(原因:...)
会话:<会话标题>(短 id)
回复「同意 / 允许一次」放行,回复「拒绝」拒绝。
(5 分钟内未答复将自动拒绝)
失败关闭:只有明确回复「同意 / 允许 / 可以 / 是 / yes / ok」才放行;拒绝、超时、被新请求抢占、无法识别的一律按拒绝处理。
不想让 QQ 接手审批(仍用浏览器 UI):approvalBridge: false。
完成通知
一个任务只推一条(这是默认行为,不是每条回合/每个子代理都推):
【任务完成】把 clash 模拟器重构一下
本任务共 3 个回合
含 2 个子代理
总耗时:4 分 12 秒
最后一轮工具调用:2 次
工作区:/mnt/e/clash-royale-simulator-main
时间:2026/9/17 23:30
会话名显示的是「会话标题」,不是 session-xxxx:取 DSH 自己的 session/title
(dsh-session-title 生成,网页侧边栏看到的就是它,LLM 标题优先、否则是内置 fallback),
所以 QQ 上的名字和网页上一致;拿不到标题才退回短 id(如 bdc6683a)。
需要用户挑一个会话的场景(/ws 列表、审批、接入确认)会额外带一个短 id 后缀做消歧。
降噪规则:
- 子代理不单独推:
notifySubagents: false(默认)。所有层级的子代理(含嵌套)都折进顶层会话, 只贡献「含 N 个子代理」这个统计。 - 连续回合合并:一个任务里父会话跑多个回合(含等子代理、继续下一步)时,
只有整个会话闲下来(
notifyQuietMs内没有新活动)才推一条。 - 不会永远不推:持续忙碌超过
notifyStaleMs(默认 15 分钟)会先推一次进度,且一个任务只推一次 (推完仍会在任务真正结束时补一条完整的完成通知)。 - 正在跑的回合不会误报:会话自己的回合还没结束时不推(否则「一件事」会被拆成两条);
连插件是在回合中途热装/重启的(没看到
turn/start)也能正确判断——从会话日志推断。 - 被 QQ 驱动过的会话不推完成通知(走桥接回复链路,避免重复)。
- 想回到「每个回合都推」:
notifyOnlyQuiet: false+notifySubagents: true。
上下线通知(独立守护进程)
插件跑在 DSH 进程内,dsh 一旦被杀就来不及发通知。因此另配一个独立守护脚本 qq-watch.js(独立于 dsh 常驻):
- HTTP 探活 dsh 的 web 端口 + PID 双重监控。
- 判定下线(连续 N 次失败)→ 发「⚠️ 已停止/下线」;恢复 → 发「✅ 已恢复」。
配置见脚本头部注释;凭据可放 qq-watch.config.json(appId/appSecret/openid/sandbox/baseUrl)。启动:
DSH_HOME=... setsid nohup node qq-watch.js >/dev/null 2>&1 &
文件
dist/index.js— 插件主逻辑(bundle)。cordis.patch.yml— bundle 层,插入qq-notify插件行。package.json— 声明dsh.bundle.patch。qq-watch.js— 独立的 DSH 上下线守护脚本。qq-watch.config.json— 守护脚本凭据配置(可选)。tools/smoke.mjs— 离线冒烟自检(改完代码先跑它:抓「配置变量未声明」与「prepend 注册回归」)。docs/dsh-compat-0.1.6.md— dsh 升级兼容性记录(改了什么、怎么复验)。
设计要点
- 插件
inject: [],所有服务(sessions/agents)用ctx.get()懒取,不阻塞 dsh 启动(web 启动约 10.8s)。新版 dsh 实测:非作用域(untagged)监听器被全局接纳,所以inject: []依然能收到session/event、approval/request、user-questions/request。 - 驱动已有会话时复用
agents.get()拿到的 live agent,用agent.send投递到 inbox 队列,不agents.resume已占用的会话(避免already exists),也不打断 web。 - 读会话日志一律走
sessionEventsOf(),不直接碰session.events(新版已删除)。 - 通知按「会话 → 顶层会话」聚合:
pendingNotices每个会话一个聚合条目,noteActivity()重排静默定时器,rootAncestorOf()把嵌套子代理上溯到顶层;isTurnOpen()判断会话自己是否还在跑(事件状态 + 会话日志双保险)。 - 审批/提问两个 waterfall 用
prepend注册:dsh-api-remotes的浏览器转发器先注册且会直接应答,不抢占就永远轮不到 QQ。 - 发射是不可逆副作用:QQ 推送失败只记录、不阻塞 agent 循环;outbox 每条最多重投 8 次后丢弃(避免永久失败项无限重试)。
常见问题
排查顺序(通知没来时按这个顺序看 $DSH_HOME/qq-notify.log):
插件加载 appId=… bridge=… notifyEvents=… dryRun=…—— 没有这行说明插件根本没起来(配置缺失/没装进 profile/被 disabled)。通知 turn/end 会话=… kind=completed(订阅=…)—— 有这行说明事件收到了(debug: true时每回合都打)。子代理结束 → 计入顶层会话 …/通知: 会话 … 仍在忙,推迟推送—— 降噪在工作(子代理折进顶层、忙时推迟)。通知(quiet|stale) 会话=… 回合=… 子代理=… → QQ—— 聚合通知出队(quiet=闲下来、stale=忙碌兜底)。pushQQ: 成功 …/pushQQ: 失败 …—— 真正的投递结果;失败会保留在 outbox 里按outboxIntervalMs重试(最多 8 次)。- 出现
监听器异常:…行 —— 说明 dsh 事件载荷又变了(把该行连同 dsh 版本一起反馈,通常改一个读取字段即可)。
觉得太安静(任务结束没收到)?先看第 3、4 步:正常最多等
notifyQuietMs(默认 20s)。 如果会话一直在跑,会推迟到它闲下来;超过notifyStaleMs(默认 5 分钟)一定会推一条进度。
- QQ 驱动已有会话报「already exists」:该会话正被 web 端占用。当前版本复用 live agent + inbox 排队,不会再报;若仍出现,确认插件已更新。
- 启动时「token 请求超时」:首次 token 获取偶发超时(网络/事件循环),插件会自动重试,网关稍后连上,不影响 web 可用。
- 通知只写日志没发出去:检查是否误开了
dryRun: true。 - 日志文件不在
$DSH_HOME:插件按DSH_HOME || process.cwd()落日志;某些启动方式(如 Windows 原生)没有DSH_HOME,日志会落在 dsh 源码根目录。