mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-11 18:10:41 +08:00
style: cargo fmt 格式化 Rust 代码,修复 CI 格式检查失败
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
|
||||
/// 获取当前用户 UID(macOS/Linux 用 id -u,Windows 返回 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}"))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
// 尝试提取 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::<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...");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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` 脚本
|
||||
|
||||
Reference in New Issue
Block a user