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:
晴天
2026-03-03 01:46:19 +08:00
parent 53f46d8ef2
commit 05771ffa63
23 changed files with 1014 additions and 124 deletions

View File

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

View File

@@ -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()));

View File

@@ -6,6 +6,7 @@ pub mod device;
pub mod extensions;
pub mod logs;
pub mod memory;
pub mod pairing;
pub mod service;
/// 获取 OpenClaw 配置目录 (~/.openclaw/)

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

View File

@@ -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),

View File

@@ -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,