DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

JHyugou /

JHyugou/dsh-remind

Verified

DSH 插件:任务完成 / 审批请求 / DSH 提问时用 Windows 原生通知(带提示音)提醒你——只有你不在看 DSH 时才打扰

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a609cba7

dsh-remind

任务做完了 / DSH 需要你批准或回答,但你在看别的窗口 —— 用 Windows 原生通知(带提示音)提醒你。

DSH 的会话跑完不会主动告诉你,切出去以后只能靠回来瞄一眼;DSH 等你批准或回答时更是干等着。这个插件把「这一轮真的做完了」和「DSH 需要你处理」都变成一条 Windows 通知,而且只在你不在看 DSH 的时候才打扰。


它做什么

能力 说明
完成任务提醒 顶层会话的 agent 从 running 落回 idle(这一轮真的结束);后台任务(jobs)落地时也会补一条
审批提醒 DSH 请求批准时提醒。只有真的在等你决定才提醒:先等 800ms 确认窗口,期间被自动批准/拒绝/自动审批器决定掉就不打扰
提问提醒 DSH 调用 ask_user_question 向你提问时提醒,通知里带问题原文(多个问题显示第一条 + 数量)
提示音 三类通知都可配提示音(default / reminder / alarm / im / mail / sms / call … 或 none 静音);默认任务完成用 default,审批/提问用更醒目的 reminder
只在该提醒时提醒 用 Win32 GetForegroundWindow 读当前前台窗口的标题与进程名,命中 focusMarkers(默认 DeepSeek Harness)就认为你正在看 DSH,不弹通知(审批/提问可单独配置)
通知形态 Windows 原生 Toast(进操作中心、可关闭、可点击)
通知内容 任务完成:DSH · 任务完成 + 会话标题 · 工作区 · 用时;审批:DSH · 需要批准 + 工具名 · 会话;提问:DSH · 需要你的回答 + 问题文本 · 会话

防误报设计

一轮「结束」到真正「没事干了」之间可能还有后续动作,所以判定做了四层:

  1. 只认顶层会话:ctx.agents.roots() 排除子代理——子代理结束时父轮还在跑,不该打扰。
  2. 静默期复查(默认 700ms):复查时仍须满足「agent 仍是 idle、inbox 没有排队的 nextTurn/nextStep、该会话没有 running|stopping 的 job、workspace/session-activity 里没有 turn/job/subagent」。任何一条不满足就放弃这次提醒。
  3. 同会话冷却(默认 5s):避免 turn 结束与 job 落地撞在一起弹两条。
  4. 宽限期(默认 1500ms):弹通知前先等 1.5 秒,你刚切回 DSH 就不打扰。

审批 / 提问提醒怎么判断「真的在等你」

  • 审批:订阅 approval/asked / approval/decided 审计对。注意 approval/asked 对每一次审批都会追加,包括策略为 never(直接拒绝)或自动审批器立即决定的情况——所以不是「一见到 asked 就弹」,而是先静默 attentionSettleMs(默认 800ms):期间收到同 id 的 approval/decided 就当作没发生。
  • 提问:订阅 tool/call,且工具名是 ask_user_question。解析其参数 JSON 取问题文本;同样先静默 800ms,期间收到同 callId 的 tool/result(你已经答完或工具已返回)就取消。
  • 子代理里发出的审批/提问会沿 session.header.parentSession 上溯,展示到顶层会话的标题上;turn/end 或会话销毁时清掉该会话所有待发提醒。

架构与关键取舍

src/index.js            宿主端 cordis 插件:订阅事件、注册 /api/dsh-remind.* 路由、拼通知
src/detect.js           「任务完成」判定(纯逻辑,可脱离 DSH 单测)
src/attention.js        「审批 / 提问」判定(确认窗口、去重、子代理上溯)
src/notify.js           Windows 通知层:调助手脚本、提示音归一化、解析结果
scripts/dsh-remind.ps1  单次 PowerShell 助手:宽限 → 前台判定 → WinRT Toast + 提示音(一次进程完成)
src/config.js           配置读写(原子写、BOM 容错、非法值夹紧)
src/contract.js         对 DSH 宿主接口的全部假设清单(升级体检用)
test/                   单测 + 真实通知实测
tools/                  侦察与验证工具(读 app.asar、带鉴权调路由、真实宿主端到端、doctor)

为什么是 PowerShell,而不是 Electron 的 Notification? DSH 桌面端把宿主跑在独立的 Node 进程里(@deepseek-ai/dsh-desktop-host),插件拿不到 Electron API;DSH 自身也没有对插件开放的 OS 级通知接口(整个壳里只有更新提示那一处 Notification)。所以走系统自带能力:Windows PowerShell 5.1 的 WinRT Toast API。整轮(含前台判定)实测约 300ms,任务完成这种低频事件完全够用。

为什么不用 koffi FFI 直接调 user32? koffi 的原生 .node 在 DSH 安装目录里有两套(3.1.1 / 3.3.2,互不兼容),且只能按绝对路径加载;插件里引它会把「DSH 升级 / 换机器」变成故障源。PowerShell 是每台 Windows 都有的稳定面。

前台判定为什么看窗口标题而不是页面可见性? GUI 可能在 Electron 窗口里,也可能在浏览器标签页里(比如 Edge 打开 http://127.0.0.1:19387)。两种情况的活动窗口标题都来自页面 <title>DeepSeek Harness</title>,所以标题匹配同时覆盖两种形态;浏览器里切到别的标签页时,窗口标题变成那个标签页的标题,于是正确判定为「不在看 DSH」。


安装

本机已安装并已生效(desktop profile):

dsh plugin --profile desktop add <克隆下来的仓库目录>

这一步会:把包以 link: 形式加进 %USERPROFILE%\.dsh\profiles\desktop\package.json 的 dependencies,并把 dsh-remind 追加到 dsh.profile.bundles。插件的 cordis.patch.yml 负责把自己挂进 profile 组合树:

- insert:
    - id: dsh-remind
      name: dsh-remind

卸载:dsh plugin --profile desktop remove dsh-remind,或在 GUI 的 Plugins 页里移除。

改完插件源码要重启 DSH 才生效:插件以 link: 方式安装,profile 热重载会重新执行 apply(),但 Node 的 ESM 模块缓存不会失效——已加载过的模块仍是旧代码。实测:改动 src/ 后触发 profile 重载,日志里出现的仍是旧版 ready 行;重启桌面端后才是新代码。纯配置改动(config.json 或 setConfig 路由)不受影响。


配置

配置文件:%USERPROFILE%\.dsh\plugins\dsh-remind\config.json(改完重启 DSH 生效;也可以用下面的 setConfig 路由热改)。

字段 默认 含义
enabled true 总开关
onlyWhenUnfocused true 只在「前台不是 DSH」时提醒;false = 每次都提醒
focusMarkers ["DeepSeek Harness"] 命中即视为「正在看 DSH」的窗口标题/进程名片段(子串匹配)
graceMs 1500 弹通知前的宽限期:你马上切回来就不打扰
settleMs 700 判定「真的做完了」前的静默期
minRunMs 0 短于此时长的任务不提醒(0 = 不限制)
cooldownMs 5000 同一会话两次提醒的最小间隔
notifyOnJob true 后台任务全部落地时也提醒
title DSH · 任务完成 通知标题
bodyTemplate {{session}} · {{workspace}} · 用时 {{duration}} 正文模板,支持 {{session}} {{workspace}} {{duration}} {{cause}};空片段连同分隔符自动省略
sound default 任务完成通知的提示音:default / im / mail / reminder / sms / alarm / alarm2…alarm10 / call / call2…call10 / loop / none
notifyOnApproval true DSH 请求批准时提醒
notifyOnQuestion true DSH 向你提问时提醒
attentionOnlyWhenUnfocused true 审批/提问是否也遵守前台判定(false = 正在看 DSH 也弹)
attentionGraceMs 300 审批/提问通知的宽限期(比任务完成更短)
attentionSettleMs 800 审批/提问的确认窗口:期间被决定/回答就不打扰
approvalTitle DSH · 需要批准 审批提醒标题
approvalBodyTemplate {{tool}} · {{session}} 审批正文模板,支持 {{tool}} {{reason}} {{session}} {{workspace}}
questionTitle DSH · 需要你的回答 提问提醒标题
questionBodyTemplate {{question}} · {{session}} 提问正文模板,支持 {{question}} {{session}} {{workspace}}
attentionSound reminder 审批/提问提醒的提示音
openOnClick false 开启后通知可点击,用默认浏览器打开带令牌的 DSH 地址
actionLabel 打开 DSH 可点击时的按钮文案
aumid "" Toast 的 AUMID;留空用系统 PowerShell 已注册的那个
debug false 把每次判定写进 events.jsonl,排查「为什么没提醒」时打开

自检与排查

1) 立刻弹一条测试通知 —— 在 DSH 页面(浏览器/桌面窗口均可)按 F12 打开控制台执行:

// 任务完成样式
fetch('/api/dsh-remind.test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
  .then(r => r.json()).then(console.log)
// 审批 / 提问样式(带 reminder 提示音)
fetch('/api/dsh-remind.test', { method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ kind: 'approval' }) }).then(r => r.json()).then(console.log)
fetch('/api/dsh-remind.test', { method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ kind: 'question', sound: 'alarm' }) }).then(r => r.json()).then(console.log)

2) 看当前状态、前台判定与最近判定:

fetch('/api/dsh-remind.status', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
  .then(r => r.json()).then(x => console.log(x.result))

返回里的 foreground 就是「现在算不算在看 DSH」的判定依据(窗口标题、进程名、是否命中);recent 是最近若干次任务完成判定(notified / focused / queued-work / jobs-running / session-active / cooldown …);attention.recent 是审批/提问判定,attention.pending 是还在确认窗口里的条数。

3) 热改配置(不重启):

fetch('/api/dsh-remind.setConfig', { method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ onlyWhenUnfocused: false, debug: true }) }).then(r => r.json()).then(console.log)

4) 判定轨迹:debug: true 时,%USERPROFILE%\.dsh\plugins\dsh-remind\events.jsonl 会记录加载与每次判定。

5) 命令行测试:

node --test                                        # 单测(判定逻辑、配置、路由装配)
$env:DSH_REMIND_E2E='1'; node --test test/notify.test.mjs   # 真实弹一条 Windows 通知

验证记录(本机实测)

项目 结果
前台窗口探测 ✅ 正确读出前台窗口标题与进程名,耗时 ~250ms
原生 Toast(中文 + emoji) ✅ 弹出并进入操作中心历史(History.GetHistory 可查)
提示音 ✅ Toast XML 带 <audio src="ms-winsoundevent:Notification.Reminder" loop="false"/>,实测可闻
通知层实测 ✅ node --test 全部通过(含前台命中时正确跳过、提示音参数)
真实宿主加载 ✅ desktop(DSH 0.2.1-rc.2)宿主启动即加载,events.jsonl 记录 ready: … approval=true question=true sound=default/reminder
真实任务端到端 ✅ 临时 profile 跑一轮真实任务:status running → status idle → notified,并真实弹出通知
真实提问端到端 ✅ 临时 web profile 里让 agent 真的调用 ask_user_question → 插件 attention.recent 记录 kind=question, action=notify,通知里带问题原文与 reminder 提示音
审批/提问通知样式 ✅ /api/dsh-remind.test 的 `kind=approval
插件路由 ✅ /api/dsh-remind.{status,config,setConfig,probe,test} 在真实宿主里可用(信封与裸 JSON 两种请求体都支持)
升级稳定性 ✅ 零依赖 + 零 peerDependencies,不会被 DSH 的版本兼容校验拦下(evaluatePluginCompatibility 无 peer 直接放行);bundle 解析失败只会「跳过 + stderr 一行」,不会导致 profile 启动失败
契约体检 ✅ node tools/doctor.mjs 五层全 PASS:20 条公开契约对着新版本 DSH 自带 API 目录逐条核对通过,另有 2 条实现核对(审批/提问事件名、提问工具名)通过

DSH 升级后的稳定性

结论:升级不会把插件卸载或禁用,真正的风险是宿主接口漂移;后者已用「契约清单 + 体检脚本」兜住。

升级不会动它(已核对 DSH 自身源码)

事实 依据
插件代码在本地克隆目录里,不在 DSH 程序目录里;升级只替换程序目录 桌面端更新走 app-update.yml 的 generic 源,替换安装目录;DSH home 与 profile 属用户数据
升级后 DSH 会重启宿主,插件从源码重新加载(等于一次干净的重载) 壳里的更新流程装完即重启
零 npm 依赖、零 peerDependencies → 永远不会被「版本不兼容」拦下 dsh-app-boot 的 evaluatePluginCompatibility() 第一句就是 if (!Object.hasOwn(fields, "peerDependencies")) return undefined,没有 peer 直接放行(对比:dsh-plugin-subscriptions 就因为有 peer 范围而被判不兼容、启动时拒绝激活)
bundle 解析只看「profile 的 node_modules 里能不能找到」 resolveBundleDir() 依次从安装锚点与 profile 目录解析,与版本无关
最坏情况是「被跳过」而不是「启动失败」 loadProfileDirectory() 对每个 bundle 逐个 try/catch:解析失败/缺 dsh.bundle/peer 不兼容 → 该 bundle 进 skippedBundles,profile 照常启动,stderr 打一行 dsh: skipping profile bundle "dsh-remind": …

真正的风险:宿主接口漂移

插件的每条假设都写在 src/contract.js 里,分三类:

  • 20 条公开契约(DSH 自己的 API 目录 dsh-tool-cordis/lib/types/api-catalog.js 里有声明):agent/status 事件、AgentStatus 取值、agents.roots()/get()、jobs.list()/events.subscribe()、sessions.get()、SessionHeader.cwd / parentSession、sessionTitle.get()、sessionProjections.stateOf()、workspace/session-activity 瀑布、approval/request 瀑布、session/event 与 tool/call 事件、connection.fetch.register() 等。
  • 3 条运行时形状(目录里没声明,属于 Agent 对象的内部字段):agent.status、agent.inbox.nextTurn/nextStep、agent.session。这三条是最大变数。
  • 2 条实现核对(API 目录里没声明、但插件依赖的实现细节):审批审计事件名(approval/asked / approval/decided)、提问工具名(ask_user_question)。doctor 直接读新版本 DSH 的实现文件确认关键字串还在。

关键假设失效时的降级路径:

假设失效 后果(降级,而不是崩溃)
agent.status 没了 静默期复查放宽 → 可能多提醒一次
agent.inbox 没了 当作「没有排队工作」 → 宁可多提醒,不漏
agent.session 没了 通知正文退化成「会话已完成」
sessionTitle / cwd 没了 正文里对应片段自动省略
connection.fetch.register 没了 只是没有自检路由,核心通知照常
审批事件名 / 提问工具名变了 只是审批或提问提醒失效,任务完成提醒照常
agent/status 事件本身改名/改语义 任务完成提醒会「静默失效」 → 由下面的体检负责第一时间发现

另外,workspace/session-activity 的判断刻意用否定表:只有已知不算干活的 schedule(已排期提醒)被忽略,其余任何种类都当作「还有活在跑」。所以新版本引入新活动种类时,插件会保守地少打扰,而不会把新种类的活误判成「已完成」。

升级后跑一次体检

node tools/doctor.mjs          # 五层体检,10 秒出结论
node tools/doctor.mjs --toast  # 顺带真弹一条通知
  • L1 包完整性:入口能 import、dsh.bundle.patch 与助手脚本齐全、powershell 就位
  • L2 profile 注册:依赖与 dsh.profile.bundles 条目、node_modules/dsh-remind 链接都在
  • L3 宿主契约:直接读新版本 DSH 自带的 API 目录,逐条核对上面 20 条公开契约;再读 DSH 的审批/提问实现文件核对 2 条实现细节 —— 哪条没了就点名哪条,并说明后果
  • L4 通知通道:真调一次 Windows 助手(前台探测;--toast 才弹)
  • L5 运行痕迹:读 events.jsonl 最近一次加载与判定;给了 --url <带令牌地址> 还能问运行中的宿主要 health

退出码 0 = 可用。运行期同一份清单也在生效:插件加载时逐项探测,缺失项会写进宿主日志,并出现在 /api/dsh-remind.status 的 health 字段里。

万一 L3 报某条契约失效

插件是本地 link: 的,改完源码重启 DSH 即生效:按 contract.js 里那条的 why 定位受影响代码 → 适配新接口 → node --test → node tools/doctor.mjs 复验。若 L1/L2 报缺失(例如 profile 被重建),一条命令装回来:

dsh plugin --profile desktop add <克隆下来的仓库目录>

已知边界

  • 通知卡片的“应用名”显示为 Windows PowerShell:Windows 要求 Toast 必须带一个已注册的 AUMID,而 DSH 桌面端从未调用 setAppUserModelId(壳里零命中),com.deepseek.dsh 只写在 package.json 里、运行时没用到。所以默认借用系统 PowerShell 的注册 AUMID。想显示成 “DeepSeek Harness”,需要给开始菜单快捷方式注册一个 AUMID(后续可做)。
  • 审批提醒只在「真的在等你决定」时出现:审批策略为 never(当前会话常见)或自动审批器瞬间决定时,approval/asked 之后同一刻就会 approval/decided,确认窗口会把它过滤掉——这是刻意的,避免为不需要你操作的审批弹窗。只有当审批真的挂起等待 UI 应答时才会提醒。
  • 提问提醒只认 ask_user_question 工具:这是 DSH 的标准提问工具;如果某个 profile 不加载它(例如 headless 模板的极简工具集),自然也不会有提问提醒。
  • 专注助手/勿扰模式会照常拦截 Toast(系统行为,插件无法绕过)。
  • 前台判定基于窗口标题/进程名子串。若你把 GUI 套在标题不含 DeepSeek Harness 的外壳里,请把该标题加进 focusMarkers。
  • openOnClick 默认关闭:开启后点击通知会用默认浏览器打开带令牌的地址(等于新开一个已登录的 DSH 页面,而不是聚焦已有窗口)。
  • 一轮结束后若宿主进程在静默期内直接退出(如 dsh headless "..." 一次跑完即退),判定来不及执行——短命进程的固有限制;桌面端宿主长期驻留,不受影响。

开发工具(tools/)

工具 用途
doctor.mjs 升级后体检:五层核对(包完整性 / profile 注册 / 新版本 API 目录契约核对 / 通知通道 / 运行痕迹)
asar.mjs 不装 Electron 也能列/读/解 app.asar(list / read / extract;也可当模块 import)
api.mjs 带启动令牌换 cookie 后调插件路由(Windows 命令行会吃掉裸 JSON 的引号,复杂请求体用 @文件)
e2e-turn.mjs 在真实宿主里建会话、发提示、轮询「任务完成」判定
e2e-attention.mjs 在真实宿主里让 agent 调用 ask_user_question,轮询「提问提醒」判定(兼容新旧两种 RPC 信封)
live-reload-check.mjs 临时摘除/加回 bundle 以触发 profile 热重载(用于确认插件是否已在运行中的宿主里生效)
verify-toast.ps1 查某个 AUMID 的操作中心历史,确认 Toast 真的被系统接受
—/ 5

No ratings yet

Verified DSH bundle

Commit a609cba71228

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout