修复:nvm 用户 Node.js/CLI 检测失败的问题

Bug1: check_node_at_path 参数名不匹配(snake_case vs camelCase)
- Tauri v2 默认期望 camelCase,前端发送的 node_dir 改为 nodeDir
- 同步修复 save_custom_node_path 和所有 memory 函数的 agent_id → agentId

Bug2: Windows 上 OpenClaw CLI 已安装但检测显示 
- is_cli_installed() 仅检查 %APPDATA%\npm\openclaw.cmd
- 增加 PATH 查找兜底,兼容 nvm、自定义 prefix 等安装方式

增强: enhanced_path() 扫描 nvm 版本目录
- macOS/Linux: 扫描 ~/.nvm/versions/node/*/bin
- Windows: 扫描 %APPDATA%\nvm\* 和 %NVM_HOME%\*
This commit is contained in:
晴天
2026-03-05 23:16:44 +08:00
parent bb2c6df8ec
commit ef3ae03546
3 changed files with 59 additions and 9 deletions

View File

@@ -36,7 +36,7 @@ pub fn enhanced_path() -> String {
#[cfg(not(target_os = "windows"))]
{
let extra: Vec<String> = vec![
let mut extra: Vec<String> = vec![
"/usr/local/bin".into(),
"/opt/homebrew/bin".into(),
format!("{}/.nvm/current/bin", home.display()),
@@ -45,6 +45,18 @@ pub fn enhanced_path() -> String {
format!("{}/.fnm/current/bin", home.display()),
format!("{}/n/bin", home.display()),
];
// 扫描 nvm 实际安装的版本目录(兼容无 current 符号链接的情况)
let nvm_versions = home.join(".nvm/versions/node");
if nvm_versions.is_dir() {
if let Ok(entries) = std::fs::read_dir(&nvm_versions) {
for entry in entries.flatten() {
let bin = entry.path().join("bin");
if bin.is_dir() {
extra.push(bin.to_string_lossy().to_string());
}
}
}
}
let mut parts: Vec<&str> = vec![];
if let Some(ref cp) = custom_path {
parts.push(cp.as_str());
@@ -74,6 +86,32 @@ pub fn enhanced_path() -> String {
if !appdata.is_empty() {
extra.push(format!(r"{}\npm", appdata));
extra.push(format!(r"{}\nvm", appdata));
// 扫描 nvm-windows 实际安装的版本目录
let nvm_dir = std::path::Path::new(&appdata).join("nvm");
if nvm_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(&nvm_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() && p.join("node.exe").exists() {
extra.push(p.to_string_lossy().to_string());
}
}
}
}
}
// NVM_HOME 环境变量(用户可能自定义了 nvm 安装目录)
if let Ok(nvm_home) = std::env::var("NVM_HOME") {
let nvm_path = std::path::Path::new(&nvm_home);
if nvm_path.is_dir() {
if let Ok(entries) = std::fs::read_dir(nvm_path) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() && p.join("node.exe").exists() {
extra.push(p.to_string_lossy().to_string());
}
}
}
}
}
extra.push(format!(r"{}\.volta\bin", home.display()));

View File

@@ -213,8 +213,9 @@ mod platform {
Ok(0)
}
/// 检测 openclaw CLI 是否已安装(文件系统检测,避免 spawn 进程)
/// 检测 openclaw CLI 是否已安装
pub fn is_cli_installed() -> bool {
// 方式1: 检查常见文件路径
if let Ok(appdata) = std::env::var("APPDATA") {
let cmd_path = std::path::Path::new(&appdata)
.join("npm")
@@ -223,6 +224,17 @@ mod platform {
return true;
}
}
// 方式2: 通过 PATH 查找(兼容 nvm、自定义 prefix 等)
let mut cmd = std::process::Command::new("cmd");
cmd.args(["/c", "openclaw", "--version"]);
cmd.env("PATH", crate::commands::enhanced_path());
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
if let Ok(o) = cmd.output() {
if o.status.success() {
return true;
}
}
false
}

View File

@@ -278,19 +278,19 @@ export const api = {
searchLog: (logName, query, maxResults = 50) => invoke('search_log', { logName, query, maxResults }),
// 记忆文件
listMemoryFiles: (category, agentId) => cachedInvoke('list_memory_files', { category, agent_id: agentId || null }),
readMemoryFile: (path, agentId) => cachedInvoke('read_memory_file', { path, agent_id: agentId || null }, 5000),
writeMemoryFile: (path, content, category, agentId) => { invalidate('list_memory_files', 'read_memory_file'); return invoke('write_memory_file', { path, content, category: category || 'memory', agent_id: agentId || null }) },
deleteMemoryFile: (path, agentId) => { invalidate('list_memory_files'); return invoke('delete_memory_file', { path, agent_id: agentId || null }) },
exportMemoryZip: (category, agentId) => invoke('export_memory_zip', { category, agent_id: agentId || null }),
listMemoryFiles: (category, agentId) => cachedInvoke('list_memory_files', { category, agentId: agentId || null }),
readMemoryFile: (path, agentId) => cachedInvoke('read_memory_file', { path, agentId: agentId || null }, 5000),
writeMemoryFile: (path, content, category, agentId) => { invalidate('list_memory_files', 'read_memory_file'); return invoke('write_memory_file', { path, content, category: category || 'memory', agentId: agentId || null }) },
deleteMemoryFile: (path, agentId) => { invalidate('list_memory_files'); return invoke('delete_memory_file', { path, agentId: agentId || null }) },
exportMemoryZip: (category, agentId) => invoke('export_memory_zip', { category, agentId: agentId || null }),
// 安装/部署
checkInstallation: () => cachedInvoke('check_installation', {}, 60000),
initOpenclawConfig: () => { invalidate('check_installation'); return invoke('init_openclaw_config') },
checkNode: () => cachedInvoke('check_node', {}, 60000),
checkNodeAtPath: (nodeDir) => invoke('check_node_at_path', { node_dir: nodeDir }),
checkNodeAtPath: (nodeDir) => invoke('check_node_at_path', { nodeDir }),
scanNodePaths: () => invoke('scan_node_paths'),
saveCustomNodePath: (nodeDir) => invoke('save_custom_node_path', { node_dir: nodeDir }),
saveCustomNodePath: (nodeDir) => invoke('save_custom_node_path', { nodeDir }),
getDeployConfig: () => cachedInvoke('get_deploy_config'),
patchModelVision: () => invoke('patch_model_vision'),
checkPanelUpdate: () => invoke('check_panel_update'),