feat: 会话通知插件 v1.0.1(系统通知 + 应用内轻弹窗)

会话完成、需要工具授权、需要回答时提醒:
- 窗口不在前台走系统通知(Windows / macOS / Linux 同一条渲染进程通知通道,无平台分支),点击回到 DSH 并跳到该会话;
- 窗口在前台走应用内轻弹窗(右上角、6 秒自动消失、可悬停暂停、Esc 关闭、点「查看」跳转);
- 正在看的那个会话完成时不打扰。

触发与去重:只对「运行中 → 空闲」的转变提醒,同一会话同一轮只报一次,同一待处理请求只报一次;
子智能体会话与空白会话跳过;打开应用时已在运行的会话只建基线不补发;
多个会话同时完成有 1.5 秒节流,完成提醒前有 400ms 确认窗口(那一轮又跑起来就不报)。

设置 → 通用 → 会话通知:权限状态、请求权限、测试通知、完成/授权/提问三个开关,
以及观察器自身的健康状态(收不到会话状态时直接显示,不会静默失效)。

实现形态遵循 DSH 插件规范:dsh.manifestVersion 1、dsh.bundle.patch 声明宿主行、
dsh.client 声明 platform: web 的浏览器半;宿主半为空壳,不注册任何服务与路由。
会话状态只通过 slot 的标准 props(useSessions / useSessionStatus)读取,
只往 settings.general.item 与 shell.overlay 注册,样式只用主题 token,不 import 客户端包。

修复:观察器原先挂在 sidebar.panellist,而该 slot 的宿主会把每个条目的 id 当作一个
左侧面板按钮(label 取 options.label ?? options.id,条目作为图标内联渲染),
导致左侧多出一行空面板并挤坏侧边栏;现改挂通用浮层 shell.overlay。
This commit is contained in:
pyh
2026-09-30 15:43:36 +08:00
commit 6b83a6ac19
12 changed files with 1250 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# 仓库内一律使用 LF:这是跨平台插件(Windows / macOS / Linux),
# 换行符不该随检出机器变化。
* text=auto eol=lf
+6
View File
@@ -0,0 +1,6 @@
# 忽略安装与构建产物;这个包没有依赖,也没有构建步骤。
node_modules/
pnpm-lock.yaml
*.tgz
.DS_Store
Thumbs.db
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 pyh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+91
View File
@@ -0,0 +1,91 @@
# 会话通知
DeepSeek Harness(DSH)插件:会话**需要你注意**时提醒你——窗口不在前台时用**系统通知**,窗口在前台时用**应用内轻弹窗**。
Windows / macOS / Linux 三个平台走的是同一条通知通道(渲染进程的 Web Notification API),插件里没有平台分支。
## 提醒什么
| 触发 | 通知标题 |
| --- | --- |
| 会话从「运行中」变为「空闲」(一轮回答结束) | 会话已完成 |
| 待处理的工具授权请求(`approval`) | 需要授权 |
| 待处理的提问 / 计划确认(`question` / `plan-review`) | 需要回答 |
每条只提醒一次:同一会话的同一轮只报一次完成,同一个待处理请求只报一次;请求消失后再来才会再报。
## 走哪个通道
| 状态 | 行为 |
| --- | --- |
| DSH 窗口**不在前台** | **系统通知**(这是插件唯一能拿到的系统通知通道,三个平台一致)。点通知:窗口回到前台并跳到该会话 |
| DSH 窗口在前台,且**不是**你正在看的那个会话 | **应用内轻弹窗**:右上角悬浮,点「查看」跳过去,6 秒自动消失,鼠标悬停时不消失,Esc 关掉最上面一条 |
| DSH 窗口在前台,且就是你**正在看的**那个会话 | 不打扰(你在看,不需要弹) |
## 设置
**设置 → 通用 → 会话通知**:
- 显示系统通知权限状态;
- 未授权时点「允许通知」请求权限(浏览器要求必须由你手动点击才能弹权限框),授权后自动发一条测试通知;
- 已授权时点「测试通知」随时验证;
- 权限被系统层关闭时,按平台给出开启路径(Windows / macOS 的通知设置、Linux 桌面环境的通知设置);
- 三个开关分别控制**完成 / 授权 / 提问**三类提醒(默认全开;开关状态存在当前页面内存里,刷新回到默认——本插件不写配置文件)。
## 安装
在 DSH 的 **插件 → 添加插件** 里填本目录的绝对路径即可(`plugin_manager` 也会做同样的安装):
```text
D:\DeepSeek Harness Plugins\dsh-session-notify
```
安装后:
- **浏览器半**随页面加载,刷新页面即生效;
- **宿主半**为空壳(不改任何 DSH 状态),如需重启才生效,完全退出 DSH 再打开即可;
- 本插件没有第三方依赖,不需要 pnpm 下载,也不需要构建步骤。
卸载:插件页里移除 `@dsh-plugin/session-notify`(它同时是 profile 的一个 bundle)。
## 平台说明与验证范围
- **通道是跨平台统一的**:插件的提醒都通过页面(Electron 渲染进程)的 `Notification` 构造器发出,Windows 通知中心 / macOS 通知中心 / Linux 通知守护(libnotify、GNOME、KDE)都由系统把它转成原生通知;应用内轻弹窗是纯 DOM,与平台无关。
- **宿主半拿不到系统通知**:DSH Desktop 的宿主进程是纯 Node 进程(不是 Electron 主进程),没有 Electron 的 `Notification` 可用,所以插件的所有提醒都在浏览器半产生——这也正是"三个平台一套代码"的原因。
- **已验证**:Windows 上的安装、三个 slot 条目注册、以及 22 条投递规则(见下)的离线验证;设置行与轻弹窗的文案/交互按 DSH 自带的 toast 与 switch 组件对齐。
- **未验证**:macOS 与 Linux 上的实际弹窗效果本机无法测试。结构上它们与 Windows 共用同一条通道、同一份代码,只有"权限被系统关闭时显示的开启路径"是分平台的。
- 提醒只在 DSH 进程运行、页面打开时产生:**DSH 完全退出期间结束的会话不会再补发通知**。
## 行为细节(都已验证)
- **只有观测到「运行中 → 空闲」的转变才提醒**:打开应用时已经在跑的会话只建立基线,不补发;历史里早已结束的会话不会被翻出来提醒。
- **子智能体会话不单独提醒**(`origin === 'subagent'`),归属它的主会话结束时才提醒。
- **新会话(空白会话)完成不提醒**。
- **授权/提问在会话仍在运行时被延后提醒**:先记下,等这一轮真正停下来再报,避免在模型还在跑的时候就打扰你。
- **节流**:1.5 秒内只投递一条,多个会话同时完成不会刷屏;完成提醒前有 400ms 的确认窗口,如果那一轮马上又跑起来就不报。
- **同一会话的完成提醒会覆盖上一条**(通知带 `tag`),不堆叠。
## 兼容性
基于 **DeepSeek Harness 0.2.0-rc.2** 编写,遵循 DSH 插件规范:`dsh.manifestVersion: 1`、`dsh.bundle.patch` 声明宿主行、`dsh.client` 声明 `platform: "web"` 的浏览器半。
依赖的都是稳定契约:
- 浏览器半只依赖 **slot 的标准 props**(`useSessions`、`useSessionStatus`)读取会话状态,这是 DSH 官方给插件读会话数据的方式;不轮询、不自己订阅会话事件、不读别的插件的 DOM;
- 只往两个官方 slot 注册:`settings.general.item`(设置行)与 `shell.overlay`(两个条目:轻弹窗层 + 无渲染的观察器,内容永远是 `null`);
- **不要**把"无渲染"条目放进 `sidebar.panellist`:该 slot 的宿主会把**每个条目的 id 当成一个左侧面板按钮**(`entriesOfSlot('sidebar.panellist')` → 面板列表,按钮文字取 `options.label ?? options.id`,条目本身作为图标内联渲染),放进去会在左侧多出一行空面板并挤坏侧边栏。本插件第一版踩过这个坑,现已改挂到通用浮层;
- 样式只用主题 token,不 import 任何 `@deepseek-ai/dsh-client-*` 包(规范要求,也是渲染不被上游改动打断的前提);
- 宿主半不声明 `inject`、不注册服务、不注册路由。
上游若改动 slot 名或 hook 名:注册会静默跳过而不是把页面弄坏,插件会退化成"不提醒";这类情况请按上面的契约名核对。
## 验证方式(开发记录)
`client.js` 是纯 JavaScript、浏览器端运行,可以在 Node 里用桩模块加载器评估并驱动:
- 模块接线:加载后返回的插件、`inject` 声明、注册集合(含"观察器绝不在 sidebar.* 里"这一条)、以及各组件在空状态 / 权限被拒 / 无 Notification API 下的渲染都不报错;
- 投递规则:40 项断言覆盖上面"行为细节"里的每一条(完成提醒一次、当前会话不打扰、后台走系统通知、授权/提问各提醒一次、运行中延后、子会话跳过、历史不补发、开关生效、弹窗自动消失等)。
## License
[MIT](LICENSE)
+1012
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
# Bundle patch layer for @dsh-plugin/session-notify.
#
# One Host row is enough: the Host half owns no services and no routes, and the
# browser half is picked up automatically from this package's `dsh.client`
# declaration (`dsh-client-modules` scans enabled Loader entries, serves
# `/plugins/@dsh-plugin/session-notify/client.js`, and boots it with the page).
- insert:
- id: session-notify
name: '@dsh-plugin/session-notify'
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<!-- Notification bell (bundle artwork; the only place a literal color is allowed). -->
<path d="M8 2.5C5.72 2.5 3.88 4.34 3.88 6.62V9.1L3.1 11.2C3.03 11.39 3.17 11.58 3.37 11.58H12.63C12.83 11.58 12.97 11.39 12.9 11.2L12.12 9.1V6.62C12.12 4.34 10.28 2.5 8 2.5Z" fill="#247bbf"/>
<path d="M6.3 12.7C6.52 13.62 7.2 14.25 8 14.25C8.8 14.25 9.48 13.62 9.7 12.7" stroke="#247bbf" stroke-width="1.2" stroke-linecap="round"/>
<circle cx="12.6" cy="3.4" r="2.1" fill="#e5484d" stroke="#ffffff" stroke-width="0.9"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

+29
View File
@@ -0,0 +1,29 @@
/**
* Host half of the session-notify bundle.
*
* Every notification in this plugin is produced by the browser half: the page
* owns the DOM (the in-app light popup) and is the only place a Web
* `Notification` can be constructed, which is the one system-notification
* channel that behaves the same on Windows, macOS, and Linux inside the Harness
* Desktop shell. The Host half therefore owns no state, registers no service,
* no route, and no event listener — it exists so the bundle has the ordinary
* profile-level plugin row its patch declares, and so a future Host-side
* capability (for example persisting the per-kind switches into this profile's
* patch) has a place to live.
*
* The export form is the documented one — `export function apply(ctx, config)`
* with no `inject` and no `Config` — and it is deliberately the only export
* form this package uses.
*
* @module @dsh-plugin/session-notify
*/
/**
* Mount the Host row.
* @param ctx - Host context; no service is required, so this never blocks activation.
*/
export function apply(ctx) {
ctx.logger?.debug?.(
'session-notify: host half idle by design — notifications run in the browser half',
)
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "Session notifications",
"description": "Tells you when a conversation finishes, needs approval, or needs an answer — a system notification when the window is in the background, a light in-app popup when it is in the foreground."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"meta": {
"title": "会话通知",
"description": "会话完成、需要授权或需要回答时提醒你:窗口不在前台用系统通知,窗口在前台用应用内轻弹窗。"
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* CommonJS-resolvable entry alias for the Host half.
*
* `package.json` `exports` names `./index.js`, which is what the Cordis Loader
* and the plugin manager read. A loader that resolves this package by
* `main` instead of `exports` would otherwise find nothing, so this file
* re-exports the same plugin without adding behavior or export forms.
*
* @module @dsh-plugin/session-notify/main
*/
export { apply } from './index.js'
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@dsh-plugin/session-notify",
"version": "1.0.1",
"private": true,
"type": "module",
"description": "会话完成、需要授权、需要回答时提醒你:窗口不在前台用系统通知,窗口在前台用应用内轻弹窗。",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://gitea.iwake.top/dsh-plugin/session-notify.git"
},
"homepage": "https://gitea.iwake.top/dsh-plugin/session-notify",
"exports": {
".": "./index.js",
"./client": "./client.js",
"./package.json": "./package.json",
"./locale/*.json": "./locale/*.json"
},
"icon": "./icon.svg",
"meta": {
"title": "会话通知",
"description": "会话完成 / 需要授权 / 需要回答时提醒,支持系统通知与应用内轻弹窗。"
},
"files": [
"index.js",
"main.js",
"client.js",
"cordis.patch.yml",
"icon.svg",
"locale/*.json",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=22"
},
"dsh": {
"manifestVersion": 1,
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"platform": "web",
"immediately": true,
"inject": [
"@deepseek-ai/dsh-client-ui-workspace"
]
}
}
}