dsh-tray-flash:让 DSH 在 Windows 上用闪烁提醒你「还有事没处理」

前言

跑 DSH 的人都会碰到一种情况:Agent 跑完一轮或者停下来等你审批、等你回答,但窗口不在前台,你切过去看了一眼侧边栏,黄点绿点还在,事情其实没处理。系统的通知栏不会替你盯着这些点。

dsh-tray-flash 要做的就是这件事:把侧边栏上那个小圆点的状态,映射成 Windows 通知区域托盘图标的隐显闪烁,同时任务栏按钮一起闪。就像 QQ、微信收到消息那样 —— 图标消失又出现,一直到你真正看过、点消失为止。

下面介绍的这个插件由 xymy666 维护,属于 DeepSeek Harness 社区插件目录里的「客户端」分类,仓库地址见文末。

插件定位

一句话:给 DSH 的桌面会话加一个「未读 / 等待中」的常驻视觉提醒。

它分两半,分别跑在不同位置:

半 位置 作用
Host lib/index.js 闪烁、托盘换图、接收点状态
Client lib/client.js 读浏览器里的点,上报给 Host

这个分工决定了它依赖 DSH 的桌面 profile(Electron 应用)才能用,其他平台上插件静默不动作。

它什么时候闪

触发条件有三类:

触发 说明
一轮结束 任何会话的 turn/end,跳过被打断或取消的那一轮
等待审批 工具请求人工批准(approval/asked)
等待回答 模型调用 ask_user_question,含计划审批

不触发的情况有三项:子代理会话(ignoreSubagent,默认开)、后台任务、定时任务。

需要提一句主会话的坑。DSH 源码里主会话永远不会产生绿点,也就是说主会话跑完一轮之后什么点都不会出现,按「点消失才停」的逻辑,闪烁就永远停不下来。插件的处理办法是给主会话记一个虚拟未读(virtualMainDot),窗口重新获得焦点时清掉 —— 这是「你看到这一轮了」最接近的等价物。

它什么时候停

点全部消失的时候。 点是侧边栏会话行前面那个小圆点:

点 含义 谁负责
黄点 有审批 / 计划审批 / 提问在等你 Client 的 pendingInteraction
绿点 后台会话跑完了一轮,你还没看 Client 的 completionUnread

有真实点的时候,焦点、失焦、切换会话、鼠标移动都不能提前结束闪烁,这些只是「碰了一下窗口」。唯一的例外是上一节说的主会话虚拟点,它本来就是用焦点来清掉的。

另外两种情况:

  1. 窗口被关闭或销毁 → 停;
  2. Client 失联(页面没加载,或超过 dotsStaleSeconds 没上报)→ 退回「窗口获得焦点就停」。

没有时间上限。 点挂着就一直闪。默认 maxFlashSeconds: 0,不限时;想要保险值就填正数,例如 maxFlashSeconds: 600。

这里有一个调参教训值得记:早先版本默认上限 120 秒,结果绿点挂超过两分钟就永久停闪了,原因是「点数量不变」不会重新触发闪烁,30 秒心跳报的也是同一个数字,提醒就此丢失,所以默认改成了不限时。

补充一类原规则覆盖不到的情况:窗口本来就在前台,刚完成的就是你正在看的那个会话,此时没有点可等,「窗口在前台」在回合结束前就已经成立,不能算作你已经看过 —— 结果是闪一下就停,盯着屏幕的人反而没有反馈。

flashUntilMouseMoves(默认开)把确认信号从「窗口获得焦点」换成「鼠标动了」。它只在没有真实黄点/绿点时生效:

情况 停止条件
窗口在后台时开始闪,且没有真实点 切回窗口即停
窗口在前台、当前会话完成,且没有真实点 鼠标移动、切换会话、或窗口失焦才停
侧边栏还有黄点/绿点 焦点、失焦、会话切换、鼠标移动都不算数,一直闪到点归零

最后一行是硬规则。0.5.1 曾把鼠标规则套在所有前台触发的闪烁上,结果点还挂在侧边栏,用户点一下主窗口就不闪了;0.5.2 改成真实点优先。鼠标检测走已有的 1 秒轮询,不新增定时器,任意 1 像素移动都算。

托盘图标怎么闪的

DSH 的托盘图标由 Electron 主进程持有,插件通过 Shell_NotifyIconW(NIM_MODIFY) 在原图上就地换图,不新建图标、不动 tooltip 和右键菜单,只设 NIF_ICON。

闪烁是原图标 ↔ 全透明交替。透明那张不是另画的图,而是一个 alpha 全 0 的图标:

node scripts/build-icon.mjs        # 产物:assets/blank.ico

默认图换成全透明之所以成立,关键在于 shell 认的是 alpha 通道,而不是 AND 掩码。作者在本机(Windows 11 26H2)实测过:

写法 结果
alpha 全 0 + AND 掩码全 1 整块变空白
alpha 255 + AND 掩码全 1 渲染成黑方块
1bpp 图标 也变空白,截图哈希与第一种一致

还有一条性质必须确认:透明期间点击是否还能用。实测 Shell_NotifyIconGetRect 在透明帧期内仍返回同一矩形,图标只是看不见,槽位和点击都不受影响。

三条安全性质:

  • DesktopTray 运行期只调用 setToolTip 和 setContextMenu,从不改图标,所以切换语言不会把透明刷回去;
  • 图标 ID 是运行期探测的(用 Shell_NotifyIconGetRect 逐个数试),不硬编码;
  • 应用重启必定重建托盘图标,崩溃也不会留下永久空白图标。

托盘不可用(找不到宿主窗口、图标加载失败、shell 拒绝)时,自动降级为只闪任务栏,日志里只警告一次。

安装与启用

安装前说明一下:desktop profile 由 Electron 应用独占,dsh plugin --profile desktop 会直接报错,插件管理器的 install_bundle 也不一定可用。所以安装等价于 pnpm 的 link:,需要手动做三步。

1、备份现有配置:

Copy-Item "$env:USERPROFILE\.dsh\profiles\desktop\package.json" `
          "$env:USERPROFILE\.dsh\profiles\desktop\package.json.bak-tray-flash"

2、在 dependencies 里加一行,并在 dsh.profile.bundles 末尾追加名字:

"dsh-tray-flash": "link:E:/0_opencode/.00工作空间/dsh-tray-flash"

3、建目录联接,与 pnpm link: 的产物等价:

New-Item -ItemType Junction `
  -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-tray-flash" `
  -Target "E:\0_opencode\.00工作空间\插件制作\dsh-tray-flash"

路径按你实际存放插件的位置替换。

不需要改 ~/.dsh/profiles/desktop/cordis.patch.yml,本包自带的 cordis.patch.yml 会插入自己的行。

改完之后重启 DeepSeek Harness。desktop profile 未开 patchReload,Host 代码必须重启才生效。

回滚:

Copy-Item "$env:USERPROFILE\.dsh\profiles\desktop\package.json.bak-tray-flash" `
          "$env:USERPROFILE\.dsh\profiles\desktop\package.json" -Force
Remove-Item "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-tray-flash" -Force

配置

在 cordis.patch.yml(任意一层)覆盖同名行即可:

- insert:
    - id: tray-flash
      name: dsh-tray-flash
      config:
        stopWhenDotsClear: true   # 点全消失才停(核心行为)
        virtualMainDot: true      # 给主会话造虚拟未读(焦点时清)
        dotsStaleSeconds: 60      # Client 多久没上报就算失联
        trayBlink: true           # 托盘图标隐显闪烁(false 只闪任务栏)
        trayBlinkIntervalMs: 600  # 托盘闪烁半周期(100–5000)
        flashCaption: true        # 标题栏也闪(false 只闪任务栏按钮)
        maxFlashSeconds: 0        # 0 = 跟随点,永不超时(默认);正数 = 保险上限
        onlyWhenUnfocused: true   # 前台就不闪(只在点状态未知时生效)
        flashUntilMouseMoves: true # 前台+当前会话完成时,闪到你动鼠标才停
        ignoreInterrupted: true   # 被打断的一轮不闪
        ignoreSubagent: true      # 子代理不闪
        flashTimeout: 0           # FlashWindowEx 的 dwTimeout(ms)
        flashCount: 0             # FlashWindowEx 的 uCount
        pollIntervalMs: 1000      # 轮询间隔(200–5000)
        allowTestFlash: true      # 注册自检路由
        enabled: true             # 总开关

非法值不会让插件激活失败,会被替换成默认值并在日志里告警一次。

自检

插件默认注册三个只允许本机同源访问的路由:

路由 方法 作用
/__tray-flash/self-test POST 立刻闪一次,返回命中的窗口句柄与配置
/__tray-flash/state GET / POST 读状态 / 停止闪烁
/__tray-flash/dots POST / DELETE Client 上报点数量 / 清零

看点的实时状态:

Invoke-RestMethod http://127.0.0.1:20120/__tray-flash/state | Select-Object -ExpandProperty state |
  Select-Object flashing, lastStop, dots

dots 里能看到 counts(黄/绿点各几个)、virtualMainUnread(主会话虚拟未读是否挂着)、status(live / stale / absent)。

配合 scripts/selftest.mjs 可以做端到端验证:

node scripts/selftest.mjs            # 默认 http://127.0.0.1:36001
node scripts/selftest.mjs 36001

脚本会触发闪烁、连拍任务栏 12 帧、逐像素比对、停止、打印判定。这个脚本需要 DSH 正在运行,会短暂改动真实托盘图标,不在单元测试里。

测试

node --test test/*.test.mjs

仓库里写了 175 个用例,覆盖点跟踪(计数、虚拟未读、失联降级、Client 重启)、点触发闪烁(绿点晚于 turn/end 出现时仍能启动、只在数量增加时启动、已在闪时不重启、点长期不变仍持续闪)、状态机(点消失才停、真实黄/绿点优先于焦点与鼠标、焦点回退、超时仅在手配时生效、去重、子代理与打断过滤、托盘联动)、Client 半(点数、节流、未变化不重发、30 秒心跳、卸载清空、失败静默)、托盘闪烁(id 探测、透明帧、图标生命周期、恢复、降级)、Win32 绑定、配置归一化、koffi 兜底链、本机围栏与路由,以及 Host 入口的激活与释放。

真实环境验证脚本还有三个(probe-tray.mjs、probe-tray-visual.mjs、probe-tray-encoding.mjs),用法见 README。

适用场景与注意

适合这类人:主要在 Windows 上用 DSH 桌面客户端,平时会让 Agent 在后台跑,或者会碰到审批和提问交互,希望不用盯侧边栏也能知道「还有事没处理」。

几点需要知道的限制:

  • 只在 Windows 上有效,其他平台插件静默不动作;
  • 托盘图标闪烁依赖图标 ID 探测,如果同一个进程里有多个托盘图标,只会取探测到的第一个;
  • 闪烁用的是 FlashWindowEx,其返回值不可信,同一次成功调用读回 32768 或 0 都可能出现,真伪只以屏幕是否在闪为准;
  • Host 与窗口目前在同一进程树,靠 process.ppid 找持有主窗口的进程,这个假设在架构变动后需要重审;
  • 插件以当前 DSH 进程权限运行,能访问桌面和通知区域,安装前应自行检查源码与许可证(仓库声明为 MIT)。

关于生态背景:DSH 的理念是「一切皆插件」,社区目录是独立站点,与 DeepSeek / 幻方没有官方从属关系,它不是官方应用商店。

小结

dsh-tray-flash 做的事情很窄:把侧边栏上的黄点和绿点翻译成托盘图标的隐显闪烁和任务栏按钮的闪烁,点全部消失才停。对经常让 Agent 在后台跑的人来说,这个提醒比看窗口焦点可靠。目录页和仓库地址:

  • 目录页:https://www.skillhub.cn/plugins/xymy666/dsh-tray-flash
  • 代码仓库:https://github.com/xymy666/dsh-tray-flash
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜