style: cargo fmt 格式化 Rust 代码,修复 CI 格式检查失败

This commit is contained in:
晴天
2026-03-04 12:20:59 +08:00
parent e62f270422
commit 7cd6bb9b1b
11 changed files with 250 additions and 208 deletions

View File

@@ -1,8 +1,8 @@
use crate::utils::openclaw_command_async;
/// Agent 管理命令 — 调用 openclaw CLI 实现增删改查 /// Agent 管理命令 — 调用 openclaw CLI 实现增删改查
use serde_json::Value; use serde_json::Value;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use crate::utils::openclaw_command_async;
/// 获取 agent 列表 /// 获取 agent 列表
#[tauri::command] #[tauri::command]
@@ -19,13 +19,16 @@ pub async fn list_agents() -> Result<Value, String> {
} }
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(&stdout) serde_json::from_str(&stdout).map_err(|e| format!("解析 JSON 失败: {e}"))
.map_err(|e| format!("解析 JSON 失败: {e}"))
} }
/// 创建新 agent /// 创建新 agent
#[tauri::command] #[tauri::command]
pub async fn add_agent(name: String, model: String, workspace: Option<String>) -> Result<Value, String> { pub async fn add_agent(
name: String,
model: String,
workspace: Option<String>,
) -> Result<Value, String> {
let ws = match workspace { let ws = match workspace {
Some(ref w) if !w.is_empty() => std::path::PathBuf::from(w), Some(ref w) if !w.is_empty() => std::path::PathBuf::from(w),
_ => super::openclaw_dir() _ => super::openclaw_dir()
@@ -95,10 +98,9 @@ pub fn update_agent_identity(
emoji: Option<String>, emoji: Option<String>,
) -> Result<String, String> { ) -> Result<String, String> {
let path = super::openclaw_dir().join("openclaw.json"); let path = super::openclaw_dir().join("openclaw.json");
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?;
.map_err(|e| format!("读取配置失败: {e}"))?; let mut config: Value =
let mut config: Value = serde_json::from_str(&content) serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?;
.map_err(|e| format!("解析 JSON 失败: {e}"))?;
let agents_list = config let agents_list = config
.get_mut("agents") .get_mut("agents")
@@ -113,7 +115,8 @@ pub fn update_agent_identity(
// 确保 identity 字段存在且为对象 // 确保 identity 字段存在且为对象
if !agent.get("identity").and_then(|i| i.as_object()).is_some() { if !agent.get("identity").and_then(|i| i.as_object()).is_some() {
agent.as_object_mut() agent
.as_object_mut()
.ok_or("Agent 格式错误")? .ok_or("Agent 格式错误")?
.insert("identity".to_string(), serde_json::json!({})); .insert("identity".to_string(), serde_json::json!({}));
} }
@@ -135,21 +138,21 @@ pub fn update_agent_identity(
} }
// 提前提取 workspace 路径(克隆为 String避免借用冲突 // 提前提取 workspace 路径(克隆为 String避免借用冲突
let workspace_path = agent.get("workspace") let workspace_path = agent
.get("workspace")
.and_then(|w| w.as_str()) .and_then(|w| w.as_str())
.map(|s| s.to_string()) .map(|s| s.to_string())
.or_else(|| { .or_else(|| {
config.get("agents") config
.get("agents")
.and_then(|a| a.get("defaults")) .and_then(|a| a.get("defaults"))
.and_then(|d| d.get("workspace")) .and_then(|d| d.get("workspace"))
.and_then(|w| w.as_str()) .and_then(|w| w.as_str())
.map(|s| s.to_string()) .map(|s| s.to_string())
}); });
let json = serde_json::to_string_pretty(&config) let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?;
.map_err(|e| format!("序列化失败: {e}"))?; fs::write(&path, json).map_err(|e| format!("写入配置失败: {e}"))?;
fs::write(&path, json)
.map_err(|e| format!("写入配置失败: {e}"))?;
// 删除 IDENTITY.md 文件,让配置文件生效 // 删除 IDENTITY.md 文件,让配置文件生效
if let Some(ws_str) = workspace_path { if let Some(ws_str) = workspace_path {
@@ -175,8 +178,7 @@ pub fn backup_agent(id: String) -> Result<String, String> {
let zip_name = format!("agent-{}-{}.zip", id, now.format("%Y%m%d-%H%M%S")); let zip_name = format!("agent-{}-{}.zip", id, now.format("%Y%m%d-%H%M%S"));
let zip_path = tmp_dir.join(&zip_name); let zip_path = tmp_dir.join(&zip_name);
let file = fs::File::create(&zip_path) let file = fs::File::create(&zip_path).map_err(|e| format!("创建 zip 失败: {e}"))?;
.map_err(|e| format!("创建 zip 失败: {e}"))?;
let mut zip = zip::ZipWriter::new(file); let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default() let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated); .compression_method(zip::CompressionMethod::Deflated);
@@ -193,20 +195,19 @@ fn collect_dir_to_zip(
zip: &mut zip::ZipWriter<fs::File>, zip: &mut zip::ZipWriter<fs::File>,
options: zip::write::SimpleFileOptions, options: zip::write::SimpleFileOptions,
) -> Result<(), String> { ) -> Result<(), String> {
let entries = fs::read_dir(dir) let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {e}"))?;
.map_err(|e| format!("读取目录失败: {e}"))?;
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
let rel = path.strip_prefix(base) let rel = path
.strip_prefix(base)
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_default(); .unwrap_or_default();
if path.is_dir() { if path.is_dir() {
collect_dir_to_zip(base, &path, zip, options)?; collect_dir_to_zip(base, &path, zip, options)?;
} else { } else {
let content = fs::read(&path) let content = fs::read(&path).map_err(|e| format!("读取 {rel} 失败: {e}"))?;
.map_err(|e| format!("读取 {rel} 失败: {e}"))?;
zip.start_file(&rel, options) zip.start_file(&rel, options)
.map_err(|e| format!("写入 zip 失败: {e}"))?; .map_err(|e| format!("写入 zip 失败: {e}"))?;
zip.write_all(&content) zip.write_all(&content)
@@ -220,10 +221,9 @@ fn collect_dir_to_zip(
#[tauri::command] #[tauri::command]
pub fn update_agent_model(id: String, model: String) -> Result<String, String> { pub fn update_agent_model(id: String, model: String) -> Result<String, String> {
let path = super::openclaw_dir().join("openclaw.json"); let path = super::openclaw_dir().join("openclaw.json");
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?;
.map_err(|e| format!("读取配置失败: {e}"))?; let mut config: Value =
let mut config: Value = serde_json::from_str(&content) serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?;
.map_err(|e| format!("解析 JSON 失败: {e}"))?;
let agents_list = config let agents_list = config
.get_mut("agents") .get_mut("agents")
@@ -237,14 +237,13 @@ pub fn update_agent_model(id: String, model: String) -> Result<String, String> {
.ok_or(format!("Agent「{id}」不存在"))?; .ok_or(format!("Agent「{id}」不存在"))?;
let model_obj = serde_json::json!({ "primary": model }); let model_obj = serde_json::json!({ "primary": model });
agent.as_object_mut() agent
.as_object_mut()
.ok_or("Agent 格式错误")? .ok_or("Agent 格式错误")?
.insert("model".to_string(), model_obj); .insert("model".to_string(), model_obj);
let json = serde_json::to_string_pretty(&config) let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?;
.map_err(|e| format!("序列化失败: {e}"))?; fs::write(&path, json).map_err(|e| format!("写入配置失败: {e}"))?;
fs::write(&path, json)
.map_err(|e| format!("写入配置失败: {e}"))?;
Ok("已更新".into()) Ok("已更新".into())
} }

View File

@@ -1,11 +1,11 @@
use crate::utils::openclaw_command;
/// 配置读写命令 /// 配置读写命令
use serde_json::Value; use serde_json::Value;
use std::fs; use std::fs;
use std::path::PathBuf;
use std::process::Command;
use crate::utils::openclaw_command;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use crate::models::types::VersionInfo; use crate::models::types::VersionInfo;
@@ -49,10 +49,9 @@ fn backups_dir() -> PathBuf {
#[tauri::command] #[tauri::command]
pub fn read_openclaw_config() -> Result<Value, String> { pub fn read_openclaw_config() -> Result<Value, String> {
let path = super::openclaw_dir().join("openclaw.json"); let path = super::openclaw_dir().join("openclaw.json");
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?;
.map_err(|e| format!("读取配置失败: {e}"))?; let mut config: Value =
let mut config: Value = serde_json::from_str(&content) serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?;
.map_err(|e| format!("解析 JSON 失败: {e}"))?;
// 自动清理 UI 专属字段,防止污染配置导致 CLI 启动失败 // 自动清理 UI 专属字段,防止污染配置导致 CLI 启动失败
if has_ui_fields(&config) { if has_ui_fields(&config) {
@@ -60,8 +59,7 @@ pub fn read_openclaw_config() -> Result<Value, String> {
// 静默写回清理后的配置 // 静默写回清理后的配置
let bak = super::openclaw_dir().join("openclaw.json.bak"); let bak = super::openclaw_dir().join("openclaw.json.bak");
let _ = fs::copy(&path, &bak); let _ = fs::copy(&path, &bak);
let json = serde_json::to_string_pretty(&config) let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?;
.map_err(|e| format!("序列化失败: {e}"))?;
let _ = fs::write(&path, json); let _ = fs::write(&path, json);
} }
@@ -77,10 +75,8 @@ pub fn write_openclaw_config(config: Value) -> Result<(), String> {
// 清理 UI 专属字段,避免 CLI schema 校验失败 // 清理 UI 专属字段,避免 CLI schema 校验失败
let cleaned = strip_ui_fields(config); let cleaned = strip_ui_fields(config);
// 写入 // 写入
let json = serde_json::to_string_pretty(&cleaned) let json = serde_json::to_string_pretty(&cleaned).map_err(|e| format!("序列化失败: {e}"))?;
.map_err(|e| format!("序列化失败: {e}"))?; fs::write(&path, json).map_err(|e| format!("写入失败: {e}"))
fs::write(&path, json)
.map_err(|e| format!("写入失败: {e}"))
} }
/// 检测配置中是否包含 UI 专属字段 /// 检测配置中是否包含 UI 专属字段
@@ -134,8 +130,13 @@ fn strip_ui_fields(mut val: Value) -> Value {
mobj.remove("testStatus"); mobj.remove("testStatus");
mobj.remove("testError"); mobj.remove("testError");
if !mobj.contains_key("name") { if !mobj.contains_key("name") {
if let Some(id) = mobj.get("id").and_then(|v| v.as_str()) { if let Some(id) =
mobj.insert("name".into(), Value::String(id.to_string())); 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<Value, String> {
if !path.exists() { if !path.exists() {
return Ok(Value::Object(Default::default())); return Ok(Value::Object(Default::default()));
} }
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|e| format!("读取 MCP 配置失败: {e}"))?;
.map_err(|e| format!("读取 MCP 配置失败: {e}"))?; serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))
serde_json::from_str(&content)
.map_err(|e| format!("解析 JSON 失败: {e}"))
} }
#[tauri::command] #[tauri::command]
pub fn write_mcp_config(config: Value) -> Result<(), String> { pub fn write_mcp_config(config: Value) -> Result<(), String> {
let path = super::openclaw_dir().join("mcp.json"); let path = super::openclaw_dir().join("mcp.json");
let json = serde_json::to_string_pretty(&config) let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?;
.map_err(|e| format!("序列化失败: {e}"))?; fs::write(&path, json).map_err(|e| format!("写入失败: {e}"))
fs::write(&path, json)
.map_err(|e| format!("写入失败: {e}"))
} }
/// 获取本地安装的 openclaw 版本号(异步版本) /// 获取本地安装的 openclaw 版本号(异步版本)
@@ -201,7 +198,10 @@ async fn get_local_version() -> Option<String> {
// 先查汉化版,再查官方版 // 先查汉化版,再查官方版
for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] { for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] {
let pkg_json = PathBuf::from(&appdata) 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 Ok(content) = fs::read_to_string(&pkg_json) {
if let Some(ver) = serde_json::from_str::<Value>(&content) if let Some(ver) = serde_json::from_str::<Value>(&content)
.ok() .ok()
@@ -215,9 +215,16 @@ async fn get_local_version() -> Option<String> {
} }
// 所有平台通用 fallback: CLI 输出(异步) // 所有平台通用 fallback: CLI 输出(异步)
use crate::utils::openclaw_command_async; 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(); 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 秒 /// 从 npm registry 获取最新版本号,超时 5 秒
@@ -226,7 +233,9 @@ async fn get_latest_version_for(source: &str) -> Option<String> {
.timeout(std::time::Duration::from_secs(2)) .timeout(std::time::Duration::from_secs(2))
.build() .build()
.ok()?; .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 registry = get_configured_registry();
let url = format!("{registry}/{pkg}/latest"); let url = format!("{registry}/{pkg}/latest");
let resp = client.get(&url).send().await.ok()?; let resp = client.get(&url).send().await.ok()?;
@@ -314,8 +323,8 @@ fn npm_package_name(source: &str) -> &'static str {
/// 执行 npm 全局升级 openclaw流式推送日志 /// 执行 npm 全局升级 openclaw流式推送日志
#[tauri::command] #[tauri::command]
pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<String, String> { pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<String, String> {
use std::process::Stdio;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::process::Stdio;
use tauri::Emitter; use tauri::Emitter;
let current_source = detect_installed_source(); let current_source = detect_installed_source();
@@ -329,9 +338,7 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
// 先检查是否真的安装了旧包如果没有安装npm uninstall 会报错但不影响 // 先检查是否真的安装了旧包如果没有安装npm uninstall 会报错但不影响
let _ = app.emit("upgrade-log", format!("清理遗留环境 ({old_pkg})...")); let _ = app.emit("upgrade-log", format!("清理遗留环境 ({old_pkg})..."));
let _ = app.emit("upgrade-progress", 5); let _ = app.emit("upgrade-progress", 5);
let _ = npm_command() let _ = npm_command().args(["uninstall", "-g", &old_pkg]).output();
.args(["uninstall", "-g", &old_pkg])
.output();
} }
let _ = app.emit("upgrade-log", format!("$ npm install -g {pkg}")); let _ = app.emit("upgrade-log", format!("$ npm install -g {pkg}"));
@@ -341,7 +348,9 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
let configured_registry = get_configured_registry(); let configured_registry = get_configured_registry();
let registry = if pkg_name.contains("openclaw-zh") { let registry = if pkg_name.contains("openclaw-zh") {
// 汉化版:淘宝源或官方源 // 汉化版:淘宝源或官方源
if configured_registry.contains("npmmirror.com") || configured_registry.contains("taobao.org") { if configured_registry.contains("npmmirror.com")
|| configured_registry.contains("taobao.org")
{
configured_registry.as_str() configured_registry.as_str()
} else { } else {
"https://registry.npmjs.org" "https://registry.npmjs.org"
@@ -406,9 +415,7 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
{ {
let _ = openclaw_command() let _ = openclaw_command().args(["gateway", "stop"]).output();
.args(["gateway", "stop"])
.output();
} }
// 重新安装 // 重新安装
use crate::utils::openclaw_command_async; use crate::utils::openclaw_command_async;
@@ -421,7 +428,10 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
let _ = app.emit("upgrade-log", "Gateway 服务已重装"); let _ = app.emit("upgrade-log", "Gateway 服务已重装");
} }
_ => { _ => {
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<Value, String> {
let installed = dir.join("openclaw.json").exists(); let installed = dir.join("openclaw.json").exists();
let mut result = serde_json::Map::new(); let mut result = serde_json::Map::new();
result.insert("installed".into(), Value::Bool(installed)); 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)) Ok(Value::Object(result))
} }
@@ -467,9 +480,7 @@ pub fn check_node() -> Result<Value, String> {
#[tauri::command] #[tauri::command]
pub fn write_env_file(path: String, config: String) -> Result<(), String> { pub fn write_env_file(path: String, config: String) -> Result<(), String> {
let expanded = if path.starts_with("~/") { let expanded = if path.starts_with("~/") {
dirs::home_dir() dirs::home_dir().unwrap_or_default().join(&path[2..])
.unwrap_or_default()
.join(&path[2..])
} else { } else {
PathBuf::from(&path) PathBuf::from(&path)
}; };
@@ -483,8 +494,7 @@ pub fn write_env_file(path: String, config: String) -> Result<(), String> {
if let Some(parent) = expanded.parent() { if let Some(parent) = expanded.parent() {
let _ = fs::create_dir_all(parent); let _ = fs::create_dir_all(parent);
} }
fs::write(&expanded, &config) fs::write(&expanded, &config).map_err(|e| format!("写入 .env 失败: {e}"))
.map_err(|e| format!("写入 .env 失败: {e}"))
} }
// ===== 备份管理 ===== // ===== 备份管理 =====
@@ -496,22 +506,23 @@ pub fn list_backups() -> Result<Value, String> {
return Ok(Value::Array(vec![])); return Ok(Value::Array(vec![]));
} }
let mut backups: Vec<Value> = vec![]; let mut backups: Vec<Value> = vec![];
let entries = fs::read_dir(&dir) let entries = fs::read_dir(&dir).map_err(|e| format!("读取备份目录失败: {e}"))?;
.map_err(|e| format!("读取备份目录失败: {e}"))?;
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") { if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue; 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 meta = fs::metadata(&path).ok();
let size = meta.as_ref().map(|m| m.len()).unwrap_or(0); let size = meta.as_ref().map(|m| m.len()).unwrap_or(0);
// macOS 支持 created()fallback 到 modified() // macOS 支持 created()fallback 到 modified()
let created = meta let created = meta
.and_then(|m| { .and_then(|m| m.created().ok().or_else(|| m.modified().ok()))
m.created().ok().or_else(|| m.modified().ok())
})
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs()) .map(|d| d.as_secs())
.unwrap_or(0); .unwrap_or(0);
@@ -534,8 +545,7 @@ pub fn list_backups() -> Result<Value, String> {
#[tauri::command] #[tauri::command]
pub fn create_backup() -> Result<Value, String> { pub fn create_backup() -> Result<Value, String> {
let dir = backups_dir(); let dir = backups_dir();
fs::create_dir_all(&dir) fs::create_dir_all(&dir).map_err(|e| format!("创建备份目录失败: {e}"))?;
.map_err(|e| format!("创建备份目录失败: {e}"))?;
let src = super::openclaw_dir().join("openclaw.json"); let src = super::openclaw_dir().join("openclaw.json");
if !src.exists() { if !src.exists() {
@@ -545,8 +555,7 @@ pub fn create_backup() -> Result<Value, String> {
let now = chrono::Local::now(); let now = chrono::Local::now();
let name = format!("openclaw-{}.json", now.format("%Y%m%d-%H%M%S")); let name = format!("openclaw-{}.json", now.format("%Y%m%d-%H%M%S"));
let dest = dir.join(&name); let dest = dir.join(&name);
fs::copy(&src, &dest) fs::copy(&src, &dest).map_err(|e| format!("备份失败: {e}"))?;
.map_err(|e| format!("备份失败: {e}"))?;
let size = fs::metadata(&dest).map(|m| m.len()).unwrap_or(0); let size = fs::metadata(&dest).map(|m| m.len()).unwrap_or(0);
let mut obj = serde_json::Map::new(); let mut obj = serde_json::Map::new();
@@ -576,8 +585,7 @@ pub fn restore_backup(name: String) -> Result<(), String> {
let _ = create_backup(); let _ = create_backup();
} }
fs::copy(&backup_path, &target) fs::copy(&backup_path, &target).map_err(|e| format!("恢复失败: {e}"))?;
.map_err(|e| format!("恢复失败: {e}"))?;
Ok(()) Ok(())
} }
@@ -590,8 +598,7 @@ pub fn delete_backup(name: String) -> Result<(), String> {
if !path.exists() { if !path.exists() {
return Err(format!("备份文件不存在: {name}")); return Err(format!("备份文件不存在: {name}"));
} }
fs::remove_file(&path) fs::remove_file(&path).map_err(|e| format!("删除失败: {e}"))
.map_err(|e| format!("删除失败: {e}"))
} }
/// 获取当前用户 UIDmacOS/Linux 用 id -uWindows 返回 0 /// 获取当前用户 UIDmacOS/Linux 用 id -uWindows 返回 0
@@ -663,7 +670,6 @@ pub async fn restart_gateway() -> Result<String, String> {
reload_gateway().await reload_gateway().await
} }
/// 测试模型连通性:向 provider 发送一个简单的 chat completion 请求 /// 测试模型连通性:向 provider 发送一个简单的 chat completion 请求
#[tauri::command] #[tauri::command]
pub async fn test_model( pub async fn test_model(
@@ -739,10 +745,7 @@ pub async fn test_model(
/// 获取服务商的远程模型列表(调用 /models 接口) /// 获取服务商的远程模型列表(调用 /models 接口)
#[tauri::command] #[tauri::command]
pub async fn list_remote_models( pub async fn list_remote_models(base_url: String, api_key: String) -> Result<Vec<String>, String> {
base_url: String,
api_key: String,
) -> Result<Vec<String>, String> {
let url = format!("{}/models", base_url.trim_end_matches('/')); let url = format!("{}/models", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
@@ -811,11 +814,10 @@ pub async fn install_gateway() -> Result<String, String> {
match cli_check { match cli_check {
Ok(o) if o.status.success() => {} Ok(o) if o.status.success() => {}
_ => { _ => {
return Err( return Err("openclaw CLI 未安装。请先执行以下命令安装:\n\n\
"openclaw CLI 未安装。请先执行以下命令安装:\n\n\
npm install -g @qingchencloud/openclaw-zh\n\n\ npm install -g @qingchencloud/openclaw-zh\n\n\
安装完成后再点击此按钮安装 Gateway 服务。".into() 安装完成后再点击此按钮安装 Gateway 服务。"
); .into());
} }
} }
@@ -852,16 +854,13 @@ pub fn uninstall_gateway() -> Result<String, String> {
let home = dirs::home_dir().unwrap_or_default(); let home = dirs::home_dir().unwrap_or_default();
let plist = home.join("Library/LaunchAgents/ai.openclaw.gateway.plist"); let plist = home.join("Library/LaunchAgents/ai.openclaw.gateway.plist");
if plist.exists() { if plist.exists() {
fs::remove_file(&plist) fs::remove_file(&plist).map_err(|e| format!("删除 plist 失败: {e}"))?;
.map_err(|e| format!("删除 plist 失败: {e}"))?;
} }
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
{ {
// Windows/Linux: 停止 Gateway 服务 // Windows/Linux: 停止 Gateway 服务
let _ = openclaw_command() let _ = openclaw_command().args(["gateway", "stop"]).output();
.args(["gateway", "stop"])
.output();
} }
Ok("Gateway 服务已卸载".to_string()) Ok("Gateway 服务已卸载".to_string())
@@ -875,6 +874,5 @@ pub fn get_npm_registry() -> Result<String, String> {
#[tauri::command] #[tauri::command]
pub fn set_npm_registry(registry: String) -> Result<(), String> { pub fn set_npm_registry(registry: String) -> Result<(), String> {
let path = super::openclaw_dir().join("npm-registry.txt"); let path = super::openclaw_dir().join("npm-registry.txt");
fs::write(&path, registry.trim()) fs::write(&path, registry.trim()).map_err(|e| format!("保存失败: {e}"))
.map_err(|e| format!("保存失败: {e}"))
} }

View File

@@ -1,13 +1,16 @@
/// 设备密钥管理 + Gateway connect 握手签名 /// 设备密钥管理 + Gateway connect 握手签名
use ed25519_dalek::{SigningKey, Signer, VerifyingKey}; use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use sha2::{Sha256, Digest};
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256};
use std::fs; use std::fs;
const DEVICE_KEY_FILE: &str = "clawpanel-device-key.json"; const DEVICE_KEY_FILE: &str = "clawpanel-device-key.json";
const SCOPES: &[&str] = &[ const SCOPES: &[&str] = &[
"operator.admin", "operator.approvals", "operator.pairing", "operator.admin",
"operator.read", "operator.write", "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); let path = dir.join(DEVICE_KEY_FILE);
if path.exists() { if path.exists() {
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|e| format!("读取设备密钥失败: {e}"))?;
.map_err(|e| format!("读取设备密钥失败: {e}"))?; let json: Value =
let json: Value = serde_json::from_str(&content) serde_json::from_str(&content).map_err(|e| format!("解析设备密钥失败: {e}"))?;
.map_err(|e| format!("解析设备密钥失败: {e}"))?;
let device_id = json["deviceId"].as_str().unwrap_or("").to_string(); let device_id = json["deviceId"].as_str().unwrap_or("").to_string();
let pub_b64 = json["publicKey"].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_hex = json["secretKey"].as_str().unwrap_or("");
let secret_bytes = hex::decode(secret_hex) let secret_bytes = hex::decode(secret_hex).map_err(|e| format!("解码密钥失败: {e}"))?;
.map_err(|e| format!("解码密钥失败: {e}"))?;
if secret_bytes.len() != 32 { if secret_bytes.len() != 32 {
return Err("密钥长度错误".into()); return Err("密钥长度错误".into());
} }
@@ -76,9 +77,12 @@ mod hex {
data.as_ref().iter().map(|b| format!("{b:02x}")).collect() data.as_ref().iter().map(|b| format!("{b:02x}")).collect()
} }
pub fn decode(s: &str) -> Result<Vec<u8>, String> { pub fn decode(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 { return Err("奇数长度".into()) } if s.len() % 2 != 0 {
(0..s.len()).step_by(2) return Err("奇数长度".into());
.map(|i| u8::from_str_radix(&s[i..i+2], 16).map_err(|e| e.to_string())) }
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
.collect() .collect()
} }
} }

View File

@@ -1,8 +1,8 @@
/// 扩展工具命令cftunnel + ClawApp /// 扩展工具命令cftunnel + ClawApp
use serde_json::Value; use serde_json::Value;
use std::process::Command;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
use std::process::Command;
/// 按第一个冒号(半角 ':' 或全角 ''分割返回冒号之后的内容trim 后) /// 按第一个冒号(半角 ':' 或全角 ''分割返回冒号之后的内容trim 后)
fn split_after_colon(s: &str) -> &str { fn split_after_colon(s: &str) -> &str {
@@ -30,7 +30,9 @@ fn parse_cftunnel_status(output: &str) -> serde_json::Map<String, Value> {
let running = rest.contains("运行中"); let running = rest.contains("运行中");
map.insert("running".into(), Value::Bool(running)); map.insert("running".into(), Value::Bool(running));
// 匹配英文和全角 'PID:' / 'PID' // 匹配英文和全角 '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)); .or_else(|| rest.split("PID").nth(1));
if let Some(pid_str) = pid_rest { if let Some(pid_str) = pid_rest {
let pid = pid_str.trim().trim_end_matches(')').trim(); let pid = pid_str.trim().trim_end_matches(')').trim();
@@ -73,7 +75,10 @@ fn cftunnel_bin() -> String {
let candidates = [ let candidates = [
home.join("bin").join("cftunnel.exe"), home.join("bin").join("cftunnel.exe"),
home.join(".cftunnel").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 { for path in &candidates {
if path.exists() { if path.exists() {
@@ -98,10 +103,7 @@ fn check_cftunnel_process() -> Option<(Option<u64>, bool)> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// macOS: 通过 launchctl 检测 // macOS: 通过 launchctl 检测
let output = Command::new("launchctl") let output = Command::new("launchctl").args(["list"]).output().ok()?;
.args(["list"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout); let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() { for line in text.lines() {
if line.contains("com.cftunnel") { if line.contains("com.cftunnel") {
@@ -126,7 +128,9 @@ fn check_cftunnel_process() -> Option<(Option<u64>, bool)> {
let text = String::from_utf8_lossy(&output.stdout); let text = String::from_utf8_lossy(&output.stdout);
if text.contains("cftunnel.exe") { if text.contains("cftunnel.exe") {
// 尝试提取 PIDCSV 格式: "cftunnel.exe","1234",... // 尝试提取 PIDCSV 格式: "cftunnel.exe","1234",...
let pid = text.lines().next() let pid = text
.lines()
.next()
.and_then(|line| line.split(',').nth(1)) .and_then(|line| line.split(',').nth(1))
.and_then(|s| s.trim_matches('"').parse::<u64>().ok()); .and_then(|s| s.trim_matches('"').parse::<u64>().ok());
return Some((pid, true)); return Some((pid, true));
@@ -143,7 +147,9 @@ fn check_cftunnel_process() -> Option<(Option<u64>, bool)> {
.ok()?; .ok()?;
if output.status.success() { if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout); let text = String::from_utf8_lossy(&output.stdout);
let pid = text.lines().next() let pid = text
.lines()
.next()
.and_then(|s| s.trim().parse::<u64>().ok()); .and_then(|s| s.trim().parse::<u64>().ok());
return Some((pid, true)); return Some((pid, true));
} }
@@ -182,7 +188,10 @@ pub fn get_cftunnel_status() -> Result<Value, String> {
} }
// 仅当 status 报未运行时才做进程检测补充 // 仅当 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 !reported_running {
if let Some((pid, running)) = check_cftunnel_process() { if let Some((pid, running)) = check_cftunnel_process() {
if running { if running {
@@ -251,7 +260,8 @@ pub fn get_clawapp_status() -> Result<Value, String> {
let running = std::net::TcpStream::connect_timeout( let running = std::net::TcpStream::connect_timeout(
&"127.0.0.1:3210".parse().unwrap(), &"127.0.0.1:3210".parse().unwrap(),
std::time::Duration::from_millis(150), std::time::Duration::from_millis(150),
).is_ok(); )
.is_ok();
result.insert("running".into(), Value::Bool(running)); result.insert("running".into(), Value::Bool(running));
@@ -292,10 +302,7 @@ fn check_clawapp_installed() -> bool {
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
{ {
if let Ok(out) = Command::new("npm") if let Ok(out) = Command::new("npm").args(["list", "-g", "clawapp"]).output() {
.args(["list", "-g", "clawapp"])
.output()
{
return out.status.success(); return out.status.success();
} }
false false
@@ -307,8 +314,8 @@ fn check_clawapp_installed() -> bool {
/// Windows: PowerShell 下载安装 /// Windows: PowerShell 下载安装
#[tauri::command] #[tauri::command]
pub async fn install_cftunnel(app: tauri::AppHandle) -> Result<String, String> { pub async fn install_cftunnel(app: tauri::AppHandle) -> Result<String, String> {
use std::process::Stdio;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::process::Stdio;
use tauri::Emitter; use tauri::Emitter;
let _ = app.emit("install-log", "开始安装 cftunnel..."); let _ = app.emit("install-log", "开始安装 cftunnel...");
@@ -354,10 +361,21 @@ Write-Output '安装完成'
"#; "#;
// 使用完整路径调用 PowerShell避免 MSYS2/Git Bash 环境下找不到 // 使用完整路径调用 PowerShell避免 MSYS2/Git Bash 环境下找不到
let ps_path = std::env::var("SystemRoot") 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()); .unwrap_or_else(|_| "powershell.exe".to_string());
Command::new(&ps_path) Command::new(&ps_path)
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", install_script]) .args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
install_script,
])
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
@@ -405,8 +423,8 @@ Write-Output '安装完成'
/// 一键安装 ClawApp通过 npm /// 一键安装 ClawApp通过 npm
#[tauri::command] #[tauri::command]
pub async fn install_clawapp(app: tauri::AppHandle) -> Result<String, String> { pub async fn install_clawapp(app: tauri::AppHandle) -> Result<String, String> {
use std::process::Stdio;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::process::Stdio;
use tauri::Emitter; use tauri::Emitter;
let _ = app.emit("install-log", "开始安装 ClawApp..."); let _ = app.emit("install-log", "开始安装 ClawApp...");

View File

@@ -31,8 +31,7 @@ pub fn read_log_tail(log_name: String, lines: Option<u32>) -> Result<String, Str
return Ok(String::new()); return Ok(String::new());
} }
let mut file = fs::File::open(&path) let mut file = fs::File::open(&path).map_err(|e| format!("打开日志失败: {e}"))?;
.map_err(|e| format!("打开日志失败: {e}"))?;
let file_len = file let file_len = file
.metadata() .metadata()
@@ -83,8 +82,7 @@ pub fn search_log(
return Ok(vec![]); return Ok(vec![]);
} }
let mut file = fs::File::open(&path) let mut file = fs::File::open(&path).map_err(|e| format!("打开日志失败: {e}"))?;
.map_err(|e| format!("打开日志失败: {e}"))?;
let file_len = file let file_len = file
.metadata() .metadata()

View File

@@ -1,8 +1,8 @@
use crate::utils::openclaw_command_async;
/// 记忆文件管理命令 /// 记忆文件管理命令
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use crate::utils::openclaw_command_async;
/// 检查路径是否包含不安全字符(目录遍历、绝对路径等) /// 检查路径是否包含不安全字符(目录遍历、绝对路径等)
fn is_unsafe_path(path: &str) -> bool { fn is_unsafe_path(path: &str) -> bool {
@@ -27,8 +27,8 @@ async fn agent_workspace(agent_id: &str) -> Result<PathBuf, String> {
} }
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
let agents: serde_json::Value = serde_json::from_str(&stdout) let agents: serde_json::Value =
.map_err(|e| format!("解析 JSON 失败: {e}"))?; serde_json::from_str(&stdout).map_err(|e| format!("解析 JSON 失败: {e}"))?;
if let Some(arr) = agents.as_array() { if let Some(arr) = agents.as_array() {
for a in arr { for a in arr {
@@ -63,7 +63,10 @@ async fn memory_dir_for_agent(agent_id: &str, category: &str) -> Result<PathBuf,
} }
#[tauri::command] #[tauri::command]
pub async fn list_memory_files(category: String, agent_id: Option<String>) -> Result<Vec<String>, String> { pub async fn list_memory_files(
category: String,
agent_id: Option<String>,
) -> Result<Vec<String>, String> {
let aid = agent_id.as_deref().unwrap_or("main"); let aid = agent_id.as_deref().unwrap_or("main");
let dir = memory_dir_for_agent(aid, &category).await?; let dir = memory_dir_for_agent(aid, &category).await?;
if !dir.exists() { if !dir.exists() {
@@ -82,8 +85,7 @@ fn collect_files(
files: &mut Vec<String>, files: &mut Vec<String>,
category: &str, category: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let entries = fs::read_dir(dir) let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {e}"))?;
.map_err(|e| format!("读取目录失败: {e}"))?;
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
@@ -95,7 +97,8 @@ fn collect_files(
} else { } else {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if matches!(ext, "md" | "txt" | "json" | "jsonl") { 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()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| path.to_string_lossy().to_string()); .unwrap_or_else(|_| path.to_string_lossy().to_string());
files.push(rel); files.push(rel);
@@ -122,8 +125,7 @@ pub async fn read_memory_file(path: String, agent_id: Option<String>) -> Result<
if let Ok(dir) = c { if let Ok(dir) = c {
let full = dir.join(&path); let full = dir.join(&path);
if full.exists() { if full.exists() {
return fs::read_to_string(&full) return fs::read_to_string(&full).map_err(|e| format!("读取失败: {e}"));
.map_err(|e| format!("读取失败: {e}"));
} }
} }
} }
@@ -132,7 +134,12 @@ pub async fn read_memory_file(path: String, agent_id: Option<String>) -> Result<
} }
#[tauri::command] #[tauri::command]
pub async fn write_memory_file(path: String, content: String, category: Option<String>, agent_id: Option<String>) -> Result<(), String> { pub async fn write_memory_file(
path: String,
content: String,
category: Option<String>,
agent_id: Option<String>,
) -> Result<(), String> {
if is_unsafe_path(&path) { if is_unsafe_path(&path) {
return Err("非法路径".to_string()); return Err("非法路径".to_string());
} }
@@ -165,8 +172,7 @@ pub async fn delete_memory_file(path: String, agent_id: Option<String>) -> Resul
if let Ok(dir) = c { if let Ok(dir) = c {
let full = dir.join(&path); let full = dir.join(&path);
if full.exists() { if full.exists() {
return fs::remove_file(&full) return fs::remove_file(&full).map_err(|e| format!("删除失败: {e}"));
.map_err(|e| format!("删除失败: {e}"));
} }
} }
} }
@@ -175,7 +181,10 @@ pub async fn delete_memory_file(path: String, agent_id: Option<String>) -> Resul
} }
#[tauri::command] #[tauri::command]
pub async fn export_memory_zip(category: String, agent_id: Option<String>) -> Result<String, String> { pub async fn export_memory_zip(
category: String,
agent_id: Option<String>,
) -> Result<String, String> {
let aid = agent_id.as_deref().unwrap_or("main"); let aid = agent_id.as_deref().unwrap_or("main");
let dir = memory_dir_for_agent(aid, &category).await?; let dir = memory_dir_for_agent(aid, &category).await?;
if !dir.exists() { if !dir.exists() {
@@ -196,16 +205,15 @@ pub async fn export_memory_zip(category: String, agent_id: Option<String>) -> Re
); );
let zip_path = tmp_dir.join(&zip_name); let zip_path = tmp_dir.join(&zip_name);
let file = fs::File::create(&zip_path) let file = fs::File::create(&zip_path).map_err(|e| format!("创建 zip 失败: {e}"))?;
.map_err(|e| format!("创建 zip 失败: {e}"))?;
let mut zip = zip::ZipWriter::new(file); let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default() let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated); .compression_method(zip::CompressionMethod::Deflated);
for rel_path in &files { for rel_path in &files {
let full_path = dir.join(rel_path); let full_path = dir.join(rel_path);
let content = fs::read_to_string(&full_path) let content =
.map_err(|e| format!("读取 {rel_path} 失败: {e}"))?; fs::read_to_string(&full_path).map_err(|e| format!("读取 {rel_path} 失败: {e}"))?;
zip.start_file(rel_path, options) zip.start_file(rel_path, options)
.map_err(|e| format!("写入 zip 失败: {e}"))?; .map_err(|e| format!("写入 zip 失败: {e}"))?;
zip.write_all(content.as_bytes()) zip.write_all(content.as_bytes())

View File

@@ -11,7 +11,5 @@ pub mod service;
/// 获取 OpenClaw 配置目录 (~/.openclaw/) /// 获取 OpenClaw 配置目录 (~/.openclaw/)
pub fn openclaw_dir() -> PathBuf { pub fn openclaw_dir() -> PathBuf {
dirs::home_dir() dirs::home_dir().unwrap_or_default().join(".openclaw")
.unwrap_or_default()
.join(".openclaw")
} }

View File

@@ -9,11 +9,11 @@ pub fn auto_pair_device() -> Result<String, String> {
return Err("设备密钥文件不存在".into()); return Err("设备密钥文件不存在".into());
} }
let device_key_content = std::fs::read_to_string(&device_key_path) let device_key_content =
.map_err(|e| format!("读取设备密钥失败: {e}"))?; 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) let device_key: serde_json::Value =
.map_err(|e| format!("解析设备密钥失败: {e}"))?; serde_json::from_str(&device_key_content).map_err(|e| format!("解析设备密钥失败: {e}"))?;
let device_id = device_key["deviceId"] let device_id = device_key["deviceId"]
.as_str() .as_str()
@@ -26,20 +26,20 @@ pub fn auto_pair_device() -> Result<String, String> {
.to_string(); .to_string();
// 读取或创建 paired.json // 读取或创建 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"); let devices_dir = crate::commands::openclaw_dir().join("devices");
// 确保 devices 目录存在 // 确保 devices 目录存在
if !devices_dir.exists() { if !devices_dir.exists() {
std::fs::create_dir_all(&devices_dir) std::fs::create_dir_all(&devices_dir).map_err(|e| format!("创建 devices 目录失败: {e}"))?;
.map_err(|e| format!("创建 devices 目录失败: {e}"))?;
} }
let mut paired: serde_json::Value = if paired_path.exists() { let mut paired: serde_json::Value = if paired_path.exists() {
let content = std::fs::read_to_string(&paired_path) let content = std::fs::read_to_string(&paired_path)
.map_err(|e| format!("读取 paired.json 失败: {e}"))?; .map_err(|e| format!("读取 paired.json 失败: {e}"))?;
serde_json::from_str(&content) serde_json::from_str(&content).map_err(|e| format!("解析 paired.json 失败: {e}"))?
.map_err(|e| format!("解析 paired.json 失败: {e}"))?
} else { } else {
serde_json::json!({}) serde_json::json!({})
}; };
@@ -86,8 +86,7 @@ pub fn auto_pair_device() -> Result<String, String> {
let new_content = serde_json::to_string_pretty(&paired) let new_content = serde_json::to_string_pretty(&paired)
.map_err(|e| format!("序列化 paired.json 失败: {e}"))?; .map_err(|e| format!("序列化 paired.json 失败: {e}"))?;
std::fs::write(&paired_path, new_content) std::fs::write(&paired_path, new_content).map_err(|e| format!("写入 paired.json 失败: {e}"))?;
.map_err(|e| format!("写入 paired.json 失败: {e}"))?;
Ok("设备配对成功".into()) Ok("设备配对成功".into())
} }
@@ -100,27 +99,27 @@ pub fn check_pairing_status() -> Result<bool, String> {
return Ok(false); return Ok(false);
} }
let device_key_content = std::fs::read_to_string(&device_key_path) let device_key_content =
.map_err(|e| format!("读取设备密钥失败: {e}"))?; 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) let device_key: serde_json::Value =
.map_err(|e| format!("解析设备密钥失败: {e}"))?; serde_json::from_str(&device_key_content).map_err(|e| format!("解析设备密钥失败: {e}"))?;
let device_id = device_key["deviceId"] let device_id = device_key["deviceId"].as_str().ok_or("设备 ID 不存在")?;
.as_str()
.ok_or("设备 ID 不存在")?;
// 检查 paired.json // 检查 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() { if !paired_path.exists() {
return Ok(false); return Ok(false);
} }
let content = std::fs::read_to_string(&paired_path) let content =
.map_err(|e| format!("读取 paired.json 失败: {e}"))?; std::fs::read_to_string(&paired_path).map_err(|e| format!("读取 paired.json 失败: {e}"))?;
let paired: serde_json::Value = serde_json::from_str(&content) let paired: serde_json::Value =
.map_err(|e| format!("解析 paired.json 失败: {e}"))?; serde_json::from_str(&content).map_err(|e| format!("解析 paired.json 失败: {e}"))?;
Ok(paired.get(device_id).is_some()) Ok(paired.get(device_id).is_some())
} }

View File

@@ -33,7 +33,9 @@ mod platform {
.output() .output()
.map_err(|e| format!("获取 UID 失败: {e}"))?; .map_err(|e| format!("获取 UID 失败: {e}"))?;
let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
uid_str.parse::<u32>().map_err(|e| format!("解析 UID 失败: {e}")) uid_str
.parse::<u32>()
.map_err(|e| format!("解析 UID 失败: {e}"))
} }
/// 动态扫描 LaunchAgents 目录,只返回 OpenClaw 核心服务 /// 动态扫描 LaunchAgents 目录,只返回 OpenClaw 核心服务
@@ -60,19 +62,13 @@ mod platform {
fn plist_path(label: &str) -> String { fn plist_path(label: &str) -> String {
let home = dirs::home_dir().unwrap_or_default(); let home = dirs::home_dir().unwrap_or_default();
format!( format!("{}/Library/LaunchAgents/{}.plist", home.display(), label)
"{}/Library/LaunchAgents/{}.plist",
home.display(),
label
)
} }
/// 用 launchctl print 检测单个服务状态,返回 (running, pid) /// 用 launchctl print 检测单个服务状态,返回 (running, pid)
pub fn check_service_status(uid: u32, label: &str) -> (bool, Option<u32>) { pub fn check_service_status(uid: u32, label: &str) -> (bool, Option<u32>) {
let target = format!("gui/{}/{}", uid, label); let target = format!("gui/{}/{}", uid, label);
let output = Command::new("launchctl") let output = Command::new("launchctl").args(["print", &target]).output();
.args(["print", &target])
.output();
let Ok(out) = output else { let Ok(out) = output else {
return (false, None); return (false, None);
@@ -221,8 +217,12 @@ mod platform {
/// 检测 openclaw CLI 是否已安装(文件系统检测,避免 spawn 进程) /// 检测 openclaw CLI 是否已安装(文件系统检测,避免 spawn 进程)
pub fn is_cli_installed() -> bool { pub fn is_cli_installed() -> bool {
if let Ok(appdata) = std::env::var("APPDATA") { if let Ok(appdata) = std::env::var("APPDATA") {
let cmd_path = std::path::Path::new(&appdata).join("npm").join("openclaw.cmd"); let cmd_path = std::path::Path::new(&appdata)
if cmd_path.exists() { return true; } .join("npm")
.join("openclaw.cmd");
if cmd_path.exists() {
return true;
}
} }
false false
} }
@@ -237,7 +237,11 @@ mod platform {
let config_path = crate::commands::openclaw_dir().join("openclaw.json"); let config_path = crate::commands::openclaw_dir().join("openclaw.json");
if let Ok(content) = std::fs::read_to_string(&config_path) { if let Ok(content) = std::fs::read_to_string(&config_path) {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) { if let Ok(val) = serde_json::from_str::<serde_json::Value>(&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 { if port > 0 && port < 65536 {
return port as u16; return port as u16;
} }
@@ -252,7 +256,9 @@ mod platform {
let port = read_gateway_port(); let port = read_gateway_port();
let addr = format!("127.0.0.1:{port}"); let addr = format!("127.0.0.1:{port}");
match std::net::TcpStream::connect_timeout( 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), std::time::Duration::from_millis(150),
) { ) {
Ok(_) => (true, None), Ok(_) => (true, None),
@@ -263,7 +269,10 @@ mod platform {
/// 以前台模式 spawn Gateway不需要管理员权限 /// 以前台模式 spawn Gateway不需要管理员权限
pub async fn start_service_impl(_label: &str) -> Result<(), String> { pub async fn start_service_impl(_label: &str) -> Result<(), String> {
if !is_cli_installed() { 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 { if check_service_status(0, "").0 {
return Ok(()); return Ok(());
@@ -312,7 +321,15 @@ mod platform {
if check_service_status(0, "").0 { if check_service_status(0, "").0 {
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
let _ = TokioCommand::new("cmd") 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) .creation_flags(CREATE_NO_WINDOW)
.output() .output()
.await; .await;
@@ -323,7 +340,9 @@ mod platform {
pub async fn restart_service_impl(_label: &str) -> Result<(), String> { pub async fn restart_service_impl(_label: &str) -> Result<(), String> {
let _ = stop_service_impl(_label).await; let _ = stop_service_impl(_label).await;
for _ in 0..10 { 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; tokio::time::sleep(std::time::Duration::from_millis(300)).await;
} }
start_service_impl(_label).await start_service_impl(_label).await
@@ -342,7 +361,9 @@ mod platform {
.output() .output()
.map_err(|e| format!("获取 UID 失败: {e}"))?; .map_err(|e| format!("获取 UID 失败: {e}"))?;
let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); let uid_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
uid_str.parse::<u32>().map_err(|e| format!("解析 UID 失败: {e}")) uid_str
.parse::<u32>()
.map_err(|e| format!("解析 UID 失败: {e}"))
} }
pub async fn is_cli_installed() -> bool { pub async fn is_cli_installed() -> bool {
@@ -378,7 +399,10 @@ mod platform {
async fn gateway_command(action: &str) -> Result<(), String> { async fn gateway_command(action: &str) -> Result<(), String> {
if !is_cli_installed().await { 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() let output = crate::utils::openclaw_command_async()
.args(["gateway", action]) .args(["gateway", action])
@@ -431,10 +455,7 @@ pub async fn get_services_status() -> Result<Vec<ServiceStatus>, String> {
label: label.clone(), label: label.clone(),
pid, pid,
running, running,
description: desc_map description: desc_map.get(label.as_str()).unwrap_or(&"").to_string(),
.get(label.as_str())
.unwrap_or(&"")
.to_string(),
cli_installed, cli_installed,
}); });
} }

View File

@@ -1,10 +1,10 @@
/// 系统托盘模块 /// 系统托盘模块
/// Windows / macOS / Linux 通用Tauri v2 内置跨平台支持 /// Windows / macOS / Linux 通用Tauri v2 内置跨平台支持
use tauri::{ use tauri::{
AppHandle, Manager, image::Image,
menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem}, menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem},
tray::TrayIconBuilder, tray::TrayIconBuilder,
image::Image, AppHandle, Manager,
}; };
pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> { pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
@@ -74,4 +74,3 @@ fn handle_menu_event(app: &AppHandle, id: &str) {
_ => {} _ => {}
} }
} }

View File

@@ -1,6 +1,6 @@
use std::process::Command;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
use std::process::Command;
/// 跨平台获取 openclaw 命令的方法(同步版本) /// 跨平台获取 openclaw 命令的方法(同步版本)
/// 在 Windows 上使用 `cmd /c openclaw` 以兼容全局 npm 路径下的 `.cmd` 脚本 /// 在 Windows 上使用 `cmd /c openclaw` 以兼容全局 npm 路径下的 `.cmd` 脚本