diff --git a/README.md b/README.md index 35916c1..fe68f6c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,16 @@ ClawPanel 是 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslatio > 🌐 **官网**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下载**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +### ⚡ OpenClaw 独立安装包(零依赖,无需 Node.js/npm) + +不想折腾 Node.js 环境?直接下载 [OpenClaw 独立安装包](https://github.com/qingchencloud/openclaw-standalone/releases/latest),**内置运行时,解压即用**: + +- **Windows**: 下载 `.exe` 安装向导,双击即装 +- **macOS / Linux / 树莓派**: `curl -fsSL https://dl.qrj.ai/openclaw/install.sh | bash` +- **全平台**: [GitHub Releases](https://github.com/qingchencloud/openclaw-standalone/releases/latest) + +> ClawPanel 安装 OpenClaw 时会**自动优先使用独立安装包**,无需手动操作。此方案仅供不使用 ClawPanel 的用户独立安装。 + ### 🔥 开发板 / 嵌入式设备支持 ClawPanel 提供**纯 Web 版部署模式**(零 GUI 依赖),天然兼容 ARM64 开发板和嵌入式设备: diff --git a/docs/index.html b/docs/index.html index bc7b3e7..a45fd22 100644 --- a/docs/index.html +++ b/docs/index.html @@ -34,7 +34,7 @@ "description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。", "url": "https://claw.qt.cool/", "downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest", - "softwareVersion": "0.9.3", + "softwareVersion": "0.9.4", "author": { "@type": "Organization", "name": "晴辰云 QingchenCloud", @@ -1133,7 +1133,7 @@
-
v0.9.3 最新版
+
v0.9.4 最新版

下载安装

选择你的操作系统,一键下载安装

@@ -1143,11 +1143,11 @@

macOS

支持 Apple Silicon 和 Intel 芯片

+
+

⚡ OpenClaw 独立安装包零依赖

+

不想折腾 Node.js 环境?下载独立安装包,内置运行时,解压即用。支持 Windows / macOS / Linux / 树莓派。

+
+ + + 下载独立安装包 + + GitHub 项目 +
+

ClawPanel 安装 OpenClaw 时会自动优先使用独立安装包,无需手动下载

+

查看 所有版本 · 需要帮助?阅读 安装文档

国内网络下载慢?加入 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() {
-
+
+ + +
+
+