@freely01/opencode-notify 0.3.0 → 0.4.1
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 +181 -335
- package/cli.ts +9 -7
- package/config.ts +49 -27
- package/delayed-dispatcher.ts +24 -0
- package/index.ts +58 -42
- package/message.ts +29 -8
- package/package.json +1 -1
- package/senders/feishu.ts +0 -12
- package/senders/system/darwin.ts +26 -3
- package/senders/wechat-work.ts +1 -2
- package/session-tracker.ts +13 -2
- package/terminator-detect.ts +92 -105
package/terminator-detect.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Terminator
|
|
2
|
+
* Terminator 子屏幕遮挡检测
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 与本进程的 TERMINATOR_UUID 比较,判定本屏是否被遮挡。
|
|
4
|
+
* 检测当前子屏幕是否被真正遮挡(不可见),用于判定是否需要
|
|
5
|
+
* 强制通知(即使用户在该会话中活跃)。
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* 检测策略(两级):
|
|
8
|
+
* 1. X 窗口级别:Terminator 是否是当前活跃窗口(xdotool + xprop)
|
|
9
|
+
* 否 → 用户在别的应用中 → 遮挡
|
|
10
|
+
* 2. 子屏级别:用户当前聚焦的是哪个子屏(DBus get_focused_terminal)
|
|
11
|
+
* 聚焦 != 本屏 → 用户在另一个子屏上 → 遮挡
|
|
12
|
+
*
|
|
13
|
+
* 注意:此版本 Terminator 未暴露 get_maximized_terminal 接口,
|
|
14
|
+
* 无法区分"分屏可见"和"最大化遮挡"。两级结合覆盖核心场景:
|
|
15
|
+
* - Terminator 不活跃(浏览器/IDE):强制通知
|
|
16
|
+
* - 本屏聚焦:不通知
|
|
17
|
+
* - 另一子屏聚焦(分屏或最大化):强制通知
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
import { execSync } from "node:child_process"
|
|
16
|
-
import {
|
|
21
|
+
import { debug } from "./log.js"
|
|
17
22
|
|
|
18
23
|
/** 当前进程的 TERMINATOR_UUID */
|
|
19
24
|
const MY_UUID = process.env.TERMINATOR_UUID ?? null
|
|
20
25
|
|
|
21
|
-
/** Terminator DBus
|
|
26
|
+
/** Terminator DBus 信息 */
|
|
22
27
|
const DBUS_NAME = process.env.TERMINATOR_DBUS_NAME ?? null
|
|
23
28
|
const DBUS_PATH = process.env.TERMINATOR_DBUS_PATH ?? null
|
|
24
29
|
|
|
@@ -26,10 +31,10 @@ const DBUS_PATH = process.env.TERMINATOR_DBUS_PATH ?? null
|
|
|
26
31
|
let insideTerminator: boolean | null = null
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
34
|
+
* 检测当前子屏幕是否被遮挡(不可见)
|
|
30
35
|
*
|
|
31
|
-
* @returns true →
|
|
32
|
-
* false →
|
|
36
|
+
* @returns true → 本子屏幕被遮挡(用户看不到,应强制通知)
|
|
37
|
+
* false → 本子屏幕可见(正常抑制逻辑)
|
|
33
38
|
* null → 无法确定(不在 Terminator 中或检测失败)
|
|
34
39
|
*/
|
|
35
40
|
export function isTerminalOccluded(): boolean | null {
|
|
@@ -40,57 +45,38 @@ export function isTerminalOccluded(): boolean | null {
|
|
|
40
45
|
|
|
41
46
|
insideTerminator = true
|
|
42
47
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return callXdotool()
|
|
47
|
-
}
|
|
48
|
+
// ─── 第一级:X 窗口级别 ────────────────────────────────────────────────
|
|
49
|
+
// Terminator 窗口是否是当前 X 活跃窗口?
|
|
50
|
+
const terminatorActive = isTerminatorWindowActive()
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const occluded = !uuidEqual(focused, MY_UUID)
|
|
54
|
-
debug(`Terminator 焦点检测(busctl): 本屏=${shortId(MY_UUID)} 焦点=${shortId(focused)} 遮挡=${occluded}`)
|
|
55
|
-
return occluded
|
|
56
|
-
}
|
|
57
|
-
} catch (e) {
|
|
58
|
-
debug(`Terminator busctl 失败: ${e instanceof Error ? e.message : String(e)}`)
|
|
52
|
+
if (terminatorActive === false) {
|
|
53
|
+
// 用户在其他应用中(浏览器、IDE 等),本屏不可见
|
|
54
|
+
debug(`Terminator 窗口非活跃(用户在其他应用中),判定为遮挡`)
|
|
55
|
+
return true
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const occluded = !uuidEqual(focused, MY_UUID)
|
|
66
|
-
debug(`Terminator 焦点检测(python): 本屏=${shortId(MY_UUID)} 焦点=${shortId(focused)} 遮挡=${occluded}`)
|
|
67
|
-
return occluded
|
|
68
|
-
}
|
|
69
|
-
} catch (e) {
|
|
70
|
-
debug(`Terminator python-dbus 失败: ${e instanceof Error ? e.message : String(e)}`)
|
|
71
|
-
}
|
|
58
|
+
if (terminatorActive === true) {
|
|
59
|
+
// ─── 第二级:聚焦终端检测 ──────────────────────────────────────────
|
|
60
|
+
// Terminator 窗口活跃,检查用户聚焦的是不是本屏
|
|
61
|
+
const focused = queryFocusedTerminal()
|
|
72
62
|
|
|
73
|
-
// 策略 3: gdbus
|
|
74
|
-
try {
|
|
75
|
-
const focused = callGDBus()
|
|
76
63
|
if (focused !== null) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
if (focused === MY_UUID) {
|
|
65
|
+
debug(`Terminator 本屏聚焦,判定为可见`)
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
// 用户聚焦在另一个子屏上 → 本屏不可见(无论分屏还是最大化)
|
|
69
|
+
debug(`Terminator 用户聚焦在其他子屏(${shortId(focused)}),判定为遮挡`)
|
|
70
|
+
return true
|
|
80
71
|
}
|
|
81
|
-
} catch (e) {
|
|
82
|
-
debug(`Terminator gdbus 失败: ${e instanceof Error ? e.message : String(e)}`)
|
|
83
|
-
}
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
debug(`Terminator 窗口检测(xdotool): ${result}`)
|
|
89
|
-
return result
|
|
90
|
-
} catch (e) {
|
|
91
|
-
warn(`Terminator 所有检测策略均失败,最后错误: ${e instanceof Error ? e.message : String(e)}`)
|
|
92
|
-
return null
|
|
73
|
+
// 无法查询焦点状态,保守假设可见
|
|
74
|
+
debug(`Terminator 窗口活跃,无法查询焦点状态,保守假设不遮挡`)
|
|
75
|
+
return false
|
|
93
76
|
}
|
|
77
|
+
|
|
78
|
+
// 无法确定 X 窗口状态
|
|
79
|
+
return null
|
|
94
80
|
}
|
|
95
81
|
|
|
96
82
|
/**
|
|
@@ -104,61 +90,62 @@ export function isInsideTerminator(): boolean {
|
|
|
104
90
|
|
|
105
91
|
// ─── 工具函数 ───────────────────────────────────────────────────────────────
|
|
106
92
|
|
|
107
|
-
/** 比较两个 UUID(忽略 urn:uuid: 前缀) */
|
|
108
|
-
function uuidEqual(a: string, b: string): boolean {
|
|
109
|
-
const strip = (s: string) => s.replace(/^urn:uuid:/i, "")
|
|
110
|
-
return strip(a) === strip(b)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
93
|
/** 取 UUID 前 8 位用于日志 */
|
|
114
94
|
function shortId(uuid: string): string {
|
|
115
95
|
return uuid.replace(/^urn:uuid:/i, "").slice(0, 8)
|
|
116
96
|
}
|
|
117
97
|
|
|
118
|
-
// ───
|
|
119
|
-
|
|
120
|
-
/** 策略 1: busctl(systemd) */
|
|
121
|
-
function callBusctl(): string | null {
|
|
122
|
-
const out = execSync(
|
|
123
|
-
`busctl --user call ${DBUS_NAME} ${DBUS_PATH} ${DBUS_NAME} get_focused_terminal`,
|
|
124
|
-
{ encoding: "utf-8", timeout: 3000 },
|
|
125
|
-
).trim()
|
|
126
|
-
// 输出格式: s "uuid-string"
|
|
127
|
-
const m = out.match(/s\s+"([^"]+)"/)
|
|
128
|
-
return m ? m[1] : null
|
|
129
|
-
}
|
|
98
|
+
// ─── 第一级:X 窗口检测 ──────────────────────────────────────────────────
|
|
130
99
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
100
|
+
/**
|
|
101
|
+
* 检测 Terminator 窗口是否是当前 X 活跃窗口
|
|
102
|
+
* @returns true=Terminator 是活跃窗口, false=不是, null=检测失败
|
|
103
|
+
*/
|
|
104
|
+
function isTerminatorWindowActive(): boolean | null {
|
|
105
|
+
try {
|
|
106
|
+
const out = execSync(
|
|
107
|
+
`xprop -id $(xdotool getactivewindow) WM_CLASS 2>/dev/null`,
|
|
108
|
+
{ encoding: "utf-8", timeout: 5000 },
|
|
109
|
+
).trim()
|
|
110
|
+
return out.includes('"Terminator"')
|
|
111
|
+
} catch (e) {
|
|
112
|
+
debug(`xprop/xdotool 检测 Terminator 窗口失败: ${e instanceof Error ? e.message : String(e)}`)
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
144
115
|
}
|
|
145
116
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
// ─── 第二级:聚焦终端检测 ───────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 通过 DBus 查询当前聚焦的子屏 UUID
|
|
121
|
+
* @returns UUID 字符串,或 null(查询失败)
|
|
122
|
+
*/
|
|
123
|
+
function queryFocusedTerminal(): string | null {
|
|
124
|
+
if (!DBUS_NAME || !DBUS_PATH) return null
|
|
125
|
+
|
|
126
|
+
// 策略 1: busctl(无 stderr 输出,避免未知方法报错)
|
|
127
|
+
try {
|
|
128
|
+
const out = execSync(
|
|
129
|
+
`busctl --user call ${DBUS_NAME} ${DBUS_PATH} ${DBUS_NAME} get_focused_terminal 2>/dev/null`,
|
|
130
|
+
{ encoding: "utf-8", timeout: 3000 },
|
|
131
|
+
).trim()
|
|
132
|
+
const m = out.match(/s\s+"([^"]+)"/)
|
|
133
|
+
if (m) return m[1]
|
|
134
|
+
} catch {
|
|
135
|
+
// 静默忽略
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 策略 2: gdbus(备选)
|
|
139
|
+
try {
|
|
140
|
+
const out = execSync(
|
|
141
|
+
`gdbus call --session --dest ${DBUS_NAME} --object-path ${DBUS_PATH} --method ${DBUS_NAME}.get_focused_terminal 2>/dev/null`,
|
|
142
|
+
{ encoding: "utf-8", timeout: 3000 },
|
|
143
|
+
).trim()
|
|
144
|
+
const m = out.match(/\('([^']+)'/)
|
|
145
|
+
if (m) return m[1]
|
|
146
|
+
} catch {
|
|
147
|
+
// 静默忽略
|
|
148
|
+
}
|
|
156
149
|
|
|
157
|
-
|
|
158
|
-
function callXdotool(): boolean | null {
|
|
159
|
-
const cls = execSync(
|
|
160
|
-
`xdotool getactivewindow getwindowclassname`,
|
|
161
|
-
{ encoding: "utf-8", timeout: 3000 },
|
|
162
|
-
).trim()
|
|
163
|
-
return cls === "Terminator" ? true : null
|
|
150
|
+
return null
|
|
164
151
|
}
|