dsh-session-tg-notify 0.1.4 → 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 +20 -0
- package/package.json +1 -1
- package/src/index.js +14 -3
- package/src/presence.js +3 -1
package/README.md
CHANGED
|
@@ -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 次/分钟,会把开着的页面误判成离线)。焦点信息过期时退化为「后台」,而不是「离线」。
|
|
@@ -169,3 +188,4 @@ Telegram 的 bot **不能主动给人发消息** —— 它只能回复一个已
|
|
|
169
188
|
- macOS 系统通知依赖浏览器通知权限;未授权时后台通知退化为页面内 toast(不会静默丢失)。
|
|
170
189
|
- 提示音在浏览器未解锁 autoplay 前由系统默认音替代。
|
|
171
190
|
- 标签页被系统冻结时 SSE 连接可能仍在,此时判定为「后台」而非「离线」;由于冻结的页面收不到帧,通知可能丢失。实际使用中 Chrome 对已打开的标签页一般不冻结。
|
|
191
|
+
- **锁屏探测只在 macOS 宿主上可用。** DSH 若跑在 Linux 服务器(如远程 Agent 服务器),宿主读不到你的屏幕锁屏状态,`screen.supported` 为 `false`,此时「网页后台且锁屏时推送」这一档**永不触发**(后台仍发系统通知,网页关闭仍走 Telegram)。宿主与你的机器不在同一台时,可改用 `DSH_SESSION_TG_NOTIFY_SCREENLOCK=off` 显式关闭该档,避免误以为它在工作。
|
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
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();
|