-
+
通用版
.AppImage
-
+
Debian / Ubuntu
.deb
diff --git a/package-lock.json b/package-lock.json
index 28edbb2..69ce7d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "clawpanel",
- "version": "0.16.4",
+ "version": "0.16.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawpanel",
- "version": "0.16.4",
+ "version": "0.16.5",
"license": "AGPL-3.0",
"dependencies": {
"@tauri-apps/api": "^2.5.0",
diff --git a/package.json b/package.json
index c56faff..590f3b3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "clawpanel",
- "version": "0.16.4",
+ "version": "0.16.5",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",
diff --git a/scripts/dev-api.js b/scripts/dev-api.js
index 35916e5..7a08d2f 100644
--- a/scripts/dev-api.js
+++ b/scripts/dev-api.js
@@ -585,6 +585,7 @@ function collectPreferredCliCandidates() {
if (!trimmed) continue
if (isWindows) {
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw.cmd'))
+ addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw.exe'))
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw'))
} else {
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw'))
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index c826c54..7754bc2 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -366,7 +366,7 @@ dependencies = [
[[package]]
name = "clawpanel"
-version = "0.16.4"
+version = "0.16.5"
dependencies = [
"base64 0.22.1",
"chrono",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index ee4e17f..6600b75 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
-version = "0.16.4"
+version = "0.16.5"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]
diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs
index adb6a41..a3b3313 100644
--- a/src-tauri/src/commands/config.rs
+++ b/src-tauri/src/commands/config.rs
@@ -2496,12 +2496,37 @@ fn scan_all_installations(
try_add(prefix_path.join("openclaw"));
}
if let Ok(localappdata) = std::env::var("LOCALAPPDATA") {
+ let localappdata_path = std::path::PathBuf::from(&localappdata);
try_add(
- std::path::PathBuf::from(&localappdata)
+ localappdata_path
+ .join("Programs")
+ .join("OpenClaw")
+ .join("openclaw.exe"),
+ );
+ try_add(localappdata_path.join("OpenClaw").join("openclaw.cmd"));
+ try_add(localappdata_path.join("OpenClaw").join("openclaw.exe"));
+ try_add(
+ localappdata_path
.join("Programs")
.join("nodejs")
.join("openclaw.cmd"),
);
+ try_add(
+ localappdata_path
+ .join("Programs")
+ .join("nodejs")
+ .join("openclaw.exe"),
+ );
+ try_add(
+ localappdata_path
+ .join("Programs")
+ .join("nodejs")
+ .join("node_modules")
+ .join("@qingchencloud")
+ .join("openclaw-zh")
+ .join("bin")
+ .join("openclaw.js"),
+ );
}
if let Ok(program_files) = std::env::var("ProgramFiles") {
try_add(
@@ -2607,6 +2632,15 @@ fn scan_all_installations(
#[cfg(target_os = "windows")]
{
try_add(base.join("openclaw.cmd"));
+ try_add(base.join("openclaw.exe"));
+ try_add(base.join("openclaw"));
+ try_add(
+ base.join("node_modules")
+ .join("@qingchencloud")
+ .join("openclaw-zh")
+ .join("bin")
+ .join("openclaw.js"),
+ );
}
#[cfg(not(target_os = "windows"))]
{
diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs
index e46309f..d3a5b74 100644
--- a/src-tauri/src/utils.rs
+++ b/src-tauri/src/utils.rs
@@ -1,6 +1,142 @@
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
+#[cfg(target_os = "windows")]
+fn push_unique_candidate(
+ candidates: &mut Vec
,
+ seen: &mut std::collections::HashSet,
+ path: std::path::PathBuf,
+) {
+ let key = path.to_string_lossy().replace('/', "\\").to_lowercase();
+ if seen.insert(key) {
+ candidates.push(path);
+ }
+}
+
+#[cfg(target_os = "windows")]
+fn push_windows_cli_files(
+ candidates: &mut Vec,
+ seen: &mut std::collections::HashSet,
+ base: std::path::PathBuf,
+) {
+ push_unique_candidate(candidates, seen, base.join("openclaw.cmd"));
+ push_unique_candidate(candidates, seen, base.join("openclaw.exe"));
+ push_unique_candidate(candidates, seen, base.join("openclaw"));
+ push_unique_candidate(
+ candidates,
+ seen,
+ base.join("node_modules")
+ .join("@qingchencloud")
+ .join("openclaw-zh")
+ .join("bin")
+ .join("openclaw.js"),
+ );
+ push_unique_candidate(
+ candidates,
+ seen,
+ base.join("node_modules")
+ .join("openclaw")
+ .join("bin")
+ .join("openclaw.js"),
+ );
+}
+
+#[cfg(target_os = "windows")]
+fn common_windows_cli_candidates() -> Vec {
+ let mut candidates = Vec::new();
+ let mut seen = std::collections::HashSet::new();
+
+ // 先按 enhanced PATH 顺序找,保持与用户命令行优先级一致。
+ for dir in crate::commands::enhanced_path().split(';') {
+ let dir = dir.trim();
+ if dir.is_empty() {
+ continue;
+ }
+ push_windows_cli_files(&mut candidates, &mut seen, std::path::PathBuf::from(dir));
+ }
+
+ if let Ok(appdata) = std::env::var("APPDATA") {
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ std::path::PathBuf::from(appdata).join("npm"),
+ );
+ }
+ if let Some(prefix) = crate::commands::windows_npm_global_prefix() {
+ push_windows_cli_files(&mut candidates, &mut seen, std::path::PathBuf::from(prefix));
+ }
+ for sa_dir in crate::commands::config::all_standalone_dirs() {
+ push_windows_cli_files(&mut candidates, &mut seen, sa_dir);
+ }
+ if let Ok(localappdata) = std::env::var("LOCALAPPDATA") {
+ let localappdata = std::path::PathBuf::from(localappdata);
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ localappdata.join("Programs").join("OpenClaw"),
+ );
+ push_windows_cli_files(&mut candidates, &mut seen, localappdata.join("OpenClaw"));
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ localappdata.join("Programs").join("nodejs"),
+ );
+ }
+ if let Ok(program_files) = std::env::var("ProgramFiles") {
+ let program_files = std::path::PathBuf::from(program_files);
+ push_windows_cli_files(&mut candidates, &mut seen, program_files.join("nodejs"));
+ push_windows_cli_files(&mut candidates, &mut seen, program_files.join("OpenClaw"));
+ }
+ if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") {
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ std::path::PathBuf::from(program_files_x86).join("nodejs"),
+ );
+ }
+ if let Ok(profile) = std::env::var("USERPROFILE") {
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ std::path::PathBuf::from(profile).join(".openclaw-bin"),
+ );
+ }
+ for drive in ["C", "D", "E", "F", "G"] {
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ std::path::PathBuf::from(format!(r"{drive}:\OpenClaw")),
+ );
+ push_windows_cli_files(
+ &mut candidates,
+ &mut seen,
+ std::path::PathBuf::from(format!(r"{drive}:\AI\OpenClaw")),
+ );
+ }
+
+ const CREATE_NO_WINDOW: u32 = 0x08000000;
+ let mut where_cmd = std::process::Command::new("where");
+ where_cmd.arg("openclaw");
+ where_cmd.env("PATH", crate::commands::enhanced_path());
+ where_cmd.creation_flags(CREATE_NO_WINDOW);
+ if let Ok(output) = where_cmd.output() {
+ if output.status.success() {
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ let trimmed = line.trim();
+ if !trimmed.is_empty() {
+ push_unique_candidate(
+ &mut candidates,
+ &mut seen,
+ std::path::PathBuf::from(trimmed),
+ );
+ }
+ }
+ }
+ }
+
+ candidates
+}
+
pub fn is_rejected_cli_path(cli_path: &str) -> bool {
let lower = cli_path.replace('\\', "/").to_lowercase();
lower.contains("/.cherrystudio/") || lower.contains("cherry-studio")
@@ -41,7 +177,7 @@ fn configured_cli_candidates() -> Vec {
.collect()
}
-/// Windows: 在 PATH 中查找 openclaw.cmd 的完整路径
+/// Windows: 在 PATH 和常见安装目录中查找 openclaw CLI 的完整路径
/// 避免通过 `cmd /c openclaw` 调用时 npm .cmd shim 中的引号导致
/// "\"node\"" is not recognized 错误
#[cfg(target_os = "windows")]
@@ -55,14 +191,9 @@ fn find_openclaw_cmd() -> Option {
return Some(candidate);
}
}
- let path = crate::commands::enhanced_path();
- for dir in path.split(';') {
- let candidate = std::path::Path::new(dir).join("openclaw.cmd");
- if candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy()) {
- return Some(candidate);
- }
- }
- None
+ common_windows_cli_candidates()
+ .into_iter()
+ .find(|candidate| candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy()))
}
#[cfg(not(target_os = "windows"))]
@@ -95,14 +226,7 @@ pub fn resolve_openclaw_cli_path() -> Option {
}
#[cfg(target_os = "windows")]
{
- let path = crate::commands::enhanced_path();
- for dir in path.split(';') {
- let candidate = std::path::Path::new(dir).join("openclaw.cmd");
- if candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy()) {
- return Some(candidate.to_string_lossy().to_string());
- }
- }
- None
+ find_openclaw_cmd().map(|p| p.to_string_lossy().to_string())
}
#[cfg(not(target_os = "windows"))]
{
@@ -161,7 +285,15 @@ pub fn openclaw_command() -> std::process::Command {
// 优先:找到 openclaw.cmd 完整路径,用 cmd /c "完整路径" 避免引号问题
if let Some(cmd_path) = find_openclaw_cmd() {
let mut cmd = std::process::Command::new("cmd");
- cmd.arg("/c").arg(cmd_path);
+ if cmd_path
+ .extension()
+ .and_then(|s| s.to_str())
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("js"))
+ {
+ cmd.arg("/c").arg("node").arg(cmd_path);
+ } else {
+ cmd.arg("/c").arg(cmd_path);
+ }
cmd.env("PATH", &enhanced);
apply_openclaw_dir_env(&mut cmd);
crate::commands::apply_proxy_env(&mut cmd);
@@ -197,7 +329,15 @@ pub fn openclaw_command_async() -> tokio::process::Command {
// 优先:找到 openclaw.cmd 完整路径
if let Some(cmd_path) = find_openclaw_cmd() {
let mut cmd = tokio::process::Command::new("cmd");
- cmd.arg("/c").arg(cmd_path);
+ if cmd_path
+ .extension()
+ .and_then(|s| s.to_str())
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("js"))
+ {
+ cmd.arg("/c").arg("node").arg(cmd_path);
+ } else {
+ cmd.arg("/c").arg(cmd_path);
+ }
cmd.env("PATH", &enhanced);
apply_openclaw_dir_env_tokio(&mut cmd);
crate::commands::apply_proxy_env_tokio(&mut cmd);
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 77d3200..eabe220 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
- "version": "0.16.4",
+ "version": "0.16.5",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",
diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js
index 81b48cb..ba40413 100644
--- a/src/pages/dashboard.js
+++ b/src/pages/dashboard.js
@@ -119,6 +119,22 @@ function dedupeOpenclawInstallations(list = []) {
return [...map.values()]
}
+function isCliMissingError(err) {
+ const message = String(err?.message || err || '')
+ return message.includes('openclaw CLI 未安装') || message.includes('CLI 未安装')
+}
+
+async function handleGatewayStartError(page, err, fallbackText) {
+ if (isForeignGatewayError(err)) {
+ await openGatewayConflict(page, err)
+ return
+ }
+ toast(humanizeError(err, fallbackText), isCliMissingError(err) ? 'warning' : 'error')
+ if (isCliMissingError(err)) {
+ await showInstallationCleanup({ onRefresh: () => loadDashboardData(page, true) })
+ }
+}
+
let _dashboardInitialized = false
let _dashboardVersionCache = null
let _dashboardStatusSummaryCache = null
@@ -760,8 +776,7 @@ function bindActions(page) {
toast(t('dashboard.gwStartSent'), 'success')
setTimeout(() => loadDashboardData(page), 2000)
} catch (err) {
- if (isForeignGatewayError(err)) await openGatewayConflict(page, err)
- else toast(t('dashboard.startFail') + ': ' + err, 'error')
+ await handleGatewayStartError(page, err, t('dashboard.startFail'))
}
finally { actionBtn.disabled = false; actionBtn.textContent = t('dashboard.startBtn') }
}
@@ -784,8 +799,7 @@ function bindActions(page) {
toast(t('dashboard.gwRestartSent'), 'success')
setTimeout(() => loadDashboardData(page), 3000)
} catch (err) {
- if (isForeignGatewayError(err)) await openGatewayConflict(page, err)
- else toast(t('dashboard.restartFail') + ': ' + err, 'error')
+ await handleGatewayStartError(page, err, t('dashboard.restartFail'))
}
finally { actionBtn.disabled = false; actionBtn.textContent = t('dashboard.restartBtn') }
}
@@ -798,8 +812,7 @@ function bindActions(page) {
try {
await api.restartService('ai.openclaw.gateway')
} catch (e) {
- if (isForeignGatewayError(e)) await openGatewayConflict(page, e)
- else toast(humanizeError(e, t('dashboard.restartFail')), 'error')
+ await handleGatewayStartError(page, e, t('dashboard.restartFail'))
btnRestart.disabled = false
btnRestart.classList.remove('btn-loading')
btnRestart.textContent = t('dashboard.restartGw')