Zum Hauptinhalt springen
T

dsh-ntfy-remote

tongwoojun/dsh-ntfy-remote

Jede DSH-Sitzung mit einem ntfy-Thema verbinden: Push aufs Handy, Antwort vom Handy, Weiterleitung von Freigaben und Fragen.

Installation

dsh plugin --profile web add github:tongwoojun/dsh-ntfy-remote

README

Ntfy Remote

npm version license node

让 DSH 干完活叫你一声,也能在手机上直接把活答完。

DSH 跑长任务时你得守着屏幕才知道结果;模型一旦要审批、要你选方案,任务就卡在那儿等你。 Ntfy Remote 把每个会话桥接到一个 ntfy 话题:任务完成、待审批、 待提问都推到手机,你在手机上点一下按钮或打一句话,答案就直接回到正在运行的会话里。

不需要公网 IP、不需要端口映射、不需要自己写机器人 —— 插件主动向 ntfy 服务器建立长连接, 手机端就是一个普通的 ntfy App。

为什么用它

痛点它怎么解决
长任务跑完才知道结果回合结束时把最终回复整段推到手机
模型要审批 / 提问,你不在电脑前,任务干等待决请求推到手机,点按钮或回一句话就作答
自己接推送要公网入口、端口映射、HTTPS、机器人后端手机端只是 ntfy App,插件主动出站长连接
不想为通知维护一套服务复用 ntfy 的发布 / 订阅,零后端代码
一个 Agent 只能绑一个微信 / 飞书,多会话消息全挤在一起每个会话一个话题,哪个 Agent 在找你一眼分清
通知渠道被单一 IM 绑定,换项目 / 换环境要重新配支持配置多个 ntfy 服务器,按项目、按环境分开推

和微信 / 飞书通知的区别

微信 / 飞书通知的本质是:把你的人绑进一个 IM 账号,再让 Agent 往这个账号里发消息。 这带来两个问题:

  • 一个 Agent 只能绑一个账号 / 机器人。 多开几个会话,消息全进同一个对话框,你分不清是谁在找你。
  • 渠道是写死的。 想按项目分开推、想换一个推送服务,都得重新折腾一遍接入。

Ntfy Remote 换了个思路:不绑人,绑会话

  • 每个会话 = 一个独立话题(topic)。 会话 A 的审批不会混进会话 B 的结果里,手机上按话题就能分辨来源。
  • 可以配多个 ntfy 服务器。 生产环境推一个、实验环境推另一个,或者一台手机收全部、按需订阅。
  • 渠道和 Agent 解耦。 你用的是 ntfy 这个通用发布 / 订阅通道,不是某个 IM 的机器人接口。今天用 ntfy, 明天想换,Agent 侧不用动。

一句话:微信 / 飞书是「一个 Agent 找一个账号」,它是「每个会话有自己的频道」。

功能

推送到手机

  • 回合结束:把这一轮的最终回复原文推给你(超长会拆成多条继续发,不截断)
  • 待审批 / 待提问:高优先级推送,审批直接带 Approve / Deny 按钮
  • 错误与中断:模型报错、达到输出上限、被策略拦截时,推一条精简原因
  • 你自己在桌面点「停止」、以及子 agent 的回合不会打扰你
  • 通知就落在会话话题里:手机上点开通知即可直接回复,不依赖 ntfy:// 深链接,iOS 也能用

回合心跳:知道 DSH 是在思考、卡住、还是断了

从手机发出一条消息后,最难熬的是等的那段时间——不知道它收到了没、在不在干活、还是已经死了。

回合进行中,插件会在会话话题里维护一条状态通知,它原地更新,不刷屏(ntfy 的 sequence_id 机制:手机上始终只有一条,标题里的秒数自己在走):

你看到的含义
🟢 DSH 思考中 · 1 分 20 秒正常,正在干活;正文里还有「最近一次输出:12 秒前」
🟠 DSH 疑似卡住 · 3 分 10 秒进程活着但 9×心跳间隔没任何输出,这条会响铃
✅ DSH 已完成 · 3 分 20 秒本轮正常跑完,通知原地变成结果
⏹ DSH 已中止 · 1 分 35 秒你点了「停止」(或在话题里发了 /stop)。这条同时承担了「已收到中止请求」的回执——不会再单独多推一条
🔴 DSH 心跳已停止连进程都没了——这条由 ntfy 服务器在超时后自动发出

最后一条是整个机制的要点:插件在回合开始时往服务器挂一条 3×心跳间隔定时告警, 每一拍把它往后推。进程一旦死掉就没人再推,服务器到点自己把告警发出来——这是任何 "客户端自己发心跳"的方案都做不到的(客户端死了就发不出任何东西了)。

所以判断方法很简单:

  • 秒数在走 = 活着
  • 秒数冻住 = 出事了(进程卡死 / 事件循环阻塞 / ntfy 断线)
  • 收到 🔴 告警 = 已经彻底联系不上,去看机器

状态通知上还挂了一个「停止」按钮(点击发回的是 /stop,和手打完全等价), 看到疑似卡住可以直接点,不用回话题打字。点完之后话题里只会多出你那条 /stop—— 状态通知会自己变成 ⏹ 已中止,不会再另外推一条「已请求中止当前回合。」。

状态通知带一个 dsh状态通知 标签,方便和普通推送区分。(它同时还带一个内部的 dsh-ntfy-remote 标记——那是回声过滤的主防线,不能去掉:插件订阅了自己发布的话题, 服务器会把消息原样推回来,靠这个标记才能认出「是我自己发的」而不当成用户输入。)

进程崩溃优雅退出是被区分开的:正常停止(含开发期热重载)会当场撤销那条定时告警, 不会留下假警报;只有真的崩了才会收到 🔴。

只有一个时间配置项(设置页里的「心跳间隔」,默认 20 秒):死信超时 = 3×,卡住阈值 = 9×, 由它推导——避免手填出"心跳 60 秒、死信 30 秒"这种健康时就误报的组合。

每回合发一条新的状态通知,所以空闲时你从手机发一条消息,那条 🟢 会立刻出现在通知栏最上面。 做法是:开场先把上一轮那条删掉,再用新的 sequence_id 发本轮的 —— 同一个 sequence_id 在 客户端只会原地替换(内容变、位置不动),新 id 才会被当成新通知。通知栏里因此始终只有一条

实现上不需要额外进程:定时器就跑在 DSH 进程里。而"定时器停了"恰恰是最准的故障信号。

在手机上作答

  • 审批:点 Approve / Deny
  • 提问:选项 ≤ 3 时是按钮;超过 3 个改成编号列表,回编号即可
  • 多选提问可以直接点按钮——点一下选中、再点一下取消,最后点「提交」; 也可以回 1,3 一次选中多个(,、空格都算分隔符)
  • 自由文本:在话题里直接打字,内容会作为你的消息注入正在运行的会话
  • 撤回:点 ⏳ 已排队 通知上的「删除」按钮——还排着队的那条会被直接抽掉、压根不跑(正在跑的那一轮用状态通知上的「停止」)
  • 指令/stop 中止当前回合、/status 看状态、/key 看话题、/help

撤回:发错了就把它删掉

在手机上发出去才发现说错话,不用干等它跑完。点通知上的「删除」按钮,插件就把这条当成不算数:

那条消息此刻「删除」按钮在哪插件做什么
排着队(同一会话前一轮还没跑完)插件补发的 ⏳ 已排队 通知上从队列里按条抽掉,压根不会跑
正在跑没有「删除」按钮了 —— 用状态通知上的「停止」(两者对正在跑的那轮效果一样,见下)——
早就跑完了没有按钮(没什么可撤的)——

两种情况都会顺手做三件清理:把你发的那条原消息从服务器删掉(手机 App 里它也跟着消失)、 把插件自己那条通知删掉、以及把按钮自己留下的那条载荷消息也删掉(见下面的表格)。

为什么不靠「在 App 里长按删除」? 实测(ntfy 安卓端 + 自建服务器,三轮):长按删除 只删本机、不向服务器发 DELETE,服务器因此不广播 message_delete,插件收不到任何信号—— 同时挂着的独立订阅者全程在线,一条删除事件都没看到。而按钮走的是「点击 = 往话题发一条消息」 这条通道(/stop 用的就是它),实测点击一秒内生效。

另外,桥接的订阅会周期性断线(实测一天十余次)。普通消息能在重连后用 since 补回来, 但 message_delete瞬时事件、补不回来——这也是不该把撤回挂在删除事件上的第二个理由。

「停止」和「删除」是两个按钮,别混

按钮挂在哪做什么你发的那条消息
停止状态通知(🟢 / ✅ / ⏹)只中止当前回合留在话题里(方便你改一版再发)
删除⏳ 已排队 通知从队列里按条抽掉,压根不跑一起删掉(App 里也跟着消失)

为什么状态通知上不再挂「删除」?正在跑的那一轮,它和「停止」效果重合(都只是中止), 而且它删不掉 DSH 会话里的记录(会话日志只追加,插件不该也无法回删一条已经记录的用户消息), 用户觉得"删了个寂寞"。而 ⏳ 已排队 上那个是不可替代的:那是取消一条还没轮到的消息的 唯一入口 —— 那种情况下「停止」是无用的(那会儿没有回合在跑)。

两个按钮自己留下的那条"点击载荷"都会被插件删掉 —— 否则话题里会多出一行 {"stop":true}{"undo":"…"} 的操作痕迹(实测报过)。而你手打/stop 不会被删: 按钮发的是 JSON、手打的是纯文本,插件分得清。

不是手机发起的那一轮(网页 / 桌面)没有可删的原消息,所以状态通知上只挂「停止」。

插件自己推出去的通知(状态通知、已撤销的心跳告警)你要是在 App 里删掉了,插件只记一行日志, 不会误判。

扫码添加到手机话题

会话右上角 ● ntfy 弹窗里有一张二维码:内容就是该会话的 ntfy://服务器/话题 深链接, 用手机 ntfy App 扫一下即可订阅这个话题,不必手抄那串 dsh_<会话 id>。换会话就换一张码, 扫错不会串到别的话题。

二维码在宿主侧现算GET /dsh-ntfy-remote/session/qr?sessionId=… 返回一张自包含的 SVG), 不是把话题名发给第三方在线生成服务——话题名不离开本机,断网也能出图。

通知偏好

五项,全局默认 + 每个会话单独覆盖(改过的项会高亮,可一键「跟随默认」):

设置作用默认
回合结束推送正常结束时推送最终回复
审批 / 提问推送待决时推高优先级通知并接管作答
错误 / 中断推送异常结束时推送精简原因
手机优先接管作答由手机而不是网页来答审批 / 提问
从网页发起的回合也通知手机关掉后只有手机发起的回合才会通知手机:回复、状态心跳、以及提问/审批的中转都不发给手机(提问会留在网页上由网页作答)
作答超时等手机作答的秒数;超时回落网页端,网页弹窗继续等,请求不会丢180 秒

长文本不需要配置:ntfy 对单条消息的硬上限是 4095 字节(1 个汉字约 3 字节), 超了会被服务端整条拒收,所以插件会自动按字节拆成多条继续发,内容不会丢——只有 第一条响铃,后续几条静音并在标题上标 (2/3) 这样的序号。推送正文按 markdown 渲染 (标题、列表、代码块、链接都会正常显示)。

「网页发起的回合也推送」是给「人就在电脑前,别再来打扰我」的场景准备的。关掉之后, 你在网页/桌面上顺手问的那些不会再推到手机;代价是「在电脑上发起长任务、走开后手机 收结果」这个场景也会一起没有,所以它是个开关而不是默认行为。逐会话可设——你可以 让随手问的会话安静,让跑长任务的会话照常通知。

会话里的「本会话通知偏好」把上面这张表直接写到了界面上:每一项控件下面都有一行灰字说明 它做什么(同时也是悬停提示),不用去翻文档。

多服务器

可以配置多个带名称的 ntfy 服务器(官方 ntfy.sh 或自建,支持 access token)。会话 首次开启时选定服务器,之后不可变更 —— 避免运行中改地址造成话题漂移。服务器失联或 地址填错时留了解绑退路:关闭该会话的桥接后即可解绑并重选。还有会话绑着的服务器 不允许删除,避免那些会话永久失去推送。

三处入口

  1. 会话右上角 ● ntfy:就地开关、查看话题与链接、扫码把话题加到手机、调整本会话通知偏好; 话题名旁边是两个同样式的小按钮——网页打开(新标签打开该话题页)和复制(复制话题名到剪贴板, 成功/失败就地回显)
  2. 设置 → Ntfy Remote:服务器增删改、全局默认偏好、所有会话总览
  3. 状态页 http://127.0.0.1:<端口>/dsh-ntfy-remote:内容与设置页一致,不需要任何前端构建

会话归档或删除后自动收起

你在 DSH 里归档(= 从侧边栏收起)或删除某个会话时,插件会自动断开它的桥接,并把这行 从「设置 → Ntfy Remote」的列表里移除 —— 不会留下一行"运行中"的幽灵记录,也不会继续订阅那个话题。

列表里只列出开过桥接的会话(有绑定 / 开关 / 偏好),每行都有一个 删除 按钮:只删 Ntfy Remote 里的绑定 / 开关 / 偏好,不会删除 DSH 会话(会话文件和聊天记录都在,想再用就在那个会话里 重新 /ntfy on)。与「解绑」不同,它不需要先关闭桥接,随时可用。活着但从没开过桥接的会话不会 列在这里 —— 它们在自己的 ● ntfy 弹窗里开启。

安装

前提:pnpm 在 PATH 上(dsh plugin 是它的转发器)。

dsh plugin --profile web add dsh-ntfy-remote

升级 / 卸载:

dsh plugin --profile web update dsh-ntfy-remote
dsh plugin --profile web remove dsh-ntfy-remote

快速开始

  1. 手机装 ntfy App(iOS / Android / F-Droid 都有)
  2. 在 DSH 输入框里执行 /ntfy on
  3. 在会话右上角 ● ntfy 弹窗里扫描二维码订阅本会话话题;或者执行 /ntfy test 发一条 测试通知,在手机上点开它,同样落在本会话的话题里
  4. 之后这个会话的完成、审批、提问都会推到这台手机 —— 直接在话题里回复即可作答

或者在dsh web 中进入会话,在右上角找到ntfy进行点击操作

命令

DSH 输入框

命令作用
/ntfy on [服务器名]用默认(或指定)服务器开启本会话桥接,指定仅首次有效
/ntfy off关闭桥接(保留服务器绑定与偏好)
/ntfy key只看话题
/ntfy status绑定服务器、话题、偏好及其来源
/ntfy servers列出服务器(★ 标默认)
/ntfy test发一条测试通知

手机上(在话题里发文本)

指令作用
/stop中止当前回合
/status / /key桥接开关、会话是否在运行、服务器、话题与订阅地址
/help帮助
其它任何文本作为你的消息注入会话;审批 / 提问待决时优先结算该请求

⚠️ 安全:请务必读这一段

话题名是 dsh_<完整会话 id>可以从会话 id 直接推导出来(44 位会话 id 再加一个密钥 就会超过 ntfy 的 64 字话题名上限,所以话题名里没有密钥)。而会话 id 不是机密 —— 它会出现在界面、日志、状态页和截图里。

后果是:知道会话 id 的人就能往该话题发消息,而插件会把非自己发布的消息当作你的指令 注入会话。 插件只能过滤掉自己发的消息,无法分辨其余消息是不是你本人

所以:

  • 用公共 ntfy.sh 时:注册账号、启用 access token(坑,付费用户),并使用保留话题前缀,让别人无法发布。建议自建ntfy。
  • 或者自建 ntfy:给话题配置 ACL,只允许你的账号读写
  • 不要把会话 id 随意外传(截图、日志、分享状态页都要留意)

注入的消息仍然走 DSH 的沙箱与权限策略,不会绕过审批;但「手机指令 = 用户指令」本身 就是权限入口。

已知限制

  • 同一个 DSH_HOME 下不要同时跑多个 dsh 实例:它们会订阅同一话题,同一条回复可能被处理两次
  • 会话不在内存中(冷会话)时无法注入,插件会提示你先在 DSH 里打开它
  • 长文本自动按字节拆成多条发送(无需配置),暂不支持发送附件
  • 子 agent(subagent)的回合不推送,避免刷屏

常见问题

手机的ntfy App哪里下? iOS:App Store Android:Github

手机收不到推送? 按顺序检查:/ntfy status 是否显示「桥接:已开启」→ ntfy App 里是否订阅了该话题 → App 的通知权限 → 自建服务器是否可达。 注意坑:公共 ntfy.sh 的免费额度会限流(HTTP 429)。 如果有自己的服务可以自己搭建ntfy 参考: Github 插件日志在 $DSH_HOME/dsh-ntfy-remote/plugin.log

通知里多出来的 dsh-ntfy-remote 小字是什么? 插件给自己消息打的标记,用来过滤回声 —— 单话题下没有它就会「自己回自己」形成死循环。

我在桌面点「停止」也会推给我吗? 不会,用户主动取消不推送。

手机上答过了,网页端还会再弹一次吗? 不会。手机优先接管期间网页端不显示该提问;只有等不到手机回执、超时之后才回落到网页端, 而本地弹窗一直在等,请求不会丢。

版本更新

每次发版都在这里补一行(详细变更见 CHANGELOG.md)。

版本增加了什么 / 修了什么
v1.1.1撤回改走通知上的「删除」按钮(不再依赖「在 App 里长按删除」——实测那个只删本机、不发 DELETE):排队中的补一条「⏳ 已排队」通知并挂按钮;点下去 = 撤回那一轮 + 把你发的原消息从服务器删掉(App 里随之消失)+ 删掉按钮自己那条载荷。「停止」也改成发 JSON 载荷、点完不留痕迹(手打的 /stop 不受影响)。状态通知改成每回合一条,空闲时一发消息就置顶。另修并发发布偶发 403 导致通知静默丢失(改为自动重试)
v1.1.0回合心跳:手机上看得到「🟢 思考中 / 🟠 疑似卡住 / ✅ 已完成 / ⏹ 已中止 / 🔴 心跳已停止」,秒数原地更新不刷屏;状态通知上带「停止」按钮;撤回(在手机上删掉自己发的消息,即中止那一轮且不回推);多选提问可以点按钮(点选 / 取消 + 提交);新增逐会话开关「网页发起的回合也推送」(关掉后手机端静默);修掉「空回复哨兵 triggered 被当成用户输入」与「过期按钮回执被当成用户输入」
v1.0.6分片推到手机后顺序错乱(倒序发送 + 相邻 1.1 秒);修分片还没发完就点按钮丢回执的竞态;移除「单条上限」配置项(字节预算改固定值)
v1.0.5会话弹窗新增二维码扫码订阅;长中文通知改为按字节分片续发(原先超限会被 ntfy 静默拒收、整条丢失);提问补齐选项描述 / 短标题 / 计划正文;推送正文按 markdown 渲染;修自建服务器按钮 HTTP 403
v1.0.4新增二维码编码器 qr.js(弹窗接入在 v1.0.5)
v1.0.3README 补充卖点,新增「和微信 / 飞书通知的区别」(纯文档)
v1.0.2弹窗底部新增 ntfy App 下载入口(iOS / Android)
v1.0.1修多选提问只回一个选项;/stop 不再多推一条 unknown 中断;偏好取值校验;会话归档自动断开桥接;会话列表新增「删除」
v1.0.0首个发布:会话 ↔ ntfy 话题桥接、回合推送、手机作答

开发者

实现细节、架构取舍、开发 / 测试 / 发布流程、逐项验证记录见 README_FOR_ME.md;版本变化见 CHANGELOG.md

License

MIT

web 操作参考图片

dsh web中的展示

设置->ntfy remote 配置

截图

会话中的配置1

截图

会话中的配置2 截图

手机订阅 打开 ntfy app 添加话题,话就是会话中的配置2中的:dsh_session-823eacc5-00c7-4495-898c-ebb283e43ed0 截图 然后点击订阅。

如果需要实时通知需要在该话题下点击右上角 "..." 弹出如图 截图

然后在点击订阅设置中的即时交付 截图

微信群

截图

Ähnliche Plugins