# 设计说明 ## 为什么只剩浏览器半身 插件原先让宿主进程用 PowerShell 播放 `C:\Windows\Media` 下的 wav。实测下来,实际听到的提示音是系统通知自带的音效,宿主那一侧没有可感知的效果,因此整条宿主播音链路已删除,插件只保留浏览器半身。 宿主入口文件仍然存在,但没有任何运行时行为。客户端模块系统按 Loader entry 扫描 `dsh.client` 声明,entry 指向的宿主模块必须存在,浏览器半身才会被加载,因此 `src/index.ts` 只导出插件名和一个空的 `apply`。 ## 触发信号 浏览器半身盯两个快照源: | 信号 | 来源 | 触发条件 | | --- | --- | --- | | 等待回复 | `ctx.uiSession.sessionStatus` | 某会话出现新 key 的交互,question、plan-review、approval 都算 | | 一轮跑完 | `ctx.sessions.list` 的 `byId[].running` | 某会话运行态从真落到假 | 两者都是可订阅的只读快照,插件不修改任何状态,只观察。 ## 去重与节流 按用户选择,两种触发都要响,且不判断页面是否聚焦、不区分是否为当前会话,所以「响太多次」是主要风险,靠三道闸门控制。 1. **首帧只建基线**:初次读到的快照只记录不发声,避免每次刷新页面补报一轮旧状态。 2. **同一待回答请求只响一次**:按交互的稳定 key 记忆,消失后清理记录但不发声。 3. **滚动最小间隔 800 毫秒**:两次响铃之间至少间隔这么久。窗口内到达的请求不丢弃,而是挂到窗口边界的定时器上补发,保证不漏报。该时刻写入 localStorage 跨标签页共享,多开标签页时同一件事只响一轮。 ## goal 自动续跑时的通知 会话带一个处于 active 的目标时,DSH 会在轮与轮之间自动接着跑,每个间隙都会落一次空闲。这个空闲报「完成了一轮」会让人误以为可以回来看了,但它确实值得让人知道,于是改为一条临时提示:标题「DSH 目标进行中」,正文带会话标题与已跑轮数,并且不设 requireInteraction,横幅按系统默认时长自动消失,不在系统通知中心里长期占位。 判定看会话摘要里的 `projectionValues.goal`:持久阶段是 active 就走这条路径,否则仍然是「完成了一轮」的常驻通知,等对应会话被操作才撤。等待回答的通知不受影响,目标模式里模型提问照样响。 goal 投影随会话控制流一起到达列表快照,因此读到的就是当下的阶段;投影值缺失或没有目标时按普通会话处理。 ## 通知的存活与关闭 通知分两类。要等人处理的「完成了一轮」与「需要你回答」不设自动关闭时间,并且带上 `requireInteraction`,会一直留在系统通知里,直到对应会话发生操作;目标自动续跑时的「目标进行中」是临时提示,不要求常驻,横幅按系统默认时长自动消失。插件按会话 id 记录当前挂着的通知,每个会话最多一条,新的先关掉旧的。 以下任一情况出现,就算对应会话得到了一次操作,插件会关掉它的通知: 1. **会话被选中**:`sessions.list.current` 变成了该会话。 2. **开始新一轮**:该会话的运行态从假升到真,说明用户回到了这里。 3. **待回答被解决**:该会话的待回答交互 key 消失;被新 key 顶掉也算,替换场景下先关旧通知再响新的一条,不会误关。 4. **会话被移除**:该会话从列表消失。 选中信号有两处细节。`current` 短暂变成 `undefined` 是 DSH 的 masked gap,选中会话暂时不在列表时就会出现,不算一次操作,也不覆盖上一个有效选中。选中变化先于响铃处理,因此同一帧里既跑完又刚被选中的会话,通知不会被建立后立刻关掉。 点击通知会先聚焦窗口,再经 `ctx.sessions.open(sessionId)` 切到该会话,然后关掉这条通知;会话已不在列表时只聚焦、不切换。插件卸载时会把还挂着的通知全部关掉。 只识别这四种状态信号,因此通知只有在 DSH 里真的对那个会话动了手才会消失;单纯回到浏览器窗口不会关掉它。 ## 后台任务提示 一轮对话跑完时后台任务常常还在跑,只报「已空闲」会让人以为全都干完了,因此该会话还有运行中的任务时,完成通知的正文用数量摘要替换「已空闲」,次一级的位置列出任务详情的前几个: ``` 测试会话 后台还有 2 个任务 bash-3 构建产物、subagent-1 调研 ``` 等待回答那条通知不带任务信息。 名册来自可选的 `ctx.jobs`,它是 dsh-api-job-controller 的客户端半身,按会话引用计数的 `job.list` 流提供「该会话看得到的任务集合」。插件在会话出现在列表里时持有引用、会话从列表消失时释放,因此在触发那一刻读到的就是当下的名册。 只取 owner 等于该会话的任务。无主任务会出现在每个会话的名册里,若一并列入,每条通知都会带上同一批名字。数量行报总数,详情行最多列前三个任务,每个用 id 加 producer 提供的 label,label 超过 24 字取头截断,多余的任务只在末尾报总数。 `ctx.jobs` 用 `ctx.get` 按可选服务读取,`dsh.client.inject` 只保证加载顺序。组合里没有 job-controller 时照旧报「已空闲」。 ## 全屏静默 Windows 11 的「专注」有一条自动规则「当我在全屏使用应用时」,全屏看视频或玩游戏时会自动进入专注状态,通知横幅不显示、通知音被静音,通知只落进通知中心。插件不自己发声,提示音就是系统通知自带的音效,所以这种场合没有任何声音提示。 ## 已知边界 - 浏览器半身依赖 `uiSession` 与 `sessions`,缺任一则不激活。 - 后台任务摘要依赖组合里的 job-controller,缺失时通知照发,完成通知照旧报「已空闲」。 - 「一轮跑完」只覆盖正常结束与被打断这两种空闲落点,插件不区分具体结束原因。目标因达到 maxGoalRounds 上限而停下时持久阶段仍是 active,插件读不到进程本地的 activation,这一轮只会报一条「目标进行中」的临时提示,不会报「完成了一轮」;暂停、阻塞、完成这三种终态都照常报完成。 - 系统通知被静默时横幅和声音一起消失,插件察觉不到,也没有补偿手段。 - 页面被关掉后,浏览器通知可能仍留在系统通知中心,此时插件已不在运行,无法再收回,只能手动清掉。