fix: 修复 dsh.ico 帧偏移 bug + 托盘图标加固 + BOM 经验

- tools/repair-ico.ps1: 检测并修复 ICO 帧偏移量未计目录长度的损坏模式
- assets/dsh.ico: 已修复(原文件偏移全错,.NET Icon 无法加载)
- tools/dsh-app.ps1: 归档加固版托盘脚本(图标加载兜底)
- tools/dsh-app.ps1.README.md: 修复记录 + UTF-8 BOM 踩坑
This commit is contained in:
江晨
2026-08-17 12:53:53 +08:00
parent 3e364d576d
commit 096e5af69b
5 changed files with 192 additions and 0 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+117
View File
@@ -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)
+28
View File
@@ -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 <dsh bin.js> 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)))
```
+47
View File
@@ -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 <file.ico> [more.ico ...]
# Each file is backed up as <file>.bak-<timestamp> 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"
}