wingsky-1/dsh-plugin-hub--packages-dsh-notifier ↗★ 16
@wingsky-1/dsh-notifier
审批/完成/错误事件通知:浏览器 Notification + 系统原生 toast(Windows PowerShell WinRT / macOS osascript / Linux notify-send,均无需额外安装);提示音可配、每条通知独立显示不互相替换、非安全上下文自动降级横幅
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:wingsky-1/dsh-plugin-hub#9dc74922bc481b9e517f245538501740b6b9bbb4&path:packages/dsh-notifier说明文档
阅读完整 README ↗配置(官方 settings 存储:设置 → 插件 → dsh-notifier 卡片可改)
配置保存在官方 settings 存储(/settings.yaml,命名空间 dsh-notifier),
经「设置 → 插件 → dsh-notifier」卡片读写(issue #76);旧版自建
dsh-notifier.json(位于 DSH_HOME,默认 ~/.dsh)在升级后启动时一次性迁移
进官方存储,原文件改名
dsh-notifier.json.migrated.bak(损坏则改名 .corrupted.bak,不写入),
自建读写链路已废弃。
未知键语义(前向兼容,issue #470):dsh-notifier 对配置中无法识别的键 采取「透传保留」策略——读取与写入口径一致,未知键不会被丢弃,也不会被校验 或改写(仅组合层装配键名例外,见下):
- 读取:
GET /api/dsh-notifier/config的user(settings 用户层原始节)与effective(生效配置)均原样返回未知键,供未来版本/第三方键保持可见; - 写入:
PUT /api/dsh-notifier/config为增量 patch——仅合并提交的已知键; 存量 user 层中已有的未知键不受已知键保存影响,本次 patch 中携带的未知键 一并原样保留(不会静默丢弃)。纯未知键 patch(如{"futureKey":1})返回 200 并写入;仅空 patch{}(或无任何可写键,如只含装配键)返回 400 「需至少包含一个配置键」; - 升级路径:某键在某版本还是未知键(已透传进 user 层)、下一版本成为已知键
时——旧脏键不会被自动清洗(升级本身不覆盖用户已表态字段);读取时
normalize 对已知键非法值丢弃回默认(脏值不影响生效配置与其他键);你主动
提交该键且值非法时才返回 400 + hint。若想清除残留脏键,可在
settings.yaml中手动删除; - 存量迁移:旧
dsh-notifier.json的未知键在迁移时透传保留——user 层 缺失则补写、已存在不覆盖;纯未知键 legacy 不再被当作「无有效键」丢弃; - 边界例外:
patch必须是对象:数组、null等非对象形态一律 400(数组不会按数字 索引透传成脏键);- 原型链/特殊成员键(
__proto__、constructor、prototype、toString、hasOwnProperty、valueOf等,JSON 文本可注入为自有键)在读取透传与写入 通道中一律剔除,不参与校验也不写入; - 组合层装配键(
configFile/toastScript/historyFile/statusFile/enabled)是 cordis 组合层/启动参数,不进入 settings 用户层——PUT 与 迁移提交同名键一律剔除,entry 组合层走白名单过滤; - Bark 频道实例内
device_key/device_keys/ciphertext与 webhook 频道实例内WEBHOOK_RESERVED_KEYS(auth_token/access_token/bearer_token/api_key/apikey/client_secret/secret/password_hash)保留键仍一律剔除/写拒, 未知参数仅透传 string/number 值; - 未知键不参与合法性校验(已知键非法仍返回 400 + hint)。
后果提示:升级后若设置页未显示某字段但 settings.yaml 中仍在,属预期保留
行为,不会因保存其他已知配置而丢失。
配置项示例(默认值;channels / kindRoutes / allowKinds 为 M2 新增键):
{
"notifyAsk": true,
"notifyQuestion": true,
"notifyTaskDone": true,
"notifySubagentDone": false,
"notifyTaskError": true,
"notifyTurnEnd": false,
"systemNotify": true,
"browserNotify": true,
"notifyWhenVisible": false,
"notifySound": true,
"browserSound": true,
"systemSound": true,
"quietHours": { "enabled": false, "start": "22:00", "end": "08:00", "allowKinds": [] },
"errorMergeWindowMs": 60000,
"askRemindMin": 5,
"doneMergeWindowMs": 3000,
"historyMaxAgeDays": 0,
"maxConnections": 16,
"channels": [],
"kindRoutes": {},
"allowKinds": [],
"sanitizeContent": true
}
maxConnections:SSE 连接表上限(默认 16,范围 1~1024)。含义为服务端未释放句柄数, 非「在线设备数」——半开连接(设备息屏/切网/NAT 静默掐断)不发 FIN,close/error 不触发。 #515 起连接表由 shared/sse-hub 管理,三路回收互补:stalled 回收(写被拒连续超 90s → 断开)、maxAge 轮换(存活超 120min 且无业务帧 → 主动断开,客户端自动重连 + since 补拉无感知)、上限淘汰(超出淘汰最老)。上限保证表有界;若长期持续超限 (淘汰后客户端重连、再次被淘汰的 churn 循环),说明该值低于峰值并发连接数,应调大 到不小于峰值再观察。连接回收路径计数见/api/dsh-notifier/health的sseEvicts。
每通道声音(#640 / #641)
弹窗与声音按通道独立(浏览器 / 系统各一套),支持「只弹不响 / 只响不弹 / 静默」:
| 键 | 类型 | 语义 |
|---|---|---|
browserNotify / systemNotify | boolean | 既有弹窗开关(沿用) |
browserSound | boolean | 音色 id | 浏览器通道声音 |
systemSound | boolean | 音色 id | 系统通道声音 |
notifySound | boolean | 废弃只读别名(见下) |
取值:false = 静音(弹窗仍可弹、不发声);true = 跟随系统默认;
音色 id = 显式内置音色(ding / bell / chime / pop——4 音色全平台语义一致,
在设置卡每通道的「音色」下拉中选择,可点 ▶ 试听:试听为浏览器本地 Web Audio 合成,
仅作听感参考,实际系统提示音随平台与系统设置)。
投递组合(2×2 全组合有效):
| 弹窗 | 声音 | 行为 |
|---|---|---|
| 开 | false | 弹通知实体,静音 |
| 开 | true | 弹通知实体,发声交给系统默认 |
| 开 | 音色 id | 弹通知实体;应用自播对应音色(系统通知静音防双响) |
| 关 | false | 完全不投递(静默) |
| 关 | true/音色 id | 只响不弹:不弹实体、仅自播(页面存活 / 宿主自播) |
notifySound(旧全局键)降级兼容别名:仅保留读取与存量迁移,设置页不再写它; 读取时browserSound/systemSound缺失回落notifySound,再缺省true; 存量 legacy json 迁移会补写browserSound/systemSound= 你的notifySound旧值 (settings user 层存量不迁移,读面回落 + 首次保存固化)。旧版关闭过提示音的存量 用户在升级后仍保持静音(新键按旧值等价初始化),不会「突然有声」。- 浏览器声音解锁前提:浏览器
true/音色自播需要页面音频已解锁——浏览器自动 播放策略要求一次用户交互(打开通知中心 / 声音行任何交互都会解锁 AudioContext); 纯后台从未交互的页面,声音可能不可用(此时通知照常弹出、仅无声),属浏览器 策略约束而非插件缺陷。 - Linux
true特例(#640 修复):Linux 桌面守护进程对声音 hint 支持参差 (GNOME 默认无声 / KDE 2025 才支持 / Xfce 依赖 libcanberra),systemSound=true解释为「默认事件音自播」——宿主用pw-play(PipeWire)或paplay(PulseAudio)播message-new-instant事件音(freedesktop 声音主题),不依赖 守护进程。headless(无桌面/音频会话)服务器静默。存量 Linux 升级行为变化: 之前系统通知无声(notify-send 无声音 hint),升级后声音开 = 自播事件音(需宿主 有音频会话且安装 pw-play/paplay 之一;播放器/事件文件缺失时静默,不影响通知)。 - 音色 × 平台映射(近似,尽力而为):
| 音色 | 浏览器(Web Audio 合成) | macOS | Linux(freedesktop 事件音) | Windows |
|---|---|---|---|---|
ding | 双短高音 | Glass(NSSound) | message-new-instant.oga | C:\Windows\Media\Windows Ding.wav |
bell | 单中高音 | Tink | bell.oga | Windows Chimes.wav |
chime | 三音上行 | Sosumi | complete.oga | Windows Chord.wav |
pop | 短促低音 | Pop | message.oga | Windows Balloon.wav |
true(跟随系统) | OS 默认(不 silent) | Glass(现状保留) | 默认事件音自播 | toast 默认系统音;只响不弹时近似默认音 wav |
macOS 发声受系统「允许通知声音」设置约束;Windows 音色经宿主 SoundPlayer
播放系统内置 wav(白名单路径,缺失静默);Linux 事件文件为
/usr/share/sounds/freedesktop/stereo/ 下基线包确定存在的 oga(多路径探测,
缺失静默)。自播一律数组传参(无 shell 拼接面),文件走白名单路径。
- 宿主平台见
/api/dsh-notifier/health的platform字段(设置页系统卡按它显示 平台提示——浏览器 OS 与宿主 OS 可能不同机,别混淆)。
Bark 推送频道(M2,issue #366)
设置 tab「通知中心 → 投递频道 → 添加 Bark 推送」配置(也可直接编辑上述配置 JSON)。
每实例字段:id(自动生成后锁定)、name(显示名)、baseUrl(Bark 服务器地址,
http/https)、deviceKey(Bark App 内查看;响应中一律掩码 ********,提交掩码 =
保持原值)、enabled(默认 false——出站授权须显式开启)。
可选参数(全部缺省不发送;未知 string/number 键原样透传,Bark 未来参数前向兼容;
device_key/device_keys/ciphertext 为保留键不可透传):
| 字段 | 说明 |
|---|---|
sound | 铃声名(Bark Sounds 列表) |
group | 分组(同组在手机上折叠) |
icon | 图标 URL(需手机网络可达,非服务器可达;SVG 需 iOS 17+;留空用 Bark 默认) |
url | 点击通知跳转 URL |
badge | App 角标数字 |
level | 实例级紧急度覆盖;缺省按事件 severity 自动映射:failure→timeSensitive、warning/success→active、info→passive |
levels | 按事件(kind)紧急度稀疏映射(见下) |
levels(kind→level 稀疏映射矩阵):为具体事件类型指定 Bark 紧急度,
优先于实例级 level 与 severity 自动映射;未配置的类型走默认。适合「提问必响、
子任务完成静音」这类按事件差异化诉求:
{ "id": "phone", "type": "bark", "baseUrl": "https://api.day.app", "deviceKey": "…",
"enabled": true, "levels": { "question": "timeSensitive", "subagent-done": "passive" } }
- 键为事件 kind(内置
ask/question/done/subagent-done/error/turn-end/test或动态 kind,任意字符串); 值限active/timeSensitive/passive/critical;至多 64 项、每键至多 64 字符。 - 完整优先级:
levels[kind]>level> severity 映射 > 不携带。 - 注意:
critical需苹果特殊授权(普通 App 无法申请),未获授权时 Bark 可能降级/拒绝。 - 与
kindRoutes(kind→channelId[] 路由)正交:路由决定「投给哪些频道」,levels决定「在本实例上多响」。
投递可靠性:10s 硬超时、网络错误/5xx 重试 ×2(4xx 不重试)、实例级在途并发 ≤2
(内置频道不受限);成功判定双查 HTTP 2xx + 响应体 code===200。
投递终态(成功/失败 + 脱敏错误摘要)落盘 DSH_HOME 下的 dsh-notifier-status.json
(默认 ~/.dsh)并经
wingsky-notify/sent 事件广播(cordis Events),设置页频道卡状态行在卡片加载
与发送测试后刷新(无轮询,D20 口径)。
通知历史 jsonl、频道投递状态 json、SSE seq 计数文件(
notifier-seq.json)与 旧版迁移源 json 的落盘/读取路径均感知DSH_HOME(#510):未设置时为~/.dsh, 设置后随隔离 home 走——隔离环境(多实例 / 测试沙箱 / dsh-verify-isolated) 读写面不触碰真实~/.dsh。
kindRoutes:kind → channelId[] 稀疏路由(如 { "error": ["browser", "system", "bark:phone"] });
未声明条目的 kind 广播全部启用频道;设置页事件区可双向编辑(与频道卡共享同一份配置)。
allowKinds:已确认的动态 kind 清单(其他插件注册的通知类型经你确认后持久化于此)。
Webhook 推送频道(#508)
设置 tab「通知中心 → 投递频道 → 添加 Webhook 推送」配置(也可直接编辑上述配置 JSON)。
用途:安卓经 ntfy / Gotify / 自建推送网关接收通知,补齐 Bark(iOS)未覆盖的推送
通道——每次投递向 url POST 一份 JSON body。
每实例字段(type 固定为 "webhook"):
| 字段 | 说明 |
|---|---|
id | 实例 id(2-32 位小写字母/数字/连字符,创建后锁定;kindRoutes 对齐键与掩码回填对齐键) |
name | 显示名(缺省回退 id) |
url | 目标地址(http/https;normalize 规范化为 origin+path——去 query/hash、拒绝带凭据 URL) |
enabled | 是否启用(默认 false——出站授权须显式开启) |
auth | 认证方式:none(默认)/ bearer / basic / header |
token | bearer 认证令牌(secret:响应一律掩码 ********) |
username | Basic 认证用户名(非 secret) |
password | Basic 认证密码(secret:响应一律掩码) |
headerName / headerValue | 自定义请求头认证(headerValue 为 secret:响应掩码);头名限字母/数字/连字符(≤64 字符),禁 content-type / content-length / host / cookie / authorization |
preset | 预设:ntfy(默认)/ gotify / custom(自建网关);决定 {{priority}} 映射与默认模板 |
template | JSON body 模板(≤8192 字符;留空 = 预设默认模板) |
timeoutSec | 投递超时秒(1-60,默认 10;服务端权威 clamp) |
预设与 {{priority}} 频道感知映射(按 preset 选择映射表;{{severity}} 恒为 severity 原文):
| preset | info | success | warning | failure |
|---|---|---|---|---|
ntfy | default | low | high | urgent |
gotify | 3 | 3 | 7 | 9 |
custom 不映射——{{priority}} 直出 severity 原文,由网关自行处理。
模板占位符清单:{{title}}、{{message}}、{{kind}}、{{severity}}、{{priority}}(映射见上表)、{{source}}(渲染为空串,预留位)、{{ts}}(毫秒时间戳取整,数字直出——唯一允许以裸值形态出现在模板中的占位符)。
渲染语义(JSON-aware 两步法):先把 {{ts}} 替换为数字字面量 → 模板整体 JSON.parse → 树遍历仅对字符串值做占位符替换 → 重新 JSON.stringify。替换发生在已解析字符串内部、重新序列化时统一转义——通知内容含引号 / "}} 也无法逃逸出字符串注入额外字段(防注入收口)。模板不是合法 JSON = 该频道投递失败并落记录(不静默降级为文本,不影响其他频道)。ntfy 预设默认模板含 "topic": "" 占位,投递前改成你的主题名。
投递可靠性:超时 1-60s(默认 10);失败不自动重试——4xx / 5xx / 网络错误 / 渲染失败统一为失败终态,落 status 文件与通知历史(错误摘要已脱敏),可经「发送测试通知」重发验证。kindRoutes 中以 webhook: 引用(与 bark: 同款 type:id 形态)。
实例示例(与 Bark 实例同存于 channels 数组,id 跨类型去重):
{ "id": "droid", "type": "webhook", "url": "https://ntfy.sh/mytopic",
"enabled": true, "auth": "bearer", "token": "…", "preset": "ntfy", "timeoutSec": 10 }