# dsh-auto-open-web

> English: [README.en.md](README.en.md)

dsh web profile 启动后自动打开独立应用窗口（或网页标签页）的常驻插件，并在
设置界面提供配置表单（手动维护浏览器位置等）。**最新版**的配置入口是
「插件管理」页里本插件**那一行**的配置页（dsh ≥ 0.1.6，含 0.2.x）；
**最新 rc 线（0.1.5-rc.x）**为 **设置 → 插件配置**卡片。

> **0.2.1-rc.3 适配（0.1.30）**：清单新增 `engines.dsh`（`>=0.1.5-rc.1 <0.3.0`，
> 0.2.1-alpha.3 起优先于 peer 判定、旧版读 peer）；bundle 配置层同步到 0.2.1 的
> `web-runtime` 键集（补上 `publicUrl`）；宿主侧把 dsh ≥0.1.7 的 **Volatile 引用**
> 解包后再归一化（此前 `.volatile()` 字段读到的都是包装对象，所有设置项实际
> 一直按默认值生效）；GUI 地址改用 `webApp.getAdvertisedUrl()`（`--public-url`、
> TLS、非回环绑定都正确）。

## 行为

启动时（HTTP 服务绑定完成、取得实际监听端口后），按 `windowKind` 选择窗口类型：

1. **WebView2 宿主**（`windowKind: webview2`，默认，仅 Windows）：启动随包分发的
   `DshAppWindow.exe`（WinForms + WebView2，独立进程，无标签栏/地址栏），
   **直接加载 GUI 根地址**（无 iframe、无包装页、无注入脚本）。
   **任务栏/窗口图标 = DSH 图标**（窗口由宿主进程所有，直接设置 Form.Icon，
   不受浏览器任务栏身份限制）。**随 DSH 退出**（宿主监视父进程 PID）。
   **记忆窗口大小/位置/最大化状态**（`%LOCALAPPDATA%\DeepSeekHarness\window-state.json`，
   关闭时保存、启动时恢复；显示器布局变化时回退居中）。
2. **浏览器应用窗口**（`windowKind: browser`）：`--app` **专用 Edge/Chrome 实例**
   （`--user-data-dir=~/.dsh/<browser>-app-profile`，独立进程树与存储，
   **不与正常浏览器页面共用进程/Cookie/缓存**；`--no-first-run` 跳过首启欢迎页）。
   **随 DSH 退出（含强杀）**：浏览器实例加入 **Job Object**
   （`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`，koffi 驱动
   `JOBOBJECT_EXTENDED_LIMIT_INFORMATION` 结构，实测 144 字节）；DSH 无论
   正常退出还是被强杀（`taskkill /F`、崩溃、关机等），Windows 内核随作业最后
   一个句柄关闭自动结束作业内全部进程——专用实例整个进程树随之消亡，无需
   依赖任何退出事件。另有两层兜底：DSH 正常退出时 `process 'exit'` 结束该
   专用实例的整个进程树（仅匹配我们自己的 user-data-dir，不影响正常浏览器）；
   DSH 被强杀后的残留实例由下次启动前预清理。
3. **默认浏览器打开（最后兜底）**：所选类型不可用时（宿主缺失/浏览器找不到/
   非 Windows 等）→ **降级为官方同款的默认浏览器交接**——把 URL 交给操作系统
   默认浏览器（普通标签页），保证用户至少能打开 GUI。实现与 DSH 核心
   （web-app bundle）启动时打开网页的方式一致：优先用 DSH 部署自带的
   `open` 包（win32 = PowerShell Start，darwin = `open`，linux = `xdg-open`），
   `open` 包不可用时退回平台原生拉起（win32 = `cmd /c start`，darwin = `open`，
   linux = `xdg-open`）。
   `appWindow: false` 时**与官方相同**:默认浏览器打开(普通标签页,官方
   open 方式)。
4. **`--no-open` / SSH 会话（与官方相同的不打开）**：`dsh web --no-open`
   （webStartup 服务的 `openBrowser === false`）或 SSH 会话（`SSH_CONNECTION`/
   `SSH_TTY`，dsh ≥0.1.7 经 launcher 的 `launchEnvironment` 快照只认**继承进程层**，
   与官方 `launchedThroughSsh` 同源——项目/用户 `.env` 里的同名变量不算 SSH）时
   不打开任何窗口/页面——插件读取与官方**同一来源**做同样的抑制。

监听地址/端口取自 webServer 服务的真实值（`--port` 自定义、`--port 0` 均正确）。
打开窗口/浏览器时用的 URL 是**公告地址**：dsh ≥0.2.1 优先
`webApp.getAdvertisedUrl()`（尊重 `--public-url`、TLS、非回环绑定），其次
`webServer.getOrigin()`，旧版退回 `http://127.0.0.1:<port>`；连接服务再附加
本进程的启动令牌。**无需等待**：插件把 webServer 声明为硬依赖（`inject`），
Cordis 会等 webServer 插件 `Service.init()` 完成（HTTP socket 已绑定、端口已写入）
后才激活本插件，apply 时端口直接可用。
两种模式都随 DSH 退出而关闭：webview2 宿主监视父进程；browser 专用实例由
Job Object（强杀也生效）+ 退出清理结束进程树。

> **与 DSH 核心浏览器交接的关系**：DSH 核心（web-app bundle）默认会在启动后
> 用系统默认浏览器打开 GUI（`openBrowser: true`，普通标签页/窗口，非独立窗口）。
> 安装本插件后，插件的 bundle 补丁会把 `web-runtime.openBrowser` 置为 `false`，
> 打开行为完全由本插件接管：`appWindow: true` 时打开独立应用窗口，不弹出
> 普通浏览器页面；`appWindow: false` 时插件执行**与官方核心相同的默认浏览器
> 交接**（open 包 → 平台原生拉起），行为与未安装插件时一致；`dsh web
> --no-open`（或 SSH 会话）时与官方一致不打开任何窗口/页面。卸载插件后
> 恢复官方默认行为。

### WebView2 宿主要求（仅 webview2 模式）

- Windows 10 1803+ / Windows 11 / Windows Server 2016+
  （Win7/8.1 已于 2023-01 终止支持，见微软公告）
- WebView2 Runtime（常青版，通常随 Edge 预装；本机已验证 151.x）
- **无 .NET 10 运行时要求**：宿主自 0.1.15 起目标 **.NET Framework 4.7.2**，
  由 Windows 10 1803+ / Windows 11 **操作系统自带**（Win11 为 4.8.x），
  无需安装任何 .NET Core/10/自包含运行时。构建机需 .NET SDK 与 .NET
  Framework 4.x 定位包（随 VS/SDK 安装；无定位包时给 csproj 加
  `Microsoft.NETFramework.ReferenceAssemblies` NuGet 包）。
- **DPI 感知（自 0.1.20）**：宿主声明 **PerMonitorV2**（`host/app.manifest`
  清单 + `App.config` 的 WinForms `DpiAwareness` 段）。.NET Framework 的
  WinForms 默认是 DPI 不感知进程，在 125%/150% 等缩放显示器上 Windows 会把
  窗口按 96 DPI 渲染后整体位图放大（DPI 虚拟化）→ 文字与网页内容发糊；
  声明感知后按显示器真实 DPI 原生渲染，字迹清晰。

## 配置

两种途径，等价：

1. **设置表单**（推荐）——两条受支持的版本线，插件**同时**注册、哪条存在就哪条生效：
   - **最新版（dsh ≥ 0.1.6，含 0.1.7-alpha.1 与 0.2.x）**：**「插件管理」页 →
     `dsh-auto-open-web`（bundle 详情页）**，表单直接显示在该页说明与组件行之间
     （插槽 `plugins.bundle.config`，key = **bundle 包名**）—— 与 `dsh-harness-tags`
     同款席位。0.2.1 该槽的 owner props 仍是 `{ view: 'summary' | 'page', form? }`，
     bundle 页只传 `{ view: 'page' }`，**数据由插件自绑**：命名空间 = 本条目 id，
     经客户端服务 `configForms` 取 ConfigForm（快照 + `set`/`unset`/订阅；镜像首次
     加载中会订阅 `describe()`，命名空间到达后自动补渲染）。此形态只有「保存」，
     **离开页面即丢弃暂存**。
   - **最新 rc 线（0.1.5-rc.x）**：设置 → 插件配置 → 「自动打开网页」折叠卡片
     （插槽 `settings.plugin.item`，按设置命名空间 keyed）。数据走宿主
     `settings.register` + 客户端 `settingsScope`。

   两条线的**客户端服务互不存在**（新版的 `configForms` ↔ rc 线的
   `settingsScope`），因此插件把这两个服务都改为**惰性读取**、`inject` 只声明
   两线共有的服务（`slots`/`locale`/`connection`/`remote`）——否则硬注入会让插件
   在另一条线上永远 pending、设置界面完全不出现。
   dsh ≥0.1.7 的 `.volatile()` 字段在宿主侧是 **Volatile 引用**（`config.<f>.get()`）：
   插件统一解包后再归一化；schemastery 没有 `.volatile()` 的旧线自动退回普通 schema。
   更早的版本（0.1.0-rc.*、0.1.1-rc.*、0.1.2-*/0.1.3-alpha/0.1.5-alpha/0.1.6-alpha.1）
   **不再保证**。

   两种入口都可编辑 `appWindow`（独立应用窗口）、`windowKind`（WebView2 宿主 /
   浏览器应用窗口）、`browserPath`（浏览器可执行文件，支持「浏览」原生对话框
   选择；位于窗口类型下方，仅选择「浏览器应用窗口」时使能）、
   `exitOnWindowClose`（窗口关闭时退出 DSH，默认关闭）。
   保存后设置值优先于行配置（新版落在该行的用户层覆盖上，rc 线落在
   `auto-open-web` 设置命名空间）；宿主仅保留「浏览」「测试」辅助路由
   （官方通道无法覆盖的能力）。
2. **行配置**（cordis.patch.yml）：作为启动种子，设置表单保存前生效。

| 字段 | 默认 | 说明 |
| --- | --- | --- |
| `appWindow` | `true` | 启动时自动打开独立应用窗口；false 时与官方相同——用系统默认浏览器打开 GUI（普通标签页，官方 open 方式） |
| `windowKind` | `webview2` | `webview2` = WebView2 宿主（独立进程、任务栏 DSH 图标、随 DSH 退出）；`browser` = 浏览器 `--app` 专用实例。所选类型不可用时仅记录日志、不打开 |
| `exitOnWindowClose` | `false` | **（实验性）**关闭自动打开的窗口时随之退出 DSH（默认关闭；仅 `appWindow` 开启时生效）。窗口进程**正常**退出（用户关闭窗口）时触发 `process.exit(0)`；启动失败/崩溃/被强杀（非 0 退出码）不触发，避免误退出。**设置卡片保存后当前会话即时生效**（退出监听始终注册、行为由实时标志决定），无需重启 DSH |
| `browserPath` | `''` | 手动指定的浏览器可执行文件路径（单条，如 `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`；仅浏览器模式使用），优先于内置候选 Edge → Chrome；路径不存在会跳过并告警。设置卡片上的「浏览」按钮弹出**原生文件对话框**：与官方工作区目录选择器同一机制（子进程 + koffi 驱动 IFileOpenDialog，对话框是子进程的第一个窗口，自动置顶；不使用 PowerShell）。「测试」按钮**真实拉起**一个 `--app` 专用测试实例（独立 user-data-dir `~/.dsh/<browser>-test-profile`，不污染正式实例）：确认浏览器主进程存活后报告成功，窗口展示数秒后自动结束该测试进程树（精确 pid，不动正式实例；Job Object 可用时测试实例也加入作业，DSH 退出时兜底）；测试使用当前输入的路径（未保存也能测），失败会显示原因 |

### browserPath 行配置示例

在 `~/.dsh/profiles/web/cordis.patch.yml` 中按 id 覆写该行的 config（覆写会整体替换 config，未列出的字段用默认值）：

```yaml
- id: auto-open-web
  config:
    browserPath: 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
```

## 打包与安装

本包是**组合包(bundle)**:一个附带配置层的 npm 包——`package.json` 的
`dsh.bundle` 声明配置层文件(`cordis.patch.yml`),profile 安装它时按包名激活
插件行。已发布到 **npm registry**(`dsh-auto-open-web` 的 `latest` 即当前兼容版,
含 dsh 0.2.1-rc.3 适配)与 **GitHub**
（https://github.com/jinsiyu/dsh-auto-open-web，main 分支）。

### 打包

```bash
cd dsh-auto-open-web
pnpm pack          # prepack 钩子自动先编译 WebView2 宿主(dotnet publish),产出 dsh-auto-open-web-0.1.31.tgz
```

### 安装方式(任选其一)

**方式一:源码 checkout 链接(开发期,改动即时生效)**

```bash
# 绝对路径,避免 pnpm 自链接
dsh plugin --profile web add C:\path\to\dsh-auto-open-web
```

**方式二:tarball(发布产物,推荐交付;无需构建授权)**

```bash
dsh plugin --profile web add ./dsh-auto-open-web-0.1.31.tgz
```

**方式三:npm 注册表(推荐交付;装 latest 即当前兼容版)**

```bash
dsh plugin --profile web add dsh-auto-open-web
```

**方式四:GitHub 源码安装**

```bash
dsh plugin --profile web add github:jinsiyu/dsh-auto-open-web#main
```

### 卸载

```bash
dsh plugin --profile web remove dsh-auto-open-web   # 同时移除依赖与对应配置层
```

### 效果与层顺序

安装后:pnpm 将包加入 `profiles/web/node_modules`,`dsh` 把
`dsh-auto-open-web` 追加到 `dsh.profile.bundles`;启动时 bundle 的
`cordis.patch.yml` 插入插件行(`name: auto-open-web`,按包名解析)。
重启 `dsh web` 后,配置表单出现在对应入口(最新版,含 0.2.x:「插件管理」页里
`dsh-auto-open-web` 那一行的配置页;最新 rc 线:设置 → 插件配置卡片)。
客户端 bundle 由 modules 行按 `dsh.client` 声明在启动时扫描进浏览器清单;
插件在客户端**同时**向 `plugins.bundle.config`(新)与 `settings.plugin.item`
(旧)两个插槽注册,由客户端插槽机制决定哪个生效(不存在者保持 pending,
不报错、不影响另一者)。

生效配置按以下顺序逐层组合(后应用的层按行胜出,整行替换 config 而非深合并):
每个 bundle 的 patch(按 bundles 列表顺序)→ profile 自己的
`cordis.patch.yml` → 全局 `$DSH_HOME/cordis.patch.yml` → `--patch` overlay。
用户可在自己 profile 的 `cordis.patch.yml` 中覆盖本包的行,无需改动包。

### 注意事项

- **本包零 dependencies**:所有运行时依赖均由 **DSH 部署提供**,以 optional
  peer / `engines.dsh` 声明(安装无警告):
  - `@deepseek-ai/dsh`(宿主):清单**同时**声明 `engines.dsh` 与
    `@deepseek-ai/dsh` peer,范围都是 **`>=0.1.5-rc.1 <0.3.0`**。
    dsh 0.2.1-alpha.3 起只读 `engines.dsh`、忽略这些 peer;旧版忽略
    `engines.dsh`、读 peer。范围覆盖 0.1.5-rc 线、0.1.7 与 **0.2.x**
    (已在 0.2.1-rc.3 上验证);更早的 0.1.0-rc.*/0.1.1-rc.*/0.1.2-*/
    0.1.3-alpha/0.1.5-alpha 不再保证。快照 store 统一走
    `@deepseek-ai/dsh-client-store`(自 0.1.2-alpha.2 起,rc.8 时代的
    `dsh-client-runtime` 回退已移除);不做运行时版本检测
  - `@deepseek-ai/schemastery`(配置 schema 校验器;运行时解析:常规 import
    优先,其次 Windows 全局 npm 布局下的 DSH 部署副本,最后
    `<DSH_HOME>/profiles/node_modules` 共享依赖层)
  - `koffi`(仅 Windows 的 Job Object / 进程校验 / 原生对话框;同样运行时
    逐级解析部署副本,失败仅降级)
  因此任何平台安装都无构建拦截——鸿蒙等无 koffi 预编译的环境开箱即装;
  posix 平台走 posix 降级适配器(browser 模式可用;webview2 / 原生对话框
  不可用),且平台适配器按平台条件加载,posix 上完全不求值 win32.js。
- **设置卡片 UI 完全自绘**(自 0.1.10 起):不依赖官方任何组件/CSS 模板
  (不 require `dsh-client-ui-primitives`,不复刻官方样式表),卡片外壳、
  字段、按钮、输入框、图标全部手写;观感经官方设计令牌变量
  (`--dsw-alias-*` / `--dsw-static-*`)对齐,浅/深色自动跟随主题。
  **维护提示(0.1.30,0.2.1-rc.3 适配)**:三处必须与新版契约同步——
  (1) **清单兼容性**:dsh 0.2.1-alpha.3 起 `engines.dsh` 单独决定兼容性、忽略
  `@deepseek-ai/dsh*` peer;旧版忽略 `engines.dsh` 只读 peer。因此
  `package.json` **同时**声明两者,范围保持一致(`>=0.1.5-rc.1 <0.3.0`)。
  只改 peer 不改 engines 会让 0.2.1+ 继续按旧 peer 拒绝加载(表现为启动时
  `dsh: skipping profile bundle "dsh-auto-open-web": ... incompatible`,而且
  `--dump-config` 仍然 exit 0,只有 stderr 能看出来)。
  (2) **Config 读到的是 Volatile 引用**:dsh 0.1.7 起 `.volatile()` 字段的
  parse 输出是 `{ get(): T }` 包装(apply 形参和 `ctx.fiber.entry.options.config`
  都一样),必须 `.get()` 解包后才能当布尔/字符串用;直接 `config.appWindow !== false`
  会永远得到默认值。`lib/index.js` 的 `unwrapConfigValue()` / `readConfigField()`
  统一处理两种形态;旧线 schemastery 没有 `.volatile()` 时用
  `volatileField()` 能力探测退回普通 schema,不要直接链式调用。
  (3) **bundle patch 的 web-runtime 键集**:0.2.1-rc.1 起该行配置是
  `openBrowser / printUrl / publicUrl / surfaceContext`(0.2.1-alpha.1 加
  `publicUrl`,rc.1 删 `trustedHosts`),补丁整行替换 config,漏掉 `publicUrl`
  会让 `--public-url` 失效;为兼容旧线仍保留 `trustedHosts`(新版 schema 忽略)。
  对应断言在 `smoke-config.mjs`。
  **维护提示(0.1.26,新版设置投影)**:dsh 0.1.7 起宿主只把**标记为
  `volatile` 的 Config 字段**投影成设置表单(`dsh-settings` 的 `volatileForm()`
  对非 volatile 字段直接返回 undefined → `describe()` 里没有本条目 → 页面显示
  "本部署未提供该设置命名空间")。因此 `lib/index.js` 的 `Config` 四个字段都必须
  链上 schemastery 的 **`.volatile()`**(官方 `dsh-agent-loop` 同款写法);
  **不要**写成 `.set('volatile', true)` —— `Schema.prototype.set` 是写 dict 子字段,
  会把 schema 弄坏、条目 import 失败。
  0.1.7 把设置命名空间改为"**每 profile 条目一个**"（= 本条目 id），与 bundle
  席位配套：页面只给 `view`，插件自己经惰性读取的 `configForms` 绑定该命名空间
  （rc 线同理，用惰性读取的 `settingsScope`）。
  **维护提示(0.1.22)**:主按钮(保存)必须用**随主题反转**的令牌对
  `background:var(--dsw-alias-label-primary)` +
  `color:var(--dsw-alias-bg-layer-3)`(与官方 `PluginConfigForm` 的 save 同款)。
  深色模式下 `--dsw-alias-brand-primary` 是**近白色**,若沿用不存在的
  `--dsw-alias-label-inverse`(回退 #fff)会渲染成白字白底、按钮不可见;
  同理不要引用并不存在的 `--dsw-alias-label-error`(真名是
  `--dsw-alias-state-error-primary`)。这两条已由 `smoke-client-slots.mjs`
  断言守护。
  仅使用官方公开的数据/机制接口:`slots`(插槽)、`locale`(文案)、
  bundle Config 表单(新版:页面下发 `{ view, form? }`,bundle 页不带 form 时
  经 `configForms` 自绑)、rc 线的 `settingsScope`(惰性读取)、快照 store。
- **开发冒烟**(仓库根目录,需要能解析 `@deepseek-ai/schemastery`——本地
  `node_modules` 副本或 DSH 部署均可):
  `node smoke-config.mjs && node smoke-client-slots.mjs && node smoke-fallback.mjs`。
- **npm 包已含 WebView2 宿主编译产物**(prepack 编译后发布);**GitHub
  main 分支与源码 checkout 方式不含** `host-publish/`(构建产物被
  .gitignore 忽略):webview2 模式需先在 `node_modules/dsh-auto-open-web`
  下执行 `pnpm run build:host` 生成(需 .NET SDK);browser 模式无需构建。
- 若手动编辑 `package.json` 安装(不经 dsh plugin 命令),需同时追加
  `dependencies` 与 `dsh.profile.bundles` 两项;使用本地 `file:` 依赖时
  `dsh web` 启动会把 `file:` 规范化成 `^0.1.31`,运行时不受影响。

## 图标

- GUI 页面图标：GUI 自带 `/favicon.svg`（与 index.html 一致）。
- **任务栏/窗口图标（WebView2 宿主）**：宿主进程直接设置 `Form.Icon` = 插件生成的
  DSH .ico（`~/.dsh/auto-open-web-icon.ico`），与浏览器任务栏身份机制无关。
  .ico 来源：抓取本机 `favicon.svg`，用 **sharp**（部署自带，运行时向上解析，
  未声明为依赖）栅格化为 16/32/48/64/128/256 PNG 后组装。
- **图标固定策略（自 0.1.14）**：生成的 .ico 一经写盘即**缓存固定**——后续启动
  检测到缓存存在且非空就直接复用，不再抓取 favicon/栅格化（图标不会因某次
  favicon 或 sharp 瞬时失败而消失，启动也更快）；仅缓存缺失时生成一次。
  另有**兜底**：`host/icon.ico`（与缓存同源）已通过 `<ApplicationIcon>`
  嵌入宿主 exe，即使缓存文件缺失，窗口/任务栏也显示 DSH 图标而非默认图标。

## 平台支持

- Windows：`windowKind: webview2`（默认，任务栏图标 DSH）或 `windowKind: browser`（--app 专用实例）；所选类型不可用时降级为默认浏览器打开（官方同款，普通标签页）
- macOS/Linux：`webview2` 模式不可用（降级为默认浏览器打开）；`browser` 模式未测试（--app 专用实例）

## 0.2.1-rc.3 适配记录（0.1.30）

**症状**：DSH 升到 0.2.1 后插件完全不加载（不弹窗口、设置页没有本插件的配置项）。

**根因**：0.2.x 的 app-boot 在装载 profile bundle 时先做兼容性判定
（`packages/boot/app-boot/src/plugin-compatibility.ts`）：清单有 `engines.dsh` 时只看它，
否则要求每个 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peer 都满足运行时版本
（semver 范围带 `includePrerelease: true`）。旧清单的 peer 上限是 `<0.2.0`：
0.2.0-rc.x 因为 includePrerelease 恰好通过，0.2.1-rc.3 不再通过 → bundle 进入
`skippedBundles`，启动 stderr 打出

```
dsh: skipping profile bundle "dsh-auto-open-web": Error: Plugin dsh-auto-open-web@0.1.29 is incompatible with dsh 0.2.1-rc.3: requirements {"@deepseek-ai/dsh":">=0.1.5-rc.1 <0.2.0"} ...
```

注意 `dsh --dump-config` 对 skipped bundle **仍 exit 0**，只有 stderr 能看出来。

**修复**（0.1.30）：
1. `engines.dsh` 与 `@deepseek-ai/dsh` peer 同步为 `>=0.1.5-rc.1 <0.3.0`
   （0.2.1-alpha.3+ 读 engines，旧版读 peer）。
2. `cordis.patch.yml` 的 `web-runtime` 配置补 `publicUrl`（0.2.1-alpha.1 引入；
   该补丁整行替换 config，漏掉它会让 `--public-url` 失效）。
3. 宿主 `.volatile()` 字段按 **Volatile 引用**解包——这是 0.1.7 起的长期欠账：
   此前设置卡片能显示、能保存，但 apply / 退出判定读到的永远是默认值。
4. GUI 地址改用 `webApp.getAdvertisedUrl()`（`--public-url` / TLS / 非回环绑定都正确）。
5. 客户端在命名空间首载时订阅 `configForms.describe()` 镜像，页面不会卡在
   "本部署未提供该设置命名空间"。
6. sharp / open / koffi 解析补 `<DSH_HOME>/profiles/node_modules` 共享层兜底
   （桌面版把核心装进 app.asar，没有 `%APPDATA%\npm` 全局布局）。

**验证**（本机 0.2.1-rc.3，隔离 `DSH_HOME` + 最小 profile，不写真实 profile）：
- 0.1.29 基线：`--dump-config` 的 stderr 出现上述 skipping 行；
- 0.1.30：`--dump-config` 无 skipping，合成树含 `- id: auto-open-web` 与
  `openBrowser: false`；`dsh web --no-open --port 3199` 到达就绪
  （`dsh web: http://127.0.0.1:3199/?token=...`）、stderr 为空、插件日志
  `[auto-open-web] --no-open given; not opening anything (official behavior)`；
  `GET /` 200，`GET /auto-open-web/pick-browser|test-browser` 405
  （POST-only 路由已注册）；
- `node smoke-config.mjs && node smoke-client-slots.mjs && node smoke-fallback.mjs` 全绿。

> 发布记录：0.1.30 为 0.2.1-rc.3 适配版；0.1.31 为**仅文档修正**版（代码与 0.1.30 相同，
> 修正 npm 页面里"发布前"相关措辞与打包示例版本号）。

## 边界情况

- 正常重启：webview2 模式下旧宿主窗口随旧 DSH 进程退出；新 DSH 打开新宿主窗口
- `browser` 模式：重启后旧窗口保持原样（需手动刷新；可与新窗口短暂并存）；
  DSH 被强杀（`taskkill /F`、崩溃）时专用实例由 Job Object 一并结束，不残留
- 插件被移除：无注入、无残留路由，零残留影响
