# 设计说明

## 为什么只剩浏览器半身

插件原先让宿主进程用 PowerShell 播放 `C:\Windows\Media` 下的 wav。实测下来，实际听到的提示音是系统通知自带的音效，宿主那一侧没有可感知的效果，因此整条宿主播音链路已删除，插件只保留浏览器半身。

宿主入口文件仍然存在，但没有任何运行时行为。客户端模块系统按 Loader entry 扫描 `dsh.client` 声明，entry 指向的宿主模块必须存在，浏览器半身才会被加载，因此 `src/index.ts` 只导出插件名和一个空的 `apply`。

## 触发信号

浏览器半身盯两个快照源：

| 信号 | 来源 | 触发条件 |
| --- | --- | --- |
| 待人工回答 | `ctx.uiSession.pendingInteractions` | 某会话出现新 key 的交互，question、plan-review、approval 都算 |
| 一轮跑完 | `ctx.sessions.list` 的 `byId[].running` | 某会话运行态从真落到假 |

两者都是可订阅的只读快照，插件不修改任何状态，只观察。

## 去重与节流

按用户选择，两种触发都要响，且不判断页面是否聚焦、不区分是否为当前会话，所以「响太多次」是主要风险，靠三道闸门控制。

1. **首帧只建基线**：初次读到的快照只记录不发声，避免每次刷新页面补报一轮旧状态。
2. **同一待回答请求只响一次**：按交互的稳定 key 记忆，消失后清理记录但不发声。
3. **滚动最小间隔 800 毫秒**：两次响铃之间至少间隔这么久。窗口内到达的请求不丢弃，而是挂到窗口边界的定时器上补发，保证不漏报。该时刻写入 localStorage 跨标签页共享，多开标签页时同一件事只响一轮。

## 通知的存活与关闭

浏览器系统通知不再设自动关闭时间，并且带上 `requireInteraction`，会一直留在系统通知里，直到对应会话发生操作。插件按会话 id 记录当前挂着的通知，每个会话最多一条，新的先关掉旧的。

以下任一情况出现，就算对应会话得到了一次操作，插件会关掉它的通知：

1. **会话被选中**：`sessions.list.current` 变成了该会话。
2. **开始新一轮**：该会话的运行态从假升到真，说明用户回到了这里。
3. **待回答被解决**：该会话的待回答交互 key 消失；被新 key 顶掉也算，替换场景下先关旧通知再响新的一条，不会误关。
4. **会话被移除**：该会话从列表消失。

选中信号有两处细节。`current` 短暂变成 `undefined` 是 DSH 的 masked gap，选中会话暂时不在列表时就会出现，不算一次操作，也不覆盖上一个有效选中。选中变化先于响铃处理，因此同一帧里既跑完又刚被选中的会话，通知不会被建立后立刻关掉。

点击通知会先聚焦窗口，再经 `ctx.sessions.open(sessionId)` 切到该会话，然后关掉这条通知；会话已不在列表时只聚焦、不切换。插件卸载时会把还挂着的通知全部关掉。

只识别这四种状态信号，因此通知只有在 DSH 里真的对那个会话动了手才会消失；单纯回到浏览器窗口不会关掉它。

## 全屏静默

Windows 11 的「专注」有一条自动规则「当我在全屏使用应用时」，全屏看视频或玩游戏时会自动进入专注状态，通知横幅不显示、通知音被静音，通知只落进通知中心。插件不自己发声，提示音就是系统通知自带的音效，所以这种场合没有任何声音提示。

## 已知边界

- 浏览器半身依赖 `uiSession` 与 `sessions`，缺任一则不激活。
- 「一轮跑完」只覆盖正常结束与被打断这两种空闲落点，插件不区分具体结束原因。
- 系统通知被静默时横幅和声音一起消失，插件察觉不到，也没有补偿手段。
- 页面被关掉后，浏览器通知可能仍留在系统通知中心，此时插件已不在运行，无法再收回，只能手动清掉。
