style: cargo fmt

This commit is contained in:
晴天
2026-04-11 04:45:58 +08:00
parent 271dc93b08
commit fef694a087
7 changed files with 267 additions and 124 deletions

View File

@@ -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 11wmic 已废弃)
#[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 11wmic 已废弃)
#[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()),
}
}
}

View File

@@ -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, "未配置 allowedOriginsautoPair 会自动修复)", t),
None => finish_step(
"allowed_origins",
false,
"未配置 allowedOriginsautoPair 会自动修复)",
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 {

View File

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

View File

@@ -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"),
);
}
}

View File

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

View File

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

View File

@@ -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" => {