mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
feat: Node.js path scanning + manual path input + git HTTPS auto-fix (v0.4.2)
- Add scan_node_paths: auto-scan C/D/E/F/G drives for Node.js installations - Add check_node_at_path: verify Node.js at user-specified directory - Add save_custom_node_path: persist custom path to ~/.openclaw/clawpanel.json - enhanced_path() now loads saved custom path and applies to all commands - Windows enhanced_path: scan Program Files, LOCALAPPDATA, APPDATA, common drives - Auto git config HTTPS-instead-of-SSH before npm install (fixes exit 128) - Setup page: auto-scan button + manual path input when Node.js not detected - Error diagnosis: add EPERM, MODULE_NOT_FOUND, SSH publickey patterns - README: expanded troubleshooting section
This commit is contained in:
@@ -32,6 +32,7 @@ fn npm_command() -> Command {
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let mut cmd = Command::new("cmd");
|
||||
cmd.args(["/c", "npm", "--registry", ®istry]);
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
cmd
|
||||
}
|
||||
@@ -510,6 +511,15 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
|
||||
let old_pkg = npm_package_name(¤t_source);
|
||||
let need_uninstall_old = current_source != source;
|
||||
|
||||
// 自动配置 git 使用 HTTPS 替代 SSH,避免用户没配 SSH Key 导致依赖安装失败
|
||||
let _ = app.emit("upgrade-log", "配置 Git HTTPS 模式...");
|
||||
let _ = Command::new("git")
|
||||
.args(["config", "--global", "url.https://github.com/.insteadOf", "ssh://git@github.com/"])
|
||||
.output();
|
||||
let _ = Command::new("git")
|
||||
.args(["config", "--global", "url.https://github.com/.insteadOf", "git@github.com:"])
|
||||
.output();
|
||||
|
||||
let _ = app.emit("upgrade-log", format!("$ npm install -g {pkg}"));
|
||||
let _ = app.emit("upgrade-progress", 10);
|
||||
|
||||
@@ -636,7 +646,6 @@ pub fn check_node() -> Result<Value, String> {
|
||||
let mut result = serde_json::Map::new();
|
||||
let mut cmd = Command::new("node");
|
||||
cmd.arg("--version");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||
@@ -654,6 +663,137 @@ pub fn check_node() -> Result<Value, String> {
|
||||
Ok(Value::Object(result))
|
||||
}
|
||||
|
||||
/// 在指定路径下检测 node 是否存在
|
||||
#[tauri::command]
|
||||
pub fn check_node_at_path(node_dir: String) -> Result<Value, String> {
|
||||
let dir = std::path::PathBuf::from(&node_dir);
|
||||
#[cfg(target_os = "windows")]
|
||||
let node_bin = dir.join("node.exe");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let node_bin = dir.join("node");
|
||||
|
||||
let mut result = serde_json::Map::new();
|
||||
if !node_bin.exists() {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
result.insert("version".into(), Value::Null);
|
||||
return Ok(Value::Object(result));
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&node_bin);
|
||||
cmd.arg("--version");
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000);
|
||||
match cmd.output() {
|
||||
Ok(o) if o.status.success() => {
|
||||
let ver = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||
result.insert("installed".into(), Value::Bool(true));
|
||||
result.insert("version".into(), Value::String(ver));
|
||||
result.insert("path".into(), Value::String(node_dir));
|
||||
}
|
||||
_ => {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
result.insert("version".into(), Value::Null);
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(result))
|
||||
}
|
||||
|
||||
/// 扫描常见路径,返回所有找到的 Node.js 安装
|
||||
#[tauri::command]
|
||||
pub fn scan_node_paths() -> Result<Value, String> {
|
||||
let mut found: Vec<Value> = vec![];
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
let mut candidates: Vec<String> = vec![];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let pf = std::env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".into());
|
||||
let pf86 = std::env::var("ProgramFiles(x86)")
|
||||
.unwrap_or_else(|_| r"C:\Program Files (x86)".into());
|
||||
let localappdata = std::env::var("LOCALAPPDATA").unwrap_or_default();
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_default();
|
||||
|
||||
candidates.push(format!(r"{}\nodejs", pf));
|
||||
candidates.push(format!(r"{}\nodejs", pf86));
|
||||
if !localappdata.is_empty() {
|
||||
candidates.push(format!(r"{}\Programs\nodejs", localappdata));
|
||||
}
|
||||
if !appdata.is_empty() {
|
||||
candidates.push(format!(r"{}\npm", appdata));
|
||||
}
|
||||
candidates.push(format!(r"{}\.volta\bin", home.display()));
|
||||
candidates.push(format!(r"{}\.nvm", home.display()));
|
||||
|
||||
for drive in &["C", "D", "E", "F", "G"] {
|
||||
candidates.push(format!(r"{}:\nodejs", drive));
|
||||
candidates.push(format!(r"{}:\Node", drive));
|
||||
candidates.push(format!(r"{}:\Node.js", drive));
|
||||
candidates.push(format!(r"{}:\Program Files\nodejs", drive));
|
||||
// 扫描常见 AI 工具目录
|
||||
candidates.push(format!(r"{}:\AI\Node", drive));
|
||||
candidates.push(format!(r"{}:\AI\nodejs", drive));
|
||||
candidates.push(format!(r"{}:\Dev\nodejs", drive));
|
||||
candidates.push(format!(r"{}:\Tools\nodejs", drive));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
candidates.push("/usr/local/bin".into());
|
||||
candidates.push("/opt/homebrew/bin".into());
|
||||
candidates.push(format!("{}/.nvm/current/bin", home.display()));
|
||||
candidates.push(format!("{}/.volta/bin", home.display()));
|
||||
candidates.push(format!("{}/.nodenv/shims", home.display()));
|
||||
candidates.push(format!("{}/.fnm/current/bin", home.display()));
|
||||
candidates.push(format!("{}/n/bin", home.display()));
|
||||
}
|
||||
|
||||
for dir in &candidates {
|
||||
let path = std::path::Path::new(dir);
|
||||
#[cfg(target_os = "windows")]
|
||||
let node_bin = path.join("node.exe");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let node_bin = path.join("node");
|
||||
|
||||
if node_bin.exists() {
|
||||
let mut cmd = Command::new(&node_bin);
|
||||
cmd.arg("--version");
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000);
|
||||
if let Ok(o) = cmd.output() {
|
||||
if o.status.success() {
|
||||
let ver = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||
let mut entry = serde_json::Map::new();
|
||||
entry.insert("path".into(), Value::String(dir.clone()));
|
||||
entry.insert("version".into(), Value::String(ver));
|
||||
found.push(Value::Object(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Array(found))
|
||||
}
|
||||
|
||||
/// 保存用户自定义的 Node.js 路径到 ~/.openclaw/clawpanel.json
|
||||
#[tauri::command]
|
||||
pub fn save_custom_node_path(node_dir: String) -> Result<(), String> {
|
||||
let config_path = super::openclaw_dir().join("clawpanel.json");
|
||||
let mut config: serde_json::Map<String, Value> = if config_path.exists() {
|
||||
let content =
|
||||
std::fs::read_to_string(&config_path).map_err(|e| format!("读取配置失败: {e}"))?;
|
||||
serde_json::from_str(&content).unwrap_or_default()
|
||||
} else {
|
||||
serde_json::Map::new()
|
||||
};
|
||||
config.insert("nodePath".into(), Value::String(node_dir));
|
||||
let json = serde_json::to_string_pretty(&Value::Object(config))
|
||||
.map_err(|e| format!("序列化失败: {e}"))?;
|
||||
std::fs::write(&config_path, json).map_err(|e| format!("写入配置失败: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_env_file(path: String, config: String) -> Result<(), String> {
|
||||
let expanded = if let Some(stripped) = path.strip_prefix("~/") {
|
||||
|
||||
@@ -14,24 +14,88 @@ pub fn openclaw_dir() -> PathBuf {
|
||||
dirs::home_dir().unwrap_or_default().join(".openclaw")
|
||||
}
|
||||
|
||||
/// macOS/Linux 上 Tauri 从 Finder 启动时 PATH 很短(只有 /usr/bin:/bin:/usr/sbin:/sbin),
|
||||
/// 需要补充 Node.js / npm 常见安装路径,否则 check_node / npm_command 找不到命令
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
/// Tauri 应用启动时 PATH 可能不完整:
|
||||
/// - macOS 从 Finder 启动时 PATH 只有 /usr/bin:/bin:/usr/sbin:/sbin
|
||||
/// - Windows 上安装 Node.js 到非默认路径、或安装后未重启进程
|
||||
/// 补充 Node.js / npm 常见安装路径
|
||||
pub fn enhanced_path() -> String {
|
||||
let current = std::env::var("PATH").unwrap_or_default();
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
let extra: Vec<String> = vec![
|
||||
"/usr/local/bin".into(),
|
||||
"/opt/homebrew/bin".into(),
|
||||
format!("{}/.nvm/current/bin", home.display()),
|
||||
format!("{}/.volta/bin", home.display()),
|
||||
format!("{}/.nodenv/shims", home.display()),
|
||||
format!("{}/.fnm/current/bin", home.display()),
|
||||
format!("{}/n/bin", home.display()),
|
||||
];
|
||||
let mut parts: Vec<&str> = extra.iter().map(|s| s.as_str()).collect();
|
||||
if !current.is_empty() {
|
||||
parts.push(¤t);
|
||||
|
||||
// 读取用户保存的自定义 Node.js 路径
|
||||
let custom_path = openclaw_dir()
|
||||
.join("clawpanel.json")
|
||||
.exists()
|
||||
.then(|| {
|
||||
std::fs::read_to_string(openclaw_dir().join("clawpanel.json"))
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
.and_then(|v| v.get("nodePath")?.as_str().map(String::from))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let extra: Vec<String> = vec![
|
||||
"/usr/local/bin".into(),
|
||||
"/opt/homebrew/bin".into(),
|
||||
format!("{}/.nvm/current/bin", home.display()),
|
||||
format!("{}/.volta/bin", home.display()),
|
||||
format!("{}/.nodenv/shims", home.display()),
|
||||
format!("{}/.fnm/current/bin", home.display()),
|
||||
format!("{}/n/bin", home.display()),
|
||||
];
|
||||
let mut parts: Vec<&str> = vec![];
|
||||
if let Some(ref cp) = custom_path {
|
||||
parts.push(cp.as_str());
|
||||
}
|
||||
parts.extend(extra.iter().map(|s| s.as_str()));
|
||||
if !current.is_empty() {
|
||||
parts.push(¤t);
|
||||
}
|
||||
parts.join(":")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let pf = std::env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".into());
|
||||
let pf86 = std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| r"C:\Program Files (x86)".into());
|
||||
let localappdata = std::env::var("LOCALAPPDATA").unwrap_or_default();
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_default();
|
||||
|
||||
let mut extra: Vec<String> = vec![
|
||||
format!(r"{}\nodejs", pf),
|
||||
format!(r"{}\nodejs", pf86),
|
||||
];
|
||||
if !localappdata.is_empty() {
|
||||
extra.push(format!(r"{}\Programs\nodejs", localappdata));
|
||||
extra.push(format!(r"{}\fnm_multishells", localappdata));
|
||||
}
|
||||
if !appdata.is_empty() {
|
||||
extra.push(format!(r"{}\npm", appdata));
|
||||
extra.push(format!(r"{}\nvm", appdata));
|
||||
}
|
||||
extra.push(format!(r"{}\.volta\bin", home.display()));
|
||||
|
||||
// 扫描常见盘符下的 Node 安装(用户可能装在 D:\、F:\ 等)
|
||||
for drive in &["C", "D", "E", "F"] {
|
||||
extra.push(format!(r"{}:\nodejs", drive));
|
||||
extra.push(format!(r"{}:\Node", drive));
|
||||
extra.push(format!(r"{}:\Program Files\nodejs", drive));
|
||||
}
|
||||
|
||||
let mut parts: Vec<&str> = vec![];
|
||||
if !current.is_empty() {
|
||||
parts.push(¤t);
|
||||
}
|
||||
if let Some(ref cp) = custom_path {
|
||||
parts.push(cp.as_str());
|
||||
}
|
||||
for p in &extra {
|
||||
if std::path::Path::new(p).exists() {
|
||||
parts.push(p.as_str());
|
||||
}
|
||||
}
|
||||
parts.join(";")
|
||||
}
|
||||
parts.join(":")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ pub fn run() {
|
||||
config::get_version_info,
|
||||
config::check_installation,
|
||||
config::check_node,
|
||||
config::check_node_at_path,
|
||||
config::scan_node_paths,
|
||||
config::save_custom_node_path,
|
||||
config::write_env_file,
|
||||
config::list_backups,
|
||||
config::create_backup,
|
||||
|
||||
@@ -10,6 +10,7 @@ pub fn openclaw_command() -> std::process::Command {
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let mut cmd = std::process::Command::new("cmd");
|
||||
cmd.arg("/c").arg("openclaw");
|
||||
cmd.env("PATH", crate::commands::enhanced_path());
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
cmd
|
||||
}
|
||||
@@ -28,6 +29,7 @@ pub fn openclaw_command_async() -> tokio::process::Command {
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let mut cmd = tokio::process::Command::new("cmd");
|
||||
cmd.arg("/c").arg("openclaw");
|
||||
cmd.env("PATH", crate::commands::enhanced_path());
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
cmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user