DSH 统一通知与远程控制插件:一个 notify() API 对接 27 个推送渠道,同时把审批、提问、会话从手机送回桌面,长任务自动心跳并支持一键停止。
- Language
- JavaScript
- License
- MIT
- Branch
- main
Install
$ dsh plugin --profile web add github:THEWOLFWALKER/dsh-notifierRun the command above in your terminal to install this plugin via the dsh CLI. You can switch Profile in the top-right corner. New to dsh? Read the beginner tutorial
一句话定位
DSH 的统一通知与远程控制插件:宿主事件(任务完成/出错/审批)和模型主动推送共用一个 notify() API,把消息送到 27 个推送渠道;再通过其中 6 个通道反向把审批、提问、消息从手机送回桌面,并把长任务变成带心跳和停止按钮的移动指挥中心。
核心能力
- 27 个出站渠道:Telegram、Slack、Discord、飞书、钉钉、企业微信(群/应用)、QQ 机器人、OneBot、Teams、Mattermost、Google Chat、Bark、Pushover、PushDeer、Chanify、ntfy、Gotify、iGot、WxPusher、PushPlus、Server酱、Qmsg、息知、Webhook、终端响铃、系统桌面通知
- 自动状态推送:宿主事件(任务开始、完成、出错、被阻塞、被中止、达 Token 上限、异常中断、需要审批)按级别自动转发到所有配置渠道;同会话轮次按 10 秒尾沿合并去重
- 分级路由:紧急/活跃/被动三档语义映射到各渠道原生送达形式(静默推送、优先级、@提醒),配分档重试
- 远程审批与对话:Telegram、飞书、QQ、WxPusher、微信、钉钉六通道支持回传,点按钮或回复 1/2 远程裁决审批;纯文本消息进入 agent 会话(追问/中途注入/steer)
- 移动指挥中心:长任务 15 分钟首条心跳 + 每 15 分钟再来一条,10 分钟无事件报疑似卡住;Telegram/飞书卡片自带一键停止按钮
- 远程提问:ask_user 工具把选择题推到手机(飞书/Telegram 选项卡,其他通道回复编号),用户作答后回传 agent;超时永不代答
- 本机 Web 管理台:仅绑 127.0.0.1 + Bearer token,含总览、通知流、成员、配对码、绑定矩阵、会话台账、通道凭证 6 页,移动端自适应
- 开放事件源:其他插件可注入 notifier 服务推送,或订阅 dsh-notifier/sent 事件监听每次广播结果;按 source 限流 10 次/分钟
- 身份体系:运行时的配对码准入、复合键绑定(channel:userId 隔离渠道身份)、成员管理与扫码授权;空名单以引导态启动
技术实现
- 语言: JavaScript ESM(
.mjs,纯 ESM,零构建步骤) - 关键依赖:
@deepseek-ai/cordis(宿主插件框架)、@larksuiteoapi/node-sdk(飞书长连接,懒加载可选)、@tencent-connect/qqbot-connector(QQ 机器人,可选)、qrcode-terminal(终端扫码,可选) - 架构模式: 声明式 spec 引擎(绝大多数渠道以数据声明表生成适配器)+ 12 个手写适配器;通过
ctx.provide('notifier', facade)暴露服务给宿主和其他插件;事件总线监听turn/end、approval/asked、agent/error自动推送;HMAC 一次性 token 处理远程审批 - 入口文件:
src/index.mjs(apply 装配入口)、src/config.mjs(配置解析与默认值)、src/notify.mjs(出站核心)
适用场景
你装了 agent 跑 DSH,但任务动辄十几分钟起步,桌面挂着浏览器看进度很费神;想切到手机就能收到完成/出错通知,点卡片就能停掉跑飞的 turn,甚至能在路上回一条消息纠偏 agent 的执行方向。希望同一套配置既覆盖 Telegram、飞书这些海外/办公 IM,也覆盖 Server酱、Bark、PushPlus 这些国内推送 App,且新增渠道不用动主流程。
前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| DSH | 0.1.0-rc.6 | dshWorkshop.compatibility.dshVersions 显式声明;通过 cordis.patch.yml 注入插件行 |
| Node.js | >= 22 | engines.node 强制要求;飞书与 QQ 入站依赖原生 WebSocket |
| 平台 | 跨平台 | 桌面通知渠道在 macOS/Windows/Linux 各有原生实现(osascript/notify-send/PowerShell Toast),其余渠道纯网络无平台限制 |
| 原生模块 | 无 | 仅用 node:crypto、原生 WebSocket、fetch;不引入任何 native addon |
安装方式
dsh plugin --profile web add github:THEWOLFWALKER/dsh-notifier
配置项
写在 profile 的 cordis.patch.yml 中 dsh-notifier 行的 config 下。主要块如下(详细 schema 见 src/config.mjs):
| 配置 | 类型 | 说明 | 默认值 |
|---|---|---|---|
channels[] | 数组 | 渠道列表,每项含 type 与渠道凭证字段(如 telegram 的 botToken+chatId) | 空数组(不报错,无操作) |
enabled | 布尔 | 总开关,关掉后不订阅事件不注册工具,仅暴露 no-op stub 服务保证依赖方不卡启动 | 开 |
titlePrefix | 字符串 | 自动推送标题前缀(可空) | 空 |
summaryMaxChars | 数字 | 自动推送回复正文截断字符数 | 500 |
debounceMs | 数字 | 同会话连续事件尾沿合并窗口(毫秒) | 10000 |
graceSeconds | 数字 | turn 结束后的空闲宽限窗,期间用户在终端输入即取消打扰 | 0 |
events.turnStart.enabled | 布尔 | 任务开始是否推送 | 关 |
events.longRunning | 对象 | 长任务心跳(firstAfterMs 首条延迟、everyMs 后续间隔,下限 60s) | 开,900000 / 同值 |
events.stall | 对象 | 疑似卡住提醒(afterMs 静默时长阈值,下限 60s) | 开,600000 |
events.turnEnd | 布尔/对象 | 任务结束总开关,可按 reason.kind(completed/error/blocked/aborted/max-tokens/interrupted)分控 | 全开 |
events.approval | 布尔 | 审批请求是否推送 | 开 |
events.agentError | 布尔 | agent 报错是否推送 | 开 |
toolRateLimitPerMinute | 数字 | 模型调 notify 工具每分钟上限(防注入刷屏) | 10 |
inbound.allowUsers | 字符串数组 | 入站白名单用户 ID(首次 YAML 播撒后由管理台或 /pair 管理) | 空(进入引导态) |
inbound.telegram / feishu / qq / wxpusher / wechat / dingtalk | 对象 | 各入站通道凭证(具体字段见 FIELD_HINTS,可省略走扫码/CLI 落盘凭证) | 各自缺省 |
inbound.tokenSecret | 字符串 | HMAC 一次性 token 的对称密钥;不填则重启后旧 token 全废 | 进程随机 |
inbound.stateDir | 字符串 | state.json / 账本 / 审计文件目录 | ~/.dsh-notifier/ |
inbound.conversation | 对象 | 会话路由参数(mergeWindowMs 等) | 默认 |
approval.mode | 字符串 | 审批远程响应模式(answer 远程可决 / observe 只旁观) | 未启用 |
approval.timeoutMs | 数字 | 审批超时回落桌面的等待时长 | 120000 |
questions.enabled | 布尔 | 是否启用 ask_user 远程提问工具 | 开 |
questions.timeoutMs | 数字 | 提问超时(30s-30min 钳制) | 300000 |
questions.rateLimitPerMinute | 数字 | 模型调 ask_user 每分钟上限 | 6 |
route.sessionTtlHours | 数字 | 会话路由过期时长 | 24 |
admin.enabled | 布尔 | 是否启动本机 Web 管理台(host 写死 127.0.0.1 不可配) | 关 |
admin.port | 数字 | 管理台端口(1-65535) | 8104 |
admin.token | 字符串 | 管理台 Bearer token;不填首启自动生成(stdout 打印一次) | 自动生成 |
digest.enabled | 布尔 | 是否启用通知账本 + 每日摘要推送 | 关 |
digest.maxEntries | 数字 | 账本保留条目上限 | 默认 |
segment.maxCodepoints | 数字 | 超长消息按 Unicode 码点分段阈值 | 1200 |
public.enabled | 布尔 | 是否对外暴露 notifier 服务 | 开 |
public.limitPerMinutePerSource | 数字 | 每个调用方每分钟推送上限 | 10 |
public.emit | 布尔 | 是否发射 dsh-notifier/sent 事件 | 开 |
常见问题
Q: 安装后需要先做什么才能收到通知?
A: 在 profile 的 cordis.patch.yml 里给 dsh-notifier 行的 config 加 channels,至少配置一个渠道的凭证。空 channels 时插件不会报错,只会订阅事件但不动作。
Q: 怎么把审批从手机送回桌面?
A: 启用 inbound 部分并配置至少一个入站通道(telegram/feishu/qq/wxpusher/wechat/dingtalk)。首次部署时在 inbound.allowUsers 写白名单用户 ID(YAML 首播撒后改由管理台或 /pair 管理)。后续在通道私聊里点按钮或回复 1/2 即可裁决。
Q: 通知频率怎么控制?
A: 三个维度叠加:events 段控制哪些宿主事件触发推送;debounceMs 控制同会话连续轮次尾沿合并(默认 10s);graceSeconds 控制空闲宽限窗(默认 0)。三者互不影响,可独立调。
Q: 长任务跑着没动静怎么知道还活着?
A: 默认 longRunning(15 分钟首条心跳、之后每 15 分钟再来一条)和 stall(10 分钟无事件即疑似卡住)都开启。Telegram/飞书渠道的通知卡片自带一键停止按钮,点下去就取消 turn。turnStart 默认关闭,避免每个任务开始都响。
Q: 其他插件可以借用这个通知系统吗?
A: 可以。注入 notifier 服务(ctx.inject(['notifier'], …))调 push(),或订阅 dsh-notifier/sent 事件监听每次广播结果。按 source 限流 10 次/分钟(public.limitPerMinutePerSource),超额返回结构化 skipped 不会抛错。
Q: Web 管理台安全吗?
A: host 写死 127.0.0.1,永不绑公网(不可配)。token 首启自动生成并在 stdout 打印一次,重启后用 SHA-256 哈希校验;显式配置 admin.token 时以其为准。需要远程访问请自行反代。
Q: 卸载后状态数据会保留吗?
A: state.json 留在 $HOME/dsh-notifier/(stateDir 可改),记录白名单、配对码、账本、审计日志。卸载插件不会自动清盘,重装可继续;如需彻底清理请手动删除该目录。
Q: 渠道配额用完或者断网时会发生什么?
A: 每渠道独立熔断(短时重试退避),未达熔断阈值时按分档重试;阈值耗尽则该渠道跳过,宿主继续运行,账本记录失败次数,其他渠道不受影响。
上手难度
进阶 — 配置矩阵大(27 渠道 × 入站出站双向 × 多级开关),新手建议从 Web 管理台入手(自动生成配置),手写 YAML 时注意 ${ENV:NAME} 引用语义(出/入站均支持)。
已知问题与限制
- WxPusher 入站依赖 HTTP 回调,需要公网可达(frp/反代由用户自行解决);其他 5 个入站通道走长连接/长轮询,免公网 IP
- QQ/钉钉/微信入站不支持按钮卡片,仅靠编号回复(回复 1 批准 / 2 拒绝)裁决
- Windows 系统桌面通知需要 PowerShell 的 BurntToast 模块,否则该渠道本地侧无效
- 钉钉 Stream 实现里复用了官方 SDK 字段拼写(
uesrAgent,源码里已写明)——属上游约定,照搬 - 飞书入站依赖
@larksuiteoapi/node-sdk(optionalDependency),未安装时 SDK 加载失败给中文指引后静默降级 - 长任务心跳/卡住提醒的时距下限钳制 60s(
Math.max(60_000, Number(x) || 默认)),不允许配 0 关闭——关闭请用enabled: false state.json多进程同写已加键级合并 + 跨进程写锁 + 读收敛三层防御,但极端情况下(同时大量写入)仍可能降级为无锁写入并 warn 一次- v0.7.3 修复过飞书 WSClient
logger: null静默崩溃与卡片 message_id 取值错(CHANGELOG #1/#6),升级前请确认已修复 - Web 管理台 host 写死 127.0.0.1(不可配),需要远程访问由用户自行反代——任何暴露管理台的尝试都被代码层阻断
Your agent, in your pocket. — 通知、审批、遥控,全在你的手机里。
English · 简体中文
Unified notification push plugin for DeepSeek Harness (DSH) — one minimal notify() API in front, 27 channels behind.
Your agent and the harness itself both push through it: session events (turn/end · approval/asked · agent/error) auto-notify, the model calls a notify tool directly, and six inbound channels bring approvals and conversations back from your phone. v0.3 adds a local web console and multi-agent routing; v0.4 adds native desktop notifications; v0.5 turns your phone into a command center — long-task heartbeats, stall alerts, and a stop button riding the notification itself; v0.7 upgrades "who counts as family" from opaque YAML strings into a runtime identity system — pairing codes, composite-key bindings, and a members page in the admin console — all with zero runtime dependencies.
How it works
DSH agent ──notify() tool─────────┐
├─▶ notifier core ─▶ 27 channels (IM webhooks / push apps / China apps)
DSH session events ──auto push────┘ level routing · tiered retries · segmentation · anti-disturb · ledger
heartbeat ⏱ / stall ⚠ (v0.5) ──▶ cards with a ⏹ stop button
your phone ──6 inbound channels───▶ remote approval (buttons · reply 1/2) · remote conversation (followup/inject/steer)
Every message resolves through one chain — level (timeSensitive / active / passive) → routing (multi-agent matrix) → channel adapter (resolve(cfg) + send(msg)). Two trigger lines feed it: the harness auto-pushes session events (debounced, deduped), and the model calls the notify tool. Six inbound channels ride the same core in reverse for approvals and conversation — and since v0.5 the outbound line reports back too: long-running turns send heartbeats, silent turns raise stall alerts, and Telegram/Feishu notifications carry a one-click stop action.
Screenshots
The web admin console (admin.enabled: true, loopback only, mobile-friendly since v0.5) — all six pages (demo data):
| Page | What it shows |
|---|---|
| Dashboard | session stats, outbound/inbound channel health groups, recent audit |
| Notifications (v0.4.0) | live SSE event stream, system-notification preferences, event log |
| Members (v0.7.0) | identity bindings (roles / labels / pairing time), pairing codes, pending-binding confirmations |
| Bindings | agent × channel checkbox grid, per-channel default agent |
| Sessions | per-session outbound resolution with override editing |
| Channels | credential forms for every channel (masked ***), test send, QR scan |

Quick start
dsh plugin add dsh-notifier --profile <profile-name>
--profileis required (DSH 0.1.0-rc.6+): plugin installs target a named profile — use the one you run (e.g.web).
Add channels to your profile patch (cordis.patch.yml):
insert:
- id: dsh-notifier
name: dsh-notifier
config:
channels:
- type: telegram
botToken: "123456:ABC-DEF..."
chatId: "987654321"
- type: dingtalk
webhook: "https://oapi.dingtalk.com/robot/send?access_token=..."
secret: "SEC..."
- type: bark
key: "your-device-key"
That's it. turn/end, approval/asked, and agent/error events now reach every configured channel, and the model can push on its own with notify({ message, channel, title }). Long tasks send heartbeats and stall alerts out of the box (v0.5 defaults), and you can stop a runaway turn right from the notification card.
Core features
| Feature | What it does |
|---|---|
| Dual trigger lines | Auto status push (turn/end · approval/asked · agent/error) plus a model-facing notify tool. |
| 27 channels | Telegram, Slack, Discord, Feishu, DingTalk, WeCom, WeCom App, QQ bot, OneBot, Teams, Mattermost, Google Chat, Bark, Pushover, PushDeer, Chanify, ntfy, Gotify, iGot, WxPusher, PushPlus, Server酱, Qmsg, 息知, webhook, bell, desktop — zero runtime deps. |
| Level routing | timeSensitive / active / passive → per-channel delivery semantics (silent push, priority headers, @-mentions) with tiered retries. |
| Remote approval | Answer approvals from your phone — Telegram buttons, Feishu cards, QQ / WxPusher / WeChat iLink / DingTalk reply 1/2. Silence never approves. |
| Remote conversation | Chat with your agent: plain text → followup/inject, ! prefix steers mid-turn, a merge window reassembles mobile typing. |
| Mobile command center (v0.5.0) | Long-task heartbeats (default 15min start) and stall alerts (default 10min no events); Telegram/Feishu cards carry a ⏹ stop button (HMAC one-time tokens, same trust chain as approvals); /quiet·/unquiet mute or restore a session's pushes from your phone. |
| Open event source (v0.6.0) | Other plugins push via the notifier service (ctx.inject(['notifier'], …) — shared config, routing, ledger, rate limits, flush) and subscribe to every broadcast via ctx.on('dsh-notifier/sent'). Per-source rate limiting (10/min), 20k-codepoint clamps, never-reject API; consumer contract in PLUGINS.md. |
| Identity system (v0.7.0) | "Who can drive inbound" becomes a runtime object: pairing codes (/pair <code> in any DM; first redeemer becomes owner), composite-key bindings (channel:userId — a Telegram-bound id no longer admits a Feishu message), role management (last owner can't be deleted or demoted), and rejection receipts that tell unbound senders how to get in. Empty whitelist boots into a guided state with a bootstrap pairing code on stderr instead of refusing to start. Full setup-to-daily-use walkthrough: docs/guide.md (中文). |
| Multi-agent routing (v0.3.2) | Bidirectional agent × channel matrix; sessions auto-register; /agent command family + route.mjs CLI. |
| Web admin console (v0.3.3) | 127.0.0.1-only + Bearer token; six pages — dashboard / notify / members (v0.7) / bindings / sessions / channels; responsive ≤768px layout (v0.5). |
| QR login (v0.3.1) | One-command official scan authorization for QQ / DingTalk / Feishu (WeChat keeps iLink). |
| Desktop notifications (v0.4.0) | Native desktop channel (osascript / notify-send / PowerShell toast) + admin SSE live stream. |
| Long-message segmentation | Over-budget messages split into ordered (i/n) segments. |
| Anti-disturb rules | Per-result event gating, keyword include/exclude, idle grace window. |
| Ledger & daily digest | Append-only JSONL ledger + one passive summary of yesterday's traffic. |
| Secrets safe | role('secret') keys redacted everywhere; ${ENV:NAME} refs keep secrets out of the profile. |
| Never breaks startup | Misconfigured channels are skipped silently with a log line. |
Configuration
All channels live under config.channels. Key example:
insert:
- id: dsh-notifier
config:
channels:
- type: telegram
botToken: "123456:ABC-DEF..."
chatId: "987654321"
- type: feishu
webhook: "https://open.feishu.cn/open-apis/bot/v2/hook/..."
- type: wxpusher
appToken: "AT_..."
uids: ["UID_..."]
- type: serverchan
sct: "SCT..."
Optional blocks each opt in under their own key:
| Block | Purpose | Key |
|---|---|---|
inbound | Remote approval + conversation | allowUsers: [...] (first-import only since v0.7; manage members at runtime via the admin console or /pair) |
approval | Timeout, numbered reply, escalation | mode: answer |
conversation | Merge window, steer prefix | mergeWindowMs: 1500 |
route | Multi-agent routing | sessionTtlHours: 24 |
admin | Web console | enabled: true, port: 8104 |
events / keywords / graceSeconds | Anti-disturb gates | exclude: ["heartbeat"] |
events.turnStart / longRunning / stall | v0.5 status line | longRunning: { firstAfterMs: 900000 } |
digest | Ledger + daily summary | enabled: true |
v0.5 status line defaults: longRunning and stall are on (15min first heartbeat, then every 15min; stall after 10min of silence) — zero-config long tasks are no longer a black box. turnStart is off by default (one message per turn is noise at the desk; turn it on when you fire a task and walk away). All timings clamp to a 60s floor; disable any of them with enabled: false.
Channels
| type | Channel | Auth | Free? |
|---|---|---|---|
bark | Bark (iOS) | device key (or self-host URL) | ✅ |
bell | Terminal bell (local) | — | local |
chanify | Chanify (iOS) | token (or self-host) | ✅ |
desktop | Desktop notification (local) | — (Windows needs BurntToast module) | local |
dingtalk | DingTalk custom robot | webhook + secret (HMAC sign) | ✅ |
discord | Discord webhook | webhook URL | ✅ |
feishu | Feishu custom bot | webhook (+ sign secret) | ✅ |
gchat | Google Chat | space webhook URL | ✅ |
gotify | Gotify | server URL + app token | self-host |
igot | iGot (iOS) | push key | ✅ (limits) |
mattermost | Mattermost | base URL + token (+ channel) | self-host |
ntfy | ntfy | topic (+ server URL) | ✅ (self-host) |
onebot | OneBot 11 (QQ) | HTTP endpoint | self-host |
pushdeer | PushDeer | push key | ✅ |
pushover | Pushover | user key + app token | paid (one-time) |
pushplus | PushPlus (WeChat) | token | ✅ (limits) |
qmsg | Qmsg酱 (QQ) | key + qq number | ✅ (limits) |
qq-bot | QQ official bot | appId + appSecret | ✅ |
serverchan | Server酱 (WeChat) | sendkey | ✅ (limits) |
slack | Slack | incoming webhook URL | ✅ |
teams | Microsoft Teams | Power Automate workflow URL | ✅ |
telegram | Telegram Bot API | bot token + chat id | ✅ |
webhook | Any custom endpoint | — | — |
wecom | WeCom group robot | webhook key | ✅ |
wecom-app | WeCom app message | corpid + agentId + secret | ✅ |
wxpusher | WxPusher (WeChat) | appToken + uid | ✅ (limits) |
xizhi | 息知 Xizhi | sendkey | ✅ (limits) |
Six channels also open inbound (remote approval + conversation): telegram, feishu, qq-bot, wxpusher, wechat, dingtalk — long-lived connections or long polling, so no public IP is required (only the WxPusher callback needs one). Since v0.5, telegram and feishu additionally carry notification action cards (stop button). Since v0.7, every inbound channel answers /help /whoami /pair /unpair registration commands, and outbound card targets resolve through a three-tier priority (per-channel bindings → channel config lists → global fallback) with per-channel id-shape guards.
Architecture
src/
adapters/ 27 channel adapters (resolve(cfg) + send(msg)) + declarative spec engine
config.mjs channel registry + config schema — single source of truth for the matrix
index.mjs plugin assembly: patch, tools, event listeners, admin wiring
event-listener.mjs auto-push line (debounce, dedup, level routing) + v0.5 status wiring
status/ v0.5 turn tracker (heartbeat / stall detection, pure logic)
actions.mjs v0.5 notification action dispatch (turn/cancel, HMAC one-time tokens)
notify.mjs notify / notify_test tools + sliding-window rate limiting
routing/ multi-agent matrix (resolveOutbound / resolveInbound)
inbound/ six inbound channels (telegram/feishu/qq/wxpusher/wechat/dingtalk) + v0.7 identity stack
(identity.mjs bindings · pairing.mjs codes · commands.mjs registration · target-guard.mjs resolution)
approval/ HMAC one-time tokens, dedup, escalation
admin/ web console (6 pages, SSE, bearer auth, mobile layout)
ledger.mjs JSONL ledger + daily digest
rules.mjs anti-disturb gates (event / keyword / grace)
scripts/ channel-login.mjs · test-channel.mjs · route.mjs · gen-channel-matrix.mjs
test/ 846 tests (node --test)
Design rules: pure ESM (.mjs), zero runtime dependencies, a declarative spec engine for the bulk of channels, thin honest adapters, no build step.
Development
npm test # node --test, 846 cases
To add a channel: implement the adapter interface (resolve(cfg) + send(msg)) in src/adapters/ and register it in src/config.mjs; the channel matrix above self-regenerates via node scripts/gen-channel-matrix.mjs.
License
MIT · third-party notices in THIRD_PARTY_NOTICES.md