fix: 修复多项关键 Bug,与 openclaw 上游协议对齐

- main.js: wsClient.connect 传参格式错误(完整 ws:// URL → host:port)
- ws-client.js: request() 等待重连时不处理 onReady 握手失败
- gateway.js: bind 写入非法值 'all',改为 openclaw 合法值 'lan'
- device.rs: connect payload 从 v2 升级到 v3,补充 platform/deviceFamily
- config.rs: macOS reload_gateway 在 async fn 中用同步 Command 阻塞 tokio
- service.rs: Windows check_service_status 端口硬编码 18789,改为读配置
- extensions.rs: parse_cftunnel_status 全角冒号解析失败,添加 split_after_colon
- tauri-api.js: cachedInvoke miss 时 logRequest 被记录两次
- tauri-api.js: mock 补充 list_agents / restart_gateway
- chat.js: 附件对象冗余 data 字段(双倍内存)+ 缩进修复
- services.js: 服务操作缺少操作中 toast 反馈
This commit is contained in:
晴天
2026-03-04 12:16:58 +08:00
parent 05771ffa63
commit dab61ccd24
24 changed files with 882 additions and 209 deletions

View File

@@ -4,20 +4,35 @@ use std::process::Command;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
/// 按第一个冒号(半角 ':' 或全角 ''分割返回冒号之后的内容trim 后)
fn split_after_colon(s: &str) -> &str {
// 半角冒号是 1 字节,全角冒号是 3 字节UTF-8: \xef\xbc\x9a
if let Some(pos) = s.find(':') {
return s[pos + 1..].trim();
}
if let Some(pos) = s.find('') {
return s[pos + ''.len_utf8()..].trim();
}
""
}
/// 解析 cftunnel status 输出
fn parse_cftunnel_status(output: &str) -> serde_json::Map<String, Value> {
let mut map = serde_json::Map::new();
for line in output.lines() {
let line = line.trim();
if line.starts_with("隧道:") || line.starts_with("隧道:") {
let rest = line.splitn(2, ':').nth(1).unwrap_or("").trim();
let rest = split_after_colon(line);
let name = rest.split('(').next().unwrap_or(rest).trim();
map.insert("tunnel_name".into(), Value::String(name.to_string()));
} else if line.starts_with("状态:") || line.starts_with("状态:") {
let rest = line.splitn(2, ':').nth(1).unwrap_or("").trim();
let rest = split_after_colon(line);
let running = rest.contains("运行中");
map.insert("running".into(), Value::Bool(running));
if let Some(pid_str) = rest.split("PID:").nth(1) {
// 匹配英文和全角 'PID:' / 'PID'
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();
if let Ok(p) = pid.parse::<u64>() {
map.insert("pid".into(), Value::Number(p.into()));
@@ -228,6 +243,10 @@ pub fn get_cftunnel_logs(lines: Option<u32>) -> Result<String, String> {
pub fn get_clawapp_status() -> Result<Value, String> {
let mut result = serde_json::Map::new();
// 检测是否已安装(检查 npm 全局包)
let installed = check_clawapp_installed();
result.insert("installed".into(), Value::Bool(installed));
// 跨平台方式:尝试连接端口检测是否在运行
let running = std::net::TcpStream::connect_timeout(
&"127.0.0.1:3210".parse().unwrap(),
@@ -259,6 +278,30 @@ pub fn get_clawapp_status() -> Result<Value, String> {
Ok(Value::Object(result))
}
/// 检测 ClawApp 是否已安装
fn check_clawapp_installed() -> bool {
#[cfg(target_os = "windows")]
{
let mut cmd = Command::new("cmd");
cmd.args(["/c", "npm", "list", "-g", "clawapp"]);
cmd.creation_flags(0x08000000);
if let Ok(out) = cmd.output() {
return out.status.success();
}
false
}
#[cfg(not(target_os = "windows"))]
{
if let Ok(out) = Command::new("npm")
.args(["list", "-g", "clawapp"])
.output()
{
return out.status.success();
}
false
}
}
/// 一键安装 cftunnel
/// macOS/Linux: bash 脚本安装
/// Windows: PowerShell 下载安装
@@ -358,3 +401,75 @@ Write-Output '安装完成'
let _ = app.emit("install-log", "✅ cftunnel 安装成功");
Ok("安装成功".into())
}
/// 一键安装 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 tauri::Emitter;
let _ = app.emit("install-log", "开始安装 ClawApp...");
let _ = app.emit("install-progress", 10);
let _ = app.emit("install-log", "通过 npm 安装 clawapp...");
let _ = app.emit("install-progress", 30);
#[cfg(target_os = "windows")]
let mut child = {
let mut cmd = Command::new("cmd");
cmd.args(["/c", "npm", "install", "-g", "clawapp"]);
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("启动安装进程失败: {e}"))?
};
#[cfg(not(target_os = "windows"))]
let mut child = {
Command::new("npm")
.args(["install", "-g", "clawapp"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("启动安装进程失败: {e}"))?
};
let stderr = child.stderr.take();
let stdout = child.stdout.take();
let app2 = app.clone();
let handle = std::thread::spawn(move || {
if let Some(pipe) = stderr {
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
let _ = app2.emit("install-log", &line);
}
}
});
let mut progress = 40;
if let Some(pipe) = stdout {
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
let _ = app.emit("install-log", &line);
if progress < 90 {
progress += 5;
let _ = app.emit("install-progress", progress);
}
}
}
let _ = handle.join();
let _ = app.emit("install-progress", 95);
let status = child.wait().map_err(|e| format!("等待安装进程失败: {e}"))?;
let _ = app.emit("install-progress", 100);
if !status.success() {
let _ = app.emit("install-log", "❌ 安装失败");
return Err("安装失败,请查看日志".into());
}
let _ = app.emit("install-log", "✅ ClawApp 安装成功");
Ok("安装成功".into())
}