refactor(desktop): split kernel_commands/pipeline_commands into modules, add SaaS client libs and gateway modules
Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines) into focused sub-modules under kernel_commands/ and pipeline_commands/ directories. Add gateway module (commands, config, io, runtime), health_check, and 15 new TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel sub-systems (a2a, agent, chat, hands, skills, triggers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
245
desktop/src-tauri/src/gateway/commands.rs
Normal file
245
desktop/src-tauri/src/gateway/commands.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use super::config::{
|
||||
approve_local_device_pairing, ensure_local_gateway_ready_for_tauri, read_local_gateway_auth,
|
||||
LocalGatewayAuth, LocalGatewayPairingApprovalResult, LocalGatewayPrepareResult,
|
||||
};
|
||||
use super::io::{parse_json_output, read_gateway_status, run_zclaw, LocalGatewayStatus};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct VersionResponse {
|
||||
version: String,
|
||||
commit: Option<String>,
|
||||
build_date: Option<String>,
|
||||
runtime_source: Option<String>,
|
||||
raw: Value,
|
||||
}
|
||||
|
||||
/// Process information structure
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ProcessInfo {
|
||||
pid: u32,
|
||||
name: String,
|
||||
status: String,
|
||||
cpu_percent: Option<f64>,
|
||||
memory_mb: Option<f64>,
|
||||
uptime_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// Process list response
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ProcessListResponse {
|
||||
processes: Vec<ProcessInfo>,
|
||||
total_count: usize,
|
||||
runtime_source: Option<String>,
|
||||
}
|
||||
|
||||
/// Process logs response
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ProcessLogsResponse {
|
||||
pid: Option<u32>,
|
||||
logs: String,
|
||||
lines: usize,
|
||||
runtime_source: Option<String>,
|
||||
}
|
||||
|
||||
/// Get ZCLAW Kernel status
|
||||
#[tauri::command]
|
||||
pub fn zclaw_status(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||||
read_gateway_status(&app)
|
||||
}
|
||||
|
||||
/// Start ZCLAW Kernel
|
||||
#[tauri::command]
|
||||
pub fn zclaw_start(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||||
ensure_local_gateway_ready_for_tauri(&app)?;
|
||||
run_zclaw(&app, &["gateway", "start", "--json"])?;
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
read_gateway_status(&app)
|
||||
}
|
||||
|
||||
/// Stop ZCLAW Kernel
|
||||
#[tauri::command]
|
||||
pub fn zclaw_stop(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||||
run_zclaw(&app, &["gateway", "stop", "--json"])?;
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
read_gateway_status(&app)
|
||||
}
|
||||
|
||||
/// Restart ZCLAW Kernel
|
||||
#[tauri::command]
|
||||
pub fn zclaw_restart(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||||
ensure_local_gateway_ready_for_tauri(&app)?;
|
||||
run_zclaw(&app, &["gateway", "restart", "--json"])?;
|
||||
thread::sleep(Duration::from_millis(1200));
|
||||
read_gateway_status(&app)
|
||||
}
|
||||
|
||||
/// Get local auth token from ZCLAW config
|
||||
#[tauri::command]
|
||||
pub fn zclaw_local_auth() -> Result<LocalGatewayAuth, String> {
|
||||
read_local_gateway_auth()
|
||||
}
|
||||
|
||||
/// Prepare ZCLAW for Tauri (update allowed origins)
|
||||
#[tauri::command]
|
||||
pub fn zclaw_prepare_for_tauri(app: AppHandle) -> Result<LocalGatewayPrepareResult, String> {
|
||||
ensure_local_gateway_ready_for_tauri(&app)
|
||||
}
|
||||
|
||||
/// Approve device pairing request
|
||||
#[tauri::command]
|
||||
pub fn zclaw_approve_device_pairing(
|
||||
app: AppHandle,
|
||||
device_id: String,
|
||||
public_key_base64: String,
|
||||
url: Option<String>,
|
||||
) -> Result<LocalGatewayPairingApprovalResult, String> {
|
||||
approve_local_device_pairing(&app, &device_id, &public_key_base64, url.as_deref())
|
||||
}
|
||||
|
||||
/// Run ZCLAW doctor to diagnose issues
|
||||
#[tauri::command]
|
||||
pub fn zclaw_doctor(app: AppHandle) -> Result<String, String> {
|
||||
let result = run_zclaw(&app, &["doctor", "--json"])?;
|
||||
Ok(result.stdout)
|
||||
}
|
||||
|
||||
/// List ZCLAW processes
|
||||
#[tauri::command]
|
||||
pub fn zclaw_process_list(app: AppHandle) -> Result<ProcessListResponse, String> {
|
||||
let result = run_zclaw(&app, &["process", "list", "--json"])?;
|
||||
|
||||
let raw = parse_json_output(&result.stdout).unwrap_or_else(|_| json!({"processes": []}));
|
||||
|
||||
let processes: Vec<ProcessInfo> = raw
|
||||
.get("processes")
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|p| {
|
||||
Some(ProcessInfo {
|
||||
pid: p.get("pid").and_then(Value::as_u64)?.try_into().ok()?,
|
||||
name: p.get("name").and_then(Value::as_str)?.to_string(),
|
||||
status: p
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
cpu_percent: p.get("cpuPercent").and_then(Value::as_f64),
|
||||
memory_mb: p.get("memoryMb").and_then(Value::as_f64),
|
||||
uptime_seconds: p.get("uptimeSeconds").and_then(Value::as_u64),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ProcessListResponse {
|
||||
total_count: processes.len(),
|
||||
processes,
|
||||
runtime_source: Some(result.runtime.source),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get ZCLAW process logs
|
||||
#[tauri::command]
|
||||
pub fn zclaw_process_logs(
|
||||
app: AppHandle,
|
||||
pid: Option<u32>,
|
||||
lines: Option<usize>,
|
||||
) -> Result<ProcessLogsResponse, String> {
|
||||
let line_count = lines.unwrap_or(100);
|
||||
let lines_str = line_count.to_string();
|
||||
|
||||
// Build owned strings first to avoid lifetime issues
|
||||
let args: Vec<String> = if let Some(pid_value) = pid {
|
||||
vec![
|
||||
"process".to_string(),
|
||||
"logs".to_string(),
|
||||
"--pid".to_string(),
|
||||
pid_value.to_string(),
|
||||
"--lines".to_string(),
|
||||
lines_str,
|
||||
"--json".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
"process".to_string(),
|
||||
"logs".to_string(),
|
||||
"--lines".to_string(),
|
||||
lines_str,
|
||||
"--json".to_string(),
|
||||
]
|
||||
};
|
||||
|
||||
// Convert to &str for the command
|
||||
let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
let result = run_zclaw(&app, &args_refs)?;
|
||||
|
||||
// Parse the logs - could be JSON array or plain text
|
||||
let logs = if let Ok(json) = parse_json_output(&result.stdout) {
|
||||
// If JSON format, extract logs array or convert to string
|
||||
if let Some(log_lines) = json.get("logs").and_then(Value::as_array) {
|
||||
log_lines
|
||||
.iter()
|
||||
.filter_map(|l| l.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else if let Some(log_text) = json.get("log").and_then(Value::as_str) {
|
||||
log_text.to_string()
|
||||
} else {
|
||||
result.stdout.clone()
|
||||
}
|
||||
} else {
|
||||
result.stdout.clone()
|
||||
};
|
||||
|
||||
let log_lines_count = logs.lines().count();
|
||||
|
||||
Ok(ProcessLogsResponse {
|
||||
pid,
|
||||
logs,
|
||||
lines: log_lines_count,
|
||||
runtime_source: Some(result.runtime.source),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get ZCLAW version information
|
||||
#[tauri::command]
|
||||
pub fn zclaw_version(app: AppHandle) -> Result<VersionResponse, String> {
|
||||
let result = run_zclaw(&app, &["--version", "--json"])?;
|
||||
|
||||
let raw = parse_json_output(&result.stdout).unwrap_or_else(|_| {
|
||||
// Fallback: try to parse plain text version output
|
||||
json!({
|
||||
"version": result.stdout.trim(),
|
||||
"raw": result.stdout.trim()
|
||||
})
|
||||
});
|
||||
|
||||
let version = raw
|
||||
.get("version")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let commit = raw.get("commit").and_then(Value::as_str).map(ToOwned::to_owned);
|
||||
let build_date = raw.get("buildDate").and_then(Value::as_str).map(ToOwned::to_owned);
|
||||
|
||||
Ok(VersionResponse {
|
||||
version,
|
||||
commit,
|
||||
build_date,
|
||||
runtime_source: Some(result.runtime.source),
|
||||
raw,
|
||||
})
|
||||
}
|
||||
237
desktop/src-tauri/src/gateway/config.rs
Normal file
237
desktop/src-tauri/src/gateway/config.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use super::io::{read_gateway_status, run_zclaw, parse_json_output};
|
||||
use super::runtime::{resolve_zclaw_config_path, TAURI_ALLOWED_ORIGINS};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalGatewayAuth {
|
||||
pub config_path: Option<String>,
|
||||
pub gateway_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalGatewayPrepareResult {
|
||||
pub config_path: Option<String>,
|
||||
pub origins_updated: bool,
|
||||
pub gateway_restarted: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalGatewayPairingApprovalResult {
|
||||
pub approved: bool,
|
||||
pub request_id: Option<String>,
|
||||
pub device_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse TOML config and extract gateway token
|
||||
pub fn read_local_gateway_auth() -> Result<LocalGatewayAuth, String> {
|
||||
let config_path = resolve_zclaw_config_path()
|
||||
.ok_or_else(|| "未找到 ZCLAW 配置目录。".to_string())?;
|
||||
let config_text = fs::read_to_string(&config_path)
|
||||
.map_err(|error| format!("读取 ZCLAW 配置失败: {error}"))?;
|
||||
|
||||
// Parse TOML format - simple extraction for gateway.token
|
||||
let gateway_token = extract_toml_token(&config_text);
|
||||
|
||||
Ok(LocalGatewayAuth {
|
||||
config_path: Some(config_path.display().to_string()),
|
||||
gateway_token,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract gateway.token from TOML config text
|
||||
fn extract_toml_token(config_text: &str) -> Option<String> {
|
||||
// Simple TOML parsing for gateway.token
|
||||
// Format: token = "value" under [gateway] section
|
||||
let mut in_gateway_section = false;
|
||||
for line in config_text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("[gateway") {
|
||||
in_gateway_section = true;
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with('[') && !trimmed.starts_with("[gateway") {
|
||||
in_gateway_section = false;
|
||||
continue;
|
||||
}
|
||||
if in_gateway_section && trimmed.starts_with("token") {
|
||||
if let Some(eq_pos) = trimmed.find('=') {
|
||||
let value = trimmed[eq_pos + 1..].trim();
|
||||
// Remove quotes
|
||||
let value = value.trim_matches('"').trim_matches('\'');
|
||||
if !value.is_empty() {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ensure Tauri origins are allowed in ZCLAW config
|
||||
fn ensure_tauri_allowed_origins(config_text: &str) -> (String, bool) {
|
||||
let mut lines: Vec<String> = config_text.lines().map(|s| s.to_string()).collect();
|
||||
let mut changed = false;
|
||||
let mut in_control_ui = false;
|
||||
let mut has_allowed_origins = false;
|
||||
|
||||
// Find or create [gateway.controlUi] section with allowedOrigins
|
||||
for i in 0..lines.len() {
|
||||
let trimmed = lines[i].trim();
|
||||
|
||||
if trimmed.starts_with("[gateway.controlUi") || trimmed == "[gateway.controlUi]" {
|
||||
in_control_ui = true;
|
||||
} else if trimmed.starts_with('[') && in_control_ui {
|
||||
in_control_ui = false;
|
||||
}
|
||||
|
||||
if in_control_ui && trimmed.starts_with("allowedOrigins") {
|
||||
has_allowed_origins = true;
|
||||
// Check if all required origins are present
|
||||
for origin in TAURI_ALLOWED_ORIGINS {
|
||||
if !lines[i].contains(origin) {
|
||||
// Append origin to the array
|
||||
// This is a simple approach - for production, use proper TOML parsing
|
||||
if lines[i].ends_with(']') {
|
||||
let insert_pos = lines[i].len() - 1;
|
||||
lines[i].insert_str(insert_pos, &format!(", \"{}\"", origin));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no allowedOrigins found, add the section
|
||||
if !has_allowed_origins {
|
||||
// Find [gateway] section and add controlUi after it
|
||||
for i in 0..lines.len() {
|
||||
if lines[i].trim().starts_with("[gateway]") || lines[i].trim() == "[gateway]" {
|
||||
// Insert controlUi section after gateway
|
||||
let origins: String = TAURI_ALLOWED_ORIGINS
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
lines.insert(i + 1, "[gateway.controlUi]".to_string());
|
||||
lines.insert(i + 2, format!("allowedOrigins = [{}]", origins));
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no [gateway] section found, create it
|
||||
if !changed {
|
||||
let origins: String = TAURI_ALLOWED_ORIGINS
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
lines.push("[gateway]".to_string());
|
||||
lines.push("[gateway.controlUi]".to_string());
|
||||
lines.push(format!("allowedOrigins = [{}]", origins));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
(lines.join("\n"), changed)
|
||||
}
|
||||
|
||||
pub fn ensure_local_gateway_ready_for_tauri(app: &AppHandle) -> Result<LocalGatewayPrepareResult, String> {
|
||||
let config_path = resolve_zclaw_config_path()
|
||||
.ok_or_else(|| "未找到 ZCLAW 配置目录。".to_string())?;
|
||||
let config_text = fs::read_to_string(&config_path)
|
||||
.map_err(|error| format!("读取 ZCLAW 配置失败: {error}"))?;
|
||||
|
||||
let (updated_config, origins_updated) = ensure_tauri_allowed_origins(&config_text);
|
||||
|
||||
if origins_updated {
|
||||
fs::write(&config_path, format!("{}\n", updated_config))
|
||||
.map_err(|error| format!("写入 ZCLAW 配置失败: {error}"))?;
|
||||
}
|
||||
|
||||
let mut gateway_restarted = false;
|
||||
if origins_updated {
|
||||
if let Ok(status) = read_gateway_status(app) {
|
||||
if status.port_status.as_deref() == Some("busy") || !status.listener_pids.is_empty() {
|
||||
run_zclaw(app, &["gateway", "restart", "--json"])?;
|
||||
thread::sleep(Duration::from_millis(1200));
|
||||
gateway_restarted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LocalGatewayPrepareResult {
|
||||
config_path: Some(config_path.display().to_string()),
|
||||
origins_updated,
|
||||
gateway_restarted,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn approve_local_device_pairing(
|
||||
app: &AppHandle,
|
||||
device_id: &str,
|
||||
public_key_base64: &str,
|
||||
url: Option<&str>,
|
||||
) -> Result<LocalGatewayPairingApprovalResult, String> {
|
||||
let local_auth = read_local_gateway_auth()?;
|
||||
let gateway_token = local_auth
|
||||
.gateway_token
|
||||
.ok_or_else(|| "本地 Gateway token 不可用,无法自动批准设备配对。".to_string())?;
|
||||
|
||||
let devices_output = run_zclaw(app, &["devices", "list", "--json"])?;
|
||||
let devices_json = parse_json_output(&devices_output.stdout)?;
|
||||
let pending = devices_json
|
||||
.get("pending")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| "设备列表输出缺少 pending 数组。".to_string())?;
|
||||
|
||||
let pending_request = pending.iter().find(|entry| {
|
||||
entry.get("deviceId").and_then(Value::as_str) == Some(device_id)
|
||||
&& entry.get("publicKey").and_then(Value::as_str) == Some(public_key_base64)
|
||||
});
|
||||
|
||||
let Some(request) = pending_request else {
|
||||
return Ok(LocalGatewayPairingApprovalResult {
|
||||
approved: false,
|
||||
request_id: None,
|
||||
device_id: Some(device_id.to_string()),
|
||||
});
|
||||
};
|
||||
|
||||
let request_id = request
|
||||
.get("requestId")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "待批准设备缺少 requestId。".to_string())?
|
||||
.to_string();
|
||||
|
||||
// Use ZCLAW default port 4200
|
||||
let gateway_url = url.unwrap_or("ws://127.0.0.1:4200").to_string();
|
||||
let args = vec![
|
||||
"devices".to_string(),
|
||||
"approve".to_string(),
|
||||
request_id.clone(),
|
||||
"--json".to_string(),
|
||||
"--token".to_string(),
|
||||
gateway_token,
|
||||
"--url".to_string(),
|
||||
gateway_url,
|
||||
];
|
||||
let arg_refs = args.iter().map(|value| value.as_str()).collect::<Vec<_>>();
|
||||
run_zclaw(app, &arg_refs)?;
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
|
||||
Ok(LocalGatewayPairingApprovalResult {
|
||||
approved: true,
|
||||
request_id: Some(request_id),
|
||||
device_id: Some(device_id.to_string()),
|
||||
})
|
||||
}
|
||||
167
desktop/src-tauri/src/gateway/io.rs
Normal file
167
desktop/src-tauri/src/gateway/io.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::process::Command;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use super::runtime::{
|
||||
command_error, resolve_zclaw_runtime, runtime_path_string, ZclawCommandOutput, ZclawRuntime,
|
||||
ZCLAW_DEFAULT_PORT,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalGatewayStatus {
|
||||
pub supported: bool,
|
||||
pub cli_available: bool,
|
||||
pub runtime_source: Option<String>,
|
||||
pub runtime_path: Option<String>,
|
||||
pub service_label: Option<String>,
|
||||
pub service_loaded: bool,
|
||||
pub service_status: Option<String>,
|
||||
pub config_ok: bool,
|
||||
pub port: Option<u16>,
|
||||
pub port_status: Option<String>,
|
||||
pub probe_url: Option<String>,
|
||||
pub listener_pids: Vec<u32>,
|
||||
pub error: Option<String>,
|
||||
pub raw: Value,
|
||||
}
|
||||
|
||||
pub fn run_zclaw(app: &AppHandle, args: &[&str]) -> Result<ZclawCommandOutput, String> {
|
||||
let runtime = resolve_zclaw_runtime(app);
|
||||
let mut command = Command::new(&runtime.executable);
|
||||
command.args(&runtime.pre_args).args(args);
|
||||
let output = command.output().map_err(|error| command_error(&runtime, error))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(ZclawCommandOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
|
||||
runtime,
|
||||
})
|
||||
} else {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let message = if stderr.is_empty() {
|
||||
stdout
|
||||
} else if stdout.is_empty() {
|
||||
stderr
|
||||
} else {
|
||||
format!("{stderr}\n{stdout}")
|
||||
};
|
||||
|
||||
if message.is_empty() {
|
||||
Err(format!("ZCLAW {:?} 执行失败: {}", args, output.status))
|
||||
} else {
|
||||
Err(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_output(stdout: &str) -> Result<Value, String> {
|
||||
if let Ok(raw) = serde_json::from_str::<Value>(stdout) {
|
||||
return Ok(raw);
|
||||
}
|
||||
|
||||
if let Some(index) = stdout.find('{') {
|
||||
let trimmed = &stdout[index..];
|
||||
return serde_json::from_str::<Value>(trimmed)
|
||||
.map_err(|error| format!("解析 Gateway 状态失败: {error}"));
|
||||
}
|
||||
|
||||
Err("Gateway 状态输出不包含可解析的 JSON。".to_string())
|
||||
}
|
||||
|
||||
pub fn unavailable_status(error: String, runtime: Option<&ZclawRuntime>) -> LocalGatewayStatus {
|
||||
LocalGatewayStatus {
|
||||
supported: true,
|
||||
cli_available: false,
|
||||
runtime_source: runtime.map(|value| value.source.clone()),
|
||||
runtime_path: runtime.map(runtime_path_string),
|
||||
service_label: None,
|
||||
service_loaded: false,
|
||||
service_status: None,
|
||||
config_ok: false,
|
||||
port: None,
|
||||
port_status: None,
|
||||
probe_url: None,
|
||||
listener_pids: Vec::new(),
|
||||
error: Some(error),
|
||||
raw: json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_gateway_status(raw: Value, runtime: &ZclawRuntime) -> LocalGatewayStatus {
|
||||
let listener_pids = raw
|
||||
.get("port")
|
||||
.and_then(|port| port.get("listeners"))
|
||||
.and_then(Value::as_array)
|
||||
.map(|listeners| {
|
||||
listeners
|
||||
.iter()
|
||||
.filter_map(|listener| listener.get("pid").and_then(Value::as_u64))
|
||||
.filter_map(|pid| u32::try_from(pid).ok())
|
||||
.collect::<Vec<u32>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
LocalGatewayStatus {
|
||||
supported: true,
|
||||
cli_available: true,
|
||||
runtime_source: Some(runtime.source.clone()),
|
||||
runtime_path: Some(runtime_path_string(runtime)),
|
||||
service_label: raw
|
||||
.get("service")
|
||||
.and_then(|service| service.get("label"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
service_loaded: raw
|
||||
.get("service")
|
||||
.and_then(|service| service.get("loaded"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
service_status: raw
|
||||
.get("service")
|
||||
.and_then(|service| service.get("runtime"))
|
||||
.and_then(|runtime| runtime.get("status"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
config_ok: raw
|
||||
.get("service")
|
||||
.and_then(|service| service.get("configAudit"))
|
||||
.and_then(|config_audit| config_audit.get("ok"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
port: raw
|
||||
.get("gateway")
|
||||
.and_then(|gateway| gateway.get("port"))
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|port| u16::try_from(port).ok())
|
||||
.or(Some(ZCLAW_DEFAULT_PORT)),
|
||||
port_status: raw
|
||||
.get("port")
|
||||
.and_then(|port| port.get("status"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
probe_url: raw
|
||||
.get("gateway")
|
||||
.and_then(|gateway| gateway.get("probeUrl"))
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
listener_pids,
|
||||
error: None,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_gateway_status(app: &AppHandle) -> Result<LocalGatewayStatus, String> {
|
||||
match run_zclaw(app, &["gateway", "status", "--json", "--no-probe"]) {
|
||||
Ok(result) => {
|
||||
let raw = parse_json_output(&result.stdout)?;
|
||||
Ok(parse_gateway_status(raw, &result.runtime))
|
||||
}
|
||||
Err(error) => {
|
||||
let runtime = resolve_zclaw_runtime(app);
|
||||
Ok(unavailable_status(error, Some(&runtime)))
|
||||
}
|
||||
}
|
||||
}
|
||||
4
desktop/src-tauri/src/gateway/mod.rs
Normal file
4
desktop/src-tauri/src/gateway/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod io;
|
||||
pub mod runtime;
|
||||
290
desktop/src-tauri/src/gateway/runtime.rs
Normal file
290
desktop/src-tauri/src/gateway/runtime.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub(crate) struct ZclawRuntime {
|
||||
pub source: String,
|
||||
pub executable: PathBuf,
|
||||
pub pre_args: Vec<String>,
|
||||
pub display_path: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) struct ZclawCommandOutput {
|
||||
pub stdout: String,
|
||||
pub runtime: ZclawRuntime,
|
||||
}
|
||||
|
||||
/// Default ZCLAW Kernel port
|
||||
pub const ZCLAW_DEFAULT_PORT: u16 = 4200;
|
||||
|
||||
pub(super) const TAURI_ALLOWED_ORIGINS: [&str; 2] = ["http://tauri.localhost", "tauri://localhost"];
|
||||
|
||||
pub(super) fn command_error(runtime: &ZclawRuntime, error: std::io::Error) -> String {
|
||||
if error.kind() == std::io::ErrorKind::NotFound {
|
||||
match runtime.source.as_str() {
|
||||
"bundled" => format!(
|
||||
"未找到 ZCLAW 内置运行时:{}",
|
||||
runtime.display_path.display()
|
||||
),
|
||||
"development" => format!(
|
||||
"未找到开发态运行时:{}",
|
||||
runtime.display_path.display()
|
||||
),
|
||||
"override" => format!(
|
||||
"未找到 ZCLAW_BIN 指定的运行时:{}",
|
||||
runtime.display_path.display()
|
||||
),
|
||||
_ => "未找到运行时。请重新安装 ZCLAW,或在开发环境中安装 ZCLAW CLI。"
|
||||
.to_string(),
|
||||
}
|
||||
} else {
|
||||
format!("运行 ZCLAW 失败: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn runtime_path_string(runtime: &ZclawRuntime) -> String {
|
||||
runtime.display_path.display().to_string()
|
||||
}
|
||||
|
||||
fn binary_extension() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
".exe"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fn zclaw_sidecar_filename() -> String {
|
||||
format!("zclaw-{}{}", env!("TARGET"), binary_extension())
|
||||
}
|
||||
|
||||
fn zclaw_plain_filename() -> String {
|
||||
format!("zclaw{}", binary_extension())
|
||||
}
|
||||
|
||||
fn push_runtime_candidate(candidates: &mut Vec<ZclawRuntime>, source: &str, executable: PathBuf) {
|
||||
if candidates.iter().any(|candidate| candidate.display_path == executable) {
|
||||
return;
|
||||
}
|
||||
|
||||
candidates.push(ZclawRuntime {
|
||||
source: source.to_string(),
|
||||
display_path: executable.clone(),
|
||||
executable,
|
||||
pre_args: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Build binary runtime (ZCLAW is a single binary, not npm package)
|
||||
fn build_binary_runtime(source: &str, root_dir: &PathBuf) -> Option<ZclawRuntime> {
|
||||
// Try platform-specific binary names
|
||||
let binary_names = get_platform_binary_names();
|
||||
|
||||
for name in binary_names {
|
||||
let binary_path = root_dir.join(&name);
|
||||
if binary_path.is_file() {
|
||||
return Some(ZclawRuntime {
|
||||
source: source.to_string(),
|
||||
executable: binary_path.clone(),
|
||||
pre_args: Vec::new(),
|
||||
display_path: binary_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get platform-specific binary names for ZCLAW
|
||||
fn get_platform_binary_names() -> Vec<String> {
|
||||
let mut names = Vec::new();
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
names.push("zclaw.exe".to_string());
|
||||
names.push(format!("zclaw-{}.exe", env!("TARGET")));
|
||||
} else if cfg!(target_os = "macos") {
|
||||
if cfg!(target_arch = "aarch64") {
|
||||
names.push("zclaw-aarch64-apple-darwin".to_string());
|
||||
} else {
|
||||
names.push("zclaw-x86_64-apple-darwin".to_string());
|
||||
}
|
||||
names.push(format!("zclaw-{}", env!("TARGET")));
|
||||
names.push("zclaw".to_string());
|
||||
} else {
|
||||
// Linux
|
||||
if cfg!(target_arch = "aarch64") {
|
||||
names.push("zclaw-aarch64-unknown-linux-gnu".to_string());
|
||||
} else {
|
||||
names.push("zclaw-x86_64-unknown-linux-gnu".to_string());
|
||||
}
|
||||
names.push(format!("zclaw-{}", env!("TARGET")));
|
||||
names.push("zclaw".to_string());
|
||||
}
|
||||
|
||||
names
|
||||
}
|
||||
|
||||
/// Legacy: Build staged runtime using Node.js (for backward compatibility)
|
||||
fn build_staged_runtime_legacy(source: &str, root_dir: PathBuf) -> Option<ZclawRuntime> {
|
||||
let node_executable = root_dir.join(if cfg!(target_os = "windows") {
|
||||
"node.exe"
|
||||
} else {
|
||||
"node"
|
||||
});
|
||||
let entrypoint = root_dir
|
||||
.join("node_modules")
|
||||
.join("zclaw")
|
||||
.join("zclaw.mjs");
|
||||
|
||||
if !node_executable.is_file() || !entrypoint.is_file() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ZclawRuntime {
|
||||
source: source.to_string(),
|
||||
executable: node_executable,
|
||||
pre_args: vec![entrypoint.display().to_string()],
|
||||
display_path: root_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build staged runtime - prefers binary, falls back to Node.js for legacy support
|
||||
fn build_staged_runtime(source: &str, root_dir: PathBuf) -> Option<ZclawRuntime> {
|
||||
// First, try to find the binary directly
|
||||
if let Some(runtime) = build_binary_runtime(source, &root_dir) {
|
||||
return Some(runtime);
|
||||
}
|
||||
|
||||
// Fallback to Node.js-based runtime for backward compatibility
|
||||
build_staged_runtime_legacy(source, root_dir)
|
||||
}
|
||||
|
||||
fn push_staged_runtime_candidate(candidates: &mut Vec<ZclawRuntime>, source: &str, root_dir: PathBuf) {
|
||||
if candidates.iter().any(|candidate| candidate.display_path == root_dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(runtime) = build_staged_runtime(source, root_dir) {
|
||||
candidates.push(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
fn bundled_runtime_candidates(app: &AppHandle) -> Vec<ZclawRuntime> {
|
||||
let mut candidates = Vec::new();
|
||||
let sidecar_name = zclaw_sidecar_filename();
|
||||
let plain_name = zclaw_plain_filename();
|
||||
let platform_names = get_platform_binary_names();
|
||||
|
||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||
// Primary: zclaw-runtime directory (contains binary + manifest)
|
||||
push_staged_runtime_candidate(
|
||||
&mut candidates,
|
||||
"bundled",
|
||||
resource_dir.join("zclaw-runtime"),
|
||||
);
|
||||
|
||||
// Alternative: binaries directory
|
||||
for name in &platform_names {
|
||||
push_runtime_candidate(
|
||||
&mut candidates,
|
||||
"bundled",
|
||||
resource_dir.join("binaries").join(name),
|
||||
);
|
||||
}
|
||||
|
||||
// Alternative: root level binaries
|
||||
push_runtime_candidate(&mut candidates, "bundled", resource_dir.join(&plain_name));
|
||||
push_runtime_candidate(&mut candidates, "bundled", resource_dir.join(&sidecar_name));
|
||||
}
|
||||
|
||||
if let Ok(current_exe) = std::env::current_exe() {
|
||||
if let Some(exe_dir) = current_exe.parent() {
|
||||
// Windows NSIS installer location
|
||||
push_staged_runtime_candidate(
|
||||
&mut candidates,
|
||||
"bundled",
|
||||
exe_dir.join("resources").join("zclaw-runtime"),
|
||||
);
|
||||
|
||||
// Alternative: binaries next to exe
|
||||
for name in &platform_names {
|
||||
push_runtime_candidate(
|
||||
&mut candidates,
|
||||
"bundled",
|
||||
exe_dir.join("binaries").join(name),
|
||||
);
|
||||
}
|
||||
|
||||
push_runtime_candidate(&mut candidates, "bundled", exe_dir.join(&plain_name));
|
||||
push_runtime_candidate(&mut candidates, "bundled", exe_dir.join(&sidecar_name));
|
||||
}
|
||||
}
|
||||
|
||||
// Development mode
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
push_staged_runtime_candidate(
|
||||
&mut candidates,
|
||||
"development",
|
||||
manifest_dir.join("resources").join("zclaw-runtime"),
|
||||
);
|
||||
|
||||
for name in &platform_names {
|
||||
push_runtime_candidate(
|
||||
&mut candidates,
|
||||
"development",
|
||||
manifest_dir.join("binaries").join(name),
|
||||
);
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
/// Resolve ZCLAW runtime location
|
||||
/// Priority: ZCLAW_BIN env > bundled > system PATH
|
||||
pub fn resolve_zclaw_runtime(app: &AppHandle) -> ZclawRuntime {
|
||||
if let Ok(override_path) = std::env::var("ZCLAW_BIN") {
|
||||
let override_path = PathBuf::from(override_path);
|
||||
if override_path.is_dir() {
|
||||
if let Some(runtime) = build_staged_runtime("override", override_path.clone()) {
|
||||
return runtime;
|
||||
}
|
||||
}
|
||||
|
||||
return ZclawRuntime {
|
||||
source: "override".to_string(),
|
||||
display_path: override_path.clone(),
|
||||
executable: override_path,
|
||||
pre_args: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(runtime) = bundled_runtime_candidates(app)
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.executable.is_file())
|
||||
{
|
||||
return runtime;
|
||||
}
|
||||
|
||||
ZclawRuntime {
|
||||
source: "system".to_string(),
|
||||
display_path: PathBuf::from("zclaw"),
|
||||
executable: PathBuf::from("zclaw"),
|
||||
pre_args: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve ZCLAW config path (TOML format)
|
||||
/// Priority: ZCLAW_HOME env > ~/.zclaw/
|
||||
pub fn resolve_zclaw_config_path() -> Option<PathBuf> {
|
||||
if let Ok(value) = std::env::var("ZCLAW_HOME") {
|
||||
return Some(PathBuf::from(value).join("zclaw.toml"));
|
||||
}
|
||||
|
||||
if let Ok(value) = std::env::var("HOME") {
|
||||
return Some(PathBuf::from(value).join(".zclaw").join("zclaw.toml"));
|
||||
}
|
||||
|
||||
if let Ok(value) = std::env::var("USERPROFILE") {
|
||||
return Some(PathBuf::from(value).join(".zclaw").join("zclaw.toml"));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user