dsh-desktop-notify
English | 中文
给 DeepSeek Harness 加上桌面通知。
会话需要你时弹出 Windows 通知,点一下就能跳回那个会话。
两种触发时机:
| 触发 | 何时弹出 |
|---|---|
| 需要你介入 | Agent 卡在等你授权,或等你回答问题。 |
| 跑完了 | 一个正在运行的会话停了下来,且这一轮至少跑了 minRunMs。 |
安装
需要桌面端应用。通知这条链路依赖 Electron 运行渲染进程;浏览器标签页没有
AppUserModelID,没身份可挂。
在插件页面里装(不用开终端)
打开 设置 → 插件 → 添加插件,在「包名或地址」输入框里粘下面任一种:
| 粘什么 | 什么时候用 |
|---|---|
github:guanhaisen/dsh-desktop-notify |
直接从本仓库装。不需要先发布到 npm。 |
https://github.com/guanhaisen/dsh-desktop-notify |
同上,写成网址形式。 |
D:\path\to\dsh-desktop-notify |
装本机已有的代码目录。必须是绝对路径。 |
然后点「安装」。插件管理器会在你的 profile 里执行 pnpm add、把 bundle 加进
profile 的清单,通知功能通常不用重启就生效 —— 如果第一枚通知没来,按 Ctrl+R
刷新一下页面。
> 从 git 安装取的是仓库的默认分支。如果你 fork 之后默认分支不是 main,
> 就把完整地址带 ref 粘进去:github:你/dsh-desktop-notify#main。
> 插件页面明确提示:已安装的插件暂不支持自动更新,要升级就卸载后重新安装。
> github: 形式跟随默认分支,所以重装即可拿到新提交。
从命令行装
# 在 DSH 桌面 profile 目录里执行:
# Windows: %USERPROFILE%\.dsh\profiles\desktop
pnpm add github:guanhaisen/dsh-desktop-notify
或者 link 一份工作副本,改完代码保存即生效:
pnpm add link:/path/to/dsh-desktop-notify
无论哪种方式,包名都必须出现在该目录 package.json 的 bundle 列表里 ——
插件页面会自动加,命令行安装不会:
{
"dsh": {
"profile": {
"bundles": ["...已有的 bundle...", "dsh-desktop-notify"]
}
}
}
> 必须保持 dependencies 为空:pnpm 不会安装被 link 的包自己的依赖,
> 一旦解析失败,Cordis 插件树初始化会整体抛错,DSH 直接起不来。
这些情况下不会弹通知
这一段看起来像 bug,其实是设计:
| 情况 | 为什么静默 |
|---|---|
| 你正看着那个会话 | 提示已经在眼前了,再弹是打扰。 |
这一轮跑得比 minRunMs(20 秒)短 |
快速回答不值得通知。 |
| 开窗时那个会话本来就在跑 | 首次快照只做基线,否则每次启动都炸一堆通知。 |
| Windows 开了勿扰模式 | 系统会拦掉横幅。通知仍会进通知中心。 |
如果这些都不符合、却还是没有通知,去看决策日志 —— 见
验证它真的能用。它把每一次"没弹"连同原因都记下来了,
所以"为什么我没收到通知"是能查的。
为什么这是一个插件,而不是改外壳
最直觉的做法 —— 在 Electron 主进程里加一个 Notification 调用 —— 在这里行不通,
而想清楚为什么,整个设计就出来了。
桌面端 Host 不是 Electron 主进程。 lib/main.js 把它作为一个独立的 Node 子进程
拉起来:
spawn(this.node, ['--expose-internals', entry, ...], { stdio: ['ignore','pipe','pipe','ipc'] })
这个子进程是 Electron 可执行文件以 Node 模式重新进入的,所以 Host 插件里
require('electron') 拿不到 Notification,那个进程也没有 AppUserModelID。
而外壳自己的通知代码(lib/main.js)只用在一件事上:强制更新。
但渲染进程什么都有:
dsh-app:注册时带了standard: true和secure: true,所以窗口是安全上下文 ——
这是 Web Notification API 的硬性前提。- 应用 session 装了权限处理器,对所有非 media 权限直接返回
true。
于是渲染进程里一个普通的 Web Notification,就是一枚真正的 Windows 通知,
且挂在应用自己的身份(electron.app.DeepSeek Harness)下。不用改外壳、不用辅助进程,
应用升级也不会失效。
状态从哪来
不轮询,也不另造一份真相。@deepseek-ai/dsh-client-ui-session 已经在维护的正好就是
这几个事实,并作为 ctx.uiSession.sessionStatus 暴露出来:
Map
数据源是 api-session/status 这个 Remote 事件。本插件只读它。
pendingInteraction 就是这个交互对象本身,它的 kind 是三者之一:
kind |
由谁产生 | 显示的文案 |
|---|---|---|
approval |
@deepseek-ai/dsh-client-ui-approval |
某个工具请求越权执行 |
question |
@deepseek-ai/dsh-client-ui-user-questions |
Agent 提了一个问题 |
plan-review |
同上,当问题实际是一份计划时 | 有份计划等你审阅 |
它的 key 是用来去重的标识,所以同一个等待被重复上报时不会弹第二次。
文件构成
| 文件 | 作用 |
|---|---|
index.js |
Host 半。只占住浏览器半绑定的那个 Loader 行。不注册任何东西 —— 没有工具、没有服务、没有路由。 |
client.js |
浏览器半。监听会话状态、判定何时通知、渲染通知、接好点击跳转,并发布 DesktopNotify Inspect provider。 |
cordis.patch.yml |
bundle 插入的那一行 Host 记录。 |
test/watcher.test.mjs |
决策逻辑的行为测试,纯 node 就能跑。 |
配置
在 profile patch 里该插件的那一行设置,或在「插件」页面里改:
| 键 | 默认值 | 含义 |
|---|---|---|
notifyOnAttention |
true |
会话卡在等你时通知。 |
notifyOnComplete |
true |
跑完时通知。 |
minRunMs |
20000 |
比这短的运行忽略掉。快速回答不值得通知。 |
sound |
true |
播放系统通知声音。 |
去重
同一个交互不会重复通知:标识就是交互自己的 key,所以同一个等待被再次上报时保持静默。
开窗后的首次快照被当作基线而不是新消息 —— 这正是避免"一启动就为已在运行的会话
炸出一堆通知"的原因。
验证它真的能用
通知是异步且视觉的,两个半都观测不到操作系统最终渲染出来的那枚通知。
所以浏览器半发布了一个 Cordis Inspect provider DesktopNotify,可以从页面外部查询:
| 方法 | 回答什么 |
|---|---|
status |
半挂载了吗?Notification.permission 是什么?是不是安全上下文?跟了哪些会话、从何时开始? |
testToast |
立刻真弹一条,并报告实际构造出了什么。 |
decisions |
内存里的决策日志 —— 每一条发出去的,以及每一条故意没发的,连同原因。 |
decisions 就是用来回答"为什么我没收到通知"的:被静默的记录会带
suppressed: 'watched'(你正看着那个会话)或 suppressed: 'too-short'
(跑完时还没到 minRunMs)。
这个日志只在内存里,上限 100 条。它刻意不落盘:一个可能悄悄没出现的文件,
比没有文件更糟。
对着操作系统核实
testToast 返回 delivered: true 只说明 Notification 对象构造出来了。
要确认通知真的到了操作系统,对比 Windows 自己记录的这个应用 AUMID 的时间戳:
$k = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\electron.app.DeepSeek Harness'
[DateTime]::FromFileTime([int64](Get-ItemProperty $k).LastNotificationAddedTime)
这个插件就是这么验证的:先静置 12 秒做对照,确认时间戳不动;再每弹一次,确认它前进。
> 提醒一句:时间戳前进只证明通知平台接收了,不等于横幅渲染出来给人看了。
> 完整的验证还得加上人眼确认。我自己踩过这个坑 —— 排查半天,最后发现是勿扰模式。
开发
node --check index.js
node --check client.js
node test/watcher.test.mjs
测试会在假的 __ModuleLoader__ 下加载 client.js,喂给它合成的会话状态变化,
断言哪些通知应该弹、决策日志记了什么。失败时退出码非 0。
约束
dependencies 必须保持为空。 桌面 profile 用 pnpm 的 link: 协议安装这个包,
而 pnpm 不会安装被链接的包自己的依赖。一旦声明依赖,Cordis 插件树初始化时
依赖解析会失败,DSH 就起不来了。