mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
fix: 重构版本源检测逻辑 + standalone 目录集中化 + Linux 平台检测补全 (#161)
* fix: Windows 版本检测错误——优先从活跃 CLI 读取版本,修复源判断和包检查顺序 - get_local_version() Windows 块新增活跃 CLI 优先检测(与 macOS 一致), 避免残留的 standalone 汉化版目录被优先扫描到 - detect_installed_source() 修复 standalone 被错误映射为 official(应为 chinese) - read_version_from_installation() 根据 classify_cli_source 动态决定 package.json 检查顺序,避免硬编码汉化版优先导致官方版用户看到错误版本 🤖 Generated with [Qoder][https://qoder.com] * fix: Windows 版本检测忽略残留文件,仅当 CLI 二进制存在时才读取版本 standalone 目录和 npm 全局目录中可能存在卸载后的残留 node_modules/ package.json,导致面板误判 OpenClaw 已安装并显示错误版本号。 现在在读取版本前先检查 openclaw.cmd 是否实际存在。 🤖 Generated with [Qoder][https://qoder.com] * fix: 重构版本源检测逻辑,修复跨源切换后显示旧源的问题 - 新增 detect_source_from_cmd_shim() 通过读取 Windows .cmd shim 内容判断 npm 包归属,替代不可靠的文件系统残留目录扫描 - 重写 detect_installed_source() Windows 检测块,优先使用 shim 内容信号 - upgrade_openclaw_inner() 跨源切换时清理 standalone 安装目录 - get_local_version() npm 段改用 shim 内容判断活跃包 - build_enhanced_path() 三平台添加 standalone 安装目录,避免 dashboard 超时 - 所有平台 fallback 从 "official" 改为 "unknown",支持非面板安装场景 - 前端 dashboard/about 支持 official/chinese/unknown 三源显示 - 新增 unknownSource i18n key(中/英/繁/日/韩) 🤖 Generated with [Qoder][https://qoder.com] * fix: 移除 config.rs 末尾多余的闭合括号,修复编译错误 🤖 Generated with [Qoder][https://qoder.com] * fix: 移除 config.rs 末尾重复的 configure_git_https 和 invalidate_path_cache 定义 🤖 Generated with [Qoder][https://qoder.com] * refactor: standalone 目录集中化、unsafe set_var 适配、Linux 平台检测补全 - 提升 all_standalone_dirs() / standalone_install_dir() 为 pub(crate), 作为全局唯一的 standalone 路径来源 - build_enhanced_path() 三平台块改为调用 config::all_standalone_dirs() - service.rs 三平台 candidate 函数改为调用 super::config::all_standalone_dirs() - utils.rs common_non_windows_cli_candidates() 改为调用集中函数 - std::env::set_var 包裹 unsafe 块,附 SAFETY 注释,适配 Rust 1.83+ - 补全 Linux detect_installed_source(): CLI 路径分类 -> symlink -> standalone -> npm list - 补全 Linux get_local_version(): 活跃 CLI -> standalone VERSION -> symlink package.json 🤖 Generated with [Qoder][https://qoder.com] * fix: service.rs 模块路径修正 super::config -> crate::commands::config mod platform 内部 super 指向 service 模块而非 commands, 需要完整路径 crate::commands::config 才能访问 all_standalone_dirs() 🤖 Generated with [Qoder][https://qoder.com] --------- Co-authored-by: SEVENTEEN-TAN <SEVENTEEN-TAN@users.noreply.github.com>
This commit is contained in:
@@ -192,7 +192,7 @@ fn standalone_archive_ext() -> &'static str {
|
||||
}
|
||||
|
||||
/// standalone 安装目录
|
||||
fn standalone_install_dir() -> Option<PathBuf> {
|
||||
pub(crate) fn standalone_install_dir() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Inno Setup PrivilegesRequired=lowest 默认安装到 %LOCALAPPDATA%\Programs
|
||||
@@ -207,7 +207,7 @@ fn standalone_install_dir() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// 所有可能的 standalone 安装位置(用于检测和卸载)
|
||||
fn all_standalone_dirs() -> Vec<PathBuf> {
|
||||
pub(crate) fn all_standalone_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -1194,7 +1194,22 @@ async fn get_local_version() -> Option<String> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 优先从活跃 CLI 路径读取版本(与 macOS 逻辑一致)
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let cli_pb = PathBuf::from(&cli_path);
|
||||
let resolved = std::fs::canonicalize(&cli_pb).unwrap_or_else(|_| cli_pb.clone());
|
||||
if let Some(ver) = read_version_from_installation(&resolved)
|
||||
.or_else(|| read_version_from_installation(&cli_pb))
|
||||
{
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
// 仅当 CLI 二进制实际存在时才读取版本,避免残留文件误判为已安装
|
||||
if !sa_dir.join("openclaw.cmd").exists() {
|
||||
continue;
|
||||
}
|
||||
let version_file = sa_dir.join("VERSION");
|
||||
if let Ok(content) = fs::read_to_string(&version_file) {
|
||||
for line in content.lines() {
|
||||
@@ -1222,21 +1237,87 @@ async fn get_local_version() -> Option<String> {
|
||||
}
|
||||
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let cli_is_zh = crate::utils::resolve_openclaw_cli_path()
|
||||
.map(|p| crate::utils::classify_cli_source(&p) == "npm-zh")
|
||||
.unwrap_or(false);
|
||||
let pkgs: &[&str] = if cli_is_zh {
|
||||
&["@qingchencloud/openclaw-zh", "openclaw"]
|
||||
let npm_bin = PathBuf::from(&appdata).join("npm");
|
||||
let shim_path = npm_bin.join("openclaw.cmd");
|
||||
// 仅当 npm 全局 CLI shim 存在时才读取版本
|
||||
if !shim_path.exists() {
|
||||
// npm 全局无 CLI shim,跳过
|
||||
} else {
|
||||
&["openclaw", "@qingchencloud/openclaw-zh"]
|
||||
};
|
||||
for pkg in pkgs {
|
||||
let pkg_json = PathBuf::from(&appdata)
|
||||
.join("npm")
|
||||
.join("node_modules")
|
||||
.join(pkg)
|
||||
.join("package.json");
|
||||
if let Ok(content) = fs::read_to_string(&pkg_json) {
|
||||
// 读 .cmd 内容判断活跃包,而非依赖 classify_cli_source(路径无法区分)
|
||||
let is_zh = detect_source_from_cmd_shim(&shim_path)
|
||||
.map(|s| s == "chinese")
|
||||
.unwrap_or(false);
|
||||
let pkgs: &[&str] = if is_zh {
|
||||
&["@qingchencloud/openclaw-zh", "openclaw"]
|
||||
} else {
|
||||
&["openclaw", "@qingchencloud/openclaw-zh"]
|
||||
};
|
||||
for pkg in pkgs {
|
||||
let pkg_json = npm_bin.join("node_modules").join(pkg).join("package.json");
|
||||
if let Ok(content) = fs::read_to_string(&pkg_json) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Linux: 参照 macOS/Windows 实现,完整检测链
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// 1. 活跃 CLI 优先
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let cli_pb = PathBuf::from(&cli_path);
|
||||
let resolved = std::fs::canonicalize(&cli_pb).unwrap_or_else(|_| cli_pb.clone());
|
||||
if let Some(ver) = read_version_from_installation(&resolved)
|
||||
.or_else(|| read_version_from_installation(&cli_pb))
|
||||
{
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
// 2. standalone 目录
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
if !sa_dir.join("openclaw").exists() {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. symlink -> package.json
|
||||
if let Ok(target) = fs::read_link("/usr/local/bin/openclaw") {
|
||||
let pkg_json = PathBuf::from("/usr/local/bin")
|
||||
.join(&target)
|
||||
.parent()
|
||||
.map(|p| p.join("package.json"));
|
||||
if let Some(ref pkg_path) = pkg_json {
|
||||
if let Ok(content) = fs::read_to_string(pkg_path) {
|
||||
if let Some(ver) = serde_json::from_str::<Value>(&content)
|
||||
.ok()
|
||||
.and_then(|v| v.get("version")?.as_str().map(String::from))
|
||||
@@ -1298,9 +1379,27 @@ async fn get_latest_version_for(source: &str) -> Option<String> {
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
/// 从 Windows .cmd shim 文件内容判断实际关联的 npm 包来源
|
||||
/// npm 生成的 shim 末尾引用实际 JS 入口,据此区分官方版与汉化版
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_source_from_cmd_shim(cmd_path: &std::path::Path) -> Option<String> {
|
||||
let content = std::fs::read_to_string(cmd_path).ok()?;
|
||||
let lower = content.to_lowercase();
|
||||
// 汉化版标记:@qingchencloud 或 openclaw-zh
|
||||
if lower.contains("openclaw-zh") || lower.contains("@qingchencloud") {
|
||||
return Some("chinese".into());
|
||||
}
|
||||
// 确认是 npm shim(含 node_modules 引用)→ 官方版
|
||||
if lower.contains("node_modules") {
|
||||
return Some("official".into());
|
||||
}
|
||||
// standalone 的 .cmd 可能不含 node_modules(自定义脚本),由 classify 处理
|
||||
None
|
||||
}
|
||||
|
||||
/// 检测当前安装的是官方版还是汉化版
|
||||
/// macOS: 优先检查 homebrew symlink,fallback 到 npm list
|
||||
/// Windows: 优先检查 npm 全局目录下的 package.json,避免调用 npm list 阻塞
|
||||
/// macOS: 优先检查 symlink 指向的实际路径
|
||||
/// Windows: 读取 .cmd shim 内容判断实际关联的包
|
||||
/// Linux: 直接用 npm list
|
||||
fn detect_installed_source() -> String {
|
||||
// macOS: 检查 openclaw bin 的 symlink 指向
|
||||
@@ -1332,49 +1431,73 @@ fn detect_installed_source() -> String {
|
||||
return "chinese".into();
|
||||
}
|
||||
}
|
||||
"official".into()
|
||||
"unknown".into()
|
||||
}
|
||||
// Windows: 优先通过 CLI 路径判断,fallback 到文件系统检测
|
||||
// Windows: 通过活跃 CLI 的 .cmd shim 内容判断来源
|
||||
// npm 生成的 .cmd shim 最后一行包含实际 JS 入口路径,例如:
|
||||
// "%dp0%\node_modules\openclaw\bin\openclaw.js" → 官方版
|
||||
// "%dp0%\node_modules\@qingchencloud\openclaw-zh\..." → 汉化版
|
||||
// 读取内容即可一锤定音,不依赖文件系统扫描(避免残留目录误判)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 优先通过实际 CLI 路径判断
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let source = crate::utils::classify_cli_source(&cli_path);
|
||||
if source == "npm-zh" {
|
||||
// 路径本身能确定的情况(standalone 目录、npm-zh 路径含 openclaw-zh)
|
||||
if source == "npm-zh" || source == "standalone" {
|
||||
return "chinese".into();
|
||||
}
|
||||
if source == "standalone" {
|
||||
return "official".into();
|
||||
}
|
||||
// npm-official/npm-global: Windows .cmd shim 路径不含包名,需继续检查文件系统
|
||||
}
|
||||
// 检查所有可能的 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-official / npm-global / unknown: 路径不含包名,读 .cmd 内容判断
|
||||
if let Some(shim_source) = detect_source_from_cmd_shim(std::path::Path::new(&cli_path))
|
||||
{
|
||||
return shim_source;
|
||||
}
|
||||
}
|
||||
// 检查 npm 全局目录
|
||||
if let Some(appdata) = std::env::var_os("APPDATA") {
|
||||
let zh_dir = PathBuf::from(&appdata)
|
||||
.join("npm")
|
||||
.join("node_modules")
|
||||
.join("@qingchencloud")
|
||||
.join("openclaw-zh");
|
||||
if zh_dir.exists() {
|
||||
return "chinese".into();
|
||||
// 无活跃 CLI 时的兜底:仅检查 npm 全局目录中实际存在的 shim
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let shim = PathBuf::from(&appdata).join("npm").join("openclaw.cmd");
|
||||
if let Some(s) = detect_source_from_cmd_shim(&shim) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
// 默认返回官方版
|
||||
"official".into()
|
||||
// 确实无法判断
|
||||
"unknown".into()
|
||||
}
|
||||
// 所有平台通用: npm list 检测
|
||||
// Linux: 参照 macOS 实现,完整检测链
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
{
|
||||
// 1. 活跃 CLI 路径分类(与 macOS 一致)
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let resolved = std::fs::canonicalize(&cli_path)
|
||||
.ok()
|
||||
.unwrap_or_else(|| PathBuf::from(&cli_path));
|
||||
let source = crate::utils::classify_cli_source(&resolved.to_string_lossy());
|
||||
if source == "npm-zh" || source == "standalone" {
|
||||
return "chinese".into();
|
||||
}
|
||||
if source == "npm-official" || source == "npm-global" {
|
||||
return "official".into();
|
||||
}
|
||||
}
|
||||
// 2. 检查 symlink 指向(/usr/local/bin/openclaw, ~/bin/openclaw)
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
for link in &[
|
||||
PathBuf::from("/usr/local/bin/openclaw"),
|
||||
home.join("bin").join("openclaw"),
|
||||
] {
|
||||
if let Ok(target) = std::fs::read_link(link) {
|
||||
if target.to_string_lossy().contains("openclaw-zh") {
|
||||
return "chinese".into();
|
||||
}
|
||||
return "official".into();
|
||||
}
|
||||
}
|
||||
// 3. standalone 目录检测
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
if sa_dir.join("openclaw").exists() || sa_dir.join("VERSION").exists() {
|
||||
return "chinese".into();
|
||||
}
|
||||
}
|
||||
// 4. npm list 兜底
|
||||
if let Ok(o) = npm_command()
|
||||
.args(["list", "-g", "@qingchencloud/openclaw-zh", "--depth=0"])
|
||||
.output()
|
||||
@@ -1383,7 +1506,7 @@ fn detect_installed_source() -> String {
|
||||
return "chinese".into();
|
||||
}
|
||||
}
|
||||
"official".into()
|
||||
"unknown".into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1391,14 +1514,23 @@ fn detect_installed_source() -> String {
|
||||
pub async fn get_version_info() -> Result<VersionInfo, String> {
|
||||
let current = get_local_version().await;
|
||||
let mut source = detect_installed_source();
|
||||
// 兜底:版本号含 -zh 则一定是汉化版(文件系统检测可能误判)
|
||||
// 兜底:版本号含 -zh 则一定是汉化版
|
||||
if let Some(ref ver) = current {
|
||||
if ver.contains("-zh") && source != "chinese" {
|
||||
source = "chinese".to_string();
|
||||
}
|
||||
}
|
||||
let latest = get_latest_version_for(&source).await;
|
||||
let recommended = recommended_version_for(&source);
|
||||
// unknown 来源不查询 latest/recommended(无法确定对应哪个 npm 包)
|
||||
let latest = if source == "unknown" {
|
||||
None
|
||||
} else {
|
||||
get_latest_version_for(&source).await
|
||||
};
|
||||
let recommended = if source == "unknown" {
|
||||
None
|
||||
} else {
|
||||
recommended_version_for(&source)
|
||||
};
|
||||
let update_available = match (¤t, &recommended) {
|
||||
(Some(c), Some(r)) => recommended_is_newer(r, c),
|
||||
(None, Some(_)) => true,
|
||||
@@ -1546,8 +1678,16 @@ fn read_version_from_installation(cli_path: &std::path::Path) -> Option<String>
|
||||
}
|
||||
}
|
||||
}
|
||||
// 根据 CLI 路径判断来源,决定 package.json 检查顺序
|
||||
// 避免残留的另一来源包被优先读取
|
||||
let cli_source = crate::utils::classify_cli_source(&cli_path.to_string_lossy());
|
||||
let pkg_names: &[&str] = if cli_source == "npm-zh" || cli_source == "standalone" {
|
||||
&["@qingchencloud/openclaw-zh", "openclaw"]
|
||||
} else {
|
||||
&["openclaw", "@qingchencloud/openclaw-zh"]
|
||||
};
|
||||
// 尝试从 package.json 读取
|
||||
for pkg_name in &["@qingchencloud/openclaw-zh", "openclaw"] {
|
||||
for pkg_name in pkg_names {
|
||||
let pkg_json = dir.join("node_modules").join(pkg_name).join("package.json");
|
||||
if let Ok(content) = std::fs::read_to_string(&pkg_json) {
|
||||
if let Some(ver) = serde_json::from_str::<serde_json::Value>(&content)
|
||||
@@ -1560,7 +1700,7 @@ fn read_version_from_installation(cli_path: &std::path::Path) -> Option<String>
|
||||
}
|
||||
// npm shim 情况:向上查找 node_modules
|
||||
if let Some(parent) = dir.parent() {
|
||||
for pkg_name in &["@qingchencloud/openclaw-zh", "openclaw"] {
|
||||
for pkg_name in pkg_names {
|
||||
let pkg_json = parent
|
||||
.join("node_modules")
|
||||
.join(pkg_name)
|
||||
@@ -2010,6 +2150,14 @@ async fn try_standalone_install(
|
||||
])
|
||||
.creation_flags(0x08000000)
|
||||
.status();
|
||||
// 同步更新当前进程的 PATH 环境变量,使后续 resolve_openclaw_cli_path()
|
||||
// 和 build_enhanced_path() 能立即发现 standalone 安装的 CLI,
|
||||
// 无需重启应用(注册表写入仅对新进程生效)
|
||||
// SAFETY: 在 Tauri 命令处理器中单次调用,此时无其他线程并发读写 PATH。
|
||||
// enhanced_path 使用独立的 RwLock 缓存,不受影响。
|
||||
unsafe {
|
||||
std::env::set_var("PATH", format!("{};{}", current_path, install_str));
|
||||
}
|
||||
let _ = app.emit("upgrade-log", format!("已添加到 PATH: {install_str}"));
|
||||
}
|
||||
}
|
||||
@@ -2621,6 +2769,18 @@ async fn upgrade_openclaw_inner(
|
||||
if need_uninstall_old {
|
||||
let _ = app.emit("upgrade-log", format!("清理旧版本 ({old_pkg})..."));
|
||||
let _ = npm_command().args(["uninstall", "-g", old_pkg]).output();
|
||||
|
||||
// 清理 standalone 安装目录(不论从 standalone 切走还是切到 standalone,
|
||||
// npm 路径已经安装了新 CLI,standalone 残留会干扰源检测)
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
if sa_dir.exists() {
|
||||
let _ = app.emit(
|
||||
"upgrade-log",
|
||||
format!("清理 standalone 残留: {}", sa_dir.display()),
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&sa_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换源后重装 Gateway 服务
|
||||
|
||||
@@ -258,6 +258,10 @@ fn build_enhanced_path() -> String {
|
||||
if let Ok(prefix) = std::env::var("NPM_CONFIG_PREFIX") {
|
||||
extra.push(format!("{}/bin", prefix));
|
||||
}
|
||||
// standalone 安装目录(集中管理,避免多处硬编码)
|
||||
for sa_dir in config::all_standalone_dirs() {
|
||||
extra.push(sa_dir.to_string_lossy().into_owned());
|
||||
}
|
||||
// 扫描 nvm 实际安装的版本目录(兼容无 current 符号链接的情况)
|
||||
// 按版本号倒序排列,确保最新版优先(修复 #143:v20 排在 v24 前面)
|
||||
let nvm_versions = home.join(".nvm/versions/node");
|
||||
@@ -326,6 +330,10 @@ fn build_enhanced_path() -> String {
|
||||
if let Ok(prefix) = std::env::var("NPM_CONFIG_PREFIX") {
|
||||
extra.push(format!("{}/bin", prefix));
|
||||
}
|
||||
// standalone 安装目录(集中管理,避免多处硬编码)
|
||||
for sa_dir in config::all_standalone_dirs() {
|
||||
extra.push(sa_dir.to_string_lossy().into_owned());
|
||||
}
|
||||
// NVM_DIR 环境变量(用户可能自定义了 nvm 安装目录)
|
||||
// 按版本号倒序排列,确保最新版优先(修复 #143:v20 排在 v24 前面)
|
||||
let nvm_dir = std::env::var("NVM_DIR")
|
||||
@@ -539,6 +547,14 @@ fn build_enhanced_path() -> String {
|
||||
extra.push(format!(r"{}\npm", appdata));
|
||||
}
|
||||
|
||||
// 6.5 standalone 安装目录(集中管理,避免多处硬编码)
|
||||
// standalone 安装后通过注册表写入用户 PATH,但当前进程的 PATH 环境变量不会
|
||||
// 实时更新,需要显式添加到 enhanced_path 以确保 resolve_openclaw_cli_path()
|
||||
// 能找到 standalone 安装的 openclaw.cmd
|
||||
for sa_dir in config::all_standalone_dirs() {
|
||||
extra.push(sa_dir.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
// 7. 系统默认 Node.js 安装路径(优先级最低)
|
||||
extra.push(format!(r"{}\nodejs", pf));
|
||||
extra.push(format!(r"{}\nodejs", pf86));
|
||||
|
||||
@@ -329,10 +329,11 @@ mod platform {
|
||||
|
||||
fn common_cli_candidates() -> Vec<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
candidates.push(home.join(".openclaw-bin").join("openclaw"));
|
||||
// standalone 安装目录(集中管理,避免多处硬编码)
|
||||
for sa_dir in crate::commands::config::all_standalone_dirs() {
|
||||
candidates.push(sa_dir.join("openclaw"));
|
||||
}
|
||||
candidates.push(PathBuf::from("/opt/openclaw/openclaw"));
|
||||
// Homebrew 路径(非 standalone,保留)
|
||||
candidates.push(PathBuf::from("/opt/homebrew/bin/openclaw"));
|
||||
candidates.push(PathBuf::from("/usr/local/bin/openclaw"));
|
||||
candidates
|
||||
@@ -849,23 +850,9 @@ 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"));
|
||||
// standalone 安装目录(集中管理,避免多处硬编码)
|
||||
for sa_dir in crate::commands::config::all_standalone_dirs() {
|
||||
candidates.push(sa_dir.join("openclaw.cmd"));
|
||||
}
|
||||
|
||||
if let Ok(appdata) = env::var("APPDATA") {
|
||||
@@ -1224,6 +1211,10 @@ mod platform {
|
||||
.join("openclaw"),
|
||||
);
|
||||
}
|
||||
// standalone 安装目录(集中管理,避免多处硬编码)
|
||||
for sa_dir in crate::commands::config::all_standalone_dirs() {
|
||||
candidates.push(sa_dir.join("openclaw"));
|
||||
}
|
||||
candidates.push(PathBuf::from("/usr/local/bin/openclaw"));
|
||||
candidates.push(PathBuf::from("/usr/bin/openclaw"));
|
||||
for segment in crate::commands::enhanced_path().split(':') {
|
||||
|
||||
@@ -38,11 +38,14 @@ fn find_openclaw_cmd() -> Option<std::path::PathBuf> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn common_non_windows_cli_candidates() -> Vec<std::path::PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
// standalone 安装目录(集中管理,避免多处硬编码)
|
||||
for sa_dir in crate::commands::config::all_standalone_dirs() {
|
||||
candidates.push(sa_dir.join("openclaw"));
|
||||
}
|
||||
// 其他标准路径
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
candidates.push(home.join(".openclaw-bin").join("openclaw"));
|
||||
candidates.push(home.join(".local").join("bin").join("openclaw"));
|
||||
}
|
||||
candidates.push(std::path::PathBuf::from("/opt/openclaw/openclaw"));
|
||||
candidates.push(std::path::PathBuf::from("/opt/homebrew/bin/openclaw"));
|
||||
candidates.push(std::path::PathBuf::from("/usr/local/bin/openclaw"));
|
||||
candidates.push(std::path::PathBuf::from("/usr/bin/openclaw"));
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
checkingUpdate: _('检查更新中...', 'Checking for updates...', '檢查更新中...', '更新を確認中...', '업데이트 확인 중...', 'Đang kiểm tra cập nhật...', 'Verificando actualizaciones...', 'Verificando atualizações...', 'Проверка обновлений...', 'Vérification des mises à jour...', 'Suche nach Updates...'),
|
||||
official: _('原版', 'Official', '', '公式', '공식', 'Chính thức', 'Oficial', 'Oficial', 'Официальный', 'Officiel', 'Offiziell'),
|
||||
chinese: _('汉化版', 'Chinese', '漢化版', '中国語版', '중국어판'),
|
||||
unknownSource: _('未知来源', 'Unknown Source', '未知來源', '不明なソース', '알 수 없는 출처'),
|
||||
policyAhead: _('检测到你本地安装的是高于推荐稳定版的 {current},可能存在接口、事件或配置兼容性问题。建议回退到 {recommended};如果你要继续使用高版本,请自行验证兼容性并关注 issue / release。', 'Your local installation {current} is ahead of the recommended stable version. There may be API, event, or config compatibility issues. Consider rolling back to {recommended}; if you want to keep the newer version, verify compatibility yourself and watch issues/releases.', '檢測到你本地安裝的是高於推薦穩定版的 {current},可能存在介面、事件或設定相容性問題。建議回退到 {recommended};如果你要繼續使用高版本,請自行驗證相容性並關注 issue / release。'),
|
||||
policyDefault: _('当前面板默认只保证推荐稳定版的兼容性;如果你要尝试其他版本或预览版,请自行验证兼容性。若希望面板尽快支持最新版特性,欢迎提交 issue 告诉我们。', 'This panel only guarantees compatibility with the recommended stable version. If you want to try other versions or previews, verify compatibility yourself. Submit an issue if you want us to support the latest version sooner.', '目前面板預設只保證推薦穩定版的相容性;如果你要尝試其他版本或預覽版,請自行驗證相容性。若希望面板尽快支援最新版特性,欢迎提交 issue 告诉我們。'),
|
||||
notInstalled: _('未安装', 'Not installed', '未安裝', '未インストール', '미설치', 'Chưa cài đặt', 'No instalado', 'Não instalado', 'Не установлен', 'Non installé', 'Nicht installiert'),
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
versionLabel: _('版本', 'Version', '', 'バージョン', '버전', 'Phiên bản', 'Versión', 'Versão', 'Версия'),
|
||||
versionOfficial: _('官方', 'Official', '', '公式', '공식'),
|
||||
versionChinese: _('汉化', 'Chinese', '漢化', '中国語版', '중국어판'),
|
||||
versionUnknownSource: _('未知来源', 'Unknown Source', '未知來源', '不明なソース', '알 수 없는 출처'),
|
||||
versionUnknown: _('版本信息未获取', 'Version info unavailable', '版本資訊未取得', 'バージョン情報未取得', '버전 정보 없음'),
|
||||
versionAhead: _('当前版本高于推荐稳定版 {version},可能不稳定', 'Current version is ahead of recommended stable {version}, may be unstable', '目前版本高於推薦穩定版 {version},可能不穩定', '現在のバージョンは推奨安定版 {version} より新しく、不安定な可能性があります', '현재 버전이 권장 안정 버전 {version}보다 높아 불안정할 수 있습니다'),
|
||||
versionStable: _('稳定版 {version}', 'Stable {version}', '穩定版 {version}', '安定版 {version}', '안정 버전 {version}'),
|
||||
|
||||
@@ -83,7 +83,7 @@ async function loadData(page) {
|
||||
checkHotUpdate(cards, panelVersion)
|
||||
|
||||
const isInstalled = !!version.current
|
||||
const sourceLabel = version.source === 'official' ? t('about.official') : t('about.chinese')
|
||||
const sourceLabel = version.source === 'official' ? t('about.official') : version.source === 'chinese' ? t('about.chinese') : t('about.unknownSource')
|
||||
const btnSm = 'padding:2px 8px;font-size:var(--font-size-xs)'
|
||||
const hasRecommended = !!version.recommended
|
||||
const aheadOfRecommended = isInstalled && hasRecommended && !!version.ahead_of_recommended
|
||||
@@ -250,18 +250,18 @@ async function showVersionPicker(page, currentVersion) {
|
||||
if (!targetVer || targetVer === '') { hintEl.textContent = ''; confirmBtn.disabled = true; return }
|
||||
const targetTag = select.selectedIndex === 0 ? t('about.tagRecommended') : t('about.tagNeedTest')
|
||||
|
||||
const sameSource = targetSource === (currentVersion.source === 'official' ? 'official' : 'chinese')
|
||||
const sameSource = targetSource === currentVersion.source
|
||||
|
||||
if (!isInstalled) {
|
||||
confirmBtn.textContent = t('about.btnInstall')
|
||||
hintEl.textContent = t('about.hintInstall', { source: targetSource === 'official' ? t('about.official') : t('about.chinese'), ver: targetVer, tag: targetTag })
|
||||
hintEl.textContent = t('about.hintInstall', { source: targetSource === 'official' ? t('about.official') : targetSource === 'chinese' ? t('about.chinese') : t('about.unknownSource'), ver: targetVer, tag: targetTag })
|
||||
confirmBtn.disabled = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!sameSource) {
|
||||
confirmBtn.textContent = t('about.btnSwitch')
|
||||
hintEl.innerHTML = `${t('about.hintCurrent')}: <strong>${currentVersion.source === 'official' ? t('about.official') : t('about.chinese')} ${currentVersion.current}</strong> → <strong>${targetSource === 'official' ? t('about.official') : t('about.chinese')} ${targetVer}</strong>${targetTag}`
|
||||
hintEl.innerHTML = `${t('about.hintCurrent')}: <strong>${currentVersion.source === 'official' ? t('about.official') : currentVersion.source === 'chinese' ? t('about.chinese') : t('about.unknownSource')} ${currentVersion.current}</strong> → <strong>${targetSource === 'official' ? t('about.official') : targetSource === 'chinese' ? t('about.chinese') : t('about.unknownSource')} ${targetVer}</strong>${targetTag}`
|
||||
confirmBtn.disabled = false
|
||||
return
|
||||
}
|
||||
@@ -310,7 +310,7 @@ async function showVersionPicker(page, currentVersion) {
|
||||
const versions = showNightly ? allVersions : (stable.length > 0 ? stable : allVersions)
|
||||
const nightlyCount = allVersions.length - stable.length
|
||||
select.innerHTML = versions.map((v, idx) => {
|
||||
const isCurrent = isInstalled && v === currentVersion.current && source === (currentVersion.source === 'official' ? 'official' : 'chinese')
|
||||
const isCurrent = isInstalled && v === currentVersion.current && source === currentVersion.source
|
||||
return `<option value="${v}">${v}${idx === 0 ? ` (${t('about.recommended')})` : ''}${isCurrent ? ` (${t('about.current')})` : ''}</option>`
|
||||
}).join('')
|
||||
// nightly 切换提示
|
||||
|
||||
@@ -186,7 +186,7 @@ function renderStatCards(page, services, version, agents, config) {
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">${t('dashboard.versionLabel')} · ${version.source === 'official' ? t('dashboard.versionOfficial') : t('dashboard.versionChinese')}</span>
|
||||
<span class="stat-card-label">${t('dashboard.versionLabel')} · ${version.source === 'official' ? t('dashboard.versionOfficial') : version.source === 'chinese' ? t('dashboard.versionChinese') : t('dashboard.versionUnknownSource')}</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${version.current || t('common.unknown')}</div>
|
||||
<div class="stat-card-meta">${versionMeta}</div>
|
||||
|
||||
Reference in New Issue
Block a user