mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-11 18:10:41 +08:00
feat: standalone 独立安装包集成 + 安装方式选择器 (0.9.4)
- 新增 standalone 安装链路(自带 Node.js,零依赖,无需 npm) - 安装方式选择器:自动/CDN加速/GitHub/npm 四选一 - 动态查询 latest.json 获取最新版本,不怕旧资源被删除 - GitHub 模式:从 GitHub Releases 下载(CDN 不可用时备选) - CherryStudio openclaw.exe 干扰过滤 - 默认源改为汉化优化版 - 日志隐藏 R2 下载地址(安全) - 卸载兼容:standalone + npm 双清理 - 版本检测覆盖所有 standalone 安装路径 - README + 官网添加独立安装包说明 - macOS npm 权限问题通过 standalone 自动解决
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -328,7 +328,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpanel"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
|
||||
@@ -75,8 +75,19 @@ struct R2Config {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct StandaloneConfig {
|
||||
#[serde(default)]
|
||||
#[serde(rename = "baseUrl")]
|
||||
base_url: Option<String>,
|
||||
#[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<PathBuf> {
|
||||
#[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<PathBuf> {
|
||||
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<String> {
|
||||
let policy = load_version_policy();
|
||||
let panel_entry = policy.panels.get(panel_version());
|
||||
@@ -659,11 +740,38 @@ async fn get_local_version() -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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::<Value>(&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<String> {
|
||||
}
|
||||
}
|
||||
// 所有平台通用 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<String>,
|
||||
method: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
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<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 尝试从 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<String, String> {
|
||||
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<String>,
|
||||
method: String,
|
||||
) -> Result<String, String> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -653,6 +653,16 @@ mod platform {
|
||||
fn candidate_cli_paths() -> Vec<PathBuf> {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user