mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +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:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -5,17 +5,27 @@
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.4.2] - 2026-03-06
|
||||
|
||||
### 修复 (Bug Fixes)
|
||||
|
||||
- **Windows Node.js 检测失败** — `enhanced_path()` 扩展为跨平台,Windows 上自动扫描 Program Files、LOCALAPPDATA、APPDATA、常见盘符(C/D/E/F)下的 Node.js 安装路径
|
||||
- **Git SSH 导致安装失败 (exit 128)** — npm 依赖使用 SSH 协议拉取 GitHub 仓库,用户没配 SSH Key 时报 `Permission denied (publickey)`。安装前自动执行 `git config --global url.https://...insteadOf ssh://...` 切换为 HTTPS
|
||||
- **npm 安装失败无引导** — 安装/升级 OpenClaw 失败时仅显示"安装失败",现在自动诊断错误类型(Git SSH 权限 / Git 未安装 / EPERM 文件占用 / MODULE_NOT_FOUND 安装不完整 / ENOENT / 权限不足 / 网络错误 / 缓存损坏)并给出具体修复命令
|
||||
|
||||
### 优化 (Improvements)
|
||||
|
||||
- **Node.js 路径扫描** — 检测不到 Node.js 时提供「自动扫描」按钮,扫描 C/D/E/F/G 盘常见安装路径(含 AI 工具目录),找到后一键选用
|
||||
- **手动指定 Node.js 路径** — 用户可手动输入 Node.js 安装目录,检测通过后自动保存到 `~/.openclaw/clawpanel.json`,后续所有命令自动使用
|
||||
- **跨平台检测引导** — 安装引导页 Node.js 检测失败时,macOS 提示从终端启动,Windows 提示重启 ClawPanel 或检查 PATH
|
||||
- **错误诊断模块** — 新增 `error-diagnosis.js` 共享模块,安装引导页和服务管理页共用错误诊断逻辑
|
||||
- **README 常见问题** — 新增 7 个常见安装问题的排查指南
|
||||
|
||||
## [0.4.1] - 2026-03-06
|
||||
|
||||
### 修复 (Bug Fixes)
|
||||
|
||||
- **macOS Node.js 检测失败** — Tauri 从 Finder 启动时 PATH 不含 `/usr/local/bin`、`/opt/homebrew/bin` 等常见路径,导致 `check_node`、`npm_command`、`openclaw_command` 找不到命令。新增 `enhanced_path()` 补充 nvm/volta/nodenv/fnm/n 等 Node.js 管理器路径
|
||||
- **npm 安装失败无引导** — 安装/升级 OpenClaw 失败时仅显示"安装失败",现在自动诊断错误类型(Git 未安装 / 文件访问 / 权限不足 / 网络错误 / 缓存损坏)并给出具体修复命令
|
||||
|
||||
### 优化 (Improvements)
|
||||
|
||||
- **macOS 检测引导** — 安装引导页 Node.js 检测失败时,macOS 用户会看到"已经装了但检测不到?"提示,引导从终端启动
|
||||
- **错误诊断模块** — 新增 `error-diagnosis.js` 共享模块,安装引导页和服务管理页共用错误诊断逻辑
|
||||
|
||||
## [0.4.0] - 2026-03-05
|
||||
|
||||
|
||||
22
README.md
22
README.md
@@ -294,11 +294,29 @@ open /Applications/ClawPanel.app
|
||||
|
||||
### Windows 安装报 exit 128 (access rights)
|
||||
|
||||
npm 依赖需要 Git,但系统未安装。请先安装 [Git for Windows](https://git-scm.com/download/win),安装后重启 ClawPanel。
|
||||
npm 依赖需要 Git。如果已装 Git 但仍报 128,是因为依赖用了 SSH 协议拉代码但你没配 GitHub SSH Key。运行以下命令改用 HTTPS:
|
||||
|
||||
```powershell
|
||||
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
|
||||
git config --global url."https://github.com/".insteadOf git@github.com:
|
||||
```
|
||||
|
||||
没装 Git 的请先安装 [Git for Windows](https://git-scm.com/download/win)。**v0.4.2+ 已自动配置 HTTPS 模式。**
|
||||
|
||||
### Windows 安装报 EPERM (operation not permitted)
|
||||
|
||||
文件被其他进程锁定。先关闭 ClawPanel 和所有 Node.js 进程,以管理员身份打开 PowerShell 重装:
|
||||
|
||||
```powershell
|
||||
npm cache clean --force
|
||||
npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com
|
||||
```
|
||||
|
||||
### 安装后 Node.js 检测不到(Windows)
|
||||
|
||||
安装 Node.js 后需要重启 ClawPanel(或重启电脑),新的 PATH 环境变量才能生效。
|
||||
安装 Node.js 后需要**重启 ClawPanel**,新的 PATH 环境变量才能生效。
|
||||
|
||||
如果安装在非默认路径(如 `D:\nodejs`、`F:\AI\Node`),请确认该目录已加入系统 PATH 环境变量。**v0.4.2+ 已自动扫描常见安装路径。**
|
||||
|
||||
## 相关项目
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。",
|
||||
"url": "https://claw.qt.cool/",
|
||||
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
|
||||
"softwareVersion": "0.4.1",
|
||||
"softwareVersion": "0.4.2",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "晴辰云 QingchenCloud",
|
||||
@@ -429,7 +429,7 @@
|
||||
<div class="orb orb-1" id="orb1"></div>
|
||||
<div class="orb orb-2" id="orb2"></div>
|
||||
<div class="hero-inner">
|
||||
<div class="reveal hero-badge"><span class="pulse"></span> v0.4.1 已发布 — Gateway 守护、配置同步、流式超时</div>
|
||||
<div class="reveal hero-badge"><span class="pulse"></span> v0.4.2 已发布 — Gateway 守护、配置同步、流式超时</div>
|
||||
<h1 class="reveal hero-title">管理你的 <span class="gradient-text shimmer">AI Agent</span><br>从未如此直观</h1>
|
||||
<p class="reveal hero-subtitle">ClawPanel 是 <strong>OpenClaw</strong> 的可视化管理面板,基于 Tauri v2 构建。<br>仪表盘、模型配置、实时聊天、记忆管理 — 一站式掌控。</p>
|
||||
<div class="reveal hero-cta">
|
||||
@@ -696,7 +696,7 @@
|
||||
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
|
||||
<div class="container-sm" style="position:relative;z-index:10">
|
||||
<div class="section-header">
|
||||
<div class="reveal download-version"><span class="pulse"></span> v0.4.1 最新版</div>
|
||||
<div class="reveal download-version"><span class="pulse"></span> v0.4.2 最新版</div>
|
||||
<h2 class="reveal section-title"><span class="gradient-text">下载安装</span></h2>
|
||||
<p class="reveal section-desc">选择你的操作系统,一键下载安装</p>
|
||||
</div>
|
||||
@@ -706,11 +706,11 @@
|
||||
<h3>macOS</h3>
|
||||
<p class="dl-desc">支持 Apple Silicon 和 Intel 芯片</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.1_aarch64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.2_aarch64.dmg" target="_blank" rel="noopener">
|
||||
Apple Silicon (M1/M2/M3/M4)
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.1_x64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.2_x64.dmg" target="_blank" rel="noopener">
|
||||
Intel 芯片
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
@@ -721,11 +721,11 @@
|
||||
<h3>Windows</h3>
|
||||
<p class="dl-desc">支持 Windows 10 及以上版本</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.1_x64-setup.exe" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.2_x64-setup.exe" target="_blank" rel="noopener">
|
||||
安装程序
|
||||
<span class="dl-format">.exe</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.1_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.2_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
MSI 安装包
|
||||
<span class="dl-format">.msi</span>
|
||||
</a>
|
||||
@@ -736,11 +736,11 @@
|
||||
<h3>Linux</h3>
|
||||
<p class="dl-desc">支持主流 Linux 发行版</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.1_amd64.AppImage" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.2_amd64.AppImage" target="_blank" rel="noopener">
|
||||
通用版
|
||||
<span class="dl-format">.AppImage</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.1_amd64.deb" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.4.2_amd64.deb" target="_blank" rel="noopener">
|
||||
Debian / Ubuntu
|
||||
<span class="dl-format">.deb</span>
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"private": true,
|
||||
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
|
||||
"type": "module",
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -328,7 +328,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpanel"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "ClawPanel",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"identifier": "ai.openclaw.clawpanel",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
export function diagnoseInstallError(errStr) {
|
||||
const s = errStr.toLowerCase()
|
||||
|
||||
// git SSH 权限问题(有 git 但没配 SSH Key)
|
||||
if (s.includes('permission denied (publickey)') || s.includes('ssh://git@github')) {
|
||||
return {
|
||||
title: '安装失败 — Git SSH 权限',
|
||||
hint: '依赖包用了 SSH 协议拉取代码,但你没配 GitHub SSH Key。运行以下命令改用 HTTPS:',
|
||||
command: 'git config --global url."https://github.com/".insteadOf ssh://git@github.com/',
|
||||
}
|
||||
}
|
||||
|
||||
// git 未安装(exit 128 + access rights)
|
||||
if (s.includes('code 128') || s.includes('exit 128') || s.includes('access rights')) {
|
||||
return {
|
||||
@@ -19,6 +28,24 @@ export function diagnoseInstallError(errStr) {
|
||||
}
|
||||
}
|
||||
|
||||
// EPERM(文件被占用/权限问题)
|
||||
if (s.includes('eperm') || s.includes('operation not permitted')) {
|
||||
return {
|
||||
title: '安装失败 — 文件被占用',
|
||||
hint: '有文件被锁定无法写入。先关闭所有 ClawPanel 和 Node.js 进程,然后在管理员终端手动安装:',
|
||||
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
|
||||
}
|
||||
}
|
||||
|
||||
// MODULE_NOT_FOUND(安装不完整)
|
||||
if (s.includes('module_not_found') || s.includes('cannot find module')) {
|
||||
return {
|
||||
title: '安装不完整',
|
||||
hint: '上次安装可能中断了。先清理残留再重装:',
|
||||
command: 'npm cache clean --force && npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
|
||||
}
|
||||
}
|
||||
|
||||
// ENOENT(文件找不到)
|
||||
if (s.includes('enoent') || s.includes('-4058') || s.includes('code -4058')) {
|
||||
return {
|
||||
|
||||
@@ -287,6 +287,9 @@ export const api = {
|
||||
// 安装/部署
|
||||
checkInstallation: () => cachedInvoke('check_installation', {}, 60000),
|
||||
checkNode: () => cachedInvoke('check_node', {}, 60000),
|
||||
checkNodeAtPath: (nodeDir) => invoke('check_node_at_path', { node_dir: nodeDir }),
|
||||
scanNodePaths: () => invoke('scan_node_paths'),
|
||||
saveCustomNodePath: (nodeDir) => invoke('save_custom_node_path', { node_dir: nodeDir }),
|
||||
getDeployConfig: () => cachedInvoke('get_deploy_config'),
|
||||
patchModelVision: () => invoke('patch_model_vision'),
|
||||
checkPanelUpdate: () => invoke('check_panel_update'),
|
||||
|
||||
@@ -86,11 +86,24 @@ function renderSteps(page, { node, cliOk, config }) {
|
||||
</p>
|
||||
<a class="btn btn-primary btn-sm" href="https://nodejs.org/" target="_blank" rel="noopener">下载 Node.js</a>
|
||||
<span class="form-hint" style="margin-left:8px">安装后点击「重新检测」</span>
|
||||
${isMacPlatform() ? `
|
||||
<div style="margin-top:var(--space-sm);padding:8px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.6">
|
||||
<strong>已经装了但检测不到?</strong> macOS 上从 Finder 启动可能找不到 Node.js。试试关掉 ClawPanel 后从终端启动:<br>
|
||||
<code style="background:var(--bg-secondary);padding:2px 6px;border-radius:3px;user-select:all">open /Applications/ClawPanel.app</code>
|
||||
</div>` : ''}`
|
||||
<strong>已经装了但检测不到?</strong>
|
||||
${isMacPlatform()
|
||||
? `macOS 上从 Finder 启动可能找不到 Node.js。试试关掉 ClawPanel 后从终端启动:<br>
|
||||
<code style="background:var(--bg-secondary);padding:2px 6px;border-radius:3px;user-select:all">open /Applications/ClawPanel.app</code>`
|
||||
: `安装 Node.js 后需要<strong>重启 ClawPanel</strong>,新的环境变量才能生效。`
|
||||
}
|
||||
<div style="margin-top:8px;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">🔍 自动扫描</button>
|
||||
<span style="color:var(--text-tertiary)">或手动指定路径:</span>
|
||||
</div>
|
||||
<div style="margin-top:6px;display:flex;gap:6px">
|
||||
<input id="input-node-path" type="text" placeholder="${isMacPlatform() ? '/usr/local/bin' : 'F:\\\\AI\\\\Node'}"
|
||||
style="flex:1;padding:4px 8px;border:1px solid var(--border-primary);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:11px;font-family:monospace">
|
||||
<button class="btn btn-primary btn-sm" id="btn-check-path" style="font-size:11px;padding:3px 10px">检测</button>
|
||||
</div>
|
||||
<div id="scan-result" style="margin-top:6px;display:none"></div>
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
`
|
||||
@@ -174,6 +187,66 @@ function bindEvents(page, nodeOk) {
|
||||
window.location.hash = '/dashboard'
|
||||
})
|
||||
|
||||
// 自动扫描 Node.js
|
||||
page.querySelector('#btn-scan-node')?.addEventListener('click', async () => {
|
||||
const btn = page.querySelector('#btn-scan-node')
|
||||
const resultEl = page.querySelector('#scan-result')
|
||||
btn.disabled = true
|
||||
btn.textContent = '扫描中...'
|
||||
resultEl.style.display = 'block'
|
||||
resultEl.innerHTML = '<span style="color:var(--text-tertiary)">正在扫描常见安装路径...</span>'
|
||||
try {
|
||||
const results = await api.scanNodePaths()
|
||||
if (results.length === 0) {
|
||||
resultEl.innerHTML = '<span style="color:var(--warning)">未找到 Node.js 安装,请手动指定路径或下载安装。</span>'
|
||||
} else {
|
||||
resultEl.innerHTML = results.map(r =>
|
||||
`<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
|
||||
<span style="color:var(--success)">✓</span>
|
||||
<code style="flex:1;background:var(--bg-secondary);padding:2px 6px;border-radius:3px;font-size:11px">${r.path}</code>
|
||||
<span style="font-size:11px;color:var(--text-tertiary)">${r.version}</span>
|
||||
<button class="btn btn-primary btn-sm btn-use-path" data-path="${r.path}" style="font-size:10px;padding:2px 8px">使用</button>
|
||||
</div>`
|
||||
).join('')
|
||||
resultEl.querySelectorAll('.btn-use-path').forEach(b => {
|
||||
b.addEventListener('click', async () => {
|
||||
await api.saveCustomNodePath(b.dataset.path)
|
||||
toast('Node.js 路径已保存,正在重新检测...', 'success')
|
||||
setTimeout(() => window.location.reload(), 500)
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
resultEl.innerHTML = `<span style="color:var(--danger)">扫描失败: ${e}</span>`
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
btn.textContent = '🔍 自动扫描'
|
||||
}
|
||||
})
|
||||
|
||||
// 手动指定路径检测
|
||||
page.querySelector('#btn-check-path')?.addEventListener('click', async () => {
|
||||
const input = page.querySelector('#input-node-path')
|
||||
const resultEl = page.querySelector('#scan-result')
|
||||
const dir = input?.value?.trim()
|
||||
if (!dir) { toast('请输入 Node.js 安装目录', 'warning'); return }
|
||||
resultEl.style.display = 'block'
|
||||
resultEl.innerHTML = '<span style="color:var(--text-tertiary)">检测中...</span>'
|
||||
try {
|
||||
const result = await api.checkNodeAtPath(dir)
|
||||
if (result.installed) {
|
||||
await api.saveCustomNodePath(dir)
|
||||
resultEl.innerHTML = `<span style="color:var(--success)">✓ 找到 Node.js ${result.version},路径已保存</span>`
|
||||
toast('Node.js 路径已保存,正在重新检测...', 'success')
|
||||
setTimeout(() => window.location.reload(), 500)
|
||||
} else {
|
||||
resultEl.innerHTML = `<span style="color:var(--warning)">该目录下未找到 node 可执行文件,请确认路径正确。</span>`
|
||||
}
|
||||
} catch (e) {
|
||||
resultEl.innerHTML = `<span style="color:var(--danger)">检测失败: ${e}</span>`
|
||||
}
|
||||
})
|
||||
|
||||
// 一键安装
|
||||
const installBtn = page.querySelector('#btn-install')
|
||||
if (!installBtn || !nodeOk) return
|
||||
|
||||
Reference in New Issue
Block a user