@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 CHANGED
@@ -13,7 +13,7 @@ opencode 通知插件 — 监听会话中的关键事件,通过多渠道推送
13
13
  > - Linux 系统通知需 `libnotify` 包(桌面发行版通常预装)
14
14
  > - 事件映射基于 @opencode-ai/plugin@1.15.12 的行为,后续版本升级可能影响兼容性
15
15
  > - `run_completed` 事件暂未实现(opencode 无直接完成事件)
16
- > - 屏幕跑马灯效果仅 Linux X11 环境支持(依赖 Python + PyGObject),Wayland/macOS/Windows 不生效
16
+ > - 屏幕跑马灯效果:Linux X11Python + 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
- - **屏幕跑马灯**: Linux X11 环境。使用 Python + PyGObject(GTK 3),Ubuntu GNOME 桌面内置,无需额外安装。Wayland 暂不支持
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
  ![跑马灯效果](doc/de.png)
159
159
 
160
160
  - 独立渠道,可与系统通知分开启停、分开配置事件过滤
161
- - 使用 Python + PyGObject(GTK 3) 创建透明覆盖窗口,不干扰当前操作
162
- - 60fps 动画,彩色灯光沿四边循环运动(红→橙→黄→绿→蓝)
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 X11
510
- │ │ ├── index.ts # 入口 + 平台守卫
511
- │ │ └── linux.ts # Linux 实现 (Python+GTK)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@freely01/opencode-notify",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "opencode 通知插件 - 监听会话事件并通过多渠道推送通知",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * 屏幕跑马灯发送器
3
3
  *
4
- * 通知时在屏幕四边产生彩色跑马灯效果。
5
- * 当前仅 Linux X11 支持(Python + GTK 透明覆盖窗口)。
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
- // Linux 支持跑马灯效果
24
- if (process.platform !== "linux") return
25
- await flash(this.config)
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
+ }
@@ -4,8 +4,10 @@
4
4
  * 根据运行平台自动选择实现:
5
5
  * - macOS → darwin.ts (osascript)
6
6
  * - Linux → linux.ts (notify-send)
7
- * - Windows → win32.ts (PowerShell BurntToast / MessageBox)
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, " ")
@@ -1,28 +1,51 @@
1
1
  /**
2
2
  * Windows 系统通知
3
3
  *
4
- * 优先使用 BurntToast(需额外安装),失败时回退为 MessageBox。
5
- * 安装 BurntToast:
6
- * Install-Module -Name BurntToast -Force
4
+ * 策略:
5
+ * 1. 通过注册表注册 opencode-notify 通知发送方
6
+ * 2. WinRT Native Toast — 使用 PowerShell UUID AppId(确认可弹窗)
7
+ * 3. NotifyIcon BalloonTip — 非阻塞式最终回退
7
8
  *
8
- * PowerShell script 使用单引号包裹字符串以避免扩展问题。
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
- // PowerShell 脚本:优先 BurntToast,失败回退 MessageBox
15
- const psScript = [
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
- execSync(`powershell -NoProfile -Command ${JSON.stringify(psScript)}`, {
25
- timeout: 10000,
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
  }