dsh-notify —— DSH Web 通知
在「需要你介入」或「任务跑完」时提醒你,这样你可以把 DSH 丢在后台去干别的。
提醒方式与时机全部可以在 设置 → 通知 里改,不用碰配置文件。
零第三方依赖、零构建步骤:Host 半侧是一个空的 cordis 插件,全部逻辑都在浏览器半侧。
Desktop / in-page notifications for the DeepSeek Harness (
dsh) Web GUI: notify on approval requests,ask_user_question, turn end, background-job end and session errors — with a full settings page (Settings → 通知) for per-scene toggles, per-session mute, quiet hours, minimum duration, throttling, system notification / in-page toast / sound channels. Zero runtime dependencies, no build step.
安装
# 从 GitHub
dsh plugin --profile web add github:printz1/dsh-notify
# 本地开发时(软链,改完文件即时生效)
dsh plugin --profile web add link:/path/to/dsh-notify
装完重启 dsh 后端,然后刷新浏览器页面(客户端 bundle 是页面加载时才拉的)。
自检:node tools/verify-install.mjs 验宿主认不认这个包,node tools/selftest.mjs 验行为。
卸载:
dsh plugin --profile web remove dsh-notify,然后重启后端。
装在哪
- 会话头部右上角:一个铃铛按钮,单击 = 授权 / 一键开关总开关(详细配置在设置页)
- 设置 → 通知:完整配置面板
触发场景
| 场景 | 来源 | 通知标题 | 默认 |
|---|---|---|---|
| 需要你审批 | ctx.uiSession.pendingInteractions |
DSH · 需要你审批 |
开 |
| 模型在等你回答 | 同上(question / plan-review) |
DSH · 模型在等你回答 / 计划等你审阅 |
开 |
| 一轮指令结束 | api-session/status 由 running 变 idle |
DSH · 任务已结束 |
开 |
| 后台任务结束 | sessions.list 的 jobsBySession 差分 |
DSH · 后台任务已完成 / 被中断 / 失败 |
开 |
| 会话出错 | api-session/error |
DSH · 任务出错了 |
开 |
| 子会话(subagent)结束 | 同「一轮指令结束」 | DSH · 任务已结束 |
关 |
点通知 / 点气泡都会 focus 窗口并切到对应会话。
设置 → 通知(全部开关)
通知方式
| 项 | 默认 | 说明 |
|---|---|---|
| 启用通知 | 开 | 总开关,关掉后下面全部不生效 |
| 系统通知 | 开 | 浏览器 / Windows 通知中心;右侧按钮显示授权状态并可申请权限 |
| 页面内气泡 | 关 | DSH 页面右上角弹一条,不依赖浏览器授权;系统通知被拒时的替代方案 |
| 提示音 | 关 | 两声短促合成音(Web Audio,不引外部音频文件),旁边有「试听」 |
什么时候提醒
每个场景一个开关,另有三个细分项:
- 只提醒失败或被中断的(属于「后台任务结束」):后台任务多的时候建议打开,成功的不吵
- 子会话结束也提醒:默认关 —— 否则 subagent / workflow 每跑完一个子任务都响一次
过滤与勿扰
| 项 | 默认 | 说明 |
|---|---|---|
| 盯屏时也提醒 | 关 | 默认:页面在前台且聚焦时静默,免得每轮对话都响一次 |
| 免打扰时段 | 空 | 如 23:00-08:00(跨天自动识别,可逗号分隔多段),时段内完全不提醒 |
| 只提醒运行超过(秒) | 0 | 短任务不打扰;页面加载时已在跑的任务不受此限制 |
| 同一会话提醒间隔(秒) | 0 | 只作用于「一轮结束」和「后台任务」;审批 / 提问 / 出错永不节流 |
静音的会话
- 「静音此会话」把当前打开的会话加进名单,名单里可逐个「取消静音」
- 静音的会话完全不提醒,连审批也不提醒 —— 适合你就是要它自己跑完的长任务
诊断
- 测试系统通知 / 测试气泡 / 测试提示音:改了设置没动静时,先确认渠道通不通
- 最近通知:最多 50 条,显示时间 + 内容 + 实际生效的渠道(如
[系统+气泡]),刷新页面清空 - 恢复默认设置:把配置清回出厂值
一个必须先做的动作
浏览器要求「用户手势」才能申请通知权限,所以系统通知第一次要手点一次:
- 点会话头部右上角的铃铛,或
- 打开
设置 → 通知,在「系统通知」那一行点 请求权限
如果被拒过:点地址栏左侧的图标 → 网站设置 → 通知 → 允许,再回来点「请求权限」。
http://127.0.0.1:3080 属于安全上下文,通知 API 可用。
不想授权也行:把「页面内气泡」打开即可,它不依赖任何授权。
两个可见渠道都关掉时,会退回闪烁标签页标题(🔔 …),切回标签页自动恢复。
配置存在哪
localStorage 的 dsh-notify:config 一个键(JSON)。不走 DSH 官方设置系统,
因为它的命名空间是白名单制,第三方客户端插件读不到自己的。
0.1.x 的三个旧键(dsh-notify:enabled / :scenes / :notify-when-focused)
在首次加载时自动迁移进新配置,之后不再读取。
想直接改也行(改完刷新页面):
// 例:只在后台提醒,且夜间勿扰
localStorage.setItem('dsh-notify:config', JSON.stringify({
enabled: true, system: true, toast: false, sound: false,
scenes: { approval: true, question: true, turnEnd: true, job: true,
jobFailuresOnly: false, error: true, subagent: false },
notifyWhenFocused: false, quietHours: '23:00-08:00',
minDurationSec: 0, throttleSec: 0, mutedSessions: []
}))
非法值会被规整(布尔回落默认、数字夹紧到 0..86400、静音名单只留字符串),不必担心写坏。
目录结构
dsh-notify/
├─ package.json # dsh.bundle.patch + dsh.client 双面声明
├─ cordis.patch.yml # 插入一行 `name: dsh-notify` 到 web profile 的插件树
├─ lib/index.js # Host 半侧:空的 apply()(合法插件入口,零副作用)
├─ lib/client.js # 浏览器半侧:全部逻辑(通知链路 + 头部按钮 + 设置面板)
├─ tools/selftest.mjs # 行为自检:假 DOM/假 ctx 里跑 122 项断言
├─ tools/verify-install.mjs # 安装自检:宿主认不认这个客户端包(30 项)
└─ README.md
三条硬性约束(改坏了会静默不加载)
cordis.patch.yml里那行的name必须是裸包名(dsh-notify)。客户端模块扫描用exactPackageSpecifier(),带子路径的三段写法直接返回undefined。lib/client.js里window.__ModuleLoader__.load({ id })的id必须与包名逐字相同 —— 引导图里的 entry id 就是包名。dsh.client.inject留空。它声明的是模块图依赖(本插件要require哪些 DSH 客户端包), 本插件只require('react');remote/sessions/uiSession/slots全走 cordis 服务注入(exports.inject),cordis 会让插件在依赖服务就绪前停在 PENDING, 所以不需要也不应该在dsh.client.inject里列@deepseek-ai/dsh-client-ui-slots这类不是 Loader 行的包。
为什么铃铛挂在会话头部而不是侧栏
会话头部右上从右到左是 headerCorner(single,已被 sidebar-right 占用)→
headerUtilities(list,现有 open-in-app order -10、session-log-download order 0)
→ headerActions(在标题旁)。本插件挂 headerUtilities、取 order: 10 排到最右。
早先挂在侧栏底部的
sidebar.footer.action:那是个不换行的横向 flex 行,而重启后端 (width: calc(100% + 8px))和 cordis 面板(width: 100%)各自声明满宽,第三个成员必然 被挤出侧栏右边缘裁掉 —— 这就是「太靠右、看不清」的根因。已弃用该位置。
为什么设置面板用 settings.section
settings.section 是 list 槽(root 作用域),每个条目就是设置弹窗左侧的一页
(现有 general 0 / models 10 / plugins 15 / agent-presets 20),本插件取 30 排在最后,
注册项 label: '通知' 就是导航文字。面板里的开关/输入框直接用 DSH 的 CSS 变量手写,
不依赖 @deepseek-ai/dsh-client-ui-primitives(该包没有独立类型,属未公开内部)。
自检
两个脚本,各管一段,都不用开浏览器:
1. 行为自检
node tools/selftest.mjs
在 Node 里用假的 window / document / Notification / AudioContext + 假的 client ctx
真跑 lib/client.js,122 项断言覆盖:五个通知场景与子会话过滤、渠道三选与兜底、
免打扰时段、时长下限、节流、静音会话、前台静音、总开关、场景开关、旧配置迁移、
设置面板(注册项/分组/11 个开关/文本数字输入/静音名单/恢复默认/诊断按钮/日志)、
头部按钮(槽位/order/尺寸/SVG/状态)、单个订阅抛错不影响其它场景。
2. 安装自检
node tools/verify-install.mjs
客户端包被宿主忽略时不会报错,只是页面上什么都不出现 —— 所以单独验这一条链路:
裸包名判定(含「带子路径会被静默忽略」的对照)、dsh.client 声明过宿主的校验、
exports["./client"] 能解析到真实文件、bundle 自报 id 与行名逐字一致、
Host 半侧导出 apply() 且零 import、bundle 能在无 DOM 环境执行并导出 apply/inject。
最后直接 import 宿主的 stripClientSuffix / orderByModuleGraph 真货做交叉验证。
宿主的校验函数是逐字抄过来的,宿主升级后这里不会自动跟 —— 所以那两条真导入的交叉验证 是关键:它们会跟着宿主一起变。30 项全绿只说明「宿主会认这个包」,不代表页面渲染正常。
排错
| 现象 | 原因 / 处理 |
|---|---|
| 头部没有铃铛按钮 | 后端没重启;或没打开任何会话(该槽位是会话级作用域);或 dsh --profile web --dump-config | grep -A1 'id: notify' 里没有这一行 |
| 设置里没有「通知」页 | 后端没重启;或客户端 bundle 没重新拉取(必须刷新页面) |
| 有按钮但从不弹 | 没点过授权;或页面正前台聚焦(见「盯屏时也提醒」);或总开关关了;或该会话被静音;或落在免打扰时段 |
| 系统通知通不了 | 设置 → 通知 看「系统通知」那行的状态;被拒就去站点设置放开;实在不行改用「页面内气泡」 |
| 想确认它加载了 | 浏览器控制台看 window.__DSH_BOOT__ 里有没有 dsh-notify 行;或看「最近通知」有没有记录 |
| 改代码后没变化 | 软链是实时的,但需要重启后端 + 刷新页面;node --check lib/client.js 先确认语法 |
卸载
dsh plugin --profile web remove dsh-notify
然后重启后端。localStorage 里的 dsh-notify:* 可自行清掉。
No comments yet. Be the first to write one.