From bf9cb52e259a7757374b8422f3f7f100ae7525c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Mon, 20 Apr 2026 11:57:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(setup):=20=E4=BF=AE=E5=A4=8D=20Node.js/Git/?= =?UTF-8?q?OpenClaw=20=E8=A3=85=E5=AE=8C=E4=B8=8D=E8=AF=86=E5=88=AB?= =?UTF-8?q?=EF=BC=88=E9=9C=80=E9=87=8D=E5=90=AF=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈 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 用户实测反馈 --- src-tauri/src/commands/config.rs | 33 ++++++++++++++++++++++++++++++-- src/pages/setup.js | 28 ++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 11de5a9..f7f92be 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -2607,11 +2607,15 @@ pub fn check_openclaw_at_path(cli_path: String) -> Result { } fn find_git_path() -> Option { + // #Compat-4: 必须把子进程 PATH 替换成 enhanced_path,否则继承的是 Tauri 启动时快照, + // 用户新装的 git 不在快照里,`where git` / `which git` 就找不到。对齐 find_node_path 的做法。 + let enhanced = super::enhanced_path(); #[cfg(target_os = "windows")] { let mut cmd = Command::new("where"); cmd.arg("git"); cmd.creation_flags(0x08000000); + cmd.env("PATH", &enhanced); if let Ok(output) = cmd.output() { if output.status.success() { if let Some(first_line) = String::from_utf8_lossy(&output.stdout).lines().next() { @@ -2626,7 +2630,10 @@ fn find_git_path() -> Option { #[cfg(not(target_os = "windows"))] { - if let Ok(output) = Command::new("which").arg("git").output() { + let mut cmd = Command::new("which"); + cmd.arg("git"); + cmd.env("PATH", &enhanced); + if let Ok(output) = cmd.output() { if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !path.is_empty() && std::path::Path::new(&path).exists() { @@ -3975,6 +3982,14 @@ async fn upgrade_openclaw_inner( } } + // #Compat-4: npm 首次安装场景下,前面 `if need_uninstall_old` 块被跳过, + // PATH 缓存和 CLI 检测缓存都是装 openclaw 之前的旧快照。必须在这里统一刷新一次, + // 否则前端 `check_installation`/`get_services_status` 拿到的仍是「CLI 未安装」 + // —— 用户反馈「一键装完日志显示成功,但面板不识别,重启客户端才能用」。 + // 切换源场景前面已刷过,这里重刷无害(几十 ms 扫描开销可接受)。 + super::refresh_enhanced_path(); + crate::commands::service::invalidate_cli_detection_cache(); + let new_ver = get_local_version().await.unwrap_or_else(|| "未知".into()); let msg = format!("✅ 安装完成,当前版本: {new_ver}"); let _ = app.emit("upgrade-log", &msg); @@ -5788,8 +5803,12 @@ pub fn check_git() -> Result { } else { find_git_path() }; - let mut cmd = Command::new(&git); + // #Compat-4: 优先用 find_git_path 拿到的绝对路径执行 --version(避免依赖子进程 PATH), + // 回退到 "git" 时也把 enhanced_path 注入子进程 PATH,让刚装完 git 的场景立即可识别。 + let exec = git_path.as_deref().unwrap_or(&git); + let mut cmd = Command::new(exec); cmd.arg("--version"); + cmd.env("PATH", super::enhanced_path()); #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); match cmd.output() { @@ -6028,6 +6047,10 @@ pub async fn auto_install_git(app: tauri::AppHandle) -> Result { .map_err(|e| format!("等待 winget 完成失败: {e}"))?; if status.success() { let _ = app.emit("upgrade-log", "Git 安装成功!"); + // #Compat-4: 刷新 PATH 缓存,使 check_git 能立即检测到新装的 git, + // 避免用户反馈「装完不识别,重启客户端才能用」 + super::refresh_enhanced_path(); + crate::commands::service::invalidate_cli_detection_cache(); return Ok("Git 已通过 winget 安装".to_string()); } Err("winget 安装 Git 失败,请手动下载安装: https://git-scm.com/downloads".to_string()) @@ -6045,6 +6068,9 @@ pub async fn auto_install_git(app: tauri::AppHandle) -> Result { let status = child.wait().map_err(|e| format!("等待安装完成失败: {e}"))?; if status.success() { let _ = app.emit("upgrade-log", "Git 安装已触发,请在弹出的窗口中确认安装。"); + // #Compat-4: 刷新缓存(即便是"触发"而非同步完成,下次检测时缓存也已清) + super::refresh_enhanced_path(); + crate::commands::service::invalidate_cli_detection_cache(); return Ok("已触发 xcode-select 安装,请在弹窗中确认".to_string()); } Err( @@ -6130,6 +6156,9 @@ pub async fn auto_install_git(app: tauri::AppHandle) -> Result { let status = child.wait().map_err(|e| format!("等待安装完成失败: {e}"))?; if status.success() { let _ = app.emit("upgrade-log", "Git 安装成功!"); + // #Compat-4: 刷新 PATH 缓存,使 check_git 立即识别新装的 git + super::refresh_enhanced_path(); + crate::commands::service::invalidate_cli_detection_cache(); return Ok("Git 已安装".to_string()); } Err("Git 安装失败,请手动执行: sudo apt install git".to_string()) diff --git a/src/pages/setup.js b/src/pages/setup.js index 674318a..895723a 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -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 自带 guard:page 从 DOM 移除后自动卸载监听器,防止跨页面泄漏。 + // 同时监听 visibilitychange(tab 切换)和 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) {
` - // 清除缓存,确保拿到最新检测结果 + // 清除前端 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(),