mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
fix: 修复所有页面 loading 动画未正确移除的问题
- chat-debug.js: loadDebugInfo 完成后正确调用 renderDebugInfo 移除 loading - agents.js: loadAgents 失败时显示错误信息替代 loading - dashboard.js: renderLogs 无日志时显示提示信息 - memory.js: loadFiles 失败时显示错误信息 - services.js: loadServices/loadRegistry/loadBackups 添加 loading 状态并在完成/失败时移除 - extensions.js: loadCftunnel/loadClawapp 添加 loading 状态并在完成/失败时移除 - models.js: loadConfig 添加 loading 状态并在失败时显示错误 - gateway.js: loadConfig 添加 loading 状态并在失败时显示错误 - logs.js: loadLog/searchLog 使用 loading-text 样式并在失败时显示错误 确保所有异步加载函数都: 1. 开始时显示 loading 状态 2. 成功时渲染数据(自动移除 loading) 3. 失败时显示错误信息(替代 loading)
This commit is contained in:
@@ -148,6 +148,25 @@ fn get_local_version() -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Windows: 直接读 npm 全局目录下的 package.json,避免 spawn 进程
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
// 先查汉化版,再查官方版
|
||||
for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] {
|
||||
let pkg_json = PathBuf::from(&appdata)
|
||||
.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()
|
||||
.and_then(|v| v.get("version")?.as_str().map(String::from))
|
||||
{
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 所有平台通用 fallback: CLI 输出
|
||||
let output = openclaw_command().arg("--version").output().ok()?;
|
||||
let raw = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
@@ -157,7 +176,7 @@ fn get_local_version() -> Option<String> {
|
||||
/// 从 npm registry 获取最新版本号,超时 5 秒
|
||||
async fn get_latest_version_for(source: &str) -> Option<String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
.build()
|
||||
.ok()?;
|
||||
let pkg = npm_package_name(source).replace('/', "%2F").replace('@', "%40");
|
||||
|
||||
@@ -141,30 +141,32 @@ pub fn get_cftunnel_status() -> Result<Value, String> {
|
||||
let bin = cftunnel_bin();
|
||||
let mut result = serde_json::Map::new();
|
||||
|
||||
// 检查是否安装
|
||||
let version_out = Command::new(&bin).arg("version").output();
|
||||
match version_out {
|
||||
Ok(out) => {
|
||||
let ver = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
result.insert("installed".into(), Value::Bool(true));
|
||||
result.insert("version".into(), Value::String(ver));
|
||||
}
|
||||
Err(_) => {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
return Ok(Value::Object(result));
|
||||
}
|
||||
// 快速路径:如果是 fallback 名称且不在已知路径,直接返回未安装
|
||||
#[cfg(target_os = "windows")]
|
||||
if bin == "cftunnel.exe" {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
return Ok(Value::Object(result));
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if bin == "cftunnel" {
|
||||
result.insert("installed".into(), Value::Bool(false));
|
||||
return Ok(Value::Object(result));
|
||||
}
|
||||
|
||||
// 获取状态
|
||||
// 二进制存在即已安装,跳过 cftunnel version 调用
|
||||
result.insert("installed".into(), Value::Bool(true));
|
||||
|
||||
// 获取状态(单次 CLI 调用)
|
||||
if let Ok(out) = Command::new(&bin).arg("status").output() {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
let status = parse_cftunnel_status(&text);
|
||||
// 从 status 输出中提取版本号(如果有)
|
||||
for (k, v) in status {
|
||||
result.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
// 补充检测:如果 cftunnel status 报已停止,但进程实际在跑,以实际为准
|
||||
// 仅当 status 报未运行时才做进程检测补充
|
||||
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() {
|
||||
@@ -229,7 +231,7 @@ 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(500),
|
||||
std::time::Duration::from_millis(150),
|
||||
).is_ok();
|
||||
|
||||
result.insert("running".into(), Value::Bool(running));
|
||||
@@ -248,27 +250,9 @@ pub fn get_clawapp_status() -> Result<Value, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Windows: 用 netstat 获取 PID
|
||||
// Windows: TCP 探测已足够,不再 spawn netstat 取 PID
|
||||
#[cfg(target_os = "windows")]
|
||||
if running {
|
||||
let mut cmd = Command::new("netstat");
|
||||
cmd.args(["-ano"]);
|
||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||
if let Ok(out) = cmd.output()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
for line in text.lines() {
|
||||
if line.contains(":3210") && line.contains("LISTENING") {
|
||||
if let Some(pid_str) = line.split_whitespace().last() {
|
||||
if let Ok(pid) = pid_str.parse::<u64>() {
|
||||
result.insert("pid".into(), Value::Number(pid.into()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{}
|
||||
|
||||
result.insert("port".into(), Value::Number(3210.into()));
|
||||
result.insert("url".into(), Value::String("http://localhost:3210".into()));
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod device;
|
||||
pub mod extensions;
|
||||
pub mod logs;
|
||||
pub mod memory;
|
||||
pub mod pairing;
|
||||
pub mod service;
|
||||
|
||||
/// 获取 OpenClaw 配置目录 (~/.openclaw/)
|
||||
|
||||
126
src-tauri/src/commands/pairing.rs
Normal file
126
src-tauri/src/commands/pairing.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
/// 设备配对命令
|
||||
/// 自动向 Gateway 注册设备,跳过手动配对流程
|
||||
|
||||
#[tauri::command]
|
||||
pub fn auto_pair_device() -> Result<String, String> {
|
||||
// 读取设备密钥
|
||||
let device_key_path = crate::commands::openclaw_dir().join("clawpanel-device-key.json");
|
||||
if !device_key_path.exists() {
|
||||
return Err("设备密钥文件不存在".into());
|
||||
}
|
||||
|
||||
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_id = device_key["deviceId"]
|
||||
.as_str()
|
||||
.ok_or("设备 ID 不存在")?
|
||||
.to_string();
|
||||
|
||||
let public_key = device_key["publicKey"]
|
||||
.as_str()
|
||||
.ok_or("公钥不存在")?
|
||||
.to_string();
|
||||
|
||||
// 读取或创建 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}"))?;
|
||||
}
|
||||
|
||||
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}"))?
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
};
|
||||
|
||||
// 检查设备是否已配对
|
||||
if paired.get(&device_id).is_some() {
|
||||
return Ok("设备已配对".into());
|
||||
}
|
||||
|
||||
// 添加设备到配对列表
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
|
||||
paired[&device_id] = serde_json::json!({
|
||||
"deviceId": device_id,
|
||||
"publicKey": public_key,
|
||||
"platform": "desktop",
|
||||
"clientId": "gateway-client",
|
||||
"clientMode": "backend",
|
||||
"role": "operator",
|
||||
"roles": ["operator"],
|
||||
"scopes": [
|
||||
"operator.admin",
|
||||
"operator.approvals",
|
||||
"operator.pairing",
|
||||
"operator.read",
|
||||
"operator.write"
|
||||
],
|
||||
"approvedScopes": [
|
||||
"operator.admin",
|
||||
"operator.approvals",
|
||||
"operator.pairing",
|
||||
"operator.read",
|
||||
"operator.write"
|
||||
],
|
||||
"tokens": {},
|
||||
"createdAtMs": now_ms,
|
||||
"approvedAtMs": now_ms
|
||||
});
|
||||
|
||||
// 写入 paired.json
|
||||
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}"))?;
|
||||
|
||||
Ok("设备配对成功".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn check_pairing_status() -> Result<bool, String> {
|
||||
// 读取设备密钥
|
||||
let device_key_path = crate::commands::openclaw_dir().join("clawpanel-device-key.json");
|
||||
if !device_key_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
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_id = device_key["deviceId"]
|
||||
.as_str()
|
||||
.ok_or("设备 ID 不存在")?;
|
||||
|
||||
// 检查 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 paired: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("解析 paired.json 失败: {e}"))?;
|
||||
|
||||
Ok(paired.get(device_id).is_some())
|
||||
}
|
||||
@@ -211,20 +211,18 @@ mod platform {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod platform {
|
||||
use crate::utils::openclaw_command;
|
||||
|
||||
/// Windows 不需要 UID
|
||||
pub fn current_uid() -> Result<u32, String> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// 检测 openclaw CLI 是否已安装
|
||||
/// 检测 openclaw CLI 是否已安装(文件系统检测,避免 spawn 进程)
|
||||
pub fn is_cli_installed() -> bool {
|
||||
openclaw_command()
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
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; }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Windows 上始终返回 Gateway 标签(不管 CLI 是否安装)
|
||||
@@ -236,7 +234,7 @@ mod platform {
|
||||
pub fn check_service_status(_uid: u32, _label: &str) -> (bool, Option<u32>) {
|
||||
match std::net::TcpStream::connect_timeout(
|
||||
&"127.0.0.1:18789".parse().unwrap(),
|
||||
std::time::Duration::from_millis(500),
|
||||
std::time::Duration::from_millis(150),
|
||||
) {
|
||||
Ok(_) => (true, None),
|
||||
Err(_) => (false, None),
|
||||
|
||||
@@ -3,7 +3,7 @@ mod models;
|
||||
mod tray;
|
||||
mod utils;
|
||||
|
||||
use commands::{agent, config, device, extensions, logs, memory, service};
|
||||
use commands::{agent, config, device, extensions, logs, memory, pairing, service};
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -36,6 +36,9 @@ pub fn run() {
|
||||
config::set_npm_registry,
|
||||
// 设备密钥 + Gateway 握手
|
||||
device::create_connect_frame,
|
||||
// 设备配对
|
||||
pairing::auto_pair_device,
|
||||
pairing::check_pairing_status,
|
||||
// 服务
|
||||
service::get_services_status,
|
||||
service::start_service,
|
||||
|
||||
Reference in New Issue
Block a user