dsh-plugin-notify:让 DeepSeek Harness 在你离开时叫你回来

项目地址:https://github.com/This-is-XiaoDeng/dsh-plugin-notify

许可证:MIT

运行时:Node.js ≥ 20,宿主需带 Web 面(任何常规的 dsh web profile)

概述

dsh-plugin-notify 是给 DeepSeek Harness(DSH)写的会话状态通知插件。会话停止、因错误退出、向你提问或等待授权时,它同时推一条 WxPusher 消息(微信里)和一条真正的浏览器系统通知——后者带提示音和 requireInteraction,横幅会一直留到你手动关闭。

所有开关都在原生的「设置 → 通知」页面里,改完立即生效,不重启、不重挂插件。

背景

DSH 的会话经常跑很久:一次重构、一轮评测、一个十几分钟的构建、一篇长文。人不会一直盯着那个页面——切到别的标签页改别的东西,去开个会,或者干脆带着手机在外面。

等回到页面,常见的情况是:会话二十分钟前就停了,最后一条消息是在问你一个问题;或者某一步报错了,它自己安静地停在那儿。你要的其实不是「跑完了没有」这个状态,而是它停了、它出错了、它在等你的时候,你能不能马上知道。

桌面通知是最直接的答案,但它只在电脑前有用;出门在外,微信里的一条消息才叫得动人。所以这个插件做了两条通道,共用同一份内容。

四个触发点

事件监听来源通知里带什么
会话停止api-session/status → running: false会话标题 + 最后一条模型回复
因错误退出api-session/error、agent/error错误信息
收到提问user-questions/request第一个问题(多个时标 +N)
等待授权approval/request工具名

停止通知里最有价值的是「最后一条模型回复」。长任务结束时,模型通常会在最后一段里总结「改了什么、还剩什么、哪里需要你决定」,把它直接放进通知,在手机上就能看完结论,不必再打开 DSH。

所以这部分的处理按通道分开:WxPusher 那条不截断、保留换行,用 white-space: pre-wrap 渲染;浏览器通知受系统通知的长度限制,按 maxDetailChars 截断(默认 300 字)。

几个降噪开关:

  • minRunMs(默认 2000):跑得比这还短的运行,不报告停止。
  • stopDebounceMs(默认 1200):智能体进入空闲后先等一会儿再报;期间又开始运行就取消报告,避免一次请求内部的多次空闲反复打扰。
  • 错误优先:同一会话刚发过错误通知时,紧随的停止通知会被压掉(5 秒去重窗口)——错误那条信息更精确。
  • rootSessionsOnly(默认开):忽略子智能体会话,只关心你自己发起的那些。

两条通道,一份内容

宿主侧只在状态变化时检测一次,组一帧(frame),同一帧既发 WxPusher,也通过 SSE 广播给所有打开的页面。内容相同,呈现方式各自贴合场景:

  • WxPusher:HTML 正文,微信里收到一张卡片消息,标题形如「会话已停止 · 会话标题」。
  • 浏览器:new Notification(...),PNG 图标、可选两声短提示音(WebAudio 合成,不用音频文件)、requireInteraction 常驻,并用 tag 按会话合并同一会话的重复通知。

功能特性

  • 🔔 四个触发点:停止、出错、提问、等待授权,各带独立开关。
  • 📣 两条通道,一个 payload:电脑前看系统通知,手机上收微信推送,不会一边响一边不响。
  • 🖥 真正的系统通知:不是页内 toast,是操作系统级的通知中心横幅;可以设为常驻,直到你手动关闭。
  • ⚙️ 原生设置页:一个 settings.section 页面,由插件自己的 Config schema 驱动;每个字段都是 volatile,改完立即生效。
  • 🔒 只写密文:WxPusher 的 appToken 声明为 role('secret')——存在 profile 里,但永远不会随响应返回;编辑走按路径寻址的写入,不会把没回显过的密文覆盖掉。
  • 🧪 每条通道独立测试:浏览器和 WxPusher 各有一个测试按钮,结果各自报,不会测完还分不清测的是哪条。
  • 📦 除 schema 库外零运行时依赖:一个宿主文件、一个客户端文件,没有构建步骤。

实现方式

宿主                                    浏览器
────                                    ───────
api-session/status ─┐
api-session/error  ─┤
agent/error        ─┼─► 检测 ─► 帧 ─┬─► WxPusher HTTPS 推送
user-questions/…   ─┤                │
approval/request   ─┘                └─► GET /dsh-plugin-notify/events(SSE)
                                                      │
                                                      ▼
                                          new Notification(title, {
                                            body, icon, tag,
                                            requireInteraction, silent,
                                          })  +  WebAudio 提示音

几个设计上的取舍:

  • 宿主侧注册两个精确路由:SSE 流 /dsh-plugin-notify/events,以及测试端点 /dsh-plugin-notify/test(?channel=browser 只广播、?channel=wxpusher 只推送,不带则两者都做)。
  • 客户端用文档相对路径开 EventSource:dsh-plugin-notify/events 会跟着页面被挂在哪个前缀下解析,并沿用页面自身的鉴权,不需要在插件里再写一遍地址或鉴权逻辑。EventSource 自己会重连,宿主每 25 秒发一次 : ping 心跳。
  • 只观察 waterfall 事件,不改变语义:user-questions/request 和 approval/request 是 Cordis 的 waterfall 事件,插件只旁听,并且始终 next() 委派——一个通知插件绝不能吞掉本该交给其他应答者的请求。
  • 设置页自己找命名空间:通过 ctx.configForms.describe() 解析,先按条目 id 匹配,再按它认识的那份 schema 匹配;即使 profile 把 bundle patch 嵌进 include 组、给条目 id 加了前缀,页面依然能找到自己。
  • 没有自建 Remote 命名空间:浏览器半侧因此走上面两条 HTTP 路由,而不是类型化的 Remote 通道——SSE 本来就是「服务器单向推给页面」的场景。

安装

⚠️ npm 上已经有一个同名的包(另一个项目),所以 pnpm add dsh-plugin-notify 装到的不是这个。下面的命令都指定仓库地址。

命令行,一行装好:

dsh plugin --profile web add github:This-is-XiaoDeng/dsh-plugin-notify

这条命令把包写进该 profile 的 dependencies 和 dsh.profile.bundles,不需要再改别的文件;profile 开着 patchReload: live 时通常直接生效。

或者在「插件」页点添加,填同一个地址,再启用它带来的 bundle:

github:This-is-XiaoDeng/dsh-plugin-notify

手动装进 profile 也可以:

cd ~/.dsh/profiles/web
pnpm add github:This-is-XiaoDeng/dsh-plugin-notify

然后确认 package.json 的 dsh.profile.bundles 里有它:

{
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "@deepseek-ai/dsh-web-app",
        "dsh-plugin-notify"
      ]
    }
  }
}

前置条件:带 Web 面的宿主(@deepseek-ai/dsh-web-app)。没有 ctx.webServer 时插件仍然推 WxPusher,只是不往浏览器发。

配置

打开 设置 → 通知,页面直接写进插件所在的 profile 条目。

字段默认说明
enabledtrue两条通道的总开关
events.stoppedtrue会话停止时提醒
events.errortrue会话或某一步出错时提醒
events.questiontrue智能体提问时提醒
events.approvalfalse等待授权决定时提醒
browser.enabledtrue是否发浏览器通知
browser.soundtrue两声短提示音(WebAudio,无音频文件)
browser.requireInteractiontrue通知常驻,直到手动关闭
browser.onlyWhenHiddentrue当前标签页可见时不打扰
wxpusher.enabledfalse是否启用 WxPusher 通道
wxpusher.appToken空WxPusher appToken(只写密文)
wxpusher.uids / topicIds空接收者,每行一个
advanced.stopDebounceMs1200判定「已停止」前的空闲宽限期
advanced.minRunMs2000短于该时长的运行不通知
advanced.rootSessionsOnlytrue忽略子智能体会话
advanced.includeLastModelMessagetrue停止通知附带最后一条模型回复
advanced.maxDetailChars300浏览器通知里错误/提问的截断长度(WxPusher 不截断)

同样的值也存在于 profile patch 里,升级不会丢:

- id: dsh-plugin-notify
  config:
    events:
      approval: true
    wxpusher:
      enabled: true
      uids:
        - UID_xxxxxxxx

接上 WxPusher

  1. 登录 https://wxpusher.zjiecode.com,创建一个应用,复制它的 appToken。
  2. 用微信扫应用二维码关注,然后在应用的用户列表里复制自己的 UID(也可以改用主题 ID,一次推给多个订阅者)。
  3. 把 appToken 和 UID 填进设置页,点保存。

appToken 输入框每次加载都是空的——宿主只会告诉页面「是否已设置」,密文本身不出 profile。

测试

两张卡片各自带一个测试按钮,各报各的结果:

卡片按钮行为
浏览器通知发送测试通知向所有已连接页面广播一帧,并推一条 WxPusher 消息——整条链路一起测
WxPusher 推送发送测试推送只推 WxPusher,不弹系统通知;wxpusher.enabled 还关着时也能用,方便先验凭据再开通道

按钮下面会报自己的结果:

浏览器:1 个页面收到 · WxPusher:已发送
WxPusher:未填写 appToken
WxPusher:被拒绝(appToken不正确)
浏览器:没有已连接的页面(浏览器通知可能已关闭) · WxPusher:未填写 UID 或主题 ID

浏览器权限那一栏会显示当前的 Notification.permission:default 就点「请求权限」;denied 要去浏览器的站点设置里重新允许。提示音需要页面先有过一次点击或按键浏览器才允许启动 AudioContext,插件会挂上第一次手势来解锁。

已知限制

  • WxPusher 正文用中文拼装(受众在手机上);浏览器通知通过 Harness 的 locale 服务本地化。
  • 「停止」信号取自智能体进入空闲,所以多轮对话中途的空闲也会报一次,这是设计如此——stopDebounceMs 和 minRunMs 是调节旋钮。
  • 除 profile 配置外不做持久化:没有历史记录,也不会补发错过的通知。
  • 需要带 Web 面的宿主才有浏览器通知,纯 CLI 的 profile 只有 WxPusher。

最后

这个插件解决的是一个很小的问题:你不在看的时候,会话停了,谁来告诉你。

如果你也经常把 DSH 的会话放着跑、然后忘记回来看,可以装上试试。它会在该回来的时候叫你一声。

项目还在早期,欢迎 issue 和 PR。


项目地址:https://github.com/This-is-XiaoDeng/dsh-plugin-notify
Web , DSH , Cordis
最后修改于:2026年10月31日 20:28

添加新评论