dsh-session-tg-notify 0.1.3 → 0.1.5

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 CHANGED
@@ -86,7 +86,7 @@ dsh plugin --profile web add link:/绝对路径/dsh-session-tg-notify
86
86
 
87
87
  > 后台这一档为什么要求**锁屏**而不是「失焦」:切到别的应用、点一下浏览器外部都会让页面失焦,那时人还在电脑前,推手机是纯打扰。锁屏才是「人走了」的可靠信号。
88
88
 
89
- 权威定义写在 `test/truth-table.mjs`:它把「状态 × 订阅组合 × 锁屏开关 × 实际锁屏」的全矩阵用**真实投递**跑一遍并对照期望断言。改路由逻辑必须先让这张表继续成立。
89
+ 上面这张表就是权威定义:**状态 × 订阅组合 × 锁屏开关 × 实际锁屏**的全矩阵,任何一格的改动都要同步这里。改路由逻辑时必须保证这张表仍然成立。
90
90
 
91
91
  ### 锁屏检测怎么工作
92
92
 
@@ -120,6 +120,25 @@ Telegram 的 bot **不能主动给人发消息** —— 它只能回复一个已
120
120
 
121
121
  > 如果你另外装了 `dsh-connect-telegram` 这类也用 `getUpdates` 长轮询的插件,两者会抢夺更新,需要错开(例如只保留其一)。
122
122
 
123
+ ## 各运行形态的表现
124
+
125
+ 同一份代码同时用于 **Web**(浏览器)、**桌面版**(Electron)与 **headless**,无需按 profile 分叉——判定源和数据通路在三端一致。下表是实测结论:
126
+
127
+ | 能力 | 机制 | Web | 桌面版 | headless |
128
+ |---|---|---|---|---|
129
+ | APP/页面内 toast | 往 `document.body` 插 DOM | ✅ | ✅ | — |
130
+ | 系统通知 | 页面侧 `new Notification()` | ✅ | ✅(需系统已授权) | — |
131
+ | 前台/后台判定 | `document.visibilityState` + `document.hasFocus()` | ✅ | ✅ | — |
132
+ | 锁屏判定 | 宿主 `ioreg` 读 `CGSSessionScreenIsLocked` | ✅ | ✅ | ✅ |
133
+
134
+ - **桌面版不需要特殊适配。** Electron 单窗口常见问题(`document.hasFocus()` 恒为 `true` 导致永远判不出后台)在 DSH 桌面版上**不存在**:实测连续切换到 Finder / TextEdit / Safari,每次都正确翻成 `background`,且不会自抢焦点。
135
+ - **系统通知由页面发出**这一条对桌面版同样成立——Electron 渲染进程有完整 DOM,主进程也带 `setPermissionRequestHandler`(会处理通知权限请求)。
136
+ - **锁屏判定走宿主**,与客户端无关,所以「网页后台且锁屏时推送」那一档在桌面版照常生效。
137
+ - **桌面版只发系统通知的条件就是「APP 不在前台」**,不做前后台 toast 区分——窗口在前台时走页面内提示,失焦时走系统通知。
138
+ - `headless` 没有客户端,SSE 恒无连接 → 始终判为「离线」,因此只走 Telegram。
139
+
140
+ 如果桌面版首次收不到系统通知,先确认 macOS **系统设置 → 通知 → DeepSeek Harness** 已允许;未授权时客户端会静默降级为页面内提示。
141
+
123
142
  ## 设计要点
124
143
 
125
144
  - **SSE 长连接即存活信号**:页面与后端之间只有一条 SSE 通道,它同时承担存活判定、前后台状态上报和通知下发。**在线只看连接是否存在**,页面关闭 → 连接断开 → 后端自动改走 Telegram。心跳(10s)只用来判断焦点信息是否还新鲜,不参与生死判定 —— 否则会被 Chrome 的后台标签页定时器节流误伤(隐藏超约 5 分钟后 `setInterval` 收缩到约 1 次/分钟,会把开着的页面误判成离线)。焦点信息过期时退化为「后台」,而不是「离线」。
@@ -163,16 +182,10 @@ Telegram 的 bot **不能主动给人发消息** —— 它只能回复一个已
163
182
  | `POST /session-notify/test` | 逐事件测试推送 `{ kind, channel, clientId }` |
164
183
  | `POST /session-notify/telegram-check` | 校验 Bot Token 并返回 bot 身份 |
165
184
 
166
- ## 测试
167
-
168
- ```sh
169
- node test/smoke.mjs
170
- ```
171
-
172
- 零依赖,用假 ctx 驱动插件,覆盖三态路由、子代理过滤、去重、事件级开关、HTTP API 等 30 项断言。
173
185
 
174
186
  ## 已知限制
175
187
 
176
188
  - macOS 系统通知依赖浏览器通知权限;未授权时后台通知退化为页面内 toast(不会静默丢失)。
177
189
  - 提示音在浏览器未解锁 autoplay 前由系统默认音替代。
178
190
  - 标签页被系统冻结时 SSE 连接可能仍在,此时判定为「后台」而非「离线」;由于冻结的页面收不到帧,通知可能丢失。实际使用中 Chrome 对已打开的标签页一般不冻结。
191
+ - **锁屏探测只在 macOS 宿主上可用。** DSH 若跑在 Linux 服务器(如远程 Agent 服务器),宿主读不到你的屏幕锁屏状态,`screen.supported` 为 `false`,此时「网页后台且锁屏时推送」这一档**永不触发**(后台仍发系统通知,网页关闭仍走 Telegram)。宿主与你的机器不在同一台时,可改用 `DSH_SESSION_TG_NOTIFY_SCREENLOCK=off` 显式关闭该档,避免误以为它在工作。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-tg-notify",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "DSH 会话通知插件:监控会话事件(完成 / 审批 / 提问 / 受阻 / 出错),按浏览器前后台与锁屏状态路由到页面内 toast、系统通知或 Telegram,点击通知直达对应会话。",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -34,7 +34,7 @@ import { readScreenLocked } from './screenlock.js';
34
34
  import { sendTelegram, getTelegramMe, getTelegramChats, getTelegramChat } from './telegram.js';
35
35
 
36
36
  export const name = 'session-notify';
37
- export const VERSION = '0.1.3';
37
+ export const VERSION = '0.1.5';
38
38
  /** 只消费事件与 webServer,不依赖其他服务。 */
39
39
  export const inject = [];
40
40
 
@@ -54,6 +54,17 @@ export function apply(ctx, cordisConfig = {}, options = {}) {
54
54
  const send = options.sendTelegram ?? sendTelegram;
55
55
 
56
56
  // ------------------------------------------------------------ 锁屏探测
57
+ /**
58
+ * 锁屏档可以被环境变量显式关闭。存在的理由:DSH 跑在 Linux 远程服务器上时
59
+ * 宿主读不到你的屏幕锁屏状态,那一档本来就不会触发;显式关掉能让设置面板
60
+ * 诚实地显示「已禁用」,而不是让人以为开关在工作。
61
+ * DSH_SESSION_TG_NOTIFY_SCREENLOCK=off → 永远报告 supported:false
62
+ */
63
+ const screenlockEnabled = String(process.env.DSH_SESSION_TG_NOTIFY_SCREENLOCK ?? '').toLowerCase() !== 'off';
64
+ if (!screenlockEnabled) {
65
+ console.log('[session-notify] 锁屏探测已被 DSH_SESSION_TG_NOTIFY_SCREENLOCK=off 关闭');
66
+ }
67
+
57
68
  /**
58
69
  * 最近一次锁屏探测结果。锁屏只在「后台 + 订阅了 TG + 开了锁屏推送」这一种
59
70
  * 组合下影响路由,所以平时不轮询、按需探测;设置页读取时也会强制探一次,
@@ -63,7 +74,7 @@ export function apply(ctx, cordisConfig = {}, options = {}) {
63
74
  let lastScreen = { supported: null, locked: false, signal: 'unknown', at: 0 };
64
75
 
65
76
  const probeScreenLocked = async () => {
66
- const result = await lockProbe();
77
+ const result = screenlockEnabled ? await lockProbe() : { supported: false, locked: false, signal: 'disabled' };
67
78
  lastScreen = { ...result, at: Date.now() };
68
79
  return lastScreen;
69
80
  };
@@ -76,7 +87,7 @@ export function apply(ctx, cordisConfig = {}, options = {}) {
76
87
  * 便于区分是 ioreg 命中还是屏保兜底。
77
88
  */
78
89
  const lockPollMs = Number.isSafeInteger(options.lockPollMs) ? options.lockPollMs : 20000;
79
- if (lockPollMs > 0 && typeof ctx.effect === 'function') {
90
+ if (screenlockEnabled && lockPollMs > 0 && typeof ctx.effect === 'function') {
80
91
  ctx.effect(() => {
81
92
  let previous = null;
82
93
  const timer = setInterval(() => {
package/src/presence.js CHANGED
@@ -87,8 +87,10 @@ export function createPresenceHub(options = {}) {
87
87
  clients.set(clientId, { res, lastSeen: now(), visibility: 'visible', focused: false });
88
88
  writeFrame(res, 'ready', { ...ready, clientId });
89
89
  res.on('close', () => {
90
- console.log(`[session-notify] 页面连接断开(剩余 ${clients.size - 1} 个)`);
90
+ // 先 detach 再记录剩余数:反过来的话 clients.size 还没减,
91
+ // 只能靠 -1 硬凑,日志会打出「剩余 -1 个」这种误导性的值。
91
92
  detach(clientId);
93
+ console.log(`[session-notify] 页面连接断开(剩余 ${clients.size} 个)`);
92
94
  });
93
95
  console.log(`[session-notify] 页面连接建立(clientId=${clientId},共 ${clients.size} 个)`);
94
96
  startHeartbeat();