dsh-notify-yimit 0.2.1 → 0.2.3
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.en.md +4 -3
- package/README.md +12 -2
- package/lib/client.js +146 -0
- package/lib/index.js +225 -32
- package/lib/toast-host.ps1 +487 -16
- package/lib/typert.host.js +28 -0
- package/package.json +4 -2
- package/scripts/check-prepublish.mjs +60 -0
package/lib/toast-host.ps1
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
<# dsh-notify-yimit 常驻浮窗宿主(Windows PowerShell + WPF)。
|
|
1
|
+
<# dsh-notify-yimit 常驻浮窗宿主(Windows PowerShell + WPF)。
|
|
2
|
+
|
|
3
|
+
★★★ 编码要求:本文件必须保存为「UTF-8 with BOM」(文件头 EF BB BF)。★★★
|
|
4
|
+
本文件含大量中文,而宿主是用 Windows PowerShell 5.1(`powershell.exe`)拉起的,
|
|
5
|
+
PS 5.1 对**没有 BOM** 的 .ps1 会按**系统 ANSI 代码页**解码。在中文 Windows
|
|
6
|
+
(ACP=936/GBK,以及 932/950)下,中文字节会被错误解码、吃掉字符串收尾引号,
|
|
7
|
+
导致整段脚本语法错误 —— 宿主进程秒退,**所有自定义通知全部不显示,且没有任何报错**。
|
|
8
|
+
表现特征:报错行往往是纯 ASCII 行(例如 `$q.Enqueue('{"cmd":"shutdown"}')`),
|
|
9
|
+
真正的病灶在它上面的中文行。
|
|
10
|
+
注意:ACP=65001(UTF-8)的机器上不会复现,PS 7(pwsh)也不会复现 —— 所以本地验证
|
|
11
|
+
必须按 936 解码后再交给解析器,或直接用带 BOM 的文件。
|
|
12
|
+
凡是重写/另存本文件后,请重新确认 BOM 仍在(编辑器与多数脚本化改写都会丢掉它)。
|
|
2
13
|
|
|
3
14
|
倒计时即进度(本版核心变化):
|
|
4
15
|
- 非粘性通知整卡抽象为倒计时:强调色 @12% "液面层"锚定左缘,ScaleX 1→0,
|
|
@@ -59,23 +70,169 @@ function Add-GridRow($grid) {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
# 编辑模式几何回传:样板窗当前 left/top/width/height(工作区相对坐标)。
|
|
73
|
+
# 样板窗额外回传 baselineTop = 基准线(卡片可见底边)在窗口内的 Y 偏移,
|
|
74
|
+
# 供前端把它换算成屏幕坐标存进配置。
|
|
62
75
|
function Send-Geometry($w) {
|
|
63
76
|
try {
|
|
64
77
|
$work = [System.Windows.SystemParameters]::WorkArea
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
top = [Math]::Round($w.Top - $work.Top)
|
|
69
|
-
width = [Math]::Round($w.ActualWidth)
|
|
70
|
-
height = [Math]::Round($w.ActualHeight)
|
|
78
|
+
$lineInWin = 0.0
|
|
79
|
+
if ($w.Tag -is [hashtable] -and $null -ne $w.Tag.lineInWin) {
|
|
80
|
+
$lineInWin = [double]$w.Tag.lineInWin
|
|
71
81
|
}
|
|
82
|
+
# ── 坐标口径必须与"重开时的定位口径"完全一致 ──
|
|
83
|
+
# 定位时:$win.Top = $pos.top − lineInWin(即 $pos.top 是基准线的屏幕 Y)。
|
|
84
|
+
# 因此这里回传的 top 也必须是**基准线的屏幕 Y**,而不是窗口顶边:
|
|
85
|
+
# top = 窗口顶边 + lineInWin
|
|
86
|
+
# 若回传窗口顶边,index.js 再把它当基准线存下来,就会出现
|
|
87
|
+
# "每进一次编辑模式基准线就往下漂一个带高"的问题(实测复现过)。
|
|
88
|
+
# baselineTop 仍回传"基准线从窗口顶边算起的偏移",供前端换算/调试。
|
|
89
|
+
$msg = @{
|
|
90
|
+
type = 'geometry'
|
|
91
|
+
left = [Math]::Round($w.Left - $work.Left)
|
|
92
|
+
top = [Math]::Round($w.Top - $work.Top + $lineInWin)
|
|
93
|
+
width = [Math]::Round($w.ActualWidth)
|
|
94
|
+
height = [Math]::Round($w.ActualHeight)
|
|
95
|
+
baselineTop = [Math]::Round($lineInWin)
|
|
96
|
+
}
|
|
97
|
+
Send-Report $msg
|
|
72
98
|
} catch { }
|
|
73
99
|
}
|
|
74
100
|
|
|
75
101
|
# ───────────────────────── 编辑模式样板窗 ─────────────────────────
|
|
76
102
|
|
|
103
|
+
# 基准线指示带固定高度(px):横线(2) + 3px 间距 + 箭头(19) + 2px 余量。
|
|
104
|
+
# 固定值而非 Auto:基准线到窗口底边的距离必须可预测,否则回传的基准线位置
|
|
105
|
+
# 会随字体度量浮动、与实际画的那条线对不上(编辑模式靠它反推窗口落点)。
|
|
106
|
+
$script:ANCHOR_BAND_H = 26
|
|
107
|
+
|
|
108
|
+
# 通知窗口内"可见卡片底边"到**窗口底边**的距离(px)。
|
|
109
|
+
# 实测地推:卡片底边 = 窗口顶边 + (窗口高 − 12);该 12 即 $border.Margin 的辉光边距。
|
|
110
|
+
# 注意它是"到窗口底边"的距离,不是"到窗口顶边"的偏移 —— 因此底部锚定的抬升量
|
|
111
|
+
# 必须写成 (窗口高 − TOAST_BOTTOM_GAP),随卡片高度自适应,绝不能写成固定值:
|
|
112
|
+
# 窗口顶边 = 锚点Y − (窗口高 − TOAST_BOTTOM_GAP) − offsetY
|
|
113
|
+
# 这样"卡片可见底边 = 锚点Y − offsetY",内容多长底边都钉在锚点上。
|
|
114
|
+
$script:TOAST_BOTTOM_GAP = 12
|
|
115
|
+
|
|
116
|
+
# 箭头用矢量多边形而非字体字形绘制:字形自带行距死区(上/下各数 px),
|
|
117
|
+
# 会把箭头推离横线、甚至溢出窗口被裁切;矢量图形能精确对齐到横线。
|
|
118
|
+
$script:ANCHOR_ARROW_W = 14
|
|
119
|
+
$script:ANCHOR_ARROW_H = 19
|
|
120
|
+
$script:ANCHOR_ARROW_GAP = 3
|
|
121
|
+
|
|
122
|
+
# 构造向上的空心箭头(chevron)多边形:顶点在正上方,两翼向下张开。
|
|
123
|
+
# 返回 PointCollection,坐标系原点在箭头外接框左上角。
|
|
124
|
+
function New-AnchorArrowPoints([double]$w, [double]$h) {
|
|
125
|
+
$hw = $w / 2.0
|
|
126
|
+
$th = [Math]::Max(2.0, $h * 0.45) # 两翼的竖直厚度
|
|
127
|
+
$tip = $hw * 0.62 # 翼尖相对中轴的横向偏移
|
|
128
|
+
$notch = $h * 0.42 # 内凹点深度(形成空心)
|
|
129
|
+
# 注意:PowerShell 中 `-` 与 `*` 的优先级容易误读,复合运算一律加括号。
|
|
130
|
+
$pts = New-Object System.Windows.Media.PointCollection
|
|
131
|
+
$pts.Add((New-Object System.Windows.Point($hw, 0))) # 顶点(贴住横线)
|
|
132
|
+
$pts.Add((New-Object System.Windows.Point($w, $th))) # 右翼外侧
|
|
133
|
+
$pts.Add((New-Object System.Windows.Point(($w - ($hw - $tip)), ($th + $tip)))) # 右翼尖端
|
|
134
|
+
$pts.Add((New-Object System.Windows.Point($hw, $notch))) # 内凹点
|
|
135
|
+
$pts.Add((New-Object System.Windows.Point(($hw - $tip), ($th + $tip)))) # 左翼尖端
|
|
136
|
+
$pts.Add((New-Object System.Windows.Point(0, $th))) # 左翼外侧
|
|
137
|
+
# 逗号运算符:PointCollection 实现了 IEnumerable,直接 return 会被 PowerShell
|
|
138
|
+
# 展开成 Object[],导致赋值给 Polygon.Points 失败。
|
|
139
|
+
return , $pts
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# 基准线指示带(仅编辑模式):一条横线 + 居中 ↑,紧贴在卡片"可见底边"正下方,
|
|
143
|
+
# 用来告诉用户"通知以该位置为基准,自定义通知均在此线上方"。
|
|
144
|
+
#
|
|
145
|
+
# 几何约定(方案 A,务必与 New-EditSample 的边距算式保持一致):
|
|
146
|
+
# · 横线位于网格第 0 行,即带的顶边;↑ 叠放在同一格内(线画在箭头之上)。
|
|
147
|
+
# · 带高固定(常量 $script:ANCHOR_BAND_H):固定值而非 Auto,基准线到窗口底边
|
|
148
|
+
# 的距离才可预测,回传的基准线位置才不会与实际画的线不一致。
|
|
149
|
+
function New-AnchorBand([System.Windows.Media.Color]$accent, [string]$label, [double]$maxLabelWidth) {
|
|
150
|
+
$grid = New-Object System.Windows.Controls.Grid
|
|
151
|
+
$grid.Height = $script:ANCHOR_BAND_H
|
|
152
|
+
$grid.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
|
153
|
+
# 必须 Top:若用 Center/Stretch 居中,线会被推到格子中间,离卡片底边多出一段距离。
|
|
154
|
+
$grid.VerticalAlignment = [System.Windows.VerticalAlignment]::Top
|
|
155
|
+
|
|
156
|
+
# ① 横线:强调色实线 + 辉光,两端小圆点形成"标尺"感
|
|
157
|
+
$line = New-Object System.Windows.Controls.Grid
|
|
158
|
+
$line.Height = 2
|
|
159
|
+
$line.VerticalAlignment = [System.Windows.VerticalAlignment]::Top
|
|
160
|
+
$line.Background = New-Object System.Windows.Media.SolidColorBrush($accent)
|
|
161
|
+
$line.Effect = New-GlowEffect $accent 0.55
|
|
162
|
+
foreach ($side in @('Left', 'Right')) {
|
|
163
|
+
$dot = New-Object System.Windows.Shapes.Ellipse
|
|
164
|
+
$dot.Width = 6
|
|
165
|
+
$dot.Height = 6
|
|
166
|
+
$dot.Fill = New-Object System.Windows.Media.SolidColorBrush($accent)
|
|
167
|
+
# 竖直居中于 2px 线上,视觉上成为线段两端的端点。
|
|
168
|
+
# 注意:枚举成员不能用 ::$side 这种写法展开(PowerShell 会当成属性访问而报错),
|
|
169
|
+
# 必须显式分支赋值。
|
|
170
|
+
if ($side -eq 'Left') {
|
|
171
|
+
$dot.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
|
|
172
|
+
} else {
|
|
173
|
+
$dot.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
|
|
174
|
+
}
|
|
175
|
+
$dot.Margin = New-Object System.Windows.Thickness(0, -2, 0, -2)
|
|
176
|
+
$line.Children.Add($dot) | Out-Null
|
|
177
|
+
}
|
|
178
|
+
$grid.Children.Add($line) | Out-Null
|
|
179
|
+
|
|
180
|
+
# ② ↑ + 可选文字:叠放在线之下(同一格)。
|
|
181
|
+
# 三列等分(各 1*):中列 = ↑,天然落在整条带的正中点;右列 = 文字。
|
|
182
|
+
# 这样"文字不会把 ↑ 推偏",且文字过宽时由自身的 TextTrimming 处理。
|
|
183
|
+
$cap = New-Object System.Windows.Controls.Grid
|
|
184
|
+
$cap.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
|
185
|
+
$cap.VerticalAlignment = [System.Windows.VerticalAlignment]::Top
|
|
186
|
+
$cap.Margin = New-Object System.Windows.Thickness(0, 3, 0, 0)
|
|
187
|
+
foreach ($i in 1..3) {
|
|
188
|
+
$col = New-Object System.Windows.Controls.ColumnDefinition
|
|
189
|
+
$col.Width = New-Object System.Windows.GridLength(1, [System.Windows.GridUnitType]::Star)
|
|
190
|
+
$cap.ColumnDefinitions.Add($col) | Out-Null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
$arrow = New-Object System.Windows.Shapes.Polygon
|
|
194
|
+
$arrow.Points = New-AnchorArrowPoints $script:ANCHOR_ARROW_W $script:ANCHOR_ARROW_H
|
|
195
|
+
$arrow.Fill = New-Object System.Windows.Media.SolidColorBrush($accent)
|
|
196
|
+
$arrow.Width = $script:ANCHOR_ARROW_W
|
|
197
|
+
$arrow.Height = $script:ANCHOR_ARROW_H
|
|
198
|
+
$arrow.Stretch = [System.Windows.Media.Stretch]::Fill
|
|
199
|
+
$arrow.Effect = New-GlowEffect $accent 0.45
|
|
200
|
+
# 顶点贴住横线,只留 $script:ANCHOR_ARROW_GAP 的呼吸量(矢量图形无字形死区)。
|
|
201
|
+
$arrow.Margin = New-Object System.Windows.Thickness(0, $script:ANCHOR_ARROW_GAP, 0, 0)
|
|
202
|
+
$arrow.VerticalAlignment = [System.Windows.VerticalAlignment]::Top
|
|
203
|
+
# 用一个撑满中列的容器来居中箭头:不依赖"列宽 = 箭头宽"这一巧合,
|
|
204
|
+
# 否则同列的文字一变宽,箭头就会被带偏。
|
|
205
|
+
$arrowBox = New-Object System.Windows.Controls.Border
|
|
206
|
+
$arrowBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
|
207
|
+
$arrowBox.VerticalAlignment = [System.Windows.VerticalAlignment]::Top
|
|
208
|
+
$arrowBox.Child = $arrow
|
|
209
|
+
[System.Windows.Controls.Grid]::SetColumn($arrowBox, 1)
|
|
210
|
+
$cap.Children.Add($arrowBox) | Out-Null
|
|
211
|
+
|
|
212
|
+
if (-not [string]::IsNullOrEmpty($label)) {
|
|
213
|
+
$txt = New-Object System.Windows.Controls.TextBlock
|
|
214
|
+
$txt.Text = $label
|
|
215
|
+
$txt.Foreground = New-Object System.Windows.Media.SolidColorBrush($accent)
|
|
216
|
+
$txt.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
|
|
217
|
+
$txt.FontSize = 11
|
|
218
|
+
$txt.Opacity = 0.92
|
|
219
|
+
$txt.VerticalAlignment = [System.Windows.VerticalAlignment]::Top
|
|
220
|
+
$txt.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
|
|
221
|
+
$txt.Margin = New-Object System.Windows.Thickness(10, 1, 0, 0)
|
|
222
|
+
# 限宽 + 省略号:文案过长时按字符截断,不挤压 ↑、也不超出窗口。
|
|
223
|
+
$txt.TextTrimming = [System.Windows.TextTrimming]::CharacterEllipsis
|
|
224
|
+
$txt.MaxWidth = [Math]::Max(80, $maxLabelWidth)
|
|
225
|
+
[System.Windows.Controls.Grid]::SetColumn($txt, 2)
|
|
226
|
+
$cap.Children.Add($txt) | Out-Null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$grid.Children.Add($cap) | Out-Null
|
|
230
|
+
return $grid
|
|
231
|
+
}
|
|
232
|
+
|
|
77
233
|
# 编辑模式样板窗:拖拽移动 + 左右边缘圆点把手缩放;拖拽/缩放结束后回传 geometry。
|
|
78
|
-
# 强调色描边 + 辉光;内嵌 5 类型实时色卡预览(chips)
|
|
234
|
+
# 强调色描边 + 辉光;内嵌 5 类型实时色卡预览(chips);多语言提示;
|
|
235
|
+
# 卡片下方带"基准线 + ↑"指示带(方案 A)。
|
|
79
236
|
function New-EditSample($cmd) {
|
|
80
237
|
$bg = Parse-Color ([string]$cmd.bg) "#203a5c"
|
|
81
238
|
$fg = Parse-Color ([string]$cmd.fg) "#e8f0fb"
|
|
@@ -84,6 +241,25 @@ function New-EditSample($cmd) {
|
|
|
84
241
|
$fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
|
|
85
242
|
$accentBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
|
|
86
243
|
|
|
244
|
+
# ── 几何总纲(务必与手柄、带行的算式保持一致) ──
|
|
245
|
+
# $glowMargin 同时是:左右辉光呼吸量、卡片左/右/上边距、左右缩放把手的边距。
|
|
246
|
+
# · 卡片:左/右边距相等 → 始终居中;卡宽 = 窗口宽 − 2×$glowMargin。
|
|
247
|
+
# · 手柄:左右边距同样为 $glowMargin(宽 8px)→ 左右手柄"内沿"之间
|
|
248
|
+
# = 窗口宽 − 2×$glowMargin = 卡宽,即手柄与卡片严格等宽、等左右沿。
|
|
249
|
+
# · 下边距必须为 0:卡片因此一直铺到第 0 行的底边,而第 1 行(带行)紧随其后,
|
|
250
|
+
# 所以横线正好压在卡片可见底边上;卡片高度 = 行高 = 手柄高度。
|
|
251
|
+
# 若在这里留边距,卡片会在自己所在行内被顶上去,横线就会离开卡片底边。
|
|
252
|
+
# · 底部呼吸量由带行自身(透明)提供,无需再留边距。
|
|
253
|
+
$glowMargin = 8
|
|
254
|
+
$bandHeight = [double]$script:ANCHOR_BAND_H
|
|
255
|
+
$bottomMargin = 0
|
|
256
|
+
# 基准线(卡片可见底边)到**窗口底边**的距离 = 卡片下边距 + 带高。
|
|
257
|
+
# 实测:带(含自身下边距)紧贴第 0 行底部,故卡片可见底边 = 窗口高 − 该值,
|
|
258
|
+
# 也就是横线自身的 Y。这是"从底部算"的量。
|
|
259
|
+
# 注意:真正用于定位/回传的是"从窗口顶边算"的 lineInWin(在 Loaded 里实测),
|
|
260
|
+
# 两者基准不同,不可混用 —— 混用会导致每次重开编辑模式基准线漂移一个带高。
|
|
261
|
+
$baselineFromBottom = $bottomMargin + $bandHeight
|
|
262
|
+
|
|
87
263
|
$win = New-Object System.Windows.Window
|
|
88
264
|
$win.WindowStyle = [System.Windows.WindowStyle]::None
|
|
89
265
|
$win.AllowsTransparency = $true
|
|
@@ -99,7 +275,7 @@ function New-EditSample($cmd) {
|
|
|
99
275
|
$border.BorderBrush = $accentBrush
|
|
100
276
|
$border.BorderThickness = New-Object System.Windows.Thickness(1)
|
|
101
277
|
$border.CornerRadius = New-Object System.Windows.CornerRadius(14)
|
|
102
|
-
$border.Margin = New-Object System.Windows.Thickness(
|
|
278
|
+
$border.Margin = New-Object System.Windows.Thickness($glowMargin, $glowMargin, $glowMargin, $bottomMargin)
|
|
103
279
|
|
|
104
280
|
$root = New-Object System.Windows.Controls.StackPanel
|
|
105
281
|
$root.Margin = New-Object System.Windows.Thickness(16, 14, 16, 14)
|
|
@@ -175,6 +351,8 @@ function New-EditSample($cmd) {
|
|
|
175
351
|
$leftStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
|
|
176
352
|
$leftStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
|
|
177
353
|
$leftStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
|
|
354
|
+
# 把手竖直范围 = 卡片可见上下沿(下边距用带高,故把手不会压到基准线指示带上)。
|
|
355
|
+
$leftStrip.Margin = New-Object System.Windows.Thickness(0, $glowMargin, 0, $bottomMargin)
|
|
178
356
|
$leftStrip.Background = $edgeBrush
|
|
179
357
|
$leftStrip.CornerRadius = New-Object System.Windows.CornerRadius(14, 0, 0, 14)
|
|
180
358
|
$rightStrip = New-Object System.Windows.Controls.Border
|
|
@@ -182,6 +360,7 @@ function New-EditSample($cmd) {
|
|
|
182
360
|
$rightStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
|
|
183
361
|
$rightStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
|
|
184
362
|
$rightStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
|
|
363
|
+
$rightStrip.Margin = New-Object System.Windows.Thickness(0, $glowMargin, 0, $bottomMargin)
|
|
185
364
|
$rightStrip.Background = $edgeBrush
|
|
186
365
|
$rightStrip.CornerRadius = New-Object System.Windows.CornerRadius(0, 14, 14, 0)
|
|
187
366
|
|
|
@@ -206,24 +385,62 @@ function New-EditSample($cmd) {
|
|
|
206
385
|
Add-GripDots $leftStrip $fg
|
|
207
386
|
Add-GripDots $rightStrip $fg
|
|
208
387
|
|
|
209
|
-
# 外层 Grid:边框 + 左右缩放把手叠放(把手覆盖窗口左右边缘)
|
|
388
|
+
# 外层 Grid:边框 + 基准线指示带 + 左右缩放把手叠放(把手覆盖窗口左右边缘)
|
|
210
389
|
$outer = New-Object System.Windows.Controls.Grid
|
|
390
|
+
# 两条等分行:第 0 行卡片(占满剩余高度),第 1 行 = 基准线指示带。
|
|
391
|
+
$rowCard = New-Object System.Windows.Controls.RowDefinition
|
|
392
|
+
$rowCard.Height = New-Object System.Windows.GridLength(1, [System.Windows.GridUnitType]::Star)
|
|
393
|
+
$rowBand = New-Object System.Windows.Controls.RowDefinition
|
|
394
|
+
$rowBand.Height = [System.Windows.GridLength]::Auto
|
|
395
|
+
$outer.RowDefinitions.Add($rowCard) | Out-Null
|
|
396
|
+
$outer.RowDefinitions.Add($rowBand) | Out-Null
|
|
397
|
+
|
|
398
|
+
# 文案限宽:窗口宽度的一半多一点,过长按字符省略。
|
|
399
|
+
$band = New-AnchorBand $accent ([string]$cmd.anchorLabel) ([Math]::Max(80, $win.Width * 0.55))
|
|
400
|
+
[System.Windows.Controls.Grid]::SetRow($band, 1)
|
|
211
401
|
$outer.Children.Add($border) | Out-Null
|
|
402
|
+
$outer.Children.Add($band) | Out-Null
|
|
212
403
|
$outer.Children.Add($leftStrip) | Out-Null
|
|
213
404
|
$outer.Children.Add($rightStrip) | Out-Null
|
|
214
405
|
|
|
215
406
|
# 共享拖拽/缩放状态(事件回调看不到函数局部变量,放 Tag)
|
|
216
|
-
|
|
407
|
+
# lineInWin:基准线(卡片可见底边)从**窗口顶边**量起的 Y 偏移。
|
|
408
|
+
# 定位与回传都用它,保证"存下的坐标"与"重开时的落点"是同一个量。
|
|
409
|
+
$win.Tag = @{
|
|
410
|
+
resizing = $false
|
|
411
|
+
startX = 0.0
|
|
412
|
+
startWidth = 0.0
|
|
413
|
+
startLeft = 0.0
|
|
414
|
+
edge = 'right'
|
|
415
|
+
lineInWin = 0.0
|
|
416
|
+
}
|
|
217
417
|
|
|
218
418
|
$win.Add_Loaded({
|
|
219
419
|
$work = [System.Windows.SystemParameters]::WorkArea
|
|
220
420
|
$pos = $cmd.position
|
|
421
|
+
# ── 基准线自测(消除"从顶部算/从底部算"的混用) ──
|
|
422
|
+
# 先把窗口顶边临时归零,等布局落定后量出基准线在窗口内的真实 Y(从窗口顶边算起)。
|
|
423
|
+
# 之后定位与 Send-Geometry 回传都用这同一个量,于是"存下的坐标"与"重开的落点"
|
|
424
|
+
# 必然一致,不会再出现每进一次编辑模式就漂移一截的问题。
|
|
425
|
+
$win.Top = 0
|
|
426
|
+
$win.UpdateLayout()
|
|
427
|
+
$lineInWin = $border.TranslatePoint(
|
|
428
|
+
(New-Object System.Windows.Point(0, $border.ActualHeight)), $win).Y
|
|
429
|
+
if ($lineInWin -le 0) {
|
|
430
|
+
# 兜底:布局尚未就绪时用"窗口高 − 从底部算的距离"估算。
|
|
431
|
+
$lineInWin = $win.ActualHeight - $bottomMargin - $bandHeight
|
|
432
|
+
}
|
|
433
|
+
$win.Tag.lineInWin = $lineInWin
|
|
221
434
|
if ($null -ne $pos -and $null -ne $pos.left -and $null -ne $pos.top) {
|
|
222
435
|
$win.Left = [double]$pos.left + $work.Left
|
|
223
|
-
$win.Top = [double]$pos.top + $work.Top
|
|
436
|
+
$win.Top = [double]$pos.top + $work.Top - $lineInWin
|
|
224
437
|
} else {
|
|
225
|
-
|
|
226
|
-
|
|
438
|
+
# 编辑模式的样板窗贴住工作区右下角:窗口底边 = 工作区底边。
|
|
439
|
+
# (此处不加 20px 呼吸量 —— 用户明确要求样板窗能贴到屏幕最底部;
|
|
440
|
+
# 窗口底边即"基准线 + ↑"指示带所在行,基准线因此落在离屏幕底
|
|
441
|
+
# 一个带高的位置,通知向上堆叠时正好从屏幕底部往上排。)
|
|
442
|
+
$win.Left = $work.Right - $win.ActualWidth
|
|
443
|
+
$win.Top = $work.Bottom - $win.ActualHeight
|
|
227
444
|
}
|
|
228
445
|
Send-Geometry $win
|
|
229
446
|
}.GetNewClosure()) | Out-Null
|
|
@@ -313,6 +530,227 @@ $downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.C
|
|
|
313
530
|
$hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196, 202, 208))
|
|
314
531
|
$hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44, 52, 62))
|
|
315
532
|
|
|
533
|
+
# 「返回应用」:把已在运行的 DeepSeek Harness 桌面窗口切到前台。
|
|
534
|
+
#
|
|
535
|
+
# 为什么不能靠 dsh://open:应用只在 macOS 的 `open-url` 事件里处理 `dsh://open`
|
|
536
|
+
# (Windows 上协议 URL 是作为 argv 交给新进程的,而它的 `second-instance` 处理器
|
|
537
|
+
# 不读 argv、也不校验 URL),所以那条路在 Windows 上是死代码。
|
|
538
|
+
#
|
|
539
|
+
# 因此改为直接枚举窗口并激活。合法性:用户点按钮的那一刻,前台窗口就是这个 WPF
|
|
540
|
+
# 浮窗(属于本宿主进程),所以宿主调用 SetForegroundWindow 不会被
|
|
541
|
+
# "只有前台进程才能抢占前台"的系统限制挡住。
|
|
542
|
+
$script:ToastWin32Ready = $false
|
|
543
|
+
try {
|
|
544
|
+
if (-not ('ToastWin32' -as [type])) {
|
|
545
|
+
Add-Type -TypeDefinition @'
|
|
546
|
+
using System;
|
|
547
|
+
using System.Runtime.InteropServices;
|
|
548
|
+
|
|
549
|
+
public class ToastWin32 {
|
|
550
|
+
public delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
|
|
551
|
+
|
|
552
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
553
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
554
|
+
|
|
555
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr p);
|
|
556
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
|
557
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
558
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowTextLength(IntPtr h);
|
|
559
|
+
[DllImport("user32.dll")] public static extern IntPtr GetShellWindow();
|
|
560
|
+
[DllImport("user32.dll")] public static extern IntPtr GetWindow(IntPtr h, uint cmd);
|
|
561
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
562
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
|
563
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
564
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
565
|
+
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
|
566
|
+
[DllImport("user32.dll")] public static extern bool GetWindowPlacement(IntPtr h, ref WINDOWPLACEMENT wp);
|
|
567
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
|
568
|
+
[DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr h);
|
|
569
|
+
|
|
570
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
571
|
+
public struct POINT { public int X, Y; }
|
|
572
|
+
|
|
573
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
574
|
+
public struct WINDOWPLACEMENT {
|
|
575
|
+
public int length;
|
|
576
|
+
public int flags;
|
|
577
|
+
public int showCmd;
|
|
578
|
+
public POINT ptMinPosition;
|
|
579
|
+
public POINT ptMaxPosition;
|
|
580
|
+
public RECT rcNormalPosition;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private const uint GW_OWNER = 4;
|
|
584
|
+
private const int SW_SHOWNORMAL = 1;
|
|
585
|
+
private const int SW_SHOWMINIMIZED = 2;
|
|
586
|
+
private const int SW_SHOWMAXIMIZED = 3;
|
|
587
|
+
private const int SW_SHOW = 5;
|
|
588
|
+
private const int SW_RESTORE = 9;
|
|
589
|
+
/// WPF_RESTORETOMAXIMIZED:最小化前是最大化状态
|
|
590
|
+
private const int WPF_RESTORETOMAXIMIZED = 0x0002;
|
|
591
|
+
|
|
592
|
+
/// 找出 pid 名下最可能是"主窗口"的顶层窗口。
|
|
593
|
+
///
|
|
594
|
+
/// 实测要点(这台机器上枚举到 280 个窗口、其中 20 个可见):
|
|
595
|
+
/// 应用最小化到托盘 / 被隐藏时,它的主窗口会变成 **不可见**,甚至带上 owner
|
|
596
|
+
/// 关系。若像最初那样要求"可见 + 非 owner",这两条会把主窗口全部滤掉,
|
|
597
|
+
/// 结果就是"点了返回应用没反应"。因此这里分级挑选:
|
|
598
|
+
/// 1) 可见 + 有标题 + 非 owner(正常情况,最可靠)
|
|
599
|
+
/// 2) 有标题 + 非 owner(隐藏/托盘态)
|
|
600
|
+
/// 3) 有标题(连 owner 关系都能容忍)
|
|
601
|
+
/// 每级内部取面积最大者,Electron 的辅助窗口通常面积很小或没有标题。
|
|
602
|
+
public static IntPtr FindMainWindow(uint pid) {
|
|
603
|
+
IntPtr shell = GetShellWindow();
|
|
604
|
+
IntPtr[] best = new IntPtr[3];
|
|
605
|
+
long[] bestArea = new long[3] { -1, -1, -1 };
|
|
606
|
+
EnumWindows(delegate(IntPtr h, IntPtr p) {
|
|
607
|
+
uint wpid = 0;
|
|
608
|
+
GetWindowThreadProcessId(h, out wpid);
|
|
609
|
+
if (wpid != pid) return true;
|
|
610
|
+
if (h == shell) return true;
|
|
611
|
+
if (GetWindowTextLength(h) <= 0) return true;
|
|
612
|
+
|
|
613
|
+
bool visible = IsWindowVisible(h);
|
|
614
|
+
bool owned = GetWindow(h, GW_OWNER) != IntPtr.Zero;
|
|
615
|
+
RECT r;
|
|
616
|
+
if (!GetWindowRect(h, out r)) return true;
|
|
617
|
+
long area = (long)(r.Right - r.Left) * (long)(r.Bottom - r.Top);
|
|
618
|
+
|
|
619
|
+
int tier;
|
|
620
|
+
if (visible && !owned) tier = 0;
|
|
621
|
+
else if (!owned) tier = 1;
|
|
622
|
+
else tier = 2;
|
|
623
|
+
|
|
624
|
+
if (area > bestArea[tier]) { bestArea[tier] = area; best[tier] = h; }
|
|
625
|
+
return true;
|
|
626
|
+
}, IntPtr.Zero);
|
|
627
|
+
|
|
628
|
+
for (int i = 0; i < 3; i++) { if (best[i] != IntPtr.Zero) return best[i]; }
|
|
629
|
+
return IntPtr.Zero;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// 把 hWnd 提到前台,并**保持窗口原有的尺寸/最大化状态**。
|
|
633
|
+
///
|
|
634
|
+
/// 关键:`ShowWindow(h, SW_RESTORE)` 会把"最小化**或**最大化"的窗口还原成
|
|
635
|
+
/// 原始(默认)尺寸 —— 这正是"窗口本来最大化、只是不在最上层,点返回应用后
|
|
636
|
+
/// 变成默认大小"的原因。所以这里必须先查询显示状态:
|
|
637
|
+
/// · 最小化 → SW_RESTORE(恢复原尺寸,若原先最大化会恢复成最大化)
|
|
638
|
+
/// · 最大化 → SW_SHOWMAXIMIZED
|
|
639
|
+
/// · 普通 → SW_SHOW(只显示/置顶,**不改尺寸**)
|
|
640
|
+
/// SetForegroundWindow 受"仅前台进程可抢占前台"限制,失败时按通行做法
|
|
641
|
+
/// 附加到当前前台线程的输入队列后重试一次。
|
|
642
|
+
public static bool Activate(IntPtr h) {
|
|
643
|
+
if (h == IntPtr.Zero) return false;
|
|
644
|
+
|
|
645
|
+
WINDOWPLACEMENT wp = new WINDOWPLACEMENT();
|
|
646
|
+
wp.length = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
|
|
647
|
+
int show = SW_SHOW;
|
|
648
|
+
if (GetWindowPlacement(h, ref wp)) {
|
|
649
|
+
int cur = wp.showCmd;
|
|
650
|
+
if (cur == SW_SHOWMINIMIZED) {
|
|
651
|
+
show = (wp.flags & WPF_RESTORETOMAXIMIZED) != 0 ? SW_SHOWMAXIMIZED : SW_RESTORE;
|
|
652
|
+
} else if (cur == SW_SHOWMAXIMIZED) {
|
|
653
|
+
show = SW_SHOWMAXIMIZED;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
ShowWindow(h, show);
|
|
657
|
+
|
|
658
|
+
if (SetForegroundWindow(h)) return true;
|
|
659
|
+
IntPtr fg = GetForegroundWindow();
|
|
660
|
+
uint dummy = 0;
|
|
661
|
+
uint tidFg = fg == IntPtr.Zero ? 0u : GetWindowThreadProcessId(fg, out dummy);
|
|
662
|
+
uint tidMe = GetWindowThreadProcessId(GetShellWindow(), out dummy);
|
|
663
|
+
if (tidFg != 0u && tidFg != tidMe) {
|
|
664
|
+
if (AttachThreadInput(tidMe, tidFg, true)) {
|
|
665
|
+
bool ok = SetForegroundWindow(h);
|
|
666
|
+
AttachThreadInput(tidMe, tidFg, false);
|
|
667
|
+
if (ok) return true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return SetForegroundWindow(h);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
'@ -ErrorAction Stop
|
|
674
|
+
}
|
|
675
|
+
$script:ToastWin32Ready = $true
|
|
676
|
+
} catch {
|
|
677
|
+
# Win32 互操作不可用(极少见):activateLabel 会因此不下发,按钮不出现。
|
|
678
|
+
$script:ToastWin32Ready = $false
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function Write-ActLog([string]$msg, [switch]$Always) {
|
|
682
|
+
# 诊断日志:激活这一步涉及"按 exe 路径找进程 + 枚举窗口"两处环境相关行为,
|
|
683
|
+
# 出问题时按钮只会静默无反应,没有日志无法定位。
|
|
684
|
+
#
|
|
685
|
+
# 写入策略(避免往用户安装目录里持续写日志):
|
|
686
|
+
# · -Always:仅启动探针使用,每次宿主加载写一行(极小,且是诊断秒退的关键线索);
|
|
687
|
+
# · 其余(逐次点击的明细)只有设了 DSH_DEBUG 才写 —— 与插件其它调试输出的约定一致。
|
|
688
|
+
if (-not $Always -and [string]::IsNullOrEmpty($env:DSH_DEBUG)) { return }
|
|
689
|
+
# 落点优先级:脚本所在目录(真实宿主 = lib/)→ DSH_HOME。
|
|
690
|
+
try {
|
|
691
|
+
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg`r`n"
|
|
692
|
+
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
693
|
+
$dirs = @()
|
|
694
|
+
if (-not [string]::IsNullOrEmpty($PSScriptRoot)) { $dirs += $PSScriptRoot }
|
|
695
|
+
if (-not [string]::IsNullOrEmpty($env:DSH_HOME)) { $dirs += $env:DSH_HOME }
|
|
696
|
+
foreach ($d in $dirs) {
|
|
697
|
+
try {
|
|
698
|
+
[System.IO.File]::AppendAllText((Join-Path $d 'activate-debug.log'), $line, $enc)
|
|
699
|
+
return
|
|
700
|
+
} catch { }
|
|
701
|
+
}
|
|
702
|
+
} catch { }
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
# 启动探针:宿主每次加载(含插件重载)都记一行门控状态。
|
|
706
|
+
# 必须放在 Write-ActLog 定义之后,否则这个调用会因函数未定义而静默失效。
|
|
707
|
+
try {
|
|
708
|
+
$probe = "boot: Win32Ready=$script:ToastWin32Ready"
|
|
709
|
+
try { $probe += " selfExe='$((Get-Process -Id $PID).Path)'" } catch { $probe += " selfExe=<取不到>" }
|
|
710
|
+
try { $probe += " DSH_HOME='$env:DSH_HOME'" } catch { }
|
|
711
|
+
try { $probe += " PSScriptRoot='$PSScriptRoot'" } catch { }
|
|
712
|
+
Write-ActLog $probe -Always
|
|
713
|
+
} catch { }
|
|
714
|
+
|
|
715
|
+
function Invoke-ToastActivateApp([string]$appPath) {
|
|
716
|
+
try {
|
|
717
|
+
Write-ActLog "--- click ---"
|
|
718
|
+
Write-ActLog "Win32Ready=$script:ToastWin32Ready appPath='$appPath'"
|
|
719
|
+
if (-not $script:ToastWin32Ready) { Write-ActLog "中止: Win32 助手不可用"; return }
|
|
720
|
+
if ([string]::IsNullOrEmpty($appPath)) { Write-ActLog "中止: appPath 为空"; return }
|
|
721
|
+
$full = $null
|
|
722
|
+
try { $full = [System.IO.Path]::GetFullPath($appPath) } catch { Write-ActLog "中止: GetFullPath 失败"; return }
|
|
723
|
+
Write-ActLog "full='$full' exists=$(Test-Path -LiteralPath $full)"
|
|
724
|
+
if (-not (Test-Path -LiteralPath $full)) { Write-ActLog "中止: exe 路径不存在"; return }
|
|
725
|
+
|
|
726
|
+
# Electron 的主进程与各渲染进程都是同一个 exe,窗口可能挂在其中任意一个 PID 上,
|
|
727
|
+
# 所以把全部同 exe 的进程当作一组来挑窗口,而不是"谁先回答就用谁"。
|
|
728
|
+
$procs = @()
|
|
729
|
+
foreach ($p in (Get-Process -ErrorAction SilentlyContinue)) {
|
|
730
|
+
try { if ($p.Path -eq $full) { $procs += $p } } catch { }
|
|
731
|
+
}
|
|
732
|
+
Write-ActLog "按 exe 匹配=$($procs.Count) pids=$(($procs | ForEach-Object { $_.Id }) -join ',')"
|
|
733
|
+
if ($procs.Count -eq 0) {
|
|
734
|
+
# 兜底:按进程名匹配(某些情况下 Path 取不到)。
|
|
735
|
+
$nm = [System.IO.Path]::GetFileNameWithoutExtension($full)
|
|
736
|
+
$procs = @(Get-Process -Name $nm -ErrorAction SilentlyContinue)
|
|
737
|
+
Write-ActLog "按名 '$nm' 兜底=$($procs.Count) pids=$(($procs | ForEach-Object { $_.Id }) -join ',')"
|
|
738
|
+
}
|
|
739
|
+
foreach ($p in $procs) {
|
|
740
|
+
$h = [ToastWin32]::FindMainWindow([uint32]$p.Id)
|
|
741
|
+
Write-ActLog " pid=$($p.Id) hwnd=$h"
|
|
742
|
+
if ($h -ne [IntPtr]::Zero) {
|
|
743
|
+
$ok = [ToastWin32]::Activate($h)
|
|
744
|
+
Write-ActLog " Activate=$ok fg=$([ToastWin32]::GetForegroundWindow())"
|
|
745
|
+
if ($ok) { return }
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
Write-ActLog "结束: 未找到可激活窗口"
|
|
749
|
+
} catch {
|
|
750
|
+
Write-ActLog "异常: $($_.Exception.GetType().Name) :: $($_.Exception.Message)"
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
316
754
|
# 浮窗按钮:primary 时传入强调色画刷,hover/按压基于该色自动提亮/压暗;
|
|
317
755
|
# secondary 保持描边透明底样式。
|
|
318
756
|
function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary, [System.Windows.Media.SolidColorBrush]$fg, [System.Windows.Media.SolidColorBrush]$bg) {
|
|
@@ -500,6 +938,19 @@ function New-ToastWindow($cmd) {
|
|
|
500
938
|
$ignoreLabel = if ([string]::IsNullOrEmpty([string]$cmd.ignoreLabel)) { "Ignore" } else { [string]$cmd.ignoreLabel }
|
|
501
939
|
$ignoreBtn = New-ToastButton $ignoreLabel ({ Close-WithFade $win }.GetNewClosure()) $false $fgBrush $bgBrush
|
|
502
940
|
$btnRow.Children.Add($ignoreBtn) | Out-Null
|
|
941
|
+
# 「返回应用」:位于「忽略」右侧;仅桌面端出现(由 index.js 按 isDesktopRuntime 下发
|
|
942
|
+
# activateLabel)。点击后把应用窗口切到前台,但**不**关闭本条通知。
|
|
943
|
+
# 另外要求 Win32 互操作可用(否则无法激活窗口,宁可不显示这个按钮)。
|
|
944
|
+
$activateLabel = [string]$cmd.activateLabel
|
|
945
|
+
$appPath = [string]$cmd.appPath
|
|
946
|
+
if ($script:ToastWin32Ready -and -not [string]::IsNullOrEmpty($activateLabel) -and
|
|
947
|
+
-not [string]::IsNullOrEmpty($appPath)) {
|
|
948
|
+
$activateBtn = New-ToastButton $activateLabel ({
|
|
949
|
+
Invoke-ToastActivateApp $appPath
|
|
950
|
+
# 按要求保留通知,不做 Close-WithFade。
|
|
951
|
+
}.GetNewClosure()) $false $fgBrush $bgBrush
|
|
952
|
+
$btnRow.Children.Add($activateBtn) | Out-Null
|
|
953
|
+
}
|
|
503
954
|
# 缺省 true = 保留「跳转会话」;桌面端宿主会显式传 jumpEnabled=false 来隐藏它。
|
|
504
955
|
$jumpEnabled = $true
|
|
505
956
|
if ($cmd.PSObject.Properties.Name -contains 'jumpEnabled') { $jumpEnabled = [bool]$cmd.jumpEnabled }
|
|
@@ -555,12 +1006,32 @@ function New-ToastWindow($cmd) {
|
|
|
555
1006
|
$win.Add_Loaded({
|
|
556
1007
|
$work = [System.Windows.SystemParameters]::WorkArea
|
|
557
1008
|
$pos =$cmd.position
|
|
1009
|
+
# ── 底部锚定定位 ──
|
|
1010
|
+
# 目标:本条通知"可见卡片底边"落在锚点上(卡片多高都只影响顶边)。
|
|
1011
|
+
#
|
|
1012
|
+
# 抬升量由布局实测得出,而不是靠写死的常量:
|
|
1013
|
+
# $border 就是那张可见卡片,先把窗口顶边临时放到 0,量出它相对窗口的下边缘
|
|
1014
|
+
# ($borderBottomInWin),于是
|
|
1015
|
+
# 窗口顶边 = 锚点Y − $borderBottomInWin − offsetY
|
|
1016
|
+
# 其中 offsetY = 该条之上已叠的高度(首条为 0)。
|
|
1017
|
+
# 先前用固定常量推算,卡片高度随内容变化时底边会偏离锚点(实测可差 12px 以上),
|
|
1018
|
+
# 所以这里改成"用布局真实值",与卡片高度彻底解耦。
|
|
1019
|
+
# 另:也不能写成 "$pos.top − offsetY"(那是把 pos.top 当窗口顶边的顶边锚定)。
|
|
1020
|
+
$win.Top = 0
|
|
1021
|
+
$win.UpdateLayout()
|
|
1022
|
+
$borderBottomInWin = $border.TranslatePoint(
|
|
1023
|
+
(New-Object System.Windows.Point(0, $border.ActualHeight)), $win).Y
|
|
1024
|
+
if ($borderBottomInWin -le 0) {
|
|
1025
|
+
# 兜底:布局尚未就绪时退回到"窗口高 − 常量"的估算。
|
|
1026
|
+
$borderBottomInWin = $win.ActualHeight - $script:TOAST_BOTTOM_GAP
|
|
1027
|
+
}
|
|
558
1028
|
if ($null -ne$pos -and $null -ne$pos.left -and $null -ne$pos.top) {
|
|
559
1029
|
$targetLeft = [double]$pos.left + $work.Left
|
|
560
|
-
$targetTop = [double]$pos.top + $work.Top - [int]$cmd.offsetY
|
|
1030
|
+
$targetTop = [double]$pos.top + $work.Top - $borderBottomInWin - [int]$cmd.offsetY
|
|
561
1031
|
} else {
|
|
1032
|
+
# 默认右下:可见卡片底边距工作区底边 20px(与 index.js 的 TOAST_EDGE_PAD 一致)。
|
|
562
1033
|
$targetLeft =$work.Right - $win.ActualWidth - 20
|
|
563
|
-
$targetTop =$work.Bottom -
|
|
1034
|
+
$targetTop =$work.Bottom - 20 - $borderBottomInWin - [int]$cmd.offsetY
|
|
564
1035
|
}
|
|
565
1036
|
$win.Left =$targetLeft
|
|
566
1037
|
$win.Top =$targetTop + 36
|
package/lib/typert.host.js
CHANGED
|
@@ -52,10 +52,27 @@ const notifyEventSchema = z.object({
|
|
|
52
52
|
at: z.number(),
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 宿主故障状态(④ 设置页横幅的数据源)。
|
|
57
|
+
* null = 正常。旧宿主不返回该字段,靠 .default(null) 兼容。
|
|
58
|
+
*/
|
|
59
|
+
const hostErrorSchema = z.object({
|
|
60
|
+
code: z.number().nullable(),
|
|
61
|
+
signal: z.string().nullable(),
|
|
62
|
+
at: z.number(),
|
|
63
|
+
count: z.number(),
|
|
64
|
+
retryAt: z.number(),
|
|
65
|
+
}).nullable().default(null)
|
|
66
|
+
|
|
55
67
|
const getStateSchema = z.object({
|
|
56
68
|
config: configSchema,
|
|
57
69
|
// 可选+默认 false:旧宿主(无该字段)仍能通过校验,按 web 端处理。
|
|
58
70
|
desktop: z.boolean().default(false),
|
|
71
|
+
// 可选+默认:旧宿主无这两个字段也能通过校验。
|
|
72
|
+
// 注意 zod 的 z.object 会**丢弃**未声明字段,所以新增字段必须在这里声明,
|
|
73
|
+
// 否则宿主返回了也会被 codec 吃掉(已实测确认)。
|
|
74
|
+
hostError: hostErrorSchema,
|
|
75
|
+
pendingCount: z.number().default(0),
|
|
59
76
|
events: z.array(notifyEventSchema),
|
|
60
77
|
})
|
|
61
78
|
|
|
@@ -179,6 +196,17 @@ export const TYPERT = {
|
|
|
179
196
|
parameters: [],
|
|
180
197
|
result: _ack$codec,
|
|
181
198
|
},
|
|
199
|
+
{
|
|
200
|
+
id: 'dsh-notify-yimit#notify/retryHost',
|
|
201
|
+
service: 'notify',
|
|
202
|
+
namespace: 'notify',
|
|
203
|
+
method: 'retryHost',
|
|
204
|
+
invocation: {
|
|
205
|
+
kind: 'direct'
|
|
206
|
+
},
|
|
207
|
+
parameters: [],
|
|
208
|
+
result: _ack$codec,
|
|
209
|
+
},
|
|
182
210
|
],
|
|
183
211
|
model: {
|
|
184
212
|
services: [{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-yimit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "DeepSeek Harness 通知插件:任务完成 / 任务出错 / 运行中(实时活动)/ 等待审批 / 等待回答 时,通过 Windows 系统通知或插件自定义样式通知提醒用户,支持点击跳转到对应会话;设置页集成在 DSH 设置界面。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"lib/typert.host.js",
|
|
34
34
|
"lib/client.js",
|
|
35
35
|
"lib/toast-host.ps1",
|
|
36
|
+
"scripts/check-prepublish.mjs",
|
|
36
37
|
"cordis.patch.yml",
|
|
37
38
|
"README.md",
|
|
38
39
|
"README.en.md"
|
|
39
40
|
],
|
|
40
41
|
"scripts": {
|
|
41
|
-
"prepublishOnly": "node -
|
|
42
|
+
"prepublishOnly": "node scripts/check-prepublish.mjs",
|
|
43
|
+
"check:prepublish": "node scripts/check-prepublish.mjs"
|
|
42
44
|
},
|
|
43
45
|
"repository": {
|
|
44
46
|
"type": "git",
|