feat: ClawPanel v0.1.0 项目骨架

- Tauri v2 + Vanilla JS + Vite 技术栈
- 9 个页面: 仪表盘/服务管理/日志/模型配置/Agent配置/Gateway/MCP工具/记忆文件/部署
- Rust 后端: 配置读写/服务管理(launchd)/日志读取/记忆文件管理
- 暗色主题 + 玻璃拟态 UI
- Mock 数据支持纯浏览器开发调试
This commit is contained in:
晴天
2026-02-26 22:34:55 +08:00
commit e26c4d9307
54 changed files with 13839 additions and 0 deletions

View 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,
})
}

View 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)
}

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

View File

@@ -0,0 +1,4 @@
pub mod config;
pub mod logs;
pub mod memory;
pub mod service;

View 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(())
}