dsh-notify-windows 0.7.8 → 0.8.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  本项目遵循 [Semantic Versioning](https://semver.org/)。
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.8.0] - 2026-10-14
8
+
9
+ ### 新增
10
+
11
+ - **多语言 toast 文案(`locale` / `messages`)**:新增内置语言包 `zh`(默认,**保持现有文案不变**)、
12
+ `en`、`ru`,并由 `locale` 选择;任意一条文案都可用 `messages` 按 key 覆盖,覆盖优先于语言包。
13
+ 占位符(`{turn}` / `{tool}` / `{reason}` / `{version}` / `{code}`)由事件数据代入:未知占位符
14
+ **原样保留**而非清空,非字符串覆盖值被忽略,未知 `locale` 回退默认语言包——写错不会产生空正文或
15
+ `[object Object]`。默认值不变,故本次改动对既有安装零影响(升级后文案与行为完全一致)。
16
+ - 审批(`approval`)与提问(`ask-user`)路径的日志行补充 `title` 与 `body` 字段,与既有的
17
+ `notify` 行对齐,便于排查「实际发出去的文案是什么」。新增字段为**增量**,不破坏既有日志消费方。
18
+
19
+ ### 测试
20
+
21
+ - `test/probe.test.mjs` 新增 5 个用例(共 20/20 通过):
22
+ - `resolveMessages`:各语言包**键集合一致**、每条文案非空、`{turn}` / `{tool}` / `{reason}` /
23
+ `{version}` / `{code}` 占位符**逐项齐备**(漏一个即变红)、未知 `locale` 回退默认包、
24
+ `messages` 覆盖优先于语言包、非字符串覆盖被忽略、`messages` 非对象不崩;
25
+ - 端到端 `locale=en` / `locale=ru`:完成回合正文与轮次后缀(`Task completed (turn 1)` /
26
+ `Задача завершена (раунд 1)`);
27
+ - 端到端 `messages` 覆盖:未覆盖的键仍用所选语言包;
28
+ - 端到端 approval / ask 的标题与正文,含 `approvalGeneric` 与 `askFallback` 两条兜底文案。
29
+
30
+ ### 修复
31
+
32
+ - `resolveMessages` 的 locale 查找改用 `Object.hasOwn`:`locale` 若恰为 `Object.prototype` 成员名
33
+ (`constructor` / `toString` / `__proto__` / `hasOwnProperty` / `valueOf`),裸索引会取到函数、
34
+ 展开成空包,正文渲染成字面量 `undefined` —— 与「未知 locale 回退默认包」的承诺不符。
35
+ 现在与其它未知 locale 一样回退 `zh`。
36
+
5
37
  ## [0.7.8] - 2026-09-29
6
38
 
7
39
  ### 兼容性
package/README.en.md CHANGED
@@ -72,6 +72,8 @@ in `$DSH_HOME/profiles/web/cordis.patch.yml` (hot-applied by the running host
72
72
  | --- | --- | --- |
73
73
  | `enabled` | `true` | master switch |
74
74
  | `reasons` | `[completed, error, max-tokens]` | turn-end reasons to notify (`aborted` / `interrupted` / `blocked` also available) |
75
+ | `locale` | `zh` | built-in message pack for the toast text: `zh` / `en` / `ru` |
76
+ | `messages` | `{}` | per-key wording overrides applied on top of `locale` (see Localization) |
75
77
  | `includeSubagents` | `false` | also notify subagent sessions |
76
78
  | `notifyOnStart` | `false` | toast when the plugin loads |
77
79
  | `notifyOnApproval` | `true` | toast on `approval/asked` (skipped under policy `never`) |
@@ -87,6 +89,42 @@ in `$DSH_HOME/profiles/web/cordis.patch.yml` (hot-applied by the running host
87
89
  | `preferExisting` | `true` | when true, launcher prefers focusing an open DSH window; when false, just open the URL in the default browser |
88
90
  | `webUrl` | `''` | override the auto-discovered base URL (e.g. `http://192.168.1.5:4000`); leave empty to auto-discover (defaults to `http://127.0.0.1:3080`) |
89
91
 
92
+ ## 🌐 Localization
93
+
94
+ Toast wording comes from a built-in message pack selected by `locale`. The default is `zh`, so an
95
+ existing install sees no change after upgrading; `en` and `ru` ship as complete packs. Any single
96
+ string can be overridden per install through `messages`, which wins over the selected pack:
97
+
98
+ ```yaml
99
+ - id: dsh-notify
100
+ name: dsh-notify-windows
101
+ config:
102
+ locale: ru
103
+ messages:
104
+ approvalTitle: ' · Требуется одобрение'
105
+ turnSuffix: ' — попытка {turn}'
106
+ ```
107
+
108
+ | Key | Used for |
109
+ | --- | --- |
110
+ | `completed` / `error` / `max-tokens` / `aborted` / `interrupted` / `blocked` | the turn/end reason line (looked up by `reason.kind`) |
111
+ | `turnEnded` | fallback when a reason has no entry in the pack |
112
+ | `turnSuffix` | turn number appended to the reason line — `{turn}` |
113
+ | `approvalTitle` | title suffix of an approval toast |
114
+ | `approvalToolWithReason` / `approvalTool` / `approvalGeneric` | approval body — `{tool}`, `{reason}` |
115
+ | `askTitle` | title suffix of a question toast |
116
+ | `askFallback` | question body when no question text could be extracted |
117
+ | `activated` | the `notifyOnStart` toast — `{version}` |
118
+ | `warnSpawnFailed` / `warnToastFailed` / `warnExitCode` / `warnChannelUnavailable` | DSH log wording — `{code}`; the `dsh-notify:` tag itself is never translated, so logs stay greppable across locales |
119
+
120
+ `{placeholder}` tokens are substituted from the event data. An unknown placeholder is left verbatim
121
+ rather than blanked, and a non-string override is ignored — a typo can therefore never produce an
122
+ empty or `[object Object]` toast. An unknown `locale` falls back to the default pack.
123
+
124
+ The `approval` and `ask-user` log entries now carry `title` and `body` alongside the existing
125
+ metadata (matching what the `notify` entry already did), so `%TEMP%\dsh-notify\notify.log` shows
126
+ the text that was actually sent.
127
+
90
128
  ## 🔔 Triggers
91
129
 
92
130
  | Scenario | Session event | Sample toast |
package/README.md CHANGED
@@ -74,6 +74,8 @@ dsh plugin --profile web remove dsh-notify-windows
74
74
  | --- | --- | --- |
75
75
  | `enabled` | `true` | 总开关 |
76
76
  | `reasons` | `[completed, error, max-tokens]` | 需要提醒的回合结束原因(可选 `aborted` / `interrupted` / `blocked`) |
77
+ | `locale` | `zh` | toast 文案的内置语言包:`zh` / `en` / `ru` |
78
+ | `messages` | `{}` | 按 key 覆盖文案,优先级高于 `locale`(见「多语言」) |
77
79
  | `includeSubagents` | `false` | 是否也提醒子代理会话 |
78
80
  | `notifyOnStart` | `false` | 插件加载时发一条「已激活」通知 |
79
81
  | `notifyOnApproval` | `true` | 审批请求提醒(策略 `never` 时自动跳过) |
@@ -89,6 +91,39 @@ dsh plugin --profile web remove dsh-notify-windows
89
91
  | `preferExisting` | `true` | true 走 launcher 优先聚焦已开 DSH 窗口;false 直接用默认浏览器打开 URL |
90
92
  | `webUrl` | `''` | 覆盖自动发现的基地址(如 `http://192.168.1.5:4000`),留空自动发现(默认 `http://127.0.0.1:3080`) |
91
93
 
94
+ ## 🌐 多语言
95
+
96
+ toast 文案取自内置语言包,由 `locale` 选择。默认 `zh`,因此已有安装升级后文案完全不变;
97
+ `en` 与 `ru` 为完整语言包。任何一条文案都可以用 `messages` 按 key 覆盖,覆盖优先于语言包:
98
+
99
+ ```yaml
100
+ - id: dsh-notify
101
+ name: dsh-notify-windows
102
+ config:
103
+ locale: ru
104
+ messages:
105
+ approvalTitle: ' · Требуется одобрение'
106
+ turnSuffix: ' — попытка {turn}'
107
+ ```
108
+
109
+ | key | 用途 |
110
+ | --- | --- |
111
+ | `completed` / `error` / `max-tokens` / `aborted` / `interrupted` / `blocked` | 回合结束原因行(按 `reason.kind` 查表) |
112
+ | `turnEnded` | 原因在语言包中没有对应条目时的兜底文案 |
113
+ | `turnSuffix` | 追加在原因行后的轮次后缀 —— `{turn}` |
114
+ | `approvalTitle` | 审批通知的标题后缀 |
115
+ | `approvalToolWithReason` / `approvalTool` / `approvalGeneric` | 审批正文 —— `{tool}` / `{reason}` |
116
+ | `askTitle` | 提问通知的标题后缀 |
117
+ | `askFallback` | 提取不到问题文本时的提问正文 |
118
+ | `activated` | `notifyOnStart` 的「已激活」通知 —— `{version}` |
119
+ | `warnSpawnFailed` / `warnToastFailed` / `warnExitCode` / `warnChannelUnavailable` | DSH 日志文案 —— `{code}`;`dsh-notify:` 前缀不翻译,保证跨语言可 grep |
120
+
121
+ 说明:`{占位符}` 由事件数据代入;未知占位符**原样保留**(不会被清空),非字符串的覆盖值会被忽略,
122
+ 因此写错不会产生空正文或 `[object Object]`;未知 `locale` 回退默认语言包。
123
+
124
+ 审批 / 提问路径的日志行新增 `title` 与 `body` 字段(与 `notify` 行一致),
125
+ `%TEMP%\dsh-notify\notify.log` 因此能直接看到实际发出的文案。
126
+
92
127
  ## 🔔 触发场景
93
128
 
94
129
  | 场景 | 会话事件 | 通知示例 |
package/lib/index.js CHANGED
@@ -15,6 +15,11 @@
15
15
  // Goal rounds (/goal auto-continuation) are quiet by default: only the final
16
16
  // round that completes or blocks the goal produces a toast.
17
17
  //
18
+ // Wording is localizable: `locale` selects a built-in message pack (MESSAGES —
19
+ // "zh" by default, "en" and "ru" also ship) and `messages` overrides individual
20
+ // strings on top of it. The default pack keeps the original text, so an install
21
+ // that sets neither key behaves exactly as before.
22
+ //
18
23
  // Zero runtime dependencies: notifications go through the toast sender
19
24
  // script (./notify.ps1) executed by Windows PowerShell 5.1, whose .NET
20
25
  // Framework runtime supports the WinRT toast API.
@@ -54,15 +59,128 @@ const WSL_POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershel
54
59
  const resolvePowerShell = () => (existsSync(WSL_POWERSHELL) ? WSL_POWERSHELL : "powershell.exe");
55
60
  const POWERSHELL = resolvePowerShell();
56
61
 
57
- const REASON_TEXT = {
58
- completed: "任务已完成",
59
- error: "任务出错",
60
- "max-tokens": "输出达到 token 上限",
61
- aborted: "任务已取消",
62
- interrupted: "任务中断",
63
- blocked: "任务未开始",
62
+ /**
63
+ * Built-in toast message packs.
64
+ *
65
+ * The keys serve two purposes at once. The turn/end reason kinds (`completed`,
66
+ * `error`, `max-tokens`, `aborted`, `interrupted`, `blocked`) are looked up
67
+ * directly by `reason.kind`, so every pack must carry one entry per reason;
68
+ * the remaining keys hold the surrounding toast and log wording.
69
+ *
70
+ * `zh` is the original wording and stays the default, so an existing install
71
+ * sees no change after upgrading. `en` and `ru` are complete packs. Any pack
72
+ * can be partially overridden per install through the `messages` config.
73
+ * `{placeholder}` tokens are substituted by `format()`.
74
+ */
75
+ export const MESSAGES = {
76
+ zh: {
77
+ // turn/end reason kinds
78
+ completed: "任务已完成",
79
+ error: "任务出错",
80
+ "max-tokens": "输出达到 token 上限",
81
+ aborted: "任务已取消",
82
+ interrupted: "任务中断",
83
+ blocked: "任务未开始",
84
+ // turn/end wording
85
+ turnEnded: "任务已结束",
86
+ turnSuffix: "(第 {turn} 轮)",
87
+ // approval/asked wording
88
+ approvalTitle: " · 需要审批",
89
+ approvalToolWithReason: "工具 {tool}:{reason}",
90
+ approvalTool: "工具 {tool}",
91
+ approvalGeneric: "有一项操作等待你的批准",
92
+ // ask_user_question wording
93
+ askTitle: " · 需要回答",
94
+ askFallback: "Agent 正在等待你的确认",
95
+ // activation toast
96
+ activated: "任务完成提醒已激活(v{version})",
97
+ // logger messages (the "dsh-notify:" tag stays untranslated, so logs
98
+ // remain greppable across locales)
99
+ warnSpawnFailed: "启动通知进程失败",
100
+ warnToastFailed: "通知进程未成功显示 toast",
101
+ warnExitCode: "(退出码 {code})",
102
+ warnChannelUnavailable: "通知通道不可用(powershell.exe 或 notify.ps1 缺失),桌面通知将不会发送",
103
+ },
104
+ en: {
105
+ completed: "Task completed",
106
+ error: "Task errored",
107
+ "max-tokens": "Output reached the token cap",
108
+ aborted: "Task cancelled",
109
+ interrupted: "Task interrupted",
110
+ blocked: "Task not started",
111
+ turnEnded: "Task ended",
112
+ turnSuffix: " (turn {turn})",
113
+ approvalTitle: " · Needs approval",
114
+ approvalToolWithReason: "tool {tool}: {reason}",
115
+ approvalTool: "tool {tool}",
116
+ approvalGeneric: "An operation is waiting for your approval",
117
+ askTitle: " · Needs your answer",
118
+ askFallback: "The agent is waiting for your confirmation",
119
+ activated: "Task-completion reminders activated (v{version})",
120
+ warnSpawnFailed: "failed to start the notification process",
121
+ warnToastFailed: "the notification process did not show a toast",
122
+ warnExitCode: " (exit code {code})",
123
+ warnChannelUnavailable: "notification channel unavailable (powershell.exe or notify.ps1 missing); desktop toasts will not be sent",
124
+ },
125
+ ru: {
126
+ completed: "Задача завершена",
127
+ error: "Ошибка в задаче",
128
+ "max-tokens": "Достигнут лимит токенов",
129
+ aborted: "Задача отменена",
130
+ interrupted: "Задача прервана",
131
+ blocked: "Задача не начата",
132
+ turnEnded: "Задача завершена",
133
+ turnSuffix: " (раунд {turn})",
134
+ approvalTitle: " · Требуется одобрение",
135
+ approvalToolWithReason: "Инструмент {tool}: {reason}",
136
+ approvalTool: "Инструмент {tool}",
137
+ approvalGeneric: "Одна операция ожидает вашего одобрения",
138
+ askTitle: " · Требуется ответ",
139
+ askFallback: "Агент ждёт вашего подтверждения",
140
+ activated: "Напоминания о задачах активированы (v{version})",
141
+ warnSpawnFailed: "не удалось запустить процесс уведомления",
142
+ warnToastFailed: "не удалось показать тост",
143
+ warnExitCode: " (код выхода {code})",
144
+ warnChannelUnavailable: "канал уведомлений недоступен (нет powershell.exe или notify.ps1); тосты отправляться не будут",
145
+ },
64
146
  };
65
147
 
148
+ /**
149
+ * Substitute `{name}` tokens with `params.name`. An unknown token is left
150
+ * verbatim rather than blanked, so a typo in an override stays visible in the
151
+ * toast instead of silently dropping text.
152
+ */
153
+ function format(template, params) {
154
+ if (typeof template !== "string") return "";
155
+ return template.replace(/\{(\w+)\}/g, (match, key) =>
156
+ Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : match,
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Resolve the effective message pack: the selected locale (an unknown locale
162
+ * falls back to the default pack rather than leaving the toast empty), then
163
+ * the install's own `messages` overrides on top.
164
+ *
165
+ * A non-string override is ignored: it could only ever produce "[object
166
+ * Object]" in a toast, and silently dropping it is better than leaking it.
167
+ */
168
+ export function resolveMessages(config = {}) {
169
+ // An unknown locale falls back to the default pack. `Object.hasOwn` (not a
170
+ // bare index) is required: a locale naming an Object.prototype member
171
+ // ("constructor", "toString", "__proto__", …) would otherwise resolve to a
172
+ // function, spread into an empty pack, and render a literal "undefined" body.
173
+ const base = Object.hasOwn(MESSAGES, config.locale) ? MESSAGES[config.locale] : MESSAGES.zh;
174
+ const resolved = { ...base };
175
+ const overrides = config.messages;
176
+ if (overrides && typeof overrides === "object") {
177
+ for (const [key, value] of Object.entries(overrides)) {
178
+ if (typeof value === "string") resolved[key] = value;
179
+ }
180
+ }
181
+ return resolved;
182
+ }
183
+
66
184
  export const name = "dsh-notify-windows";
67
185
 
68
186
  function truncate(text, max) {
@@ -195,6 +313,8 @@ export const apply = (ctx, config = {}) => {
195
313
  const cfg = {
196
314
  enabled: true,
197
315
  reasons: ["completed", "error", "max-tokens"],
316
+ locale: "zh", // built-in pack: "zh" (default) | "en" | "ru"
317
+ messages: {}, // per-key overrides applied on top of the locale pack
198
318
  includeSubagents: false,
199
319
  notifyOnStart: false,
200
320
  notifyOnApproval: true,
@@ -213,6 +333,7 @@ export const apply = (ctx, config = {}) => {
213
333
  ...config,
214
334
  };
215
335
  const reasons = new Set(cfg.reasons);
336
+ const t = resolveMessages(cfg);
216
337
  const logPath = join(tmpdir(), "dsh-notify", "notify.log");
217
338
 
218
339
  const log = (entry) => {
@@ -284,7 +405,7 @@ export const apply = (ctx, config = {}) => {
284
405
  child.stderr?.on("data", (chunk) => { stderr += chunk; });
285
406
  child.on("error", (error) => {
286
407
  log({ event: "error", message: String(error?.message ?? error) });
287
- ctx.logger?.warn?.("dsh-notify: 启动通知进程失败", error);
408
+ ctx.logger?.warn?.("dsh-notify: " + t.warnSpawnFailed, error);
288
409
  });
289
410
  child.on("exit", (code) => {
290
411
  const shown = /toast shown/.test(stdout);
@@ -296,7 +417,7 @@ export const apply = (ctx, config = {}) => {
296
417
  stdout: stdout.slice(0, 500),
297
418
  stderr: stderr.slice(0, 500),
298
419
  });
299
- ctx.logger?.warn?.("dsh-notify: 通知进程未成功显示 toast" + (code !== null ? "(退出码 " + code + ")" : ""));
420
+ ctx.logger?.warn?.("dsh-notify: " + t.warnToastFailed + (code !== null ? format(t.warnExitCode, { code }) : ""));
300
421
  }
301
422
  });
302
423
  };
@@ -354,8 +475,8 @@ export const apply = (ctx, config = {}) => {
354
475
  }
355
476
 
356
477
  const title = titleFor(session) || cfg.appName;
357
- let body = REASON_TEXT[reason.kind] ?? "任务已结束";
358
- if (Number.isInteger(event.data.turn)) body += "(第 " + event.data.turn + " 轮)";
478
+ let body = t[reason.kind] ?? t.turnEnded;
479
+ if (Number.isInteger(event.data.turn)) body += format(t.turnSuffix, { turn: event.data.turn });
359
480
  if (cfg.excerpt && window.lastAssistantText) {
360
481
  body += "\n" + truncate(window.lastAssistantText, cfg.excerptMaxChars);
361
482
  }
@@ -380,10 +501,11 @@ export const apply = (ctx, config = {}) => {
380
501
  const data = event.data ?? {};
381
502
  const body = typeof data.toolName === "string"
382
503
  ? (typeof data.reason === "string" && data.reason.length > 0
383
- ? "工具 " + data.toolName + ":" + truncate(data.reason, 100)
384
- : "工具 " + data.toolName)
385
- : "有一项操作等待你的批准";
386
- sendToast(cfg.appName + " · 需要审批", body, { sessionId: session?.id });
504
+ ? format(t.approvalToolWithReason, { tool: data.toolName, reason: truncate(data.reason, 100) })
505
+ : format(t.approvalTool, { tool: data.toolName }))
506
+ : t.approvalGeneric;
507
+ const title = cfg.appName + t.approvalTitle;
508
+ sendToast(title, body, { sessionId: session?.id });
387
509
  const url = cfg.openOnClick && session?.id ? conversationUrl(session?.id) : undefined;
388
510
  log({
389
511
  event: "approval",
@@ -391,6 +513,8 @@ export const apply = (ctx, config = {}) => {
391
513
  id: data.id,
392
514
  toolName: data.toolName,
393
515
  reason: data.reason,
516
+ title,
517
+ body,
394
518
  ...(url ? { url } : {}),
395
519
  });
396
520
  return;
@@ -405,12 +529,16 @@ export const apply = (ctx, config = {}) => {
405
529
  question = extractQuestionFromCode(data.arguments);
406
530
  }
407
531
  if (question !== undefined || data.name === "ask_user_question") {
408
- sendToast(cfg.appName + " · 需要回答", question ? truncate(question, 100) : "Agent 正在等待你的确认", { sessionId: session?.id });
532
+ const body = question ? truncate(question, 100) : t.askFallback;
533
+ const title = cfg.appName + t.askTitle;
534
+ sendToast(title, body, { sessionId: session?.id });
409
535
  const url = cfg.openOnClick && session?.id ? conversationUrl(session?.id) : undefined;
410
536
  log({
411
537
  event: "ask-user",
412
538
  sessionId: session?.id,
413
539
  question: question ?? null,
540
+ title,
541
+ body,
414
542
  ...(url ? { url } : {}),
415
543
  });
416
544
  }
@@ -433,10 +561,10 @@ export const apply = (ctx, config = {}) => {
433
561
  sessionId: null,
434
562
  });
435
563
  if (cfg.enabled && !channelOk) {
436
- ctx.logger?.warn?.("dsh-notify: 通知通道不可用(powershell.exe 或 notify.ps1 缺失),桌面通知将不会发送");
564
+ ctx.logger?.warn?.("dsh-notify: " + t.warnChannelUnavailable);
437
565
  }
438
566
  if (cfg.enabled && cfg.notifyOnStart) {
439
567
  // Activation toast has no session: leave non-clickable (legacy behavior).
440
- sendToast(cfg.appName, "任务完成提醒已激活(v" + VERSION + ")");
568
+ sendToast(cfg.appName, format(t.activated, { version: VERSION }));
441
569
  }
442
570
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-notify-windows",
3
- "version": "0.7.8",
4
- "description": "DeepSeek Harness (DSH) 插件:任务完成 / 等待审批 / 等待回答时发送 Windows 系统桌面通知",
3
+ "version": "0.8.0",
4
+ "description": "DeepSeek Harness (DSH) \u63d2\u4ef6\uff1a\u4efb\u52a1\u5b8c\u6210 / \u7b49\u5f85\u5ba1\u6279 / \u7b49\u5f85\u56de\u7b54\u65f6\u53d1\u9001 Windows \u7cfb\u7edf\u684c\u9762\u901a\u77e5",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/SeverusZh/dsh-notify-windows.git"