# dsh-message-push English | [中文](README.md) A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Cordis plugin: **every non-subagent session that stops pushes a message to the configured messaging platforms** (QQ / Telegram / Feishu / WeChat), so nobody has to watch the screen. ## What triggers a push | Situation | Trigger | Text | |---|---|---| | Task completed | `turn/end(completed)` + `agent/status(idle)` | ✅ Task completed | | Task interrupted | `turn/end(aborted)` | ⏸ Task interrupted | | Task blocked | `turn/end(blocked)` | 🚫 Task blocked | | Task failed | `turn/end(error)` | ❌ Task failed (with the error) | | Token limit | `turn/end(max-tokens)` | ↯ Reached token limit | | Approval needed | `approval/request` | ⚠️ Approval needed | | Answer needed | `user-questions/request` | ❓ Your answer is needed | Each push carries the session name (title / directory plus the session id tail), a preview of the last reply, and an “open the Web UI” link. This plugin is the **push layer**: it does not decide approvals, it does not accept replies as decisions, and it never injects inbound messages into an agent session. Decisions stay in the Web UI; the push only calls you back. Other plugins can call `ctx.messagePush.notify({...})` to reuse the same channels and de-duplication. ## Install From GitHub (pinning a tag is steadier): ```sh dsh plugin --profile web add github:DNAlec/dsh-message-push ``` From a local checkout (development): ```sh git clone https://github.com/DNAlec/dsh-message-push.git dsh plugin --profile web add "$PWD/dsh-message-push" ``` `pnpm` is required. **Restart `dsh web`** afterwards — profile composition resolves at startup, and the “Message push” settings page appears only then. Optional dependencies (QQ QR provisioning / the Feishu SDK) are declared in the package and installed by pnpm; a missing one only degrades that channel. From npm (after a tagged release): ```sh dsh plugin --profile web add @dnalec/dsh-message-push ``` Uninstall with `dsh plugin --profile web remove @dnalec/dsh-message-push`. Configuration and credentials under `~/.dsh/message-push/` are not deleted. ## Configure Settings → **Message push**. 1. **Connect at least one channel** (enabled + bound target + connected): - **QQ** — “Scan to create bot” with mobile QQ, or paste AppID / AppSecret. Scanning also binds the scanner as the push target. - **Telegram** — the @BotFather bot token; saving starts long polling. - **Feishu / Lark** — open-platform App ID / Secret (needs the optional `@larksuiteoapi/node-sdk`). - **WeChat** — “Scan to log in” (official iLink, DMs only; a dedicated account is recommended). 2. **Bind a push target**: DM the bot, then click that chat under “Recent incoming”; or, while unbound, reply **`是`** / `yes` / `ok` in a DM and that chat becomes the target. > This is a **private bot** design: whoever confirms first owns the push target. Never expose the bot to strangers. QQ groups must @ the bot, and the speaker’s `userId` must be set. 3. **Send test push** — receiving it means the channel works. 4. Adjust **Notification rules**: master switch, `webUrl`, preview length, coalesce window, per-reason toggles, and “push subagent sessions too” (off by default). ## Triggering and de-duplication - **Top-level sessions only** by default: sessions with `header.origin === 'subagent'` or `delegationDepth > 0` are skipped. - **One push per turn**: `turn/end` and `agent/status(idle)` arrive in no guaranteed order. The first one holds the notification until `turn/end` supplies the reason (at most `reasonUnknownDelayMs`, default 8s; on timeout it pushes “Session stopped”). - **Approval / answer requests win**: they are more specific, push immediately, and a following `idle` folds into the same message (“N more pending in this window”) instead of adding a “task completed” notice. - **Coalesce window**: same-session, same-kind events within `repeatWindowSecs` (default 60s) merge, re-notifying only when the count changes. - **A failed push never breaks the host**: with no usable channel it writes `PUSH_SKIP` / `PUSH_FAIL` to the audit log and warns at most once per 60s. ## Service API (for other plugins) The plugin publishes its service with `ctx.provide('messagePush', …)` on the host plane: ```js const push = ctx.get('messagePush') if (push) { push.notify({ kind: 'needs-approval', sessionId, title: 'Fix push plugin', body: 'Approve deleting 3 files' }) } ``` | Method | Purpose | |---|---| | `broadcast(text, opts?)` | Send to every enabled, bound, connected channel → `{ sent, ok, failed, skipped }` | | `send(channelId, chatId, text, opts?)` | Send to one channel/target | | `notify(event)` | Push through the watcher pipeline (de-duplication + coalescing) | | `onInbound(fn)` | Observe inbound text (read-only); return `true` to claim and stop host handling | | `channels()` | Currently pushable channel ids | | `state` | Last push time / result / error | A corrupt config file makes the plugin run on in-memory defaults and **never overwrite the disk**; the settings page offers an explicit overwrite. ## Data Everything lives in `$DSH_HOME/message-push/` (default `~/.dsh/message-push/`, mode `0600`). Never commit it. | File | Contents | |---|---| | `config.json` | Channel switches, push targets, notification rules | | `secrets.json` | QQ AppID/Secret, Telegram token, Feishu AppID/Secret | | `wechat.json` | WeChat iLink login state | | `audit.log` | `PUSH` / `PUSH_FAIL` / `PUSH_SKIP` / `BIND` / `INBOX` / `CONFIG` / `WARN` | No audit line means this plugin did not push anything. ## Development ```sh npm test # node --test tests/*.test.mjs npm run check # node --check on every source file ``` Plain JS, zero `@deepseek-ai/*` imports: it uses only the cordis injection name `timer` plus the runtime's `fetch` / `WebSocket`. Read [AGENTS.md](AGENTS.md) before changing code. ## License [MIT](LICENSE)