# P2-04 安装器设计：对比旧 install.ps1 的 4 个高危问题如何被消除

**日期**：2026-10-16
**交付物**：`installer\install.ps1` / `uninstall.ps1` / `rollback.ps1` / `test_installer_roundtrip.ps1`（均 UTF-8 BOM）
**底座**：P0-04 已验收的 `core\deploy.ps1` 纯函数模块（Install/Uninstall/Rollback/Verify + manifest schema），安装器只做"探测→多选→逐版本编排"，不重复造轮子。
**加载方式（用户已拍板 2026-10-16）**：**方案 A —— vendor 目录落盘**，部署目标
`<NX>\UGII\vendor\NX Copilot\startup\`（NxBridge.dll + NxBridge.men/.btn/.rtb，每版本独立一份），
不写任何环境变量。vendor 约定是否符合西门子官方推荐仍列入 blocked（见 done.json）。

---

## 一、旧 install.ps1 的 4 个高危问题 → 新方案的消除方式

| # | 旧 install.ps1 的问题 | 新安装器的消除机制 | 落点 |
|---|---|---|---|
| ① | 卸载 `Remove-Item -Recurse -Force` 整目录，无备份无清单 | 一切以 manifest 为准：只删【本次 action=added 的文件】；覆盖过的文件从逐文件 `.bak` 还原；备份缺失 → 记 FAIL 拒绝执行，绝不盲删 | deploy.ps1 Uninstall（按清单反向）+ rollback 预检备份完整性 |
| ② | 无条件清空全局唯一的 UGII_VENDOR_DIR（别家插件一起失效） | 新安装器【根本不写任何环境变量】（manifest 的 env_vars 恒为空数组）；NX 拾取靠 vendor 目录约定，版本切换靠进程级环境（nx_launch 已有）。deploy.ps1 的条件恢复逻辑保留为兜底：只在"当前值 == 自己写入的值"时才恢复 | install.ps1 不传 -SetEnvVars；deploy.ps1 restore_condition=current_must_equal_new |
| ③ | 永久写 UGII_LOCAL_USER_DEFAULTS 关闭 NXOpen 签名警告，卸载不移除 | 新安装器没有任何一处写 UGII_LOCAL_USER_DEFAULTS / 注册表；签名警告保持 NX 默认行为 | install.ps1 全文无环境写入路径 |
| ④ | 注册表探测写错键（Unigraphics Solutions\NX）+ 只扫 C:/D: 盘 | 版本探测复用 P0-01 权威模块 `core\nx_versions.ps1`（注册表 HKLM\SOFTWARE\Siemens\NX <数字> 的 INSTALLDIR/UGII_BASE_DIR，不猜盘符），并用 `<NX>\NXBIN\run_journal.exe` 存在性校验可用性 | install.ps1 第 1 步，versions.json 单一事实源 |

## 二、install.ps1 流程

```
探测（nx_versions.ps1 -Json）
  → 选择（-Version 2212,2412 / all / 交互多选含 [a]全部；未 -Yes 时打印计划并要求确认）
  → 逐版本：
      ① 组装 staging 包（相对路径映射 NX 根）：
         nx\dist\<ver>\NxBridge.dll + nx\startup\{NxBridge.men,.btn,.rtb}
         → <ManifestDir>\staging_<ver>\UGII\vendor\NX Copilot\startup\
      ② Install-NxPackage（deploy.ps1）：
         写入权限探针 → 逐文件 三态(added/overwritten/skipped) + 覆盖先 .bak
         → manifest（SHA256 before/after + backup_path + env_vars）
  → 汇总：任一版本失败 → 退出码 1
```

- `-DryRun`：零副作用（测试 S1b 断言假根文件数保持 0）。
- manifest 落盘：`状态\installer_<TaskId>_<stamp>\install_<ver>.json`（UTF-8 无 BOM）。
- **注意**：同一版本重复安装要分 ManifestDir（manifest 文件名 install_<ver>.json 会互相覆盖）——测试 S6 已把这个坑写进注释。

## 三、uninstall.ps1 / rollback.ps1

- `uninstall.ps1 -Manifest <json 或目录>`：目录模式处理其中全部 install_*.json（倒序）。
  反向语义：added→删除；overwritten→从 .bak 还原并删 .bak；env_vars→条件恢复（见上表②）。
- `rollback.ps1 -Manifest <json 或目录>`：先预检全部 .bak 存在（缺一个立即整体中止），
  再以 -IgnoreRestoreCondition 按"我装之前"的快照强制还原。
- 全部支持 `-DryRun`（uninstall）、三态输出、失败非零退出码。

## 四、测试证据（22/22，沙箱，退出码 0）

`installer\test_installer_roundtrip.ps1`——思路同 `tools\test_deploy_roundtrip.ps1`，
但目标不是真实 NX 目录，而是 `状态\sandbox_p204` 下的**假 NX 根**（全程不写真实环境变量、不碰 NX 安装目录）：

| 断言 | 结果 |
|---|---|
| S1 install -DryRun 零副作用 | OK |
| S2 双版本各部署 4 文件 + 双 manifest | OK |
| S3 Verify SHA256 双 manifest 全过 | OK |
| S4 篡改 → Verify 退出码 1 | OK |
| S5 uninstall 目录模式反向 → 双假根清零 | OK |
| S6 覆盖装产生 .bak → 漂移 → rollback 还原 SHA → uninstall(added) 清零 | OK |
| S7 env 条件恢复三分支（匹配→DRY 还原 / 改动→WARN 跳过 / 已不存在→SKIP）+ DryRun 不写真实变量 | OK |

```
结果: PASSED 22  FAILED 0
roundtrip exit: 0
```

## 五、测试与开发中抓到并修复的真 bug（各带备份）

1. **deploy.ps1 manifest 读取编码 bug（P0-04 遗留，本卡抓出）**：
   manifest 以 UTF-8 无 BOM 写出，但三处 `Get-Content -Raw` 读回未指定编码——
   PS 5.1 按 ANSI/GBK 解码，中文路径「状态」的 UTF-8 字节（E7 8A B6 E6 80 81）被 CP936
   配对成 鐘舵€乗，其中 `0x81+0x5C` **吞掉一个反斜杠** → JSON 出现非法转义 `\s` →
   ConvertFrom-Json 崩溃。P0-04 自测用 ASCII 路径（__SELFTEST__）未暴露。
   修复：Uninstall/Rollback/Verify 三处读 manifest 显式 `-Encoding UTF8`。
   备份：`backup\deploy.ps1.before_P204_20261016_222807`。
2. **测试壳 `$args` 参数名冲突**：`Invoke-Ps([string[]] $args)` 与 PowerShell 自动变量冲突，
   splatting 失效、子进程收不到参数。改为 `$CliArgs` 并在函数内临时降 EAP 收集 stderr。

## 六、人工执行清单（真机完整往返，需授权后执行）

```powershell
# 安装（ DryRun 先行）
powershell -ExecutionPolicy Bypass -File D:\NX_AI\installer\install.ps1 -DryRun
powershell -ExecutionPolicy Bypass -File D:\NX_AI\installer\install.ps1 -Version all -Yes
# → 记录 manifest 目录，如 状态\installer_INSTALLER_<stamp>\

# Verify（两个 install_*.json 各跑一次，core\deploy.ps1 dot-source 后调 Verify-NxPackage，
# 或直接看 install.ps1 输出的"新增/覆盖/失败"三态汇总）

# 卸载（目录模式一次反清两版本）
powershell -ExecutionPolicy Bypass -File D:\NX_AI\installer\uninstall.ps1 -Manifest <manifest目录> -DryRun
powershell -ExecutionPolicy Bypass -File D:\NX_AI\installer\uninstall.ps1 -Manifest <manifest目录>

# 回滚演练（可选）：先重装一次制造 overwritten+.bak，再 rollback
powershell -ExecutionPolicy Bypass -File D:\NX_AI\installer\install.ps1 -Version 2412 -Yes
powershell -ExecutionPolicy Bypass -File D:\NX_AI\installer\rollback.ps1 -Manifest <2412的manifest>

# 终态检查：两版本 <NX>\UGII\vendor\NX Copilot 下无残留文件；
# Get-ChildItem env: 查看进程级变量；[Environment]::GetEnvironmentVariable('UGII_VENDOR_DIR','User') 应无本工程痕迹
```

**vendor 官方加载方式确认**（blocked 项）：用户已选方案 A 布局；"NX 是否自动加载
vendor\...\startup 下的 .men/.dll"需在真机安装后启动 NX 一次即可肉眼确认
（若未加载，fallback 是进程级 UGII_VENDOR_DIR 方案 B，安装器无需改动，仅启动器补一行）。
