fix(setup): 修复 Node.js/Git/OpenClaw 装完不识别(需重启客户端)

用户反馈 0.13.3 版本有三个共性 bug:
1. 手动装 Node.js → 装完 panel 不识别,必须重启客户端
2. 一键装 Git → 装完 panel 不识别,必须重启客户端
3. 一键装 OpenClaw → 装完 panel 不识别,必须重启客户端

## 根因

Tauri 进程的 `std::env::var("PATH")` 是启动时快照,不会随系统 PATH 更
新。`enhanced_path()` 虽然扫描了常见安装目录,但**不存在的目录不会被
加入缓存**(line 828 `std::path::Path::new(p).exists()` 过滤)。装完
新程序后,新路径不在 enhanced_path 缓存里,CLI 检测又依赖子进程的
PATH,导致「找不到刚装的二进制」。

三个 bug 各有其子因:

### Bug 1: Git 检测 `find_git_path` 不用 enhanced_path
对比 `find_node_path` 显式 `cmd.env("PATH", enhanced_path)`,
`find_git_path` 里的 `where git` / `which git` 子进程继承的是 Tauri
启动时快照的老 PATH。即使 `refresh_enhanced_path()` 刷了缓存,子进
程也看不到新路径。`check_git` 后续调 `Command::new("git")` 同理,且
拿到 `git_path` 绝对路径后也没用。

### Bug 2: `auto_install_git` 三平台都不刷缓存
winget/xcode-select/apt 安装成功后直接 return,没有
`refresh_enhanced_path()` / `invalidate_cli_detection_cache()`。前端
`runDetect` 虽然会调 `invalidatePathCache`,但 Bug 1 让刷缓存也白刷。

### Bug 3: `upgrade_openclaw` npm 首装分支不刷缓存
npm 分支里只有 `if need_uninstall_old`(切换源场景)分支末尾刷了
PATH 缓存。**首次安装** `need_uninstall_old == false`,if 块整个跳
过,函数直接返回,CLI 检测缓存(60s TTL)和 PATH 缓存都是旧的。前
端 `setTimeout(reload, 1500)` 触发 SPA 重建,但 Tauri 进程没重启,
缓存没刷 → `is_cli_installed()` 返回 false。

### Bug 4: 手动装 Node.js 没有 hook 点
用户点「下载 Node.js」跳浏览器,装完回到 panel,panel 不知道用户装
了。虽然顶部有「重新检测」按钮,但 UX 上容易错过。`runDetect` 里虽
然会 `await api.invalidatePathCache()`,但需要用户主动触发。

## 修复

### Rust 端

1. **`find_git_path`**:子进程 `where`/`which` 显式 `env("PATH",
   enhanced_path)`,对齐 `find_node_path` 的做法。
2. **`check_git`**:优先用 `find_git_path` 返回的绝对路径执行
   `--version`;fallback 到 `"git"` 时也注入 enhanced_path 到子进程
   PATH,确保刚装完的场景能识别。
3. **`auto_install_git`**:winget/xcode-select/apt 三个平台的成功分
   支都调 `super::refresh_enhanced_path()` +
   `invalidate_cli_detection_cache()`。
4. **`upgrade_openclaw`**:npm 分支末尾(if need_uninstall_old 块之
   后、`get_local_version` 之前)无条件刷缓存,覆盖首装场景。切换源
   场景虽然前面已刷过一次,这里重刷无害(几十 ms 文件系统扫描开销
   可接受)。

### 前端

5. **`setup.js::render`**:注册 `visibilitychange` + window `focus`
   监听器,用户从浏览器装完 Node.js 切回 panel 时自动 `runDetect`。
   handler 自带 guard(`page.isConnected` 检查),页面切走后监听器
   自动卸载,防止泄漏。加 3 秒节流,避免 focus + visibilitychange
   同时连发触发重复检测。

## 验证

- `cargo fmt --check` 通过
- `cargo clippy --all-targets -- -D warnings` 通过
- `npm run build` 通过(setup chunk 未变,setup.js 新增 ~22 行)
- 本地需要用户验证:
  - [ ] 手动装完 Node.js → 切回 panel 自动识别
  - [ ] 一键装 Git → 装完立刻识别(无需重启)
  - [ ] 一键装 OpenClaw(首次 npm 安装)→ 装完立刻识别
  - [ ] 一键装 OpenClaw(切换源)→ 装完立刻识别(原本就工作,不回归)

Refs: 0.13.3 用户实测反馈
This commit is contained in:
晴天
2026-04-20 11:57:08 +08:00
parent dfb81066b4
commit bf9cb52e25
2 changed files with 58 additions and 3 deletions

View File

@@ -108,6 +108,28 @@ export async function render() {
`
page.querySelector('#btn-recheck').addEventListener('click', () => runDetect(page))
// #Compat-4: 用户在浏览器里手动装完 Node.js 后切回 panel或用户装完 Git/OpenClaw
// 后 app 失焦又重新获得焦点时,自动重新检测,避免「装完不识别」。
// handler 自带 guardpage 从 DOM 移除后自动卸载监听器,防止跨页面泄漏。
// 同时监听 visibilitychangetab 切换)和 window focus桌面端窗口激活兜底不同平台行为。
let _lastRedetectAt = 0
const onVisibilityChange = () => {
if (!page.isConnected) {
document.removeEventListener('visibilitychange', onVisibilityChange)
window.removeEventListener('focus', onVisibilityChange)
return
}
if (document.visibilityState !== 'visible') return
// 3 秒内不重复触发(避免 focus + visibilitychange 同时连发)
const now = Date.now()
if (now - _lastRedetectAt < 3000) return
_lastRedetectAt = now
runDetect(page)
}
document.addEventListener('visibilitychange', onVisibilityChange)
window.addEventListener('focus', onVisibilityChange)
runDetect(page)
return page
}
@@ -165,8 +187,12 @@ async function runDetect(page) {
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
`
// 清除缓存,确保拿到最新检测结果
// 清除前端 invoke 缓存
invalidate('get_version_info', 'check_node', 'check_git', 'get_services_status', 'check_installation')
// #Compat-4: 同步刷新 Rust 端 PATH 缓存 + CLI 检测缓存
// 用户手动装完 Node.js/Git 后Tauri 进程的 PATH 仍是启动时快照,且 enhanced_path 有缓存。
// 必须先调此命令扫描文件系统新装路径,才能让 where/which 找到新二进制。
try { await api.invalidatePathCache() } catch {}
// 并行检测 Node.js、Git、OpenClaw CLI、配置文件
const [nodeRes, gitRes, clawRes, configRes, versionRes] = await Promise.allSettled([
api.checkNode(),