dsh-session-tg-notify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/cordis.patch.yml +6 -0
- package/package.json +52 -0
- package/src/client.js +777 -0
- package/src/config.js +207 -0
- package/src/events.js +308 -0
- package/src/index.js +392 -0
- package/src/presence.js +177 -0
- package/src/screenlock.js +72 -0
- package/src/telegram.js +162 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# dsh-session-tg-notify
|
|
2
|
+
|
|
3
|
+
DSH 会话通知插件:监控会话事件,按**浏览器前后台状态**自动选择通知通道。
|
|
4
|
+
|
|
5
|
+
- **网页在前台**(标签页可见且浏览器有焦点)→ 页面内 toast
|
|
6
|
+
- **网页在后台**(开着但不是当前标签页 / 浏览器没焦点)→ macOS 系统通知,点击通知直达该标签页;可配置是否**同时**再推一条 Telegram
|
|
7
|
+
- **网页没打开** → Telegram
|
|
8
|
+
|
|
9
|
+
不修改 DSH 官方代码,完全通过插件机制接入;通道与事件都在设置面板里配置,保存即生效,无需重启。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
dsh plugin --profile web add link:/绝对路径/dsh-session-tg-notify
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`link:` 安装会建立软链接,改完源码重启 DSH 即生效,不需要重新安装。
|
|
18
|
+
|
|
19
|
+
## 使用
|
|
20
|
+
|
|
21
|
+
对话页顶栏(Session log 旁边)出现铃铛按钮,点击打开设置:
|
|
22
|
+
|
|
23
|
+
| 区域 | 可配置项 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| 总开关 | 一键关闭全部通知 |
|
|
26
|
+
| 状态指示 | 当前判定为前台 / 后台 / 离线、是否锁屏,以及打开的标签页数量 |
|
|
27
|
+
| 事件与通道 | 5 类事件各自勾选「桌面」「TG」,每行带**独立测试推送**按钮 |
|
|
28
|
+
| 桌面通知 | 提示音开关、音色(叮 / 双响 / 三连音 / 静音)、系统通知权限状态与授权按钮 |
|
|
29
|
+
| Telegram | 启用开关、Bot Token、Chat ID、保存凭据、测试连接、**网页后台且锁屏时推送** |
|
|
30
|
+
|
|
31
|
+
**点击通知直达会话**:页面内 toast 与 macOS 系统通知都可点击,点后会切回该标签页并打开对应会话(走 DSH 客户端的 `uiWorkspace.openSession`)。会话已归档或已被删除时导航失败会静默忽略。
|
|
32
|
+
|
|
33
|
+
### 可通知的事件
|
|
34
|
+
|
|
35
|
+
| 事件 | 触发时机 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| ✅ 会话完成 | 主会话某个 turn 结束,且最后一条 assistant 消息是**纯文本最终回答**(tool-call-only 的 turn 不算) |
|
|
38
|
+
| 🔐 需要审批 | 工具操作等待人工批准(`approval/asked`) |
|
|
39
|
+
| ❓ 需要回答 | Agent 调用 `ask_user_question` |
|
|
40
|
+
| ⚠️ 目标受阻 | goal 进入 blocked |
|
|
41
|
+
| ✗ 运行出错 | `agent/error` |
|
|
42
|
+
|
|
43
|
+
**子代理会话一律不通知。**
|
|
44
|
+
|
|
45
|
+
### 通道判定规则
|
|
46
|
+
|
|
47
|
+
先分清两个概念,剩下的规则就唯一了:
|
|
48
|
+
|
|
49
|
+
- **订阅**:设置页每行的「桌面」「TG」勾选框 —— "这个事件我愿意通过该通道收到"。
|
|
50
|
+
- **通道**:全局开关与凭据(浏览器通知权限 / Telegram 启用 + Token + Chat ID)—— 通道没启动时订阅也不会发出,宿主会打印一条 warning 说明原因。
|
|
51
|
+
|
|
52
|
+
| 网页状态 | 桌面通道 | Telegram 通道 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **前台**(标签页可见且有焦点) | 页面内 toast | **永不发送** —— 你正看着屏幕,不打扰手机 |
|
|
55
|
+
| **后台**(页面开着但没在看) | macOS 系统通知(点击直达会话) | 需**同时**满足三件事:该事件订阅了 TG、开了「网页后台且锁屏时推送」、**且屏幕确实已锁定** |
|
|
56
|
+
| **离线**(页面已关) | 无处可发 | 该事件订阅了 TG 即发送(与锁屏无关) |
|
|
57
|
+
|
|
58
|
+
一行里两个框都**不勾** = 该事件完全不打扰。只勾 TG 不勾桌面 = 只在网页关闭时收到手机推送。
|
|
59
|
+
|
|
60
|
+
> 后台这一档为什么要求**锁屏**而不是「失焦」:切到别的应用、点一下浏览器外部都会让页面失焦,那时人还在电脑前,推手机是纯打扰。锁屏才是「人走了」的可靠信号。
|
|
61
|
+
|
|
62
|
+
权威定义写在 `test/truth-table.mjs`:它把「状态 × 订阅组合 × 锁屏开关 × 实际锁屏」的全矩阵用**真实投递**跑一遍并对照期望断言。改路由逻辑必须先让这张表继续成立。
|
|
63
|
+
|
|
64
|
+
### 锁屏检测怎么工作
|
|
65
|
+
|
|
66
|
+
宿主读 macOS 的 console user 信息:
|
|
67
|
+
|
|
68
|
+
- **主信号**:`ioreg -n Root -d1 -a` 里的 `IOConsoleUsers[].CGSSessionScreenIsLocked`。该键**只在锁屏时出现**,所以「键缺失 = 未锁定」。
|
|
69
|
+
- **兜底**:`pgrep -x ScreenSaverEngine`(仅主信号不可用时使用)。
|
|
70
|
+
- 只在「后台 + 订阅了 TG + 开了锁屏推送」这一种组合下才真的去探测,其余情况不付这次子进程开销(约 34ms)。
|
|
71
|
+
- 非 macOS 平台返回 `supported: false`,退化为「未锁定」,不会把通知永久吞掉。
|
|
72
|
+
|
|
73
|
+
**这套锁屏判定尚未在真实锁屏状态下验证过**(开发时无法去锁用户的屏幕)。验证方法:锁屏、走开一会儿、解锁回来,然后查日志:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
grep "锁屏状态变化" ~/.dsh/logs/web.log
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
应该能看到成对的「已锁定 / 已解锁」。设置面板的状态行在后台时会显示「(已锁屏)」或「(未锁屏)」,也是同一份探测结果。
|
|
80
|
+
|
|
81
|
+
### Telegram 首次配置(Chat ID 是必需的)
|
|
82
|
+
|
|
83
|
+
Telegram 的 bot **不能主动给人发消息** —— 它只能回复一个已经和它建立过会话的 chat,所以 `chat_id` 是必填的,插件无法替你猜出「你的私聊」。正确顺序:
|
|
84
|
+
|
|
85
|
+
1. 在 Telegram 里找 **@BotFather** 创建 bot,拿到 Bot Token。
|
|
86
|
+
2. **先在 Telegram 里给这个 bot 发一条消息**(`/start` 即可)—— 不做这步,后面读不到任何会话。
|
|
87
|
+
3. 打开设置面板 → Telegram → 填入 Bot Token → 点「保存凭据」。
|
|
88
|
+
4. 点「**获取 Chat ID**」:插件调用 `getUpdates` 把最近给 bot 发过消息的 chat 读出来并自动填入(多个时填第一个并在提示里列出其余)。
|
|
89
|
+
5. 点「测试连接」:现在会同时校验 token(`getMe`)**和** Chat ID(`getChat`)。只显示「Bot Token 有效,但 Chat ID 不可用」就说明第 4 步没做成功。
|
|
90
|
+
6. 回到事件表格,点某行的「TG」发一条真实测试。
|
|
91
|
+
|
|
92
|
+
插件从不带 `offset` 调用 `getUpdates`,因此不会消费(确认)更新,历史消息一直可见,重复点「获取 Chat ID」也不会失效。
|
|
93
|
+
|
|
94
|
+
> 如果你另外装了 `dsh-connect-telegram` 这类也用 `getUpdates` 长轮询的插件,两者会抢夺更新,需要错开(例如只保留其一)。
|
|
95
|
+
|
|
96
|
+
## 设计要点
|
|
97
|
+
|
|
98
|
+
- **SSE 长连接即存活信号**:页面与后端之间只有一条 SSE 通道,它同时承担存活判定、前后台状态上报和通知下发。**在线只看连接是否存在**,页面关闭 → 连接断开 → 后端自动改走 Telegram。心跳(10s)只用来判断焦点信息是否还新鲜,不参与生死判定 —— 否则会被 Chrome 的后台标签页定时器节流误伤(隐藏超约 5 分钟后 `setInterval` 收缩到约 1 次/分钟,会把开着的页面误判成离线)。焦点信息过期时退化为「后台」,而不是「离线」。
|
|
99
|
+
- **系统通知由页面发出**(Web Notification API),因此点击通知切回标签页是浏览器原生行为——不需要 AppleScript、辅助功能授权或多标签页匹配。
|
|
100
|
+
- **提示音用 Web Audio 合成**,无素材文件。浏览器 autoplay 策略要求先有一次用户手势解锁;未解锁时回退为系统默认通知音,保证一定有声音。
|
|
101
|
+
- **子代理过滤**:DSH 中活的子会话 `header.delegationDepth >= 1`(主会话通常没有该字段),另有 `header.origin === 'subagent'` 兜底;异常值保守拒绝。
|
|
102
|
+
- **去重**:同一审批 / 同一提问 / 同一 turn 只通知一次,容器有界,随会话销毁回收。
|
|
103
|
+
|
|
104
|
+
## 安全说明(请读一遍)
|
|
105
|
+
|
|
106
|
+
`/session-notify/*` 挂在 DSH web server 上,**不经过 DSH 自身的 `?token=` 校验**。它只受「DSH 进程监听在哪个地址」和「前置代理有没有加鉴权」两层保护:
|
|
107
|
+
|
|
108
|
+
- **本机直连**:若 webserver 绑在 `127.0.0.1`,只有本机进程能访问,最安全。
|
|
109
|
+
- **绑到 `0.0.0.0`**(为了局域网访问 GUI):同网段任何设备都能直接 `curl` 本插件接口,从而改配置(包括把 Bot Token 换成自己的)、触发测试推送、读取 Chat ID。
|
|
110
|
+
- **置于反向代理之后**:若代理对整个站点统一加了鉴权(例如 Caddy 的 `basic_auth` 作用于整个站点块),`/session-notify/*` 一并受保护,同源请求会自动携带凭据。**但若代理只保护部分路径,就必须确认这条前缀也在保护范围内。**
|
|
111
|
+
|
|
112
|
+
已经内置的防护:
|
|
113
|
+
|
|
114
|
+
- 拒绝 `Origin` 与 `Host` 不匹配的请求(防跨站表单/脚本伪造)。
|
|
115
|
+
- **挡不住直接 `curl`** —— 它不带 `Origin`,会正常通过。不要把它当作网络层鉴权。
|
|
116
|
+
- `GET /state` 不回显 Bot Token;配置文件权限 `0600`;界面与日志里只输出 `~/...` 形式的路径。
|
|
117
|
+
|
|
118
|
+
建议:本机使用就把 webserver 绑回 `host: 127.0.0.1`;对外暴露时确保代理鉴权覆盖整个站点。
|
|
119
|
+
|
|
120
|
+
## 配置存储
|
|
121
|
+
|
|
122
|
+
`~/.config/dsh/session-notify.json`(权限 `0600`,内含 Telegram Bot Token),可用环境变量 `DSH_SESSION_NOTIFY_CONFIG` 覆盖路径。设置面板里的改动会即时写回该文件。
|
|
123
|
+
|
|
124
|
+
设置面板只显示 `~/...` 形式,不打印绝对路径 —— 绝对路径会把用户名渲染进界面,截图分享即泄漏。`GET /state` 同时返回 `configPath`(绝对,供 curl/运维)与 `configPathDisplay`(收敛为 `~`,供界面)。每台机器各有自己的这份配置,Telegram 凭据需要各配一次。
|
|
125
|
+
|
|
126
|
+
## HTTP API
|
|
127
|
+
|
|
128
|
+
挂在 DSH web server 的 `/session-notify` 前缀下,供设置面板使用:
|
|
129
|
+
|
|
130
|
+
| 端点 | 用途 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `GET /session-notify/state` | 读取配置(Token 已隐去)+ 在线状态 + 事件元数据 |
|
|
133
|
+
| `GET /session-notify/events?clientId=` | SSE:存活信号 + `toast` / `webnotify` 帧 |
|
|
134
|
+
| `POST /session-notify/presence` | 上报 `{ clientId, visibility, focused }` |
|
|
135
|
+
| `POST /session-notify/config` | 保存配置局部补丁,立即生效 |
|
|
136
|
+
| `POST /session-notify/test` | 逐事件测试推送 `{ kind, channel, clientId }` |
|
|
137
|
+
| `POST /session-notify/telegram-check` | 校验 Bot Token 并返回 bot 身份 |
|
|
138
|
+
|
|
139
|
+
## 测试
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
node test/smoke.mjs
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
零依赖,用假 ctx 驱动插件,覆盖三态路由、子代理过滤、去重、事件级开关、HTTP API 等 30 项断言。
|
|
146
|
+
|
|
147
|
+
## 已知限制
|
|
148
|
+
|
|
149
|
+
- macOS 系统通知依赖浏览器通知权限;未授权时后台通知退化为页面内 toast(不会静默丢失)。
|
|
150
|
+
- 提示音在浏览器未解锁 autoplay 前由系统默认音替代。
|
|
151
|
+
- 标签页被系统冻结时 SSE 连接可能仍在,此时判定为「后台」而非「离线」;由于冻结的页面收不到帧,通知可能丢失。实际使用中 Chrome 对已打开的标签页一般不冻结。
|
package/cordis.patch.yml
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-session-tg-notify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH 会话通知插件:监控会话事件(完成 / 审批 / 提问 / 受阻 / 出错),按浏览器前后台与锁屏状态路由到页面内 toast、系统通知或 Telegram,点击通知直达对应会话。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./config.js": "./src/config.js",
|
|
10
|
+
"./events.js": "./src/events.js",
|
|
11
|
+
"./presence.js": "./src/presence.js",
|
|
12
|
+
"./screenlock.js": "./src/screenlock.js",
|
|
13
|
+
"./telegram.js": "./src/telegram.js",
|
|
14
|
+
"./client": "./src/client.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"dsh",
|
|
29
|
+
"deepseek-harness",
|
|
30
|
+
"notification",
|
|
31
|
+
"telegram",
|
|
32
|
+
"plugin"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/kriskwok/dsh-ios-app.git",
|
|
37
|
+
"directory": "dsh-session-notify"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"platform": "web",
|
|
45
|
+
"inject": [
|
|
46
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
47
|
+
"@deepseek-ai/dsh-client-locale",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|