dsh-notify-tone 0.2.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +49 -10
  2. package/lib/client.js +108 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-notify-tone(声音 + 视觉 + 系统通知提醒插件)
2
2
 
3
+ > 本项目由 **DeepSeek** 贡献(协助设计、开发与测试)
4
+
3
5
  dsh web GUI 的提醒插件:
4
6
 
5
7
  - **需要操作提醒**:AI 执行任务中遇到需要用户授权或选择时(沙箱权限授权弹窗、`ask_user_question` 提问、计划审批等),同时触发提示音、屏幕光效与系统通知,提醒你回到窗口处理。
@@ -11,14 +13,19 @@ dsh web GUI 的提醒插件:
11
13
  - **视觉提醒**:屏幕四周大圆弧光带 + 泡泡式弹性向内挤压脉冲(柔和、不遮挡、不拦截鼠标);「需要操作」「回答完成」**各自独立配色**(8 种预设 + 🌈 自定义色相),选色即时预览。
12
14
  - **系统通知**:触发时弹出**操作系统级桌面通知**(含系统声音),**在浏览器外、其他窗口、浏览器最小化时也能看到**;点击通知可跳回 dsh 窗口。首次使用需在浏览器中授权(打开开关时自动请求)。
13
15
  - **分别开关**:总开关、声音开关、视觉开关、系统通知开关**四者独立**,互不影响。
14
- - **持久生效**:所有开关、音色、颜色都存 `localStorage`,重启后保持;旧版设置(`dsh.notify-sound.*`)自动迁移。
16
+ - **🔔 按钮可拖动**:按住 🔔 拖动即可移动到任意位置(不与其他插件按钮冲突),位置自动记忆,**重启后保持**。
17
+ - **菜单方向自适应**:按钮靠近屏幕边缘时,设置菜单自动选择向上/向下弹出,**不会被裁剪、不遮挡按钮**。
18
+ - **持久生效**:所有开关、音色、颜色、按钮位置都存 `localStorage`,重启后保持;旧版设置(`dsh.notify-sound.*`)自动迁移。
15
19
  - **悬浮设置菜单**:鼠标悬停页面右下角 🔔 按钮自动弹出:总开关、声音/视觉/系统通知分别开关、音色选择(点击试听)、颜色选择(展开面板,点击预览)。
16
20
  - **不侵入**:不改动 dsh 任何原有组件与第三方插件;插件只做「监听会话状态 + 发声 + 视觉层 + 系统通知 + 一个悬浮按钮/菜单」。
17
21
  ![alt text](/image_1.jpg)
18
22
  ![alt text](/image_3.jpg)
19
23
  ![alt text](/image_2.jpg)
20
24
 
21
- ## 安装
25
+ ## 安装
26
+
27
+ ### 前置
28
+ 已安装DeepSeek Harness,`dsh web`能正常启动
22
29
 
23
30
  ### 方式一:npm 安装(已发布到npm)
24
31
 
@@ -40,14 +47,8 @@ dsh plugin --profile web add link:/绝对路径/dsh-notify-tone
40
47
 
41
48
  安装后**重启 dsh**(完全退出 `dsh web` 进程再启动)即生效。
42
49
 
43
- 本机当前以本地目录方式挂载:包位于 `~/.dsh/profiles/web/node_modules/dsh-notify-tone/`,
44
- 启用条目在 `~/.dsh/profiles/web/cordis.patch.yml`:
45
-
46
- ```yaml
47
- - insert:
48
- - id: notify-tone
49
- name: 'dsh-notify-tone'
50
- ```
50
+ > 说明:本插件通过 npm 安装后,由 dsh 自动登记进 profile 的 `dsh.profile.bundles` 并激活,
51
+ > 无需手动修改 `cordis.patch.yml`。
51
52
 
52
53
  ## 卸载
53
54
 
@@ -58,4 +59,42 @@ dsh plugin --profile web add link:/绝对路径/dsh-notify-tone
58
59
 
59
60
  - 听不到声音:浏览器自动播放策略要求页面至少被点击过一次;点击页面任意位置后再试。
60
61
  - 收不到系统通知:① 打开 🔔 菜单「系统通知」开关并允许浏览器的授权询问;② 若显示"权限已被禁用",需到浏览器站点设置(地址栏左侧锁图标 → 网站设置)中把「通知」改为允许;③ Windows 还需确认系统"通知和操作"未对浏览器静音。
62
+ - **通知横幅不弹出、直接进通知中心**:这是浏览器/系统的正常行为——**当 dsh 页面正处于前台时,浏览器不会弹横幅**(你正在看页面,横幅会打扰),通知会直接收进通知中心;**切到其他应用/桌面后,通知才会以横幅 + 系统提示音弹出**。若你希望在页面前台时也看到横幅,可尝试:Windows 设置 → 系统 → 通知 → 应用列表中找到浏览器 → 确认「显示通知横幅」已开启。
61
63
  - 若之后运行过 `pnpm install` 导致手动放置的包被清理,请重新复制本目录到 `~/.dsh/profiles/web/node_modules/`(或在 profile 目录执行 `dsh plugin --profile web add link:<本目录绝对路径>`)。
64
+
65
+ ### pnpm 11 + dsh-better-sidebar:安装后插件不生效
66
+
67
+ **现象**:执行 `dsh plugin --profile web add dsh-notify-tone` 时提示安装成功(`+ dsh-notify-tone ^0.2.0`),但末尾报
68
+ `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: node-pty@1.1.0` 和 `dsh: pnpm failed ...`;
69
+ 重启 dsh 后插件没有出现。
70
+
71
+ **原因**:pnpm 11 的安全策略默认**阻止依赖运行构建脚本**。`node-pty` 是
72
+ `dsh-better-sidebar`(另一个第三方插件)的依赖,与本插件无关;当它的构建脚本被忽略时,
73
+ pnpm 会以**非零退出码**结束,而 dsh 只在 pnpm 成功(退出码 0)时才把插件同步进
74
+ `dsh.profile.bundles`,导致本插件依赖已装、但未进入 bundles 层、不加载。
75
+
76
+ **解决**:编辑 profile 的 `pnpm-workspace.yaml`(`~/.dsh/profiles/web/pnpm-workspace.yaml`),
77
+ 在 `allowBuilds:` 下把 node-pty 设为允许构建:
78
+
79
+ ```yaml
80
+ allowBuilds:
81
+ node-pty: true
82
+ ```
83
+
84
+ (如果该行是占位文本 `node-pty: set this to true or false`,直接改成 `true`;
85
+ 也可以运行 pnpm 提示的 `pnpm approve-builds` 交互式批准。)然后**重新执行**:
86
+
87
+ ```bash
88
+ dsh plugin --profile web add dsh-notify-tone
89
+ ```
90
+
91
+ 确认输出末尾为 `Done`、无 `ERR_PNPM_IGNORED_BUILDS` 后重启 dsh 即可。
92
+
93
+ > 本插件自身零依赖、无构建脚本、纯浏览器端,这个问题只在你同时安装
94
+ > `dsh-better-sidebar` 且使用 pnpm 11 时可能出现。
95
+
96
+ ## 致谢
97
+
98
+ 本项目由 **DeepSeek** 贡献:功能设计、代码实现、测试验证与文档编写均由 DeepSeek 协助完成,作者负责需求提出与验收。
99
+
100
+ MIT License
package/lib/client.js CHANGED
@@ -38,6 +38,7 @@ window.__ModuleLoader__.load({
38
38
  const MENU_ID = "dsh-notify-tone-menu";
39
39
  const FX_ID = "dsh-notify-tone-fx";
40
40
  const FX_STYLE_ID = "dsh-notify-tone-fx-style";
41
+ const KEY_POSITION = "dsh.notify-tone.ui.position"; // 🔔 按钮位置(JSON: {left, top})
41
42
 
42
43
  // ===================== 提示音预设 =====================
43
44
  const PRESETS = {
@@ -670,16 +671,118 @@ window.__ModuleLoader__.load({
670
671
  return;
671
672
  }
672
673
 
673
- const root = el("div", "", "position:fixed;right:18px;bottom:18px;z-index:2147483001;display:flex;flex-direction:column;align-items:flex-end;");
674
+ // touch-action:none 防止触屏拖动时页面滚动
675
+ const root = el("div", "", "position:fixed;right:18px;bottom:18px;z-index:2147483001;display:flex;flex-direction:column;align-items:flex-end;touch-action:none;");
676
+ root.id = ROOT_ID; // 必须设置 id:幂等检查 getElementById(ROOT_ID) 依赖它,否则重复 apply 会挂出多个铃铛
674
677
 
675
- const menu = el("div", "", "display:none;margin-bottom:8px;min-width:240px;max-height:64vh;overflow-y:auto;background:rgba(24,24,30,0.96);border:1px solid rgba(128,128,128,0.3);border-radius:10px;padding:10px 12px;box-shadow:0 6px 24px rgba(0,0,0,0.45);backdrop-filter:blur(6px);color:#e8e8ea;");
678
+ // 菜单绝对定位(相对 root),由 positionMenu() 动态决定向上/向下弹出,
679
+ // 避免按钮靠近屏幕边缘时菜单被裁剪或遮挡按钮
680
+ const menu = el("div", "", "display:none;position:absolute;right:0;min-width:240px;max-height:64vh;overflow-y:auto;background:rgba(24,24,30,0.96);border:1px solid rgba(128,128,128,0.3);border-radius:10px;padding:10px 12px;box-shadow:0 6px 24px rgba(0,0,0,0.45);backdrop-filter:blur(6px);color:#e8e8ea;");
676
681
  root.appendChild(menu);
677
682
 
678
- const btn = el("button", "🔔", "width:38px;height:38px;border-radius:50%;border:1px solid rgba(128,128,128,0.35);background:rgba(28,28,34,0.75);color:#fff;font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,0.35);backdrop-filter:blur(4px);transition:opacity .15s ease;");
683
+ const btn = el("button", "🔔", "width:38px;height:38px;border-radius:50%;border:1px solid rgba(128,128,128,0.35);background:rgba(28,28,34,0.75);color:#fff;font-size:18px;line-height:1;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,0.35);backdrop-filter:blur(4px);transition:opacity .15s ease;");
679
684
  btn.id = TOGGLE_ID;
680
685
  btn.type = "button";
686
+ btn.title = "拖动可移动位置;点击开/关;悬停打开设置菜单";
687
+
688
+ let hideTimer = null;
689
+ let dragState = null;
690
+ let suppressClick = false;
691
+
692
+ // 根据按钮位置决定菜单弹出方向:上方空间不足则向下弹出
693
+ function positionMenu() {
694
+ if (typeof window === "undefined") return;
695
+ const vh = window.innerHeight || 0;
696
+ const rect = typeof btn.getBoundingClientRect === "function" ? btn.getBoundingClientRect() : null;
697
+ const spaceAbove = rect ? rect.top : vh;
698
+ const spaceBelow = rect ? vh - rect.bottom : 0;
699
+ const menuMaxH = (vh * 64) / 100; // 与 max-height 对齐
700
+ if (spaceAbove < menuMaxH && spaceBelow > spaceAbove) {
701
+ // 上方空间不足且下方更大 → 向下弹出
702
+ menu.style.top = "calc(100% + 8px)";
703
+ menu.style.bottom = "auto";
704
+ } else {
705
+ // 默认向上弹出
706
+ menu.style.top = "auto";
707
+ menu.style.bottom = "calc(100% + 8px)";
708
+ }
709
+ }
710
+
711
+ // 恢复上次保存的位置(持久化)
712
+ function applyStoredPosition() {
713
+ try {
714
+ const raw = storageGet(KEY_POSITION, "");
715
+ if (!raw) return;
716
+ const pos = JSON.parse(raw);
717
+ if (Number.isFinite(pos.left) && Number.isFinite(pos.top)) {
718
+ root.style.left = pos.left + "px";
719
+ root.style.top = pos.top + "px";
720
+ root.style.right = "auto";
721
+ root.style.bottom = "auto";
722
+ }
723
+ } catch {
724
+ /* 位置数据损坏时忽略,使用默认位置 */
725
+ }
726
+ }
727
+
728
+ // —— 🔔 拖拽移动(Pointer Events,鼠标/触屏通用)——
729
+ btn.addEventListener("pointerdown", (event) => {
730
+ if (event.button !== undefined && event.button !== 0) return; // 仅左键/触摸
731
+ event.preventDefault();
732
+ clearTimeout(hideTimer); // 拖动时收起菜单
733
+ menu.style.display = "none";
734
+ expandedColor = null;
735
+ dragState = {
736
+ startX: event.clientX,
737
+ startY: event.clientY,
738
+ left: root.offsetLeft,
739
+ top: root.offsetTop,
740
+ moved: false,
741
+ };
742
+ btn.style.cursor = "grabbing";
743
+ if (typeof btn.setPointerCapture === "function") {
744
+ try { btn.setPointerCapture(event.pointerId); } catch { /* 忽略 */ }
745
+ }
746
+ });
747
+
748
+ btn.addEventListener("pointermove", (event) => {
749
+ if (!dragState) return;
750
+ event.preventDefault();
751
+ const dx = event.clientX - dragState.startX;
752
+ const dy = event.clientY - dragState.startY;
753
+ if (!dragState.moved && Math.hypot(dx, dy) < 4) return; // 4px 内视为点击
754
+ dragState.moved = true;
755
+ // 限制在可视区域内
756
+ const vw = (typeof window !== "undefined" && window.innerWidth) || 0;
757
+ const vh = (typeof window !== "undefined" && window.innerHeight) || 0;
758
+ const bw = root.offsetWidth || 40;
759
+ const bh = root.offsetHeight || 40;
760
+ const left = Math.min(Math.max(0, dragState.left + dx), Math.max(0, vw - bw));
761
+ const top = Math.min(Math.max(0, dragState.top + dy), Math.max(0, vh - bh));
762
+ root.style.left = left + "px";
763
+ root.style.top = top + "px";
764
+ root.style.right = "auto";
765
+ root.style.bottom = "auto";
766
+ });
767
+
768
+ function endDrag() {
769
+ if (!dragState) return;
770
+ const moved = dragState.moved;
771
+ dragState = null;
772
+ btn.style.cursor = "grab";
773
+ if (moved) {
774
+ suppressClick = true; // 拖动后抑制随后的 click(避免误切换开关)
775
+ try {
776
+ storageSet(KEY_POSITION, JSON.stringify({ left: root.offsetLeft, top: root.offsetTop }));
777
+ } catch { /* 忽略 */ }
778
+ }
779
+ }
780
+ btn.addEventListener("pointerup", endDrag);
781
+ btn.addEventListener("pointercancel", endDrag);
782
+
681
783
  btn.addEventListener("click", (event) => {
682
784
  event.stopPropagation();
785
+ if (suppressClick) { suppressClick = false; return; } // 拖拽结束后的 click 不切换开关
683
786
  const next = !isEnabled();
684
787
  setEnabled(next);
685
788
  updateToggle(btn);
@@ -693,10 +796,11 @@ window.__ModuleLoader__.load({
693
796
  root.appendChild(btn);
694
797
 
695
798
  document.body.appendChild(root);
799
+ applyStoredPosition(); // 恢复持久化的位置
696
800
 
697
- let hideTimer = null;
698
801
  root.addEventListener("mouseenter", () => {
699
802
  clearTimeout(hideTimer);
803
+ positionMenu(); // 先根据按钮位置决定弹出方向,再显示
700
804
  renderMenu(menu, btn);
701
805
  menu.style.display = "block";
702
806
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-tone",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "dsh web GUI 提示音插件:AI 需要用户授权/选择时与每轮回答结束时播放提示音提醒用户。多套内置音色可选(经典双音/清脆叮咚/明亮三连/柔和轻音),悬停铃铛按钮可开关、选音色并试听,状态持久化。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",