dsh-messager 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.en.md +177 -45
  2. package/README.md +161 -147
  3. package/cordis.patch.yml +13 -0
  4. package/lib/channels/dingtalk.d.ts +38 -0
  5. package/lib/channels/dingtalk.d.ts.map +1 -0
  6. package/lib/channels/dingtalk.js +60 -0
  7. package/lib/channels/dingtalk.js.map +1 -0
  8. package/lib/channels/discord.d.ts +28 -0
  9. package/lib/channels/discord.d.ts.map +1 -0
  10. package/lib/channels/discord.js +44 -0
  11. package/lib/channels/discord.js.map +1 -0
  12. package/lib/channels/telegram.d.ts +31 -0
  13. package/lib/channels/telegram.d.ts.map +1 -0
  14. package/lib/channels/telegram.js +52 -0
  15. package/lib/channels/telegram.js.map +1 -0
  16. package/lib/channels/wecom.d.ts +34 -0
  17. package/lib/channels/wecom.d.ts.map +1 -0
  18. package/lib/channels/wecom.js +53 -0
  19. package/lib/channels/wecom.js.map +1 -0
  20. package/lib/client.js +391 -54
  21. package/lib/client.js.map +1 -1
  22. package/lib/config.d.ts +46 -0
  23. package/lib/config.d.ts.map +1 -1
  24. package/lib/config.js +27 -0
  25. package/lib/config.js.map +1 -1
  26. package/lib/index.d.ts +2 -2
  27. package/lib/index.d.ts.map +1 -1
  28. package/lib/index.js +33 -2
  29. package/lib/index.js.map +1 -1
  30. package/lib/notify.d.ts +1 -1
  31. package/lib/notify.d.ts.map +1 -1
  32. package/lib/notify.js +10 -6
  33. package/lib/notify.js.map +1 -1
  34. package/lib/types/client/card-controller.d.ts.map +1 -1
  35. package/lib/types/client/locales.d.ts +48 -2
  36. package/lib/types/client/locales.d.ts.map +1 -1
  37. package/lib/types/client/settings-form.d.ts.map +1 -1
  38. package/lib/types/config.d.ts +46 -0
  39. package/lib/types/config.d.ts.map +1 -1
  40. package/package.json +121 -31
package/README.en.md CHANGED
@@ -4,82 +4,214 @@
4
4
  [![npm version](https://img.shields.io/npm/v/dsh-messager)](https://www.npmjs.com/package/dsh-messager)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D20-blue.svg)](package.json)
6
6
 
7
- > **Task-status notification plugin for DeepSeek Harness (DSH).** Get notified via **system notifications** (OS toast), **browser notifications**, and a **Feishu (Lark) bot** (webhook) whenever a session needs attention, a task completes, or a task errors.
7
+ > **Task-status notification plugin for DeepSeek Harness (DSH).** Get notified via **system notifications** (OS toast), **browser notifications**, and **Feishu / WeCom / Discord / DingTalk / Telegram** whenever a session needs attention, a task completes, or a task errors — no more staring at the status dots in the session list.
8
8
 
9
- A single-package, dual-runtime design: the **host side** (Node) handles system notifications and the Feishu webhook; the **client side** (browser) handles Web Notification. Both share one config source (namespace `messager`), editable live from the "Notifications" settings section — config flows through the plugin's own webserver route (`/dsh-messager/config`), so it works on all environments including release/npx/npm installs (no DSH settings allowlist dependency).
9
+ A single-package, dual-runtime design: the **host side** (Node server) handles system notifications and all third-party channels; the **client side** (browser) handles Web Notification. Both share one config source (settings namespace `messager`), editable live from the "Messenger" settings section — config flows through the plugin's own webserver route (`/dsh-messager/config`), so it works on all environments including release/npx/npm installs (no DSH settings allowlist dependency).
10
10
 
11
- ## Features
11
+ ## Feature overview
12
12
 
13
- - 🟢 Three triggers — needs-interaction / completed / errored, each toggleable
14
- - 🔔 Three channels — system (node-notifier), browser (Notification API), Feishu bot (webhook + HMAC-SHA256 signature)
15
- - ⚙️ Hot-reloadable config applies instantly, no restart
16
- - 🌐 Internationalized settings section (简体中文 / English) via `ctx.locale`
17
- - ⌨️ Browser shortcut `Ctrl+Shift+M` opens the settings panel (no apiproxy allowlist needed)
18
- - 🔌 Extensible channels via the `NotifyChannel` interface
13
+ | Need | Implementation |
14
+ | --- | --- |
15
+ | Triggers | Needs-interaction (approval `approval/asked`, question/plan-review `ask_user_question`, client `pendingInteraction`), task completed (`agent/status` running→idle, root sessions only + `turn/end` reason), task errored (`agent/error`) |
16
+ | Channels | System (node-notifier toast), browser (Notification API), Feishu (interactive card + HMAC-SHA256 signature), WeCom (markdown + optional signing), Discord (embed card), DingTalk (actionCard + optional signing), Telegram (Bot API HTML message); extensible via the `NotifyChannel` interface |
17
+ | Configurable | Trigger toggles, per-channel enable/verbosity/icon, dedup cooldowns, title prefix, etc. — see [Configuration](#configuration) |
19
18
 
20
- Trigger semantics align with the Web UI status dots: orange = needs interaction, green = completed (non-current session), blue = running (not notified).
19
+ Trigger semantics align with the Web UI status dots: **orange = needs interaction** (`pendingInteraction`), **green = task completed** (`running→idle` and not the current session), **blue = running** (not notified).
21
20
 
22
21
  ## Installation
23
22
 
24
- Install from npm (recommended, prebuilt `lib/` no build approval needed):
23
+ > 📖 A full step-by-step guide for **end users** is at [doc/用户安装指南.md](doc/用户安装指南.md) (Chinese)covering **from source** (clone + local build) and **pnpm** (local checkout / tarball / git / npm) installs.
24
+
25
+ **One step for a formal install** (host + browser client both take effect; then just start with `dsh web` — **no** `--patch` needed):
25
26
 
26
27
  ```sh
27
- dsh plugin --profile web add dsh-messager
28
- dsh web
28
+ # Build inside the plugin repo
29
+ pnpm install
30
+ pnpm build
31
+
32
+ dsh plugin --profile web add <plugin-path>
33
+ dsh web # or dsh --profile web
29
34
  ```
30
35
 
31
- If you don't have the `dsh` CLI preinstalled, run it on the fly via npx:
36
+ > `pnpm install` and `pnpm build` run inside your plugin checkout; replace `<plugin-path>` with that directory (absolute or relative both work).
32
37
 
33
- ```sh
34
- npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-messager
35
- npx -p @deepseek-ai/dsh dsh web
36
- ```
38
+ > - `--patch` is **not** an install step — it is an optional **development** tool (see "Local development" below): it loads only the host side, writes nothing to the profile, and applies to the current launch only. After installing the bundle, **do not** start the same plugin with `--patch` simultaneously (the host side loads twice and the settings namespace registration conflicts).
39
+ > - When running DSH **from source** (from the deepseek-harness repo root), replace `dsh` with `pnpm dsh` — identical behavior: `pnpm dsh plugin --profile web add …`, `pnpm dsh web`. The profile directory stays `$DSH_HOME/profiles/web` (`dsh web` is the `--profile web` alias).
40
+ > - Installing from git with pnpm ≥ 10 requires approving build scripts: add the package key pnpm prompts for to the profile's `pnpm-workspace.yaml` `allowBuilds` (see the official DSH publish tutorial).
37
41
 
38
- From source (for development / customizing):
42
+ ### Settings section "Messenger" (available everywhere)
39
43
 
40
- ```sh
41
- git clone https://github.com/ly6170/dsh-messager.git
42
- cd dsh-messager
43
- pnpm install
44
- pnpm build
45
- npx -p @deepseek-ai/dsh dsh plugin --profile web add ./
46
- npx -p @deepseek-ai/dsh dsh web
47
- ```
44
+ After installation, a **「通知&信使」** (Messenger) section appears in the DSH settings sidebar (below "Agent presets"; its position is computed dynamically from existing sections, not hard-coded). It hosts the full config form, read/written through the plugin's own webserver route (`/dsh-messager/config`, same-origin check + redacted view) straight to the host `settings` service — **no dependency on the DSH settings allowlist; works out of the box on release (npx) installs**, no patches needed.
45
+
46
+ > The config is the same source as `settings.yaml` (same namespace): any change applies instantly everywhere.
47
+
48
+ ## Local development
49
+
50
+ - **Host side (quick)**: from the DSH repo root run
51
+ `pnpm dsh web --patch <plugin-path>/cordis.yml` — loads the TS source directly (HMR works). In source mode the host runs via tsx, so no build is needed to load that path.
52
+ - **Full dual-runtime**: the browser (client) side requires the plugin to enter the Loader as a package for clientModules to scan it into the Web bundle (`--patch` file-path entries are not scanned), so do a full install into the profile:
53
+ ```sh
54
+ dsh plugin --profile web add <plugin-path> # source mode: pnpm dsh plugin ...
55
+ pnpm dsh web # run from the DSH source repo
56
+ ```
57
+ **Restart** `pnpm dsh web` after `plugin add` (clientModules scans at startup; a running instance does not hot-add new bundles). After changing client code, re-run `pnpm run build:client` in your own repo and refresh the page (the bundle carries a rev hash so it is re-fetched; the DSH repo's `dev:web` watcher only watches in-workspace client plugins, not external ones).
58
+
59
+ Browser notifications require user permission: the plugin requests it once on load when the permission is `default`; if denied, the browser channel degrades silently (other channels are unaffected) — re-authorize in the browser's site settings.
48
60
 
49
61
  ## Configuration
50
62
 
51
- Precedence: **schema defaults → base (that row's `config:`) → user layer (settings section/document/RPC)**. Any change applies instantly.
63
+ Precedence: **schema defaults → base (that row's `config:`) → user layer (Web settings section)**. The host registers the Loader config as the `messager` namespace's base, therefore:
64
+
65
+ - How to write base differs by usage: for dev debugging write `config:` on the row in `cordis.yml` (patch overlay); for formal installs override the row by `id: messager` in the **profile's `cordis.patch.yml`**, or edit the `cordis.patch.yml` inside the bundle package;
66
+ - The user layer has three entry points, **same source, no conflicts, any change applies instantly** (host `watch` rebuilds channels; client refetches on `settings/document-updated`):
67
+ 1. **Settings section**: Settings → "Messenger" (full field form, available in all environments);
68
+ 2. **Settings document**: edit the `messager:` block of `$DSH_HOME/settings.yaml` directly (all fields, including dedup throttling not shown in the form);
69
+ 3. **RPC**: settings.describe / settings.mutate (host side; the Web allowlist does not affect this plugin's section, since the section uses the plugin's own config route).
70
+
71
+ > Config read/write path: settings section → `GET/POST /dsh-messager/config` (webserver route, same-origin check) → host `settings` service (describe redacted view / mutate per-field ops) → settings.yaml. After a write, the `settings/document-updated` event (built-in DSH forwarding) drives the frontend refresh.
72
+ >
73
+ > 🌐 **i18n**: the section menu and form follow the DSH display language (简体中文 / English); dictionaries are registered in `ctx.locale` (zh/en key sets identical; missing keys fail loud by showing the key name).
52
74
 
53
75
  | Field | Type | Default | Description |
54
76
  | --- | --- | --- | --- |
55
- | `triggers.interaction` | boolean | `true` | Notify on needs-interaction |
56
- | `triggers.completed` | boolean | `true` | Notify on task completed |
57
- | `triggers.error` | boolean | `true` | Notify on task error |
77
+ | `triggers.interaction` | boolean | `true` | Notify when interaction is needed (approval/question/plan-review) |
78
+ | `triggers.completed` | boolean | `true` | Notify when a task completes |
79
+ | `triggers.error` | boolean | `true` | Notify when a task errors |
58
80
  | `system.enabled` | boolean | `true` | System notification channel |
59
- | `system.icon` | string | - | Absolute icon path (must exist) |
81
+ | `system.icon` | string | - | Absolute icon path (node-notifier needs a file path, **and the file must exist**) |
60
82
  | `system.verbosity` | `minimal\|normal\|detailed` | `normal` | System content verbosity |
61
83
  | `browser.enabled` | boolean | `true` | Browser notification channel |
62
- | `browser.onlyWhenHidden` | boolean | `true` | Notify only when page hidden/unfocused |
84
+ | `browser.icon` | string | - | Icon URL or data URL |
85
+ | `browser.onlyWhenHidden` | boolean | `true` | Notify only when the page is hidden/unfocused |
63
86
  | `browser.verbosity` | `minimal\|normal\|detailed` | `normal` | Browser content verbosity |
64
87
  | `feishu.enabled` | boolean | `false` | Feishu bot (webhook) channel |
65
88
  | `feishu.webhookUrl` | string | - | Custom bot webhook URL |
66
- | `feishu.secret` | string (secret) | - | Signing secret |
89
+ | `feishu.secret` | string (secret) | - | Signing secret (bot "security settings - signature") |
67
90
  | `feishu.timeoutMs` | number | `5000` | Per-request timeout |
68
91
  | `feishu.verbosity` | `minimal\|normal\|detailed` | `normal` | Card content verbosity |
69
- | `dedup.interactionCooldownMs` | number | `10000` | Cooldown for same session/trigger |
70
- | `dedup.completedDebounceMs` | number | `1000` | Completion debounce |
71
- | `dedup.perChannelPerMinute` | number | `20` | Per-channel per-minute cap |
72
- | `message.titlePrefix` | string | - | Title prefix |
73
- | `message.includeSessionTitle` | boolean | `true` | Include session title |
74
- | `message.guiUrl` | string | `http://127.0.0.1:3080` | "Open" link target |
92
+ | `wecom.enabled` | boolean | `false` | WeCom group bot (webhook) channel |
93
+ | `wecom.webhookUrl` | string | - | Group bot webhook URL (with `?key=`) |
94
+ | `wecom.secret` | string (secret) | - | Signing secret ("security settings - signing", HMAC-SHA256, no URL encoding) |
95
+ | `wecom.timeoutMs` | number | `5000` | Per-request timeout |
96
+ | `wecom.verbosity` | `minimal\|normal\|detailed` | `normal` | Message content verbosity |
97
+ | `discord.enabled` | boolean | `false` | Discord channel (webhook) |
98
+ | `discord.webhookUrl` | string | - | Discord webhook URL (`.../api/webhooks/<id>/<token>`) |
99
+ | `discord.timeoutMs` | number | `5000` | Per-request timeout |
100
+ | `discord.verbosity` | `minimal\|normal\|detailed` | `normal` | Embed content verbosity |
101
+ | `dingtalk.enabled` | boolean | `false` | DingTalk custom bot (webhook) channel |
102
+ | `dingtalk.webhookUrl` | string | - | Custom bot webhook URL (with `?access_token=`) |
103
+ | `dingtalk.secret` | string (secret) | - | Signing secret ("security settings - signing", HMAC-SHA256 + URL encoding) |
104
+ | `dingtalk.timeoutMs` | number | `5000` | Per-request timeout |
105
+ | `dingtalk.verbosity` | `minimal\|normal\|detailed` | `normal` | Card content verbosity |
106
+ | `telegram.enabled` | boolean | `false` | Telegram channel (Bot API) |
107
+ | `telegram.botToken` | string (secret) | - | Bot token (from @BotFather) |
108
+ | `telegram.chatId` | string | - | Target chat_id (numeric ID or `@channel-username`) |
109
+ | `telegram.timeoutMs` | number | `5000` | Per-request timeout |
110
+ | `telegram.verbosity` | `minimal\|normal\|detailed` | `normal` | Message content verbosity |
111
+ | `dedup.interactionCooldownMs` | number | `10000` | Cooldown for same session/trigger (also the cross-tab dedup window) |
112
+ | `dedup.completedDebounceMs` | number | `1000` | Completion debounce (waits for the turn/end reason, merges boundary jitter) |
113
+ | `dedup.perChannelPerMinute` | number | `20` | Per-channel per-minute cap (protects against third-party rate limits/spam) |
114
+ | `message.titlePrefix` | string | - | Title prefix, e.g. `[DSH]` |
115
+ | `message.includeSessionTitle` | boolean | `true` | Include session title in the body |
116
+ | `message.guiUrl` | string | `http://127.0.0.1:3080` | "Open" link/button target |
117
+
118
+ Verbosity: `minimal` = title only; `normal` adds session title/tool name/end reason/error summary; `detailed` adds turn/step, approval reason and the GUI link.
119
+
120
+ ## Trigger signals (event → notification mapping)
121
+
122
+ | Trigger | Host side (system/feishu/wecom/discord/dingtalk/telegram) | Client side (browser) |
123
+ | --- | --- | --- |
124
+ | Approval | `session/event` `approval/asked` | summary `pendingInteraction==='approval'` appears |
125
+ | Question / plan-review | `session/event` `tool/call` (`ask_user_question`) | `pendingInteraction==='question'/'plan-review'` appears |
126
+ | Task completed | `agent/status` running→idle (root sessions only) + `turn/end` reason | summary `running:true→false` and not the current session |
127
+ | Task errored | `agent/error` | - (covered by the host side) |
128
+
129
+ ## Channel extension
130
+
131
+ Add a third-party channel (DingTalk / WeCom / Telegram…) by implementing the `NotifyChannel` interface and registering it in `buildChannels()` in `src/index.ts`:
132
+
133
+ ```ts
134
+ export interface NotifyChannel {
135
+ readonly id: string
136
+ send(payload: NotificationPayload): Promise<void>
137
+ }
138
+ ```
139
+
140
+ ## Project structure
141
+
142
+ ```
143
+ dsh-messager/
144
+ ├── package.json # dsh.bundle + dsh.client dual declarations; exports["./client"]
145
+ ├── tsconfig.json # host side (Node)
146
+ ├── tsconfig.client.json # client declaration output (lib/types/client)
147
+ ├── tsdown.config.ts # client bundle (__ModuleLoader__.load contract)
148
+ ├── cordis.yml # local development overlay (host side)
149
+ ├── cordis.patch.yml # bundle config layer (applies after install)
150
+ ├── assets/icon.png # default notification icon
151
+ ├── src/
152
+ │ ├── index.ts # host apply: event wiring + settings registration + channel building + route mounting
153
+ │ ├── config.ts # Config schema (shared by Loader config and settings)
154
+ │ ├── config-shared.ts # cross-end shared types for the config route (host/client)
155
+ │ ├── config-route.ts # webserver config route (GET view / POST ops, same-origin check)
156
+ │ ├── signals.ts # event → Signal extraction (pure functions)
157
+ │ ├── notify.ts # dispatch: filter/cooldown/debounce/rate-limit + NotifyChannel interface
158
+ │ ├── templates.ts # verbosity template rendering (pure functions)
159
+ │ ├── settings.ts # settings namespace registration (base = Loader config)
160
+ │ ├── channels/ # system (node-notifier), feishu/wecom/discord/dingtalk/telegram (webhook/Bot API + signing)
161
+ │ └── client/ # browser side: sessions diff, Notification, settings section, config sync
162
+ │ ├── index.ts # section registration (dynamic order) + browser notifications + config route accessor
163
+ │ ├── section.tsx # "Messenger" settings section component
164
+ │ ├── settings-form.tsx # shared form body (groups + FieldRow + action bar)
165
+ │ ├── card-controller.ts # form controller (pure logic, unit-testable)
166
+ │ ├── fetch-scope.ts # fetch adapter for ScopeLike (config route)
167
+ │ ├── locales.ts # zh/en dictionaries (ctx.locale registration)
168
+ │ ├── config.ts # browser-notification config handle (via the config route)
169
+ │ └── diff.ts # session summary diff (pure functions)
170
+ └── tests/ # vitest unit tests (126)
171
+ ```
172
+
173
+ ## Testing
174
+
175
+ ```sh
176
+ pnpm test # 126 unit tests: signal extraction/templates/dispatch/channel payloads & signatures/config parsing/client diff/config route/fetch scope/locale consistency/form gating
177
+ pnpm typecheck # host side
178
+ pnpm build # host tsc + client declarations + client bundle (lib/)
179
+ ```
180
+
181
+ ## Version compatibility (DSH 0.1.1-rc.2+ / APIProxy → @Remote)
182
+
183
+ - Starting with **v0.2.1**, all `@deepseek-ai/dsh-*` peerDependencies are upgraded from `0.1.0-rc.6` to **`0.1.1-rc.2`** (the current npm release line), matching DSH's migration away from the legacy APIProxy surface toward the unified **@Remote gateway**.
184
+ - Compatibility notes:
185
+ - **No source changes were required in the plugin**: client-side event subscription already uses `ctx.remote.$on` (@Remote/Typert), and config reads/writes go through the plugin's own webserver route `/dsh-messager/config` — neither depends on the old APIProxy surface.
186
+ - `peerDependencies` now includes all type-surface peers required by `dsh-api-remotes@0.1.1-rc.2` (api-gateway / credentials / llm / commands / typert-registry, etc.), so client-side Typert declaration merging stays complete (forwarded events such as `settings/document-updated` can be subscribed with full typings).
187
+ - Newer DSH Web settings has a namespace allowlist (`WEB_SETTINGS_NAMESPACES`): for the native DSH settings page to read the `messager` namespace, add `messager` to that allowlist in DSH source if needed — the plugin's own settings route is not restricted by it.
75
188
 
76
189
  ## Known limitations
77
190
 
78
- - Browser notifications require site permission; with `onlyWhenHidden=false` they also pop when visible.
79
- - Multi-tab dedup uses localStorage cooldown; each browser notifies separately.
80
- - Subagent completions are not notified (root sessions only) forked sessions count as top-level and do notify.
81
- - Channel failures are only logged; they don't affect other channels or the plugin.
82
- - Completion/interaction dedup state is in-memory and resets after a DSH restart.
191
+ - Browser notifications require site permission; with `onlyWhenHidden=false` they also pop while visible.
192
+ - Multi-tab dedup uses a localStorage cooldown; different browsers notify independently.
193
+ - Subagent completions are not notified (root sessions only), to avoid noise.
194
+ - Channel failures (webhook timeouts, unavailable toasts) are only logged; they don't affect other channels or the plugin.
195
+ - Completion/interaction dedup state is in-memory and resets on DSH restart (acceptable).
196
+
197
+ ### System notification (node-notifier) cross-platform prerequisites
198
+
199
+ `node-notifier` invokes completely different underlying programs on the three platforms:
200
+
201
+ | Platform | Backend | Prerequisites / differences |
202
+ | --- | --- | --- |
203
+ | Windows | PowerShell ToastNotification | Built in, nothing to install; `sound` maps reliably only on Windows |
204
+ | macOS | terminal-notifier | First use **downloads** a third-party binary, and a logged-in GUI session (Dock present) is required; `sound` has no effect |
205
+ | Linux | notify-send (libnotify) | Requires `libnotify-bin` and a **running notification daemon** (GNOME Shell / Plasma / mako / dunst etc.); `sound` has no effect |
206
+
207
+ - **Icon**: `system.icon` must be an **existing file path**. Windows usually degrades silently on a missing path, but Linux/macOS may fail outright — the channel layer validates existence and falls back to no icon.
208
+ - **Environment differences are not plugin bugs**: on Linux without a notification daemon, or macOS unable to download terminal-notifier / not in a GUI session, notifications may not pop or fail silently — check those prerequisites first, not the plugin; on failure the dispatcher logs the concrete error via `logWarn`.
209
+
210
+ ## Roadmap
211
+
212
+ - Third-party channel extension: email
213
+ - Trigger extension: background job completion, goal round completion
214
+ - Notification history, per-session mute, do-not-disturb windows
83
215
 
84
216
  ## License
85
217