mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-27 11:20:04 +08:00
style: cargo fmt
This commit is contained in:
@@ -486,17 +486,28 @@ fn npm_command_elevated() -> Command {
|
||||
/// 解决 Windows 上 EEXIST(文件已存在)和文件被占用的问题
|
||||
fn pre_install_cleanup() {
|
||||
/// 带超时执行命令(spawn + try_wait),防止任何子进程无限阻塞
|
||||
fn run_with_timeout(mut child: std::process::Child, timeout_secs: u64) -> Option<std::process::Output> {
|
||||
fn run_with_timeout(
|
||||
mut child: std::process::Child,
|
||||
timeout_secs: u64,
|
||||
) -> Option<std::process::Output> {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let stdout = child.stdout.take().map(|mut s| {
|
||||
let mut buf = Vec::new();
|
||||
let _ = std::io::Read::read_to_end(&mut s, &mut buf);
|
||||
buf
|
||||
}).unwrap_or_default();
|
||||
return Some(std::process::Output { status, stdout, stderr: Vec::new() });
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.map(|mut s| {
|
||||
let mut buf = Vec::new();
|
||||
let _ = std::io::Read::read_to_end(&mut s, &mut buf);
|
||||
buf
|
||||
})
|
||||
.unwrap_or_default();
|
||||
return Some(std::process::Output {
|
||||
status,
|
||||
stdout,
|
||||
stderr: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
@@ -546,7 +557,10 @@ fn pre_install_cleanup() {
|
||||
// 同时杀死 standalone 目录下的 node.exe 进程(每个目录 10s 超时)
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
if sa_dir.exists() {
|
||||
let dir_lower = sa_dir.to_string_lossy().to_lowercase().replace('\\', "\\\\");
|
||||
let dir_lower = sa_dir
|
||||
.to_string_lossy()
|
||||
.to_lowercase()
|
||||
.replace('\\', "\\\\");
|
||||
let ps_script = format!(
|
||||
"Get-Process -Name node -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -and $_.Path.ToLower().Contains('{}') }} | Select-Object -ExpandProperty Id",
|
||||
dir_lower
|
||||
@@ -561,7 +575,9 @@ fn pre_install_cleanup() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Ok(_pid) = line.trim().parse::<u32>() {
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", line.trim()]).output();
|
||||
let _ = Command::new("taskkill")
|
||||
.args(["/F", "/PID", line.trim()])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2971,9 +2987,7 @@ async fn try_standalone_install(
|
||||
// 兼容两种 latest.json 格式:
|
||||
// 新格式(CI 生成): { "editions": { "zh": { "version": "...", "base_url": "..." } } }
|
||||
// 旧格式(兼容): { "version": "...", "base_url": "..." }
|
||||
let edition_obj = manifest
|
||||
.get("editions")
|
||||
.and_then(|e| e.get("zh"));
|
||||
let edition_obj = manifest.get("editions").and_then(|e| e.get("zh"));
|
||||
let (remote_version, manifest_base_url, archive_prefix) = if let Some(ed) = edition_obj {
|
||||
let ver = ed
|
||||
.get("version")
|
||||
@@ -3059,7 +3073,10 @@ async fn try_standalone_install(
|
||||
let dl_mb = downloaded as f64 / 1_048_576.0;
|
||||
let total_mb = total_bytes as f64 / 1_048_576.0;
|
||||
let real_pct = (downloaded as f64 / total_bytes as f64 * 100.0) as u32;
|
||||
let _ = app.emit("upgrade-log", format!("下载中 {real_pct}% ({dl_mb:.0}/{total_mb:.0}MB)"));
|
||||
let _ = app.emit(
|
||||
"upgrade-log",
|
||||
format!("下载中 {real_pct}% ({dl_mb:.0}/{total_mb:.0}MB)"),
|
||||
);
|
||||
}
|
||||
last_progress = pct;
|
||||
let _ = app.emit("upgrade-progress", pct.min(70));
|
||||
@@ -3581,7 +3598,9 @@ async fn upgrade_openclaw_inner(
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
super::refresh_enhanced_path();
|
||||
crate::commands::service::invalidate_cli_detection_cache();
|
||||
let msg = format!("✅ standalone (GitHub) 安装完成,当前版本: {installed_ver}");
|
||||
let msg = format!(
|
||||
"✅ standalone (GitHub) 安装完成,当前版本: {installed_ver}"
|
||||
);
|
||||
let _ = app.emit("upgrade-log", &msg);
|
||||
return Ok(msg);
|
||||
}
|
||||
@@ -3593,7 +3612,9 @@ async fn upgrade_openclaw_inner(
|
||||
);
|
||||
let _ = app.emit("upgrade-progress", 5);
|
||||
} else {
|
||||
return Err(format!("standalone 安装失败: CDN={cdn_reason}, GitHub={gh_reason}"));
|
||||
return Err(format!(
|
||||
"standalone 安装失败: CDN={cdn_reason}, GitHub={gh_reason}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3872,7 +3893,10 @@ async fn upgrade_openclaw_inner(
|
||||
// 使用 PowerShell Get-Process(兼容 Windows 11,wmic 已废弃)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let dir_lower = sa_dir.to_string_lossy().to_lowercase().replace('\\', "\\\\");
|
||||
let dir_lower = sa_dir
|
||||
.to_string_lossy()
|
||||
.to_lowercase()
|
||||
.replace('\\', "\\\\");
|
||||
let ps_script = format!(
|
||||
"Get-Process -Name node -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -and $_.Path.ToLower().Contains('{}') }} | Select-Object -ExpandProperty Id",
|
||||
dir_lower
|
||||
@@ -3884,8 +3908,11 @@ async fn upgrade_openclaw_inner(
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Ok(pid) = line.trim().parse::<u32>() {
|
||||
let _ = app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output();
|
||||
let _ =
|
||||
app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3902,7 +3929,10 @@ async fn upgrade_openclaw_inner(
|
||||
if let Err(e) = std::fs::remove_dir_all(&sa_dir) {
|
||||
let _ = app.emit(
|
||||
"upgrade-log",
|
||||
format!("⚠️ 清理 standalone 残留失败: {e}(可手动删除 {})", sa_dir.display()),
|
||||
format!(
|
||||
"⚠️ 清理 standalone 残留失败: {e}(可手动删除 {})",
|
||||
sa_dir.display()
|
||||
),
|
||||
);
|
||||
} else {
|
||||
let _ = app.emit("upgrade-log", "standalone 残留已清理(重试成功)✓");
|
||||
@@ -4033,7 +4063,10 @@ async fn uninstall_openclaw_inner(
|
||||
// 使用 PowerShell Get-Process(兼容 Windows 11,wmic 已废弃)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let dir_lower = sa_dir.to_string_lossy().to_lowercase().replace('\\', "\\\\");
|
||||
let dir_lower = sa_dir
|
||||
.to_string_lossy()
|
||||
.to_lowercase()
|
||||
.replace('\\', "\\\\");
|
||||
let ps_script = format!(
|
||||
"Get-Process -Name node -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -and $_.Path.ToLower().Contains('{}') }} | Select-Object -ExpandProperty Id",
|
||||
dir_lower
|
||||
@@ -4046,7 +4079,9 @@ async fn uninstall_openclaw_inner(
|
||||
for line in text.lines() {
|
||||
if let Ok(pid) = line.trim().parse::<u32>() {
|
||||
let _ = app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output();
|
||||
let _ = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4066,7 +4101,10 @@ async fn uninstall_openclaw_inner(
|
||||
if let Err(e) = std::fs::remove_dir_all(sa_dir) {
|
||||
let _ = app.emit(
|
||||
"upgrade-log",
|
||||
format!("⚠️ 清理 standalone 失败: {e}(可手动删除 {})", sa_dir.display()),
|
||||
format!(
|
||||
"⚠️ 清理 standalone 失败: {e}(可手动删除 {})",
|
||||
sa_dir.display()
|
||||
),
|
||||
);
|
||||
} else {
|
||||
let _ = app.emit("upgrade-log", "standalone 安装已清理(重试成功)✓");
|
||||
@@ -4903,14 +4941,13 @@ async fn reload_gateway_internal(app: Option<&tauri::AppHandle>) -> Result<Strin
|
||||
{
|
||||
match reload_gateway_via_http().await {
|
||||
Ok(msg) => Ok(msg),
|
||||
Err(_) => {
|
||||
crate::commands::service::restart_service(
|
||||
app.cloned().ok_or_else(|| "缺少 AppHandle,无法回退到 Gateway 进程重启".to_string())?,
|
||||
"ai.openclaw.gateway".into(),
|
||||
)
|
||||
.await
|
||||
.map(|_| "Gateway 已重启".to_string())
|
||||
}
|
||||
Err(_) => crate::commands::service::restart_service(
|
||||
app.cloned()
|
||||
.ok_or_else(|| "缺少 AppHandle,无法回退到 Gateway 进程重启".to_string())?,
|
||||
"ai.openclaw.gateway".into(),
|
||||
)
|
||||
.await
|
||||
.map(|_| "Gateway 已重启".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,19 @@ fn collect_env() -> DiagnoseEnv {
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||
let auth = val.get("gateway").and_then(|g| g.get("auth"));
|
||||
if let Some(auth) = auth {
|
||||
if auth.get("token").and_then(|t| t.as_str()).map(|s| !s.is_empty()).unwrap_or(false) {
|
||||
if auth
|
||||
.get("token")
|
||||
.and_then(|t| t.as_str())
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"token".to_string()
|
||||
} else if auth.get("password").and_then(|p| p.as_str()).map(|s| !s.is_empty()).unwrap_or(false) {
|
||||
} else if auth
|
||||
.get("password")
|
||||
.and_then(|p| p.as_str())
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"password".to_string()
|
||||
} else {
|
||||
"none".to_string()
|
||||
@@ -88,7 +98,11 @@ fn collect_env() -> DiagnoseEnv {
|
||||
let err_log_path = openclaw_dir.join("logs").join("gateway.err.log");
|
||||
let err_log_excerpt = if let Ok(bytes) = std::fs::read(&err_log_path) {
|
||||
let max = 2048;
|
||||
let tail = if bytes.len() > max { &bytes[bytes.len() - max..] } else { &bytes[..] };
|
||||
let tail = if bytes.len() > max {
|
||||
&bytes[bytes.len() - max..]
|
||||
} else {
|
||||
&bytes[..]
|
||||
};
|
||||
String::from_utf8_lossy(tail).to_string()
|
||||
} else {
|
||||
String::new()
|
||||
@@ -123,18 +137,16 @@ fn check_config() -> DiagnoseStep {
|
||||
return finish_step("config", false, "openclaw.json 不存在", t);
|
||||
}
|
||||
match std::fs::read_to_string(&config_path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(val) => {
|
||||
if val.get("gateway").is_some() {
|
||||
finish_step("config", true, "配置文件有效,含 gateway 配置", t)
|
||||
} else {
|
||||
finish_step("config", false, "配置文件缺少 gateway 段", t)
|
||||
}
|
||||
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(val) => {
|
||||
if val.get("gateway").is_some() {
|
||||
finish_step("config", true, "配置文件有效,含 gateway 配置", t)
|
||||
} else {
|
||||
finish_step("config", false, "配置文件缺少 gateway 段", t)
|
||||
}
|
||||
Err(e) => finish_step("config", false, &format!("JSON 解析失败: {e}"), t),
|
||||
}
|
||||
}
|
||||
Err(e) => finish_step("config", false, &format!("JSON 解析失败: {e}"), t),
|
||||
},
|
||||
Err(e) => finish_step("config", false, &format!("读取失败: {e}"), t),
|
||||
}
|
||||
}
|
||||
@@ -159,7 +171,12 @@ fn check_device_key() -> DiagnoseStep {
|
||||
Err(e) => finish_step("device_key", false, &format!("读取失败: {e}"), t),
|
||||
}
|
||||
} else {
|
||||
finish_step("device_key", false, "设备密钥不存在(将在首次连接时自动生成)", t)
|
||||
finish_step(
|
||||
"device_key",
|
||||
false,
|
||||
"设备密钥不存在(将在首次连接时自动生成)",
|
||||
t,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,15 +195,32 @@ fn check_allowed_origins() -> DiagnoseStep {
|
||||
match origins {
|
||||
Some(arr) if !arr.is_empty() => {
|
||||
let list: Vec<&str> = arr.iter().filter_map(|v| v.as_str()).collect();
|
||||
let has_tauri = list.iter().any(|o| o.contains("tauri://") || o.contains("https://tauri.localhost"));
|
||||
let has_tauri = list.iter().any(|o| {
|
||||
o.contains("tauri://") || o.contains("https://tauri.localhost")
|
||||
});
|
||||
if has_tauri {
|
||||
finish_step("allowed_origins", true, &format!("allowedOrigins 包含 Tauri origin: {:?}", list), t)
|
||||
finish_step(
|
||||
"allowed_origins",
|
||||
true,
|
||||
&format!("allowedOrigins 包含 Tauri origin: {:?}", list),
|
||||
t,
|
||||
)
|
||||
} else {
|
||||
finish_step("allowed_origins", false, &format!("allowedOrigins 缺少 Tauri origin: {:?}", list), t)
|
||||
finish_step(
|
||||
"allowed_origins",
|
||||
false,
|
||||
&format!("allowedOrigins 缺少 Tauri origin: {:?}", list),
|
||||
t,
|
||||
)
|
||||
}
|
||||
}
|
||||
Some(_) => finish_step("allowed_origins", false, "allowedOrigins 为空数组", t),
|
||||
None => finish_step("allowed_origins", false, "未配置 allowedOrigins(autoPair 会自动修复)", t),
|
||||
None => finish_step(
|
||||
"allowed_origins",
|
||||
false,
|
||||
"未配置 allowedOrigins(autoPair 会自动修复)",
|
||||
t,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
finish_step("allowed_origins", false, "配置文件解析失败", t)
|
||||
@@ -208,21 +242,43 @@ async fn check_http_health(port: u16) -> DiagnoseStep {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
if status.is_success() {
|
||||
finish_step("http_health", true, &format!("HTTP /health 返回 {status}"), t)
|
||||
finish_step(
|
||||
"http_health",
|
||||
true,
|
||||
&format!("HTTP /health 返回 {status}"),
|
||||
t,
|
||||
)
|
||||
} else {
|
||||
finish_step("http_health", false, &format!("HTTP /health 返回 {status}"), t)
|
||||
finish_step(
|
||||
"http_health",
|
||||
false,
|
||||
&format!("HTTP /health 返回 {status}"),
|
||||
t,
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => finish_step("http_health", false, &format!("HTTP /health 请求失败: {e}"), t),
|
||||
Err(e) => finish_step(
|
||||
"http_health",
|
||||
false,
|
||||
&format!("HTTP /health 请求失败: {e}"),
|
||||
t,
|
||||
),
|
||||
},
|
||||
Err(e) => finish_step("http_health", false, &format!("HTTP client 创建失败: {e}"), t),
|
||||
Err(e) => finish_step(
|
||||
"http_health",
|
||||
false,
|
||||
&format!("HTTP client 创建失败: {e}"),
|
||||
t,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查 Gateway 错误日志
|
||||
fn check_error_log() -> DiagnoseStep {
|
||||
let t = step_timer();
|
||||
let log_path = crate::commands::openclaw_dir().join("logs").join("gateway.err.log");
|
||||
let log_path = crate::commands::openclaw_dir()
|
||||
.join("logs")
|
||||
.join("gateway.err.log");
|
||||
if !log_path.exists() {
|
||||
return finish_step("err_log", true, "无错误日志(正常)", t);
|
||||
}
|
||||
@@ -234,13 +290,29 @@ fn check_error_log() -> DiagnoseStep {
|
||||
} else {
|
||||
// 读最后 1KB 看有没有关键错误
|
||||
let content = std::fs::read(&log_path).unwrap_or_default();
|
||||
let tail = if content.len() > 1024 { &content[content.len() - 1024..] } else { &content[..] };
|
||||
let text = String::from_utf8_lossy(tail).to_lowercase();
|
||||
let has_fatal = text.contains("fatal") || text.contains("eaddrinuse") || text.contains("config invalid");
|
||||
if has_fatal {
|
||||
finish_step("err_log", false, &format!("错误日志含关键错误 ({size} bytes)"), t)
|
||||
let tail = if content.len() > 1024 {
|
||||
&content[content.len() - 1024..]
|
||||
} else {
|
||||
finish_step("err_log", true, &format!("错误日志存在但无致命错误 ({size} bytes)"), t)
|
||||
&content[..]
|
||||
};
|
||||
let text = String::from_utf8_lossy(tail).to_lowercase();
|
||||
let has_fatal = text.contains("fatal")
|
||||
|| text.contains("eaddrinuse")
|
||||
|| text.contains("config invalid");
|
||||
if has_fatal {
|
||||
finish_step(
|
||||
"err_log",
|
||||
false,
|
||||
&format!("错误日志含关键错误 ({size} bytes)"),
|
||||
t,
|
||||
)
|
||||
} else {
|
||||
finish_step(
|
||||
"err_log",
|
||||
true,
|
||||
&format!("错误日志存在但无致命错误 ({size} bytes)"),
|
||||
t,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +346,11 @@ pub async fn diagnose_gateway_connection() -> DiagnoseResult {
|
||||
steps.push(check_error_log());
|
||||
|
||||
let overall_ok = steps.iter().all(|s| s.ok);
|
||||
let failed: Vec<&str> = steps.iter().filter(|s| !s.ok).map(|s| s.name.as_str()).collect();
|
||||
let failed: Vec<&str> = steps
|
||||
.iter()
|
||||
.filter(|s| !s.ok)
|
||||
.map(|s| s.name.as_str())
|
||||
.collect();
|
||||
let summary = if overall_ok {
|
||||
"所有检查项通过".to_string()
|
||||
} else {
|
||||
|
||||
@@ -2122,13 +2122,19 @@ pub async fn list_all_plugins() -> Result<Value, String> {
|
||||
if let Ok(rd) = std::fs::read_dir(&ext_dir) {
|
||||
for entry in rd.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with('.') { continue; }
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
let p = entry.path();
|
||||
if !p.is_dir() { continue; }
|
||||
if !p.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let has_marker = p.join("package.json").is_file()
|
||||
|| p.join("plugin.ts").is_file()
|
||||
|| p.join("index.js").is_file();
|
||||
if !has_marker { continue; }
|
||||
if !has_marker {
|
||||
continue;
|
||||
}
|
||||
|
||||
let plugin_id = name.clone();
|
||||
seen.insert(plugin_id.clone());
|
||||
@@ -2150,7 +2156,10 @@ pub async fn list_all_plugins() -> Result<Value, String> {
|
||||
let description = std::fs::read_to_string(p.join("package.json"))
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<Value>(&s).ok())
|
||||
.and_then(|v| v.get("description").and_then(|v| v.as_str().map(String::from)));
|
||||
.and_then(|v| {
|
||||
v.get("description")
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
});
|
||||
|
||||
plugins.push(json!({
|
||||
"id": plugin_id,
|
||||
@@ -2168,9 +2177,14 @@ pub async fn list_all_plugins() -> Result<Value, String> {
|
||||
|
||||
// Also include entries from config that might not be in extensions dir (built-in)
|
||||
for (pid, entry_val) in &entries {
|
||||
if seen.contains(pid.as_str()) { continue; }
|
||||
if seen.contains(pid.as_str()) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(pid.clone());
|
||||
let enabled = entry_val.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let enabled = entry_val
|
||||
.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let allowed = allow_arr.iter().any(|v| v.as_str() == Some(pid.as_str()));
|
||||
let builtin = is_plugin_builtin(pid);
|
||||
plugins.push(json!({
|
||||
@@ -3275,11 +3289,9 @@ pub async fn install_qqbot_plugin(
|
||||
);
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = crate::commands::service::restart_service(
|
||||
app2,
|
||||
"ai.openclaw.gateway".into(),
|
||||
)
|
||||
.await;
|
||||
let _ =
|
||||
crate::commands::service::restart_service(app2, "ai.openclaw.gateway".into())
|
||||
.await;
|
||||
});
|
||||
Ok("安装成功".into())
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ fn default_openclaw_dir() -> PathBuf {
|
||||
fn panel_path_key(path: &std::path::Path) -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return path
|
||||
.to_string_lossy()
|
||||
.replace('/', "\\")
|
||||
.to_lowercase();
|
||||
return path.to_string_lossy().replace('/', "\\").to_lowercase();
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
@@ -66,21 +63,24 @@ fn panel_config_candidate_paths() -> Vec<PathBuf> {
|
||||
if !trimmed.is_empty() {
|
||||
push_unique_panel_config_path(
|
||||
&mut paths,
|
||||
PathBuf::from(trimmed).join(".openclaw").join("clawpanel.json"),
|
||||
PathBuf::from(trimmed)
|
||||
.join(".openclaw")
|
||||
.join("clawpanel.json"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let (Ok(home_drive), Ok(home_path)) = (
|
||||
std::env::var("HOMEDRIVE"),
|
||||
std::env::var("HOMEPATH"),
|
||||
) {
|
||||
if let (Ok(home_drive), Ok(home_path)) =
|
||||
(std::env::var("HOMEDRIVE"), std::env::var("HOMEPATH"))
|
||||
{
|
||||
let combined = format!("{}{}", home_drive.trim(), home_path.trim());
|
||||
let trimmed = combined.trim();
|
||||
if !trimmed.is_empty() {
|
||||
push_unique_panel_config_path(
|
||||
&mut paths,
|
||||
PathBuf::from(trimmed).join(".openclaw").join("clawpanel.json"),
|
||||
PathBuf::from(trimmed)
|
||||
.join(".openclaw")
|
||||
.join("clawpanel.json"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,8 +281,8 @@ fn looks_like_gateway_config_mismatch(reason: &str) -> bool {
|
||||
/// 当 `openclaw doctor --fix` 无法修复时作为二级回退
|
||||
fn try_direct_config_strip() -> Result<bool, String> {
|
||||
let config_path = crate::commands::openclaw_dir().join("openclaw.json");
|
||||
let raw = std::fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("读取配置文件失败: {e}"))?;
|
||||
let raw =
|
||||
std::fs::read_to_string(&config_path).map_err(|e| format!("读取配置文件失败: {e}"))?;
|
||||
let mut doc: serde_json::Value =
|
||||
serde_json::from_str(&raw).map_err(|e| format!("解析配置文件失败: {e}"))?;
|
||||
|
||||
@@ -346,10 +346,9 @@ fn try_direct_config_strip() -> Result<bool, String> {
|
||||
}
|
||||
|
||||
if changed {
|
||||
let formatted = serde_json::to_string_pretty(&doc)
|
||||
.map_err(|e| format!("序列化配置失败: {e}"))?;
|
||||
std::fs::write(&config_path, formatted)
|
||||
.map_err(|e| format!("写入配置文件失败: {e}"))?;
|
||||
let formatted =
|
||||
serde_json::to_string_pretty(&doc).map_err(|e| format!("序列化配置失败: {e}"))?;
|
||||
std::fs::write(&config_path, formatted).map_err(|e| format!("写入配置文件失败: {e}"))?;
|
||||
guardian_log("直接修复: 已写回 openclaw.json");
|
||||
}
|
||||
|
||||
@@ -458,7 +457,10 @@ async fn try_auto_fix_gateway_config(
|
||||
"auto_fix_failure",
|
||||
"已尝试自动执行 openclaw doctor --fix,但修复超时 (30s)",
|
||||
);
|
||||
Err("检测到 Gateway 配置异常,已尝试自动执行 openclaw doctor --fix,但修复超时 (30s)".into())
|
||||
Err(
|
||||
"检测到 Gateway 配置异常,已尝试自动执行 openclaw doctor --fix,但修复超时 (30s)"
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,11 +899,17 @@ mod platform {
|
||||
Err(_) => return (false, None),
|
||||
};
|
||||
// 两次尝试:第一次 1 秒,失败后短暂等待再用 2 秒重试,避免瞬态超时误判
|
||||
let connected = std::net::TcpStream::connect_timeout(&socket_addr, std::time::Duration::from_secs(1)).is_ok()
|
||||
|| {
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
std::net::TcpStream::connect_timeout(&socket_addr, std::time::Duration::from_secs(2)).is_ok()
|
||||
};
|
||||
let connected =
|
||||
std::net::TcpStream::connect_timeout(&socket_addr, std::time::Duration::from_secs(1))
|
||||
.is_ok()
|
||||
|| {
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
std::net::TcpStream::connect_timeout(
|
||||
&socket_addr,
|
||||
std::time::Duration::from_secs(2),
|
||||
)
|
||||
.is_ok()
|
||||
};
|
||||
if connected {
|
||||
let pid = get_pid_by_lsof(port);
|
||||
(true, pid)
|
||||
@@ -1158,13 +1166,11 @@ mod platform {
|
||||
use std::io::{Read, Write as IoWrite};
|
||||
use std::net::TcpStream;
|
||||
let addr = format!("127.0.0.1:{port}");
|
||||
let mut stream = match TcpStream::connect_timeout(
|
||||
&addr.parse().unwrap(),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let mut stream =
|
||||
match TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_secs(3)) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(3)));
|
||||
let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
|
||||
let req = format!("GET /health HTTP/1.0\r\nHost: 127.0.0.1:{port}\r\n\r\n");
|
||||
@@ -1227,7 +1233,8 @@ mod platform {
|
||||
|
||||
if let Some(cmdline) = read_process_command_line(pid) {
|
||||
let cmdline_lower = cmdline.to_lowercase();
|
||||
let is_gateway = cmdline_lower.contains("openclaw") && cmdline_lower.contains("gateway");
|
||||
let is_gateway =
|
||||
cmdline_lower.contains("openclaw") && cmdline_lower.contains("gateway");
|
||||
let our_pid = *LAST_KNOWN_GATEWAY_PID.lock().unwrap();
|
||||
|
||||
if is_gateway {
|
||||
@@ -1500,8 +1507,8 @@ mod platform {
|
||||
Err(_) => return (false, None),
|
||||
};
|
||||
// 两次尝试:第一次 1 秒,失败后短暂等待再用 2 秒重试,避免瞬态超时误判
|
||||
let connected = std::net::TcpStream::connect_timeout(&socket_addr, Duration::from_secs(1)).is_ok()
|
||||
|| {
|
||||
let connected =
|
||||
std::net::TcpStream::connect_timeout(&socket_addr, Duration::from_secs(1)).is_ok() || {
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
std::net::TcpStream::connect_timeout(&socket_addr, Duration::from_secs(2)).is_ok()
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ pub async fn skills_list(agent_id: Option<String>) -> Result<Value, String> {
|
||||
#[tauri::command]
|
||||
pub async fn skills_info(name: String, agent_id: Option<String>) -> Result<Value, String> {
|
||||
let agent_ws = resolve_agent_skills_dir(agent_id.as_deref());
|
||||
scan_custom_skill_detail(&name, agent_ws.as_deref()).ok_or_else(|| format!("Skill「{name}」不存在"))
|
||||
scan_custom_skill_detail(&name, agent_ws.as_deref())
|
||||
.ok_or_else(|| format!("Skill「{name}」不存在"))
|
||||
}
|
||||
|
||||
/// 检查 Skills 依赖状态(纯本地扫描)
|
||||
@@ -150,8 +151,8 @@ pub async fn skills_uninstall(name: String, agent_id: Option<String>) -> Result<
|
||||
return Err("无效的 Skill 名称".to_string());
|
||||
}
|
||||
let agent_ws = resolve_agent_skills_dir(agent_id.as_deref());
|
||||
let skills_dir =
|
||||
resolve_custom_skill_dir_with_agent(&name, agent_ws.as_deref()).ok_or_else(|| format!("Skill「{name}」不存在"))?;
|
||||
let skills_dir = resolve_custom_skill_dir_with_agent(&name, agent_ws.as_deref())
|
||||
.ok_or_else(|| format!("Skill「{name}」不存在"))?;
|
||||
if !skills_dir.exists() {
|
||||
return Err(format!("Skill「{name}」不存在"));
|
||||
}
|
||||
@@ -166,8 +167,8 @@ pub async fn skills_validate(name: String) -> Result<Value, String> {
|
||||
return Err("无效的 Skill 名称".to_string());
|
||||
}
|
||||
|
||||
let skill_dir =
|
||||
resolve_custom_skill_dir_with_agent(&name, None).ok_or_else(|| format!("Skill「{name}」不存在"))?;
|
||||
let skill_dir = resolve_custom_skill_dir_with_agent(&name, None)
|
||||
.ok_or_else(|| format!("Skill「{name}」不存在"))?;
|
||||
if !skill_dir.exists() {
|
||||
return Err(format!("Skill「{name}」不存在"));
|
||||
}
|
||||
@@ -457,7 +458,9 @@ fn clean_cli_output(text: &str) -> String {
|
||||
/// 根据 agentId 解析该 Agent 的 workspace/skills 目录
|
||||
/// 如果 agentId 为 None 或 "main",返回 None(使用默认的 ~/.openclaw/skills)
|
||||
fn resolve_agent_skills_dir(agent_id: Option<&str>) -> Option<std::path::PathBuf> {
|
||||
let id = agent_id.map(|s| s.trim()).filter(|s| !s.is_empty() && *s != "main")?;
|
||||
let id = agent_id
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty() && *s != "main")?;
|
||||
// 读取 openclaw.json 获取 agent workspace
|
||||
let config = super::config::load_openclaw_json().ok()?;
|
||||
let workspace = config
|
||||
@@ -485,7 +488,9 @@ fn resolve_agent_skills_dir(agent_id: Option<&str>) -> Option<std::path::PathBuf
|
||||
Some(expanded.join("skills"))
|
||||
}
|
||||
|
||||
fn custom_skill_roots_for_agent(agent_skills_dir: Option<&std::path::Path>) -> Vec<(std::path::PathBuf, &'static str)> {
|
||||
fn custom_skill_roots_for_agent(
|
||||
agent_skills_dir: Option<&std::path::Path>,
|
||||
) -> Vec<(std::path::PathBuf, &'static str)> {
|
||||
let mut roots = Vec::new();
|
||||
|
||||
// 如果指定了 agent 的 skills 目录,优先放在第一位
|
||||
@@ -532,14 +537,20 @@ fn custom_skill_roots_for_agent(agent_skills_dir: Option<&std::path::Path>) -> V
|
||||
roots
|
||||
}
|
||||
|
||||
fn resolve_custom_skill_dir_with_agent(name: &str, agent_skills_dir: Option<&std::path::Path>) -> Option<std::path::PathBuf> {
|
||||
fn resolve_custom_skill_dir_with_agent(
|
||||
name: &str,
|
||||
agent_skills_dir: Option<&std::path::Path>,
|
||||
) -> Option<std::path::PathBuf> {
|
||||
custom_skill_roots_for_agent(agent_skills_dir)
|
||||
.into_iter()
|
||||
.map(|(root, _)| root.join(name))
|
||||
.find(|path| path.exists())
|
||||
}
|
||||
|
||||
fn scan_custom_skill_detail(name: &str, agent_skills_dir: Option<&std::path::Path>) -> Option<Value> {
|
||||
fn scan_custom_skill_detail(
|
||||
name: &str,
|
||||
agent_skills_dir: Option<&std::path::Path>,
|
||||
) -> Option<Value> {
|
||||
for (root, source_label) in custom_skill_roots_for_agent(agent_skills_dir) {
|
||||
let skill_path = root.join(name);
|
||||
if !skill_path.exists() {
|
||||
@@ -593,7 +604,9 @@ fn scan_custom_skill_detail(name: &str, agent_skills_dir: Option<&std::path::Pat
|
||||
None
|
||||
}
|
||||
|
||||
fn scan_local_skill_entries_for_agent(agent_skills_dir: Option<&std::path::Path>) -> Result<Vec<Value>, String> {
|
||||
fn scan_local_skill_entries_for_agent(
|
||||
agent_skills_dir: Option<&std::path::Path>,
|
||||
) -> Result<Vec<Value>, String> {
|
||||
let mut skills = Vec::new();
|
||||
|
||||
for (skills_dir, source_label) in custom_skill_roots_for_agent(agent_skills_dir) {
|
||||
@@ -663,7 +676,10 @@ fn scan_local_skill_entries() -> Result<Vec<Value>, String> {
|
||||
}
|
||||
|
||||
/// CLI 不可用或当前结果不可用时的兜底:扫描本地自定义 Skills 目录
|
||||
fn scan_local_skills(cli_diagnostic: Option<Value>, agent_skills_dir: Option<&std::path::Path>) -> Result<Value, String> {
|
||||
fn scan_local_skills(
|
||||
cli_diagnostic: Option<Value>,
|
||||
agent_skills_dir: Option<&std::path::Path>,
|
||||
) -> Result<Value, String> {
|
||||
let roots = custom_skill_roots_for_agent(agent_skills_dir);
|
||||
let scanned_roots: Vec<String> = roots
|
||||
.iter()
|
||||
|
||||
@@ -62,11 +62,8 @@ fn handle_menu_event(app: &AppHandle, id: &str) {
|
||||
"gateway_start" => {
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = crate::commands::service::start_service(
|
||||
app2,
|
||||
"ai.openclaw.gateway".into(),
|
||||
)
|
||||
.await;
|
||||
let _ = crate::commands::service::start_service(app2, "ai.openclaw.gateway".into())
|
||||
.await;
|
||||
});
|
||||
}
|
||||
"gateway_stop" => {
|
||||
@@ -77,11 +74,9 @@ fn handle_menu_event(app: &AppHandle, id: &str) {
|
||||
"gateway_restart" => {
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = crate::commands::service::restart_service(
|
||||
app2,
|
||||
"ai.openclaw.gateway".into(),
|
||||
)
|
||||
.await;
|
||||
let _ =
|
||||
crate::commands::service::restart_service(app2, "ai.openclaw.gateway".into())
|
||||
.await;
|
||||
});
|
||||
}
|
||||
"quit" => {
|
||||
|
||||
Reference in New Issue
Block a user