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 实现增删改查
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<Value, String> {
}
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<String>) -> Result<Value, String> {
pub async fn add_agent(
name: String,
model: String,
workspace: Option<String>,
) -> Result<Value, String> {
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<String>,
) -> Result<String, String> {
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<String, String> {
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<fs::File>,
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<String, String> {
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<String, String> {
.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())
}

View File

@@ -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<Value, String> {
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<Value, String> {
// 静默写回清理后的配置
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<Value, String> {
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<String> {
// 先查汉化版,再查官方版
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::<Value>(&content)
.ok()
@@ -215,9 +215,16 @@ async fn get_local_version() -> Option<String> {
}
// 所有平台通用 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<String> {
.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<String, String> {
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<S
// 先检查是否真的安装了旧包如果没有安装npm uninstall 会报错但不影响
let _ = app.emit("upgrade-log", format!("清理遗留环境 ({old_pkg})..."));
let _ = app.emit("upgrade-progress", 5);
let _ = npm_command()
.args(["uninstall", "-g", &old_pkg])
.output();
let _ = npm_command().args(["uninstall", "-g", &old_pkg]).output();
}
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 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()
} else {
"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"))]
{
let _ = openclaw_command()
.args(["gateway", "stop"])
.output();
let _ = openclaw_command().args(["gateway", "stop"]).output();
}
// 重新安装
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 重装失败,请手动执行 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 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<Value, String> {
#[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<Value, String> {
return Ok(Value::Array(vec![]));
}
let mut backups: Vec<Value> = 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<Value, String> {
#[tauri::command]
pub fn create_backup() -> Result<Value, String> {
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<Value, String> {
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}"))
}
/// 获取当前用户 UIDmacOS/Linux 用 id -uWindows 返回 0
@@ -663,7 +670,6 @@ pub async fn restart_gateway() -> Result<String, String> {
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<Vec<String>, String> {
pub async fn list_remote_models(base_url: String, api_key: String) -> Result<Vec<String>, 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<String, String> {
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<String, String> {
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<String, String> {
#[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}"))
}

View File

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

View File

@@ -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<String, Value> {
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<u64>, 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<u64>, bool)> {
let text = String::from_utf8_lossy(&output.stdout);
if text.contains("cftunnel.exe") {
// 尝试提取 PIDCSV 格式: "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::<u64>().ok());
return Some((pid, true));
@@ -143,7 +147,9 @@ fn check_cftunnel_process() -> Option<(Option<u64>, 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::<u64>().ok());
return Some((pid, true));
}
@@ -182,7 +188,10 @@ pub fn get_cftunnel_status() -> Result<Value, String> {
}
// 仅当 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<Value, String> {
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<String, String> {
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<String, String> {
use std::process::Stdio;
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use tauri::Emitter;
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());
}
let mut file = fs::File::open(&path)
.map_err(|e| format!("打开日志失败: {e}"))?;
let mut file = fs::File::open(&path).map_err(|e| format!("打开日志失败: {e}"))?;
let file_len = file
.metadata()
@@ -83,8 +82,7 @@ pub fn search_log(
return Ok(vec![]);
}
let mut file = fs::File::open(&path)
.map_err(|e| format!("打开日志失败: {e}"))?;
let mut file = fs::File::open(&path).map_err(|e| format!("打开日志失败: {e}"))?;
let file_len = file
.metadata()

View File

@@ -1,8 +1,8 @@
use crate::utils::openclaw_command_async;
/// 记忆文件管理命令
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use crate::utils::openclaw_command_async;
/// 检查路径是否包含不安全字符(目录遍历、绝对路径等)
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 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<PathBuf,
}
#[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 dir = memory_dir_for_agent(aid, &category).await?;
if !dir.exists() {
@@ -82,8 +85,7 @@ fn collect_files(
files: &mut Vec<String>,
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<String>) -> 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<String>) -> Result<
}
#[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) {
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 {
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<String>) -> Resul
}
#[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 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<String>) -> 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())

View File

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

View File

@@ -9,11 +9,11 @@ pub fn auto_pair_device() -> Result<String, String> {
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<String, String> {
.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<String, String> {
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<bool, String> {
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())
}

View File

@@ -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::<u32>().map_err(|e| format!("解析 UID 失败: {e}"))
uid_str
.parse::<u32>()
.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<u32>) {
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::<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 {
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::<u32>().map_err(|e| format!("解析 UID 失败: {e}"))
uid_str
.parse::<u32>()
.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<Vec<ServiceStatus>, 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,
});
}

View File

@@ -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<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")]
use std::os::windows::process::CommandExt;
use std::process::Command;
/// 跨平台获取 openclaw 命令的方法(同步版本)
/// 在 Windows 上使用 `cmd /c openclaw` 以兼容全局 npm 路径下的 `.cmd` 脚本