diff --git a/src-tauri/src/commands/agent.rs b/src-tauri/src/commands/agent.rs index 9cbf99c..aaeb703 100644 --- a/src-tauri/src/commands/agent.rs +++ b/src-tauri/src/commands/agent.rs @@ -1,8 +1,8 @@ +use crate::utils::openclaw_command_async; /// Agent 管理命令 — 调用 openclaw CLI 实现增删改查 use serde_json::Value; use std::fs; use std::io::Write; -use crate::utils::openclaw_command_async; /// 获取 agent 列表 #[tauri::command] @@ -19,13 +19,16 @@ pub async fn list_agents() -> Result { } let stdout = String::from_utf8_lossy(&output.stdout); - serde_json::from_str(&stdout) - .map_err(|e| format!("解析 JSON 失败: {e}")) + serde_json::from_str(&stdout).map_err(|e| format!("解析 JSON 失败: {e}")) } /// 创建新 agent #[tauri::command] -pub async fn add_agent(name: String, model: String, workspace: Option) -> Result { +pub async fn add_agent( + name: String, + model: String, + workspace: Option, +) -> Result { let ws = match workspace { Some(ref w) if !w.is_empty() => std::path::PathBuf::from(w), _ => super::openclaw_dir() @@ -95,10 +98,9 @@ pub fn update_agent_identity( emoji: Option, ) -> Result { let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path) - .map_err(|e| format!("读取配置失败: {e}"))?; - let mut config: Value = serde_json::from_str(&content) - .map_err(|e| format!("解析 JSON 失败: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; + let mut config: Value = + serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; let agents_list = config .get_mut("agents") @@ -113,7 +115,8 @@ pub fn update_agent_identity( // 确保 identity 字段存在且为对象 if !agent.get("identity").and_then(|i| i.as_object()).is_some() { - agent.as_object_mut() + agent + .as_object_mut() .ok_or("Agent 格式错误")? .insert("identity".to_string(), serde_json::json!({})); } @@ -135,21 +138,21 @@ pub fn update_agent_identity( } // 提前提取 workspace 路径(克隆为 String,避免借用冲突) - let workspace_path = agent.get("workspace") + let workspace_path = agent + .get("workspace") .and_then(|w| w.as_str()) .map(|s| s.to_string()) .or_else(|| { - config.get("agents") + config + .get("agents") .and_then(|a| a.get("defaults")) .and_then(|d| d.get("workspace")) .and_then(|w| w.as_str()) .map(|s| s.to_string()) }); - let json = serde_json::to_string_pretty(&config) - .map_err(|e| format!("序列化失败: {e}"))?; - fs::write(&path, json) - .map_err(|e| format!("写入配置失败: {e}"))?; + let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; + fs::write(&path, json).map_err(|e| format!("写入配置失败: {e}"))?; // 删除 IDENTITY.md 文件,让配置文件生效 if let Some(ws_str) = workspace_path { @@ -175,8 +178,7 @@ pub fn backup_agent(id: String) -> Result { let zip_name = format!("agent-{}-{}.zip", id, now.format("%Y%m%d-%H%M%S")); let zip_path = tmp_dir.join(&zip_name); - let file = fs::File::create(&zip_path) - .map_err(|e| format!("创建 zip 失败: {e}"))?; + let file = fs::File::create(&zip_path).map_err(|e| format!("创建 zip 失败: {e}"))?; let mut zip = zip::ZipWriter::new(file); let options = zip::write::SimpleFileOptions::default() .compression_method(zip::CompressionMethod::Deflated); @@ -193,20 +195,19 @@ fn collect_dir_to_zip( zip: &mut zip::ZipWriter, options: zip::write::SimpleFileOptions, ) -> Result<(), String> { - let entries = fs::read_dir(dir) - .map_err(|e| format!("读取目录失败: {e}"))?; + let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {e}"))?; for entry in entries.flatten() { let path = entry.path(); - let rel = path.strip_prefix(base) + let rel = path + .strip_prefix(base) .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); if path.is_dir() { collect_dir_to_zip(base, &path, zip, options)?; } else { - let content = fs::read(&path) - .map_err(|e| format!("读取 {rel} 失败: {e}"))?; + let content = fs::read(&path).map_err(|e| format!("读取 {rel} 失败: {e}"))?; zip.start_file(&rel, options) .map_err(|e| format!("写入 zip 失败: {e}"))?; zip.write_all(&content) @@ -220,10 +221,9 @@ fn collect_dir_to_zip( #[tauri::command] pub fn update_agent_model(id: String, model: String) -> Result { let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path) - .map_err(|e| format!("读取配置失败: {e}"))?; - let mut config: Value = serde_json::from_str(&content) - .map_err(|e| format!("解析 JSON 失败: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; + let mut config: Value = + serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; let agents_list = config .get_mut("agents") @@ -237,14 +237,13 @@ pub fn update_agent_model(id: String, model: String) -> Result { .ok_or(format!("Agent「{id}」不存在"))?; let model_obj = serde_json::json!({ "primary": model }); - agent.as_object_mut() + agent + .as_object_mut() .ok_or("Agent 格式错误")? .insert("model".to_string(), model_obj); - let json = serde_json::to_string_pretty(&config) - .map_err(|e| format!("序列化失败: {e}"))?; - fs::write(&path, json) - .map_err(|e| format!("写入配置失败: {e}"))?; + let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; + fs::write(&path, json).map_err(|e| format!("写入配置失败: {e}"))?; Ok("已更新".into()) } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index cde7dc4..56ec473 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -1,11 +1,11 @@ +use crate::utils::openclaw_command; /// 配置读写命令 use serde_json::Value; use std::fs; -use std::path::PathBuf; -use std::process::Command; -use crate::utils::openclaw_command; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; +use std::path::PathBuf; +use std::process::Command; use crate::models::types::VersionInfo; @@ -49,10 +49,9 @@ fn backups_dir() -> PathBuf { #[tauri::command] pub fn read_openclaw_config() -> Result { let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path) - .map_err(|e| format!("读取配置失败: {e}"))?; - let mut config: Value = serde_json::from_str(&content) - .map_err(|e| format!("解析 JSON 失败: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; + let mut config: Value = + serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; // 自动清理 UI 专属字段,防止污染配置导致 CLI 启动失败 if has_ui_fields(&config) { @@ -60,8 +59,7 @@ pub fn read_openclaw_config() -> Result { // 静默写回清理后的配置 let bak = super::openclaw_dir().join("openclaw.json.bak"); let _ = fs::copy(&path, &bak); - let json = serde_json::to_string_pretty(&config) - .map_err(|e| format!("序列化失败: {e}"))?; + let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; let _ = fs::write(&path, json); } @@ -77,10 +75,8 @@ pub fn write_openclaw_config(config: Value) -> Result<(), String> { // 清理 UI 专属字段,避免 CLI schema 校验失败 let cleaned = strip_ui_fields(config); // 写入 - let json = serde_json::to_string_pretty(&cleaned) - .map_err(|e| format!("序列化失败: {e}"))?; - fs::write(&path, json) - .map_err(|e| format!("写入失败: {e}")) + let json = serde_json::to_string_pretty(&cleaned).map_err(|e| format!("序列化失败: {e}"))?; + fs::write(&path, json).map_err(|e| format!("写入失败: {e}")) } /// 检测配置中是否包含 UI 专属字段 @@ -134,8 +130,13 @@ fn strip_ui_fields(mut val: Value) -> Value { mobj.remove("testStatus"); mobj.remove("testError"); if !mobj.contains_key("name") { - if let Some(id) = mobj.get("id").and_then(|v| v.as_str()) { - mobj.insert("name".into(), Value::String(id.to_string())); + if let Some(id) = + mobj.get("id").and_then(|v| v.as_str()) + { + mobj.insert( + "name".into(), + Value::String(id.to_string()), + ); } } } @@ -157,19 +158,15 @@ pub fn read_mcp_config() -> Result { if !path.exists() { return Ok(Value::Object(Default::default())); } - let content = fs::read_to_string(&path) - .map_err(|e| format!("读取 MCP 配置失败: {e}"))?; - serde_json::from_str(&content) - .map_err(|e| format!("解析 JSON 失败: {e}")) + let content = fs::read_to_string(&path).map_err(|e| format!("读取 MCP 配置失败: {e}"))?; + serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}")) } #[tauri::command] pub fn write_mcp_config(config: Value) -> Result<(), String> { let path = super::openclaw_dir().join("mcp.json"); - let json = serde_json::to_string_pretty(&config) - .map_err(|e| format!("序列化失败: {e}"))?; - fs::write(&path, json) - .map_err(|e| format!("写入失败: {e}")) + let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; + fs::write(&path, json).map_err(|e| format!("写入失败: {e}")) } /// 获取本地安装的 openclaw 版本号(异步版本) @@ -201,7 +198,10 @@ async fn get_local_version() -> Option { // 先查汉化版,再查官方版 for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] { let pkg_json = PathBuf::from(&appdata) - .join("npm").join("node_modules").join(pkg).join("package.json"); + .join("npm") + .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::(&content) .ok() @@ -215,9 +215,16 @@ async fn get_local_version() -> Option { } // 所有平台通用 fallback: CLI 输出(异步) use crate::utils::openclaw_command_async; - let output = openclaw_command_async().arg("--version").output().await.ok()?; + let output = openclaw_command_async() + .arg("--version") + .output() + .await + .ok()?; let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); - raw.split_whitespace().last().filter(|s| !s.is_empty()).map(String::from) + raw.split_whitespace() + .last() + .filter(|s| !s.is_empty()) + .map(String::from) } /// 从 npm registry 获取最新版本号,超时 5 秒 @@ -226,7 +233,9 @@ async fn get_latest_version_for(source: &str) -> Option { .timeout(std::time::Duration::from_secs(2)) .build() .ok()?; - let pkg = npm_package_name(source).replace('/', "%2F").replace('@', "%40"); + let pkg = npm_package_name(source) + .replace('/', "%2F") + .replace('@', "%40"); let registry = get_configured_registry(); let url = format!("{registry}/{pkg}/latest"); let resp = client.get(&url).send().await.ok()?; @@ -314,8 +323,8 @@ fn npm_package_name(source: &str) -> &'static str { /// 执行 npm 全局升级 openclaw(流式推送日志) #[tauri::command] pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result { - use std::process::Stdio; use std::io::{BufRead, BufReader}; + use std::process::Stdio; use tauri::Emitter; let current_source = detect_installed_source(); @@ -329,9 +338,7 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result Result Result Result { - let _ = app.emit("upgrade-log", "⚠️ Gateway 重装失败,请手动执行 openclaw gateway install"); + let _ = app.emit( + "upgrade-log", + "⚠️ Gateway 重装失败,请手动执行 openclaw gateway install", + ); } } } @@ -438,7 +448,10 @@ pub fn check_installation() -> Result { let installed = dir.join("openclaw.json").exists(); let mut result = serde_json::Map::new(); result.insert("installed".into(), Value::Bool(installed)); - result.insert("path".into(), Value::String(dir.to_string_lossy().to_string())); + result.insert( + "path".into(), + Value::String(dir.to_string_lossy().to_string()), + ); Ok(Value::Object(result)) } @@ -467,9 +480,7 @@ pub fn check_node() -> Result { #[tauri::command] pub fn write_env_file(path: String, config: String) -> Result<(), String> { let expanded = if path.starts_with("~/") { - dirs::home_dir() - .unwrap_or_default() - .join(&path[2..]) + dirs::home_dir().unwrap_or_default().join(&path[2..]) } else { PathBuf::from(&path) }; @@ -483,8 +494,7 @@ pub fn write_env_file(path: String, config: String) -> Result<(), String> { if let Some(parent) = expanded.parent() { let _ = fs::create_dir_all(parent); } - fs::write(&expanded, &config) - .map_err(|e| format!("写入 .env 失败: {e}")) + fs::write(&expanded, &config).map_err(|e| format!("写入 .env 失败: {e}")) } // ===== 备份管理 ===== @@ -496,22 +506,23 @@ pub fn list_backups() -> Result { return Ok(Value::Array(vec![])); } let mut backups: Vec = vec![]; - let entries = fs::read_dir(&dir) - .map_err(|e| format!("读取备份目录失败: {e}"))?; + let entries = fs::read_dir(&dir).map_err(|e| format!("读取备份目录失败: {e}"))?; for entry in entries.flatten() { let path = entry.path(); if path.extension().and_then(|e| e.to_str()) != Some("json") { continue; } - let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); + let name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); let meta = fs::metadata(&path).ok(); let size = meta.as_ref().map(|m| m.len()).unwrap_or(0); // macOS 支持 created(),fallback 到 modified() let created = meta - .and_then(|m| { - m.created().ok().or_else(|| m.modified().ok()) - }) + .and_then(|m| m.created().ok().or_else(|| m.modified().ok())) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .map(|d| d.as_secs()) .unwrap_or(0); @@ -534,8 +545,7 @@ pub fn list_backups() -> Result { #[tauri::command] pub fn create_backup() -> Result { let dir = backups_dir(); - fs::create_dir_all(&dir) - .map_err(|e| format!("创建备份目录失败: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("创建备份目录失败: {e}"))?; let src = super::openclaw_dir().join("openclaw.json"); if !src.exists() { @@ -545,8 +555,7 @@ pub fn create_backup() -> Result { let now = chrono::Local::now(); let name = format!("openclaw-{}.json", now.format("%Y%m%d-%H%M%S")); let dest = dir.join(&name); - fs::copy(&src, &dest) - .map_err(|e| format!("备份失败: {e}"))?; + fs::copy(&src, &dest).map_err(|e| format!("备份失败: {e}"))?; let size = fs::metadata(&dest).map(|m| m.len()).unwrap_or(0); let mut obj = serde_json::Map::new(); @@ -576,8 +585,7 @@ pub fn restore_backup(name: String) -> Result<(), String> { let _ = create_backup(); } - fs::copy(&backup_path, &target) - .map_err(|e| format!("恢复失败: {e}"))?; + fs::copy(&backup_path, &target).map_err(|e| format!("恢复失败: {e}"))?; Ok(()) } @@ -590,8 +598,7 @@ pub fn delete_backup(name: String) -> Result<(), String> { if !path.exists() { return Err(format!("备份文件不存在: {name}")); } - fs::remove_file(&path) - .map_err(|e| format!("删除失败: {e}")) + fs::remove_file(&path).map_err(|e| format!("删除失败: {e}")) } /// 获取当前用户 UID(macOS/Linux 用 id -u,Windows 返回 0) @@ -663,7 +670,6 @@ pub async fn restart_gateway() -> Result { reload_gateway().await } - /// 测试模型连通性:向 provider 发送一个简单的 chat completion 请求 #[tauri::command] pub async fn test_model( @@ -739,10 +745,7 @@ pub async fn test_model( /// 获取服务商的远程模型列表(调用 /models 接口) #[tauri::command] -pub async fn list_remote_models( - base_url: String, - api_key: String, -) -> Result, String> { +pub async fn list_remote_models(base_url: String, api_key: String) -> Result, String> { let url = format!("{}/models", base_url.trim_end_matches('/')); let client = reqwest::Client::builder() @@ -811,11 +814,10 @@ pub async fn install_gateway() -> Result { match cli_check { Ok(o) if o.status.success() => {} _ => { - return Err( - "openclaw CLI 未安装。请先执行以下命令安装:\n\n\ + return Err("openclaw CLI 未安装。请先执行以下命令安装:\n\n\ npm install -g @qingchencloud/openclaw-zh\n\n\ - 安装完成后再点击此按钮安装 Gateway 服务。".into() - ); + 安装完成后再点击此按钮安装 Gateway 服务。" + .into()); } } @@ -852,16 +854,13 @@ pub fn uninstall_gateway() -> Result { let home = dirs::home_dir().unwrap_or_default(); let plist = home.join("Library/LaunchAgents/ai.openclaw.gateway.plist"); if plist.exists() { - fs::remove_file(&plist) - .map_err(|e| format!("删除 plist 失败: {e}"))?; + fs::remove_file(&plist).map_err(|e| format!("删除 plist 失败: {e}"))?; } } #[cfg(not(target_os = "macos"))] { // Windows/Linux: 停止 Gateway 服务 - let _ = openclaw_command() - .args(["gateway", "stop"]) - .output(); + let _ = openclaw_command().args(["gateway", "stop"]).output(); } Ok("Gateway 服务已卸载".to_string()) @@ -875,6 +874,5 @@ pub fn get_npm_registry() -> Result { #[tauri::command] pub fn set_npm_registry(registry: String) -> Result<(), String> { let path = super::openclaw_dir().join("npm-registry.txt"); - fs::write(&path, registry.trim()) - .map_err(|e| format!("保存失败: {e}")) + fs::write(&path, registry.trim()).map_err(|e| format!("保存失败: {e}")) } diff --git a/src-tauri/src/commands/device.rs b/src-tauri/src/commands/device.rs index d3e1d5a..10e5285 100644 --- a/src-tauri/src/commands/device.rs +++ b/src-tauri/src/commands/device.rs @@ -1,13 +1,16 @@ /// 设备密钥管理 + Gateway connect 握手签名 -use ed25519_dalek::{SigningKey, Signer, VerifyingKey}; -use sha2::{Sha256, Digest}; +use ed25519_dalek::{Signer, SigningKey, VerifyingKey}; use serde_json::Value; +use sha2::{Digest, Sha256}; use std::fs; const DEVICE_KEY_FILE: &str = "clawpanel-device-key.json"; const SCOPES: &[&str] = &[ - "operator.admin", "operator.approvals", "operator.pairing", - "operator.read", "operator.write", + "operator.admin", + "operator.approvals", + "operator.pairing", + "operator.read", + "operator.write", ]; /// 获取或生成设备密钥 @@ -16,17 +19,15 @@ fn get_or_create_key() -> Result<(String, String, SigningKey), String> { let path = dir.join(DEVICE_KEY_FILE); if path.exists() { - let content = fs::read_to_string(&path) - .map_err(|e| format!("读取设备密钥失败: {e}"))?; - let json: Value = serde_json::from_str(&content) - .map_err(|e| format!("解析设备密钥失败: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("读取设备密钥失败: {e}"))?; + let json: Value = + serde_json::from_str(&content).map_err(|e| format!("解析设备密钥失败: {e}"))?; let device_id = json["deviceId"].as_str().unwrap_or("").to_string(); let pub_b64 = json["publicKey"].as_str().unwrap_or("").to_string(); let secret_hex = json["secretKey"].as_str().unwrap_or(""); - let secret_bytes = hex::decode(secret_hex) - .map_err(|e| format!("解码密钥失败: {e}"))?; + let secret_bytes = hex::decode(secret_hex).map_err(|e| format!("解码密钥失败: {e}"))?; if secret_bytes.len() != 32 { return Err("密钥长度错误".into()); } @@ -76,9 +77,12 @@ mod hex { data.as_ref().iter().map(|b| format!("{b:02x}")).collect() } pub fn decode(s: &str) -> Result, String> { - if s.len() % 2 != 0 { return Err("奇数长度".into()) } - (0..s.len()).step_by(2) - .map(|i| u8::from_str_radix(&s[i..i+2], 16).map_err(|e| e.to_string())) + if s.len() % 2 != 0 { + return Err("奇数长度".into()); + } + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())) .collect() } } diff --git a/src-tauri/src/commands/extensions.rs b/src-tauri/src/commands/extensions.rs index b3ad387..7f88556 100644 --- a/src-tauri/src/commands/extensions.rs +++ b/src-tauri/src/commands/extensions.rs @@ -1,8 +1,8 @@ /// 扩展工具命令(cftunnel + ClawApp) use serde_json::Value; -use std::process::Command; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; +use std::process::Command; /// 按第一个冒号(半角 ':' 或全角 ':')分割,返回冒号之后的内容(trim 后) fn split_after_colon(s: &str) -> &str { @@ -30,7 +30,9 @@ fn parse_cftunnel_status(output: &str) -> serde_json::Map { let running = rest.contains("运行中"); map.insert("running".into(), Value::Bool(running)); // 匹配英文和全角 'PID:' / 'PID:' - let pid_rest = rest.split("PID:").nth(1) + let pid_rest = rest + .split("PID:") + .nth(1) .or_else(|| rest.split("PID:").nth(1)); if let Some(pid_str) = pid_rest { let pid = pid_str.trim().trim_end_matches(')').trim(); @@ -73,7 +75,10 @@ fn cftunnel_bin() -> String { let candidates = [ home.join("bin").join("cftunnel.exe"), home.join(".cftunnel").join("cftunnel.exe"), - home.join("AppData").join("Local").join("cftunnel").join("cftunnel.exe"), + home.join("AppData") + .join("Local") + .join("cftunnel") + .join("cftunnel.exe"), ]; for path in &candidates { if path.exists() { @@ -98,10 +103,7 @@ fn check_cftunnel_process() -> Option<(Option, bool)> { #[cfg(target_os = "macos")] { // macOS: 通过 launchctl 检测 - let output = Command::new("launchctl") - .args(["list"]) - .output() - .ok()?; + let output = Command::new("launchctl").args(["list"]).output().ok()?; let text = String::from_utf8_lossy(&output.stdout); for line in text.lines() { if line.contains("com.cftunnel") { @@ -126,7 +128,9 @@ fn check_cftunnel_process() -> Option<(Option, bool)> { let text = String::from_utf8_lossy(&output.stdout); if text.contains("cftunnel.exe") { // 尝试提取 PID(CSV 格式: "cftunnel.exe","1234",...) - let pid = text.lines().next() + let pid = text + .lines() + .next() .and_then(|line| line.split(',').nth(1)) .and_then(|s| s.trim_matches('"').parse::().ok()); return Some((pid, true)); @@ -143,7 +147,9 @@ fn check_cftunnel_process() -> Option<(Option, bool)> { .ok()?; if output.status.success() { let text = String::from_utf8_lossy(&output.stdout); - let pid = text.lines().next() + let pid = text + .lines() + .next() .and_then(|s| s.trim().parse::().ok()); return Some((pid, true)); } @@ -182,7 +188,10 @@ pub fn get_cftunnel_status() -> Result { } // 仅当 status 报未运行时才做进程检测补充 - let reported_running = result.get("running").and_then(|v| v.as_bool()).unwrap_or(false); + let reported_running = result + .get("running") + .and_then(|v| v.as_bool()) + .unwrap_or(false); if !reported_running { if let Some((pid, running)) = check_cftunnel_process() { if running { @@ -251,7 +260,8 @@ pub fn get_clawapp_status() -> Result { let running = std::net::TcpStream::connect_timeout( &"127.0.0.1:3210".parse().unwrap(), std::time::Duration::from_millis(150), - ).is_ok(); + ) + .is_ok(); result.insert("running".into(), Value::Bool(running)); @@ -292,10 +302,7 @@ fn check_clawapp_installed() -> bool { } #[cfg(not(target_os = "windows"))] { - if let Ok(out) = Command::new("npm") - .args(["list", "-g", "clawapp"]) - .output() - { + if let Ok(out) = Command::new("npm").args(["list", "-g", "clawapp"]).output() { return out.status.success(); } false @@ -307,8 +314,8 @@ fn check_clawapp_installed() -> bool { /// Windows: PowerShell 下载安装 #[tauri::command] pub async fn install_cftunnel(app: tauri::AppHandle) -> Result { - use std::process::Stdio; use std::io::{BufRead, BufReader}; + use std::process::Stdio; use tauri::Emitter; let _ = app.emit("install-log", "开始安装 cftunnel..."); @@ -354,10 +361,21 @@ Write-Output '安装完成' "#; // 使用完整路径调用 PowerShell,避免 MSYS2/Git Bash 环境下找不到 let ps_path = std::env::var("SystemRoot") - .map(|root| format!("{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", root)) + .map(|root| { + format!( + "{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + root + ) + }) .unwrap_or_else(|_| "powershell.exe".to_string()); Command::new(&ps_path) - .args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", install_script]) + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + install_script, + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() @@ -405,8 +423,8 @@ Write-Output '安装完成' /// 一键安装 ClawApp(通过 npm) #[tauri::command] pub async fn install_clawapp(app: tauri::AppHandle) -> Result { - use std::process::Stdio; use std::io::{BufRead, BufReader}; + use std::process::Stdio; use tauri::Emitter; let _ = app.emit("install-log", "开始安装 ClawApp..."); diff --git a/src-tauri/src/commands/logs.rs b/src-tauri/src/commands/logs.rs index 69f24ae..e12c539 100644 --- a/src-tauri/src/commands/logs.rs +++ b/src-tauri/src/commands/logs.rs @@ -31,8 +31,7 @@ pub fn read_log_tail(log_name: String, lines: Option) -> Result bool { @@ -27,8 +27,8 @@ async fn agent_workspace(agent_id: &str) -> Result { } let stdout = String::from_utf8_lossy(&output.stdout); - let agents: serde_json::Value = serde_json::from_str(&stdout) - .map_err(|e| format!("解析 JSON 失败: {e}"))?; + let agents: serde_json::Value = + serde_json::from_str(&stdout).map_err(|e| format!("解析 JSON 失败: {e}"))?; if let Some(arr) = agents.as_array() { for a in arr { @@ -63,7 +63,10 @@ async fn memory_dir_for_agent(agent_id: &str, category: &str) -> Result) -> Result, String> { +pub async fn list_memory_files( + category: String, + agent_id: Option, +) -> Result, String> { let aid = agent_id.as_deref().unwrap_or("main"); let dir = memory_dir_for_agent(aid, &category).await?; if !dir.exists() { @@ -82,8 +85,7 @@ fn collect_files( files: &mut Vec, category: &str, ) -> Result<(), String> { - let entries = fs::read_dir(dir) - .map_err(|e| format!("读取目录失败: {e}"))?; + let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {e}"))?; for entry in entries.flatten() { let path = entry.path(); @@ -95,7 +97,8 @@ fn collect_files( } else { let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); if matches!(ext, "md" | "txt" | "json" | "jsonl") { - let rel = path.strip_prefix(base) + let rel = path + .strip_prefix(base) .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| path.to_string_lossy().to_string()); files.push(rel); @@ -122,8 +125,7 @@ pub async fn read_memory_file(path: String, agent_id: Option) -> Result< if let Ok(dir) = c { let full = dir.join(&path); if full.exists() { - return fs::read_to_string(&full) - .map_err(|e| format!("读取失败: {e}")); + return fs::read_to_string(&full).map_err(|e| format!("读取失败: {e}")); } } } @@ -132,7 +134,12 @@ pub async fn read_memory_file(path: String, agent_id: Option) -> Result< } #[tauri::command] -pub async fn write_memory_file(path: String, content: String, category: Option, agent_id: Option) -> Result<(), String> { +pub async fn write_memory_file( + path: String, + content: String, + category: Option, + agent_id: Option, +) -> Result<(), String> { if is_unsafe_path(&path) { return Err("非法路径".to_string()); } @@ -165,8 +172,7 @@ pub async fn delete_memory_file(path: String, agent_id: Option) -> Resul if let Ok(dir) = c { let full = dir.join(&path); if full.exists() { - return fs::remove_file(&full) - .map_err(|e| format!("删除失败: {e}")); + return fs::remove_file(&full).map_err(|e| format!("删除失败: {e}")); } } } @@ -175,7 +181,10 @@ pub async fn delete_memory_file(path: String, agent_id: Option) -> Resul } #[tauri::command] -pub async fn export_memory_zip(category: String, agent_id: Option) -> Result { +pub async fn export_memory_zip( + category: String, + agent_id: Option, +) -> Result { let aid = agent_id.as_deref().unwrap_or("main"); let dir = memory_dir_for_agent(aid, &category).await?; if !dir.exists() { @@ -196,16 +205,15 @@ pub async fn export_memory_zip(category: String, agent_id: Option) -> Re ); let zip_path = tmp_dir.join(&zip_name); - let file = fs::File::create(&zip_path) - .map_err(|e| format!("创建 zip 失败: {e}"))?; + let file = fs::File::create(&zip_path).map_err(|e| format!("创建 zip 失败: {e}"))?; let mut zip = zip::ZipWriter::new(file); let options = zip::write::SimpleFileOptions::default() .compression_method(zip::CompressionMethod::Deflated); for rel_path in &files { let full_path = dir.join(rel_path); - let content = fs::read_to_string(&full_path) - .map_err(|e| format!("读取 {rel_path} 失败: {e}"))?; + let content = + fs::read_to_string(&full_path).map_err(|e| format!("读取 {rel_path} 失败: {e}"))?; zip.start_file(rel_path, options) .map_err(|e| format!("写入 zip 失败: {e}"))?; zip.write_all(content.as_bytes()) diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 0e3bab2..f17e8f5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -11,7 +11,5 @@ pub mod service; /// 获取 OpenClaw 配置目录 (~/.openclaw/) pub fn openclaw_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_default() - .join(".openclaw") + dirs::home_dir().unwrap_or_default().join(".openclaw") } diff --git a/src-tauri/src/commands/pairing.rs b/src-tauri/src/commands/pairing.rs index bb4102d..65138e6 100644 --- a/src-tauri/src/commands/pairing.rs +++ b/src-tauri/src/commands/pairing.rs @@ -9,11 +9,11 @@ pub fn auto_pair_device() -> Result { return Err("设备密钥文件不存在".into()); } - let device_key_content = std::fs::read_to_string(&device_key_path) - .map_err(|e| format!("读取设备密钥失败: {e}"))?; + let device_key_content = + std::fs::read_to_string(&device_key_path).map_err(|e| format!("读取设备密钥失败: {e}"))?; - let device_key: serde_json::Value = serde_json::from_str(&device_key_content) - .map_err(|e| format!("解析设备密钥失败: {e}"))?; + let device_key: serde_json::Value = + serde_json::from_str(&device_key_content).map_err(|e| format!("解析设备密钥失败: {e}"))?; let device_id = device_key["deviceId"] .as_str() @@ -26,20 +26,20 @@ pub fn auto_pair_device() -> Result { .to_string(); // 读取或创建 paired.json - let paired_path = crate::commands::openclaw_dir().join("devices").join("paired.json"); + let paired_path = crate::commands::openclaw_dir() + .join("devices") + .join("paired.json"); let devices_dir = crate::commands::openclaw_dir().join("devices"); // 确保 devices 目录存在 if !devices_dir.exists() { - std::fs::create_dir_all(&devices_dir) - .map_err(|e| format!("创建 devices 目录失败: {e}"))?; + std::fs::create_dir_all(&devices_dir).map_err(|e| format!("创建 devices 目录失败: {e}"))?; } let mut paired: serde_json::Value = if paired_path.exists() { let content = std::fs::read_to_string(&paired_path) .map_err(|e| format!("读取 paired.json 失败: {e}"))?; - serde_json::from_str(&content) - .map_err(|e| format!("解析 paired.json 失败: {e}"))? + serde_json::from_str(&content).map_err(|e| format!("解析 paired.json 失败: {e}"))? } else { serde_json::json!({}) }; @@ -86,8 +86,7 @@ pub fn auto_pair_device() -> Result { let new_content = serde_json::to_string_pretty(&paired) .map_err(|e| format!("序列化 paired.json 失败: {e}"))?; - std::fs::write(&paired_path, new_content) - .map_err(|e| format!("写入 paired.json 失败: {e}"))?; + std::fs::write(&paired_path, new_content).map_err(|e| format!("写入 paired.json 失败: {e}"))?; Ok("设备配对成功".into()) } @@ -100,27 +99,27 @@ pub fn check_pairing_status() -> Result { return Ok(false); } - let device_key_content = std::fs::read_to_string(&device_key_path) - .map_err(|e| format!("读取设备密钥失败: {e}"))?; + let device_key_content = + std::fs::read_to_string(&device_key_path).map_err(|e| format!("读取设备密钥失败: {e}"))?; - let device_key: serde_json::Value = serde_json::from_str(&device_key_content) - .map_err(|e| format!("解析设备密钥失败: {e}"))?; + let device_key: serde_json::Value = + serde_json::from_str(&device_key_content).map_err(|e| format!("解析设备密钥失败: {e}"))?; - let device_id = device_key["deviceId"] - .as_str() - .ok_or("设备 ID 不存在")?; + let device_id = device_key["deviceId"].as_str().ok_or("设备 ID 不存在")?; // 检查 paired.json - let paired_path = crate::commands::openclaw_dir().join("devices").join("paired.json"); + let paired_path = crate::commands::openclaw_dir() + .join("devices") + .join("paired.json"); if !paired_path.exists() { return Ok(false); } - let content = std::fs::read_to_string(&paired_path) - .map_err(|e| format!("读取 paired.json 失败: {e}"))?; + let content = + std::fs::read_to_string(&paired_path).map_err(|e| format!("读取 paired.json 失败: {e}"))?; - let paired: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("解析 paired.json 失败: {e}"))?; + let paired: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("解析 paired.json 失败: {e}"))?; Ok(paired.get(device_id).is_some()) } diff --git a/src-tauri/src/commands/service.rs b/src-tauri/src/commands/service.rs index c950da0..60efd1f 100644 --- a/src-tauri/src/commands/service.rs +++ b/src-tauri/src/commands/service.rs @@ -33,7 +33,9 @@ mod platform { .output() .map_err(|e| format!("获取 UID 失败: {e}"))?; let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - uid_str.parse::().map_err(|e| format!("解析 UID 失败: {e}")) + uid_str + .parse::() + .map_err(|e| format!("解析 UID 失败: {e}")) } /// 动态扫描 LaunchAgents 目录,只返回 OpenClaw 核心服务 @@ -60,19 +62,13 @@ mod platform { fn plist_path(label: &str) -> String { let home = dirs::home_dir().unwrap_or_default(); - format!( - "{}/Library/LaunchAgents/{}.plist", - home.display(), - label - ) + format!("{}/Library/LaunchAgents/{}.plist", home.display(), label) } /// 用 launchctl print 检测单个服务状态,返回 (running, pid) pub fn check_service_status(uid: u32, label: &str) -> (bool, Option) { let target = format!("gui/{}/{}", uid, label); - let output = Command::new("launchctl") - .args(["print", &target]) - .output(); + let output = Command::new("launchctl").args(["print", &target]).output(); let Ok(out) = output else { return (false, None); @@ -221,8 +217,12 @@ mod platform { /// 检测 openclaw CLI 是否已安装(文件系统检测,避免 spawn 进程) pub fn is_cli_installed() -> bool { if let Ok(appdata) = std::env::var("APPDATA") { - let cmd_path = std::path::Path::new(&appdata).join("npm").join("openclaw.cmd"); - if cmd_path.exists() { return true; } + let cmd_path = std::path::Path::new(&appdata) + .join("npm") + .join("openclaw.cmd"); + if cmd_path.exists() { + return true; + } } false } @@ -237,7 +237,11 @@ mod platform { let config_path = crate::commands::openclaw_dir().join("openclaw.json"); if let Ok(content) = std::fs::read_to_string(&config_path) { if let Ok(val) = serde_json::from_str::(&content) { - if let Some(port) = val.get("gateway").and_then(|g| g.get("port")).and_then(|p| p.as_u64()) { + if let Some(port) = val + .get("gateway") + .and_then(|g| g.get("port")) + .and_then(|p| p.as_u64()) + { if port > 0 && port < 65536 { return port as u16; } @@ -252,7 +256,9 @@ mod platform { let port = read_gateway_port(); let addr = format!("127.0.0.1:{port}"); match std::net::TcpStream::connect_timeout( - &addr.parse().unwrap_or_else(|_| "127.0.0.1:18789".parse().unwrap()), + &addr + .parse() + .unwrap_or_else(|_| "127.0.0.1:18789".parse().unwrap()), std::time::Duration::from_millis(150), ) { Ok(_) => (true, None), @@ -263,7 +269,10 @@ mod platform { /// 以前台模式 spawn Gateway(不需要管理员权限) pub async fn start_service_impl(_label: &str) -> Result<(), String> { if !is_cli_installed() { - return Err("openclaw CLI 未安装,请先通过 npm install -g @qingchencloud/openclaw-zh 安装".into()); + return Err( + "openclaw CLI 未安装,请先通过 npm install -g @qingchencloud/openclaw-zh 安装" + .into(), + ); } if check_service_status(0, "").0 { return Ok(()); @@ -312,7 +321,15 @@ mod platform { if check_service_status(0, "").0 { const CREATE_NO_WINDOW: u32 = 0x08000000; let _ = TokioCommand::new("cmd") - .args(["/c", "taskkill", "/f", "/im", "node.exe", "/fi", "WINDOWTITLE eq openclaw*"]) + .args([ + "/c", + "taskkill", + "/f", + "/im", + "node.exe", + "/fi", + "WINDOWTITLE eq openclaw*", + ]) .creation_flags(CREATE_NO_WINDOW) .output() .await; @@ -323,7 +340,9 @@ mod platform { pub async fn restart_service_impl(_label: &str) -> Result<(), String> { let _ = stop_service_impl(_label).await; for _ in 0..10 { - if !check_service_status(0, "").0 { break; } + if !check_service_status(0, "").0 { + break; + } tokio::time::sleep(std::time::Duration::from_millis(300)).await; } start_service_impl(_label).await @@ -342,7 +361,9 @@ mod platform { .output() .map_err(|e| format!("获取 UID 失败: {e}"))?; let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - uid_str.parse::().map_err(|e| format!("解析 UID 失败: {e}")) + uid_str + .parse::() + .map_err(|e| format!("解析 UID 失败: {e}")) } pub async fn is_cli_installed() -> bool { @@ -378,7 +399,10 @@ mod platform { async fn gateway_command(action: &str) -> Result<(), String> { if !is_cli_installed().await { - return Err("openclaw CLI 未安装,请先通过 npm install -g @qingchencloud/openclaw-zh 安装".into()); + return Err( + "openclaw CLI 未安装,请先通过 npm install -g @qingchencloud/openclaw-zh 安装" + .into(), + ); } let output = crate::utils::openclaw_command_async() .args(["gateway", action]) @@ -431,10 +455,7 @@ pub async fn get_services_status() -> Result, String> { label: label.clone(), pid, running, - description: desc_map - .get(label.as_str()) - .unwrap_or(&"") - .to_string(), + description: desc_map.get(label.as_str()).unwrap_or(&"").to_string(), cli_installed, }); } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 027c897..1e3cafd 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1,10 +1,10 @@ /// 系统托盘模块 /// Windows / macOS / Linux 通用,Tauri v2 内置跨平台支持 use tauri::{ - AppHandle, Manager, + image::Image, menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem}, tray::TrayIconBuilder, - image::Image, + AppHandle, Manager, }; pub fn setup_tray(app: &AppHandle) -> Result<(), Box> { @@ -74,4 +74,3 @@ fn handle_menu_event(app: &AppHandle, id: &str) { _ => {} } } - diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 5dc6127..43aba3e 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,6 +1,6 @@ -use std::process::Command; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; +use std::process::Command; /// 跨平台获取 openclaw 命令的方法(同步版本) /// 在 Windows 上使用 `cmd /c openclaw` 以兼容全局 npm 路径下的 `.cmd` 脚本