dsh-windows-tray 1.0.10 → 1.0.12

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
@@ -35,7 +35,7 @@ dsh 默认在终端前台运行 `dsh web`——必须留着一个命令行窗口
35
35
 
36
36
  | 项 | 说明 |
37
37
  | :--- | :--- |
38
- | **dsh 内核** | 适配 `@deepseek-ai/dsh` **v0.1.1-rc.2 系列**(共享核心 `@deepseek-ai/dsh-base` 同版本);dsh 升级不影响托盘 |
38
+ | **dsh 内核** | 适配 `@deepseek-ai/dsh` **v0.1.1-rc.2 ~ 0.1.2-rc.1 系列**;**rc.1 起引入一次性 launch token 鉴权**(打开 Web UI 必须用启动横幅 `?token=` URL 首次签发持久 cookie)→ **打开/重启 Web UI 需托盘 ≥1.0.11**(自动解析 `web-menu.out.log` 的 token 并探活;无 token/失效回退裸 URL = 向后兼容);1.0.10 及更早在 rc.1+ 上打开的是裸 URL,会看到 `dsh web authentication required` 提示页 |
39
39
  | **Web 服务** | dsh web 默认监听 `127.0.0.1:3088`(即浏览器里的 Web UI)——托盘通过 TCP 探测该端口判断 Web 状态(端口被改时同步修改 `dsh-lib.ps1` 的 `$PORT` 即可) |
40
40
  | **TUI 插件** | 适配 `@deepseek-harness-tui/dsh-tui` **^0.9.2**(通过 dsh-profile `dsh-profile-dsh-tui` 挂载);托盘按进程命令行 `dsh-tui` 检测,与插件版本解耦 |
41
41
  | **Node.js** | ≥ 18(dsh 本体运行所需) |
@@ -12,7 +12,7 @@ $URL = "http://127.0.0.1:$PORT"
12
12
  $LOG_DIR = Join-Path (Join-Path $env:USERPROFILE '.dsh\logs') 'dsh-windows-tray' # 本插件专属日志目录 (logs\<插件名>\, 边界规则见 release-standards §3.1)
13
13
  $WT = "$env:LOCALAPPDATA\Microsoft\WindowsApps\wt.exe"
14
14
  $PWSH = 'D:\Program Files (x86)\PowerShell-7.6.4\pwsh.exe'
15
- $TRAY_VERSION = '1.0.10' # 托盘版本 (与 npm 包版本同步, 用于日志/排查)
15
+ $TRAY_VERSION = '1.0.12' # 托盘版本 (与 npm 包版本同步, 用于日志/排查)
16
16
  $TRAY_PKG = 'dsh-windows-tray' # npm 包名 (托盘自更新)
17
17
  $TRAY_SCRIPT = Join-Path $PSScriptRoot 'dsh-tray.ps1'
18
18
 
@@ -75,6 +75,15 @@ function Initialize-LogDir {
75
75
  }
76
76
 
77
77
  function Get-WebPid {
78
+ # 优化(1.0.12): netstat 解析优先 (原 Get-NetTCPConnection 在本机 727 进程下 avg 1.3s,
79
+ # 直接拖慢右键菜单转态路径); 解析失败回退 CIM 查询
80
+ try {
81
+ $pids = @()
82
+ foreach ($row in (& netstat -ano -p tcp 2>$null)) {
83
+ if ($row -match ":$PORT\s+\S+\s+LISTENING\s+(\d+)\s*$") { $pids += $Matches[1] }
84
+ }
85
+ if ($pids.Count -gt 0) { return @($pids | Sort-Object -Unique) }
86
+ } catch { }
78
87
  @((Get-NetTCPConnection -LocalPort $PORT -State Listen -ErrorAction SilentlyContinue).OwningProcess | Select-Object -Unique)
79
88
  }
80
89
 
@@ -160,10 +169,12 @@ function Start-Tui {
160
169
 
161
170
  # TUI 进程树: 外壳 pwsh(-NoExit) → cmd(dsh.cmd) → node(bin.js), 三层命令行都含 dsh-tui
162
171
  # 按进程名白名单过滤, 防止误杀命令行里恰好含 dsh-tui 的其他程序 (如 WindowsTerminal.exe 本体)
172
+ # 优化(1.0.12): WQL 先按 Name 窄集 (原全量枚举会拉全部进程的 CommandLine);
173
+ # ⚠️ 本机实测任何 Win32_Process 查询固定 ~450-570ms (WinRM 不可用, DCOM/持久会话均无效)——
174
+ # **调用方 (托盘状态刷新) 必须在 ThreadJob 里异步跑, 禁止在 UI/菜单路径同步调用** (见 dsh-tray.ps1)
163
175
  function Get-TuiTree {
164
- Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
165
- $_.ProcessId -ne $PID -and $_.CommandLine -and
166
- $_.CommandLine -match 'dsh-tui' -and $_.Name -match '^(node|cmd|pwsh|powershell)\.exe$'
176
+ Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='cmd.exe' OR Name='pwsh.exe' OR Name='powershell.exe'" -ErrorAction SilentlyContinue | Where-Object {
177
+ $_.ProcessId -ne $PID -and $_.CommandLine -and $_.CommandLine -match 'dsh-tui'
167
178
  }
168
179
  }
169
180
 
@@ -216,6 +227,63 @@ function Stop-Tui {
216
227
  $WEB_TITLE_PREFIX = 'DeepSeek Harness' # dsh web 页面 <title> 关键标识 (含匹配, 见上)
217
228
  $BROWSER_PROGID_MAP = @{ 'MSEdgeHTM' = 'msedge'; 'ChromeHTML' = 'chrome' } # Chromium ProgId → 进程名; 查不到=降级新开
218
229
 
230
+ # ---- dsh 0.1.2+ 一次性 launch token 鉴权 (1.0.11) ----
231
+ # rc.1 起 dsh web 每次启动生成随机 launch token, 横幅打印 「dsh web: http://127.0.0.1:<port>/?token=...」;
232
+ # 该 URL 首次访问签发持久签名 cookie (跨重启有效), 此后裸 URL 才可直访——因此托盘打开 Web UI
233
+ # 必须用带 token 的 URL (或命中已授权标签页)。
234
+ # 向后兼容: 旧版 dsh 无 token 行 / token 失效 → 全部回退裸 URL (行为同 1.0.10 及以前)。
235
+ $WEB_TOKEN_LOG = Join-Path $LOG_DIR 'web-menu.out.log' # 与 Start-Dsh 的重定向输出同文件 (每 boot 覆写)
236
+
237
+ # 解析启动日志中的 launch token URL (最后一条命中; 纯读, 网络探活在 Get-WebLaunchUrl 里做)
238
+ function Parse-WebLaunchTokenUrl {
239
+ param([string]$LogPath = $WEB_TOKEN_LOG)
240
+ if (-not (Test-Path $LogPath)) { return '' }
241
+ $found = ''
242
+ foreach ($line in (Get-Content -LiteralPath $LogPath -ErrorAction SilentlyContinue)) {
243
+ if ($line -match '(https?://\S*\?token=[A-Za-z0-9_-]+)') { $found = $Matches[1] }
244
+ }
245
+ return $found
246
+ }
247
+
248
+ # 探活: token URL 对无 cookie 请求应 303 (签发 Set-Cookie 后重定向到净 URL); 39x/200 均算有效。
249
+ # 用 HttpWebRequest 不自动跟随重定向; 401 (token 过期/被拒) 或网络异常 → 无效
250
+ function Test-WebLaunchUrl {
251
+ param([string]$Url)
252
+ foreach ($u in @($Url)) {
253
+ try {
254
+ $req = [System.Net.HttpWebRequest]::Create($u)
255
+ $req.AllowAutoRedirect = $false
256
+ $req.Timeout = 3000
257
+ $resp = $req.GetResponse()
258
+ $code = [int]$resp.StatusCode
259
+ $resp.Close()
260
+ if ($code -ge 200 -and $code -lt 400) { return $true }
261
+ } catch {
262
+ $resp = $_.Exception.Response
263
+ if ($null -ne $resp) {
264
+ $code = [int]$resp.StatusCode
265
+ $resp.Close()
266
+ if ($code -ge 200 -and $code -lt 400) { return $true }
267
+ }
268
+ }
269
+ }
270
+ return $false
271
+ }
272
+
273
+ # 决定打开哪个 URL: 日志有 token 且探活有效 → 带 token URL; 否则裸 URL (旧版 dsh 直访 200)
274
+ function Get-WebLaunchUrl {
275
+ param(
276
+ [string]$Fallback = $URL,
277
+ [string]$LogPath = $WEB_TOKEN_LOG,
278
+ [switch]$SkipProbe # 单测注入: 跳过网络探活, 只测解析与回退逻辑
279
+ )
280
+ $tokenUrl = Parse-WebLaunchTokenUrl -LogPath $LogPath
281
+ if ($tokenUrl -ne '') {
282
+ if ($SkipProbe -or (Test-WebLaunchUrl -Url $tokenUrl)) { return $tokenUrl }
283
+ }
284
+ return $Fallback
285
+ }
286
+
219
287
  function Open-DshWeb {
220
288
  param([string]$Url = $URL)
221
289
  try {
@@ -256,7 +324,9 @@ namespace DSH {
256
324
  } catch {
257
325
  Write-Host "[web] 切换检测不可用($($_.Exception.Message)), 新开" -ForegroundColor DarkGray
258
326
  }
259
- Start-Process $Url
327
+ $openUrl = Get-WebLaunchUrl -Fallback $Url
328
+ Write-Host "[web] 打开: $openUrl" -ForegroundColor DarkGray
329
+ Start-Process $openUrl
260
330
  }
261
331
 
262
332
  function Show-Status {
@@ -283,8 +353,7 @@ function Show-Status {
283
353
  # ---- 系统托盘 ----
284
354
 
285
355
  function Get-TrayProcesses {
286
- Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
287
- ($_.Name -eq 'pwsh.exe' -or $_.Name -eq 'powershell.exe') -and
356
+ Get-CimInstance Win32_Process -Filter "Name='pwsh.exe' OR Name='powershell.exe'" -ErrorAction SilentlyContinue | Where-Object {
288
357
  $_.CommandLine -and $_.CommandLine -match 'dsh-tray\.ps1' -and
289
358
  $_.ProcessId -ne $PID
290
359
  }
@@ -354,31 +354,66 @@ $script:prevState = $null # $null=初始; $true=运行; $false=停止
354
354
  $script:prevTui = $null # TUI 状态 ($null=初始; $true=运行; $false=停止)
355
355
  $script:firstRun = $true # 首次刷新只设图标, 不弹变化通知
356
356
  $script:lastTuiCheck = 0 # TUI 检测节流 (常规 20s 一次, 打开菜单时强制)
357
+ $script:tuiJob = $null # 运行中的 TUI 检测任务 (1.0.12 异步化)
358
+ $script:tuiRefreshQueued = $false # 任务在跑期间又到期/强制刷新 → 完成后立即再刷
359
+
360
+ # ThreadJob 异步化 (1.0.12): 本机实测任何 Win32_Process WMI 查询固定 ~450-570ms
361
+ # (WinRM 不可用, WSMAN/DCOM 持久会话均无效) —— 若在菜单 Opening / 状态 tick 同步跑,
362
+ # 右键菜单会被 WinForms 卡住 (Opening 未返回不渲染)。TUI 检测改为后台任务,
363
+ # 结果由 2s 定时器在 UI 线程回收应用; ThreadJob 模块缺失时降级同步 (旧行为)。
364
+ Import-Module Microsoft.PowerShell.ThreadJob -ErrorAction SilentlyContinue
365
+ $script:threadJobOk = $null -ne (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue)
366
+
367
+ function Apply-TuiStatus {
368
+ param([object[]]$tuiProcs)
369
+ $tuiRunning = $tuiProcs.Count -gt 0
370
+ if ($tuiRunning) {
371
+ $itemTuiStatus.Text = "TUI 状态: 运行中 (PID $(($tuiProcs.ProcessId) -join ','))"
372
+ } else {
373
+ $itemTuiStatus.Text = 'TUI 状态: 未运行'
374
+ }
375
+ if ($tuiRunning -ne $script:prevTui) {
376
+ $script:prevTui = $tuiRunning
377
+ $itemTuiStart.Enabled = -not $tuiRunning
378
+ $itemTuiStop.Enabled = $tuiRunning
379
+ if ($tuiRunning) {
380
+ Log "TUI: 运行中 (PID $(($tuiProcs.ProcessId) -join ','))"
381
+ } else {
382
+ Log 'TUI: 未运行'
383
+ }
384
+ }
385
+ }
357
386
 
358
387
  function Update-State {
359
388
  param([switch]$ForceTui)
360
389
  try {
361
- # TUI 状态独立检测 (不随 Web 状态提前返回); WMI 查询节流, 减少周期开销
362
- if ($ForceTui -or ([Environment]::TickCount - $script:lastTuiCheck) -gt 20000) {
363
- $script:lastTuiCheck = [Environment]::TickCount
364
- $tuiProcs = @(Get-TuiProcesses)
365
- $tuiRunning = $tuiProcs.Count -gt 0
366
- if ($tuiRunning) {
367
- $itemTuiStatus.Text = "TUI 状态: 运行中 (PID $(($tuiProcs.ProcessId) -join ','))"
368
- } else {
369
- $itemTuiStatus.Text = 'TUI 状态: 未运行'
390
+ # --- TUI 检测: 异步, 绝不阻塞 UI —— 先回收已完成任务, 再按需排程 ---
391
+ if ($script:threadJobOk) {
392
+ if ($script:tuiJob -and $script:tuiJob.State -ne 'Running') {
393
+ $tuiProcs = @(Receive-Job -Job $script:tuiJob -ErrorAction SilentlyContinue)
394
+ Remove-Job $script:tuiJob -Force -ErrorAction SilentlyContinue
395
+ $script:tuiJob = $null
396
+ $script:lastTuiCheck = [Environment]::TickCount
397
+ Apply-TuiStatus $tuiProcs
398
+ if ($script:tuiRefreshQueued) { $script:tuiRefreshQueued = $false; $script:lastTuiCheck = 0 }
370
399
  }
371
- if ($tuiRunning -ne $script:prevTui) {
372
- $script:prevTui = $tuiRunning
373
- $itemTuiStart.Enabled = -not $tuiRunning
374
- $itemTuiStop.Enabled = $tuiRunning
375
- if ($tuiRunning) {
376
- Log "TUI: 运行中 (PID $(($tuiProcs.ProcessId) -join ','))"
400
+ if ($ForceTui -or (([Environment]::TickCount - $script:lastTuiCheck) -gt 20000)) {
401
+ if (-not $script:tuiJob) {
402
+ $libPath = Join-Path $PSScriptRoot 'dsh-lib.ps1'
403
+ $script:tuiJob = Start-ThreadJob -ScriptBlock {
404
+ param($lib)
405
+ . $lib
406
+ @(Get-TuiProcesses)
407
+ } -ArgumentList $libPath
377
408
  } else {
378
- Log 'TUI: 未运行'
409
+ $script:tuiRefreshQueued = $true
379
410
  }
380
411
  }
412
+ } elseif ($ForceTui -or (([Environment]::TickCount - $script:lastTuiCheck) -gt 20000)) {
413
+ $script:lastTuiCheck = [Environment]::TickCount
414
+ Apply-TuiStatus (@(Get-TuiProcesses))
381
415
  }
416
+ # --- Web 状态: Test-Port ~11ms + 转态才查 PID (netstat ~42ms), 保持在 UI 线程 ---
382
417
  $running = Test-Port $PORT
383
418
  if ($running -eq $script:prevState -and -not $script:firstRun) { return } # 无变化
384
419
  $script:prevState = $running
@@ -426,5 +461,6 @@ $timer.Dispose()
426
461
  $menu.Dispose()
427
462
  $iconRun.Dispose(); $iconStop.Dispose()
428
463
  try { $mutex.ReleaseMutex() } catch { }
464
+ try { if ($script:tuiJob) { Remove-Job $script:tuiJob -Force -ErrorAction SilentlyContinue; $script:tuiJob = $null } } catch { }
429
465
  Log '托盘已退出'
430
466
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-windows-tray",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "DSH (DeepSeek Harness) Windows 系统托盘: 蓝/灰鲸鱼状态图标 + Web/TUI 一键启停控制。纯 PowerShell + WinForms, 零依赖。",
5
5
  "license": "MIT",
6
6
  "author": "jankin_lv",