DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

xubin0628 /

xubin0628/dsh-task-notifier

Verified

DSH 任务通知插件:任务完成 / 需要批准 / 需要回答 / 出错时的横幅、提示音与系统通知,并在设置里提供「通知」页自由配置

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@9ec1935b

dsh-task-notifier

DSH 任务通知器:任务完成 / 需要你批准 / 需要你回答 / 出错时,用「应用内横幅 + 提示音 + 系统通知」提醒你。

通知哪四件事

事件(DSH 公开事件) 触发条件 横幅标题
agent/status 会话从 running 变回 idle,且不是在等你输入 任务完成
approval/request 有工具调用需要你批准(沙箱/权限) 需要你的批准
user-questions/request 有工具在等你回答问题 需要你的回答
agent/error 某一步或某一轮出错 执行出错

两个 waterfall 事件(approval / user-questions)只做通知,然后原样 next() 交回默认流程,不改变也不拦截既有批准语义;同一轮已经报过错时不再补一条"完成",避免双响。

"卡在等你输入"不当作任务完成:代理停下来等你批准 / 等你回答时,agent/status 也会落到 idle, 但那不是跑完了 —— 那种暂停已经由 approval / question 通知过一次,再补一条"任务完成"会让同一次 暂停被通知两遍且结论相反。所以 approval/request 与 user-questions/request 会把该会话标记为 awaitingUser,直到你回复(agent/inbox/claimed)或该会话重新 running 才清除;被标记期间的 running → idle 不发"完成"。/status 会把这个集合列出来,方便排查。

设置页:通知

插件在 设置 → 左侧导航里注册了一个「通知」页(客户端插件的 settings.section 槽位),可以自由配置:

分组 可配置项
通知哪些事 总开关;任务完成 / 需要我批准 / 需要我回答 / 出错失败 四个独立开关;是否包含子代理
怎么提醒我 应用内横幅;系统通知;横幅停留秒数
提示音 提示音开关;音色(清脆 / 铃铛 / 水滴 / 脉冲 / 静音)+ 试听;音量滑块(0–100%,实时生效)
  • 设置存在浏览器 localStorage(键 dsh-task-notifier:settings:v1),改完立即生效,不需要重启客户端。
  • 音色只改变音品(波形 / 包络 / 八度),四类事件仍用不同音高区分,所以换了音色也听得出是哪一类。
  • cordis.patch.yml 里的 config 只作为初始默认值:只有当你从没在设置页里改过时才生效; 一旦动过任何一项,就以设置页为准。

它怎么工作

宿主进程 (lib/index.js)                     页面半区 A (assets/notifier-client.js)
  ├─ 订阅 4 个事件                        ┌─► ├─ EventSource('/dsh-notifier/events')
  ├─ webServer 注册 4 条路由 ─── SSE ─────┘   ├─ 应用内横幅(Shadow DOM,样式隔离)
  └─ webserver/index-inject 注入 <script> ──► ├─ 提示音(WebAudio 合成,无音频资源)
                                              └─ 系统通知(页面 Notification API)
                                                    ▲
页面半区 B (lib/client.js) ── 设置页「通知」────────┘
  dsh.client 声明的客户端插件,注册进 settings.section;
  改动写 localStorage 并广播事件,半区 A 立即生效(试听也走事件,音色合成只有一份实现)

注入走两条并存的通道:

  • webserver/index-inject 的内联 script 行 —— 官方桌面端(Electron)唯一生效的通道。桌面壳的 index.html 从安装包静态 dist 直出,不经过宿主 renderIndex(),tapIndex 到不了。 必须是内联行而不是 script-src 行:页面侧解释器对 script-src 是"加载失败即 reject 整个 boot", 内联行自己 createElement 并吞掉 onerror,路由不在也只是静默失败。
  • webServer.tapIndex —— 浏览器形态(dsh web)走 renderIndex(),这条生效。

安装

插件以 bundle 形式装进某个 profile:该 profile 的 package.json 里 dsh.profile.bundles 会出现 dsh-task-notifier,依赖以 link:(软链)或 file:(拷贝)指向插件目录。

⚠️ 官方桌面端(Electron)读的是 desktop profile,而这个 profile 由客户端独占管理, dsh plugin --profile desktop 会被拒绝。正确做法是在 DSH 会话里让 DSH 自己装:

让 DSH 调用内置 plugin_manager:
  install_bundle  target = link:<插件目录绝对路径>   # 软链安装:改完存盘即生效,最省事
  install_bundle  target = file:<插件目录绝对路径>   # 拷贝安装:自包含,源码丢了插件照跑

生效方式:新装通常热生效(返回 "application":"applied");换版本、或改 package.json (例如新增 dsh.client)需要重开客户端(返回 "restart-required")。

卸载:

plugin_manager  remove_bundle  target = dsh-task-notifier

配置

改 cordis.patch.yml 的 config 段(每项都可省略,省略即用 lib/index.js 里的默认值):

键 默认 说明
notifyOnDone true 任务完成通知
notifyOnApproval true 需要批准通知
notifyOnQuestion true 需要回答通知
notifyOnError true 出错通知
includeSubagents true 子代理 / teammate 也通知;false = 只通知主会话
suppressDoneAfterError true 同一轮已报错就不再补"完成"
coalesceMs 1200 同键事件在该窗口内只发一次,防重试/并行抖动刷屏
sound true 提示音
systemNotification true 系统通知(Windows 通知中心,点击切回窗口)
toastDurationMs 8000 横幅停留时长
errorToastDurationMs 15000 出错类横幅停留时长
volume 0.35 提示音音量 0~1

配置在重启客户端后生效(profile patch 改动需要一次 config reload,桌面端最稳的是重开客户端)。

自检

宿主侧(不需要看页面):

# 插件是否活着 + 当前配置 + 已连上的页面数 + 已发通知数 + 正卡在等谁输入
Invoke-RestMethod http://127.0.0.1:19387/dsh-notifier/status

# 回看最近发过哪些通知(排查"到底发了没 / 发了几条 / 是不是误报")
Invoke-RestMethod 'http://127.0.0.1:19387/dsh-notifier/recent?limit=20'

# 真的走一遍通知链路:页面会弹横幅 + 响铃 + 系统通知
Invoke-RestMethod 'http://127.0.0.1:19387/dsh-notifier/test?kind=approval'
#   kind 可取 done | approval | question | error

进程外单元验证(不需要 DSH 在跑):

node test/smoke.mjs

排查

现象 原因 / 处理
/dsh-notifier/status 返回 404 插件没激活。看 profile 的 patch 里 dsh-task-notifier 是否 disabled: false,然后重开客户端
路由通了但页面不弹 页面半区没注入。桌面端注入表在宿主启动时收集一次,改完必须重开客户端;浏览器形态直接 Ctrl+F5
有横幅没声音 浏览器/Electron 要求先有用户手势才能播音频。在页面里点一下任意位置即可解锁
没有系统通知 页面侧 Notification.permission 不是 granted。点一下页面任意位置会触发一次授权请求,允许即可
改了 assets/notifier-client.js 不生效 宿主按 mtime 热读取,正常刷新页面即可;不生效再 Ctrl+F5
改了 lib/index.js 不生效 宿主进程的 ESM 模块缓存不会因文件改动失效 —— 必须重开客户端

改代码后怎么生效(开发工作流)

改哪个文件,决定你要做什么才能看到效果——这三者完全不同,别搞混:

你改了 怎么生效 为什么
assets/notifier-client.js 刷新页面(F5)即可 宿主每次请求 /dsh-notifier/client.js 都按 mtime 重读文件
lib/client.js(设置页) 刷新页面(F5);新增/删除 dsh.client 声明要重开客户端 客户端包体由 clientModules 提供,页面刷新即重新拉取
lib/index.js 必须重开客户端 Cordis 重新 apply 时命中 Node 的 ESM 模块缓存,文件改动不会失效
cordis.patch.yml(配置项) 必须重开客户端 profile patch 需要一次 config reload
package.json(dsh.client / exports) 必须重开客户端 客户端入口图在宿主启动时合成
任何文件 改完先跑 node test/smoke.mjs 17 个用例覆盖事件、路由、客户端交付契约,几秒出结果

推荐的改 bug 流程:

# 1) 改代码
# 2) 先跑进程外回归(不需要 DSH 在跑,失败会非零退出)
node test/smoke.mjs
# 3) 只改了页面半区 → F5;动了宿主半区或 package.json → 重开客户端
# 4) 自检确认活着
Invoke-RestMethod http://127.0.0.1:19387/dsh-notifier/status

保存、备份与迁移

先记住一件事:这个插件的源码只有一份。 profile 里的 ~/.dsh/profiles/desktop/node_modules/dsh-task-notifier 不是拷贝,而是一个指向 源码目录的 Junction(目录联接)。源码目录一旦被移动、改名或删除,插件立刻失效 (表现为启动时 entry 激活失败)。

所以源码必须放在一个你不会随手清理的固定位置:

  • 开发期用 link: 安装:改完存盘就生效(按上面的表决定要不要重启),最省事。 代价是源码目录不能动。
  • 想彻底冻结/自包含用 file: 安装:pnpm 会把包拷贝进 profile,源码丢了插件照跑。 代价是每次改完都要重新 install 一次才生效。

切换安装方式(两者都是让 DSH 调内置 plugin_manager):

# 换到新位置(绝对路径,路径含空格/CJK 都要原样写)
install_bundle  target = link:<插件目录绝对路径>

# 或者改成拷贝安装,冻结当前版本
install_bundle  target = file:<插件目录绝对路径>

迁移步骤(把源码搬出临时目录):先复制整个目录到新位置 → 用新路径重新 install_bundle → 确认 Invoke-RestMethod .../dsh-notifier/status 正常 → 再删旧目录。

版本管理

本仓库用 git 管理。养成这个习惯就够了:

cd <插件目录>
git add -A
git commit -m "fix: 描述这次改了什么"

改 package.json 的 version 并打 tag,便于回滚和对照:

git tag v0.1.1

想跨机器/长期保存,再推到一个私有仓库,之后就能像其他 DSH 插件一样从 GitHub 安装:

install_bundle  target = github:<你的用户名>/<仓库名>

已知限制

  • 宿主进程里的模块改动无法热重载:Cordis 重新 apply 时会命中 Node 的 ESM 缓存,改 lib/index.js 只能重启客户端(或改文件名后重新 install)。
  • 桌面端的 index 注入表是一次性的:宿主启动时 collectIndexInjections() 经 IPC 交给渲染层,之后没有刷新路径。
  • 桌面端拿不到 Electron 的 Notification:运行时是被桌面壳 spawn 出来的独立子进程,插件跑在子进程里。 从无包标识进程调 WinRT toast 会报 0x80073D54,所以系统通知只能在页面侧用 Notification API。
  • 路由带一层极简信任栅栏(只接受回环 Host、拒绝 Sec-Fetch-Site: cross-site),因为事件流里含会话路径。
  • 设置存在浏览器 localStorage,不写回 profile 配置:换浏览器/清缓存会回到默认值。这是刻意的取舍 —— 走 Host 设置文档(configForms)需要给条目声明 schemastery schema 并依赖宿主设置服务, 一旦版本不匹配会让整条 entry 激活失败;通知偏好属于纯展示层,不值得冒这个风险。

目录结构

dsh-task-notifier/
├── package.json            # dsh.bundle.patch + dsh.client(exports["./client"])
├── cordis.patch.yml        # 挂载声明 + 宿主侧可配置项
├── lib/index.js            # 宿主半区:事件订阅 + 路由 + 注入
├── lib/client.js           # 客户端半区:设置里的「通知」页(__ModuleLoader__ 格式)
├── assets/notifier-client.js  # 页面半区:横幅 + 提示音 + 系统通知
└── test/smoke.mjs          # 进程外冒烟测试(17 个用例)

许可证

MIT —— 见 LICENSE。

—/ 5

No ratings yet

Verified DSH bundle

Commit 9ec1935b0544

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