给 WSL2 里的 HerDr 加 Windows 原生通知
目标
让 HerDr 的 agent 跑完后,在 Windows 弹一条原生通知。通知能长时间停留,并进入 Windows 通知中心,忙完回头还能翻到。
方案
链路是:HerDr 用 system 方式投递,调用你自写的 notify-send wrapper,wrapper 再通过 powershell.exe 调 WinRT 弹 Windows toast。
选 system 是因为其余三种投递方式在 WSL2 + Windows Terminal 环境下都不可用,原因见下一节。原计划用的 stuartleeks/wsl-notify 仓库已经 404,这条路不需要额外安装,只用 WSL 自带的 powershell.exe 和 Windows 自带的 WinRT API。
背景:为什么其余方式不行
HerDr 的投递方式在 config.toml 的 [ui.toast] 段,delivery 有四种取值:off、herdr、terminal、system。
- herdr:默认值,在 TUI 界面里画一个气泡,停留时间短,切到别的窗口就看不到。
- terminal:发 OSC 9 终端通知序列,靠终端弹系统通知。投递前会经过 detect_backend(),它只认 Ghostty、iTerm2、Kitty、WezTerm。Windows Terminal 不在白名单里,结果是静默失败,不报任何错误。
- BEL 闪烁:由 BEL 字符(\a,十六进制 0x07)触发,是终端对控制字符的响应,和通知系统无关。HerDr 0.8.0 不转发 pane 程序写出的 BEL(bug #2453,已修未发布),也不会在 agent 完成时主动发 BEL(issue #153 被官方关闭)。
- system:Linux 下等价于 spawn
notify-send -- <title> [body],前置条件是环境里有 DISPLAY 或 WAYLAND_DISPLAY。WSL2 里没有桌面会话和 D-Bus 通知守护进程,直接调用没人接。但「system 只调 notify-send」这个事实可以拿来拦截:在 PATH 里放一个自己的 notify-send,盖过系统自带的。
步骤
1. 打开 system 投递
编辑 HerDr 的 config.toml:
[ui.toast]
delivery = "system"
delay_seconds = 1
保存后执行 herdr server reload-config 让 HerDr 重新加载配置。
这一步让 HerDr 在 agent 完成时调用 notify-send,而不是发 TUI 气泡或 OSC 9 序列。
2. 写 notify-send wrapper
把脚本放到 ~/.local/bin/notify-send,并保证这个目录在 $PATH 里排在 /usr/bin 之前。这样 HerDr 的 server 进程和 client 进程调 notify-send 时都会命中它,而不是系统自带的。
写完执行 chmod +x ~/.local/bin/notify-send。
#!/usr/bin/env bash
# notify-send bridge: forward Herdr system-delivery notifications to Windows toast.
# Herdr calls: notify-send -- <title> [body]
set -u
PS_SCRIPT='C:\Users\Administrator\AppData\Local\Herdr\herdr-toast.ps1'
args=()
for a in "$@"; do
[ "$a" != "--" ] && args+=("$a")
done
title="${args[0]:-Notification}"
body="${args[1]:-}"
# PowerShell single-quote escape: ' -> ''
ps_t=$(printf '%s' "$title" | sed "s/'/''/g")
ps_b=$(printf '%s' "$body" | sed "s/'/''/g")
cmd="& '${PS_SCRIPT}' -Title '${ps_t}' -Body '${ps_b}'"
b64=$(printf '%s' "$cmd" | iconv -f UTF-8 -t UTF-16LE | base64 -w0)
powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$b64" </dev/null >/dev/null 2>&1
exit 0
这个 wrapper 做三件事:
- 去掉参数里的
--,取出 title 和 body。 - 把 title 和 body 里的单引号转义成
'',避免破坏 PowerShell 的引号。 - 用 iconv 把整段命令转成 UTF-16LE,base64 编码后通过
-EncodedCommand传给 powershell.exe。这样绕开命令行转义,中文和特殊字符不会被吃掉。
末尾无条件 exit 0,让 HerDr 认为投递成功。副作用见注意事项。
3. 写 Windows 侧的 toast 脚本
把脚本放到 C:\Users\Administrator\AppData\Local\Herdr\herdr-toast.ps1。用户名换成你自己的,这里以 Administrator 为例。
param([string]$Title = "Herdr Notification", [string]$Body = "")
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
function Escape-Xml([string]$s) {
if ($null -eq $s) { return "" }
return $s -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"' -replace "'",'''
}
$appId = 'Herdr'
$t = Escape-Xml $Title
$b = Escape-Xml $Body
$xmlText = "<toast><visual><binding template='ToastGeneric'><text>$t</text><text>$b</text></binding></visual></toast>"
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($xmlText)
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
这个脚本加载 WinRT 类型,把标题和正文做 XML 转义,拼出 ToastGeneric 模板的 XML,再用 CreateToastNotifier('Herdr') 弹出去。appId 用 Herdr,方便下一步配图标。
4. 注册 AppUserModelId 和图标
不配图标时,通知会显示 PowerShell 的蓝色图标。在注册表给 Herdr 这个 AppUserModelId 注册显示名和图标,管理员 PowerShell 里执行:
New-Item -Path 'HKCU:\Software\Classes\AppUserModelId\Herdr' -Force | Out-Null
Set-ItemProperty -Path 'HKCU:\Software\Classes\AppUserModelId\Herdr' -Name 'DisplayName' -Value 'Herdr' -Type String
Set-ItemProperty -Path 'HKCU:\Software\Classes\AppUserModelId\Herdr' -Name 'IconUri' -Value 'file:///C:/Users/Administrator/AppData/Local/Herdr/herdr-logo-48.png' -Type String
图标文件:把 HerDr 自带的 assets/logo.png(512×512)缩成 48×48 或 64×64 的 PNG,放到 C:\Users\Administrator\AppData\Local\Herdr\ 下,和 IconUri 指向的路径一致。
验证
- 执行
herdr notification show,弹出 Windows 原生 toast,App 名是 Herdr,带 logo 图标。 - 让一个 agent 在后台 workspace 跑完,toast 自动弹出并进入 Windows 通知中心。
注意事项
- wrapper 必须带
-ExecutionPolicy Bypass。Windows PowerShell 默认执行策略是 Restricted,会拒绝执行 .ps1 脚本。wrapper 末尾无条件 exit 0 会掩盖这个失败,HerDr 看到投递成功,toast 却没弹。排查时把>/dev/null 2>&1去掉,直接看 PowerShell 的 stderr。 - detach 退出 TUI 后不弹通知。notify-send 由 client(TUI)进程调用,没有 client 就没有人发。只有常驻 attach 的场景才适用。
- 通知只在后台 workspace 或非激活 tab 完成时触发,激活的 tab 被抑制,由 active_tab_suppresses_notifications 控制。
shown: true只代表投递函数返回成功,不代表 toast 真的显示。链路里任何一环失败都可能显示成功但没弹,验证要用肉眼确认。

浙公网安备 33010602011771号