dsh-notify-yimit 0.1.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.
@@ -0,0 +1,393 @@
1
+ <#
2
+ dsh-notify 常驻浮窗宿主(Windows PowerShell + WPF)。
3
+ 由 dsh-notify 宿主插件在加载时 spawn 一次并常驻:所有桌面浮窗都在本进程内创建,
4
+ WPF 程序集只加载一次,通知创建延迟从 ~1s 冷启动降到 ~10ms(根治"总感觉慢")。
5
+
6
+ 协议(UTF-8,一行一个 JSON):
7
+ stdin(入站命令):
8
+ {"cmd":"show","key":"running:s1","instance":1,"title":"...","text":"...",
9
+ "bg":"#286ebd","fg":"#e6edf3","durationSec":8,"sticky":true,"offsetY":0,
10
+ "sessionId":"s1","baseUrl":"http://127.0.0.1:3080",
11
+ "browserPath":"","winTitle":"dsh-notify-running:s1"}
12
+ {"cmd":"text","key":...,"text":"..."} # 原地更新内容(不重弹、零延迟)
13
+ {"cmd":"move","key":...,"top":588} # 平滑移动(220ms QuadraticEase)
14
+ {"cmd":"close","key":...} # 160ms 淡出 + 20px 下坠后关闭
15
+ {"cmd":"shutdown"} # 关闭全部窗口并退出
16
+ stdout(出站报告):
17
+ {"type":"pos","key":...,"instance":1,"left":...,"top":...,"height":...,
18
+ "hwnd":...,"workBottom":...}
19
+ {"type":"exit","key":...,"instance":...}
20
+ 说明:主线程(STA)跑 WPF Dispatcher;stdin 由独立 runspace 线程读取并放入
21
+ ConcurrentQueue,命令在 Dispatcher 定时器(50ms)中处理——窗口操作线程安全。
22
+ #>
23
+
24
+ Add-Type -AssemblyName PresentationFramework
25
+ Add-Type -AssemblyName PresentationCore
26
+ Add-Type -AssemblyName WindowsBase
27
+
28
+ # WPF 必须运行在 STA 主线程(Windows PowerShell 5.1 控制台主线程为 STA)。
29
+ if ([System.Threading.Thread]::CurrentThread.ApartmentState -ne [System.Threading.ApartmentState]::STA) {
30
+ [Console]::Error.WriteLine("dsh-notify: 宿主需要 STA 主线程(当前 $([System.Threading.Thread]::CurrentThread.ApartmentState))")
31
+ exit 1
32
+ }
33
+
34
+ # ── 协议输出:UTF-8 无 BOM(管道传中文必须固定编码,否则乱码) ────────────────
35
+ $script:utf8 = New-Object System.Text.UTF8Encoding($false)
36
+ $script:stdout = New-Object System.IO.StreamWriter([System.Console]::OpenStandardOutput(), $script:utf8)
37
+ $script:stdout.AutoFlush = $true
38
+
39
+ function Send-Report([object]$obj) {
40
+ try { $script:stdout.WriteLine(($obj | ConvertTo-Json -Compress)) } catch { }
41
+ }
42
+
43
+ # 浮窗注册表(key → @{ Win; Body; Timer; Instance }),只在 WPF 主线程访问。
44
+ $script:toasts = @{}
45
+ $script:exitRequested = $false
46
+
47
+ # 颜色解析:#RGB / #RRGGBB / #AARRGGBB → Windows.Media.Color
48
+ function Parse-Color([string]$hex, [string]$fallback) {
49
+ $h = $hex.TrimStart('#')
50
+ if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
51
+ if ($h.Length -eq 6) { $h = "FF$h" }
52
+ if ($h.Length -ne 8 -or $h -notmatch '^[0-9a-fA-F]{8}$') { $h = $fallback.TrimStart('#') }
53
+ return [System.Windows.Media.Color]::FromArgb(
54
+ [Convert]::ToInt32($h.Substring(0,2), 16),
55
+ [Convert]::ToInt32($h.Substring(2,2), 16),
56
+ [Convert]::ToInt32($h.Substring(4,2), 16),
57
+ [Convert]::ToInt32($h.Substring(6,2), 16))
58
+ }
59
+
60
+ # 按钮用 Border 直接实现:标准圆角矩形(10px)。忽略 = 透明底 + 细边框(fg 色);
61
+ # 跳转 = 浅色实心底 + 主色文字。悬停/按压用不透明压暗色反馈(避免透明窗口闪烁)。
62
+ $downP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(158,164,170))
63
+ $downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(30,36,44))
64
+ $hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196,202,208))
65
+ $hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44,52,62))
66
+
67
+ function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary,
68
+ [System.Windows.Media.SolidColorBrush]$fg, [System.Windows.Media.SolidColorBrush]$bg) {
69
+ $bd = New-Object System.Windows.Controls.Border
70
+ $bd.CornerRadius = New-Object System.Windows.CornerRadius(10)
71
+ $bd.Padding = New-Object System.Windows.Thickness(18,7,18,7)
72
+ $bd.Margin = New-Object System.Windows.Thickness(0,0,8,0)
73
+ $bd.Cursor = [System.Windows.Input.Cursors]::Hand
74
+ $bd.Focusable = $false
75
+ $txt = New-Object System.Windows.Controls.TextBlock
76
+ $txt.Text = $label
77
+ $txt.FontFamily = New-Object System.Windows.Media.FontFamily("Microsoft YaHei UI")
78
+ $txt.FontSize = 12
79
+ $txt.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
80
+ $txt.IsHitTestVisible = $false # 点击穿透到 Border,确保命中与事件稳定
81
+ if ($primary) {
82
+ $bd.Background = $fg
83
+ $txt.Foreground = $bg
84
+ $bd.BorderBrush = [System.Windows.Media.Brushes]::Transparent
85
+ $bd.BorderThickness = New-Object System.Windows.Thickness(0)
86
+ } else {
87
+ $bd.Background = [System.Windows.Media.Brushes]::Transparent
88
+ $txt.Foreground = $fg
89
+ $bd.BorderBrush = $fg
90
+ $bd.BorderThickness = New-Object System.Windows.Thickness(1)
91
+ }
92
+ $bd.Child = $txt
93
+ # 事件回调在函数返回后触发,函数局部变量不可见:回调和主按钮标记存入 Tag(hashtable),
94
+ # 回调统一从 $this(事件源)的 Tag 读取。
95
+ $bd.Tag = @{ primary = $primary; onClick = $onClick }
96
+ $bd.Add_MouseEnter({
97
+ $info = $this.Tag
98
+ $this.Background = if ($info.primary) { $hoverP } else { $hoverI }
99
+ }) | Out-Null
100
+ $bd.Add_MouseLeave({
101
+ $info = $this.Tag
102
+ $this.Background = if ($info.primary) { $fg } else { [System.Windows.Media.Brushes]::Transparent }
103
+ }) | Out-Null
104
+ $bd.Add_MouseLeftButtonDown({
105
+ $info = $this.Tag
106
+ $this.Background = if ($info.primary) { $downP } else { $downI }
107
+ }) | Out-Null
108
+ $bd.Add_MouseLeftButtonUp({
109
+ $info = $this.Tag
110
+ try { if ($info.onClick -ne $null) { & $info.onClick } } catch { }
111
+ $this.Background = if ($info.primary) { $fg } else { [System.Windows.Media.Brushes]::Transparent }
112
+ }) | Out-Null
113
+ return $bd
114
+ }
115
+
116
+ # 动画缓动(脚本级,供各处复用)。
117
+ # 注意:PS 5.1 的 New-Object 无法用枚举参数调构造器(会报 "overload ... argument count: 1"),
118
+ # 必须默认构造后设置 EasingMode 属性。
119
+ $easeMove = New-Object System.Windows.Media.Animation.QuadraticEase
120
+ $easeMove.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
121
+ $easeExit = New-Object System.Windows.Media.Animation.CubicEase
122
+ $easeExit.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseIn
123
+
124
+ # 淡出关闭:160ms EaseIn 淡出 + 20px 下坠,动画完成后 Close(Closed 事件上报 exit)。
125
+ # 防重入:$w.Tag='closing' 只执行一次(忽略按钮 + close 命令 + 定时器可能先后触发);
126
+ # 600ms 硬性兜底,确保即使动画/定时器异常也强制关闭(绝不挂起)。
127
+ function Close-WithFade($w) {
128
+ try {
129
+ if ($w.Tag -eq 'closing') { return }
130
+ $w.Tag = 'closing'
131
+ $fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0, [TimeSpan]::FromMilliseconds(160))
132
+ $fadeOut.EasingFunction = $easeExit
133
+ # 事件回调延迟触发,看不到函数局部变量:GetNewClosure() 捕获 $w。
134
+ $fadeOut.Add_Completed({ try { $w.Close() } catch { } }.GetNewClosure()) | Out-Null
135
+ $w.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeOut)
136
+ $dropAn = New-Object System.Windows.Media.Animation.DoubleAnimation($w.Top, ($w.Top + 20), [TimeSpan]::FromMilliseconds(160))
137
+ $dropAn.EasingFunction = $easeExit
138
+ $w.BeginAnimation([System.Windows.Window]::TopProperty, $dropAn)
139
+ $backstop = New-Object System.Windows.Threading.DispatcherTimer
140
+ $backstop.Interval = [TimeSpan]::FromMilliseconds(600)
141
+ $backstop.Tag = $w
142
+ $backstop.Add_Tick({
143
+ $this.Stop()
144
+ try { if ($this.Tag.IsVisible) { $this.Tag.Close() } } catch { }
145
+ }) | Out-Null
146
+ $backstop.Start()
147
+ } catch {
148
+ try { $w.Close() } catch { }
149
+ }
150
+ }
151
+
152
+ # 建窗:按 show 命令参数创建窗口;返回 Window(由调用方 Show() 显示)。
153
+ function New-ToastWindow($cmd) {
154
+ $bg = Parse-Color ([string]$cmd.bg) "#20272f"
155
+ $fg = Parse-Color ([string]$cmd.fg) "#e6edf3"
156
+ $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
157
+ $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
158
+
159
+ $win = New-Object System.Windows.Window
160
+ $win.WindowStyle = [System.Windows.WindowStyle]::None
161
+ $win.AllowsTransparency = $true
162
+ $win.Background = [System.Windows.Media.Brushes]::Transparent
163
+ $win.Topmost = $true
164
+ $win.ShowInTaskbar = $false
165
+ $win.Width = 340
166
+ $win.SizeToContent = [System.Windows.SizeToContent]::Height
167
+ if (-not [string]::IsNullOrEmpty([string]$cmd.winTitle)) { $win.Title = [string]$cmd.winTitle }
168
+
169
+ # 圆角卡片容器(窗口圆角 12;按钮为圆角矩形 10px)。
170
+ $cornerRadius = 12
171
+ $border = New-Object System.Windows.Controls.Border
172
+ $border.Background = $bgBrush
173
+ $border.BorderBrush = $fgBrush
174
+ $border.BorderThickness = New-Object System.Windows.Thickness(1)
175
+ $border.CornerRadius = New-Object System.Windows.CornerRadius($cornerRadius)
176
+ $border.Margin = New-Object System.Windows.Thickness(12)
177
+
178
+ $root = New-Object System.Windows.Controls.StackPanel
179
+ $root.Margin = New-Object System.Windows.Thickness(14,12,14,12)
180
+ $border.Child = $root
181
+
182
+ # 标题行
183
+ $titleText = New-Object System.Windows.Controls.TextBlock
184
+ $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { "(未命名会话)" } else { [string]$cmd.title }
185
+ $titleText.Foreground = $fgBrush
186
+ $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Microsoft YaHei UI")
187
+ $titleText.FontSize = 13
188
+ $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
189
+ $titleText.TextTrimming = [System.Windows.TextTrimming]::CharacterEllipsis
190
+ $titleText.Margin = New-Object System.Windows.Thickness(0,0,0,6)
191
+ $root.Children.Add($titleText) | Out-Null
192
+
193
+ # 内容(保留引用:text 命令原地更新,零延迟不重弹)
194
+ $bodyText = New-Object System.Windows.Controls.TextBlock
195
+ $bodyText.Text = [string]$cmd.text
196
+ $bodyText.Foreground = $fgBrush
197
+ $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Microsoft YaHei UI")
198
+ $bodyText.FontSize = 13
199
+ $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
200
+ $bodyText.Margin = New-Object System.Windows.Thickness(0,0,0,10)
201
+ $root.Children.Add($bodyText) | Out-Null
202
+
203
+ # 按钮行
204
+ $btnRow = New-Object System.Windows.Controls.StackPanel
205
+ $btnRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
206
+ $btnRow.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
207
+
208
+ $ignoreBtn = New-ToastButton "忽略" ({ Close-WithFade $win }.GetNewClosure()) $false $fgBrush $bgBrush
209
+ $btnRow.Children.Add($ignoreBtn) | Out-Null
210
+
211
+ if (-not [string]::IsNullOrEmpty([string]$cmd.sessionId)) {
212
+ # 跳转会始终打开浏览器(功能恒开启):用指定浏览器(空 = 系统默认)打开 DSH 并定位会话。
213
+ $jumpBtn = New-ToastButton "跳转会话" ({
214
+ $url = "$($cmd.baseUrl)/#dsh-notify/session=$($cmd.sessionId)"
215
+ try {
216
+ if (-not [string]::IsNullOrEmpty([string]$cmd.browserPath)) {
217
+ Start-Process -FilePath ([string]$cmd.browserPath) -ArgumentList $url
218
+ } else {
219
+ Start-Process $url
220
+ }
221
+ } catch { }
222
+ Close-WithFade $win
223
+ }.GetNewClosure()) $true $fgBrush $bgBrush
224
+ $btnRow.Children.Add($jumpBtn) | Out-Null
225
+ }
226
+ $root.Children.Add($btnRow) | Out-Null
227
+
228
+ $win.Content = $border
229
+
230
+ # 右下角定位 + 滑入动画 + 淡入 + pos 回传(单 Loaded 处理器)。
231
+ # 关键顺序:先把窗口放到"起始位置"(目标下方 32px)且透明度 0,
232
+ # 再启动动画滑向目标并淡入——窗口从起始位置可见地滑入,而不是瞬间显示在终点。
233
+ $win.Add_Loaded({
234
+ $work = [System.Windows.SystemParameters]::WorkArea
235
+ $targetLeft = $work.Right - $win.ActualWidth - 20
236
+ $targetTop = $work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
237
+ $win.Left = $targetLeft
238
+ $win.Top = $targetTop + 32
239
+ $win.Opacity = 0
240
+ try {
241
+ # 滑入 + 淡入(QuadraticEase EaseOut,260ms 快出缓停;淡入 180ms 先于位移结束)。
242
+ $easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
243
+ $easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
244
+ $slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 32, $targetTop, [TimeSpan]::FromMilliseconds(260))
245
+ $slideIn.EasingFunction = $easingIn
246
+ $win.BeginAnimation([System.Windows.Window]::TopProperty, $slideIn)
247
+ $fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(180))
248
+ $fadeIn.EasingFunction = $easingIn
249
+ $win.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeIn)
250
+ # 保险:400ms 后若动画异常导致不可见/未到位,强制恢复(窗口绝不能隐形挂起)。
251
+ $guard = New-Object System.Windows.Threading.DispatcherTimer
252
+ $guard.Interval = [TimeSpan]::FromMilliseconds(400)
253
+ $guard.Tag = @{ w = $win; top = $targetTop }
254
+ $guard.Add_Tick({
255
+ $this.Stop()
256
+ $g = $this.Tag
257
+ try {
258
+ if ($g.w.Opacity -lt 0.9) { $g.w.Opacity = 1 }
259
+ if ($g.w.Top -ne $g.top) { $g.w.Top = $g.top }
260
+ } catch { }
261
+ }) | Out-Null
262
+ $guard.Start()
263
+ } catch {
264
+ $win.Opacity = 1
265
+ $win.Top = $targetTop
266
+ }
267
+ # 位置与句柄回传(workBottom 为准确屏幕底部基准,宿主用它做自适应堆叠)。
268
+ try {
269
+ $hwnd = [System.Windows.Interop.WindowInteropHelper]::new($win).Handle
270
+ Send-Report @{
271
+ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
272
+ left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
273
+ height = [Math]::Round($win.ActualHeight); hwnd = $hwnd.ToInt64()
274
+ workBottom = [Math]::Round($work.Bottom)
275
+ }
276
+ } catch { }
277
+ }.GetNewClosure()) | Out-Null
278
+
279
+ # 关闭(任何途径)即注销并上报 exit(宿主侧唯一出口;宿主据此重排上方浮窗)。
280
+ # 注意:GetNewClosure 闭包里 $script: 解析不到原脚本作用域(为 $null),必须经
281
+ # 闭包捕获的函数局部引用访问注册表($toastsRef)。
282
+ $toastsRef = $script:toasts
283
+ $win.Add_Closed({
284
+ if ($toastsRef.ContainsKey([string]$cmd.key)) {
285
+ $rec = $toastsRef[[string]$cmd.key]
286
+ if ($rec.Timer -ne $null) { try { $rec.Timer.Stop() } catch { } }
287
+ $toastsRef.Remove([string]$cmd.key) | Out-Null
288
+ }
289
+ Send-Report @{ type = 'exit'; key = [string]$cmd.key; instance = [int]$cmd.instance }
290
+ }.GetNewClosure()) | Out-Null
291
+
292
+ # 注册
293
+ $rec = @{ Win = $win; Body = $bodyText; Timer = $null; Instance = [int]$cmd.instance }
294
+ $script:toasts[[string]$cmd.key] = $rec
295
+
296
+ # 非 sticky:定时自动消失(先淡出再关闭)。
297
+ if (-not ($cmd.sticky -eq $true)) {
298
+ $dur = [Math]::Max(1, [int]$cmd.durationSec)
299
+ $timer = New-Object System.Windows.Threading.DispatcherTimer
300
+ $timer.Interval = [TimeSpan]::FromSeconds($dur)
301
+ $timer.Add_Tick({ $this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
302
+ $timer.Start()
303
+ $rec.Timer = $timer
304
+ }
305
+
306
+ return $win
307
+ }
308
+
309
+ # ── 命令处理(WPF 主线程 Dispatcher 上执行,窗口操作线程安全) ───────────────
310
+ function Invoke-Command([string]$line) {
311
+ $line = $line.Trim()
312
+ if ([string]::IsNullOrEmpty($line)) { return }
313
+ $cmd = $null
314
+ try { $cmd = $line | ConvertFrom-Json } catch { return }
315
+ if ($cmd -eq $null -or [string]::IsNullOrEmpty([string]$cmd.cmd)) { return }
316
+ switch ([string]$cmd.cmd) {
317
+ 'show' {
318
+ # 防御:同 key 已存在(极端时序,如旧窗淡出中又来新 show)→ 立即关闭旧窗再建新窗。
319
+ if ($script:toasts.ContainsKey([string]$cmd.key)) {
320
+ $old = $script:toasts[[string]$cmd.key]
321
+ try { $old.Win.Close() } catch { }
322
+ $script:toasts.Remove([string]$cmd.key) | Out-Null
323
+ }
324
+ $w = New-ToastWindow $cmd
325
+ if ($w -ne $null) { $w.Show() }
326
+ }
327
+ 'text' {
328
+ $rec = $script:toasts[[string]$cmd.key]
329
+ if ($rec -ne $null -and $rec.Body -ne $null) { $rec.Body.Text = [string]$cmd.text }
330
+ }
331
+ 'move' {
332
+ $rec = $script:toasts[[string]$cmd.key]
333
+ if ($rec -ne $null) {
334
+ $top = [int]$cmd.top
335
+ $slide = New-Object System.Windows.Media.Animation.DoubleAnimation($rec.Win.Top, $top, [TimeSpan]::FromMilliseconds(220))
336
+ $slide.EasingFunction = $easeMove
337
+ $rec.Win.BeginAnimation([System.Windows.Window]::TopProperty, $slide)
338
+ }
339
+ }
340
+ 'close' {
341
+ $rec = $script:toasts[[string]$cmd.key]
342
+ if ($rec -ne $null) { Close-WithFade $rec.Win }
343
+ }
344
+ 'shutdown' { $script:exitRequested = $true }
345
+ }
346
+ }
347
+
348
+ # ── stdin 读取:独立 runspace 线程(阻塞 ReadLine 不卡 WPF 主线程) ───────────
349
+ $script:cmdQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[string]'
350
+ $readerScript = {
351
+ param($q)
352
+ try {
353
+ $reader = New-Object System.IO.StreamReader([System.Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding($false)))
354
+ while ($true) {
355
+ $line = $reader.ReadLine()
356
+ if ($null -eq $line) { break }
357
+ $q.Enqueue($line)
358
+ }
359
+ } catch { }
360
+ # stdin 关闭 → 通知主线程优雅退出。
361
+ $q.Enqueue('{"cmd":"shutdown"}')
362
+ }
363
+ $readerRs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
364
+ $readerRs.Open()
365
+ $readerPs = [System.Management.Automation.PowerShell]::Create()
366
+ $readerPs.Runspace = $readerRs
367
+ $readerPs.AddScript($readerScript).AddArgument($script:cmdQueue) | Out-Null
368
+ $readerPs.BeginInvoke() | Out-Null
369
+
370
+ # ── 主循环:50ms 排空命令队列;收到 shutdown 后关闭全部窗口并退出 ─────────────
371
+ $cmdTimer = New-Object System.Windows.Threading.DispatcherTimer
372
+ $cmdTimer.Interval = [TimeSpan]::FromMilliseconds(50)
373
+ $cmdTimer.Add_Tick({
374
+ try {
375
+ $line = $null
376
+ while ($script:cmdQueue.TryDequeue([ref]$line)) {
377
+ if (-not [string]::IsNullOrEmpty($line)) { Invoke-Command $line }
378
+ if ($script:exitRequested) { break }
379
+ }
380
+ if ($script:exitRequested) {
381
+ $this.Stop()
382
+ foreach ($key in @($script:toasts.Keys)) {
383
+ try { $script:toasts[$key].Win.Close() } catch { }
384
+ }
385
+ [System.Windows.Threading.Dispatcher]::CurrentDispatcher.InvokeShutdown()
386
+ }
387
+ } catch { }
388
+ }) | Out-Null
389
+ $cmdTimer.Start()
390
+
391
+ [System.Windows.Threading.Dispatcher]::Run()
392
+ try { $readerPs.Dispose() } catch { }
393
+ try { $readerRs.Dispose() } catch { }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * dsh-notify 的 Host 面 Typert 清单(由 typert-loader 自动扫描注册)。
3
+ * 手写清单,结构与 @deepseek-ai/dsh-typert-generator 产物一致:
4
+ * `./typert` 导出 TYPERT,invocations 的 codec 必须是 zod v4 实例。
5
+ */
6
+
7
+ import { z } from 'zod'
8
+
9
+ const kindColorSchema = z.object({
10
+ bg: z.string(),
11
+ fg: z.string(),
12
+ })
13
+
14
+ const configSchema = z.object({
15
+ enabled: z.boolean(),
16
+ mode: z.enum(['system', 'custom', 'off']),
17
+ maxVisible: z.number(),
18
+ durationMs: z.number(),
19
+ browser: z.string(),
20
+ bgColor: z.string(),
21
+ textColor: z.string(),
22
+ colors: z.object({
23
+ completed: kindColorSchema,
24
+ error: kindColorSchema,
25
+ running: kindColorSchema,
26
+ approval: kindColorSchema,
27
+ question: kindColorSchema,
28
+ }),
29
+ })
30
+
31
+ const notifyEventSchema = z.object({
32
+ id: z.string(),
33
+ sessionId: z.string(),
34
+ title: z.string(),
35
+ kind: z.enum(['completed', 'error', 'running', 'approval', 'question']),
36
+ text: z.string(),
37
+ activity: z.string().optional(),
38
+ at: z.number(),
39
+ })
40
+
41
+ const getStateSchema = z.object({
42
+ config: configSchema,
43
+ events: z.array(notifyEventSchema),
44
+ })
45
+
46
+ const updateConfigSchema = z.object({
47
+ config: configSchema,
48
+ })
49
+
50
+ const ackSchema = z.object({
51
+ ok: z.boolean(),
52
+ })
53
+
54
+ const browserSchema = z.object({
55
+ id: z.string(),
56
+ name: z.string(),
57
+ path: z.string(),
58
+ })
59
+
60
+ const listBrowsersSchema = z.object({
61
+ browsers: z.array(browserSchema),
62
+ })
63
+
64
+ const patchSchema = z.record(z.string(), z.unknown())
65
+
66
+ const _state$codec = { mode: 'strict', typeSymbol: 'dsh-notify#NotifyState', schema: getStateSchema }
67
+ const _config$codec = { mode: 'strict', typeSymbol: 'dsh-notify#NotifyConfig', schema: configSchema }
68
+ const _patch$codec = { mode: 'strict', typeSymbol: 'dsh-notify#NotifyConfigPatch', schema: patchSchema }
69
+ const _ack$codec = { mode: 'strict', typeSymbol: 'dsh-notify#AckResult', schema: ackSchema }
70
+ const _browsers$codec = { mode: 'strict', typeSymbol: 'dsh-notify#BrowserList', schema: listBrowsersSchema }
71
+
72
+ export const TYPERT = {
73
+ package: 'dsh-notify',
74
+ face: 'host',
75
+ schemas: [],
76
+ invocations: [
77
+ {
78
+ id: 'dsh-notify#notify/getState',
79
+ service: 'notify',
80
+ namespace: 'notify',
81
+ method: 'getState',
82
+ invocation: { kind: 'direct' },
83
+ parameters: [],
84
+ result: _state$codec,
85
+ },
86
+ {
87
+ id: 'dsh-notify#notify/updateConfig',
88
+ service: 'notify',
89
+ namespace: 'notify',
90
+ method: 'updateConfig',
91
+ invocation: { kind: 'direct' },
92
+ parameters: [
93
+ { name: 'patch', wire: 'patch', source: 'json', codec: _patch$codec },
94
+ ],
95
+ result: _config$codec,
96
+ },
97
+ {
98
+ id: 'dsh-notify#notify/ackEvents',
99
+ service: 'notify',
100
+ namespace: 'notify',
101
+ method: 'ackEvents',
102
+ invocation: { kind: 'direct' },
103
+ parameters: [
104
+ { name: 'ids', wire: 'ids', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-notify#EventIds', schema: z.array(z.string()) } },
105
+ ],
106
+ result: _ack$codec,
107
+ },
108
+ {
109
+ id: 'dsh-notify#notify/listBrowsers',
110
+ service: 'notify',
111
+ namespace: 'notify',
112
+ method: 'listBrowsers',
113
+ invocation: { kind: 'direct' },
114
+ parameters: [],
115
+ result: _browsers$codec,
116
+ },
117
+ ],
118
+ model: {
119
+ services: [
120
+ {
121
+ description: 'dsh-notify 通知服务(ctx.notify):插件配置与通知事件队列。Notification config and event queue service (ctx.notify).',
122
+ summary: 'dsh-notify 通知配置与事件队列服务 (dsh-notify notification config & event queue service)。',
123
+ tags: [],
124
+ jsDoc: '/** dsh-notify 通知配置与事件队列服务(ctx.notify)。dsh-notify notification config & event queue service (ctx.notify). */',
125
+ key: 'notify',
126
+ exportName: 'NotifyService',
127
+ members: [
128
+ {
129
+ kind: 'method',
130
+ name: 'getState',
131
+ signature: 'getState(): NotifyState',
132
+ summary: '读取当前配置与全部未确认通知事件。Read the current config and all unacknowledged notification events.',
133
+ jsDoc: '/**\n * 读取当前配置与全部未确认通知事件。\n * @returns 配置与未确认事件列表。\n * Read the current config and all unacknowledged notification events.\n * @returns The config and unacknowledged events.\n */',
134
+ },
135
+ {
136
+ kind: 'method',
137
+ name: 'updateConfig',
138
+ signature: 'updateConfig(patch: NotifyConfigPatch): NotifyConfig',
139
+ summary: '深合并一份配置补丁并持久化。Deep-merge a config patch and persist it.',
140
+ jsDoc: '/**\n * 深合并一份配置补丁并持久化。\n * @param patch - 配置补丁。\n * @returns 更新后的配置。\n * Deep-merge a config patch and persist it.\n * @param patch - The config patch.\n * @returns The updated config.\n */',
141
+ },
142
+ {
143
+ kind: 'method',
144
+ name: 'ackEvents',
145
+ signature: 'ackEvents(ids: string[]): AckResult',
146
+ summary: '回执已消费的事件 id,防止重复提醒。Acknowledge consumed event ids to prevent duplicate alerts.',
147
+ jsDoc: '/**\n * 回执已消费的事件 id,从队列移除。\n * @param ids - 已展示的事件 id 列表。\n * @returns 回执结果。\n * Acknowledge consumed event ids, removing them from the queue.\n * @param ids - Ids of events already shown.\n * @returns The acknowledgement result.\n */',
148
+ },
149
+ ],
150
+ types: [],
151
+ },
152
+ ],
153
+ events: [],
154
+ objects: [],
155
+ },
156
+ }
157
+
158
+ export default TYPERT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "dsh-notify-yimit",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness 通知插件:任务完成 / 任务出错 / 运行中(实时活动)/ 等待审批 / 等待回答 时,通过 Windows 系统通知或插件自定义样式通知提醒用户,支持点击跳转到对应会话;设置页集成在 DSH 设置界面。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./lib/index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./typert": {
15
+ "default": "./lib/typert.host.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "dsh": {
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ },
23
+ "client": {
24
+ "platform": "web"
25
+ }
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/typert.host.js",
30
+ "lib/client.js",
31
+ "lib/toast-host.ps1",
32
+ "cordis.patch.yml",
33
+ "README.md",
34
+ "README.en.md"
35
+ ],
36
+ "dependencies": {
37
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
38
+ "zod": "^4.4.3"
39
+ },
40
+ "keywords": [
41
+ "deepseek",
42
+ "deepseek-harness",
43
+ "dsh",
44
+ "dsh-plugin",
45
+ "client-plugin",
46
+ "notify",
47
+ "notification",
48
+ "toast"
49
+ ],
50
+ "license": "MIT"
51
+ }