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
羽毛球分组比赛记分
小程序二维码

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

小夜