dsh-notifier

August 27, 2026 · View on GitHub

Your agent, in your pocket. — 通知、审批、遥控,全在你的手机里。

English · 简体中文

DSH Node.js JavaScript Cordis Zero deps Bilingual Channels

npm version tests license awesome-dsh-plugin omdsh workshop dshfind dshfind downloads dshfind plugin card

never miss silence push

Package metadata: dsh-notifier@0.9.0 · 1352 automated contract tests (1351 pass + 1 skip) · MIT licensed.

Bring your DeepSeek Harness agent to the places you already use. dsh-notifier puts one minimal notify() API in front of 27 channels, then adds phone-friendly approvals, questions, session controls, and a calm local console — with no second runtime to deploy.

Get started · Upgrade guide · Plugin integration

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. QQ control ingress is source-marked: C2C is single-chat, while GROUP, missing chatType, and unknown source metadata fail closed; the conversation routeUnsafe bypass is blocked. 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; v0.8 lets the agent ask you multiple-choice questions straight from your phone (ask_user: option cards + numbered-reply fallback, timeout never fabricates an answer) — 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) · remote questions (option cards, v0.8)

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). The browser opens in personal mode: first configure, pair, test, then use; bindings and sessions stay behind an explicit advanced-settings toggle. Open the exact loopback URL printed by the Web 管理台已就绪 startup line instead of guessing port 8104:

PageWhat it shows
Dashboardsession 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
Bindingsagent × channel checkbox grid, per-channel default agent
Sessionsper-session outbound resolution with override editing
Channelscredential forms for every channel (masked ***), test send, QR scan

Dashboard Notify Bindings Sessions Channels

Quick start

dsh plugin add dsh-notifier --profile <profile-name>

--profile is 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

FeatureWhat it does
Dual trigger linesAuto status push (turn/end · approval/asked · agent/error) plus a model-facing notify tool.
27 channelsTelegram, 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 routingtimeSensitive / active / passive → per-channel delivery semantics (silent push, priority headers, @-mentions) with tiered retries.
Remote approvalAnswer approvals from your phone — Telegram/Feishu cards, QQ native buttons in 1:1 chats, and numbered-reply fallback on channels without cards. Group destinations use a safe text fallback. Silence never approves.
Remote conversationChat with your agent: plain text → followup/inject, ! prefix steers mid-turn, a merge window reassembles mobile typing.
Remote questions (v0.8.0)The model asks you multiple-choice questions on your phone: 1-4 questions × 2-5 options (multi-select supported). Option cards on Feishu/Telegram and QQ 1:1 chats; other destinations use a safe numbered-reply fallback. Out-of-range answers get a re-prompt without voiding the question; timeout never fabricates an answer. Same trust chain as approvals (HMAC one-time tokens, first-arrival wins, 30s/60s escalation). The loopback Web/admin console provides a masked pending-question list with choose/reject through Control Core.
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 delivery metadata via ctx.on('dsh-notifier/sent'). Broadcast and directed sends each produce one audited event; message text is never exposed. 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 written to a local 0600 file (<stateDir>/bootstrap-paircode.txt; logs print the path, never the code) 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) with personal-first onboarding and progressive disclosure.
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 segmentationOver-budget messages split into ordered (i/n) segments.
Anti-disturb rulesPer-result event gating, keyword include/exclude, idle grace window.
Ledger & daily digestAppend-only JSONL ledger + one passive summary of yesterday's traffic.
Secrets saferole('secret') keys redacted everywhere; ${ENV:NAME} refs keep secrets out of the profile.
Never breaks startupMisconfigured 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:

BlockPurposeKey
inboundRemote approval + conversationallowUsers: [...] (first-import only since v0.7; manage members at runtime via the admin console or /pair)
approvalTimeout, numbered reply, escalationmode: answer
conversationMerge window, steer prefixmergeWindowMs: 1500
routeMulti-agent routingsessionTtlHours: 24
adminWeb consoleenabled: true (optional port; use the startup URL)
events / keywords / graceSecondsAnti-disturb gatesexclude: ["heartbeat"]
events.turnStart / longRunning / stallv0.5 status linelongRunning: { firstAfterMs: 900000 }
digestLedger + daily summaryenabled: 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

typeChannelAuthFree?
barkBark (iOS)device key (or self-host URL)
bellTerminal bell (local)local
chanifyChanify (iOS)token (or self-host)
desktopDesktop notification (local)— (Windows needs BurntToast module)local
dingtalkDingTalk custom robotwebhook + secret (HMAC sign)
discordDiscord webhookwebhook URL
feishuFeishu custom botwebhook (+ sign secret)
gchatGoogle Chatspace webhook URL
gotifyGotifyserver URL + app tokenself-host
igotiGot (iOS)push key✅ (limits)
mattermostMattermostbase URL + token (+ channel)self-host
ntfyntfytopic (+ server URL)✅ (self-host)
onebotOneBot 11 (QQ)HTTP endpointself-host
pushdeerPushDeerpush key
pushoverPushoveruser key + app tokenpaid (one-time)
pushplusPushPlus (WeChat)token✅ (limits)
qmsgQmsg酱 (QQ)key + qq number✅ (limits)
qq-botQQ official botappId + appSecret
serverchanServer酱 (WeChat)sendkey✅ (limits)
slackSlackincoming webhook URL
teamsMicrosoft TeamsPower Automate workflow URL
telegramTelegram Bot APIbot token + chat id
webhookAny custom endpoint
wecomWeCom group robotwebhook key
wecom-appWeCom app messagecorpid + agentId + secret
wxpusherWxPusher (WeChat)appToken + uid✅ (limits)
xizhi息知 Xizhisendkey✅ (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). Telegram/Feishu and QQ C2C single chats use native control buttons; other targets receive safe text or numbered-reply fallbacks, and the conversation routeUnsafe path cannot bypass the source-safety gate. QQ, WeChat iLink, and DingTalk image-message paths are wired; file receiving/sending follows the capability matrix. The loopback Web/admin console is the single control surface and offers a masked list of pending multi-choice questions plus choose/reject settlement through the shared Control Core. 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
  questions/          v0.8 ask_user remote questions (option cards + numbered fallback, rides the approval stack)
  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/                 1352 tests (1351 pass + 1 skip) in the 0.9.0 release line; historical 0.8.6 package carried 909 tests.

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          # 0.9.0 release line: 1352 (1351 pass + 1 skip)

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