wingsky-1/dsh-plugin-hub--packages-dsh-notifier16

@wingsky-1/dsh-notifier

审批/完成/错误事件通知:浏览器 Notification + 系统原生 toast(Windows PowerShell WinRT / macOS osascript / Linux notify-send,均无需额外安装);提示音可配、每条通知独立显示不互相替换、非安全上下文自动降级横幅

包名
@wingsky-1/dsh-notifier
版本
0.2.3
许可证
MIT
最近更新
2026年9月12日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:wingsky-1/dsh-plugin-hub#9dc74922bc481b9e517f245538501740b6b9bbb4&path:packages/dsh-notifier

配置(官方 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/configuser(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__constructorprototypetoStringhasOwnPropertyvalueOf 等,JSON 文本可注入为自有键)在读取透传与写入 通道中一律剔除,不参与校验也不写入;
    • 组合层装配键(configFile / toastScript / historyFile / statusFile / enabled)是 cordis 组合层/启动参数,不进入 settings 用户层——PUT 与 迁移提交同名键一律剔除,entry 组合层走白名单过滤;
    • Bark 频道实例内 device_key / device_keys / ciphertext 与 webhook 频道实例内 WEBHOOK_RESERVED_KEYSauth_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/healthsseEvicts

每通道声音(#640 / #641)

弹窗与声音按通道独立(浏览器 / 系统各一套),支持「只弹不响 / 只响不弹 / 静默」:

类型语义
browserNotify / systemNotifyboolean既有弹窗开关(沿用)
browserSoundboolean | 音色 id浏览器通道声音
systemSoundboolean | 音色 id系统通道声音
notifySoundboolean废弃只读别名(见下)

取值: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 合成)macOSLinux(freedesktop 事件音)Windows
ding双短高音Glass(NSSound)message-new-instant.ogaC:\Windows\Media\Windows Ding.wav
bell单中高音Tinkbell.ogaWindows Chimes.wav
chime三音上行Sosumicomplete.ogaWindows Chord.wav
pop短促低音Popmessage.ogaWindows Balloon.wav
true(跟随系统)OS 默认(不 silent)Glass(现状保留)默认事件音自播toast 默认系统音;只响不弹时近似默认音 wav

macOS 发声受系统「允许通知声音」设置约束;Windows 音色经宿主 SoundPlayer 播放系统内置 wav(白名单路径,缺失静默);Linux 事件文件为 /usr/share/sounds/freedesktop/stereo/ 下基线包确定存在的 oga(多路径探测, 缺失静默)。自播一律数组传参(无 shell 拼接面),文件走白名单路径。

  • 宿主平台见 /api/dsh-notifier/healthplatform 字段(设置页系统卡按它显示 平台提示——浏览器 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
badgeApp 角标数字
level实例级紧急度覆盖;缺省按事件 severity 自动映射:failure→timeSensitivewarning/success→activeinfo→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
tokenbearer 认证令牌(secret:响应一律掩码 ********
usernameBasic 认证用户名(非 secret)
passwordBasic 认证密码(secret:响应一律掩码)
headerName / headerValue自定义请求头认证(headerValue 为 secret:响应掩码);头名限字母/数字/连字符(≤64 字符),禁 content-type / content-length / host / cookie / authorization
preset预设:ntfy(默认)/ gotify / custom(自建网关);决定 {{priority}} 映射与默认模板
templateJSON body 模板(≤8192 字符;留空 = 预设默认模板)
timeoutSec投递超时秒(1-60,默认 10;服务端权威 clamp)

预设与 {{priority}} 频道感知映射(按 preset 选择映射表;{{severity}} 恒为 severity 原文):

presetinfosuccesswarningfailure
ntfydefaultlowhighurgent
gotify3379

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 }