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:
晴天
2026-03-24 11:57:00 +08:00
parent 7aa13ff7d5
commit 0c062e93e0
12 changed files with 951 additions and 84 deletions

View File

@@ -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]

View File

@@ -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())

View File

@@ -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,
}

View File

@@ -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