给 DSHDeepSeek Harness用的 Windows 原生通知插件:任何对话把控制权交还给你的时候,弹一条…
30.8/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/6 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/ADkun/dsh-windows-notifier.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
生态插件
可安装,未声明 dsh 能力
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 26 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意未发布到 npm registry,仅可从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-windows-notifier(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/13 08:11:46
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件
查看扫描证据(共 4 处,展示前 4 条)
- 写入本地文件src/plugin.js:223
appendFileSync(options.logFile, `${new Date().toISOString()} ${line}\n`) - 读取本地文件src/notify.js:42
return candidates.find((candidate) => candidate === 'powershell.exe' || existsSync(candidate)) - 读取本地文件src/notify.js:69
if (!existsSync(scriptPath)) { - 执行外部命令来自配置/变量src/notify.js:107
child = spawn(powershellPath, args, { stdio: 'ignore', windowsHide: true })
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 任务完成提醒
- 提问与审批提醒
DSH 插件,只要 DSH 会话需要你,就弹出原生 Windows 通知:任务完成、有人提问或等待审批。
README
由 DeepSeek 最新模型翻译生成dsh-windows-notifier
给 DSH(DeepSeek Harness)用的 Windows 原生通知插件:任何对话把控制权交还给你的时候,弹一条 Windows 系统通知。
它监听的是整个 DSH 进程的事件,所以后台正在运行的对话同样会通知你 —— 你不用一直盯着某个会话窗口。
什么时候会通知
| 场景 | 通知标题 | 触发事件 |
| 任务完成,可以继续对话 | ✅ 对话已完成 | agent/status → idle |
| 智能体向你提问(ask_user_question) | ❓ 需要你的输入 | user-questions/request |
| 有操作等待你批准 | 🔐 需要你批准 | approval/request |
| 某一步/某一轮出错 | ❌ 对话出错 | agent/error |
| 本轮被中止(可选,默认关) | ⏹️ 本轮已中止 | agent/status + turn/end: aborted |
通知正文第一行是对话标题(取自 DSH 的会话标题,还没生成标题时回退到工作目录名),第二行是状态说明或具体内容(提问内容、工具名、错误信息、运行时长)。
后台委派的调度方不会误报「已完成」。
展开完整文档(README 剩余 1 节)
dsh-windows-notifier
给 DSH(DeepSeek Harness)用的 Windows 原生通知插件:任何对话把控制权交还给你的时候,弹一条 Windows 系统通知。
它监听的是整个 DSH 进程的事件,所以后台正在运行的对话同样会通知你 —— 你不用一直盯着某个会话窗口。
什么时候会通知
| 场景 | 通知标题 | 触发事件 |
| 任务完成,可以继续对话 | ✅ 对话已完成 | agent/status → idle |
| 智能体向你提问(ask_user_question) | ❓ 需要你的输入 | user-questions/request |
| 有操作等待你批准 | 🔐 需要你批准 | approval/request |
| 某一步/某一轮出错 | ❌ 对话出错 | agent/error |
| 本轮被中止(可选,默认关) | ⏹️ 本轮已中止 | agent/status + turn/end: aborted |
通知正文第一行是对话标题(取自 DSH 的会话标题,还没生成标题时回退到工作目录名),第二行是状态说明或具体内容(提问内容、工具名、错误信息、运行时长)。
后台委派的调度方不会误报「已完成」。 Adg 这类多智能体模式里,调度代理一派出子代理就立刻结束自己这一轮 —— 于是那声「对话已完成」会在子代理还在跑的时候到达。waitForSubagents(默认开)让它先看一眼:该会话名下还有没有在跑的子代理(origin: 'subagent' 的子孙,任意深度;fork 不算,它不是委派)。只要还有,通知就先按下去,日志写 skip complete for <会话>: N delegated subagent(s) still running。子代理结算会唤醒调度方,等它真正跑完最后一轮、名下再没有在跑的子代理时,那条通知才补上 —— 没丢通知,只是延后到任务真的收尾。被中止(⏹️ 本轮已中止)不受这条闸门影响:它说的是这一轮被停了,什么时候都成立。
默认不通知 subagent / workflow 子会话自己一轮跑完的那声「对话已完成」—— 一次后台委派会派生好几个子会话,全部通知会变成弹窗轰炸。但子会话需要人的时候照常通知:它向你提问、等你批准、或者出错,都会弹。需要连子会话的「结束」也听,把 includeSubagents 打开。
工作原理
DSH 进程 └─ host 组合(所有 profile 共用) └─ dsh-windows-notifier ← 这一个插件行 ├─ ctx.on('internal/dispatch', …) 框架的派发公告(事件名 + 载荷) └─ ctx.on('agent/status' | 'user-questions/request' | …) 直接监听(兜底) │ └─ 一个短命的 powershell.exe → Windows Toast(WinRT)
为什么要听两个通道
DSH 的作用域事件(agent/status、user-questions/request、approval/request、agent/error)是带作用域载体派发的:Cordis 会把「上下文不在该载体作用域链上」的监听器直接丢掉。
这个过滤是真实可观测的:挂在 host 根上下文上的普通监听器能收到 agent/status,却收不到 user-questions/request —— 后者以提问的那个 agent 自身作为作用域键,而派发确实发生了,只是监听器被滤掉了(用 ctx.on('internal/dispatch', …) 能看到这次 waterfall 派发,但自己的监听器不响)。
所以插件同时观察两条通道:
1. internal/dispatch —— 框架自己的派发公告。任何非 internal/ 事件都会在作用域过滤之前在这里公布一次,因此用 { global: true } 注册的监听器能看到所有作用域的事件。这是「监听任意对话(含后台对话)」唯一可靠的入口(DSH 自己的 scope-invariant 插件也是这么监听的)。
2. 直接监听那四个事件 —— 兜底:万一某个版本不再公布派发流,直接监听仍然生效。
两条通道拿到的是同一个载荷对象,因此用一个 WeakSet 按对象身份去重:谁先到谁负责通知,另一条通道直接跳过 —— 不需要拍脑袋定一个时间窗。
怎么认出一个子会话
DSH 在每个子会话的 durable header 上盖了 origin: 'subagent'(连同 parentSession、delegationDepth),这是唯一权威的判据;includeSubagents 关着时,只有「一轮结束」这一族(completed / interrupted)会被它挡下,提问 / 审批 / 出错照旧通知。
判断本身走的是事件载荷自带的那份会话:载荷里有 live agent,live agent 上有它自己的 session,所以答案跟着事件一起来,不依赖任何服务查询。这一条是要紧的 —— 插件行是补丁层插进来的,可能在发布 sessions / sessionTitle 的那一行之前就激活,而 ctx.get() 在 apply 时取到的 undefined 会跟着进程一辈子;只查 ctx.sessions 的过滤会永远不生效,每个子会话跑完都弹一条「对话已完成」。那两个服务现在也用 ctx.inject 采用(和 settings 一样),所以会话标题也能正常读到。
另外还会从 session/created 的公告里记下子会话 id(有上限),作为「连载荷里的 session 都读不到」时的兜底。反过来,读不到会话一律按用户会话处理:宁可多弹一条,也不能把主对话吞掉。
其余特性:
- 零依赖:通知由 scripts/toast.ps1 通过 Windows PowerShell 自带的 WinRT Windows.UI.Notifications 类型弹出,不需要装 BurntToast、不需要注册 COM、不需要常驻进程。
- 不阻塞任何东西:所有监听器都只是旁观。追问/审批那条监听器仍会调用 next() 把瀑布链传下去,所以正常问答完全不受影响(有测试专门盯着这点)。通知进程异步启动并带并发上限,插件卸载时会杀掉还在跑的进程。
- 点击通知可以打开 DSH 的 Web GUI(自动读取当前 Web 服务端口,也可用 launchUrl 指定)。GUI 目前没有会话级路由,所以只能打开到首页。
安装
DSH 的能力全是 cordis.yml 里的一行行插件。装一个第三方插件 = 装包(+ 让它在 profile 里被选中)。从 0.2.0 起这个包自己带一层 bundle patch,所以「加一行」这一步由 DSH 自己做,见下面第 2 步。
装包这一步的落点由本版 DSH 定死:插件包只能从 Desktop 安装目录和当前 profile 自己的 node_modules 解析出来。旧文档里那个"所有 profile 共享的解析根"已被判过时(错误原文、源码位置与本机量法见「把所有 profile 一次装完」)。
1. 把包装进 profile
从本仓库装:默认装进每一个有 package.json 的 profile powershell -NoProfile -File scripts\sync-to-profile.ps1
只装一个 profile powershell -NoProfile -File scripts\sync-to-profile.ps1 -Profile web
(pwsh -File … 等价,只要装了 PowerShell 7;powershell 是 Windows 自带的那一个,一定能跑。)
这个脚本真正做了什么:
1. npm pack 把仓库打成 dist\dsh-windows-notifier-<版本>.tgz,内容由 package.json 的 files 字段决定(与 npm 发布一致;dist\ 不进版本库)。本机实测(2026-09-28,npm pack --dry-run --json,0.2.0):打进包的条目是 package.json、cordis.patch.yml、src\.js、scripts\toast.ps1、scripts\send-test-toast.mjs、scripts\sync-to-profile.ps1、examples\cordis.patch.yml、README.md、LICENSE —— test\ 不在其中;
2. 对每个目标 profile 执行 dsh plugin --profile <profile> add "file:<tgz 的绝对路径>" —— 与 Desktop「插件市场」走同一条路:往该 profile 的 package.json 加一条依赖,并在 profile 目录里运行 pnpm(dsh plugin 把参数原样转发给这个 pnpm,所以任何 pnpm 认的 spec 都能传)。包落在 $DSH_HOME\profiles\<profile>\node_modules\dsh-windows-notifier,是真目录(本机实测 2026-09-28:profiles\web 下装出来的那份 LinkType 为空,profile 的依赖记成 file:D:/dsh/dsh-windows-notifier/dist/dsh-windows-notifier-0.2.0.tgz);
3. 已经是该 profile 依赖的包会先 remove 再 add(原因见「改完源码怎么让它进环境」);
4. desktop profile 会被跳过:dsh 对它的每一条 plugin 子命令都直接拒绝(原文 error: profile "desktop" is managed exclusively by the Electron application,源码是 @deepseek-ai/dsh/lib/bin.js 里的 rejectElectronProfile)。它归 Desktop 应用管,请在应用里的插件市场升级(那边做的是同一套安装 + 选中);
5. -DshHome 指 DSH 用户根(默认 $env:DSH_HOME,再退到 $HOME\.dsh),-Dsh 指 dsh 命令;
6. 每个 profile 装完后核对它自己的 dsh.profile.bundles 里确实有 dsh-windows-notifier,缺了就补上(原因见下一条),并在这份机器级 patch 里发现本插件的行时给出警告(原因见「别把这一行放回机器级」)。
装包 ≠ 选中。 dsh plugin add 转发给 pnpm 之后,CLI 还会做一次「已安装的 bundle 声明对账」:清单里声明了 dsh.bundle 的依赖会被补进 dsh.profile.bundles(@deepseek-ai/dsh-app-boot 的 reconcileProfilePlugins)。本机实测 2026-09-28:这次写在命令返回之后才落盘 —— 装完立刻读 package.json 有可能读到还没补上的那一刻,所以脚本自己再核对一遍、缺了就写。只是依赖、没被选中 = 没有行、没有配置页,这是最容易「装好了却什么都没发生」的地方。
为什么是 tarball,而不是把仓库目录链接进去: 解析器只从两个根解析,其中 profile 侧要求解析到的清单路径落在该 profile 自己的 node_modules 之内 —— 共享父级一律不行;tarball 展开成的真目录正是市场安装的形状(源码级:DSH 把 profile 的 pnpm-workspace.yaml 固化成 nodeLinker: hoisted,所以 pnpm 把依赖铺成真目录,而不是 .pnpm 下的链接)。另外,pnpm 的 link: 装法(dsh plugin add <本地目录> 用的就是它)只链接一个目录,不安装该包自己的依赖,也不像市场安装那样把包记进依赖;它还省不掉重启:模块缓存的键是解析后的真实路径,链接最终仍指向仓库里那份文件,改它不会重新导入(源码级:解析器用 realpathSync 规整缓存键)。
从 GitHub 装是另一条合法路径,但本次未实测 —— dsh plugin 的后端是 pnpm,git 托管的 spec 由 pnpm 处理,DSH 的 CLI 对 git+… / github:… / .git 结尾的 spec 另有专门提示。形式例如:
dsh plugin --profile web add github:ADkun/dsh-windows-notifier
量法:在一台没装过它的机器上跑这条命令,确认 profile 的 package.json 里出现该依赖、(Get-Item $DSH_HOME\profiles\<profile>\node_modules\dsh-windows-notifier).LinkType 为空(真目录),重启后宿主日志里没有 failed to import。这条路同样只服务你指定的那一个 profile。
dsh 从 PATH 上找 pnpm,找不到就打印 dsh: pnpm was not found; install pnpm and make it available on PATH.;Desktop 自带一份并把它放上 PATH(见「没有 pnpm 时」)。--profile desktop 一律不行 —— 见上面第 4 条。
2. bundle 会自己带来那一行
从 0.2.0 起这个包声明了自己的一层 bundle patch(package.json → dsh.bundle.patch → cordis.patch.yml)—— 这层带来的是行本身,所以 profile 里不用再手写那一行,只要把包选中(dsh.profile.bundles 里有它)。第 1 步的 dsh plugin … add 之后 CLI 会对账并补上这条选中(落盘可能晚一拍,脚本会自己核对、缺了就写):
包自带的 cordis.patch.yml(bundle 层)
- insert:
- id: windows-notifier
name: 'dsh-windows-notifier'
这层带来的是行本身(上面那段就来自包里的 cordis.patch.yml)。id 是 windows-notifier,而它同时是两处地址:DSH 给每个已挂载的 profile 条目生成一份「Host 侧可校验的配置描述」,地址就是这个条目 id,浏览器半侧因此能用 configForms.get('windows-notifier') 取到它;而「插件」页那一行的摘要仍按 <包名>#<行 id>(dsh-windows-notifier#windows-notifier)注册。改这个 id,这些地方都得跟着改。
行里故意没有 config: —— 每个开关都有 schema 默认值(见 src/settings.js),新装一个 profile 不需要写任何值。
per-profile 的值写在该 profile 自己的 patch 文件里,也就是界面保存时写的那一份:
$DSH_HOME\profiles\<profile>\cordis.patch.yml
- id: windows-notifier
name: 'dsh-windows-notifier'
config:
logFile: 'D:\dsh\dsh-windows-notifier\.dsh-windows-notifier.log'
patchReload: live 的 profile 会热加载 config: 改动,不用重启;但换掉 src/ 里的代码必须重启(模块缓存按解析后的真实路径做键,不会重新 import)。
别把这一行放回机器级 $DSH_HOME\cordis.patch.yml。 两层原因:patch 命中一个已存在的 id 时是整体替换它的 config 对象(dsh-app-boot 的 applyEntryPatches 写的是 target.config = value),而机器级在每个 profile 之后应用 —— 界面上保存的值会被它悄悄盖掉;并且 Host 会直接拒绝这次保存,原文是 Configuration for "<id>" is overridden by a home patch or command-line overlay。症状是配置页能看、不能存。(2026-09-28 迁移时本机就是这么放的,那一行已删。)
把所有 profile 一次装完
机器级 $DSH_HOME/cordis.patch.yml(即 C:\Users\<你>\.dsh\cordis.patch.yml)仍然是「在每个 profile 自己的 patch 之后应用」的那一层,但不再放这个插件的行。要一次装到所有 profile,就是让脚本对每个 profile 各跑一次 dsh plugin … add,并让每个 profile 各自选中自己的 bundle:
powershell -NoProfile -File scripts\sync-to-profile.ps1
脚本要找一个 dsh 应用程序(Get-Command dsh -CommandType Application)。它在 DSH 会话里通常就在 PATH 上(Desktop 会把 %APPDATA%\DSH Desktop\host-commands\<profile>\generations\\bin\dsh.cmd 放上去),但在你刚把 DSH 停掉的那个普通 PowerShell 里通常没有 —— 而这恰恰是这个脚本最常见的运行场景。所以:
- 找不到时脚本会把这句话直接打给你(dsh executable not found (dsh).),并顺带列出它在 %APPDATA%\DSH Desktop\host-commands\\generations\\bin\ 下发现的 shim;把路径交给 -Dsh 就行: powershell -NoProfile -File scripts\sync-to-profile.ps1 -Dsh "C:\path\to\dsh.cmd"
- 名字只解析到函数或别名时也会被同一句话点出来:脚本要用 cmd.Path 调用它,函数没有这个属性(DSH 会话里 dsh 有时就是个函数)。
- npx 缓存里的那份(…\npm-cache\_npx\<hash>\node_modules\.bin\dsh.cmd)可用,但 <hash> 会随缓存刷新而变,别写进长期命令或脚本文档。想彻底省掉 -Dsh,就在 PATH 上放一份通用的 dsh(例如全局安装 npm i -g @deepseek-ai/dsh;未实测)。
机器上同时装着多个 DSH 时,用哪个 dsh 都能对任意 profile 操作 —— -Dsh 只是「用哪个 CLI」,profile 由 --profile(脚本内部传)决定。
选中 ≠ 包能解析:行由 bundle 层带来,包仍必须能从每个 profile 自己的 node_modules 解析到,所以脚本不带参数时默认装进每一个有 package.json 的 profile(desktop 除外,见第 4 条),并在装完后逐个核对 dsh.profile.bundles 里确实有它(只是依赖、没被选中 = 没有行、没有配置页)。以后新建了 profile,要再跑一次脚本。
pwsh(PowerShell 7)不是 Windows 自带的;脚本用任意一个都能跑,上面写 powershell 是因为它一定在。
历史(已失效):共享解析根。 旧文档把这包装复制(或建目录联接)到 $DSH_HOME\profiles\node_modules,称它是「每个 profile 共享的解析根,Node 从 profile 目录向上上溯就能找到它」。本版 Desktop 反过来把这个共享父级当成过时落点:
- 源码级:解析器只在 Desktop 安装目录与当前 profile 之间二选一(构造错误在 Desktop 安装目录的 resources\app\lib\package-overlay-.js),并且要求 profile 侧解析到的清单路径落在 <profileDir>\node_modules 之内;共享父级在解析器的源码里就叫 sharedFallbackDirectory,落在里面的解析结果被判过时(isObsoleteProfileFallback,见 lib\module-resolution-.js)。
- 本机实测(2026-09-27):把包装进 $DSH_HOME\profiles\node_modules 时,解析直接失败,原文是 dsh-plugin-desktop: cannot resolve package "dsh-windows-notifier" from the Desktop installation or active Profile;同时宿主日志长期出现 loader 告警 2026-09-27 15:01:53.779 [W] [hmr] windows-notifier (dsh-windows-notifier): failed to import —— 机器级 patch 那一行在每个 profile 上都生效,而包躺在被拒绝的那个根里。那个旧目录到今天仍在(scripts/sync-to-profile.ps1 只报告、不删)。
完整示例见 examples/cordis.patch.yml。
没有 pnpm 时
dsh plugin --profile … add 的底层是 pnpm,并且要求 pnpm 在 PATH 上 —— 找不到时它会打印 dsh: pnpm was not found; install pnpm and make it available on PATH.。Desktop 自带一份并把它放上 PATH(本机实测:Get-Command pnpm 命中 %APPDATA%\DSH Desktop\runtime-commands\…\bin\pnpm.cmd),所以在 DSH 会话里通常不用管。
确实没有 pnpm 时,可行的手工路线只有一条:在别处 npm pack 出 tarball,再把里面的 package\ 展开成该 profile 自己的 node_modules 目录(真目录):
$tgz = npm pack D:\path\to\dsh-windows-notifier --pack-destination $env:TEMP # 打印出 tarball 文件名
tar -xzf "$env:TEMP\$tgz" -C $env:TEMP
$dest = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-windows-notifier"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Copy-Item -Recurse -Force "$env:TEMP\package\" $dest
未实测:这条路本次没有走。量法:删掉某个 profile 里已经装好的包(连目录一起删),按上面展开,重启 DSH,然后看宿主日志有没有 failed to import,以及(开着 notifyOnActivate: true 或 logFile 时)插件有没有写激活记录。注意手工展开不会给 profile 的 package.json 加依赖条目,dsh plugin … remove 也管不到它 —— 卸载要自己删目录。
插件本身零依赖、零构建,所以不需要 npm install / pnpm install。
改完源码怎么让它进环境
改完仓库里的源码,重跑一次脚本,然后重启 DSH:
powershell -NoProfile -File scripts\sync-to-profile.ps1
脚本内部是先 remove 再 add,而不是直接 add:pnpm 对「路径和版本都没变、只有内容变了」的本地 tarball 会判成 Already up to date 而不重装(本机实测 2026-09-27:直接 add 打印 Already up to date,profile 里那份的 SHA256 一个字节没变;remove 后 add 才真的替换)。这正是「改了源码重跑脚本」能进到环境里的原因。版本号不必每次 bump —— remove/add 已经处理了内容变化。
重启也不是可选项:loader 按解析到的路径缓存 ES module,被替换的文件不会被重新 import(详见下一节)。现在也没有「复制式部署的副本」这回事了:包由 tarball 展开进 profile,改源码得到的是一份新 tarball(路径相同、版本号可能相同,内容不同)—— 所以旧文档那句「副本不会自己更新」,现在读作「环境里那份不会自己更新,只能重跑脚本 + 重启」。值得补一句:它同样不会提示你已经旧了,那正是「仓库里修好了、环境里照旧」这类问题的成因。
改配置 vs 改源码:热加载的边界
- 改 patch 文件(开关、logFile、launchUrl…)→ patchReload: live 会立刻重挂载,马上生效。
- 改界面里的设置(设置 → Windows 通知)→ 立刻生效,写的是该 profile 自己的 patch 文件,见下面「界面化配置」。
- 改插件源码 → 不会立即生效。要重跑一遍 powershell -NoProfile -File scripts\sync-to-profile.ps1(把新 tarball 装进每个 profile)再重启 DSH。DSH 的 loader 按「模块解析后的路径」缓存 ESM 模块,并且在插件行的 name 没变时复用已经加载过的那个模块;同一个文件路径改内容也不会重新导入。旧文档曾说"换个新路径(换个目录、或复制一份到别处)再让行指向它"就能生效 —— 在本版里这条也不可靠,而且得先把包解析到 profile 的 node_modules 里才谈得上换路径。
浏览器半侧同理,而且更严格:客户端模块图按「解析后的说明符」缓存包元数据(包括"没有浏览器半侧"的否定结论),所以连"换个新路径的插件行"这条路也不通(实测:改成子路径说明符后这一行会导入失败),只能重启一次。
历史(已失效):用目录联接指向仓库来"热改"。 旧文档说"联接指向仓库时,增删文件(路径变化)就能被重新导入,改同一个文件则不行"。本版不成立:(1) 联接到共享根 $DSH_HOME\profiles\node_modules 的那条路已被解析器拒绝(见「把所有 profile 一次装完」);(2) 模块缓存的键是解析后的真实路径,联接最终仍指向仓库里那份文件,增删/改文件都不会改变这个键,因此什么都不会重新导入。要改源码生效,只有"重跑脚本 + 重启"。(本次对解析器函数单独做过一次探针:把联接放在某个 profile 自己的 node_modules\<name> 下会被它接受;但这条没有在真实启动里验证,不作为做法推荐。)
配置项
全部可省略,省略即用默认值。
| 选项 | 类型 | 默认值 | 说明 |
| enabled | boolean | true | 总开关;false 时插件不做任何事 |
| notifyOnActivate | boolean | false | 激活时先弹一条「已启用」,用来确认通道正常 |
| includeSubagents | boolean | false | 是否也通知 subagent / workflow 子会话自己一轮结束;子会话的提问 / 审批 / 出错始终通知 |
| waitForSubagents | boolean | true | 会话名下还有子代理在跑时先不弹「对话已完成」,等它真正收尾再弹(挡住调度方一派出子代理就误报完成) |
| notifyOnComplete | boolean | true | 任务完成(idle)时通知 |
| notifyOnQuestion | boolean | true | 智能体提问时通知 |
| notifyOnApproval | boolean | true | 等待批准时通知 |
| notifyOnError | boolean | true | 出错时通知 |
| notifyOnInterrupted | boolean | false | 本轮被中止时通知 |
| minTaskDurationMs | number | 0 | 短于该时长的任务不通知(避免零星小任务刷屏),例如 5000 |
| sound | default \| silent | default | 是否播放通知音 |
| disappearAfterMs | number | 6000 | 通知停留时长(毫秒);0 = 一直留在屏幕上,直到你手动关掉 |
| openOnClick | boolean | true | 点击通知时打开 DSH Web 界面;false 让通知不可点击 |
| appId | string | Windows PowerShell 的 AUMID | 通知归属的应用标识;换成你注册过的 AUMID 就能改显示名 |
| powershellPath | string | 自动探测 | 指定 powershell.exe 路径(必须是 Windows PowerShell 5.1,pwsh 7 不支持 WinRT) |
| scriptPath | string | 包内 scripts/toast.ps1 | 指定自定义通知脚本 |
| launchUrl | string | 空(自动取当前 Web GUI 地址) | 点击通知打开的地址;支持 {sessionId} 占位符 |
| logFile | string | 空 | 追加调试日志到文件,排查用 |
| maxConcurrent | number | 1 | 同时运行的 powershell 进程上限 |
| timeoutMs | number | 15000 | 单个通知进程的超时时间 |
「停留时长」能做的事,受 Windows 自己限制。 横幅时长只有「约 5 秒 / 约 25 秒」两档,插件只能按 disappearAfterMs 选最近的一档(> 7000 用长档);这个值同时通过 ExpirationTime 决定它在通知中心里保留多久,所以写 3000 并不会让横幅 3 秒就走。要让通知「无限等待」,用 0:插件会带上 scenario="reminder",通知就一直留在屏幕上直到你手动关闭——这是 Windows 上唯一能做到这件事的方式。
哪些键能在界面里改: 上表 20 个键里只有 15 个是「运行时真的会重读」的,也就是可以在 设置 → Windows 通知 这一页里改:enabled、notifyOnComplete、notifyOnQuestion、notifyOnApproval、notifyOnError、notifyOnInterrupted、notifyOnActivate、includeSubagents、waitForSubagents、minTaskDurationMs、sound、disappearAfterMs、openOnClick、launchUrl、logFile。剩下 5 个是安装期的固定设施(appId、powershellPath、scriptPath、maxConcurrent、timeoutMs),只能在 patch 里写 —— Host 会拒绝界面对它们的写入。分界线就在 src/settings.js:标了 .volatile() 的是前者。
界面化配置
设置 → Windows 通知 就是本插件的配置页 —— 和「通用设置」「模型」「子智能体管理」并列的一页,上面那 15 个可热改的键都在这里。「设置 → 内置插件」里本插件那一行不再放表单,只留一句简介和一句指向该页的提示。
这套模型是 DSH 0.1.7 之后的样子,和旧版完全不同:
- 没有"插件注册的 settings 命名空间"这回事了。 Host 给每个已挂载的 profile 条目生成一份配置描述(@deepseek-ai/dsh-settings 的 describe(),地址就是条目自己的 id),浏览器侧的 configForms 服务把它镜像出来;页面用 configForms.get('windows-notifier') 拿这份描述,注册到「设置」侧栏的则是 settings.section 槽(id: 'windows-notifier'、order: 38、label 是跟着界面语言走的 thunk)。两个入口都只在这个 profile 真的服务了这行的命名空间时才注册(configForms.whileServed),否则侧栏里不会多出一个空页。
- 页面只在自己那一节被打开时才挂载,而设置外壳不会因为 Host 写回而重渲它(renderSlot("settings.section", { close }, { only: active }))。所以页面自己订阅控制器:useEffect(() => form.subscribe(sync)),快照一变就重画。这也能解释为什么它拿到的是控制器而不是一份快照 —— 快照是给"整页跟着重渲"的插件行摘要用的。
- 保存写的是该 profile 自己的 patch 文件($DSH_HOME\profiles\<profile>\cordis.patch.yml),不再是 $DSH_HOME/settings.yaml。一次保存 = 一次原子的、带 revision 栅栏的文档改动,Host 用行自己的 schema 重新校验;页面上的「恢复默认」发一条 unset,让该字段重新继承 bundle 层的默认值。
- 只允许写 .volatile() 字段:src/settings.js 里标了 .volatile() 的 15 个就是全部可写路径,写别的 Host 直接拒。字段被机器级 patch 或命令行 overlay 盖住时也会拒,原文 Configuration for "<id>" is overridden by a home patch or command-line overlay。
- 保存后立刻生效,不用重启。 页面改的是 .volatile() 引用,loader 就地改写它,宿主半侧每次做判断前都重读一遍 —— 包括总开关:关掉后不再有任何通知,但监听不摘,enabled 重新打开就立刻恢复(不再有"摘了挂不回来"的问题)。
- 这一页是插件的浏览器半侧(src/client.js),按 DSH 的 lazy-CJS 客户端模块格式手写,所以仓库仍然零构建;界面文案走 Client 的 locale 服务(zh / en 两本词典),侧栏那一项的文案也从它取。
- 行自己的 schema 需要 @deepseek-ai/schemastery(harness 自带)。环境里没有它时插件照常工作,只是没有配置表格(configForms 也就不服务这一行,页面不会出现)。
- 装好后需要重启一次 DSH 页面才会出现:客户端模块图按「解析后的说明符」缓存包元数据,其中也包括"这个包没有浏览器半侧"这个结论,所以新声明的浏览器半侧要等下一次启动才进入引导图。
验证
先单独验证 Windows 侧通道:
node scripts/send-test-toast.mjs
node scripts/send-test-toast.mjs "自定义标题" "自定义正文"
看到通知就说明通道没问题,剩下的只是让 DSH 把事件交给插件。
再验证插件本身:把 notifyOnActivate 打开(设置 → Windows 通知 这一页里,或 patch 里都行),然后保存 / 重启一次 —— 应该立刻弹出一条「已启用」通知,日志里出现 [dsh-windows-notifier] active。
不重启也能离线核对这套组合:dsh --profile <profile> --dump-config 会把每个 bundle 层、该 profile 的 patch、机器级 patch 依次应用后的结果打出来;应该能看到 windows-notifier 这一行、name: dsh-windows-notifier,以及它的 config: 里那些值。这一条在改动 patch 之后最值得跑:图层顺序错了、id 打错了、机器级那一行还在,都会在这里显形("机器级那一行还在"还会额外让界面保存被拒)。
logFile 会记录每一次决定,包括每一次"没通知"的原因,例如:
notify complete: ✅ 对话已完成 / 重构支付模块 | 已运行 2 分 13 秒,可以继续对话了。 -> http://127.0.0.1:3080 skip complete for cccc1111-child-0001: subagent turn end (includeSubagents is off) skip complete for session-…: 2 delegated subagent(s) still running 跳过 session-… 的 complete:还有 1 个委派的子代理在跑(ctx.agents)跳过 session-… 的 complete:挂接的会话没有已结算的轮次 session … 未挂接;直接上报完成,不带轮次结果跳过 question:switch off
「为什么没弹」基本都能在这几行里找到答案。
卸载
包分别装在每个 profile 自己的 node_modules 里,所以每个装过的 profile 都要卸一次:
1. dsh plugin --profile <profile> remove dsh-windows-notifier(对每个装过的 profile 跑一次)。包一走,行也就没了 —— 行是 bundle 层带来的,而这条选中项不该再留着(同一套「已安装 bundle 声明」对账会把它一并摘掉;若你的版本没摘,手工从 dsh.profile.bundles 里删掉这一项即可),不再需要手工删 patch 里的行;手工展开进目录的那种直接删掉那个目录;
2. 想留着包但先关掉,就在该 profile 的 patch 里写 disabled: true(或直接用它那一行的开关);
3. 顺手可以删掉 $DSH_HOME/settings.yaml 里残留的 dsh-windows-notifier: 分节 —— 那是旧版(0.1.x)留下的,本版不读它,留着也无害;
4. 旧落点 $DSH_HOME\profiles\node_modules\dsh-windows-notifier(历史遗留)本版已经不会被解析,可以删掉。
已知限制
- 仅 Windows:非 Windows 平台插件会安静地跳过,不会报错。
- 专注助手(Focus Assist)/「请勿打扰」会拦掉通知:这是系统行为,插件无法绕过。
- 通知显示的应用名是「Windows PowerShell」:因为用的是它现成的 AUMID,好处是零安装。想改成自己的名字,需要注册一个带 AppUserModelID 的开始菜单快捷方式,然后把 appId 指过去。
- 每条通知会短暂启动一个 powershell.exe(约 0.3–1 秒)。对「一轮任务结束」这种频率完全够用;这也是它不需要任何依赖的代价。
- 必须在有 DSH 事件的前提下工作:headless / sdk 这类最小 profile 如果不发这些事件,插件会挂载但不产生通知。headless 收尾时会话可能已经 detach,这种情况下插件仍按「完成」通知(idle 只会在状态变化时发布,所以它本身就意味着有东西跑完过)。
- 点击通知只能打开 GUI 首页:DSH 的 Web GUI 把会话选择放在内存里,没有会话级路由,所以没有可深链的地址。launchUrl 里写 {sessionId} 会被替换,但目标页面目前不消费它。不想要这个跳转就把 openOnClick 关掉(patch 或界面里都行)。
- 横幅时长只有两档:见上面配置项下的说明;0 是唯一能"无限等待"的值(scenario="reminder")。
- 必须重启一次,界面上的配置页才会出现(原因见「界面化配置」最后一条):浏览器半侧是在 DSH 启动时进入引导图的。
- 界面文案是它自己带的:src/client.js 里注册了 zh / en 两本词典,跟界面语言走(侧栏那一项的名字也来自它)。schema 里的字段说明(src/settings.js 的 LIVE_DESCRIPTIONS)只有中文,但那一页是 Host 自动生成的表格 —— 本插件把它关掉了(configure({ auto: false }),DSH 自己的插件也这么写),所以实际显示的是这一页自己的文案。
- 通知不做「用户是否正在看这个会话」的判断 —— 前台会话结束同样会弹。
开发
node --test test # 86 项测试:消息文案、配置归一化、事件接线、双通道去重、
子会话识别(载荷自带 session / 创建公告兜底)、Config schema 与实时改配置、浏览器半侧的设置页契约(槽注册、跟随 Host 快照、暂存/保存)
仓库结构:
src/plugin.js 插件本体:观察派发流 + 直接监听,决定要不要通知,并跟随界面设置 src/messages.js 纯函数:会话分类、文案、时长格式化 src/config.js 配置归一化(任何脏值都回退到默认值,不炸 profile) src/settings.js 这行的 Config schema:进配置表格的 20 个键、哪些是 .volatile()(界面可改的那 15 个) src/client.js 浏览器半侧:手写的 lazy-CJS bundle,注册「设置 → Windows 通知」这一页 src/notify.js Windows Toast 传输层:队列 + powershell 进程生命周期 cordis.patch.yml 包自带的那一层 bundle patch:插入 windows-notifier 这一行(无 config) scripts/toast.ps1 WinRT 弹窗脚本(纯 ASCII,中文通过参数以 UTF-16 传入) scripts/send-test-toast.mjs 手动验证通道 scripts/sync-to-profile.ps1 打包(npm pack)+ 装进每个 profile 自己的 node_modules 并选中 bundle(默认全部 profile,-Profile 选一个,desktop 跳过) examples/cordis.patch.yml 机器级/per-profile 覆盖与禁用的写法示例
dsh-windows-notifier is a zero-build DSH plugin that raises a native Windows toast whenever
any conversation in the process hands control back to you: a turn finished, the agent asked a
问题、等待审批,或某一步出错。子代理或工作流子级的自身回合结束默认会被过滤掉(设置 includeSubagents: true 才能也收到);子级如果提问、阻塞在审批上或报错,仍会上报,因为要回答的是用户。调度器单独处理:在 Adg 这类多智能体模式下,委派会立刻结束调度器自己的回合,所以它第一次 idle 并不代表任务结束。waitForSubagents(默认开启)会在会话仍有运行中的 origin: 'subagent' 后代时压住这条提示——任意深度都算;fork 不是委派,永远不压任何东西——并在子级回报后的最后一次 idle 时放行,因为子级落定会唤醒父级。中断报告永不压住:本回合被停止这件事,无论子级做什么都成立。它不依赖任何你需要安装的东西:Windows 侧是 Windows PowerShell 内置的 WinRT Windows.UI.Notifications,设置 schema 来自 harness 自己的 @deepseek-ai/schemastery。
子级通过会话头里的 origin: 'subagent' 识别,这个头由 DSH 打上——从事件负载本身携带的会话读取(payload.agent.session),绝不从服务查找读取,因为补丁插入的行可能在该服务存在之前就采样过了。
它的实时开关在专属设置页上编辑——Settings → Windows notifications,就在内置项旁边。不再有插件注册的设置命名空间:Host 根据每个已挂载配置档条目自己的 Config 推导出表单,并按条目 id 寻址,所以浏览器半边用 configForms.get('windows-notifier') 读取,并注册一个 settings.section 条目(id: 'windows-notifier'、order: 38、一个跟随 UI 语言的 label thunk)。Plugins 页上的行只保留一行摘要和一个指向该页的指针,注册在 <package>#<row id>——这里是 dsh-windows-notifier#windows-notifier。设置外壳只在分区打开时挂载它,Host 写回时也不重新渲染,所以该页自己订阅表单控制器。保存是一次原子、带修订栅栏的文档写入,写进该配置档自己的 cordis.patch.yml(不是 $DSH_HOME/settings.yaml),由该行自己的 schema 重新校验;该页的逐字段重置发送 unset,于是该字段重新继承 bundle 层的默认值。只有 15 个 .volatile() 字段可写——5 个安装期字段(appId、powershellPath、scriptPath、maxConcurrent、timeoutMs)会被 Host 拒绝,README 的中文表格列出了它们。该页是插件的浏览器半边,用 DSH 的 lazy-CJS 客户端模块格式手写,所以这个仓库仍然不需要构建步骤。注意,浏览器半边只在启动时进入 web 启动图:客户端模块图按解析后的包缓存每包元数据 specifier,所以安装后重启一次 DSH。
DSH 通过作用域载体派发这些事件,而 Cordis 会丢弃上下文位于载体作用域链之外的监听器——普通根上下文监听器能看到 agent/status,却永远看不到 user-questions/request。因此插件观察两个通道:internal/dispatch,框架自己的预过滤派发公告(能到达每个作用域),外加直接监听器作为兜底。两者携带同一个 payload 对象,所以用 WeakSet 按身份去重。Toast 走 Windows PowerShell 内置的 WinRT Windows.UI.Notifications 类型,所以什么都不用装,点击即可打开 Web GUI。
从 0.2.0 起,包声明自己的 bundle 补丁(package.json → dsh.bundle.patch → cordis.patch.yml)。这一层自己贡献 windows-notifier 行,所以不再需要手动编辑 cordis.patch.yml:包只需被选中,也就是它的名字要出现在该配置档的 dsh.profile.bundles 里。dsh plugin … add 通常在命令行核对已安装的 bundle 声明时追加这个选择,但实测写入发生在命令返回之后的一瞬间——而一个从未被选中的依赖根本不会挂载任何东西(没有行,没有设置页)——所以脚本会校验选择,缺失时自己写入:
powershell -NoProfile -File scripts\sync-to-profile.ps1 # every profile with a package.json
powershell -NoProfile -File scripts\sync-to-profile.ps1 -Profile web # one profile
它会运行 npm pack,然后对每个配置档执行 dsh plugin --profile <profile> add "file:<tgz>",这会添加依赖并在配置档目录内运行 pnpm,于是包以真实目录的形式落到该配置档自己的 node_modules 里。「每个配置档」有两个例外:
- desktop 被跳过:启动器拒绝为它执行任何 dsh plugin 调用(error: profile "desktop" is managed exclusively by the Electron application,rejectElectronProfile 位于 @deepseek-ai/dsh/lib/bin.js)。请从 Desktop 应用的插件市场更新该配置档,它会执行同样的安装加选择。
- 宿主正在运行的配置档可能直接拒绝替换(pnpm:another program is using this file, os error 32);停掉该宿主,然后重新运行。
当前 DSH(0.1.7-rc.2,Desktop)只从 Desktop 安装目录和活动配置档自己的 node_modules 解析插件包;共享的父级 $DSH_HOME/profiles/node_modules 被视为过时兜底并拒绝(dsh-plugin-desktop: cannot resolve package "dsh-windows-notifier" from the Desktop installation or active Profile)。不使用 link: 安装:它不会安装包自己的依赖,而指向某个 checkout 的链接会解析到模块缓存已持有的同一真实路径,所以既省不掉依赖步骤,也省不掉重启。
每个配置档自己的值(不同的日志文件、enabled: false 的配置档)要写进该配置档自己的 cordis.patch.yml —— 也就是设置页面写入的同一个文件:
- id: windows-notifier
name: 'dsh-windows-notifier'
config:
logFile: 'D:\dsh\dsh-windows-notifier\.dsh-windows-notifier.log'
不要把这一行放进机器级的 $DSH_HOME/cordis.patch.yml。 按 id 定位的补丁会替换整个 config 对象(applyEntryPatches 执行 target.config = value),而机器级这一层在每个配置档之后应用,所以它会悄悄盖掉设置页面保存的内容 —— 而且 Host 会直接拒绝保存,报 Configuration for "<id>" is overridden by a home patch or command-line overlay:页面能渲染,但存不进去。
改完源码后,重新跑一遍脚本(它会先删掉包,因为 pnpm 会把没变的本地 tarball 报成 Already up to date,不会重装),然后重启 DSH:加载器按解析后的路径缓存 ES 模块。按配置档卸载用 dsh plugin --profile <profile> remove dsh-windows-notifier —— 这也会清掉选择项,那一行随包一起消失;desktop 配置档则从 Desktop 应用的插件市场卸载。
dsh 需要 PATH 上有 pnpm(Desktop 自带一个);像 dsh plugin --profile <profile> add github:ADkun/dsh-windows-notifier 这样的 git 托管 spec 是合法的 pnpm spec 形式,但这里没有实测过,未实测的无 pnpm 回退方案见上文中文的 没有 pnpm 时 一节。
License
MIT预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
ADkun/dsh-windows-notifier
给 DSHDeepSeek Harness用的 Windows 原生通知插件:任何对话把控制权交还给你的时候,弹一条…
相关插件 · 安全 / 权限
智能体逆向工程morluto/rea
用智能体逆向工程任何东西,从应用行为到原生二进制。
★ 415,267已汉化DSH
73.3
智能体集群框架ruvnet/ruflo
面向 Claude Code 与 Codex 的智能体元基座meta-harness。
★ 80,414已汉化MCP
71.9
WorkBuddy 模型接入corrinehu/dsh-workbuddy-connect
2026 年 10 月 20 日起,WorkBuddy…
★ 1,604已汉化DSH
71.7
破甲一键通z91772524-ai/pojia-next
十五个 AI…
★ 967已汉化DSH
71.6
AI 代理虚拟终端strukto-ai/mirage
Mirage 是给 AI 智能体用的虚拟终端:一层 shell,统管数据、命令行和运行时。Unix…
★ 3,756已汉化DSH
70.8
Agent 操作层agentic-os-org/ANOLISA
ANOLISA(Agentic Nexus Operating Layer & Interface System Architecture)|智能体操作系统,带运行时、安全、可观测性,以及 Tokenless 响应压缩,降低 token 用量与成本。
★ 649已汉化DSH
70.7