DSH Unified Notification & Remote Control Plugin: A single notify() API integrates with 27 push channels, while sending approvals, questions, and conversations from mobile back to desktop. Long-running tasks use automatic heartbeat with one-click stop support.
- Language
- JavaScript
- License
- MIT
- Branch
- main
Install
$ dsh plugin --profile web add 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
Install via your agent
Install the DeepSeek Harness plugin THEWOLFWALKER/dsh-notifier for me: review the repository at https://github.com/THEWOLFWALKER/dsh-notifier first, then run the install command and verify the plugin loads successfully.
Paste this instruction to the DSH Web GUI assistant — it will install and verify for you.
One-Line Pitch
Unified notification and remote control plugin for DSH: host events (task completion/error/approval) and model-initiated pushes share a single notify() API to deliver messages to 27 push channels; 6 of those channels work bidirectionally to pull approvals, questions, and messages from phone back to desktop, and transform long-running tasks into a mobile command center with heartbeat and stop button.
Core Capabilities
- 27 Outbound Channels: Telegram, Slack, Discord, Feishu, DingTalk, Enterprise WeChat (group/app), QQ Bot, OneBot, Teams, Mattermost, Google Chat, Bark, Pushover, PushDeer, Chanify, ntfy, Gotify, iGot, WxPusher, PushPlus, Server酱, Qmsg, 息知, Webhook, Terminal Bell, System Desktop Notification
- Automatic Status Push: Host events (task start, completion, error, blocked, aborted, token limit reached, interrupted, approval needed) automatically forwarded to all configured channels; same-session rounds deduplicated with 10-second trailing-edge merging
- Tiered Routing: Emergency/Active/Passive three-tier semantics mapped to each channel's native delivery form (silent push, priority, @mention), with tiered retry
- Remote Approval & Dialogue: Six channels (Telegram, Feishu, QQ, WxPusher, WeChat, DingTalk) support callbacks; click buttons or reply 1/2 to remotely decide approvals; plain text messages enter agent session (follow-up questions/mid-course injection/steer)
- Mobile Command Center: Long tasks send first heartbeat after 15 minutes + one every 15 minutes after; no events for 10 minutes triggers potential stall warning; Telegram/Feishu cards come with one-stop stop button
- Remote Questions: ask_user tool pushes multiple-choice questions to phone (Feishu/Telegram option cards, other channels reply with number); user answers are sent back to agent; never auto-answer on timeout
- Local Web Console: Binds to 127.0.0.1 + Bearer token only, includes 6 pages: overview, notification stream, members, pairing codes, binding matrix, session ledger, channel credentials; mobile-adaptive
- Open Event Source: Other plugins can inject into notifier service for pushing, or subscribe to dsh-notifier/sent event to monitor each broadcast result; rate-limited to 10/min per source
- Identity System: Runtime pairing code admission, composite key binding (channel:userId isolates channel identity), member management, and QR code authorization; empty list starts in guided mode
Technical Implementation
- Language: JavaScript ESM (
.mjs, pure ESM, zero build steps) - Key Dependencies:
@deepseek-ai/cordis(host plugin framework),@larksuiteoapi/node-sdk(Feishu long connection, lazy-loaded optional),@tencent-connect/qqbot-connector(QQ Bot, optional),qrcode-terminal(terminal QR code, optional) - Architecture: Declarative spec engine (most channels generate adapters from data declaration tables) + 12 hand-written adapters; exposes service to host and other plugins via
ctx.provide('notifier', facade); event bus auto-pushes onturn/end,approval/asked,agent/error; HMAC one-time token for remote approval - Entry Files:
src/index.mjs(apply entry),src/config.mjs(config parsing and defaults),src/notify.mjs(outbound core)
Use Cases
You have an agent running on DSH, but tasks often take 10+ minutes, and keeping a browser open on desktop to watch progress is exhausting; you want to receive completion/error notifications on your phone, click a card to stop a runaway turn, or even reply from the road to steer the agent's execution direction. The same configuration should cover both overseas/office IM like Telegram and Feishu, and domestic push apps like Server酱, Bark, and PushPlus, with new channels not requiring changes to the main flow.
Prerequisites & Compatibility
| Dependency | Min Version | Notes |
|---|---|---|
| DSH | 0.1.0-rc.6 | Explicitly declared via dshWorkshop.compatibility.dshVersions; injected via cordis.patch.yml |
| Node.js | >= 22 | Enforced by engines.node; Feishu and QQ inbound depend on native WebSocket |
| Platform | Cross-platform | Desktop notification channels have native implementations on macOS/Windows/Linux (osascript/notify-send/PowerShell Toast), other channels are network-only |
| Native Modules | None | Uses only node:crypto, native WebSocket, fetch; no native addons |
Installation
dsh plugin --profile web add github:THEWOLFWALKER/dsh-notifier
Configuration Options
Written under config of the dsh-notifier entry in profile's cordis.patch.yml. Main blocks as follows (detailed schema in src/config.mjs):
| Config | Type | Description | Default |
|---|---|---|---|
channels[] | Array | Channel list, each item contains type and channel credential fields (e.g., telegram's botToken+chatId) | Empty array (no error, no action) |
enabled | Boolean | Master switch; when off, no event subscription or tool registration, only exposes no-op stub service to prevent dependency blockers | On |
titlePrefix | String | Auto-push title prefix (can be empty) | Empty |
summaryMaxChars | Number | Auto-push reply body truncation character count | 500 |
debounceMs | Number | Same-session consecutive event trailing-edge merge window (ms) | 10000 |
graceSeconds | Number | Idle grace window after turn ends; user terminal input cancels disturbance | 0 |
events.turnStart.enabled | Boolean | Whether to push on task start | Off |
events.longRunning | Object | Long task heartbeat (firstAfterMs first delay, everyMs subsequent interval, min 60s) | On, 900000 / same value |
events.stall | Object | Potential stall warning (afterMs silent duration threshold, min 60s) | On, 600000 |
events.turnEnd | Boolean/Object | Master switch for task completion; can control per reason.kind (completed/error/blocked/aborted/max-tokens/interrupted) | All on |
events.approval | Boolean | Whether to push approval requests | On |
events.agentError | Boolean | Whether to push agent errors | On |
toolRateLimitPerMinute | Number | Model's notify tool calls per minute limit (prevent injection spam) | 10 |
inbound.allowUsers | String Array | Inbound whitelist user IDs (first YAML broadcast then managed via console or /pair) | Empty (enters guided mode) |
inbound.telegram / feishu / qq / wxpusher / wechat / dingtalk | Object | Each inbound channel credentials (specific fields in FIELD_HINTS, can be omitted for QR code/CLI credential fallback) | Each defaults |
inbound.tokenSecret | String | Symmetric key for HMAC one-time tokens; if not set, all old tokens invalidated after restart | Process random |
inbound.stateDir | String | state.json / ledger / audit file directory | ~/.dsh-notifier/ |
inbound.conversation | Object | Conversation routing params (mergeWindowMs, etc.) | Default |
approval.mode | String | Approval remote response mode (answer remote can decide / observe observe only) | Not enabled |
approval.timeoutMs | Number | Approval timeout fallback wait time to desktop | 120000 |
questions.enabled | Boolean | Whether to enable ask_user remote question tool | On |
questions.timeoutMs | Number | Question timeout (30s-30min clamp) | 300000 |
questions.rateLimitPerMinute | Number | Model's ask_user calls per minute limit | 6 |
route.sessionTtlHours | Number | Conversation routing expiration duration | 24 |
admin.enabled | Boolean | Whether to start local web console (host hardcoded to 127.0.0.1, not configurable) | Off |
admin.port | Number | Console port (1-65535) | 8104 |
admin.token | String | Console Bearer token; if not set, auto-generated on first start (printed to stdout once) | Auto-generated |
digest.enabled | Boolean | Whether to enable notification ledger + daily digest push | Off |
digest.maxEntries | Number | Ledger entry retention limit | Default |
segment.maxCodepoints | Number | Unicode codepoint segmentation threshold for overly long messages | 1200 |
public.enabled | Boolean | Whether to expose notifier service externally | On |
public.limitPerMinutePerSource | Number | Each caller push limit per minute | 10 |
public.emit | Boolean | Whether to emit dsh-notifier/sent events | On |
FAQ
Q: What do I need to do first after installation to receive notifications?
A: Add channels under config of the dsh-notifier entry in profile's cordis.patch.yml, configuring credentials for at least one channel. Empty channels won't cause plugin errors—it will just subscribe to events without acting.
Q: How do I send approvals from phone back to desktop?
A: Enable the inbound section and configure at least one inbound channel (telegram/feishu/qq/wxpusher/wechat/dingtalk). On first deployment, write whitelist user IDs in inbound.allowUsers (after first YAML broadcast, managed via console or /pair). Then in the channel's private chat, click buttons or reply 1/2 to decide.
Q: How is notification frequency controlled?
A: Three dimensions stack: events section controls which host events trigger pushes; debounceMs controls same-session consecutive turn trailing-edge merge (default 10s); graceSeconds controls idle grace window (default 0). These three don't affect each other and can be tuned independently.
Q: How do I know a long-running task is still alive?
A: Both longRunning (first heartbeat after 15 minutes, then one every 15 minutes) and stall (no events for 10 minutes = potential stall) are on by default. Telegram/Feishu channel notification cards have a one-stop stop button—clicking it cancels the turn. turnStart is off by default to avoid alerting on every task start.
Q: Can other plugins use this notification system?
A: Yes. Inject notifier service (ctx.inject(['notifier'], …)) and call push(), or subscribe to dsh-notifier/sent event to monitor each broadcast result. Rate-limited to 10/min per source (public.limitPerMinutePerSource); excess returns structured skipped without throwing errors.
Q: Is the web console secure?
A: Host is hardcoded to 127.0.0.1, never binding to public internet (not configurable). Token is auto-generated on first start and printed to stdout once, validated with SHA-256 hash after restart; explicitly configured admin.token takes precedence. For remote access, set up your own reverse proxy.
Q: Will state data be retained after uninstall?
A: state.json stays in $HOME/dsh-notifier/ (stateDir configurable), recording whitelist, pairing codes, ledger, audit logs. Uninstalling the plugin doesn't auto-clean the disk; reinstallation can continue; manually delete the directory for complete cleanup.
Q: What happens when a channel quota is exhausted or network is down?
A: Each channel has independent circuit-breaking (short retry with backoff); when below threshold, tiered retry applies; when threshold exhausted, that channel is skipped, host continues running, ledger records failure count, other channels unaffected.
Learning Curve
Advanced — large configuration matrix (27 channels × bidirectional inbound/outbound × multi-level switches). Beginners should start with the web console (auto-generates config). When hand-writing YAML, note ${ENV:NAME} reference semantics (supported for both outbound and inbound).
Known Issues & Limitations
- WxPusher inbound depends on HTTP callback, requires public internet reach (frp/reverse proxy user responsibility); other 5 inbound channels use long connection/long polling, no public IP needed
- QQ/DingTalk/WeChat inbound don't support button cards, rely solely on number replies (reply 1 approve / 2 reject)
- Windows desktop notifications require PowerShell's BurntToast module, otherwise that channel is locally ineffective
- DingTalk Stream implementation reuses official SDK field naming (
uesrAgent, noted in source) — upstream convention, copied as-is - Feishu inbound depends on
@larksuiteoapi/node-sdk(optionalDependency); when not installed, SDK load failure gives Chinese guidance then silently downgrades - Long task heartbeat/stall warning interval min clamped at 60s (
Math.max(60_000, Number(x) || default)), setting 0 not allowed — useenabled: falseto disable state.jsonmulti-process concurrent writes already have key-level merge + cross-process write lock + read coalescing three-layer defense, but extreme cases (massive concurrent writes) may degrade to lock-free writes with one-time warn- v0.7.3 fixed Feishu WSClient
logger: nullsilent crash and card message_id wrong value (CHANGELOG #1/#6), please confirm fix before upgrading - Web console host hardcoded to 127.0.0.1 (not configurable), any attempt to expose is blocked at code level — remote access requires user-provided reverse proxy
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
Read the usage guide →
Install steps, key points, FAQ and compatibility for this plugin — auto-derived from indexed fields.
Listing badge
[](https://deepseek-plugin.org/plugins/THEWOLFWALKER/dsh-notifier)Paste this markdown into your GitHub README to link back to this listing. The badge only states the listing — not a security endorsement.