查看 所有版本 · 需要帮助?阅读 安装文档
国内网络下载慢?加入 QQ 群 或 微信群 获取安装包直传
diff --git a/openclaw-version-policy.json b/openclaw-version-policy.json
index ee643ef..f8b0e63 100644
--- a/openclaw-version-policy.json
+++ b/openclaw-version-policy.json
@@ -1,4 +1,8 @@
{
+ "standalone": {
+ "baseUrl": "https://dl.qrj.ai/openclaw-standalone",
+ "enabled": true
+ },
"r2": {
"baseUrl": "https://dl.qrj.ai/openclaw-zh",
"enabled": true
diff --git a/package.json b/package.json
index d5c7a79..457cbbf 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "clawpanel",
- "version": "0.9.3",
+ "version": "0.9.4",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",
diff --git a/scripts/dev-api.js b/scripts/dev-api.js
index 4dad103..8bd37e0 100644
--- a/scripts/dev-api.js
+++ b/scripts/dev-api.js
@@ -127,6 +127,97 @@ function r2Config() {
return policy?.r2 || { enabled: false }
}
+function standaloneConfig() {
+ const policy = loadVersionPolicy()
+ return policy?.standalone || { enabled: false }
+}
+
+function standalonePlatformKey() {
+ const arch = process.arch
+ const plat = process.platform
+ if (plat === 'win32' && arch === 'x64') return 'win-x64'
+ if (plat === 'darwin' && arch === 'arm64') return 'mac-arm64'
+ if (plat === 'darwin' && arch === 'x64') return 'mac-x64'
+ if (plat === 'linux' && arch === 'x64') return 'linux-x64'
+ if (plat === 'linux' && arch === 'arm64') return 'linux-arm64'
+ return 'unknown'
+}
+
+function standaloneInstallDir() {
+ if (isWindows) return path.join(process.env.LOCALAPPDATA || '', 'OpenClaw')
+ return path.join(os.homedir(), '.openclaw-bin')
+}
+
+async function _tryStandaloneInstall(version, logs, overrideBaseUrl = null) {
+ const cfg = standaloneConfig()
+ if (!cfg.enabled || !cfg.baseUrl) return false
+ const platform = standalonePlatformKey()
+ if (platform === 'unknown') throw new Error('当前平台不支持 standalone 安装包')
+ const installDir = standaloneInstallDir()
+
+ logs.push('📦 尝试 standalone 独立安装包(汉化版专属,自带 Node.js 运行时,无需 npm)')
+ logs.push('查询最新版本...')
+ const manifestUrl = `${cfg.baseUrl}/latest.json`
+ const resp = await globalThis.fetch(manifestUrl, { signal: AbortSignal.timeout(10000) })
+ if (!resp.ok) throw new Error(`standalone 清单不可用 (HTTP ${resp.status})`)
+ const manifest = await resp.json()
+
+ const remoteVersion = manifest.version
+ if (!remoteVersion) throw new Error('standalone 清单缺少 version 字段')
+ if (version !== 'latest' && !versionsMatch(remoteVersion, version)) {
+ throw new Error(`standalone 版本 ${remoteVersion} 与请求版本 ${version} 不匹配`)
+ }
+
+ const remoteBase = overrideBaseUrl || manifest.base_url || `${cfg.baseUrl}/${remoteVersion}`
+ const ext = isWindows ? 'zip' : 'tar.gz'
+ const filename = `openclaw-${remoteVersion}-${platform}.${ext}`
+ const downloadUrl = `${remoteBase}/${filename}`
+
+ logs.push(`从 CDN 下载: ${filename}`)
+
+ const tmpPath = path.join(os.tmpdir(), filename)
+ const dlResp = await globalThis.fetch(downloadUrl, { signal: AbortSignal.timeout(600000) })
+ if (!dlResp.ok) throw new Error(`standalone 下载失败 (HTTP ${dlResp.status})`)
+ const buffer = Buffer.from(await dlResp.arrayBuffer())
+ const sizeMb = (buffer.length / 1048576).toFixed(0)
+ logs.push(`下载完成 (${sizeMb}MB),解压安装中...`)
+ fs.writeFileSync(tmpPath, buffer)
+
+ // 清理旧安装 & 解压
+ if (fs.existsSync(installDir)) {
+ fs.rmSync(installDir, { recursive: true, force: true })
+ }
+ fs.mkdirSync(installDir, { recursive: true })
+
+ if (isWindows) {
+ // Windows: 用 PowerShell 解压 zip
+ execSync(`powershell -NoProfile -Command "Expand-Archive -Path '${tmpPath}' -DestinationPath '${installDir}' -Force"`, { windowsHide: true })
+ // 处理嵌套 openclaw/ 目录
+ const nested = path.join(installDir, 'openclaw')
+ if (fs.existsSync(nested) && fs.existsSync(path.join(nested, 'node.exe'))) {
+ for (const entry of fs.readdirSync(nested)) {
+ fs.renameSync(path.join(nested, entry), path.join(installDir, entry))
+ }
+ fs.rmSync(nested, { recursive: true, force: true })
+ }
+ } else {
+ // Unix: tar 解压
+ execSync(`tar -xzf "${tmpPath}" -C "${installDir}" --strip-components=1`, { windowsHide: true })
+ }
+
+ try { fs.unlinkSync(tmpPath) } catch {}
+
+ // 验证
+ const binFile = isWindows ? 'openclaw.cmd' : 'openclaw'
+ if (!fs.existsSync(path.join(installDir, binFile))) {
+ throw new Error('standalone 解压后未找到 openclaw 可执行文件')
+ }
+
+ logs.push(`✅ standalone 安装完成 (${remoteVersion})`)
+ logs.push(`安装目录: ${installDir}`)
+ return true
+}
+
function r2PlatformKey() {
const arch = process.arch // x64, arm64, etc.
const plat = process.platform // linux, darwin, win32
@@ -3159,7 +3250,7 @@ const handlers = {
throw new Error('查询版本失败: ' + (lastError?.message || lastError || 'unknown error'))
},
- async upgrade_openclaw({ source = 'chinese', version } = {}) {
+ async upgrade_openclaw({ source = 'chinese', version, method = 'auto' } = {}) {
const currentSource = detectInstalledSource()
const pkg = npmPackageName(source)
const recommended = recommendedVersionFor(source)
@@ -3170,16 +3261,30 @@ const handlers = {
const registry = pickRegistryForPackage(pkg)
const logs = []
- // ── R2 CDN 加速:优先尝试从 CDN 下载预装归档 ──
- if (source !== 'official') {
+ // ── standalone 安装(auto / standalone-r2 / standalone-github) ──
+ const tryStandalone = source !== 'official' && ['auto', 'standalone-r2', 'standalone-github'].includes(method)
+ if (tryStandalone) {
try {
- const r2Result = await _tryR2Install(ver, source, logs)
- if (r2Result) return logs.join('\n')
+ const githubBase = method === 'standalone-github'
+ ? `https://github.com/qingchencloud/openclaw-standalone/releases/download/v${ver}`
+ : null
+ const saResult = await _tryStandaloneInstall(ver, logs, githubBase)
+ if (saResult) {
+ const label = method === 'standalone-github' ? 'GitHub' : 'CDN'
+ logs.push(`✅ standalone (${label}) 安装完成`)
+ return logs.join('\n')
+ }
} catch (e) {
- logs.push(`CDN 加速不可用(${e.message}),降级到 npm 安装...`)
+ if (method === 'auto') {
+ logs.push(`standalone 不可用(${e.message}),降级到 npm 安装...`)
+ } else {
+ throw new Error(`standalone 安装失败: ${e.message}`)
+ }
}
}
+ // ── npm install(兜底或用户明确选择) ──
+
if (!version && recommended) {
logs.push(`ClawPanel ${PANEL_VERSION} 默认绑定 OpenClaw 稳定版: ${recommended}`)
}
@@ -3214,6 +3319,12 @@ const handlers = {
uninstall_openclaw({ cleanConfig = false } = {}) {
const npmBin = isWindows ? 'npm.cmd' : 'npm'
+ // 清理 standalone 安装
+ const saDir = standaloneInstallDir()
+ if (fs.existsSync(saDir)) {
+ try { fs.rmSync(saDir, { recursive: true, force: true }) } catch {}
+ }
+ // 清理 npm 安装
try { execSync(`${npmBin} uninstall -g openclaw 2>&1`, { timeout: 60000, windowsHide: true }) } catch {}
try { execSync(`${npmBin} uninstall -g @qingchencloud/openclaw-zh 2>&1`, { timeout: 60000, windowsHide: true }) } catch {}
if (cleanConfig && fs.existsSync(OPENCLAW_DIR)) {
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 624448f..aa8378b 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -328,7 +328,7 @@ dependencies = [
[[package]]
name = "clawpanel"
-version = "0.9.3"
+version = "0.9.4"
dependencies = [
"base64 0.22.1",
"chrono",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index a522334..429850e 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
-version = "0.9.3"
+version = "0.9.4"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]
diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs
index dc8c5c7..7b15fc5 100644
--- a/src-tauri/src/commands/config.rs
+++ b/src-tauri/src/commands/config.rs
@@ -75,8 +75,19 @@ struct R2Config {
enabled: bool,
}
+#[derive(Debug, Deserialize, Default)]
+struct StandaloneConfig {
+ #[serde(default)]
+ #[serde(rename = "baseUrl")]
+ base_url: Option
,
+ #[serde(default)]
+ enabled: bool,
+}
+
#[derive(Debug, Deserialize, Default)]
struct VersionPolicy {
+ #[serde(default)]
+ standalone: StandaloneConfig,
#[serde(default)]
r2: R2Config,
#[serde(default)]
@@ -128,6 +139,76 @@ fn r2_config() -> R2Config {
load_version_policy().r2
}
+fn standalone_config() -> StandaloneConfig {
+ load_version_policy().standalone
+}
+
+/// standalone 包的平台 key(与 CI 构建矩阵一致)
+fn standalone_platform_key() -> &'static str {
+ #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
+ { "win-x64" }
+ #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
+ { "mac-arm64" }
+ #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
+ { "mac-x64" }
+ #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
+ { "linux-x64" }
+ #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
+ { "linux-arm64" }
+ #[cfg(not(any(
+ all(target_os = "windows", target_arch = "x86_64"),
+ all(target_os = "macos", target_arch = "aarch64"),
+ all(target_os = "macos", target_arch = "x86_64"),
+ all(target_os = "linux", target_arch = "x86_64"),
+ all(target_os = "linux", target_arch = "aarch64"),
+ )))]
+ { "unknown" }
+}
+
+/// standalone 包的文件扩展名
+fn standalone_archive_ext() -> &'static str {
+ #[cfg(target_os = "windows")]
+ { "zip" }
+ #[cfg(not(target_os = "windows"))]
+ { "tar.gz" }
+}
+
+/// standalone 安装目录
+fn standalone_install_dir() -> Option {
+ #[cfg(target_os = "windows")]
+ {
+ // Inno Setup PrivilegesRequired=lowest 默认安装到 %LOCALAPPDATA%\Programs
+ std::env::var("LOCALAPPDATA").ok().map(|d| PathBuf::from(d).join("Programs").join("OpenClaw"))
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ dirs::home_dir().map(|h| h.join(".openclaw-bin"))
+ }
+}
+
+/// 所有可能的 standalone 安装位置(用于检测和卸载)
+fn all_standalone_dirs() -> Vec {
+ let mut dirs = Vec::new();
+ #[cfg(target_os = "windows")]
+ {
+ if let Ok(la) = std::env::var("LOCALAPPDATA") {
+ dirs.push(PathBuf::from(&la).join("Programs").join("OpenClaw"));
+ dirs.push(PathBuf::from(&la).join("OpenClaw"));
+ }
+ if let Ok(pf) = std::env::var("ProgramFiles") {
+ dirs.push(PathBuf::from(pf).join("OpenClaw"));
+ }
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ if let Some(h) = dirs::home_dir() {
+ dirs.push(h.join(".openclaw-bin"));
+ }
+ dirs.push(PathBuf::from("/opt/openclaw"));
+ }
+ dirs
+}
+
fn recommended_version_for(source: &str) -> Option {
let policy = load_version_policy();
let panel_entry = policy.panels.get(panel_version());
@@ -659,11 +740,38 @@ async fn get_local_version() -> Option {
}
}
}
- // Windows: 直接读 npm 全局目录下的 package.json,避免 spawn 进程
+ // Windows: 先查 standalone 安装,再查 npm 全局目录
#[cfg(target_os = "windows")]
{
+ // 检查所有 standalone 安装目录
+ for sa_dir in all_standalone_dirs() {
+ let version_file = sa_dir.join("VERSION");
+ if let Ok(content) = fs::read_to_string(&version_file) {
+ for line in content.lines() {
+ if let Some(ver) = line.strip_prefix("openclaw_version=") {
+ let ver = ver.trim();
+ if !ver.is_empty() {
+ return Some(ver.to_string());
+ }
+ }
+ }
+ }
+ let sa_pkg = sa_dir
+ .join("node_modules")
+ .join("@qingchencloud")
+ .join("openclaw-zh")
+ .join("package.json");
+ if let Ok(content) = fs::read_to_string(&sa_pkg) {
+ if let Some(ver) = serde_json::from_str::(&content)
+ .ok()
+ .and_then(|v| v.get("version")?.as_str().map(String::from))
+ {
+ return Some(ver);
+ }
+ }
+ }
+ // npm 全局目录
if let Ok(appdata) = std::env::var("APPDATA") {
- // 先查汉化版,再查官方版
for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] {
let pkg_json = PathBuf::from(&appdata)
.join("npm")
@@ -682,6 +790,25 @@ async fn get_local_version() -> Option {
}
}
// 所有平台通用 fallback: CLI 输出(异步)
+ // Windows: 先确认 openclaw 不是第三方程序(如 CherryStudio)
+ #[cfg(target_os = "windows")]
+ {
+ use std::os::windows::process::CommandExt;
+ if let Ok(o) = std::process::Command::new("where")
+ .arg("openclaw")
+ .creation_flags(0x08000000)
+ .output()
+ {
+ let stdout = String::from_utf8_lossy(&o.stdout).to_lowercase();
+ let all_third_party = stdout
+ .lines()
+ .filter(|l| !l.trim().is_empty())
+ .all(|l| l.contains(".cherrystudio") || l.contains("cherry-studio"));
+ if all_third_party {
+ return None;
+ }
+ }
+ }
use crate::utils::openclaw_command_async;
let output = openclaw_command_async()
.arg("--version")
@@ -730,6 +857,17 @@ fn detect_installed_source() -> String {
// Windows: 优先通过文件系统检测,避免 npm list 阻塞
#[cfg(target_os = "windows")]
{
+ // 检查所有可能的 standalone 安装目录
+ for sa_dir in all_standalone_dirs() {
+ let sa_zh = sa_dir
+ .join("node_modules")
+ .join("@qingchencloud")
+ .join("openclaw-zh");
+ if sa_zh.exists() {
+ return "chinese".into();
+ }
+ }
+ // 检查 npm 全局目录
if let Some(appdata) = std::env::var_os("APPDATA") {
let zh_dir = PathBuf::from(&appdata)
.join("npm")
@@ -740,7 +878,8 @@ fn detect_installed_source() -> String {
return "chinese".into();
}
}
- "official".into()
+ // 默认返回汉化版
+ "chinese".into()
}
// 所有平台通用: npm list 检测
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
@@ -875,11 +1014,12 @@ pub async fn upgrade_openclaw(
app: tauri::AppHandle,
source: String,
version: Option,
+ method: Option,
) -> Result {
let app2 = app.clone();
tauri::async_runtime::spawn(async move {
use tauri::Emitter;
- let result = upgrade_openclaw_inner(app2.clone(), source, version).await;
+ let result = upgrade_openclaw_inner(app2.clone(), source, version, method.unwrap_or_else(|| "auto".into())).await;
match result {
Ok(msg) => {
let _ = app2.emit("upgrade-done", &msg);
@@ -994,6 +1134,272 @@ fn npm_global_bin_dir() -> Option {
}
}
+/// 尝试从 standalone 独立安装包安装 OpenClaw(自带 Node.js,零依赖)
+/// 动态查询 latest.json 获取最新版本,下载对应平台的归档并解压
+/// 成功返回 Ok(版本号),失败返回 Err(原因) 供 caller 降级到 R2/npm
+async fn try_standalone_install(
+ app: &tauri::AppHandle,
+ version: &str,
+ override_base_url: Option<&str>,
+) -> Result {
+ let source_label = if override_base_url.is_some() { "GitHub" } else { "CDN" };
+ use tauri::Emitter;
+
+ let cfg = standalone_config();
+ if !cfg.enabled {
+ return Err("standalone 安装未启用".into());
+ }
+ let base_url = cfg.base_url.as_deref().ok_or("standalone baseUrl 未配置")?;
+ let platform = standalone_platform_key();
+ if platform == "unknown" {
+ return Err("当前平台不支持 standalone 安装包".into());
+ }
+ let install_dir = standalone_install_dir().ok_or("无法确定 standalone 安装目录")?;
+
+ // 1. 动态查询最新版本
+ let _ = app.emit("upgrade-log", "\u{1F4E6} 尝试 standalone 独立安装包(汉化版专属,自带 Node.js 运行时,无需 npm)");
+ let _ = app.emit("upgrade-log", "查询最新版本...");
+ let manifest_url = format!("{base_url}/latest.json");
+ let client = crate::commands::build_http_client(std::time::Duration::from_secs(10), None)
+ .map_err(|e| format!("HTTP 客户端创建失败: {e}"))?;
+ let manifest_resp = client
+ .get(&manifest_url)
+ .send()
+ .await
+ .map_err(|e| format!("standalone 清单获取失败: {e}"))?;
+ if !manifest_resp.status().is_success() {
+ return Err(format!("standalone 清单不可用 (HTTP {})", manifest_resp.status()));
+ }
+ let manifest: Value = manifest_resp
+ .json()
+ .await
+ .map_err(|e| format!("standalone 清单解析失败: {e}"))?;
+
+ let remote_version = manifest
+ .get("version")
+ .and_then(|v| v.as_str())
+ .ok_or("standalone 清单缺少 version 字段")?;
+
+ // 版本匹配检查
+ if version != "latest" && !versions_match(remote_version, version) {
+ return Err(format!(
+ "standalone 版本 {remote_version} 与请求版本 {version} 不匹配"
+ ));
+ }
+
+ let default_base = format!("{base_url}/{remote_version}");
+ let remote_base = if let Some(ovr) = override_base_url {
+ ovr
+ } else {
+ manifest
+ .get("base_url")
+ .and_then(|v| v.as_str())
+ .unwrap_or(&default_base)
+ };
+
+ // 2. 构造下载 URL
+ let ext = standalone_archive_ext();
+ let filename = format!("openclaw-{remote_version}-{platform}.{ext}");
+ let download_url = format!("{remote_base}/{filename}");
+
+ let _ = app.emit(
+ "upgrade-log",
+ format!("从 {source_label} 下载: {filename}"),
+ );
+ let _ = app.emit("upgrade-progress", 15);
+
+ // 3. 流式下载
+ let tmp_dir = std::env::temp_dir();
+ let archive_path = tmp_dir.join(&filename);
+ let dl_client =
+ crate::commands::build_http_client(std::time::Duration::from_secs(600), None)
+ .map_err(|e| format!("下载客户端创建失败: {e}"))?;
+ let dl_resp = dl_client
+ .get(&download_url)
+ .send()
+ .await
+ .map_err(|e| format!("standalone 下载失败: {e}"))?;
+ if !dl_resp.status().is_success() {
+ return Err(format!(
+ "standalone 下载失败 (HTTP {}): {download_url}",
+ dl_resp.status()
+ ));
+ }
+ let total_bytes = dl_resp.content_length().unwrap_or(0);
+ let size_mb = if total_bytes > 0 {
+ format!("{:.0}MB", total_bytes as f64 / 1_048_576.0)
+ } else {
+ "未知大小".into()
+ };
+ let _ = app.emit("upgrade-log", format!("下载中 ({size_mb})..."));
+
+ {
+ use futures_util::StreamExt;
+ use tokio::io::AsyncWriteExt;
+ let mut file = tokio::fs::File::create(&archive_path)
+ .await
+ .map_err(|e| format!("创建临时文件失败: {e}"))?;
+ let mut stream = dl_resp.bytes_stream();
+ let mut downloaded: u64 = 0;
+ let mut last_progress: u32 = 15;
+ while let Some(chunk) = stream.next().await {
+ let chunk = chunk.map_err(|e| format!("下载中断: {e}"))?;
+ file.write_all(&chunk)
+ .await
+ .map_err(|e| format!("写入失败: {e}"))?;
+ downloaded += chunk.len() as u64;
+ if total_bytes > 0 {
+ let pct = 15 + ((downloaded as f64 / total_bytes as f64) * 55.0) as u32;
+ if pct > last_progress {
+ last_progress = pct;
+ let _ = app.emit("upgrade-progress", pct.min(70));
+ }
+ }
+ }
+ file.flush()
+ .await
+ .map_err(|e| format!("刷新文件失败: {e}"))?;
+ }
+
+ let _ = app.emit("upgrade-log", "下载完成,解压安装中...");
+ let _ = app.emit("upgrade-progress", 72);
+
+ // 4. 清理旧安装 & 创建目录
+ if install_dir.exists() {
+ let _ = std::fs::remove_dir_all(&install_dir);
+ }
+ std::fs::create_dir_all(&install_dir)
+ .map_err(|e| format!("创建安装目录失败: {e}"))?;
+
+ // 5. 解压
+ #[cfg(target_os = "windows")]
+ {
+ // Windows: zip 解压
+ let archive_file = std::fs::File::open(&archive_path)
+ .map_err(|e| format!("打开归档失败: {e}"))?;
+ let mut zip_archive = zip::ZipArchive::new(archive_file)
+ .map_err(|e| format!("ZIP 解析失败: {e}"))?;
+ zip_archive
+ .extract(&install_dir)
+ .map_err(|e| format!("ZIP 解压失败: {e}"))?;
+ // 归档内可能有 openclaw/ 子目录,需要提升一层
+ let nested = install_dir.join("openclaw");
+ if nested.exists() && nested.join("node.exe").exists() {
+ for entry in std::fs::read_dir(&nested).map_err(|e| format!("读取目录失败: {e}"))? {
+ if let Ok(entry) = entry {
+ let dest = install_dir.join(entry.file_name());
+ let _ = std::fs::rename(entry.path(), &dest);
+ }
+ }
+ let _ = std::fs::remove_dir_all(&nested);
+ }
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ // Unix: tar.gz 解压
+ let status = Command::new("tar")
+ .args([
+ "-xzf",
+ &archive_path.to_string_lossy(),
+ "-C",
+ &install_dir.to_string_lossy(),
+ "--strip-components=1",
+ ])
+ .status()
+ .map_err(|e| format!("解压失败: {e}"))?;
+ if !status.success() {
+ return Err("tar 解压失败".into());
+ }
+ }
+
+ // 清理临时文件
+ let _ = std::fs::remove_file(&archive_path);
+ let _ = app.emit("upgrade-progress", 85);
+
+ // 6. 验证安装
+ #[cfg(target_os = "windows")]
+ let openclaw_bin = install_dir.join("openclaw.cmd");
+ #[cfg(not(target_os = "windows"))]
+ let openclaw_bin = install_dir.join("openclaw");
+
+ if !openclaw_bin.exists() {
+ return Err("standalone 解压后未找到 openclaw 可执行文件".into());
+ }
+
+ // 7. 添加到 PATH(Windows 用户 PATH,Unix 创建 symlink)
+ #[cfg(target_os = "windows")]
+ {
+ let install_str = install_dir.to_string_lossy().to_string();
+ // 检查是否已在 PATH 中
+ let current_path = std::env::var("PATH").unwrap_or_default();
+ if !current_path
+ .split(';')
+ .any(|p| p.eq_ignore_ascii_case(&install_str))
+ {
+ // 写入用户 PATH(注册表)
+ let _ = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-Command",
+ &format!(
+ "$p = [Environment]::GetEnvironmentVariable('Path','User'); if ($p -notlike '*{}*') {{ [Environment]::SetEnvironmentVariable('Path', $p + ';{}', 'User') }}",
+ install_str.replace('\'', "''"),
+ install_str.replace('\'', "''")
+ ),
+ ])
+ .creation_flags(0x08000000)
+ .status();
+ let _ = app.emit("upgrade-log", format!("已添加到 PATH: {install_str}"));
+ }
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ // Unix: 创建 /usr/local/bin/openclaw symlink 或 ~/bin/openclaw
+ let link_targets = [
+ PathBuf::from("/usr/local/bin/openclaw"),
+ dirs::home_dir()
+ .unwrap_or_default()
+ .join("bin")
+ .join("openclaw"),
+ ];
+ for link in &link_targets {
+ if let Some(parent) = link.parent() {
+ if parent.exists() {
+ let _ = std::fs::remove_file(link);
+ #[cfg(unix)]
+ {
+ if std::os::unix::fs::symlink(&openclaw_bin, link).is_ok() {
+ let _ = Command::new("chmod")
+ .args(["+x", &openclaw_bin.to_string_lossy()])
+ .status();
+ let _ = app.emit(
+ "upgrade-log",
+ format!("symlink 已创建: {}", link.display()),
+ );
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ let _ = app.emit("upgrade-progress", 95);
+ let _ = app.emit(
+ "upgrade-log",
+ format!("✅ standalone 独立安装包安装完成 ({remote_version})"),
+ );
+ let _ = app.emit(
+ "upgrade-log",
+ format!("安装目录: {}", install_dir.display()),
+ );
+
+ // 刷新 CLI 检测缓存
+ crate::commands::service::invalidate_cli_detection_cache();
+
+ Ok(remote_version.to_string())
+}
+
/// 尝试从 R2 CDN 下载预装归档安装 OpenClaw(跳过 npm 依赖解析)
/// 成功返回 Ok(版本号),失败返回 Err(原因) 供 caller 降级到 npm install
async fn try_r2_install(
@@ -1293,6 +1699,7 @@ async fn upgrade_openclaw_inner(
app: tauri::AppHandle,
source: String,
version: Option,
+ method: String,
) -> Result {
use std::io::{BufRead, BufReader};
use std::process::Stdio;
@@ -1309,29 +1716,46 @@ async fn upgrade_openclaw_inner(
.unwrap_or("latest");
let pkg = format!("{}@{}", pkg_name, ver);
- // ── R2 CDN 加速:优先尝试从 CDN 下载预装归档 ──
- if source != "official" {
- // 目前仅汉化版支持 R2 加速
- match try_r2_install(&app, ver, &source).await {
+ // ── standalone 安装(auto / standalone-r2 / standalone-github) ──
+ let try_standalone = source != "official"
+ && (method == "auto" || method == "standalone-r2" || method == "standalone-github");
+
+ if try_standalone {
+ // standalone-github 模式:使用 GitHub Releases 下载地址
+ let github_base = if method == "standalone-github" {
+ Some(format!(
+ "https://github.com/qingchencloud/openclaw-standalone/releases/download/v{}",
+ ver
+ ))
+ } else {
+ None
+ };
+ match try_standalone_install(&app, ver, github_base.as_deref()).await {
Ok(installed_ver) => {
let _ = app.emit("upgrade-progress", 100);
- // 刷新缓存
super::refresh_enhanced_path();
crate::commands::service::invalidate_cli_detection_cache();
- let msg = format!("✅ CDN 加速安装完成,当前版本: {installed_ver}");
+ let label = if method == "standalone-github" { "GitHub" } else { "CDN" };
+ let msg = format!("✅ standalone ({label}) 安装完成,当前版本: {installed_ver}");
let _ = app.emit("upgrade-log", &msg);
return Ok(msg);
}
Err(reason) => {
- let _ = app.emit(
- "upgrade-log",
- format!("CDN 加速不可用({reason}),降级到 npm 安装..."),
- );
- let _ = app.emit("upgrade-progress", 5);
+ if method == "auto" {
+ let _ = app.emit(
+ "upgrade-log",
+ format!("standalone 不可用({reason}),降级到 npm 安装..."),
+ );
+ let _ = app.emit("upgrade-progress", 5);
+ } else {
+ return Err(format!("standalone 安装失败: {reason}"));
+ }
}
}
}
+ // ── npm install(兜底或用户明确选择) ──
+
// 切换源时需要卸载旧包,但为避免安装失败导致 CLI 丢失,
// 先安装新包,成功后再卸载旧包
let old_pkg = npm_package_name(¤t_source);
@@ -1637,7 +2061,19 @@ async fn uninstall_openclaw_inner(
let _ = openclaw_command().args(["gateway", "uninstall"]).output();
}
- // 3. npm uninstall
+ // 3. 清理 standalone 安装(所有可能的位置)
+ for sa_dir in &all_standalone_dirs() {
+ if sa_dir.exists() {
+ let _ = app.emit("upgrade-log", format!("清理 standalone 安装: {}", sa_dir.display()));
+ if let Err(e) = std::fs::remove_dir_all(sa_dir) {
+ let _ = app.emit("upgrade-log", format!("⚠️ 清理 standalone 失败: {e}(可能需要管理员权限)"));
+ } else {
+ let _ = app.emit("upgrade-log", "standalone 安装已清理 ✓");
+ }
+ }
+ }
+
+ // 4. npm uninstall
let _ = app.emit("upgrade-log", format!("$ npm uninstall -g {pkg}"));
let _ = app.emit("upgrade-progress", 20);
diff --git a/src-tauri/src/commands/service.rs b/src-tauri/src/commands/service.rs
index 497e648..1ba6ce7 100644
--- a/src-tauri/src/commands/service.rs
+++ b/src-tauri/src/commands/service.rs
@@ -653,6 +653,16 @@ mod platform {
fn candidate_cli_paths() -> Vec {
let mut candidates = Vec::new();
+ // standalone 安装目录(优先检测,覆盖所有可能位置)
+ if let Ok(localappdata) = env::var("LOCALAPPDATA") {
+ // Inno Setup PrivilegesRequired=lowest 默认路径
+ candidates.push(Path::new(&localappdata).join("Programs").join("OpenClaw").join("openclaw.cmd"));
+ candidates.push(Path::new(&localappdata).join("OpenClaw").join("openclaw.cmd"));
+ }
+ if let Ok(pf) = env::var("ProgramFiles") {
+ candidates.push(Path::new(&pf).join("OpenClaw").join("openclaw.cmd"));
+ }
+
if let Ok(appdata) = env::var("APPDATA") {
candidates.push(Path::new(&appdata).join("npm").join("openclaw.cmd"));
}
@@ -698,24 +708,24 @@ mod platform {
}
// 方式2: 通过 where 查找(兼容 nvm、自定义 prefix 等)
+ // 过滤掉第三方 openclaw(如 CherryStudio 的 .cherrystudio/bin/openclaw.exe)
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(o) = where_cmd.output() {
- if o.status.success() && !String::from_utf8_lossy(&o.stdout).trim().is_empty() {
- return true;
- }
- }
-
- // 方式3: 直接执行版本命令兜底
- let mut cmd = std::process::Command::new("cmd");
- cmd.args(["/c", "openclaw", "--version"]);
- cmd.env("PATH", crate::commands::enhanced_path());
- cmd.creation_flags(CREATE_NO_WINDOW);
- if let Ok(o) = cmd.output() {
if o.status.success() {
- return true;
+ let stdout = String::from_utf8_lossy(&o.stdout);
+ for line in stdout.lines() {
+ let p = line.trim().to_lowercase();
+ // 跳过已知第三方 openclaw 路径
+ if p.contains(".cherrystudio") || p.contains("cherry-studio") {
+ continue;
+ }
+ if !p.is_empty() {
+ return true;
+ }
+ }
}
}
false
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 372b918..fdcced0 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.9.3",
+ "version": "0.9.4",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",
@@ -42,7 +42,10 @@
"silent": true
},
"nsis": {
- "languages": ["SimpChinese", "English"],
+ "languages": [
+ "SimpChinese",
+ "English"
+ ],
"displayLanguageSelector": true
}
}
diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js
index cee8568..f8ae335 100644
--- a/src/lib/tauri-api.js
+++ b/src/lib/tauri-api.js
@@ -175,7 +175,7 @@ export const api = {
reloadGateway: () => invoke('reload_gateway'),
restartGateway: () => invoke('restart_gateway'),
listOpenclawVersions: (source = 'chinese') => invoke('list_openclaw_versions', { source }),
- upgradeOpenclaw: (source = 'chinese', version = null) => invoke('upgrade_openclaw', { source, version }),
+ upgradeOpenclaw: (source = 'chinese', version = null, method = 'auto') => invoke('upgrade_openclaw', { source, version, method }),
uninstallOpenclaw: (cleanConfig = false) => invoke('uninstall_openclaw', { cleanConfig }),
installGateway: () => invoke('install_gateway'),
uninstallGateway: () => invoke('uninstall_gateway'),
diff --git a/src/pages/services.js b/src/pages/services.js
index 2e5d078..55f1201 100644
--- a/src/pages/services.js
+++ b/src/pages/services.js
@@ -505,7 +505,7 @@ async function handleSaveConfig(page, restart) {
// ===== 升级操作 =====
-async function doUpgradeWithModal(source, page, version = null) {
+async function doUpgradeWithModal(source, page, version = null, method = 'auto') {
const modal = showUpgradeModal('升级 / 切换版本')
let unlistenLog, unlistenProgress, unlistenDone, unlistenError
setUpgrading(true)
@@ -549,12 +549,12 @@ async function doUpgradeWithModal(source, page, version = null) {
})
// 发起后台任务(立即返回)
- await api.upgradeOpenclaw(source, version)
+ await api.upgradeOpenclaw(source, version, method)
modal.appendLog('后台任务已启动,请等待完成...')
} else {
// Web 模式:仍然同步等待(dev-api 后端没有 spawn)
modal.appendLog('Web 模式:升级过程日志不可用,请等待完成...')
- const msg = await api.upgradeOpenclaw(source, version)
+ const msg = await api.upgradeOpenclaw(source, version, method)
modal.setDone(typeof msg === 'string' ? msg : (msg?.message || '升级完成'))
await loadVersion(page)
cleanup()
diff --git a/src/pages/setup.js b/src/pages/setup.js
index 330a7f5..c4bfd3f 100644
--- a/src/pages/setup.js
+++ b/src/pages/setup.js
@@ -325,7 +325,17 @@ function renderInstallSection() {