mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-27 19:30:15 +08:00
feat: ClawPanel v0.1.0 项目骨架
- Tauri v2 + Vanilla JS + Vite 技术栈 - 9 个页面: 仪表盘/服务管理/日志/模型配置/Agent配置/Gateway/MCP工具/记忆文件/部署 - Rust 后端: 配置读写/服务管理(launchd)/日志读取/记忆文件管理 - 暗色主题 + 玻璃拟态 UI - Mock 数据支持纯浏览器开发调试
This commit is contained in:
72
src-tauri/src/commands/config.rs
Normal file
72
src-tauri/src/commands/config.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
/// 配置读写命令
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::models::types::VersionInfo;
|
||||
|
||||
fn openclaw_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".openclaw")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_openclaw_config() -> Result<Value, String> {
|
||||
let path = openclaw_dir().join("openclaw.json");
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("读取配置失败: {e}"))?;
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| format!("解析 JSON 失败: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_openclaw_config(config: Value) -> Result<(), String> {
|
||||
let path = openclaw_dir().join("openclaw.json");
|
||||
// 备份
|
||||
let bak = openclaw_dir().join("openclaw.json.bak");
|
||||
let _ = fs::copy(&path, &bak);
|
||||
// 写入
|
||||
let json = serde_json::to_string_pretty(&config)
|
||||
.map_err(|e| format!("序列化失败: {e}"))?;
|
||||
fs::write(&path, json)
|
||||
.map_err(|e| format!("写入失败: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_mcp_config() -> Result<Value, String> {
|
||||
let path = openclaw_dir().join("mcp.json");
|
||||
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}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_mcp_config(config: Value) -> Result<(), String> {
|
||||
let path = 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}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_version_info() -> Result<VersionInfo, String> {
|
||||
// 从 openclaw.json 的 meta.lastTouchedVersion 读取
|
||||
let config = read_openclaw_config()?;
|
||||
let current = config
|
||||
.get("meta")
|
||||
.and_then(|m| m.get("lastTouchedVersion"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
|
||||
Ok(VersionInfo {
|
||||
current,
|
||||
latest: None,
|
||||
update_available: false,
|
||||
})
|
||||
}
|
||||
69
src-tauri/src/commands/logs.rs
Normal file
69
src-tauri/src/commands/logs.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
/// 日志读取命令
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn log_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".openclaw")
|
||||
.join("logs")
|
||||
}
|
||||
|
||||
fn log_path(log_name: &str) -> PathBuf {
|
||||
let filename = match log_name {
|
||||
"gateway" => "gateway.log",
|
||||
"gateway-err" => "gateway.err.log",
|
||||
"guardian" => "guardian.log",
|
||||
"guardian-backup" => "guardian-backup.log",
|
||||
"config-audit" => "config-audit.jsonl",
|
||||
_ => "gateway.log",
|
||||
};
|
||||
log_dir().join(filename)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_log_tail(log_name: String, lines: usize) -> Result<String, String> {
|
||||
let path = log_path(&log_name);
|
||||
if !path.exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("读取日志失败: {e}"))?;
|
||||
|
||||
let all_lines: Vec<&str> = content.lines().collect();
|
||||
let start = if all_lines.len() > lines {
|
||||
all_lines.len() - lines
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(all_lines[start..].join("\n"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn search_log(
|
||||
log_name: String,
|
||||
query: String,
|
||||
max_results: usize,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let path = log_path(&log_name);
|
||||
if !path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let file = fs::File::open(&path)
|
||||
.map_err(|e| format!("打开日志失败: {e}"))?;
|
||||
let reader = BufReader::new(file);
|
||||
let query_lower = query.to_lowercase();
|
||||
|
||||
let results: Vec<String> = reader
|
||||
.lines()
|
||||
.filter_map(|l| l.ok())
|
||||
.filter(|l| l.to_lowercase().contains(&query_lower))
|
||||
.take(max_results)
|
||||
.collect();
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
112
src-tauri/src/commands/memory.rs
Normal file
112
src-tauri/src/commands/memory.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
/// 记忆文件管理命令
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn openclaw_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".openclaw")
|
||||
}
|
||||
|
||||
fn memory_dir(category: &str) -> PathBuf {
|
||||
match category {
|
||||
"memory" => openclaw_dir().join("workspace").join("memory"),
|
||||
"archive" => openclaw_dir().join("workspace-memory"),
|
||||
"core" => openclaw_dir().join("workspace"),
|
||||
_ => openclaw_dir().join("workspace").join("memory"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_memory_files(category: String) -> Result<Vec<String>, String> {
|
||||
let dir = memory_dir(&category);
|
||||
if !dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
collect_files(&dir, &dir, &mut files, &category)?;
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn collect_files(
|
||||
base: &PathBuf,
|
||||
dir: &PathBuf,
|
||||
files: &mut Vec<String>,
|
||||
category: &str,
|
||||
) -> Result<(), String> {
|
||||
let entries = fs::read_dir(dir)
|
||||
.map_err(|e| format!("读取目录失败: {e}"))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// core 类别只读根目录的 .md 文件
|
||||
if category != "core" {
|
||||
collect_files(base, &path, files, category)?;
|
||||
}
|
||||
} 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)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.to_string_lossy().to_string());
|
||||
files.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_memory_file(path: String) -> Result<String, String> {
|
||||
// 安全检查:路径不能包含 ..
|
||||
if path.contains("..") {
|
||||
return Err("非法路径".to_string());
|
||||
}
|
||||
|
||||
// 尝试在各个记忆目录下查找
|
||||
let candidates = [
|
||||
memory_dir("memory").join(&path),
|
||||
memory_dir("archive").join(&path),
|
||||
memory_dir("core").join(&path),
|
||||
];
|
||||
|
||||
for candidate in &candidates {
|
||||
if candidate.exists() {
|
||||
return fs::read_to_string(candidate)
|
||||
.map_err(|e| format!("读取失败: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("文件不存在: {path}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_memory_file(path: String, content: String) -> Result<(), String> {
|
||||
if path.contains("..") {
|
||||
return Err("非法路径".to_string());
|
||||
}
|
||||
|
||||
let candidates = [
|
||||
memory_dir("memory").join(&path),
|
||||
memory_dir("archive").join(&path),
|
||||
memory_dir("core").join(&path),
|
||||
];
|
||||
|
||||
for candidate in &candidates {
|
||||
if candidate.exists() {
|
||||
return fs::write(candidate, &content)
|
||||
.map_err(|e| format!("写入失败: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
// 默认写入 memory 目录
|
||||
let target = memory_dir("memory").join(&path);
|
||||
if let Some(parent) = target.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
fs::write(&target, &content)
|
||||
.map_err(|e| format!("写入失败: {e}"))
|
||||
}
|
||||
4
src-tauri/src/commands/mod.rs
Normal file
4
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod logs;
|
||||
pub mod memory;
|
||||
pub mod service;
|
||||
91
src-tauri/src/commands/service.rs
Normal file
91
src-tauri/src/commands/service.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
/// 服务管理命令 (macOS launchd)
|
||||
use std::process::Command;
|
||||
|
||||
use crate::models::types::ServiceStatus;
|
||||
|
||||
const SERVICES: &[(&str, &str)] = &[
|
||||
("ai.openclaw.gateway", "OpenClaw Gateway"),
|
||||
("com.openclaw.guardian.watch", "健康监控 (60s)"),
|
||||
("com.openclaw.guardian.backup", "配置备份 (3600s)"),
|
||||
("com.openclaw.watchdog", "看门狗 (120s)"),
|
||||
];
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_services_status() -> Result<Vec<ServiceStatus>, String> {
|
||||
let output = Command::new("launchctl")
|
||||
.arg("list")
|
||||
.output()
|
||||
.map_err(|e| format!("执行 launchctl 失败: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (label, desc) in SERVICES {
|
||||
let mut status = ServiceStatus {
|
||||
label: label.to_string(),
|
||||
pid: None,
|
||||
running: false,
|
||||
description: desc.to_string(),
|
||||
};
|
||||
|
||||
// 解析 launchctl list 输出: PID\tStatus\tLabel
|
||||
for line in stdout.lines() {
|
||||
if line.contains(label) {
|
||||
let parts: Vec<&str> = line.split('\t').collect();
|
||||
if parts.len() >= 3 {
|
||||
if let Ok(pid) = parts[0].trim().parse::<u32>() {
|
||||
status.pid = Some(pid);
|
||||
status.running = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
results.push(status);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn plist_path(label: &str) -> String {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
format!(
|
||||
"{}/Library/LaunchAgents/{}.plist",
|
||||
home.display(),
|
||||
label
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_service(label: String) -> Result<(), String> {
|
||||
let path = plist_path(&label);
|
||||
Command::new("launchctl")
|
||||
.args(["load", &path])
|
||||
.output()
|
||||
.map_err(|e| format!("启动失败: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stop_service(label: String) -> Result<(), String> {
|
||||
let path = plist_path(&label);
|
||||
Command::new("launchctl")
|
||||
.args(["unload", &path])
|
||||
.output()
|
||||
.map_err(|e| format!("停止失败: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn restart_service(label: String) -> Result<(), String> {
|
||||
let path = plist_path(&label);
|
||||
let _ = Command::new("launchctl")
|
||||
.args(["unload", &path])
|
||||
.output();
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
Command::new("launchctl")
|
||||
.args(["load", &path])
|
||||
.output()
|
||||
.map_err(|e| format!("重启失败: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user