Skip to content
dsh.fish
Bundle

dsh-web-manager

dsh web manager — Windows tray manager, WSL companion scripts, and the in-dsh runtime bridge, installable as a dsh profile plugin bundle.

Source
FYHC1
License
MIT
Updated
Updated 2 days ago

Readme

# dsh webui desktop

Windows 侧常驻托盘的 **DeepSeek Harness WebUI 管理器**:负责启动 dsh web、拉起 Edge 应用窗口、守护服务进程、常驻系统托盘,并接管窗口图标与窗口尺寸记忆。

- **跨 Windows / WSL2**:管理器本体只运行在 Windows;WSL 内的 dsh web 由管理器通过 `wsl.exe` 托管(v2.1)。
- **窗口只是视图**:关闭浏览器窗口不会结束 dsh web;点托盘图标随时重新唤起窗口。
- **图标与尺寸**:以官方 `DeepSeek Harness.ico` 通过 `WM_SETICON` 设置到应用窗口并持续维持;窗口尺寸按记忆复现。

> 与旧项目的关系:本仓库为 v2.0+ 的独立实现(独立交付),旧项目
> `FYHC1/dsh-webui-installer`(v1.4.x 脚本链路)收尾于 v1.4.2,不再大改。

## 原汁原味的独立浏览器窗口

管理器打开的不是"浏览器里的一个标签页",而是 **Edge 应用窗口(`--app`)**——一套独立的、
无地址栏/标签页/扩展干扰的 DeepSeek Harness 专用窗口,从观感到行为都像原生桌面应用:

- **官方鲸鱼图标**:任务栏与窗口图标均为官方 `DeepSeek Harness.ico`
  (`WM_SETICON` 32/16px + AUMID 任务栏身份),与 dsh 官方 WebUI 完全一致;
- **专属浏览器数据目录**:每个实例一个独立 Edge profile
  (`%LOCALAPPDATA%\dsh-web-manager-browser-<port>`),与日常浏览器彻底隔离,互不干扰;
- **记住你的窗口**:尺寸/位置按实例记忆(`Window.Size/Position`),关闭再打开原样复现;
- **弹出即是正确尺寸**:几何钩子(WinEventHook)在窗口**显示之前**完成尺寸设置——实测
  从第一帧起就是记忆尺寸、全程零跳变(Edge 本身只认自己的默认 945×1020,`--window-size`/
  `--start-minimized`/Preferences 全部无效,排除法矩阵见 docs/TESTING.md);
- **快速唤起**:关窗后预热驻留热进程,重开窗口 ~0.7–1.1s;无 URL 门控,窗口立即打开;
- **浏览器回退(v3.7)**:默认使用 Edge;本机未安装 Edge 时自动改用 **Google Chrome**,
  再不行用开源 **Chromium**(三者同为 Chromium 内核,`--app` 独立窗口、几何钩子、
  图标与隔离 profile 行为完全一致);
- **窗口只是视图**:关窗不停服务,托盘/快捷方式随时一键重新唤起。

## 快速打开窗口

| 入口 | 操作 |
|---|---|
| Windows 桌面快捷方式 | `DeepSeek Harness WebUI (win).lnk` / `(wsl).lnk`(dsh 插件自动创建/维护,双击即开对应后端独立窗口) |
| WSL 命令行 | `dsh-webui`(默认 WSL 端)/ `dsh-webui windows`(详见下文「WSL 命令:dsh-webui」) |
| 托盘 | 左键单击图标、菜单「打开窗口」、实例子菜单「打开窗口」 |
| 命令行动作 | `dsh-web-manager.exe open` / `open windows` / `open wsl` |

所有入口都**按需启动**:只拉起目标后端的服务与窗口(v3.6 起不再顺带启动另一端);
某后端**尚未配置实例**时(从未添加或已被删除),入口会自动生成一个默认实例并启动,
无需先手动「添加实例」。

## 用法

### 方式一:作为 dsh 插件安装(推荐)

本仓库本身就是一个可安装的 dsh 插件包 **`dsh-web-manager`**(运行时桥 + Windows 托盘
exe + WSL 伴生脚本打包在一起)。一条命令装入 dsh profile,之后 dsh 每次启动都会自动
确保托盘 exe 与桌面快捷方式(详见「Runtime Bridge」与「桌面快捷方式与托盘共享」两节):

```bash
# 从 GitHub 安装(推荐)
dsh plugin --profile web add github:FYHC1/dsh-web-manager
# 或从本地仓库安装(开发/离线场景)
dsh plugin --profile web add file:/path/to/dsh-web-manager
```

装完**重启 dsh**(重新运行 `dsh web`)即生效:托盘随 dsh 常驻;桌面上自动出现
`DeepSeek Harness WebUI (win)/(wsl).lnk` 快捷方式与 WSL 端 `dsh-webui` 命令,
双击/执行即可快速打开独立窗口。

### 方式二:Windows 手动安装(不装插件)

Windows PowerShell:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\Install.ps1
```

安装内容:

- 编译或复制 `dist\dsh-web-manager.exe`(.NET Framework 4.8,Win10/11 自带运行时)到 `%LOCALAPPDATA%\dsh-web-manager\app\`
- 创建桌面/开始菜单快捷方式(指向 `dsh-web-manager.exe open`)
- 冒泡式初始化共享配置 `%USERPROFILE%\.dsh-webui\config.json`(Windows 与 WSL 两侧可见)
- 可选:开机自启(仅托盘、不弹窗)

首次双击快捷方式 → 管理器以单实例启动:

1. 探测端口(默认 3080,被非 dsh 进程占用时自动顺延 3080+n,可配置关闭)
2. 未监听 → 隐藏拉起 `dsh web --port N` 并接管(managed);已监听 → 附着(attached,不抢不杀)
3. 打开 Edge `--app=http://127.0.0.1:N`(沿用记忆的窗口尺寸/位置,独立浏览器数据目录)
4. 常驻托盘:关窗不结束服务,点托盘/菜单「打开窗口」随时重新唤起
5. 心跳循环:端口健康、图标维持(`WM_SETICON` 32/16px)、窗口尺寸采集、崩溃检测与退避重启

托盘右键菜单:打开窗口 / 重启服务 / 后端(Windows 本机 ⇄ WSL)/ 退出(停服务)/ 开机自启 / 状态。

## WSL 后端(v2.1)

菜单「后端 → WSL」或 `dsh-web-manager.exe "backend wsl"` 即可把 dsh web 托管进 WSL:

- **wsl-start.sh**:WSL 侧自愈启动器(物化到 `~/.dsh-webui/`),自带崩溃循环 + pidfile + TERM 陷阱
- **发行版自动探测**:`wsl --list` 过滤辅助发行版(Docker/Rancher/Podman),
  优先级:配置 `wslDistro` > 唯一候选 > 运行中 > 默认 > 名称打分
- **所有权模型**:管理器拉起的 = managed(完整生命周期/守护/退避重启);
  外部已在跑的 WSL dsh = attached(只监控,不抢不杀)
- **端口策略**:每后端独立端口记忆(`Port` / `WslPort`),非 dsh 占用自动顺延并写回
- **健康探测**:Windows 端口探测 + WSL 侧 `ss` 解析双通道 —— 即使 localhostForwarding
  关闭,守护/状态也不误判(dsh 出于安全拒绝 `--host 0.0.0.0`,故服务只绑 127.0.0.1,
  forwarding 关闭时 Windows 无法访问,打开窗口会给出明确提示)
- **systemd 托管(v3.0)**:托盘「后端 → WSL 服务模式 → systemd」或 `wslmode systemd`。
  管理器生成 `~/.config/systemd/user/dsh-web-<port>.service`(Restart=on-failure 自愈、
  journald 日志、随登录拉起),前台运行 dsh;`systemd` 不可用(未开
  `/etc/wsl.conf [boot] systemd=true`)时自动回退 wrapper 模式。
- **双向互装**:`wsl-bootstrap.sh`(WSL→Windows)检测 manager 未运行则静默拉起,
  未安装则经共享目录 `~/.dsh-webui/wsl-bootstrap/Install.ps1` 静默安装;
  安装器会把 WSL 伴侣脚本物化进默认发行版(bootstrap.lock 先到先得防竞态)

测试沙箱:设置环境变量 `DSH_WEB_MANAGER_HOME=<目录>` 可把 config/日志/mutex/管道整体隔离,
用于并行验证而不影响真实安装。

## 多实例(v3.0)

`config.json` 的 `Instances` 数组可同时托管多个独立实例(Windows 与 WSL 混用、不同端口)。
数组留空则回退到传统单实例字段(`Port` / `WslPort` / `BackendType` 等)。

```json
{
  "Instances": [
    { "Id": "windows", "BackendType": "windows", "Port": 3081, "Profile": "web", "Enabled": true },
    { "Id": "wsl", "BackendType": "wsl", "WslPort": 3080, "WslDistro": "FedoraLinux",
      "WslServiceMode": "systemd", "Profile": "web", "Enabled": true }
  ]
}
```

每个实例独立管理:端口、窗口尺寸/位置、图标、崩溃守护、Runtime Bridge 状态。
托盘「实例」菜单列出全部实例(打开窗口 / 重启服务 / 状态),并支持**添加实例**与**删除实例**
(添加对话框可下拉选择 WSL 发行版,自动探测真实发行版,无需手填)。

## Runtime Bridge(v3.0 → 可安装的 dsh 插件包)

本仓库本身就是一个可安装的 dsh 插件包 **`dsh-web-manager`**:它把运行时的桥插件
(`lib/index.js` + `cordis.patch.yml`)和 Windows 托盘 exe、WSL 伴生脚本打包在一起。
管理器借桥拿到 dsh 的**权威状态**(版本、node、运行时长、pid、端口)并做**优雅停止**
(先 SIGTERM 再 kill),而不是只看端口猜测。

协议(line-delimited JSON,监听 `127.0.0.1:<webPort+100>`):
`ping` / `getStatus` / `getRuntimeInfo` / `shutdown`,请求形如
`{"v":1,"method":"getRuntimeInfo","token":"<BridgeToken>"}`。

**安装**(命令同「用法 · 方式一」;WSL 与 Windows 两侧 profile 各自执行一次):

```bash
# 从本地仓库安装
dsh plugin --profile web add file:/path/to/dsh-web-manager
# 或从 GitHub 安装
dsh plugin --profile web add github:FYHC1/dsh-web-manager
```

该命令会:

1. 用 pnpm 把 `dsh-web-manager` 装入 profile 的 `node_modules/`,并因
   `package.json` 的 `dsh.bundle.patch` **自动追加到 `dsh.profile.bundles`**,
   桥插件随之自动参与组合(不再需要手动拷贝目录 + 改 `cordis.patch.yml`)。
2. 托盘 exe 为**显式一步**(避开 pnpm 对构建脚本的默认拦截):Windows 侧在 profile
   目录跑一次
   `node node_modules\dsh-web-manager\scripts\install-tray.mjs`(等价于
   `powershell -ExecutionPolicy Bypass -File <profile>\node_modules\dsh-web-manager\dist\Install.ps1`:
   安装托盘 exe 到 `%LOCALAPPDATA%\dsh-web-manager\app`、初始化配置、拉起托盘)。

**从旧的手动注入迁移**:若 profile 之前手动拷过 `node_modules/dsh-runtime-bridge`
并在 `cordis.patch.yml` 里手动 `insert` 了 `dsh-runtime-bridge`,安装后请删除那段
insert 与手拷目录,避免出现两套桥(双桥会抢同一个 `DSH_BRIDGE_PORT`)。

管理器启动 dsh 时会自动注入 `DSH_BRIDGE_PORT`(=port+100)、`DSH_BRIDGE_TOKEN`
(config 的 `BridgeToken`,首次自动生成)、`DSH_PROFILE`、`DSH_WEB_PORT`。
托盘状态随之显示 `运行中 (…) · dsh <版本> · node <版本> · 运行 <时长>`。

## 桌面快捷方式与托盘共享(自动)

插件 `apply()` 时(dsh web 每次启动)会**幂等**地做两件事:

1. **确保共享托盘**:若 `%LOCALAPPDATA%\dsh-web-manager\app\dsh-web-manager.exe`
   已存在则直接复用(共用同一个托盘,不再装第二份);否则把随包的
   `dist/dsh-web-manager.exe` + 图标拷贝过去。用户配置/状态在
   `%USERPROFILE%\.dsh-webui\`,不会被覆盖。
2. **创建/修正桌面快捷方式**(按安装平台区分):
   - Windows 端 dsh 安装 → `DeepSeek Harness WebUI (win).lnk` → 管理器 `open windows`
   - WSL 端 dsh 安装 → Windows 桌面 `DeepSeek Harness WebUI (wsl).lnk` → 管理器 `open wsl`

   快捷方式目标都是同一个共享托盘 exe;若发现旧版遗留的 `wscript.exe`/`.vbs`
   快捷方式(dsh-webui-installer 时代产物)会自动替换为指向托盘的新快捷方式。

管理器新增控制动作 `open windows` / `open wsl`(`dsh-web-manager.exe open windows`
或 `open wsl`):打开指定后端的窗口,与快捷方式一致。双击快捷方式时若托盘未运行会
先冷启动托盘,再打开对应后端窗口;若托盘已在运行则直接转发。

**按需启动(v3.6)**:快捷方式只拉起目标后端——`open wsl` 不会顺带启动 Windows
端服务,反之亦然;托盘启动(开机自启)本身不启动任何服务,已在运行的服务由心跳
自动附着(如 systemd 单元),未运行的服务在打开其窗口时才启动。某后端**尚未配置
实例**时(例如实例被删除或从未添加),快捷方式会自动按后端记忆端口生成一个默认
实例并启动,无需手动「添加实例」。

## 更新机制(v3.1)

托盘「更新」菜单同时管理 **dsh** 与 **dsh web manager** 两个软件,互不影响:

- **检查 dsh 更新**:24 小时节流,经 npmmirror 查询 `@deepseek-ai/dsh` 最新版,与运行中 dsh 版本
  (优先取 Runtime Bridge 的 `dshVersion`)比对,有新版才弹通知。
- **更新 dsh**:一键 `npm install -g @deepseek-ai/dsh@latest`(走 npmmirror),完成后提示新版本号。
- **检查管理器更新**:查询 GitHub Releases(`FYHC1/dsh-web-manager/releases/latest`),与当前
  管理器版本比对,弹通知告知结果(无发布 / 已最新 / 发现新版)。
- **更新 dsh web manager**(自更新):下载最新 release 中的 `dsh-web-manager.exe` →
  校验文件版本 → 生成脱离式更新脚本 → 退出托盘(**不停止任何 dsh 服务**)→ 脚本等 exe
  解锁后替换并自动以托盘模式重启 → 管理器重新附着各实例。全程 dsh 保持运行。
  - 更新包下载到 `%LOCALAPPDATA%\dsh-web-manager\update\`,更新过程记录在
    `%LOCALAPPDATA%\dsh-web-manager\logs\manager-update.log`。
  - 启动时也会做一次 24h 节流的管理器版本检查,发现新版才弹通知。
- **更新 dsh 插件包**:一键刷新 dsh profile 里的 `dsh-web-manager` 插件(bridge + 快捷方式
  脚本)。自动读取 profile `package.json` 中记录的安装来源(如 `file:/home/.../dsh-web-manager`),
  执行 `dsh plugin --profile <p> remove` + `add <spec>`,重启 dsh 后生效。来源可通过配置
  `PluginUpdateSpec` 覆盖。

### 发布新版本(给维护者)

自更新依赖 GitHub Release 附带 `dsh-web-manager.exe` 资产(tag 形如 `v3.0.1`,跳过 prerelease):

```bash
# 在仓库根目录(WSL 或 Windows 均可,需要 gh 已登录)
gh release create v3.0.1 dist/dsh-web-manager.exe --title "dsh web manager v3.0.1" --notes "更新说明"
```

发布后,用户点托盘「更新 dsh web manager」即可自动升级。

## 状态与配置

`config.json` 关键字段(`%USERPROFILE%\.dsh-webui\config.json`):

| 字段 | 说明 | 默认 |
| --- | --- | --- |
| `Port` | Windows 后端首选端口 | `3080` |
| `AutoFallback` | 非 dsh 占用时自动顺延空闲端口 | `true` |
| `WindowBackend` | v3.8 窗口后端:`auto`(有 WebView2 运行时即内嵌,否则 Edge)/ `webview2`(强制内嵌,缺失时回退并气泡提示)/ `edge`(原 Edge `--app` 路径) | `auto` |
| `DshCommand` | dsh 命令绝对路径覆盖(离线包用;留空走 PATH/常见布局探测) | `""` |
| `DataDir` | Edge 独立浏览器数据目录(留空用默认) | `""` |
| `CloseStopsService` | 关闭窗口时同时停止服务(旧行为开关) | `false` |
| `ExitKeepService` | 退出托盘时保留服务 | `false` |
| `StopAttached` | 「关闭实例/退出」是否停止附着的外部服务;`false` = 纯窗口管理模式(只解除附着) | `true` |
| `AutoStart` | 开机自启(托盘不弹窗) | `false` |
| `BackendType` | `windows` / `wsl` | `windows` |
| `WslPort` | WSL 后端首选端口 | `3080` |
| `WslDistro` | 指定 WSL 发行版(留空自动) | `""` |
| `WslServiceMode` | WSL 服务模式:`wrapper`(自愈脚本)/ `systemd`(unit) | `wrapper` |
| `Profile` | dsh profile 名 | `web` |
| `Window.Size` / `Window.Position` | 记忆的窗口尺寸与位置 | 空(Edge 默认) |
| `Instances` | v3.0 多实例列表(Id/Profile/BackendType/Port/WslPort/WslDistro/WslServiceMode/Enabled);空 = 单实例回退 | `null` |
| `BridgeToken` | Runtime Bridge 共享密钥(首次自动生成) | `""` |
| `LastWslDistro` | 记忆上次成功使用的 WSL 发行版 | `""` |
| `LastVersionCheckUtc` / `LastKnownLatest` | dsh 更新检查节流时间戳 / 已知最新版本 | `""` |
| `LastManagerCheckUtc` / `LastKnownManagerLatest` | 管理器更新检查节流时间戳 / 已知最新版本 | `""` |
| `ManagerUpdateApi` | 管理器 Release API 覆盖地址(留空用官方 GitHub;测试/镜像用) | `""` |
| `PluginUpdateSpec` | 插件包安装来源覆盖(留空自动从 profile 的 package.json 探测) | `""` |

## WSL 命令:dsh-webui

Linux 端注册了 `dsh-webui` 命令(插件在 WSL 启动时自动安装到 `~/.local/bin/`),用于从
WSL 里手动打开**独立窗口**——它把请求转发给共享的 Windows 托盘管理器,由管理器拉起
Edge `--app` 独立窗口(无需记忆端口/URL):

```bash
dsh-webui            # 打开 WSL 后端独立窗口(默认)
dsh-webui wsl        # 同上
dsh-webui windows    # 打开 Windows 后端独立窗口
```

未安装插件时手动注册:`install -D -m 755 scripts/dsh-webui ~/.local/bin/dsh-webui`。

## 开发

```powershell
powershell -ExecutionPolicy Bypass -File scripts\Build.ps1   # 系统 csc.exe 编译 + 测试程序
```

要求:任意 Windows 10/11(自带 .NET Framework 4.8 与 C# 编译器 `csc.exe`),无需安装 Visual Studio。

## 里程碑

- **v3.8.1**:离线包预装 **dsh market**(插件商城,`dsh plugin --profile web add dshmarket`)——
  用户开箱即可在商城里下载所需插件,最小化安装 + 使用便捷(`-ExtraPlugins` 可自定义预装列表)✅ 已交付
- **v3.8**:WebView2 内嵌窗口后端(窗口归属管理器进程,任务栏鲸鱼图标不受
  「合并任务栏按钮」影响;`WindowBackend` 配置 + 运行时缺失自动回退 Edge)+
  离线安装包 `dsh-offline-bundle`(Build-Bundle / Install-Offline / Uninstall-Offline,
  便携 Node + dsh 包树 + 预烘焙 profile,断网首启验收)+ `DshCommand` 配置直连 ✅ 已交付
- **v2.0**:Windows 后端全量——托盘、窗口、图标、尺寸、守护、配置、日志、迁移、A–I 验证
- **v2.1**:WSL 后端(wsl-start.sh 自愈托管 + distro 自动探测 + attached/managed 所有权 +
  双向互装 bootstrap + 每后端端口记忆)✅ 已交付,J–Q 真机矩阵通过
- **v2.2**:后端感知健康探测(forwarding 关闭时守护不误判)+ 窗口 URL 策略(不可达时提示
  而非打开打不开的窗口)+ Error/Starting 残留清理 + 可中断 sleep + 墙钟超时 ✅ 已交付,
  R–V 真机矩阵通过
- **v3.0**:systemd 托管(W–Z 矩阵)+ Runtime Bridge 插件(权威状态/优雅停止,ping/
  getStatus/getRuntimeInfo/shutdown 协议)+ 多实例(Instances 数组,Windows+WSL 同开)+
  更新机制(24h 节流版本检查 + 托盘一键更新)✅ 已交付
- **v3.0 P1–P2 增强**:Runtime Bridge 状态接入托盘(dsh/node 版本、运行时长显示;检查更新联动)+
  Windows 侧 Runtime Bridge + 多实例「添加/删除实例」托盘 UI + FindAppWindow WMI 卡死修复 ✅ 已交付
- **v3.0 P2 后续(UI 优化)**:托盘顶部横向 Windows/WSL 激活按钮(记忆当前后端)+
  状态项两行显示(运行中 / 未启动·未知版本)+ 端口跨后端全局独占 +
  实例菜单「关闭窗口」+ 每实例独立浏览器 profile(多实例窗口不再合并)+
  菜单底部锚定(切换后端只伸缩顶部)+ 面板美化(浅色主题)✅ 已交付
- **浏览器标签页根因修复**:dsh web 启动时默认调用系统浏览器打开 URL(日志提示
  `pass --no-open to disable`)→ 三处启动命令统一加 `--no-open`(Windows / wsl-start.sh /
  wsl-systemd-start.sh),浏览器不再冒 dsh 标签,只保留 manager 拉起的独立 `--app` 窗口 ✅ 已修复
- **默认启动后端**:托盘「默认启动后端」子菜单(Windows 本机 / WSL)决定 manager 以
  `open` 启动时拉起的后端窗口;启动时自动关闭其他实例的残留窗口 ✅ 已交付
- **默认启动后端勾选指示修复**:根因是 .NET Framework 4.8 的 `ShowCheckMargin` 默认关闭,
  且子菜单又设了 `ShowImageMargin=false` → 原生勾选符号永远不会绘制,「默认启动后端」与
  「WSL 服务模式」的当前选中项看起来与未选中完全一样。修复:仅对包含勾选项的子菜单开启
  `ShowCheckMargin`(原生勾选列),无勾选项的子菜单(实例/更新)保持紧凑;
  主菜单 256×423 布局、状态项两行高度均不变 ✅ 已修复
- **开机自启灰色标识**:主菜单不开勾选列(会加宽所有项)——「开机自启」开启时该项背景
  改为灰色阴影(`#E6E6E6`),关闭时恢复白底;以 config 为准同步,`ToggleAutoStart` 失败
  也能自愈;悬停仍显示淡蓝高亮 ✅ 已交付
- **悬停高亮卡顿修复**:点击顶部 Windows/WSL 切换按钮后,焦点被托管按钮夺走导致菜单
  不再跟踪悬停 → 切换后把焦点还给菜单(`RefocusMenu`),高亮恢复即时 ✅ 已修复
- **v3.1 管理器自更新**:GitHub Releases 查询/比对(跳过 prerelease)+ 下载校验 +
  脱离式更新脚本(等 exe 解锁 → 替换 → 托盘重启,不停止 dsh)+ 托盘「检查管理器更新 /
  更新 dsh web manager」+ 启动时 24h 节流检查 + `updatemanager` 控制动作 ✅ 已交付
- **v3.1 插件包更新 + dsh-webui**:托盘「更新 dsh 插件包」(自动探测 profile 安装来源,
  `dsh plugin remove/add` 一键刷新,提示具体包名@版本与来源)+ Linux 端 `dsh-webui`
  命令(转发给共享管理器打开独立窗口,插件自动注册到 `~/.local/bin`)✅ 已交付
- **v3.1 修复四连**:①退出时立即隐藏托盘图标(`Exiting` 事件 → `NIM_DELETE`,不再出现
  幽灵图标/需点两次退出);②独立窗口尺寸记忆修复(根因:`Launch` 读管理器级 `Window`,
  而 `CaptureSize` 写入实例级 `Window`,多实例下尺寸永不生效 → 改用实例级窗口配置);
  ③状态栏严格跟随顶部 Windows/WSL 切换按钮(切换即刷新,只显示所选端,无匹配显示
  「未选择后端」);④插件包更新提示具体更新的包与来源 ✅ 已交付
- **v3.1 窗口尺寸记忆深度修复**:实例级配置修复后仍不生效的**真正根因**——Edge 150
  完全忽略 `--window-size`(fresh profile 实证:`--window-size=1500x800` 仍开 945×1020
  默认尺寸),始终按自己保存的边界打开 `--app` 窗口。最终方案:**正常启动(不最小化,
  窗口一出现即可见)→ 直接轮询启动进程 `MainWindowHandle`(150ms,无 WMI 缓存)→
  窗口出现后 ~0.2s 内 `SetWindowPos` 应用记忆尺寸**——窗口弹出快(Launch→调整 ~1.0s,
  其中 ~0.8s 是 Edge 冷启动本身)、无最小化延迟、无隐藏闪烁;若窗口意外最小化则走
  隐藏→调整→显示兜底。另保留:启动时快照记忆几何、CaptureSize 启动后 6 秒保持期
  (阻断覆盖循环)、启动前清除残留后台进程。实测:打开/关窗重开均以记忆的
  1665×1020 出现,配置稳定 ✅ 已交付

## 许可

MIT
## 任务栏图标说明(Windows)

**v3.8 起,默认窗口后端为 WebView2 内嵌窗口**:WebUI 窗口直接属于管理器进程
(`WebViewForm`,每个实例一个 STA UI 线程),任务栏按钮的图标与分组都跟随管理器
自带的鲸鱼图标——即使任务栏开启「合并任务栏按钮」也不会被 Edge 进程图标顶掉。
每实例窗口仍写入独立 AUMID(`DeepSeekHarness.WebUI.<port>`,对自有窗口
`SHGetPropertyStoreForWindow` 正常生效),多实例不会互相合并。

- 需要WebView2 运行时(Win11 / 新 Win10 已内置;缺失时管理器自动回退 Edge 模式并气泡提示)。
- 构建 WebView2 后端需要 `lib\` 下的 SDK 程序集,首次执行
  `powershell -ExecutionPolicy Bypass -File lib\Get-WebView2.ps1` 自动下载解包。
- 强制走旧路径:`config.json` 里 `"WindowBackend": "edge"`。

**Edge 模式的旧限制(回退时生效)**:窗口图标由管理器通过 `WM_SETICON` 持续设置
(32/16px,官方 `DeepSeek Harness.ico`),验证方式:`WM_GETICON` 像素采样与官方图标一致。
但若任务栏启用了「合并任务栏按钮」(TaskbarGlomLevel 0/1,Win10 生效),多个同进程窗口
(如 Edge 的多个 `--app` 窗口)会被合并成单个按钮并显示**进程图标(Edge)**,即使每个窗口
自身的图标都是 DeepSeek 鲸鱼。Edge 模式下要显示鲸鱼图标,请将任务栏设置为「从不合并」:
- 设置 → 个性化 → 任务栏 → 「合并任务栏按钮」→「从不合并」
- 或注册表:`HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced` 下
  `TaskbarGlomLevel = 2`,然后重启资源管理器(explorer)生效。

> 为什么 Edge 模式无法在代码层解决?Chromium 的 `--app` 窗口在 Windows 上
> 不接受外部进程写入窗口 AppUserModelID(`SHGetPropertyStoreForWindow` 的
> `SetValue` 对 Chromium 窗口抛 `0x80070002`,普通窗口正常)。AUMID 由页面
> manifest 决定,外部无法覆盖。这正是 v3.8 引入 WebView2 内嵌窗口的原因:
> 窗口归属管理器进程后,图标/分组问题从根上消失。

## 离线安装包(dsh-offline-bundle)

为无外网(或仅内网)的 Windows x64 目标机准备的一体化离线发行包:
便携 Node(官方 win-x64 zip)+ `@deepseek-ai/dsh` npm 包树 + 预烘焙的 `~/.dsh`
profile(已装本管理器插件)+ 管理器 dist。目标机一条命令完成安装,首次启动不联网。

```powershell
# 构建端(联网的 Windows 机器,仓库根目录):
powershell -ExecutionPolicy Bypass -File scripts\Build-Bundle.ps1
#   产出 bundle-out\dsh-offline-bundle\(node\ dsh\ profile-web\ dsh-web-manager\
#   + Install-Offline.ps1 + Uninstall-Offline.ps1 + bundle.json)
#   profile 烘焙有硬性验收:断网(死代理)启动通过才打包,否则直接失败不出包。

# 目标端(离线机器,解压后在该目录执行):
powershell -ExecutionPolicy Bypass -File Install-Offline.ps1              # 默认:装齐 + 启动托盘
powershell -ExecutionPolicy Bypass -File Install-Offline.ps1 -AutoStart   # 另加开机自启
powershell -ExecutionPolicy Bypass -File Uninstall-Offline.ps1            # 卸载(默认保留 ~/.dsh)
powershell -ExecutionPolicy Bypass -File Uninstall-Offline.ps1 -PurgeProfile  # 连 profile 一起删
```

行为要点:

- 安装位置:`%LOCALAPPDATA%\dsh-bundle\`(node + dsh 包树 + `bin\dsh.cmd` shim,
  shim 用绝对路径指向捆绑 node,不依赖全局 Node/npm);管理器照常装到
  `%LOCALAPPDATA%\dsh-web-manager\app`,并写入 `config.json` 的 `DshCommand`
  直连捆绑 shim(PATH 仅作为补充,可用 `-NoPath` 跳过)。
- profile:拷贝预烘焙的 `~/.dsh`;已存在时只补缺文件,**绝不覆盖**
  `.credentials.yaml`(API Key 留占位,在 WebUI 里填)。
- **v3.8.1**:烘焙的 profile 预装 **dsh market**(插件商城)——开箱即可在 WebUI
  的商城里安装更多插件;构建时用 `-ExtraPlugins`(Windows)/ `--extra-plugins`(WSL)
  自定义预装列表,空串则不预装。预装后仍过断网首启验收门。
- WebView2 运行时缺失不阻塞安装:管理器自动回退 Edge 窗口模式(见上)。
- 幂等升级:重跑新版 `Install-Offline.ps1` 即升级(robocopy 镜像 + 管理器版本比较
  不降级 + 配置保留);离线机的升级 = 拿新版 bundle 重跑安装器。
- `bundle.json` 记录各组件版本,供日志与升级比对(样例见仓库根 `bundle.example.json`)。
- 测试:两个脚本都识别 `DSH_WEB_MANAGER_HOME` 沙箱(管理器文件、profile、共享配置
  全部落沙箱,不碰真实用户环境;见 TESTING.md 纪律)。

### WSL 侧(bundle\wsl\ 载荷 + deb 包)

离线包可以内嵌 WSL 载荷(Linux 版便携 Node + dsh 树 + 预烘焙的 Linux `~/.dsh` profile,
构建时同样过断网启动验收门):

```bash
# 构建端(Linux 机器 / CI ubuntu runner / 任意 WSL 发行版内):
bash scripts/Build-Bundle-Wsl.sh                 # -> bundle-out/bundle-wsl/
# Windows 侧构建时合入:
powershell -File scripts\Build-Bundle.ps1 -WslPayloadDir bundle-out\bundle-wsl
# 目标机:Install-Offline.ps1 检测到 bundle\wsl\ + wsl.exe 即自动装 WSL 侧
#   (发行版自动探测:优先运行中的非辅助发行版;-WslDistro 显式指定,-SkipWsl 跳过)
```

WSL 侧落地内容(发行版内 `~/.dsh-bundle/` + `~/.local/bin/dsh` + profile 补缺 +
`~/.dsh-webui/` 伴生脚本),由 `scripts/wsl/install-wsl.sh` 完成——它同时是 deb/rpm 包的
post 脚本核心。**两种 Linux 包都带 `-wsl` 后缀**(`dsh-bundle-wsl`),与未来适配裸机
Linux 的 `dsh-bundle`(原生包)区分:

```bash
# deb(Debian/Ubuntu 系 WSL 发行版):
bash scripts/Build-Deb.sh --payload bundle-out/bundle-wsl   # -> dsh-bundle-wsl_<ver>_amd64.deb
sudo apt install ./dsh-bundle-wsl_*_amd64.deb               # /opt/dsh-bundle-wsl + postinst 接线默认用户
dsh-bundle-wsl-install                                      # 任意用户重跑用户级安装

# rpm(Fedora 系 WSL 发行版,本机即此场景;需 rpm-build):
bash scripts/Build-Rpm.sh --payload bundle-out/bundle-wsl   # -> dsh-bundle-wsl-<ver>-1.x86_64.rpm
sudo dnf install ./dsh-bundle-wsl-*.rpm                     # 同样 %post 接线默认用户
```

### CI:GitHub Actions 一键出安装包

`.github/workflows/offline-bundle.yml`:打 tag(`v*`)触发(也可手动 dispatch)。

- **wsl-payload**(ubuntu):`Build-Bundle-Wsl.sh` 出 WSL 载荷;随后 **deb**(同 job,
  `dsh-bundle-wsl_<ver>_amd64.deb`)与 **rpm**(fedora 容器 job,
  `dsh-bundle-wsl-<ver>-1.x86_64.rpm`)分别打包
- **windows-installer**(windows):下载载荷合入 bundle → `Build-Bundle.ps1` → Inno Setup
  编译出**单文件 setup EXE**(`dsh-offline-bundle-setup_<ver>_x64_<tag>.exe`,内嵌全部载荷,
  安装完自动跑 `Install-Offline.ps1 -WithWsl`,卸载走 `Uninstall-Offline.ps1`)
- **release**:tag 推送时自动发布 GitHub Release(exe + deb + rpm + bundle zip)

本地也能出 EXE:装 [Inno Setup 6](https://jrsoftware.org/isinfo.php) 后
`ISCC /DBundleDir=..\bundle-out\dsh-offline-bundle /DMyVersion=3.8.0 packaging\windows-installer.iss`。

Install

dsh plugin --profile web add github:FYHC1/dsh-web-manager

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source