dsh-windows-tray 1.0.13 → 1.0.14
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 +79 -44
- package/bin/dsh-tray.js +37 -0
- package/docs/system-tray-guide.md +36 -42
- package/install.ps1 +48 -17
- package/launcher/dsh-actions.ps1 +46 -7
- package/launcher/dsh-boot.ps1 +57 -56
- package/launcher/dsh-lib.ps1 +312 -27
- package/launcher/dsh-menu.ps1 +7 -4
- package/launcher/dsh-tray.ps1 +174 -12
- package/launcher/dsh-update.ps1 +43 -8
- package/package.json +5 -5
- package/docs/tray-dev-archive.md +0 -59
package/launcher/dsh-tray.ps1
CHANGED
|
@@ -186,11 +186,13 @@ function New-SubMenu([string]$text) {
|
|
|
186
186
|
return $it
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
# ----
|
|
189
|
+
# ---- 顶部三状态 ----
|
|
190
190
|
$itemDshStatus = New-MenuItem 'Web UI 状态: 检测中…' $null; $itemDshStatus.Enabled = $false
|
|
191
191
|
$itemTuiStatus = New-MenuItem 'TUI 状态: 检测中…' $null; $itemTuiStatus.Enabled = $false
|
|
192
|
+
$itemBackupStatus = New-MenuItem '备份: 检测中…' $null; $itemBackupStatus.Enabled = $false
|
|
192
193
|
$menu.Items.Add($itemDshStatus) | Out-Null
|
|
193
194
|
$menu.Items.Add($itemTuiStatus) | Out-Null
|
|
195
|
+
$menu.Items.Add($itemBackupStatus) | Out-Null
|
|
194
196
|
$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
|
|
195
197
|
|
|
196
198
|
# ---- Web UI 子菜单 ----
|
|
@@ -212,6 +214,21 @@ $itemTuiStop = New-MenuItem '停止 TUI' { Invoke-DshAction 'tui-stop' }
|
|
|
212
214
|
$itemTui.DropDownItems.AddRange(@($itemTuiStart, $itemTuiStop))
|
|
213
215
|
$menu.Items.Add($itemTui) | Out-Null
|
|
214
216
|
|
|
217
|
+
# ---- 备份子菜单(M2:只做可见性 + 人工触发;不做空窗告警/自动补跑)----
|
|
218
|
+
$itemBackup = New-SubMenu '备份'
|
|
219
|
+
$itemBackupNow = New-MenuItem '立即备份' { Start-BackupAction }
|
|
220
|
+
$itemBackupOpen = New-MenuItem '打开备份目录' { if (-not $BACKUP_STORE) { Show-Balloon 'DSH' '备份库还没建(点「立即备份」会自动挑盘并初始化)'; return }; try { Start-Process $BACKUP_STORE } catch { Log "打开备份目录失败: $($_.Exception.Message)" } }
|
|
221
|
+
$itemBackup.DropDownItems.AddRange(@($itemBackupNow, $itemBackupOpen))
|
|
222
|
+
$menu.Items.Add($itemBackup) | Out-Null
|
|
223
|
+
# 功能开关(plugin-config `features.backup`,**默认关**;2026-09-23 用户裁决 A–F):
|
|
224
|
+
# 关档 = 两个菜单项 Visible=$false。**刻意不"不建项"**:本文件 :18 是 $ErrorActionPreference='Stop',
|
|
225
|
+
# 而 $null.Text 赋值实测当场终止脚本(try/catch 里那句同样抛)⇒ 漏门一处 = OFF 档托盘起不来;
|
|
226
|
+
# Visible 让对象存在、引用恒合法,整类风险消失(方案定稿 §0/§4-1)。
|
|
227
|
+
if (-not $script:BackupEnabled) {
|
|
228
|
+
$itemBackupStatus.Visible = $false
|
|
229
|
+
$itemBackup.Visible = $false
|
|
230
|
+
}
|
|
231
|
+
|
|
215
232
|
$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
|
|
216
233
|
$itemLogs = New-MenuItem '打开日志目录' { try { Start-Process $LOG_DIR } catch { Log "打开日志目录失败: $($_.Exception.Message)" } }
|
|
217
234
|
$menu.Items.Add($itemLogs) | Out-Null
|
|
@@ -237,12 +254,45 @@ function Invoke-DshAction([string]$action) {
|
|
|
237
254
|
$scriptPath = Join-Path $PSScriptRoot 'dsh-actions.ps1'
|
|
238
255
|
$args = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', $scriptPath, '-Action', $action)
|
|
239
256
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
# -PassThru + 退出码反馈(2026-09-23 独审 P2-3:start/restart/tui-start 失败要弹气泡,不能只写日志)
|
|
258
|
+
if (Test-Path $PWSH) { $script:actionProc = Start-Process -FilePath $PWSH -ArgumentList $args -WindowStyle Hidden -PassThru }
|
|
259
|
+
else { $script:actionProc = Start-Process -FilePath 'powershell.exe' -ArgumentList $args -WindowStyle Hidden -PassThru }
|
|
260
|
+
$script:actionName = $action
|
|
261
|
+
$script:actionStartedAt = [Environment]::TickCount64
|
|
242
262
|
Log "已触发动作: $action"
|
|
243
263
|
} catch { Log "触发动作 $action 失败: $($_.Exception.Message)" }
|
|
244
264
|
}
|
|
245
265
|
|
|
266
|
+
# ---- 人工备份:过程与结果都要**看得见**(实现审 B-12)----
|
|
267
|
+
# 旧实现全程 -WindowStyle Hidden:81 MB 的 data 备份要分钟级,期间 UI 毫无反应,用户重复点击还会撞 run.lock。
|
|
268
|
+
function Start-BackupAction {
|
|
269
|
+
if (-not $script:BackupEnabled) {
|
|
270
|
+
# 纵深防御:OFF 档菜单项已不可见,此处再挡一层(不依赖"不可见"这一层判据)
|
|
271
|
+
Show-Balloon 'DSH' '备份未启用(本机默认关闭;启用:plugins\dsh-windows-tray\install.ps1 -EnableBackup)'
|
|
272
|
+
Log '触发备份被拒:功能未启用'
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if ($script:backupProc -and -not $script:backupProc.HasExited) {
|
|
276
|
+
Show-Balloon 'DSH' '备份已在运行中,请等它结束'
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
$scriptPath = Join-Path $PSScriptRoot 'dsh-actions.ps1'
|
|
280
|
+
$bargs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', $scriptPath, '-Action', 'backup')
|
|
281
|
+
try {
|
|
282
|
+
if (Test-Path $PWSH) { $script:backupProc = Start-Process -FilePath $PWSH -ArgumentList $bargs -WindowStyle Hidden -PassThru }
|
|
283
|
+
else { $script:backupProc = Start-Process -FilePath 'powershell.exe' -ArgumentList $bargs -WindowStyle Hidden -PassThru }
|
|
284
|
+
$script:backupStartedAt = [Environment]::TickCount64
|
|
285
|
+
$itemBackupNow.Enabled = $false
|
|
286
|
+
$itemBackupStatus.Text = '备份: 进行中…'
|
|
287
|
+
Show-Balloon 'DSH' '备份已开始(data + runtime + vault),结束后会提示'
|
|
288
|
+
Log '已触发动作: backup(进行中:菜单项置灰 + 状态行显示"进行中")'
|
|
289
|
+
} catch {
|
|
290
|
+
$script:backupProc = $null
|
|
291
|
+
Log "触发备份失败: $($_.Exception.Message)"
|
|
292
|
+
Show-Balloon 'DSH' '备份启动失败,详见 dsh-tray-actions.log'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
246
296
|
# ---- 自更新: 版本对比 / npm 定位 / 检查 / 执行 ----
|
|
247
297
|
function Compare-Version($a, $b) {
|
|
248
298
|
$pa = @(); $pb = @()
|
|
@@ -272,7 +322,8 @@ function Find-NpmCmd {
|
|
|
272
322
|
function Get-LatestTrayVersion {
|
|
273
323
|
$npm = Find-NpmCmd
|
|
274
324
|
if (-not $npm) { Log '自更新: npm 未找到'; return '' }
|
|
275
|
-
|
|
325
|
+
# 唯一临时文件(R2-P3-2:$PID 在 ThreadJob 内=宿主托盘 PID,不区分同托盘多个 job ⇒ 用 guid 唯一化)
|
|
326
|
+
$tmp = Join-Path $env:TEMP ("tray-npm-view-$PID-$([guid]::NewGuid().ToString('N').Substring(0,8)).txt")
|
|
276
327
|
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
277
328
|
$cmdLine = '/c ""' + $npm + '" view ' + $TRAY_PKG + ' version > "' + $tmp + '" 2>nul "'
|
|
278
329
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
@@ -295,9 +346,8 @@ function Get-LatestTrayVersion {
|
|
|
295
346
|
return ''
|
|
296
347
|
}
|
|
297
348
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
$latest = Get-LatestTrayVersion
|
|
349
|
+
# 「检查更新」结果落地(UI 线程,job 回收后调用)—— 与 ThreadJob 版本共用
|
|
350
|
+
function Apply-UpdateResult([string]$latest) {
|
|
301
351
|
if (-not $latest) {
|
|
302
352
|
$itemUpdateStatus.Text = "版本: v$($script:TrayVersion) · 检查失败(网络/npm)"
|
|
303
353
|
Show-Balloon 'DSH' '检查更新失败(网络或 npm 不可用),详见日志'
|
|
@@ -310,9 +360,57 @@ function Update-Check {
|
|
|
310
360
|
$itemUpdateDo.Enabled = $true
|
|
311
361
|
Show-Balloon 'DSH' "托盘有新版本 v$latest,可一键更新"
|
|
312
362
|
} else {
|
|
363
|
+
# 本地 ≥ npm 源:npm 已于 1.0.13 **冻结**(2026-09-23 用户裁决:不再发 npm、后续自用),
|
|
364
|
+
# 故本菜单今后恒落这一支(不是"检查坏了")——明写口径,升级改走源码 install.ps1。
|
|
365
|
+
$ahead = (Compare-Version $script:TrayVersion $latest) -gt 0
|
|
313
366
|
$itemUpdateStatus.Text = "版本: v$($script:TrayVersion) · 已是最新"
|
|
314
367
|
$itemUpdateDo.Enabled = $false
|
|
315
|
-
|
|
368
|
+
if ($ahead) {
|
|
369
|
+
Show-Balloon 'DSH' "本地 v$($script:TrayVersion) 高于 npm 上的 v$latest —— 本机以源码为准,升级走 install.ps1(托盘不再从 npm 装)"
|
|
370
|
+
} else {
|
|
371
|
+
Show-Balloon 'DSH' "已是最新版 v$($script:TrayVersion)"
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function Update-Check {
|
|
377
|
+
$itemUpdateStatus.Text = '检查中…'
|
|
378
|
+
if ($script:threadJobOk) {
|
|
379
|
+
# 异步(2026-09-23 独审 P1-2):npm view 同步跑在 UI 线程会冻菜单最长 20s;
|
|
380
|
+
# 与 TUI 检测同构 —— ThreadJob 里做网络阻塞活,结果由定时器 Receive-Job 回收
|
|
381
|
+
if ($script:updateJob) { Remove-Job $script:updateJob -Force -ErrorAction SilentlyContinue; $script:updateJob = $null }
|
|
382
|
+
$pkg = $TRAY_PKG
|
|
383
|
+
$script:updateJob = Start-ThreadJob -ScriptBlock {
|
|
384
|
+
param($pkg)
|
|
385
|
+
$npm = $null
|
|
386
|
+
foreach ($cand in @($env:NPM_CMD, (Join-Path $env:APPDATA 'npm\npm.cmd'), (Join-Path $env:ProgramFiles 'nodejs\npm.cmd'))) {
|
|
387
|
+
if ($cand -and (Test-Path $cand)) { $npm = $cand; break }
|
|
388
|
+
}
|
|
389
|
+
if (-not $npm) { return '' }
|
|
390
|
+
$tmp = Join-Path $env:TEMP ("tray-npm-view-$PID-$([guid]::NewGuid().ToString('N').Substring(0,8)).txt")
|
|
391
|
+
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
392
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
393
|
+
$psi.FileName = $env:ComSpec
|
|
394
|
+
$psi.Arguments = '/c ""' + $npm + '" view ' + $pkg + ' version > "' + $tmp + '" 2>nul "'
|
|
395
|
+
$psi.UseShellExecute = $false
|
|
396
|
+
$psi.CreateNoWindow = $true
|
|
397
|
+
try {
|
|
398
|
+
$p = [System.Diagnostics.Process]::Start($psi)
|
|
399
|
+
$p.WaitForExit(20000) | Out-Null
|
|
400
|
+
if (-not $p.HasExited) { $p.Kill() }
|
|
401
|
+
} catch { return '' }
|
|
402
|
+
try {
|
|
403
|
+
if (Test-Path $tmp) {
|
|
404
|
+
$v = (Get-Content $tmp -Raw).Trim()
|
|
405
|
+
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
406
|
+
if ($v -match '^[0-9]+\.[0-9]+\.[0-9]+') { return $Matches[0] }
|
|
407
|
+
}
|
|
408
|
+
} catch { }
|
|
409
|
+
return ''
|
|
410
|
+
} -ArgumentList $pkg
|
|
411
|
+
} else {
|
|
412
|
+
# ThreadJob 不可用 ⇒ 降级同步(旧行为,仅在极老环境)
|
|
413
|
+
Apply-UpdateResult (Get-LatestTrayVersion)
|
|
316
414
|
}
|
|
317
415
|
}
|
|
318
416
|
|
|
@@ -354,6 +452,10 @@ $script:prevState = $null # $null=初始; $true=运行; $false=停止
|
|
|
354
452
|
$script:prevTui = $null # TUI 状态 ($null=初始; $true=运行; $false=停止)
|
|
355
453
|
$script:firstRun = $true # 首次刷新只设图标, 不弹变化通知
|
|
356
454
|
$script:lastTuiCheck = 0 # TUI 检测节流 (常规 20s 一次, 打开菜单时强制)
|
|
455
|
+
$script:lastBackupCheck = 0 # 备份状态行节流 (30s 一次, 见 Update-State)
|
|
456
|
+
$script:backupProc = $null # 人工备份的子进程句柄 + 起始 tick(B-12 反馈)
|
|
457
|
+
$script:backupStartedAt = 0
|
|
458
|
+
$script:backupSlowNotified = $false # >30 分钟只提示一次(FB-F8)
|
|
357
459
|
$script:tuiJob = $null # 运行中的 TUI 检测任务 (1.0.12 异步化)
|
|
358
460
|
$script:tuiRefreshQueued = $false # 任务在跑期间又到期/强制刷新 → 完成后立即再刷
|
|
359
461
|
|
|
@@ -393,11 +495,11 @@ function Update-State {
|
|
|
393
495
|
$tuiProcs = @(Receive-Job -Job $script:tuiJob -ErrorAction SilentlyContinue)
|
|
394
496
|
Remove-Job $script:tuiJob -Force -ErrorAction SilentlyContinue
|
|
395
497
|
$script:tuiJob = $null
|
|
396
|
-
$script:lastTuiCheck = [Environment]::
|
|
498
|
+
$script:lastTuiCheck = [Environment]::TickCount64
|
|
397
499
|
Apply-TuiStatus $tuiProcs
|
|
398
500
|
if ($script:tuiRefreshQueued) { $script:tuiRefreshQueued = $false; $script:lastTuiCheck = 0 }
|
|
399
501
|
}
|
|
400
|
-
if ($ForceTui -or (([Environment]::
|
|
502
|
+
if ($ForceTui -or (([Environment]::TickCount64 - $script:lastTuiCheck) -gt 20000)) {
|
|
401
503
|
if (-not $script:tuiJob) {
|
|
402
504
|
$libPath = Join-Path $PSScriptRoot 'dsh-lib.ps1'
|
|
403
505
|
$script:tuiJob = Start-ThreadJob -ScriptBlock {
|
|
@@ -409,10 +511,68 @@ function Update-State {
|
|
|
409
511
|
$script:tuiRefreshQueued = $true
|
|
410
512
|
}
|
|
411
513
|
}
|
|
412
|
-
} elseif ($ForceTui -or (([Environment]::
|
|
413
|
-
$script:lastTuiCheck = [Environment]::
|
|
514
|
+
} elseif ($ForceTui -or (([Environment]::TickCount64 - $script:lastTuiCheck) -gt 20000)) {
|
|
515
|
+
$script:lastTuiCheck = [Environment]::TickCount64
|
|
414
516
|
Apply-TuiStatus (@(Get-TuiProcesses))
|
|
415
517
|
}
|
|
518
|
+
# --- 「检查更新」job 回收(2026-09-23 独审 P1-2:npm view 放 ThreadJob,UI 线程不阻塞)---
|
|
519
|
+
if ($script:updateJob -and $script:updateJob.State -ne 'Running') {
|
|
520
|
+
$latest = (Receive-Job -Job $script:updateJob -ErrorAction SilentlyContinue | Out-String).Trim()
|
|
521
|
+
Remove-Job $script:updateJob -Force -ErrorAction SilentlyContinue
|
|
522
|
+
$script:updateJob = $null
|
|
523
|
+
Apply-UpdateResult $latest
|
|
524
|
+
}
|
|
525
|
+
# --- 通用动作(start/stop/restart/tui-*)退出码反馈(2026-09-23 独审 P2-3:失败要可见)---
|
|
526
|
+
if ($script:actionProc) {
|
|
527
|
+
if ($script:actionProc.HasExited) {
|
|
528
|
+
$acode = $script:actionProc.ExitCode
|
|
529
|
+
$aname = $script:actionName
|
|
530
|
+
$script:actionProc = $null
|
|
531
|
+
if ($acode -ne 0) {
|
|
532
|
+
Show-Balloon 'DSH' "动作「$aname」失败(exit=$acode)—— 详见 dsh-tray-actions.log"
|
|
533
|
+
Log "动作 $aname 失败 exit=$acode"
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
# --- 人工备份:子进程结束 ⇒ 气泡 + 立即刷新状态行(B-12)---
|
|
538
|
+
if ($script:backupProc) {
|
|
539
|
+
if ($script:backupProc.HasExited) {
|
|
540
|
+
$bcode = $script:backupProc.ExitCode
|
|
541
|
+
$bsecs = [math]::Round(([Environment]::TickCount64 - $script:backupStartedAt) / 1000, 0)
|
|
542
|
+
$script:backupProc = $null
|
|
543
|
+
$itemBackupNow.Enabled = $true
|
|
544
|
+
$script:lastBackupCheck = 0 # 立刻让状态行反映结果(不等 30s 节流)
|
|
545
|
+
if ($bcode -eq 0) { Show-Balloon 'DSH' "备份完成 ✓($bsecs 秒)"; Log "备份完成 exit=0($bsecs 秒)" }
|
|
546
|
+
elseif ($bcode -eq 3) {
|
|
547
|
+
# exit 3 = 约定"未就绪"(缺引擎)——**不许说成"失败"**(2026-09-22:托盘不耦合引擎,
|
|
548
|
+
# 只装 npm 包的人必须看到"缺什么 + 怎么补",否则会以为功能坏了)
|
|
549
|
+
Show-Balloon 'DSH' "备份未就绪:缺备份引擎(本包只含托盘 UI)。装法:personal-docs\dsh\config\install.ps1 -Only tools";
|
|
550
|
+
Log "备份未就绪 exit=3:缺引擎($bsecs 秒)"
|
|
551
|
+
}
|
|
552
|
+
elseif ($bcode -eq 4) {
|
|
553
|
+
# exit 4 = 约定"未启用"(plugin-config 开关默认关)——与 exit 3(缺引擎)区分
|
|
554
|
+
Show-Balloon 'DSH' '备份未启用(本机默认关闭)。启用:plugins\dsh-windows-tray\install.ps1 -EnableBackup';
|
|
555
|
+
Log "备份未启用 exit=4($bsecs 秒)"
|
|
556
|
+
}
|
|
557
|
+
else { Show-Balloon 'DSH' "备份失败(exit=$bcode)—— 详见 dsh-tray-actions.log"; Log "备份失败 exit=$bcode($bsecs 秒)" }
|
|
558
|
+
} elseif (([Environment]::TickCount64 - $script:backupStartedAt) -gt 1800000) {
|
|
559
|
+
# 30 分钟未结束:**不清句柄、不放行菜单**(证伪 FB-F8:清了句柄 + 恢复菜单 ⇒ 再点会真起第二个备份撞 run.lock 报"失败")
|
|
560
|
+
# 只提示,并继续等(进程真结束的那一刻仍会被上面的分支捕获)。**不杀进程**。
|
|
561
|
+
if (-not $script:backupSlowNotified) {
|
|
562
|
+
$script:backupSlowNotified = $true
|
|
563
|
+
$itemBackupStatus.Text = '备份: 仍在运行(>30 分钟)…'
|
|
564
|
+
Show-Balloon 'DSH' '备份超过 30 分钟仍未结束(可能被锁或阻塞)—— 详见 dsh-tray-actions.log;托盘会继续等它结束'
|
|
565
|
+
Log '备份疑似超时(>30 分钟):仅提示,继续等待(不清句柄、不放行菜单、不杀进程)'
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
# --- 备份状态行(M2): 30s 节流;备份进行中则保持"进行中…"不被覆盖 ---
|
|
570
|
+
# ⚠ 关档**整支不跑**(设计 §4-3 的行为判据;实现审 R3-01/R4-A18 指出旧版仍无条件刷:
|
|
571
|
+
# 那会让"关档"仍每 30 s 读一次 <DSH_HOME>\tools\dsh-backup.mjs(Test-BackupEngine))
|
|
572
|
+
if ($script:BackupEnabled -and -not $script:backupProc -and ([Environment]::TickCount64 - $script:lastBackupCheck -gt 30000)) {
|
|
573
|
+
$script:lastBackupCheck = [Environment]::TickCount64
|
|
574
|
+
try { $itemBackupStatus.Text = Get-BackupStatusLine } catch { $itemBackupStatus.Text = '备份: 读取失败' }
|
|
575
|
+
}
|
|
416
576
|
# --- Web 状态: Test-Port ~11ms + 转态才查 PID (netstat ~42ms), 保持在 UI 线程 ---
|
|
417
577
|
$running = Test-Port $PORT
|
|
418
578
|
if ($running -eq $script:prevState -and -not $script:firstRun) { return } # 无变化
|
|
@@ -449,6 +609,8 @@ $timer.add_Tick({ Update-State })
|
|
|
449
609
|
$timer.Start()
|
|
450
610
|
|
|
451
611
|
# 启动即刷新一次状态(不弹变化通知), 并提示就绪
|
|
612
|
+
# ⚠ 同 timer:关档不刷(关档下该状态行也不可见;见设计 §4-3)
|
|
613
|
+
if ($script:BackupEnabled) { try { $itemBackupStatus.Text = Get-BackupStatusLine } catch { $itemBackupStatus.Text = '备份: 读取失败' } }
|
|
452
614
|
Update-State
|
|
453
615
|
if (-not $NoBalloon) { Show-Balloon 'DSH' "托盘已就绪 · 双击打开 Web · 右键控制 (端口 $PORT) · 若图标在 ^ 溢出区, 请拖到任务栏" }
|
|
454
616
|
Log "托盘启动 (v$($script:TrayVersion), PID $PID, 端口 $PORT)"
|
package/launcher/dsh-update.ps1
CHANGED
|
@@ -140,17 +140,52 @@ if ($code -ne 0) {
|
|
|
140
140
|
Log '新版本部署完成'
|
|
141
141
|
|
|
142
142
|
# 4) 杀旧托盘 -> 起新托盘
|
|
143
|
+
# 2026-09-22 修(自更新路径从未被验证过的三处静默):
|
|
144
|
+
# ① 停要「验死」:Stop-Process 后轮询确认进程真退出;10s 未退 ⇒ 报错退出(不重复启动)
|
|
145
|
+
# ② 起要「验活」:启动后轮询确认存在命令行含 dsh-tray.ps1 的新进程;10s 未见 ⇒ 报错
|
|
146
|
+
# ③ 只验过后才写「已启动 / ✓」——此前无条件写成功且无等待,属"报告者=被报告者"
|
|
143
147
|
if ($OldPid -gt 0) {
|
|
144
148
|
Stop-Process -Id $OldPid -Force -ErrorAction SilentlyContinue
|
|
145
|
-
|
|
149
|
+
$deadline = (Get-Date).AddSeconds(10)
|
|
150
|
+
while ((Get-Process -Id $OldPid -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 200 }
|
|
151
|
+
if (Get-Process -Id $OldPid -ErrorAction SilentlyContinue) {
|
|
152
|
+
Log "自更新失败: 旧托盘 PID $OldPid 在 10s 内未退出 (为避免双托盘, 未启动新实例)"
|
|
153
|
+
Show-Balloon '自更新失败:旧托盘未退出,请手动「退出托盘」后重新登录' 'Error'
|
|
154
|
+
exit 1
|
|
155
|
+
}
|
|
156
|
+
Log "旧托盘已停止 (PID $OldPid, 已验死)"
|
|
146
157
|
}
|
|
147
158
|
Start-Sleep -Milliseconds 500
|
|
148
159
|
$newTray = Join-Path $env:USERPROFILE '.dsh\launcher\dsh-tray.ps1'
|
|
149
|
-
if (Test-Path $
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
if (-not (Test-Path $newTray)) {
|
|
161
|
+
Log "自更新失败: 部署件不存在 $newTray"
|
|
162
|
+
Show-Balloon '自更新失败:未找到新托盘脚本' 'Error'
|
|
163
|
+
exit 1
|
|
164
|
+
}
|
|
165
|
+
$starter = if (Test-Path $PWSH) { $PWSH } else { 'powershell.exe' }
|
|
166
|
+
Start-Process -FilePath $starter -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', $newTray) -WindowStyle Hidden | Out-Null
|
|
167
|
+
$alive = $false
|
|
168
|
+
$deadline2 = (Get-Date).AddSeconds(10)
|
|
169
|
+
do {
|
|
170
|
+
Start-Sleep -Milliseconds 500
|
|
171
|
+
# R2-P2#3:验活同构(单次窄集快照 + 祖先链排除)——'dsh-tray.ps1' 子串会被 web 工具执行器子孙误命中
|
|
172
|
+
# ⇒ 旧托盘已杀却谎报"已验活"。锁⑨/⑫ 形态:窄集 + 查询管道带 $PID 自排除。
|
|
173
|
+
$snap = @(Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='cmd.exe' OR Name='pwsh.exe' OR Name='powershell.exe'" -ErrorAction SilentlyContinue |
|
|
174
|
+
Where-Object { $_.ProcessId -ne $PID })
|
|
175
|
+
$byId = @{}
|
|
176
|
+
foreach ($p in $snap) { $byId[[int]$p.ProcessId] = $p }
|
|
177
|
+
$me = Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -ErrorAction SilentlyContinue
|
|
178
|
+
if ($me) { $byId[[int]$PID] = $me }
|
|
179
|
+
$alive = @($snap | Where-Object {
|
|
180
|
+
$_.Name -in @('pwsh.exe', 'powershell.exe') -and $_.CommandLine -and $_.CommandLine -like '*dsh-tray.ps1*' -and
|
|
181
|
+
-not (Test-DshWorkerAncestor -Id ([int]$_.ProcessId) -Index $byId)
|
|
182
|
+
}).Count -gt 0
|
|
183
|
+
} while (-not $alive -and (Get-Date) -lt $deadline2)
|
|
184
|
+
if ($alive) {
|
|
185
|
+
Log '新托盘已启动 (已验活)'
|
|
186
|
+
Show-Balloon '托盘已更新并自动重启 ✓'
|
|
187
|
+
exit 0
|
|
153
188
|
}
|
|
154
|
-
Log
|
|
155
|
-
Show-Balloon '
|
|
156
|
-
exit
|
|
189
|
+
Log "自更新失败: 新托盘 10s 内未出现 (部署已完成, 可手动启动托盘)"
|
|
190
|
+
Show-Balloon '自更新失败:新托盘未起来,请手动启动(部署已完成)' 'Error'
|
|
191
|
+
exit 1
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-windows-tray",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "DSH (DeepSeek Harness) Windows 系统托盘: 蓝/灰鲸鱼状态图标 + Web/TUI 一键启停控制。纯 PowerShell + WinForms,
|
|
3
|
+
"version": "1.0.14",
|
|
4
|
+
"description": "DSH (DeepSeek Harness) Windows 系统托盘: 蓝/灰鲸鱼状态图标 + Web/TUI 一键启停控制。纯 PowerShell + WinForms, 零依赖。※ 菜单「备份」需 personal-docs 侧的备份引擎(不随本包分发,缺则显式提示未就绪)——见 README「前置依赖」。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "jankin_lv",
|
|
7
7
|
"dshCompatibility": {
|
|
8
8
|
"testedVersions": ["0.1.1-rc.2", "0.1.2-rc.1", "0.1.5-rc.1", "0.1.5-rc.2"],
|
|
9
9
|
"range": ">=0.1.1-rc.2 <0.2.0",
|
|
10
10
|
"scope": "external-tool(本包不 import 任何 @deepseek-ai 代码:只拉起 dsh 的 bin.js web、探测 3080 端口、按进程命令行识别 TUI;故不声明 @deepseek-ai peerDependencies——那只会给用户刷 unmet peer 警告而买不到任何真实校验)",
|
|
11
|
-
"note": "dsh ≥0.1.2-rc.1 起 web 用一次性 launch token;托盘 ≥1.0.11 才会解析它(本包 1.0.
|
|
11
|
+
"note": "dsh ≥0.1.2-rc.1 起 web 用一次性 launch token;托盘 ≥1.0.11 才会解析它(本包 1.0.14)"
|
|
12
12
|
},"keywords": [
|
|
13
13
|
"dsh",
|
|
14
14
|
"deepseek",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"launcher/",
|
|
30
30
|
"build/",
|
|
31
31
|
"docs/",
|
|
32
|
+
"!docs/tray-dev-archive.md",
|
|
32
33
|
"bin/",
|
|
33
34
|
"install.ps1",
|
|
34
35
|
"README.md",
|
|
@@ -39,6 +40,5 @@
|
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"prepublishOnly": "node ../../scripts/publish-check.mjs"
|
|
42
|
-
}
|
|
43
|
-
"private": false
|
|
43
|
+
}
|
|
44
44
|
}
|
package/docs/tray-dev-archive.md
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# 系统托盘开发归档(v1.0.0 · 2026-08-27)
|
|
2
|
-
|
|
3
|
-
> 托盘已发布为 npm 包 **`dsh-windows-tray`**,日常安装 / 使用 / 更新 / 卸载见 [system-tray-guide.md](system-tray-guide.md)。
|
|
4
|
-
> 本文仅归档开发期的**架构决策与历史记录**(源码细节、文件清单不再维护);通用踩坑已归入 [../../../docs/powershell-pitfalls.md](../../../docs/powershell-pitfalls.md)。
|
|
5
|
-
>
|
|
6
|
-
> **端口(2026-09-07 起)**:本包默认探测端口为 **3080**。
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 1. 架构决策(仍生效)
|
|
11
|
-
|
|
12
|
-
| 决策 | 理由 |
|
|
13
|
-
| :--- | :--- |
|
|
14
|
-
| **独立常驻进程**(不嵌入 dsh) | 需求是"看到未启动状态"——进程内嵌图标会随 dsh 消失;旁观者进程才能表达 运行/停止 两态,且能重启已死的 dsh |
|
|
15
|
-
| **端口探测**(每 2s TCP 连 127.0.0.1:3080,500ms 超时) | 不持有进程句柄;dsh 重启 PID 变化也不误判;对 dsh 实现零依赖 |
|
|
16
|
-
| **动作独立进程**(隐藏执行) | 菜单动作(含 taskkill、等待轮询)不阻塞托盘 UI 线程 |
|
|
17
|
-
| **静态 .ico 图标资源** | 不依赖 dsh 运行时接口;缺失时 `New-FallbackIcon` 画纯色圆点兜底 |
|
|
18
|
-
| **命名互斥锁单实例** | 替代 WMI 扫描,无竞态;被强杀遗留的锁自动接管(AbandonedMutexException) |
|
|
19
|
-
| **TUI 单实例锁**(仅启动侧) | 防连点误开多窗口;检测/停止侧本就支持多实例(全查全杀);物理上 TUI 可多开,放开只需移除启动锁 |
|
|
20
|
-
|
|
21
|
-
进程模型:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
托盘(pwsh) ──2s 轮询──► 端口 3080 (dsh web)
|
|
25
|
-
│ └──────────────► node 进程命令行含 'dsh-tui' (TUI)
|
|
26
|
-
├── 菜单动作 ──► 独立隐藏进程 (带日志)
|
|
27
|
-
└── 自更新 ──► npm install -g → 重部署 → 重启托盘
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## 2. 图标渲染管线(摘要)
|
|
31
|
-
|
|
32
|
-
官方 `favicon.svg`(鲸鱼)→ 浏览器渲染 512px → PowerShell 逐像素"亮度转透明度 + 填充目标色" → 多尺寸 ICO(16/24/32/48/64/256)。运行态品牌蓝 `#4176E6`,停止态中性灰 `#81858C`。源图与脚本已内置 npm 包 `build/`,`dsh-tray icons` 可随时再生成。
|
|
33
|
-
|
|
34
|
-
## 3. 社区借鉴(发布前调研)
|
|
35
|
-
|
|
36
|
-
npm 上两个同思路项目(`dsh-tray`、`dsh-tray-launcher`,均一周内发布),落地到本项目:
|
|
37
|
-
|
|
38
|
-
- **命名互斥锁单实例**(替代 WMI 扫描,无竞态)
|
|
39
|
-
- **浅色圆角菜单**(白底 + 圆角 hover + 细分隔线 + P/Invoke 圆角窗口 + CS_DROPSHADOW 投影,主/子菜单通用)
|
|
40
|
-
- **菜单打开时强制刷新** TUI 状态(`add_Opening`),平时 TUI 检测 20s 节流
|
|
41
|
-
- **停止 DSH 前 MessageBox 确认**、**版本烙印**(启动日志显示 v1.0.0,npm 安装版优先读 package.json)
|
|
42
|
-
|
|
43
|
-
不照搬的差异化设计:**旁观者架构**(退出托盘不影响 dsh)、**2s 端口轮询状态图标**、**TUI 启停集成 / 官方鲸鱼图标 / 控制台共享公共库**。
|
|
44
|
-
|
|
45
|
-
## 4. 需求变更决策记录
|
|
46
|
-
|
|
47
|
-
| 时间 | 需求 | 决策 |
|
|
48
|
-
| :--- | :--- | :--- |
|
|
49
|
-
| 2026-08-27 | 托盘内 npm 自更新 | 曾按"更新应由 npm 标准流程承担"移除 → 用户决定恢复("更新加回去吧")→ 菜单「版本信息 ▸」(位于退出托盘上方,非日常功能区)提供 检查更新 / 一键更新;README 同时保留 npm CLI 路径 |
|
|
50
|
-
| 2026-08-27 | 菜单太长 | DSH / TUI 收进子菜单(借鉴更新子菜单收纳思路) |
|
|
51
|
-
| 2026-08-27 | 只显示 dsh 状态 | 顶部双状态行:`DSH 状态: …` / `TUI 状态: …`(独立检测、互不干扰) |
|
|
52
|
-
| 2026-08-27 | 一级/二级菜单间隔大 | 根因:箭头预留撑宽父项 + 事件闭包 $null(详见 powershell-pitfalls §1.1 / §4.2) |
|
|
53
|
-
|
|
54
|
-
## 5. 已知边界
|
|
55
|
-
|
|
56
|
-
1. 3080 被其他程序占用时托盘误判"运行中"(属端口问题,与托盘无关)
|
|
57
|
-
2. 二态检测:只区分 运行/停止,无法区分"忙/卡死"(可扩展 HTTP 探测做三态)
|
|
58
|
-
3. TUI 单实例是策略而非物理限制(见 §1 决策表)
|
|
59
|
-
4. 单实例互斥锁在极端竞态下仍有极小双启动概率(影响可忽略)
|