dsh-macos-notifier
veritaswiki/dsh-macos-notifier
会话结束本轮时弹出以会话名为标题的 macOS 通知,点击通知回到该会话;同时一定会再显示一张应用内提示(前台 9 秒、后台 60 秒)。系统横幅需要在 macOS 设置里允许 DeepSeek Harness 发通知,否则只有应用内提示。
安装
dsh plugin --profile web add github:veritaswiki/dsh-macos-notifierREADME
dsh-macos-notifier — 会话结束时的 macOS 通知
会话干完这一轮时给你一条标题为会话名的 macOS 通知,点一下回到那个会话。通知由 DSH 自己的渲染层发出:不起子进程、不调用 osascript、不写状态文件。
它做什么
- 会话从「运行中」变成「已停止」时发一条 macOS 系统通知——
onStatus()只放行prev === true && isRunning === false这一条边沿,运行中途的状态抖动不弹。 - 通知标题是会话名:取
sessions快照里的byId[id].title,读不到时回退label,再读不到用DSH。 - 点通知回到那个会话:
uiWorkspace.openSession(sessionId)。 - 同一会话的通知互相替换,不堆成一列:
tag固定为dsh-session-<sessionId>;同一会话 2 秒内不重复发(THROTTLE_MS = 2000)。 - 系统通知之外必定再发一张应用内提示(同一个事件两路都发):前台 9 秒、后台 60 秒后自动消失,同屏最多 3 张。
- 会话头部工具区有一个小铃铛:点一下发一条测试通知,用来确认通道通不通。
- 子代理会话、带父会话的会话、空白会话不发通知(
isNotifiable()过滤)。
它怎么做
全部逻辑在 client.js,跑在 DSH 的渲染层里;index.js 是一个空的 no-op 插件。
| 环节 | 实现(client.js) |
|---|---|
| 触发 | ctx.get('remote').$on('api-session/status', (sessionId, isRunning) => …),交给 onStatus() 判断边沿 |
| 兜底轮询 | remote.$on 不可用时退化为 setInterval(…, 800),比对快照里的 running 字段 |
| 会话过滤 | isNotifiable() 跳过 origin === 'subagent'、带 parentSessionId、blank === true 的会话 |
| 标题 | ctx.get('sessions') 快照的 byId[sessionId].title → 回退 label → 回退 'DSH' |
| 系统通知 | new Notification(title, { body, tag: 'dsh-session-' + sessionId, silent: false }) |
| 节流 | THROTTLE_MS = 2000,按 sessionId 记时,窗口内直接返回 false |
| 点击(主路径) | n.onclick → ctx.get('uiWorkspace').openSession(sessionId) |
| 点击(兜底) | 发通知时把 sessionId 写进 localStorage['dsh.macos-notifier.pending'](PENDING_TTL_MS = 90000),window 的 focus 事件里 takePending() 补跳一次 |
| 应用内提示 | 命令式 DOM:document.body 上挂 [data-mn-layer] 容器,不经过 React 插槽,同屏最多 3 张 |
| 小铃铛 | slots.inject('conversation.session.header.utilities', …),id: 'macos-notifier-action',order: 90 |
| 样式宿主 | slots.inject('shell.overlay', …) 只注册一段 <style>,id: 'macos-notifier-style',order: 200,随插件卸载移除 |
| 宿主侧 | index.js 的 apply() 是空函数,只提供一行可启停的 Loader entry |
因为通知和跳转在同一个渲染层里,点击时手里直接握着 sessionId:
- 不起子进程:
index.js无 import,client.js只用宿主提供的require('react')和浏览器的Notification/ DOM API; - 不调用 osascript,也就不会把通知归属到别的 App;
- 不写状态文件,也不需要自定义 URL 协议。
安装
本插件是 DSH(Cordis)bundle,三种装法。
方式 A:让 Agent 用 plugin_manager 安装
在 DSH 里对 Agent 说「装 dsh-macos-notifier」,它会调 plugin_manager:
action: "install_bundle",target填安装 spec:- 已 clone 到本地:
file:/abs/path/to/dsh-macos-notifier - 或仓库:
github:veritaswiki/dsh-macos-notifier
- 已 clone 到本地:
- 装完
action: "set_bundle"启用。
方式 B:手动挂到 profile
- 把仓库放在任意目录(下面记作
/abs/path/to/dsh-macos-notifier)。 - 编辑 profile 的
package.json(~/.dsh/profiles/<profile>/package.json):dependencies里加"dsh-macos-notifier": "link:/abs/path/to/dsh-macos-notifier"dsh.profile.bundles数组里加"dsh-macos-notifier"
- 在 profile 目录安装依赖(DSH 下次启动时也会自行安装),然后重启 DSH。
方式 C:从源码
git clone https://github.com/veritaswiki/dsh-macos-notifier.git到任意目录。- 同方式 B,只把依赖写成
"dsh-macos-notifier": "file:/abs/path/to/dsh-macos-notifier"。 - 重启 DSH。
依赖说明
client.js 只 require('react')(宿主提供),index.js 没有任何 import。因此本包没有需要单独安装的依赖,也不需要任何 @deepseek-ai/* 包;仓库不带 node_modules。
装完先开系统通知
系统设置 → 通知 → DeepSeek Harness → 打开「允许通知」,样式选「横幅」或「提醒」。没开之前系统横幅不会出现,此时只有应用内提示可见。
配置
本插件没有设置页:index.js 的 apply() 是空函数,没有 Config schema,所以没有任何能在 UI 里改的配置项。可调的东西只有下面这些代码常量与系统权限:
| 键 | 位置 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
THROTTLE_MS | client.js | number | 2000 | 同一会话两条系统通知的最小间隔(毫秒) |
PENDING_TTL_MS | client.js | number | 90000 | 待跳转 sessionId 在 localStorage 里的有效期(毫秒) |
PENDING_KEY | client.js | string | 'dsh.macos-notifier.pending' | 待跳转目标的 localStorage 键名 |
| 提示卡存活时间 | client.js | number | 9000(前台)/ 60000(后台) | 应用内提示自动消失的毫秒数 |
| 同屏提示卡上限 | client.js | number | 3 | 超过就移除最早的一张 |
dsh.client.immediately | package.json | boolean | true | client 模块随宿主立即加载 |
dsh.bundle.patch | package.json | string | ./cordis.patch.yml | bundle 补丁入口 |
| 系统通知权限 | 系统设置 → 通知 → DeepSeek Harness | 枚举 | 未授权 | 未授权时 systemNotify() 直接返回 false,不发横幅 |
已知限制
- 仅 macOS 有意义:用的是渲染层的 Web
Notification。在普通浏览器里打开同一个 Web GUI 时,通知由浏览器自己发、权限另算,插件不接管。 - 系统通知未授权时没有横幅:
systemNotify()在Notification.permission !== 'granted'时直接返回false,此时只剩应用内提示。 - 首次授权要用户动手:权限只在点击小铃铛的手势里请求(页面加载时自动请求会被系统拒绝)。
denied/default时点铃铛打开的是系统通知设置,不是权限对话框。 - 依赖宿主两个契约:事件名
api-session/status,以及sessions快照字段(byId[].title/label/running/origin/parentSessionId/blank)。宿主改事件名则退化为每 800 ms 轮询;改快照结构则不再触发。全程 try/catch,不抛错、不挡界面。 - 补跳只有 90 秒窗口:点横幅若只把窗口拉到前台、没触发
onclick,靠window的focus事件补跳;超过PENDING_TTL_MS不再跳。 - 不是通知中心:不保存历史、不分组、UI 里不可调;应用内提示固定盖在页面右下角(
z-index: 2147483000)。 - 节流写死:
THROTTLE_MS = 2000硬编码在client.js,同一会话 2 秒内的第二次结束不会重复弹。
文件
| 文件 | 作用 |
|---|---|
package.json | bundle 清单:dsh.bundle.patch + dsh.client(web,立即加载) |
cordis.patch.yml | 插入 macos-notifier Loader 行(insert.name 必须等于包名) |
index.js | Host 侧:空插件,只提供一行可启停的 Loader entry |
client.js | 客户端全部逻辑:状态边沿、系统通知、应用内提示、小铃铛 |
locale/zh.json、locale/en.json | 插件管理页标题与描述 |
icon.svg | 插件图标(DSH 内部用) |
assets/icon-180.png | 180×180 位图图标 |
开发
node --check index.js
node --check client.js
node scripts/sanitize-check.mjs
本仓库没有自动化测试(没有 test/ 目录,package.json 里也没有 test 脚本);CI 只跑语法检查与脱敏门禁两步。
scripts/sanitize-check.mjs 是脱敏门禁:本机绝对路径、私有包前缀、回环地址、私钥材料、内网地址段、内部主机名、token 前缀一律不许出现,命中即退出码 1。
License
MIT © 2026 veritaswiki