mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-11 10:00:04 +08:00
feat: multi-OpenClaw CLI detection/binding + i18n infrastructure
Multi-OpenClaw Detection & Binding: - Add resolve_openclaw_cli_path() and classify_cli_source() in utils.rs - Support openclawCliPath binding in clawpanel.json (user selects CLI) - VersionInfo now includes cli_path, cli_source, all_installations - scan_all_installations() detects all OpenClaw installs on system - Dashboard shows CLI source label + multi-install warning - Settings page: CLI binding UI with auto-detect and manual selection - dev-api.js synced with cli_path/cli_source fields for Web mode i18n Infrastructure: - Create src/lib/i18n.js core module (t(), setLang(), initI18n()) - Create src/locales/zh-CN.json and src/locales/en.json - Sidebar fully i18n-ized (nav labels, sections, instance switcher) - Dashboard stat cards fully i18n-ized - Settings page: language switcher UI (live reload) - initI18n() called in main.js on startup
This commit is contained in:
@@ -1365,6 +1365,16 @@ pub async fn get_version_info() -> Result<VersionInfo, String> {
|
||||
(Some(c), Some(r)) => recommended_is_newer(c, r),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// 解析当前实际使用的 CLI 路径
|
||||
let cli_path = crate::utils::resolve_openclaw_cli_path();
|
||||
let cli_source = cli_path
|
||||
.as_ref()
|
||||
.map(|p| crate::utils::classify_cli_source(p));
|
||||
|
||||
// 扫描所有可检测到的 OpenClaw 安装
|
||||
let all_installations = scan_all_installations(&cli_path);
|
||||
|
||||
Ok(VersionInfo {
|
||||
current,
|
||||
latest,
|
||||
@@ -1375,9 +1385,148 @@ pub async fn get_version_info() -> Result<VersionInfo, String> {
|
||||
ahead_of_recommended,
|
||||
panel_version: panel_version().to_string(),
|
||||
source,
|
||||
cli_path,
|
||||
cli_source,
|
||||
all_installations: Some(all_installations),
|
||||
})
|
||||
}
|
||||
|
||||
/// 扫描系统中所有可检测到的 OpenClaw 安装
|
||||
fn scan_all_installations(
|
||||
active_path: &Option<String>,
|
||||
) -> Vec<crate::models::types::OpenClawInstallation> {
|
||||
use crate::models::types::OpenClawInstallation;
|
||||
let mut results: Vec<OpenClawInstallation> = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
||||
let mut try_add = |path: std::path::PathBuf| {
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
let canonical = path
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| path.clone())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if seen.contains(&canonical) {
|
||||
return;
|
||||
}
|
||||
seen.insert(canonical.clone());
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
let source = crate::utils::classify_cli_source(&path_str);
|
||||
let version = read_version_from_installation(&path);
|
||||
let is_active = active_path
|
||||
.as_ref()
|
||||
.map(|a| {
|
||||
let a_canon = std::path::Path::new(a)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from(a))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
a_canon == canonical
|
||||
})
|
||||
.unwrap_or(false);
|
||||
results.push(OpenClawInstallation {
|
||||
path: path_str,
|
||||
source,
|
||||
version,
|
||||
active: is_active,
|
||||
});
|
||||
};
|
||||
|
||||
// standalone 安装目录
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
#[cfg(target_os = "windows")]
|
||||
try_add(sa_dir.join("openclaw.cmd"));
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
try_add(sa_dir.join("openclaw"));
|
||||
}
|
||||
|
||||
// npm 全局目录
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
try_add(
|
||||
std::path::PathBuf::from(&appdata)
|
||||
.join("npm")
|
||||
.join("openclaw.cmd"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PATH 中找到的所有 openclaw
|
||||
let enhanced = super::enhanced_path();
|
||||
#[cfg(target_os = "windows")]
|
||||
let sep = ';';
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let sep = ':';
|
||||
for dir in enhanced.split(sep) {
|
||||
let dir = dir.trim();
|
||||
if dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let base = std::path::Path::new(dir);
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
try_add(base.join("openclaw.cmd"));
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
try_add(base.join("openclaw"));
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// 从安装路径附近读取版本信息
|
||||
fn read_version_from_installation(cli_path: &std::path::Path) -> Option<String> {
|
||||
// 尝试从同目录的 VERSION 文件读取
|
||||
if let Some(dir) = cli_path.parent() {
|
||||
let version_file = dir.join("VERSION");
|
||||
if let Ok(content) = std::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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 尝试从 package.json 读取
|
||||
for pkg_name in &["@qingchencloud/openclaw-zh", "openclaw"] {
|
||||
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)
|
||||
.ok()
|
||||
.and_then(|v| v.get("version")?.as_str().map(String::from))
|
||||
{
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
// npm shim 情况:向上查找 node_modules
|
||||
if let Some(parent) = dir.parent() {
|
||||
for pkg_name in &["@qingchencloud/openclaw-zh", "openclaw"] {
|
||||
let pkg_json = parent
|
||||
.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)
|
||||
.ok()
|
||||
.and_then(|v| v.get("version")?.as_str().map(String::from))
|
||||
{
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 获取 OpenClaw 运行时状态摘要(openclaw status --json)
|
||||
/// 包含 runtimeVersion、会话列表(含 token 用量、fastMode 等标签)
|
||||
#[tauri::command]
|
||||
|
||||
@@ -85,7 +85,7 @@ fn panel_config_path() -> PathBuf {
|
||||
default_openclaw_dir().join("clawpanel.json")
|
||||
}
|
||||
|
||||
fn read_panel_config_value() -> Option<serde_json::Value> {
|
||||
pub fn read_panel_config_value() -> Option<serde_json::Value> {
|
||||
std::fs::read_to_string(panel_config_path())
|
||||
.ok()
|
||||
.and_then(|content| serde_json::from_str(&content).ok())
|
||||
|
||||
@@ -21,4 +21,18 @@ pub struct VersionInfo {
|
||||
pub ahead_of_recommended: bool,
|
||||
pub panel_version: String,
|
||||
pub source: String,
|
||||
/// 当前实际使用的 CLI 完整路径
|
||||
pub cli_path: Option<String>,
|
||||
/// CLI 安装来源标签: standalone / npm-zh / npm-official / unknown
|
||||
pub cli_source: Option<String>,
|
||||
/// 所有检测到的 OpenClaw 安装(路径 + 来源 + 版本)
|
||||
pub all_installations: Option<Vec<OpenClawInstallation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OpenClawInstallation {
|
||||
pub path: String,
|
||||
pub source: String,
|
||||
pub version: Option<String>,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
/// 读取 clawpanel.json 中用户绑定的 CLI 路径
|
||||
fn bound_cli_path() -> Option<std::path::PathBuf> {
|
||||
let config = crate::commands::read_panel_config_value()?;
|
||||
let raw = config.get("openclawCliPath")?.as_str()?;
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let p = std::path::PathBuf::from(raw);
|
||||
if p.exists() {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows: 在 PATH 中查找 openclaw.cmd 的完整路径
|
||||
/// 避免通过 `cmd /c openclaw` 调用时 npm .cmd shim 中的引号导致
|
||||
/// "\"node\"" is not recognized 错误
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_openclaw_cmd() -> Option<std::path::PathBuf> {
|
||||
// 优先使用用户绑定的路径
|
||||
if let Some(bound) = bound_cli_path() {
|
||||
return Some(bound);
|
||||
}
|
||||
let path = crate::commands::enhanced_path();
|
||||
for dir in path.split(';') {
|
||||
let candidate = std::path::Path::new(dir).join("openclaw.cmd");
|
||||
@@ -16,6 +35,62 @@ fn find_openclaw_cmd() -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// 解析当前实际使用的 openclaw CLI 完整路径(跨平台)
|
||||
pub fn resolve_openclaw_cli_path() -> Option<String> {
|
||||
// 优先使用用户绑定的路径
|
||||
if let Some(bound) = bound_cli_path() {
|
||||
return Some(bound.to_string_lossy().to_string());
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let path = crate::commands::enhanced_path();
|
||||
for dir in path.split(';') {
|
||||
let candidate = std::path::Path::new(dir).join("openclaw.cmd");
|
||||
if candidate.exists() {
|
||||
return Some(candidate.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let path = crate::commands::enhanced_path();
|
||||
let sep = ':';
|
||||
for dir in path.split(sep) {
|
||||
let candidate = std::path::Path::new(dir).join("openclaw");
|
||||
if candidate.exists() {
|
||||
return Some(candidate.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 CLI 路径判断安装来源
|
||||
pub fn classify_cli_source(cli_path: &str) -> String {
|
||||
let lower = cli_path.replace('\\', "/").to_lowercase();
|
||||
// standalone 安装
|
||||
if lower.contains("/programs/openclaw/")
|
||||
|| lower.contains("/openclaw-bin/")
|
||||
|| lower.contains("/opt/openclaw/")
|
||||
{
|
||||
return "standalone".into();
|
||||
}
|
||||
// npm 汉化版
|
||||
if lower.contains("openclaw-zh") || lower.contains("@qingchencloud") {
|
||||
return "npm-zh".into();
|
||||
}
|
||||
// npm 全局(大概率官方版)
|
||||
if lower.contains("/npm/") || lower.contains("/node_modules/") {
|
||||
return "npm-official".into();
|
||||
}
|
||||
// Homebrew
|
||||
if lower.contains("/homebrew/") || lower.contains("/usr/local/bin") {
|
||||
return "npm-global".into();
|
||||
}
|
||||
"unknown".into()
|
||||
}
|
||||
|
||||
/// 跨平台获取 openclaw 命令的方法(同步版本)
|
||||
#[allow(dead_code)]
|
||||
pub fn openclaw_command() -> std::process::Command {
|
||||
@@ -42,7 +117,10 @@ pub fn openclaw_command() -> std::process::Command {
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let mut cmd = std::process::Command::new("openclaw");
|
||||
let bin = bound_cli_path()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "openclaw".into());
|
||||
let mut cmd = std::process::Command::new(bin);
|
||||
cmd.env("PATH", crate::commands::enhanced_path());
|
||||
crate::commands::apply_proxy_env(&mut cmd);
|
||||
cmd
|
||||
@@ -74,7 +152,10 @@ pub fn openclaw_command_async() -> tokio::process::Command {
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let mut cmd = tokio::process::Command::new("openclaw");
|
||||
let bin = bound_cli_path()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "openclaw".into());
|
||||
let mut cmd = tokio::process::Command::new(bin);
|
||||
cmd.env("PATH", crate::commands::enhanced_path());
|
||||
crate::commands::apply_proxy_env_tokio(&mut cmd);
|
||||
cmd
|
||||
|
||||
Reference in New Issue
Block a user