dsh-plugin-notify-sounds 1.6.1 → 1.6.3
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 +2 -0
- package/README.zh.md +2 -0
- package/lib/client.js +15 -6
- package/lib/index.js +70 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,8 @@ For anyone hacking on the code (exact details live in the source comments):
|
|
|
123
123
|
|
|
124
124
|
## Changelog
|
|
125
125
|
|
|
126
|
+
- **v1.6.3** (2026-10-11) The update-success message no longer flashes and vanishes: the Host remembers the install and repeats it with every check, so "Updated, restart DSH to load it" survives the page reload the update itself triggers (it swaps the client half's own file) and stays until the restart. The cached check is invalidated after an install too, so the row can no longer claim a newer version than the one it just wrote.
|
|
127
|
+
- **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).
|
|
126
128
|
- **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.
|
|
127
129
|
- **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.
|
|
128
130
|
- **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.
|
package/README.zh.md
CHANGED
|
@@ -123,6 +123,8 @@ dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
|
|
|
123
123
|
|
|
124
124
|
## 更新记录
|
|
125
125
|
|
|
126
|
+
- **v1.6.3**(2026-10-11)更新成功的提示不再一闪而过:更新结果改由**宿主**记住、随每次检查下发——即便更新替换掉了客户端自身文件、页面热重载丢掉本地状态,「更新完成:vX → vY,重启 DSH 后生效」也会一直显示到你重启;同时更新后**作废检查缓存**,不会再出现「明明刚升完却仍显示有新版」。
|
|
127
|
+
- **v1.6.2**(2026-10-11)修复一键更新在**镜像源**下必然失败:npmmirror 的 tarball 会跳转到它的 CDN,而下载器此前拒绝一切重定向,于是「只要检查由镜像应答,下载就永远到不了」(真机演练实测发现)。现在重定向改为逐跳跟随、每一跳都必须仍落在受信主机上(镜像 CDN 是唯一合法跳转),sha512 校验不变;失败提示会带上失败阶段(下载 / 校验 / 解包 / 替换),便于定位。
|
|
126
128
|
- **v1.6.1**(2026-10-11)README 重排为发布页风格:补「**与 DSH 界面保持一致**」一节讲清界面取自 DSH 自身设计体系;通篇表达润色,并修正升级说明(本插件自带更新,无需卸载重装)。本版同时是「一键更新」的发布演练。
|
|
127
129
|
- **v1.6.0**(2026-10-11)新增**检查更新与一键更新**:设置里可查 npm 新版本并一键安装——下载后按 registry 公布的 sha512 校验、只解包白名单文件(拒绝符号链接与路径穿越)、先备份再替换、失败自动还原;下载内容从不执行,新代码在 DSH 重启时生效。自动检查每天最多一次、可关;npm 官方源不通时自动改用国内镜像。
|
|
128
130
|
- **v1.5.0**(2026-10-08)「已完成」标题标记改为未读语义——离开期间发生的完成才标记,回到页面即清除,不再每次切走都重新出现;触发冷却按会话独立,2 秒内两个会话先后需要决策时都会响。补齐宿主半测试(配置 schema、唤窗路由、标题匹配)并交叉校验两半共享常量;`dsh.client.inject` 显式声明 sessions 提供包;静音时段结束后的补发行为写入文档。
|
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
|
|
|
@@ -1583,6 +1583,14 @@ window.__ModuleLoader__.load({
|
|
|
1583
1583
|
}, []);
|
|
1584
1584
|
const payload = state.payload;
|
|
1585
1585
|
const version = (text) => (typeof text === "string" && text !== "" ? `v${text}` : "?");
|
|
1586
|
+
// The Host remembers the last install and repeats it with every
|
|
1587
|
+
// answer, and this row prefers that: an update swaps the client
|
|
1588
|
+
// half's own file, so the page may reload and drop this row's
|
|
1589
|
+
// state mid-message — the news has to survive that. The Host keeps
|
|
1590
|
+
// it until DSH restarts, which is exactly as long as "restart to
|
|
1591
|
+
// load it" is true.
|
|
1592
|
+
const applied = payload?.applied ?? (state.phase === "updated" ? state.result : undefined);
|
|
1593
|
+
const appliedDone = applied !== undefined && (applied.ok === true || typeof applied.to === "string");
|
|
1586
1594
|
let status = payload === undefined
|
|
1587
1595
|
? t("update.idle")
|
|
1588
1596
|
: payload.updateAvailable === true
|
|
@@ -1590,10 +1598,11 @@ window.__ModuleLoader__.load({
|
|
|
1590
1598
|
: t("update.uptodate");
|
|
1591
1599
|
if (state.phase === "checking") status = t("update.checking");
|
|
1592
1600
|
if (state.phase === "updating") status = t("update.updating");
|
|
1593
|
-
if (state.phase === "applyFailed") status = t("update.applyFailed");
|
|
1594
1601
|
let detail = payload === undefined ? status : `${t("update.current")} ${version(payload.current)} · ${status}`;
|
|
1595
|
-
if (
|
|
1596
|
-
detail = `${t("update.done")}:${version(
|
|
1602
|
+
if (appliedDone) {
|
|
1603
|
+
detail = `${t("update.done")}:${version(applied.from)} → ${version(applied.to)},${t("update.restart")}`;
|
|
1604
|
+
} else if (state.phase === "applyFailed") {
|
|
1605
|
+
detail = t("update.applyFailed", { stage: String(state.result?.stage ?? "?") });
|
|
1597
1606
|
}
|
|
1598
1607
|
const busy = state.phase === "checking" || state.phase === "updating";
|
|
1599
1608
|
return h("div", { className: css.row }, [
|
|
@@ -1603,7 +1612,7 @@ window.__ModuleLoader__.load({
|
|
|
1603
1612
|
h("div", { className: css.hint, key: "status" }, detail),
|
|
1604
1613
|
]),
|
|
1605
1614
|
h("div", { className: css.stackControl, key: "control" }, [
|
|
1606
|
-
payload?.updateAvailable === true &&
|
|
1615
|
+
payload?.updateAvailable === true && !appliedDone ? h(Button, {
|
|
1607
1616
|
key: "apply",
|
|
1608
1617
|
variant: "outline",
|
|
1609
1618
|
size: "sm",
|
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
|
|
|
@@ -453,8 +468,8 @@ const UPDATE_CHECK_SPACING_MS = 15 * 1000;
|
|
|
453
468
|
const UPDATE_FETCH_TIMEOUT_MS = 10 * 1000;
|
|
454
469
|
const UPDATE_MAX_RESPONSE_BYTES = 256 * 1024;
|
|
455
470
|
|
|
456
|
-
/** Result of the last check,
|
|
457
|
-
const updateCheck = { checkedAt: 0, requestedAt: 0, result: undefined };
|
|
471
|
+
/** Result of the last check, when the last real request went out, and the last successful install. */
|
|
472
|
+
const updateCheck = { checkedAt: 0, requestedAt: 0, result: undefined, applied: undefined };
|
|
458
473
|
|
|
459
474
|
/**
|
|
460
475
|
* Parse `major.minor.patch` with an optional prerelease tag.
|
|
@@ -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
|
|
515
|
-
* anywhere else is refused rather than followed. The download stage checks
|
|
516
|
-
*
|
|
517
|
-
* rejected at the door
|
|
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:" &&
|
|
538
|
+
return parsed.protocol === "https:" && UPDATE_TARBALL_HOSTS.some((host) => parsed.origin === host);
|
|
523
539
|
} catch {
|
|
524
540
|
return false;
|
|
525
541
|
}
|
|
@@ -584,10 +600,10 @@ 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
|
-
const result = { ok: false, current, error: "no registry answered" };
|
|
606
|
+
const result = { ok: false, current, error: "no registry answered", applied: updateCheck.applied };
|
|
591
607
|
updateCheck.checkedAt = now;
|
|
592
608
|
updateCheck.result = result;
|
|
593
609
|
return result;
|
|
@@ -599,6 +615,7 @@ export async function checkForUpdate(options = {}) {
|
|
|
599
615
|
latest: answer.version,
|
|
600
616
|
updateAvailable: Number.isFinite(ordering) && ordering < 0,
|
|
601
617
|
checkedAt: now,
|
|
618
|
+
applied: updateCheck.applied,
|
|
602
619
|
};
|
|
603
620
|
updateCheck.checkedAt = now;
|
|
604
621
|
updateCheck.result = result;
|
|
@@ -756,20 +773,42 @@ export function verifyIntegrity(bytes, integrity) {
|
|
|
756
773
|
/**
|
|
757
774
|
* Download the tarball a validated registry answer named.
|
|
758
775
|
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
776
|
+
* Redirects are followed by hand rather than by the fetch layer: every hop's
|
|
777
|
+
* URL must itself be on `UPDATE_TARBALL_HOSTS`, so a redirect can never walk
|
|
778
|
+
* off the list — the mirror's CDN hop is the one legitimate case, and anything
|
|
779
|
+
* else is refused by name. The response is capped and time-limited like every
|
|
780
|
+
* other request; the sha512 check afterwards is what actually vouches for the
|
|
781
|
+
* bytes, whatever host they came from.
|
|
782
|
+
*
|
|
783
|
+
* @returns the tarball bytes.
|
|
762
784
|
*/
|
|
763
785
|
async function downloadTarball(url) {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
786
|
+
let current = url;
|
|
787
|
+
for (let hop = 0; hop <= UPDATE_MAX_REDIRECTS; hop++) {
|
|
788
|
+
if (!isTrustedTarball(current)) throw new Error(`download refused: ${current}`);
|
|
789
|
+
log(`update: downloading ${current}`);
|
|
790
|
+
let response;
|
|
791
|
+
try {
|
|
792
|
+
response = await fetch(current, {
|
|
793
|
+
signal: AbortSignal.timeout(60 * 1000),
|
|
794
|
+
redirect: "manual",
|
|
795
|
+
headers: { accept: "application/octet-stream" },
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
throw new Error(`download failed: ${error?.message ?? error}`);
|
|
799
|
+
}
|
|
800
|
+
if (response.status >= 300 && response.status < 400) {
|
|
801
|
+
const location = response.headers.get("location");
|
|
802
|
+
if (typeof location !== "string" || location === "") {
|
|
803
|
+
throw new Error(`download failed: HTTP ${response.status} without a redirect target`);
|
|
804
|
+
}
|
|
805
|
+
current = new URL(location, current).toString();
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (!response.ok) throw new Error(`download failed: HTTP ${response.status}`);
|
|
809
|
+
return readCappedBytes(response, UPDATE_MAX_TARBALL_BYTES);
|
|
810
|
+
}
|
|
811
|
+
throw new Error(`download refused: more than ${UPDATE_MAX_REDIRECTS} redirects`);
|
|
773
812
|
}
|
|
774
813
|
|
|
775
814
|
/**
|
|
@@ -844,6 +883,16 @@ export async function applyUpdate(options = {}) {
|
|
|
844
883
|
const files = unpackTar(gunzipSync(bytes));
|
|
845
884
|
stage = "replace";
|
|
846
885
|
installUpdate(root, files);
|
|
886
|
+
// The answer that said "update available" is now a lie about this
|
|
887
|
+
// installation: drop the cached check so the next one re-reads the
|
|
888
|
+
// manifest this install just wrote. The outcome is remembered too —
|
|
889
|
+
// the very files that were just swapped include the client half, so
|
|
890
|
+
// the page may reload and lose its own state, and the news that an
|
|
891
|
+
// update landed must survive that. It is kept until DSH restarts,
|
|
892
|
+
// which is exactly as long as "restart to load it" is true.
|
|
893
|
+
updateCheck.checkedAt = 0;
|
|
894
|
+
updateCheck.result = undefined;
|
|
895
|
+
updateCheck.applied = { from: current, to: answer.version, at: Date.now() };
|
|
847
896
|
log(`update applied: ${current ?? "?"} -> ${answer.version}`);
|
|
848
897
|
return { ok: true, from: current, to: answer.version, requiresRestart: true };
|
|
849
898
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-notify-sounds",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
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",
|