diff --git a/assets/dsh.ico b/assets/dsh.ico index 9799baf..75ed2b7 100644 Binary files a/assets/dsh.ico and b/assets/dsh.ico differ diff --git a/assets/dsh.ico.bak-20260817-125345 b/assets/dsh.ico.bak-20260817-125345 new file mode 100644 index 0000000..9799baf Binary files /dev/null and b/assets/dsh.ico.bak-20260817-125345 differ diff --git a/tools/dsh-app.ps1 b/tools/dsh-app.ps1 new file mode 100644 index 0000000..a080e63 --- /dev/null +++ b/tools/dsh-app.ps1 @@ -0,0 +1,117 @@ +# DeepSeek Harness - 托盘应用 +# 双击桌面快捷方式启动:拉起 dsh web 服务(若未运行),右下角托盘常驻, +# 右键菜单可"打开主界面"(Edge 独立应用窗口)或"退出"(停止服务并退出)。 +$ErrorActionPreference = 'Continue' + +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +$DshBin = 'C:\Users\a1703\AppData\Local\npm-cache\_npx\1e7f6d9597241db0\node_modules\@deepseek-ai\dsh\lib\bin.js' +$IconFile = 'C:\Users\a1703\.dsh\assets\dsh.ico' +$LogDir = 'C:\Users\a1703\.dsh\logs' +$LogFile = Join-Path $LogDir 'dsh-app.log' +$Url = 'http://127.0.0.1:3080' +$Port = 3080 +$WorkDir = 'C:\Users\a1703' +$Edge = @('C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe', 'C:\Program Files\Microsoft\Edge\Application\msedge.exe') | Where-Object { Test-Path $_ } | Select-Object -First 1 + +New-Item -ItemType Directory -Path $LogDir -Force | Out-Null + +function Log([string]$msg) { + try { Add-Content -Path $LogFile -Value ("{0} {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $msg) -Encoding UTF8 } catch {} +} + +function Test-Port([int]$port) { + try { + $c = New-Object System.Net.Sockets.TcpClient + $ar = $c.BeginConnect('127.0.0.1', $port, $null, $null) + if (-not $ar.AsyncWaitHandle.WaitOne(500)) { $c.Close(); return $false } + $c.EndConnect($ar); $c.Close(); return $true + } catch { return $false } +} + +function Stop-Dsh { + Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -like '*@deepseek-ai\dsh*lib*bin.js*' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } +} + +function Stop-EdgeApp { + Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -like '*--app=http://127.0.0.1:3080*' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } +} + +function Open-Main { + try { + if ($Edge) { Start-Process $Edge -ArgumentList "--app=$Url" } + else { Start-Process $Url } + Log 'opened main window' + } catch { Log "open main failed: $_" } +} + +# 单实例保护:已有托盘在运行则直接弹出主界面并退出本进程 +$mutex = New-Object System.Threading.Mutex($true, 'DSHWebTrayApp') +if (-not $mutex.WaitOne(0)) { + Log 'another instance is running; opening main window only' + Open-Main + exit +} + +# 确保服务在跑 +if (-not (Test-Port $Port)) { + Log 'dsh not listening; starting it' + try { + Start-Process -FilePath 'node' -ArgumentList @($DshBin, 'web') -WorkingDirectory $WorkDir -WindowStyle Hidden ` + -RedirectStandardOutput (Join-Path $LogDir 'dsh-web.out.log') -RedirectStandardError (Join-Path $LogDir 'dsh-web.err.log') + } catch { Log "start dsh failed: $_" } + $ready = $false + for ($i = 0; $i -lt 120; $i++) { + Start-Sleep -Milliseconds 500 + if (Test-Port $Port) { $ready = $true; break } + } + if ($ready) { Log 'dsh is ready' } else { Log 'dsh did not become ready within 60s' } +} else { + Log 'dsh already listening' +} + +# 托盘图标与菜单 +$notify = New-Object System.Windows.Forms.NotifyIcon +try { + $notify.Icon = New-Object System.Drawing.Icon($IconFile) +} catch { + Log "icon load failed: $_; falling back" + try { + # 兜底1:同目录下尝试修复版/其他 .ico + $alt = Get-ChildItem (Split-Path $IconFile) -Filter '*.ico' -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($alt) { $notify.Icon = New-Object System.Drawing.Icon($alt.FullName) } + } catch {} + if (-not $notify.Icon) { + try { $notify.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon((Get-Process -Id $PID).Path) } catch {} + } +} +$notify.Text = 'DeepSeek Harness' +$notify.Visible = $true + +$menu = New-Object System.Windows.Forms.ContextMenuStrip +$itemOpen = New-Object System.Windows.Forms.ToolStripMenuItem('打开主界面') +$itemOpen.Add_Click({ Open-Main }) +$itemExit = New-Object System.Windows.Forms.ToolStripMenuItem('退出') +$itemExit.Add_Click({ + Log 'exit requested' + try { $notify.Visible = $false } catch {} + Stop-EdgeApp + Stop-Dsh + try { $ctx.ExitThread() } catch {} +}) +$menu.Items.Add($itemOpen) | Out-Null +$menu.Items.Add($itemExit) | Out-Null +$notify.ContextMenuStrip = $menu +$notify.Add_MouseDoubleClick({ + if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) { Open-Main } +}) + +$ctx = New-Object System.Windows.Forms.ApplicationContext +Log 'tray app running' +Open-Main +[System.Windows.Forms.Application]::Run($ctx) diff --git a/tools/dsh-app.ps1.README.md b/tools/dsh-app.ps1.README.md new file mode 100644 index 0000000..d577e10 --- /dev/null +++ b/tools/dsh-app.ps1.README.md @@ -0,0 +1,28 @@ +# dsh-app.ps1 — DeepSeek Harness 托盘应用壳(归档参考版) + +> 实际部署位置:`C:\Users\a1703\.dsh\bin\dsh-app.ps1`(桌面快捷方式 `DeepSeek Harness.lnk` 指向它)。 +> 本文件为当前版本的归档副本。**必须保留 UTF-8 BOM**(见下方"踩坑"), +> 机器相关路径($DshBin 的 npx 缓存位置)在新机器需修改。 + +## 功能 + +- 双击快捷方式:检测 3080 端口 → 未运行则拉起 `node web` → 托盘常驻 +- 托盘右键:打开主界面(Edge `--app` 独立窗口)/ 退出(停止服务并退出) +- 单实例保护(Mutex) + +## 2026-08-17 修复记录 + +1. **托盘图标报错 `操作成功完成`**:根因是 `dsh.ico` 帧偏移量少算了目录长度(6+count*16), + .NET `System.Drawing.Icon` 无法解析。修复:`tools/repair-ico.ps1`(检测首帧偏移为 0 的 + 损坏模式 → 所有帧偏移 + 目录长度 → 验证)。本机 `dsh.ico` 与 web 前端 `favicon.ico` 均已修复。 +2. **图标加载加固**:`$notify.Icon` 加载失败时回退(同目录其他 .ico → 系统图标)并写日志。 + +## 踩坑:UTF-8 BOM 必须保留 + +- Windows PowerShell 5.1 读取**无 BOM** 的 .ps1 按 ANSI(GBK) 解析 → 中文变乱码、 + 引号被吞 → 整个脚本解析失败(表现为托盘静默退出)。 +- 用 `edit`/`write` 工具改写本文件后必须补 BOM: + ```powershell + $c = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8) + [System.IO.File]::WriteAllText($f, $c, (New-Object System.Text.UTF8Encoding($true))) + ``` diff --git a/tools/repair-ico.ps1 b/tools/repair-ico.ps1 new file mode 100644 index 0000000..013218d --- /dev/null +++ b/tools/repair-ico.ps1 @@ -0,0 +1,47 @@ +# repair-ico.ps1 - Fix broken ICO files whose frame offsets omit the directory size. +# +# Background: some generators write ICO frame offsets starting at 0 (not accounting +# for the 6 + count*16 directory), which makes .NET Framework System.Drawing.Icon +# throw "The operation completed successfully". This script detects that pattern +# and adds the directory length to every frame offset. +# +# Usage: powershell -NoProfile -ExecutionPolicy Bypass -File repair-ico.ps1 [more.ico ...] +# Each file is backed up as .bak- before being rewritten. + +param([Parameter(Mandatory=$true, Position=0)][string]$IcoPath) + +Add-Type -AssemblyName System.Drawing + +foreach ($path in @($IcoPath)) { + $full = (Resolve-Path $path -ErrorAction SilentlyContinue).Path + if (-not $full -or -not (Test-Path $full)) { Write-Host "SKIP (not found): $path"; continue } + $bytes = [System.IO.File]::ReadAllBytes($full) + if ($bytes.Length -lt 22) { Write-Host "SKIP (too small): $path"; continue } + + $count = [BitConverter]::ToUInt16($bytes, 4) + if ($count -lt 1 -or $count -gt 64) { Write-Host "SKIP (odd frame count $count): $path"; continue } + $dirLen = 6 + $count * 16 + + # Detect the broken pattern: first frame offset is 0 (points at the directory itself) + $off0 = [BitConverter]::ToUInt32($bytes, 6 + 12) + if ($off0 -ne 0) { Write-Host "OK (first offset=$off0, no fix needed): $path"; continue } + + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $bak = "$full.bak-$stamp" + Copy-Item $full $bak -Force + + $b = [byte[]]$bytes.Clone() + for ($i = 0; $i -lt $count; $i++) { + $o = 6 + $i * 16 + 12 + $v = [BitConverter]::ToUInt32($b, $o) + $dirLen + [BitConverter]::GetBytes($v).CopyTo($b, $o) + } + [System.IO.File]::WriteAllBytes($full, $b) + + try { + $ic = [System.Drawing.Icon]::new($full) + $ok = "OK $($ic.Width)x$($ic.Height)" + $ic.Dispose() + } catch { $ok = "STILL FAILS: $($_.Exception.InnerException.Message)" } + Write-Host "FIXED: $full (backup $bak) verify: $ok" +}