@freely01/opencode-notify 0.2.0 → 0.3.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 +10 -10
- package/package.json +1 -1
- package/senders/screen-flash/index.ts +12 -7
- package/senders/screen-flash/win32.ts +129 -0
- package/senders/system/index.ts +8 -2
- package/senders/system/win32.ts +38 -15
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ opencode 通知插件 — 监听会话中的关键事件,通过多渠道推送
|
|
|
13
13
|
> - Linux 系统通知需 `libnotify` 包(桌面发行版通常预装)
|
|
14
14
|
> - 事件映射基于 @opencode-ai/plugin@1.15.12 的行为,后续版本升级可能影响兼容性
|
|
15
15
|
> - `run_completed` 事件暂未实现(opencode 无直接完成事件)
|
|
16
|
-
> -
|
|
16
|
+
> - 屏幕跑马灯效果:Linux X11(Python + PyGObject)/ Windows(PowerShell + WinForms)四边彩色流动
|
|
17
17
|
> - 仅在 Ubuntu 24.04 (X11) 环境下测试并使用,其它平台未验证
|
|
18
18
|
>
|
|
19
19
|
> 如有问题欢迎提 Issue,但不保证及时响应和修复。
|
|
@@ -26,7 +26,7 @@ opencode 通知插件 — 监听会话中的关键事件,通过多渠道推送
|
|
|
26
26
|
- 去重机制:同一事件在时间窗口内不重复发送
|
|
27
27
|
- 会话感知抑制:活跃会话按事件类型智能过滤,不遗漏 `run_failed` 等重要通知
|
|
28
28
|
- 零外部运行时依赖(仅 js-yaml 用于配置解析)
|
|
29
|
-
- **屏幕跑马灯**:通知时屏幕四边高亮闪烁(Linux X11,Python + GTK
|
|
29
|
+
- **屏幕跑马灯**:通知时屏幕四边高亮闪烁(Linux X11,Python + GTK 内置;Windows 实验性,PowerShell + WinForms)
|
|
30
30
|
- **渠道级事件过滤**:每个渠道可独立配置监听哪些事件,灵活分流
|
|
31
31
|
- **远程延迟推送**:正常通知发出后,指定渠道额外延迟推送以防遗漏
|
|
32
32
|
- **Terminator 子屏检测**:自动检测子屏最大化场景,被遮挡的会话强制通知
|
|
@@ -39,13 +39,13 @@ opencode 通知插件 — 监听会话中的关键事件,通过多渠道推送
|
|
|
39
39
|
| 自定义 Webhook / 企业微信 / 飞书 | ✅ | ✅ | ✅ |
|
|
40
40
|
| 诊断 CLI (`bun cli.ts`) | ✅ | ✅ | ✅ |
|
|
41
41
|
| **系统消息通知** | ✅ `osascript` 内置 | ⚠️ 需 `libnotify` 包 | ⚠️ 需 BurntToast 模块 |
|
|
42
|
-
| **屏幕跑马灯** | ❌ | ✅ Python+GTK 内置 |
|
|
42
|
+
| **屏幕跑马灯** | ❌ | ✅ Python+GTK 内置 | ✅ PowerShell+WinForms |
|
|
43
43
|
|
|
44
44
|
**说明:**
|
|
45
45
|
- **macOS**: 系统通知使用 `osascript`,系统内置,开箱即用
|
|
46
46
|
- **Linux**: 系统通知使用 `notify-send`,来自 `libnotify`。桌面发行版通常预装,如缺失可 `apt install libnotify-bin` / `yum install libnotify`
|
|
47
47
|
- **Windows**: 系统通知使用 PowerShell `New-BurntToastNotification`,需额外安装 [BurntToast](https://github.com/Windos/BurntToast) 模块。Webhook 渠道不受影响
|
|
48
|
-
- **屏幕跑马灯**:
|
|
48
|
+
- **屏幕跑马灯**: Linux X11 使用 Python + PyGObject(GTK 3) 创建透明覆盖窗口,60fps 彩色四边跑马灯动画;Windows 使用 PowerShell + .NET WinForms 创建屏幕四边彩色闪烁边框(实验性)。中间完全透明可点击穿透,不影响操作。macOS 暂不支持
|
|
49
49
|
- 非系统通知模块(Webhook 推送、CLI 诊断)均为纯 HTTP/Node API,全平台一致
|
|
50
50
|
|
|
51
51
|
> **已测试渠道:** 系统通知、企业微信、自定义 Webhook(Gotify)。飞书等其他渠道理论可用,暂未做验证。
|
|
@@ -158,10 +158,9 @@ custom_webhook:
|
|
|
158
158
|

|
|
159
159
|
|
|
160
160
|
- 独立渠道,可与系统通知分开启停、分开配置事件过滤
|
|
161
|
-
- 使用 Python + PyGObject(GTK 3)
|
|
162
|
-
-
|
|
161
|
+
- Linux X11: 使用 Python + PyGObject(GTK 3) 创建透明覆盖窗口,60fps 彩色灯光沿四边循环运动
|
|
162
|
+
- Windows: 使用 PowerShell + .NET WinForms 创建屏幕四边彩色闪烁边框(8px 宽),中间完全透明可点击穿透,不阻挡任何操作
|
|
163
163
|
- 非阻塞执行,不影响通知发送速度
|
|
164
|
-
- 仅 Linux X11 环境,Ubuntu GNOME 桌面内置,无需额外安装
|
|
165
164
|
|
|
166
165
|
```yaml
|
|
167
166
|
screen_flash:
|
|
@@ -506,9 +505,10 @@ opencode-notify/
|
|
|
506
505
|
│ │ ├── darwin.ts # macOS (osascript)
|
|
507
506
|
│ │ ├── linux.ts # Linux (notify-send)
|
|
508
507
|
│ │ └── win32.ts # Windows (PowerShell)
|
|
509
|
-
│ ├── screen-flash/ # 屏幕跑马灯(Linux
|
|
510
|
-
│ │ ├── index.ts # 入口 +
|
|
511
|
-
│ │
|
|
508
|
+
│ ├── screen-flash/ # 屏幕跑马灯(Linux + Windows)
|
|
509
|
+
│ │ ├── index.ts # 入口 + 平台路由
|
|
510
|
+
│ │ ├── linux.ts # Linux 实现 (Python+GTK)
|
|
511
|
+
│ │ └── win32.ts # Windows 实现 (PowerShell+WinForms)
|
|
512
512
|
│ ├── custom-webhook.ts # 自定义 Webhook
|
|
513
513
|
│ ├── wechat-work.ts # 企业微信
|
|
514
514
|
│ └── feishu.ts # 飞书
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 屏幕跑马灯发送器
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 通知时在屏幕产生彩色闪烁效果,吸引注意力。
|
|
5
|
+
* - Linux → Python + GTK3 透明覆盖窗口(四边跑马灯动画)
|
|
6
|
+
* - Windows → PowerShell + .NET WinForms 全屏彩色闪烁
|
|
7
|
+
* - macOS / 其他 → 静默忽略
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { Sender } from "../types.js"
|
|
10
11
|
import type { Message } from "../../message.js"
|
|
11
12
|
import type { ScreenFlashChannelConfig } from "../../config.js"
|
|
12
|
-
import { flash } from "./linux.js"
|
|
13
|
+
import { flash as linuxFlash } from "./linux.js"
|
|
14
|
+
import { flash as win32Flash } from "./win32.js"
|
|
13
15
|
|
|
14
16
|
export class ScreenFlashSender implements Sender {
|
|
15
17
|
readonly name = "screen_flash"
|
|
@@ -20,8 +22,11 @@ export class ScreenFlashSender implements Sender {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
async send(_msg: Message): Promise<void> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if (process.platform === "linux") {
|
|
26
|
+
await linuxFlash(this.config)
|
|
27
|
+
} else if (process.platform === "win32") {
|
|
28
|
+
await win32Flash(this.config)
|
|
29
|
+
}
|
|
30
|
+
// macOS 及其他平台静默忽略
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows 屏幕跑马灯
|
|
3
|
+
*
|
|
4
|
+
* 使用 PowerShell + .NET Windows Forms 创建屏幕四边彩色闪烁边框。
|
|
5
|
+
* 将四边分成多个小段,逐帧偏移颜色,产生 LED 跑马灯流动效果。
|
|
6
|
+
*
|
|
7
|
+
* 所有数据通过 $form.Tag(Form 的真实属性)传递,避免事件回调作用域问题。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "node:child_process"
|
|
11
|
+
import type { ScreenFlashChannelConfig } from "../../config.js"
|
|
12
|
+
|
|
13
|
+
export async function flash(config: ScreenFlashChannelConfig): Promise<void> {
|
|
14
|
+
const duration = config.duration ?? 3.0
|
|
15
|
+
const speed = config.speed ?? 4.0
|
|
16
|
+
const borderWidth = 8
|
|
17
|
+
const totalMs = duration * 1000
|
|
18
|
+
const intervalMs = Math.max(20, Math.min(150, Math.round(60 / speed)))
|
|
19
|
+
const segSize = 10
|
|
20
|
+
|
|
21
|
+
const psScript = `
|
|
22
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
+
Add-Type -AssemblyName System.Drawing
|
|
24
|
+
|
|
25
|
+
$bc = @(
|
|
26
|
+
[System.Drawing.Color]::Red,
|
|
27
|
+
[System.Drawing.Color]::Orange,
|
|
28
|
+
[System.Drawing.Color]::Yellow,
|
|
29
|
+
[System.Drawing.Color]::LimeGreen,
|
|
30
|
+
[System.Drawing.Color]::Blue,
|
|
31
|
+
[System.Drawing.Color]::Purple
|
|
32
|
+
)
|
|
33
|
+
$g = New-Object System.Collections.ArrayList
|
|
34
|
+
for ($b = 0; $b -lt $bc.Length; $b++) {
|
|
35
|
+
$n = ($b + 1) % $bc.Length
|
|
36
|
+
for ($s = 0; $s -lt 8; $s++) {
|
|
37
|
+
$r = [int]($bc[$b].R + ($bc[$n].R - $bc[$b].R) * $s / 8)
|
|
38
|
+
$g_ = [int]($bc[$b].G + ($bc[$n].G - $bc[$b].G) * $s / 8)
|
|
39
|
+
$b_ = [int]($bc[$b].B + ($bc[$n].B - $bc[$b].B) * $s / 8)
|
|
40
|
+
[void]$g.Add([System.Drawing.Color]::FromArgb(255, [Math]::Min(255,$r), [Math]::Min(255,$g_), [Math]::Min(255,$b_)))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$form = New-Object System.Windows.Forms.Form
|
|
45
|
+
$form.WindowState = 'Maximized'
|
|
46
|
+
$form.FormBorderStyle = 'None'
|
|
47
|
+
$form.TopMost = $true
|
|
48
|
+
$form.ShowInTaskbar = $false
|
|
49
|
+
$form.BackColor = [System.Drawing.Color]::Fuchsia
|
|
50
|
+
$form.TransparencyKey = [System.Drawing.Color]::Fuchsia
|
|
51
|
+
# Tag 存哈希表: @{ offset=0; gradient=$g; segSize=${segSize}; bw=${borderWidth} }
|
|
52
|
+
$form.Tag = @{ offset = 0; gradient = $g; segSize = ${segSize}; bw = ${borderWidth} }
|
|
53
|
+
|
|
54
|
+
$form.Add_Paint({
|
|
55
|
+
$d = $form.Tag
|
|
56
|
+
$gfx = $args[1].Graphics
|
|
57
|
+
$w = $form.ClientSize.Width
|
|
58
|
+
$h = $form.ClientSize.Height
|
|
59
|
+
$gr = $d.gradient
|
|
60
|
+
$gl = $gr.Count
|
|
61
|
+
$ss = $d.segSize
|
|
62
|
+
$bw = $d.bw
|
|
63
|
+
$off = $d.offset
|
|
64
|
+
|
|
65
|
+
# 上边 (左→右)
|
|
66
|
+
for ($x = 0; $x -lt $w; $x += $ss) {
|
|
67
|
+
$gi = [int](($x / $ss) + $off) % $gl
|
|
68
|
+
$sw = [Math]::Min($ss, $w - $x)
|
|
69
|
+
$br = [System.Drawing.SolidBrush]::new($gr[$gi])
|
|
70
|
+
$gfx.FillRectangle($br, $x, 0, $sw, $bw)
|
|
71
|
+
$br.Dispose()
|
|
72
|
+
}
|
|
73
|
+
# 右边 (上→下)
|
|
74
|
+
for ($y = 0; $y -lt $h; $y += $ss) {
|
|
75
|
+
$gi = [int](($w / $ss) + ($y / $ss) + $off) % $gl
|
|
76
|
+
$sh = [Math]::Min($ss, $h - $y)
|
|
77
|
+
$br = [System.Drawing.SolidBrush]::new($gr[$gi])
|
|
78
|
+
$gfx.FillRectangle($br, $w - $bw, $y, $bw, $sh)
|
|
79
|
+
$br.Dispose()
|
|
80
|
+
}
|
|
81
|
+
# 下边 (右→左)
|
|
82
|
+
for ($x = 0; $x -lt $w; $x += $ss) {
|
|
83
|
+
$gi = [int](($w / $ss) + ($h / $ss) + ($x / $ss) + $off) % $gl
|
|
84
|
+
$sw = [Math]::Min($ss, $w - $x)
|
|
85
|
+
$br = [System.Drawing.SolidBrush]::new($gr[$gi])
|
|
86
|
+
$gfx.FillRectangle($br, $w - $x - $sw, $h - $bw, $sw, $bw)
|
|
87
|
+
$br.Dispose()
|
|
88
|
+
}
|
|
89
|
+
# 左边 (下→上)
|
|
90
|
+
for ($y = 0; $y -lt $h; $y += $ss) {
|
|
91
|
+
$gi = [int](($w / $ss) * 2 + ($h / $ss) + ($y / $ss) + $off) % $gl
|
|
92
|
+
$sh = [Math]::Min($ss, $h - $y)
|
|
93
|
+
$br = [System.Drawing.SolidBrush]::new($gr[$gi])
|
|
94
|
+
$gfx.FillRectangle($br, 0, $h - $y - $sh, $bw, $sh)
|
|
95
|
+
$br.Dispose()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
$form.Show()
|
|
100
|
+
|
|
101
|
+
# 点击穿透
|
|
102
|
+
$code = @'
|
|
103
|
+
[DllImport("user32.dll")]
|
|
104
|
+
public static extern int SetWindowLong(IntPtr h, int n, int v);
|
|
105
|
+
[DllImport("user32.dll")]
|
|
106
|
+
public static extern int GetWindowLong(IntPtr h, int n);
|
|
107
|
+
'@
|
|
108
|
+
$win32 = Add-Type -MemberDefinition $code -Name W -Namespace W -PassThru
|
|
109
|
+
$style = $win32::GetWindowLong($form.Handle, -20)
|
|
110
|
+
$win32::SetWindowLong($form.Handle, -20, $style -bor 0x20 -bor 0x80)
|
|
111
|
+
|
|
112
|
+
# 跑马灯循环
|
|
113
|
+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
|
114
|
+
while ($sw.ElapsedMilliseconds -lt ${totalMs}) {
|
|
115
|
+
$d = $form.Tag
|
|
116
|
+
$d.offset = $d.offset + 1
|
|
117
|
+
$form.Invalidate()
|
|
118
|
+
[System.Windows.Forms.Application]::DoEvents()
|
|
119
|
+
Start-Sleep -Milliseconds ${intervalMs}
|
|
120
|
+
}
|
|
121
|
+
$sw.Stop()
|
|
122
|
+
$form.Close()
|
|
123
|
+
`.trim()
|
|
124
|
+
|
|
125
|
+
execSync(`powershell -NoProfile -Command ${JSON.stringify(psScript)}`, {
|
|
126
|
+
timeout: Math.max(5000, totalMs + 5000),
|
|
127
|
+
stdio: "ignore",
|
|
128
|
+
})
|
|
129
|
+
}
|
package/senders/system/index.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* 根据运行平台自动选择实现:
|
|
5
5
|
* - macOS → darwin.ts (osascript)
|
|
6
6
|
* - Linux → linux.ts (notify-send)
|
|
7
|
-
* - Windows → win32.ts (
|
|
7
|
+
* - Windows → win32.ts (WinRT Native Toast + NotifyIcon 回退)
|
|
8
8
|
* - 其他平台 → 静默忽略
|
|
9
|
+
*
|
|
10
|
+
* Windows 首次调用时自动注册快捷方式,使 toast 出现在通知中心。
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import type { Sender } from "../types.js"
|
|
@@ -39,6 +41,11 @@ export class SystemSender implements Sender {
|
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* 转义标题和正文中的特殊字符,防止 shell 注入
|
|
44
|
+
*
|
|
45
|
+
* 各平台实际使用的逃逸:
|
|
46
|
+
* - Linux: notify-send "${title}" — 只需转义 " $ ` \
|
|
47
|
+
* - macOS: osascript -e JSON 序列化 — 全自动处理
|
|
48
|
+
* - Windows: PowerShell '${title}' — 只需转义单引号(win32.ts 内部处理)
|
|
42
49
|
*/
|
|
43
50
|
function sanitize(
|
|
44
51
|
title: string,
|
|
@@ -53,7 +60,6 @@ function sanitize(
|
|
|
53
60
|
function escape(s: string): string {
|
|
54
61
|
return s
|
|
55
62
|
.replace(/"/g, '\\"')
|
|
56
|
-
.replace(/'/g, "\\'")
|
|
57
63
|
.replace(/`/g, "\\`")
|
|
58
64
|
.replace(/\$/g, "\\$")
|
|
59
65
|
.replace(/\n/g, " ")
|
package/senders/system/win32.ts
CHANGED
|
@@ -1,28 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Windows 系统通知
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 策略:
|
|
5
|
+
* 1. 通过注册表注册 opencode-notify 通知发送方
|
|
6
|
+
* 2. WinRT Native Toast — 使用 PowerShell UUID AppId(确认可弹窗)
|
|
7
|
+
* 3. NotifyIcon BalloonTip — 非阻塞式最终回退
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* 注册表路径: HKCU\SOFTWARE\Classes\AppUserModelId\opencode-notify
|
|
10
|
+
* 此注册使通知出现在 Windows 操作中心。
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { execSync } from "node:child_process"
|
|
12
14
|
|
|
13
15
|
export async function notify(title: string, body: string): Promise<void> {
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
`try {`,
|
|
17
|
-
` New-BurntToastNotification -Text '${title}', '${body}' -ErrorAction Stop`,
|
|
18
|
-
`} catch {`,
|
|
19
|
-
` Add-Type -AssemblyName System.Windows.Forms`,
|
|
20
|
-
` [System.Windows.Forms.MessageBox]::Show('${body}', '${title}')`,
|
|
21
|
-
`}`,
|
|
22
|
-
].join("\n")
|
|
16
|
+
const t = title.replace(/'/g, "''")
|
|
17
|
+
const b = body.replace(/'/g, "''")
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
const ps = `
|
|
20
|
+
# 注册 opencode-notify 到操作中心
|
|
21
|
+
New-Item -Path 'HKCU:\\SOFTWARE\\Classes\\AppUserModelId\\opencode-notify' -Force -ErrorAction Stop | Out-Null
|
|
22
|
+
New-ItemProperty -Path 'HKCU:\\SOFTWARE\\Classes\\AppUserModelId\\opencode-notify' -Name 'DisplayName' -Value 'opencode-notify' -PropertyType String -Force -ErrorAction Stop | Out-Null
|
|
23
|
+
New-ItemProperty -Path 'HKCU:\\SOFTWARE\\Classes\\AppUserModelId\\opencode-notify' -Name 'ShowInSettings' -Value 1 -PropertyType DWord -Force -ErrorAction Stop | Out-Null
|
|
24
|
+
|
|
25
|
+
# 策略 1: WinRT Native Toast (PowerShell AppId)
|
|
26
|
+
try {
|
|
27
|
+
[Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications,ContentType=WindowsRuntime] > \$null
|
|
28
|
+
[Windows.Data.Xml.Dom.XmlDocument,Windows.Data.Xml.Dom.XmlDocument,ContentType=WindowsRuntime] > \$null
|
|
29
|
+
\$t=[Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent('ToastText02')
|
|
30
|
+
\$t.SelectSingleNode('//text[@id=\"1\"]').InnerText='${t}'
|
|
31
|
+
\$t.SelectSingleNode('//text[@id=\"2\"]').InnerText='${b}'
|
|
32
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show(\$t)
|
|
33
|
+
} catch {
|
|
34
|
+
# 策略 2: NotifyIcon 回退
|
|
35
|
+
try {
|
|
36
|
+
Add-Type -AssemblyName System.Windows.Forms,System.Drawing
|
|
37
|
+
\$n=New-Object System.Windows.Forms.NotifyIcon
|
|
38
|
+
\$n.Icon=[System.Drawing.SystemIcons]::Information
|
|
39
|
+
\$n.Visible=\$true
|
|
40
|
+
\$n.ShowBalloonTip(10000,'${t}','${b}',[System.Windows.Forms.TooltipIcon]::None)
|
|
41
|
+
Start-Sleep -Milliseconds 500
|
|
42
|
+
\$n.Dispose()
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
`.trim()
|
|
46
|
+
|
|
47
|
+
execSync(`powershell -NoProfile -Command ${JSON.stringify(ps)}`, {
|
|
48
|
+
timeout: 15000,
|
|
26
49
|
stdio: "ignore",
|
|
27
50
|
})
|
|
28
51
|
}
|