dsh-plugin-notify-sounds 1.6.5 → 1.6.8
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.en.md +180 -0
- package/README.md +110 -107
- package/lib/client.js +229 -53
- package/lib/index.js +130 -31
- package/package.json +2 -2
- package/README.zh.md +0 -175
package/lib/index.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* OS window, but this process can: on Windows it calls `user32.dll`
|
|
16
16
|
* through the Desktop's own FFI binding and finds the window by title.
|
|
17
17
|
*
|
|
18
|
-
* `apply` also
|
|
19
|
-
* because the Client contributes a dedicated
|
|
18
|
+
* `apply` also asks the settings namespace to hold off any auto-generated
|
|
19
|
+
* inventory page for this entry, because the Client contributes a dedicated
|
|
20
|
+
* Settings section of its own. (Note: `autoGenerate` is not yet consumed by
|
|
21
|
+
* any shipped client — the flag is correct and future-proof, but today the
|
|
22
|
+
* effect is a no-op. Runtime README: "no shipped client does so yet".)
|
|
20
23
|
*
|
|
21
24
|
* @module dsh-plugin-notify-sounds
|
|
22
25
|
*/
|
|
@@ -42,6 +45,15 @@ export const TIME_OF_DAY = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
|
42
45
|
/** Deep link the packaged Desktop registers (`setAsDefaultProtocolClient`); the last-resort focus path. */
|
|
43
46
|
const FOCUS_DEEP_LINK = "dsh://open";
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The desktop app's bundle id, for raising it by identity on macOS.
|
|
50
|
+
*
|
|
51
|
+
* Taken from the packaged shell's own manifest (`dshDesktopAppId`), not
|
|
52
|
+
* guessed: `open -b <id>` is the only raise that needs no URL scheme to have
|
|
53
|
+
* been registered.
|
|
54
|
+
*/
|
|
55
|
+
const MACOS_BUNDLE_ID = "com.deepseek.dsh";
|
|
56
|
+
|
|
45
57
|
/** Route the browser half posts to when a system notification is clicked. Exported so the tests can cross-check the Client half's spelling. */
|
|
46
58
|
export const FOCUS_PATH = "/notify-sounds/focus";
|
|
47
59
|
|
|
@@ -330,16 +342,32 @@ function raiseDesktopWindowDeepLink() {
|
|
|
330
342
|
/**
|
|
331
343
|
* Start a short-lived launcher without blocking, reporting failures once.
|
|
332
344
|
*
|
|
345
|
+
* The exit code matters as much as the spawn: `open` exits non-zero when no
|
|
346
|
+
* application handles the request (macOS: "Unable to find application for
|
|
347
|
+
* URL"), and a launch that never happened must not look exactly like one
|
|
348
|
+
* that did.
|
|
349
|
+
*
|
|
333
350
|
* @param command - the executable to run.
|
|
334
351
|
* @param args - its arguments.
|
|
352
|
+
* @param done - called with whether the launcher exited cleanly; optional,
|
|
353
|
+
* so a caller can chain a fallback.
|
|
335
354
|
*/
|
|
336
|
-
function launcher(command, args) {
|
|
355
|
+
function launcher(command, args, done) {
|
|
337
356
|
try {
|
|
338
357
|
const child = spawn(command, args, { windowsHide: true, stdio: "ignore" });
|
|
339
|
-
child.on("error", (error) =>
|
|
358
|
+
child.on("error", (error) => {
|
|
359
|
+
warn(`could not launch ${command}: ${error?.message ?? error}`);
|
|
360
|
+
done?.(false);
|
|
361
|
+
});
|
|
362
|
+
child.on("exit", (code) => {
|
|
363
|
+
const ok = code === 0;
|
|
364
|
+
if (!ok) warn(`${command} exited ${code}: ${args.join(" ")}`);
|
|
365
|
+
done?.(ok);
|
|
366
|
+
});
|
|
340
367
|
child.unref();
|
|
341
368
|
} catch (error) {
|
|
342
369
|
warn(`could not launch ${command}: ${error?.message ?? error}`);
|
|
370
|
+
done?.(false);
|
|
343
371
|
}
|
|
344
372
|
}
|
|
345
373
|
|
|
@@ -414,9 +442,24 @@ function focusDesktopWindow() {
|
|
|
414
442
|
raiseDesktopWindowDeepLink();
|
|
415
443
|
}
|
|
416
444
|
} else if (process.platform === "darwin") {
|
|
417
|
-
|
|
418
|
-
|
|
445
|
+
// Activate the app by bundle id first: it needs no URL scheme to
|
|
446
|
+
// have been registered, which a packaged .app only gets if its
|
|
447
|
+
// Info.plist declares CFBundleURLTypes — something this project
|
|
448
|
+
// cannot verify from source. The deep link stays as the fallback,
|
|
449
|
+
// and a launch that failed is now named in the log instead of
|
|
450
|
+
// looking exactly like one that succeeded.
|
|
451
|
+
log("raising via open -b");
|
|
452
|
+
launcher("open", ["-b", MACOS_BUNDLE_ID], (ok) => {
|
|
453
|
+
if (ok) return;
|
|
454
|
+
log("bundle-id activation did not take; falling back to the deep link");
|
|
455
|
+
launcher("open", [FOCUS_DEEP_LINK]);
|
|
456
|
+
});
|
|
419
457
|
} else {
|
|
458
|
+
// No desktop shell ships for other platforms today — the shell
|
|
459
|
+
// itself refuses anything but win32/darwin, and a browser click
|
|
460
|
+
// never reaches this function. `xdg-open` is the correct opener
|
|
461
|
+
// should a Linux shell ever appear; until then this line is
|
|
462
|
+
// unreachable rather than misleading.
|
|
420
463
|
log("raising via xdg-open + deep link");
|
|
421
464
|
launcher("xdg-open", [FOCUS_DEEP_LINK]);
|
|
422
465
|
}
|
|
@@ -471,6 +514,12 @@ const UPDATE_MAX_RESPONSE_BYTES = 256 * 1024;
|
|
|
471
514
|
/** Result of the last check, when the last real request went out, and the last successful install. */
|
|
472
515
|
const updateCheck = { checkedAt: 0, requestedAt: 0, result: undefined, applied: undefined };
|
|
473
516
|
|
|
517
|
+
/** The check currently in flight, if any — callers join it instead of starting a second one. */
|
|
518
|
+
let checkInFlight;
|
|
519
|
+
|
|
520
|
+
/** The update currently in flight, if any — one download and one swap at a time. */
|
|
521
|
+
let applyInFlight;
|
|
522
|
+
|
|
474
523
|
/**
|
|
475
524
|
* Parse `major.minor.patch` with an optional prerelease tag.
|
|
476
525
|
*
|
|
@@ -599,27 +648,40 @@ export async function checkForUpdate(options = {}) {
|
|
|
599
648
|
if (now - updateCheck.requestedAt < UPDATE_CHECK_SPACING_MS && updateCheck.result !== undefined) {
|
|
600
649
|
return updateCheck.result;
|
|
601
650
|
}
|
|
651
|
+
// Two callers arriving together — the settings row and the notice above
|
|
652
|
+
// it, or an automatic check landing on a user's click — must not become
|
|
653
|
+
// two requests. The spacing rule above cannot cover that: it only sees a
|
|
654
|
+
// finished result, and while the first request is still in the air there
|
|
655
|
+
// is none. So a second caller joins the first instead.
|
|
656
|
+
if (checkInFlight !== undefined) return checkInFlight;
|
|
602
657
|
updateCheck.requestedAt = now;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
658
|
+
checkInFlight = (async () => {
|
|
659
|
+
const current = readOwnVersion(options.root);
|
|
660
|
+
const answer = await fetchRegistryAnswer();
|
|
661
|
+
if (answer === undefined) {
|
|
662
|
+
const result = { ok: false, current, error: "no registry answered", applied: updateCheck.applied };
|
|
663
|
+
updateCheck.checkedAt = now;
|
|
664
|
+
updateCheck.result = result;
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
const ordering = current === undefined ? NaN : compareVersions(current, answer.version);
|
|
668
|
+
const result = {
|
|
669
|
+
ok: true,
|
|
670
|
+
current,
|
|
671
|
+
latest: answer.version,
|
|
672
|
+
updateAvailable: Number.isFinite(ordering) && ordering < 0,
|
|
673
|
+
checkedAt: now,
|
|
674
|
+
applied: updateCheck.applied,
|
|
675
|
+
};
|
|
607
676
|
updateCheck.checkedAt = now;
|
|
608
677
|
updateCheck.result = result;
|
|
609
678
|
return result;
|
|
679
|
+
})();
|
|
680
|
+
try {
|
|
681
|
+
return await checkInFlight;
|
|
682
|
+
} finally {
|
|
683
|
+
checkInFlight = undefined;
|
|
610
684
|
}
|
|
611
|
-
const ordering = current === undefined ? NaN : compareVersions(current, answer.version);
|
|
612
|
-
const result = {
|
|
613
|
-
ok: true,
|
|
614
|
-
current,
|
|
615
|
-
latest: answer.version,
|
|
616
|
-
updateAvailable: Number.isFinite(ordering) && ordering < 0,
|
|
617
|
-
checkedAt: now,
|
|
618
|
-
applied: updateCheck.applied,
|
|
619
|
-
};
|
|
620
|
-
updateCheck.checkedAt = now;
|
|
621
|
-
updateCheck.result = result;
|
|
622
|
-
return result;
|
|
623
685
|
}
|
|
624
686
|
|
|
625
687
|
/**
|
|
@@ -822,6 +884,7 @@ async function downloadTarball(url) {
|
|
|
822
884
|
export function installUpdate(root, files) {
|
|
823
885
|
const rootPrefix = resolve(root);
|
|
824
886
|
const backup = join(root, ".update-backup");
|
|
887
|
+
const tempName = (relative) => `${resolve(root, relative)}.update-tmp`;
|
|
825
888
|
rmSync(backup, { recursive: true, force: true });
|
|
826
889
|
const written = [];
|
|
827
890
|
try {
|
|
@@ -835,18 +898,29 @@ export function installUpdate(root, files) {
|
|
|
835
898
|
mkdirSync(dirname(aside), { recursive: true });
|
|
836
899
|
writeFileSync(aside, readFileSync(target));
|
|
837
900
|
}
|
|
838
|
-
const temp =
|
|
901
|
+
const temp = tempName(relative);
|
|
839
902
|
writeFileSync(temp, source);
|
|
840
903
|
renameSync(temp, target);
|
|
841
904
|
written.push(relative);
|
|
842
905
|
}
|
|
843
906
|
} catch (error) {
|
|
907
|
+
// Put everything back — including files the update ADDED, which have
|
|
908
|
+
// no backup to restore from and must simply go away again. A failed
|
|
909
|
+
// update has to leave nothing behind: no half-replaced file, no
|
|
910
|
+
// leftover temp, and no file that was not there before.
|
|
844
911
|
for (const relative of written) {
|
|
845
912
|
const aside = join(backup, relative);
|
|
846
|
-
|
|
913
|
+
const target = resolve(root, relative);
|
|
914
|
+
if (existsSync(aside)) writeFileSync(target, readFileSync(aside));
|
|
915
|
+
else rmSync(target, { force: true });
|
|
847
916
|
}
|
|
917
|
+
for (const relative of UPDATE_FILES) rmSync(tempName(relative), { force: true });
|
|
848
918
|
throw error;
|
|
849
919
|
}
|
|
920
|
+
// The swap succeeded, so the backup has done its job. Leaving it would
|
|
921
|
+
// park the previous version's files — a hundred-odd KB including the
|
|
922
|
+
// client half — inside the profile until some later update clears it.
|
|
923
|
+
rmSync(backup, { recursive: true, force: true });
|
|
850
924
|
}
|
|
851
925
|
|
|
852
926
|
/**
|
|
@@ -867,6 +941,21 @@ export function installUpdate(root, files) {
|
|
|
867
941
|
* upToDate: true, current }`, or `{ ok: false, stage, error }`.
|
|
868
942
|
*/
|
|
869
943
|
export async function applyUpdate(options = {}) {
|
|
944
|
+
// One download and one swap at a time. Two callers — a second tab, or a
|
|
945
|
+
// user clicking again because the first attempt looks stuck — would share
|
|
946
|
+
// the fixed temp names and the single backup directory, and the slower
|
|
947
|
+
// one's cleanup would pull the floor out from under the other.
|
|
948
|
+
if (applyInFlight !== undefined) return applyInFlight;
|
|
949
|
+
applyInFlight = runUpdate(options);
|
|
950
|
+
try {
|
|
951
|
+
return await applyInFlight;
|
|
952
|
+
} finally {
|
|
953
|
+
applyInFlight = undefined;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** `applyUpdate` without the single-flight guard. */
|
|
958
|
+
async function runUpdate(options = {}) {
|
|
870
959
|
const root = options.root ?? packageRoot();
|
|
871
960
|
const current = readOwnVersion(root);
|
|
872
961
|
let stage = "check";
|
|
@@ -952,9 +1041,12 @@ export const Config = z.object({
|
|
|
952
1041
|
* @param ctx - the Host plugin context.
|
|
953
1042
|
*/
|
|
954
1043
|
export function apply(ctx) {
|
|
955
|
-
// The Client half registers a dedicated `settings.section` page;
|
|
956
|
-
// plugin inventory
|
|
957
|
-
// namespace
|
|
1044
|
+
// The Client half registers a dedicated `settings.section` page; this asks
|
|
1045
|
+
// the plugin inventory to hold off an auto-generated form for the same
|
|
1046
|
+
// namespace so the preference is editable from exactly one place. Note:
|
|
1047
|
+
// `autoGenerate` is not yet consumed by any shipped client (runtime README
|
|
1048
|
+
// confirms), so this is a forward-compatible declaration rather than an
|
|
1049
|
+
// effect that is already in force.
|
|
958
1050
|
ctx.inject(["settings"], (child) => {
|
|
959
1051
|
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
|
|
960
1052
|
});
|
|
@@ -982,16 +1074,23 @@ export function apply(ctx) {
|
|
|
982
1074
|
return;
|
|
983
1075
|
}
|
|
984
1076
|
// Which client asked? The Desktop's renderer fetches from its
|
|
985
|
-
// own `dsh-app://app/` origin, and the shell STRIPS Origin
|
|
986
|
-
// before forwarding to this server
|
|
987
|
-
//
|
|
1077
|
+
// own `dsh-app://app/` origin, and the shell STRIPS Origin and
|
|
1078
|
+
// Sec-Fetch-Site before forwarding to this server (the shell's
|
|
1079
|
+
// `forwardWebRequest` in `lib/main.js`), so a Desktop click
|
|
1080
|
+
// arrives with neither header while a real browser always sends
|
|
1081
|
+
// at least one. Checking BOTH keeps this working if the shell
|
|
1082
|
+
// ever stops stripping only one of them: the day it strips
|
|
1083
|
+
// neither, a Desktop click would silently start looking like a
|
|
1084
|
+
// browser one and clicks would stop raising the window — with no
|
|
1085
|
+
// other symptom to explain it.
|
|
988
1086
|
//
|
|
989
1087
|
// The two need opposite things, so the choice is derived here
|
|
990
1088
|
// rather than configured: a browser click needs no help, since
|
|
991
1089
|
// activating the notification raises the browser window itself
|
|
992
1090
|
// and raising the Desktop window instead would be the wrong
|
|
993
1091
|
// window entirely.
|
|
994
|
-
const
|
|
1092
|
+
const headers = req.headers ?? {};
|
|
1093
|
+
const fromBrowser = headers.origin !== undefined || headers["sec-fetch-site"] !== undefined;
|
|
995
1094
|
res.statusCode = 204;
|
|
996
1095
|
res.end();
|
|
997
1096
|
log(`focus click (fromBrowser=${fromBrowser})`);
|
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.8",
|
|
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,7 @@
|
|
|
18
18
|
"lib/index.js",
|
|
19
19
|
"lib/client.js",
|
|
20
20
|
"cordis.patch.yml",
|
|
21
|
-
"README.
|
|
21
|
+
"README.en.md",
|
|
22
22
|
"assets"
|
|
23
23
|
],
|
|
24
24
|
"license": "MIT",
|
package/README.zh.md
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# dsh-plugin-notify-sounds
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/dsh-plugin-notify-sounds)
|
|
4
|
-
[](https://www.npmjs.com/package/dsh-plugin-notify-sounds)
|
|
5
|
-
[](LICENSE)
|
|
6
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
7
|
-
|
|
8
|
-
[English](README.md) | 中文
|
|
9
|
-
|
|
10
|
-
**智能体在等你、或干完活时,轻轻响一声。**
|
|
11
|
-
|
|
12
|
-
人不在窗口前也不怕:后台弹系统通知(**点击即回到窗口**),任务栏标题亮出「待处理 / 已完成」;你回来看过一眼之后,它就闭嘴——不会像闹钟一样反复催你。
|
|
13
|
-
|
|
14
|
-
**安装**:DSH → 设置 → 插件 → 添加插件,粘贴下面这行,点「安装」:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
dsh-plugin-notify-sounds
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## 🎨 与 DSH 界面风格统一
|
|
23
|
-
|
|
24
|
-
设置面板中的每一处视觉细节都取自 DSH 自身的设计体系:行几何、分隔线、字号与间距沿用官方设计令牌,音效选择器使用官方 `Menu` 组件,图标来自官方图标族,排布沿用「文字左 / 控件右」的原生行式。安装后不会在设置中出现风格割裂的第三方区块。
|
|
25
|
-
|
|
26
|
-
## ✨ 它能做什么
|
|
27
|
-
|
|
28
|
-
| 提醒什么 | 效果 |
|
|
29
|
-
|---|---|
|
|
30
|
-
| **需要你决策** | 智能体发起审批、计划确认、提问时提示音;审批与提问可分别设音效 |
|
|
31
|
-
| **任务完成** | 会话由运行转为空闲时提示音 |
|
|
32
|
-
| **人不在窗口前也能感知** | 后台弹系统通知(正文就是审批理由或提问原文,点击回到窗口);任务栏标题显示「待处理 / 已完成」 |
|
|
33
|
-
| **没看到会再提醒** | 未处理决策按 1/5/15/30 分钟重复提醒 |
|
|
34
|
-
| **只提醒你关心的** | 子智能体(后台任务)默认完全静默,只提醒主对话,可开关 |
|
|
35
|
-
| **该安静时安静** | 静音时段(如 22:00–08:00,可跨午夜)、仅页面不可见时提示、总开关一键全静音 |
|
|
36
|
-
| **声音随你** | 九种内置音效、自定义铃声上传、音量调节,每项独立开关 |
|
|
37
|
-
| **检测更新** | 设置里检查 npm 新版本并一键更新:下载后校验完整性才替换,失败自动还原;更新后重启 DSH 生效 |
|
|
38
|
-
|
|
39
|
-
## 💡 为什么需要它
|
|
40
|
-
|
|
41
|
-
DSH 的状态提示只有视觉——状态点、侧栏标记。你一切到别的窗口,就既看不到也听不到,智能体却可能正卡在那里等你一句话。
|
|
42
|
-
|
|
43
|
-
这个插件把「在等你」和「干完了」变成声音。配合系统通知与标题标记,人走开也知道发生了什么;而你回来处理过之后它就安静——重复提醒也只针对你**从没看过**的决策。
|
|
44
|
-
|
|
45
|
-
## 🔔 声音
|
|
46
|
-
|
|
47
|
-
双音铃 · 风铃 · 单音叮 · 水滴 · 脉冲 · 上行琶音 · 金币 · 达成 · 敲门 —— 九种内置音效全部由 Web Audio 现场合成,不含任何音频文件、不发一次网络请求。
|
|
48
|
-
|
|
49
|
-
也可以换上你自己的铃声(mp3/wav/ogg,≤512KB,自动校验并做音量归一化),或者干脆选「无声」只留通知。三种触发(审批 / 提问 / 完成)各选各的音效,设置里随时试听。
|
|
50
|
-
|
|
51
|
-
## ⚙️ 设置
|
|
52
|
-
|
|
53
|
-
设置 → **提示音**(位于「通用设置」与「模型」之间),每项功能都有独立开关,界面与 DSH 原生设置完全一致:
|
|
54
|
-
|
|
55
|
-
总开关与音量 · 三种触发各自的音效(含试听)· 子智能体提醒 · 系统通知 · 窗口标题提醒 · 重复提醒间隔 · 静音时段 · 仅后台提示 · 自定义铃声上传 · 自动检查更新与一键更新
|
|
56
|
-
|
|
57
|
-
改动即时生效,无需重启。
|
|
58
|
-
|
|
59
|
-
## 📦 安装
|
|
60
|
-
|
|
61
|
-
### 🖱️ 最简单:在 DSH 里点两下(推荐)
|
|
62
|
-
|
|
63
|
-
1. 打开 DSH → **设置 → 插件**,点**添加插件**;
|
|
64
|
-
2. 在输入框粘贴包名(本插件已发布在 [npm](https://www.npmjs.com/package/dsh-plugin-notify-sounds)):
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
dsh-plugin-notify-sounds
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
(填 GitHub 仓库地址 `https://github.com/BillCx330/dsh-plugin-notify-sounds` 或本地目录路径也可以)
|
|
71
|
-
3. 点**安装**,装完**重启 DSH** 一次。
|
|
72
|
-
|
|
73
|
-
输入框上方的「安装源」是下载源,默认中国大陆镜像源,国内一般不用改。DSH 会提示「插件以你的权限运行」——本插件只读会话状态、在本地发声;唯一的请求是点击系统通知时发给**本机 DSH** 的唤窗请求,不向外部发送任何数据。
|
|
74
|
-
|
|
75
|
-
### ⬆️ 升级
|
|
76
|
-
|
|
77
|
-
DSH 不会自动更新插件,但本插件**自带更新功能**:设置 → 提示音 →「更新」里可以检查新版并一键安装。想走手动路径也可以:在同一处先**卸载**旧版,再按上面的步骤装新版。
|
|
78
|
-
|
|
79
|
-
### 🛠️ 其他安装方式
|
|
80
|
-
|
|
81
|
-
**命令行** —— 对你实际使用的 profile 一条命令:
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
dsh plugin --profile <你的 profile> add github:BillCx330/dsh-plugin-notify-sounds
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
**Release 压缩包** —— 从 [Releases](https://github.com/BillCx330/dsh-plugin-notify-sounds/releases) 下载 `.tgz`,交给 `dsh plugin add`。
|
|
88
|
-
|
|
89
|
-
**本地目录** —— 开发或试用未发布的改动:
|
|
90
|
-
|
|
91
|
-
```sh
|
|
92
|
-
dsh plugin --profile <你的 profile> add ./dsh-plugin-notify-sounds
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**手工编辑** —— 在 profile 的 `package.json` 的 `dependencies` 加 `"dsh-plugin-notify-sounds": "github:BillCx330/dsh-plugin-notify-sounds"`,在 `dsh.profile.bundles` 追加 `"dsh-plugin-notify-sounds"`,然后在 profile 目录跑一次包安装(如 `pnpm install`)把依赖装进 `node_modules`。
|
|
96
|
-
|
|
97
|
-
命令行方式会自动写入 `dsh.profile.bundles` 并装好依赖,无需手工编辑。包内直接带预构建产物、不声明任何生命周期脚本,安装过程不执行构建脚本。安装后**重启 DSH**(日常升级仅客户端改动时,可热更生效)。
|
|
98
|
-
|
|
99
|
-
## ⚠️ 已知边界
|
|
100
|
-
|
|
101
|
-
- **手改配置**:偏好写在 profile 的 `cordis.patch.yml`,值必须符合 schema(音量 0–100、音效为预设之一、时间 `HH:MM`);写错会让插件不加载,改回即可。设置界面只会写入合法值。
|
|
102
|
-
- **一键更新只换文件**:只替换本包内的七个文件,**不碰 profile 配置**;下载内容从不执行,新代码要等**重启 DSH** 才生效。更新过程请等状态行给出结果再关窗口(中断会自动还原);若未来的新版引入了新依赖,需另行安装(当前零依赖)。
|
|
103
|
-
- **多窗口**:各窗口独立触发,同一事件可能各响一次;打开「仅在页面不可见时提示」即可避免。
|
|
104
|
-
- **点击通知回到窗口**:Windows 完整实现并实测;macOS / Linux 走系统深链,未验证。
|
|
105
|
-
- **唤窗按窗口标题定位主窗口**:刻意不读取其他进程信息(那会触发杀软主动防御)。若将来 DSH 改变标题格式,唤窗会静默失效——提示音与通知本身不受影响。
|
|
106
|
-
- **导航小图标**:依赖设置面板的 DOM 结构,若DSH 改版后自动回落为官方齿轮,仅影响外观。
|
|
107
|
-
- **通知权限**:桌面端放行;受限环境(某些浏览器策略)下被拒时,行内会引导手动允许,其余功能不受影响。
|
|
108
|
-
|
|
109
|
-
## 🧩 设计与实现
|
|
110
|
-
|
|
111
|
-
给想改代码的人看的要点(实现细节以源码注释为准):
|
|
112
|
-
|
|
113
|
-
- **触发不碰事件瀑布**:审批/提问的请求是 waterfall 事件,UI 应答后晚注册的监听器收不到。本插件改读 `uiSession.sessionStatus`(DSH 自己聚合的 pending/running 事实,与侧栏状态点同源),纯观察、零侵入。
|
|
114
|
-
- **重复提醒的节拍**:15 秒粒度检查未处理决策;**看过即除名**(决策出现时你正在窗口、或之后回到窗口,都算看过),切走不重新武装;新问题替换旧问题会重置计时。免打扰期间的提醒直接作废,不会顺延到时段结束补发。
|
|
115
|
-
- **通知点击回到窗口**:渲染进程唤不起 OS 窗口,点击时 POST 到宿主半边的 `/notify-sounds/focus`(带 connection 围栏),由宿主唤起。Windows 用 DSH 自带的 `koffi` 直接调 `user32.dll`(不引新依赖、只操作窗口、不启动 PowerShell——那会触发杀软主动防御并归因到 DSH);桌面端/浏览器端按 `Origin` 头自动区分,浏览器端不误唤桌面窗口。
|
|
116
|
-
- **通知折叠**:不把 `tag` 交给平台(平台替换旧通知时会吞掉指向它的点击),由插件自己关旧发新,并持有每一代通知对象的引用,避免点击回调被回收成死按钮。
|
|
117
|
-
- **标题标记**:只做前缀加/删,与 DSH 自身的标题管理互不干扰;前缀按实际写入的文字记录,语言切换不叠加、不残留。「已完成」是未读消息——离开期间发生的完成才标记,回到页面即清除——不是常驻的「空闲」状态,看过的完成不会在每次切走时重新出现。
|
|
118
|
-
- **免打扰即遗忘**:静音时段内发生的决策与完成不记账——不响、不通知、不打标题标记,时段结束也不补发;本该在时段内发出的重复提醒直接作废。
|
|
119
|
-
- **冷却与健壮性**:同一会话的同类触发 2 秒内不重复响(另一个会话的决策照常响);音量经 `Number.isFinite` 防护(手改 YAML 的非法值回落默认);写入被拒时控件回弹到已接受值并显示错误行;开关写入中禁用,防双击竞写。
|
|
120
|
-
- **音量滑杆**:非受控输入,拖动只写 CSS 自定义属性,松手才提交一次,不卡顿、不回弹。
|
|
121
|
-
- **主题与排版**:全部使用 `--dsw-alias-*` 令牌,分区与行几何复刻官方设置页;音效选择用官方 `Menu` 下拉,不会撑出横向滚动。
|
|
122
|
-
|
|
123
|
-
## 📜 更新记录
|
|
124
|
-
|
|
125
|
-
- **v1.6.5**(2026-10-11)免打扰改为**真的忘记**:静音时段里发生的决策与完成不再记账——不响、不通知、不打标题标记,时段结束后也不会把夜里漏掉的一起发回来;本该在时段内发出的重复提醒直接作废而非顺延。更新提示移到分区顶部(页面回顶也能第一眼看到,不再需要滚动);更新行文案简化。
|
|
126
|
-
- **v1.6.4**(2026-10-11)更新完成后不用再手动往下翻:更新会替换插件自身的客户端文件、触发页面重载并回到顶部,现在「更新完成」提示出现时,「更新」行会**平滑滚动回视野**。
|
|
127
|
-
- **v1.6.3**(2026-10-11)更新成功的提示不再一闪而过:更新结果改由**宿主**记住、随每次检查下发——即便更新替换掉了客户端自身文件、页面热重载丢掉本地状态,「更新完成:vX → vY,重启 DSH 后生效」也会一直显示到你重启;同时更新后**作废检查缓存**,不会再出现「明明刚升完却仍显示有新版」。
|
|
128
|
-
- **v1.6.2**(2026-10-11)修复一键更新在**镜像源**下必然失败:npmmirror 的 tarball 会跳转到它的 CDN,而下载器此前拒绝一切重定向,于是「只要检查由镜像应答,下载就永远到不了」(真机演练实测发现)。现在重定向改为逐跳跟随、每一跳都必须仍落在受信主机上(镜像 CDN 是唯一合法跳转),sha512 校验不变;失败提示会带上失败阶段(下载 / 校验 / 解包 / 替换),便于定位。
|
|
129
|
-
- **v1.6.1**(2026-10-11)README 重排为发布页风格:补「**与 DSH 界面保持一致**」一节讲清界面取自 DSH 自身设计体系;通篇表达润色,并修正升级说明(本插件自带更新,无需卸载重装)。本版同时是「一键更新」的发布演练。
|
|
130
|
-
- **v1.6.0**(2026-10-11)新增**检查更新与一键更新**:设置里可查 npm 新版本并一键安装——下载后按 registry 公布的 sha512 校验、只解包白名单文件(拒绝符号链接与路径穿越)、先备份再替换、失败自动还原;下载内容从不执行,新代码在 DSH 重启时生效。自动检查每天最多一次、可关;npm 官方源不通时自动改用国内镜像。
|
|
131
|
-
- **v1.5.0**(2026-10-08)「已完成」标题标记改为未读语义——离开期间发生的完成才标记,回到页面即清除,不再每次切走都重新出现;触发冷却按会话独立,2 秒内两个会话先后需要决策时都会响。补齐宿主半测试(配置 schema、唤窗路由、标题匹配)并交叉校验两半共享常量;`dsh.client.inject` 显式声明 sessions 提供包;静音时段结束后的补发行为写入文档。
|
|
132
|
-
|
|
133
|
-
- **v1.4.4**(2026-10-08)静音时段现在也会屏蔽任务栏标题提醒;进入或离开静音时段时,标题标记会自动刷新。自定义音效上传/移除按顺序写入配置,写入被拒时回滚;配置写入同步抛错也会按保存失败处理。
|
|
134
|
-
- **v1.4.3**(2026-10-08)三处修复:环境不支持音频解码时,上传自定义铃声不再被误判为有效(反正也播不出来);通知权限请求**同步抛错**也走拒绝处理,不留未处理异常;静音时段只接受合法 `HH:MM`(`99:99` 之类会被拒),Windows 原生唤窗抛错时继续走深链兜底。另补两条回归测试,并把解码失败的提示改准确(文件损坏与环境不支持解码两种情况都说清)。
|
|
135
|
-
- **v1.4.2**(2026-10-07)干净发布点:`v1.4.1` 的标签发布时被重钉过一次,本版重新发布,让下游拿到确定的标签;无行为变更。
|
|
136
|
-
- **v1.4.1**(2026-10-07)移除「没能唤回窗口」提示——判据会误报(窗口明明回来了还弹),而成功本身就是反馈;点通知现在全程静默,不再有任何跟点击弹出的通知。另:README 改版为发布页风格。
|
|
137
|
-
- **v1.4.0**(2026-10-07)看过就不再重复提醒;修复通知点击没反应(根因:平台 `tag` 替换吞掉点击);新增「子智能体的提醒」开关;另修标题前缀叠加等 5 项。
|
|
138
|
-
- **v1.3.3**(2026-10-07)重复提醒不再打扰正在看窗口的你;标题提醒改用文字(待处理 / 已完成)。
|
|
139
|
-
- **v1.3.2**(2026-10-05)两轮代码审查修复:非法 base64 中断跟踪器、通知对象回收、重复提醒吃冷却、时钟回拨静音、titleFlash 即时生效。
|
|
140
|
-
- **v1.3.1**(2026-10-05)修复唤窗时好时坏(Windows 前台锁)。
|
|
141
|
-
- **v1.3.0**(2026-10-05)唤窗改为进程内 FFI;桌面端/浏览器端自动区分。
|
|
142
|
-
- **v1.2.1**(2026-10-05)修复点击无反应。
|
|
143
|
-
- **v1.2.0**(2026-10-05)新增宿主 focus 路由(该版本点击无效,v1.3.x 修正)。
|
|
144
|
-
- **v1.1.2**(2026-10-05)修复下拉列表空白。
|
|
145
|
-
- **v1.1.1**(2026-10-05)排版与文案修复。
|
|
146
|
-
- **v1.1.0**(2026-10-05)自定义铃声、系统通知、分轨音效、重复提醒、静音时段、三种新音效。
|
|
147
|
-
- **v1.0.0**(2026-10-04)首个版本。
|
|
148
|
-
|
|
149
|
-
## 💻 开发
|
|
150
|
-
|
|
151
|
-
```powershell
|
|
152
|
-
npm install # 一次性:宿主测试需要 schemastery 开发依赖
|
|
153
|
-
node test/smoke.mjs # 客户端半:装配、触发、冷却、静音、重复提醒、通知点击、子智能体过滤
|
|
154
|
-
node test/host.mjs # 宿主半:配置 schema、唤窗路由、标题匹配、两半契约交叉校验
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
发布到 npm:升好版本号后 `npm publish`(首次发布先 `npm login` 并按提示开启 2FA)。**npm 的版本号不可覆盖**——有任何改动都请升版本再发。
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
dsh-plugin-notify-sounds/
|
|
161
|
-
├── package.json # dsh.bundle.patch + dsh.client
|
|
162
|
-
├── cordis.patch.yml # Loader 行:id: notify-sounds
|
|
163
|
-
├── lib/
|
|
164
|
-
│ ├── index.js # Host 半:配置模式 + 点通知唤窗路由
|
|
165
|
-
│ └── client.js # Client 半:音效引擎、状态监听、设置分区
|
|
166
|
-
└── test/
|
|
167
|
-
├── smoke.mjs # 客户端半
|
|
168
|
-
└── host.mjs # 宿主半
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
`package.json` 已按可发布形态声明(`files` 只带运行时产物,`@deepseek-ai/schemastery` 为 peerDependency、另有一份 devDependency 供宿主测试,纯 ESM、无构建步骤),日后发 npm 直接 `npm publish` 即可。
|
|
172
|
-
|
|
173
|
-
## 📄 License
|
|
174
|
-
|
|
175
|
-
[MIT](LICENSE) © BillCx330
|