dsh-plugin-notify-sounds 1.6.0 → 1.6.2

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/README.md CHANGED
@@ -19,6 +19,10 @@ dsh-plugin-notify-sounds
19
19
 
20
20
  ---
21
21
 
22
+ ## Consistent with DSH's own UI
23
+
24
+ Every visual detail of the settings section comes from DSH's own design system: row geometry, dividers, type sizes and spacing use the official design tokens, the sound picker is the official `Menu` primitive, the icons come from the official family, and the layout follows the native text-left, control-right row shape. Installing it does not introduce a visually disjoint third-party block into your settings.
25
+
22
26
  ## What it does
23
27
 
24
28
  | What | What you get |
@@ -40,11 +44,9 @@ This plugin turns "it is waiting for you" and "it is done" into sound. With syst
40
44
 
41
45
  ## Sounds
42
46
 
43
- Nine Web Audio synthesized sounds — no audio files, no network requests:
44
-
45
- Bell · Chime · Ding · Drop · Pulse · Arc · Coin · Success · Knock
47
+ Bell · Chime · Ding · Drop · Pulse · Arc · Coin · Success · Knock — nine built-in sounds, all synthesized live with Web Audio: no audio files, no network requests.
46
48
 
47
- Or upload your own ringtone (mp3/wav/ogg, ≤512KB, validated and volume-normalized automatically), or pick "None" for notifications only. Each trigger (approval / question / completion) can use a different sound, with preview right in the settings.
49
+ Or upload your own ringtone (mp3/wav/ogg, ≤512KB, validated and volume-normalized automatically), or pick "None" for notifications only. Each trigger (approval / question / completion) gets its own sound, with preview right in the settings.
48
50
 
49
51
  ## Settings
50
52
 
@@ -72,7 +74,7 @@ The "install source" dropdown above the box picks the download mirror (mainland
72
74
 
73
75
  ### Upgrading
74
76
 
75
- **There is no auto-update**: to upgrade, **uninstall** the old version from the same place first, then install the new one as above.
77
+ DSH does not update plugins on its own, but this plugin **updates itself**: Settings → Sounds → Updates can check for a new version and install it in one click. Uninstalling and installing again also works.
76
78
 
77
79
  ### Other ways
78
80
 
@@ -121,6 +123,8 @@ For anyone hacking on the code (exact details live in the source comments):
121
123
 
122
124
  ## Changelog
123
125
 
126
+ - **v1.6.2** (2026-10-11) Fixed one-click update always failing when the mainland mirror answered the check: the mirror serves its tarballs from its CDN, and the downloader refused every redirect — so the download could never finish (found by the live drill). Redirects are now followed one hop at a time with every hop checked against the trusted hosts (the mirror's CDN is the one legitimate hop); the sha512 check is unchanged. A failure now names the stage it died at (check / download / verify / unpack / replace).
127
+ - **v1.6.1** (2026-10-11) The README is rebuilt as a release page: a new **Consistent with DSH's own UI** section on how the UI matches DSH's own settings, copy polished throughout, and the upgrade notes corrected — the plugin updates itself, so no uninstall is needed. This release doubles as the live drill for one-click update.
124
128
  - **v1.6.0** (2026-10-11) New: **check for updates and install them in one click**. The settings section can fetch the newest npm version and install it: the download is checked against the registry's published sha512, only whitelisted files are unpacked (symlinks and path traversal are refused), the old files are kept aside, and any failure restores them. Downloaded code is never executed — the new code loads when DSH restarts. Automatic checks run at most once a day and can be turned off, with the mainland-China mirror as fallback.
125
129
  - **v1.5.0** (2026-10-08) The "finished" title mark is unread news now — set by a finish that happens while you are away, cleared when you return — instead of reappearing on every switch-away. The retrigger cooldown is per session, so two conversations deciding within 2 seconds both ring. The Host half gained tests (Config schema, focus route, title matching) that cross-check the shared constants between the halves; the sessions provider is now an explicit load prerequisite, and quiet-hours catch-up is documented.
126
130
 
package/README.zh.md CHANGED
@@ -19,6 +19,10 @@ dsh-plugin-notify-sounds
19
19
 
20
20
  ---
21
21
 
22
+ ## 与 DSH 界面保持一致
23
+
24
+ 设置面板中的每一处视觉细节都取自 DSH 自身的设计体系:行几何、分隔线、字号与间距沿用官方设计令牌,音效选择器使用官方 `Menu` 组件,图标来自官方图标族,排布沿用「文字左 / 控件右」的原生行式。安装后不会在设置中出现风格割裂的第三方区块。
25
+
22
26
  ## 它做什么
23
27
 
24
28
  | 提醒什么 | 效果 |
@@ -40,11 +44,9 @@ DSH 的状态提示只有视觉——状态点、侧栏标记。你一切到别
40
44
 
41
45
  ## 声音
42
46
 
43
- 九种 Web Audio 合成音效,不含任何音频文件、不发网络请求:
44
-
45
- 双音铃 · 风铃 · 单音叮 · 水滴 · 脉冲 · 上行琶音 · 金币 · 达成 · 敲门
47
+ 双音铃 · 风铃 · 单音叮 · 水滴 · 脉冲 · 上行琶音 · 金币 · 达成 · 敲门 —— 九种内置音效全部由 Web Audio 现场合成,不含任何音频文件、不发一次网络请求。
46
48
 
47
- 另可上传自己的铃声(mp3/wav/ogg,≤512KB,自动校验与音量归一化),或选「无声」只留通知。三种触发(审批 / 提问 / 完成)可各选各的音效,设置里直接试听。
49
+ 也可以换上你自己的铃声(mp3/wav/ogg,≤512KB,自动校验并做音量归一化),或者干脆选「无声」只留通知。三种触发(审批 / 提问 / 完成)各选各的音效,设置里随时试听。
48
50
 
49
51
  ## 设置
50
52
 
@@ -72,7 +74,7 @@ DSH 的状态提示只有视觉——状态点、侧栏标记。你一切到别
72
74
 
73
75
  ### 升级
74
76
 
75
- **装完不支持自动更新**:要升级请先在同一处**卸载**旧版,再按上面的步骤装新版。
77
+ DSH 不会自动更新插件,但本插件**自带更新**:设置 → 提示音 →「更新」里可以检查新版并一键安装。想走手动路径也可以:在同一处先**卸载**旧版,再按上面的步骤装新版。
76
78
 
77
79
  ### 其他安装方式
78
80
 
@@ -121,6 +123,8 @@ dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
121
123
 
122
124
  ## 更新记录
123
125
 
126
+ - **v1.6.2**(2026-10-11)修复一键更新在**镜像源**下必然失败:npmmirror 的 tarball 会跳转到它的 CDN,而下载器此前拒绝一切重定向,于是「只要检查由镜像应答,下载就永远到不了」(真机演练实测发现)。现在重定向改为逐跳跟随、每一跳都必须仍落在受信主机上(镜像 CDN 是唯一合法跳转),sha512 校验不变;失败提示会带上失败阶段(下载 / 校验 / 解包 / 替换),便于定位。
127
+ - **v1.6.1**(2026-10-11)README 重排为发布页风格:补「**与 DSH 界面保持一致**」一节讲清界面取自 DSH 自身设计体系;通篇表达润色,并修正升级说明(本插件自带更新,无需卸载重装)。本版同时是「一键更新」的发布演练。
124
128
  - **v1.6.0**(2026-10-11)新增**检查更新与一键更新**:设置里可查 npm 新版本并一键安装——下载后按 registry 公布的 sha512 校验、只解包白名单文件(拒绝符号链接与路径穿越)、先备份再替换、失败自动还原;下载内容从不执行,新代码在 DSH 重启时生效。自动检查每天最多一次、可关;npm 官方源不通时自动改用国内镜像。
125
129
  - **v1.5.0**(2026-10-08)「已完成」标题标记改为未读语义——离开期间发生的完成才标记,回到页面即清除,不再每次切走都重新出现;触发冷却按会话独立,2 秒内两个会话先后需要决策时都会响。补齐宿主半测试(配置 schema、唤窗路由、标题匹配)并交叉校验两半共享常量;`dsh.client.inject` 显式声明 sessions 提供包;静音时段结束后的补发行为写入文档。
126
130
 
package/lib/client.js CHANGED
@@ -193,7 +193,7 @@ window.__ModuleLoader__.load({
193
193
  "update.updating": "更新中…",
194
194
  "update.done": "更新完成",
195
195
  "update.restart": "重启 DSH 后生效",
196
- "update.applyFailed": "更新失败,已恢复原状",
196
+ "update.applyFailed": "更新失败({stage}),已恢复原状",
197
197
  };
198
198
 
199
199
  /** English dictionary, checked complete against the zh key set. */
@@ -270,7 +270,7 @@ window.__ModuleLoader__.load({
270
270
  "update.updating": "Updating…",
271
271
  "update.done": "Updated",
272
272
  "update.restart": "restart DSH to load it",
273
- "update.applyFailed": "Update failed — the previous version is intact",
273
+ "update.applyFailed": "Update failed ({stage}) — the previous version is intact",
274
274
  };
275
275
  //#endregion
276
276
 
@@ -1590,7 +1590,7 @@ window.__ModuleLoader__.load({
1590
1590
  : t("update.uptodate");
1591
1591
  if (state.phase === "checking") status = t("update.checking");
1592
1592
  if (state.phase === "updating") status = t("update.updating");
1593
- if (state.phase === "applyFailed") status = t("update.applyFailed");
1593
+ if (state.phase === "applyFailed") status = t("update.applyFailed", { stage: String(state.result?.stage ?? "?") });
1594
1594
  let detail = payload === undefined ? status : `${t("update.current")} ${version(payload.current)} · ${status}`;
1595
1595
  if (state.phase === "updated") {
1596
1596
  detail = `${t("update.done")}:${version(state.result?.from)} → ${version(state.result?.to)},${t("update.restart")}`;
package/lib/index.js CHANGED
@@ -443,6 +443,21 @@ const UPDATE_PACKAGE = "dsh-plugin-notify-sounds";
443
443
  */
444
444
  const UPDATE_REGISTRIES = ["https://registry.npmjs.org", "https://registry.npmmirror.com"];
445
445
 
446
+ /**
447
+ * Hosts a tarball may live on: the registries above, plus the one legitimate
448
+ * hop between them.
449
+ *
450
+ * The mainland mirror answers its metadata on its own host but serves the
451
+ * tarball from its CDN, so a download that refuses every redirect could never
452
+ * finish when the mirror answered (measured). The CDN is therefore trusted
453
+ * for tarballs only, and redirects are still followed one hop at a time with
454
+ * every hop checked against this list.
455
+ */
456
+ const UPDATE_TARBALL_HOSTS = [...UPDATE_REGISTRIES, "https://cdn.npmmirror.com"];
457
+
458
+ /** How many redirects a download may follow before giving up. */
459
+ const UPDATE_MAX_REDIRECTS = 4;
460
+
446
461
  /** How long one check result is trusted before the next check may run. */
447
462
  const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
448
463
 
@@ -511,15 +526,16 @@ function readOwnVersion(root) {
511
526
  * Whether a tarball URL is somewhere this plugin is willing to download from.
512
527
  *
513
528
  * The URL arrives inside a registry response, so it is untrusted input: https
514
- * only, and the host must be one of the registries above. A registry pointing
515
- * anywhere else is refused rather than followed. The download stage checks
516
- * this again on its own — validating it here too means a hostile answer is
517
- * rejected at the door, not at the moment it is about to be used.
529
+ * only, and the host must be one of `UPDATE_TARBALL_HOSTS`. A registry pointing
530
+ * anywhere else is refused rather than followed. The download stage re-checks
531
+ * every redirect against this same list, so a hostile answer — or a hostile
532
+ * redirect — is rejected at the door instead of at the moment it is about to
533
+ * be used.
518
534
  */
519
535
  export function isTrustedTarball(url) {
520
536
  try {
521
537
  const parsed = new URL(url);
522
- return parsed.protocol === "https:" && UPDATE_REGISTRIES.some((registry) => parsed.origin === registry);
538
+ return parsed.protocol === "https:" && UPDATE_TARBALL_HOSTS.some((host) => parsed.origin === host);
523
539
  } catch {
524
540
  return false;
525
541
  }
@@ -584,7 +600,7 @@ export async function checkForUpdate(options = {}) {
584
600
  return updateCheck.result;
585
601
  }
586
602
  updateCheck.requestedAt = now;
587
- const current = readOwnVersion();
603
+ const current = readOwnVersion(options.root);
588
604
  const answer = await fetchRegistryAnswer();
589
605
  if (answer === undefined) {
590
606
  const result = { ok: false, current, error: "no registry answered" };
@@ -756,20 +772,42 @@ export function verifyIntegrity(bytes, integrity) {
756
772
  /**
757
773
  * Download the tarball a validated registry answer named.
758
774
  *
759
- * The URL was checked when the answer arrived and is checked again here; the
760
- * response is capped and time-limited like every other request, and never
761
- * redirected.
775
+ * Redirects are followed by hand rather than by the fetch layer: every hop's
776
+ * URL must itself be on `UPDATE_TARBALL_HOSTS`, so a redirect can never walk
777
+ * off the list — the mirror's CDN hop is the one legitimate case, and anything
778
+ * else is refused by name. The response is capped and time-limited like every
779
+ * other request; the sha512 check afterwards is what actually vouches for the
780
+ * bytes, whatever host they came from.
781
+ *
782
+ * @returns the tarball bytes.
762
783
  */
763
784
  async function downloadTarball(url) {
764
- if (!isTrustedTarball(url)) throw new Error("tarball URL is not a trusted registry");
765
- log(`update: downloading ${url}`);
766
- const response = await fetch(url, {
767
- signal: AbortSignal.timeout(60 * 1000),
768
- redirect: "error",
769
- headers: { accept: "application/octet-stream" },
770
- });
771
- if (!response.ok) throw new Error(`download failed: HTTP ${response.status}`);
772
- return readCappedBytes(response, UPDATE_MAX_TARBALL_BYTES);
785
+ let current = url;
786
+ for (let hop = 0; hop <= UPDATE_MAX_REDIRECTS; hop++) {
787
+ if (!isTrustedTarball(current)) throw new Error(`download refused: ${current}`);
788
+ log(`update: downloading ${current}`);
789
+ let response;
790
+ try {
791
+ response = await fetch(current, {
792
+ signal: AbortSignal.timeout(60 * 1000),
793
+ redirect: "manual",
794
+ headers: { accept: "application/octet-stream" },
795
+ });
796
+ } catch (error) {
797
+ throw new Error(`download failed: ${error?.message ?? error}`);
798
+ }
799
+ if (response.status >= 300 && response.status < 400) {
800
+ const location = response.headers.get("location");
801
+ if (typeof location !== "string" || location === "") {
802
+ throw new Error(`download failed: HTTP ${response.status} without a redirect target`);
803
+ }
804
+ current = new URL(location, current).toString();
805
+ continue;
806
+ }
807
+ if (!response.ok) throw new Error(`download failed: HTTP ${response.status}`);
808
+ return readCappedBytes(response, UPDATE_MAX_TARBALL_BYTES);
809
+ }
810
+ throw new Error(`download refused: more than ${UPDATE_MAX_REDIRECTS} redirects`);
773
811
  }
774
812
 
775
813
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-notify-sounds",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Notification sounds for DeepSeek Harness — a soft chime when the agent needs your decision or finishes a task, with a Settings section styled like DSH's own · 需要决策或任务完成时播放提示音,点击系统通知可回到窗口,设置面板可调音量与音效",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -18,7 +18,8 @@
18
18
  "lib/index.js",
19
19
  "lib/client.js",
20
20
  "cordis.patch.yml",
21
- "README.zh.md"
21
+ "README.zh.md",
22
+ "assets"
22
23
  ],
23
24
  "license": "MIT",
24
25
  "repository": {