Phase 2 - Core Engines: - Heartbeat Engine: Periodic proactive checks with quiet hours support - Context Compactor: Token estimation and message summarization - CJK character handling (1.5 tokens per char) - Rule-based summary generation Phase 3 - Advanced Features: - Reflection Engine: Pattern analysis and improvement suggestions - Agent Identity: SOUL.md/AGENTS.md/USER.md management - Proposal-based changes (requires user approval) - Snapshot history for rollback All modules include: - Tauri commands for frontend integration - Unit tests - Re-exported types via mod.rs Reference: docs/plans/INTELLIGENCE-LAYER-MIGRATION.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1445 lines
48 KiB
Rust
1445 lines
48 KiB
Rust
// OpenFang Kernel integration for ZClaw desktop app
|
||
// Supports OpenFang Kernel (successor to OpenClaw Gateway)
|
||
// - Port: 4200 (was 18789)
|
||
// - Binary: openfang (was openclaw)
|
||
// - Config: ~/.openfang/openfang.toml (was ~/.openclaw/openclaw.json)
|
||
|
||
// Viking CLI sidecar module for local memory operations
|
||
mod viking_commands;
|
||
mod viking_server;
|
||
|
||
// Memory extraction and context building modules (supplement CLI)
|
||
mod memory;
|
||
mod llm;
|
||
|
||
// Browser automation module (Fantoccini-based Browser Hand)
|
||
mod browser;
|
||
|
||
// Secure storage module for OS keyring/keychain
|
||
mod secure_storage;
|
||
|
||
// Memory commands for persistent storage
|
||
mod memory_commands;
|
||
|
||
// Intelligence Layer (migrated from frontend lib/)
|
||
mod intelligence;
|
||
|
||
use serde::Serialize;
|
||
use serde_json::{json, Value};
|
||
use std::fs;
|
||
use std::net::{TcpStream, ToSocketAddrs};
|
||
use std::path::PathBuf;
|
||
use std::process::Command;
|
||
use std::thread;
|
||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||
use tauri::{AppHandle, Manager};
|
||
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct LocalGatewayStatus {
|
||
supported: bool,
|
||
cli_available: bool,
|
||
runtime_source: Option<String>,
|
||
runtime_path: Option<String>,
|
||
service_label: Option<String>,
|
||
service_loaded: bool,
|
||
service_status: Option<String>,
|
||
config_ok: bool,
|
||
port: Option<u16>,
|
||
port_status: Option<String>,
|
||
probe_url: Option<String>,
|
||
listener_pids: Vec<u32>,
|
||
error: Option<String>,
|
||
raw: Value,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct LocalGatewayAuth {
|
||
config_path: Option<String>,
|
||
gateway_token: Option<String>,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct LocalGatewayPrepareResult {
|
||
config_path: Option<String>,
|
||
origins_updated: bool,
|
||
gateway_restarted: bool,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct LocalGatewayPairingApprovalResult {
|
||
approved: bool,
|
||
request_id: Option<String>,
|
||
device_id: Option<String>,
|
||
}
|
||
|
||
struct OpenFangRuntime {
|
||
source: String,
|
||
executable: PathBuf,
|
||
pre_args: Vec<String>,
|
||
display_path: PathBuf,
|
||
}
|
||
|
||
struct OpenFangCommandOutput {
|
||
stdout: String,
|
||
runtime: OpenFangRuntime,
|
||
}
|
||
|
||
/// Default OpenFang Kernel port
|
||
const OPENFANG_DEFAULT_PORT: u16 = 4200;
|
||
|
||
const TAURI_ALLOWED_ORIGINS: [&str; 2] = ["http://tauri.localhost", "tauri://localhost"];
|
||
|
||
fn command_error(runtime: &OpenFangRuntime, error: std::io::Error) -> String {
|
||
if error.kind() == std::io::ErrorKind::NotFound {
|
||
match runtime.source.as_str() {
|
||
"bundled" => format!(
|
||
"未找到 ZCLAW 内置 OpenFang 运行时:{}",
|
||
runtime.display_path.display()
|
||
),
|
||
"development" => format!(
|
||
"未找到开发态 OpenFang 运行时:{}",
|
||
runtime.display_path.display()
|
||
),
|
||
"override" => format!(
|
||
"未找到 ZCLAW_OPENFANG_BIN 指定的 OpenFang 运行时:{}",
|
||
runtime.display_path.display()
|
||
),
|
||
_ => "未找到 OpenFang 运行时。请重新安装 ZCLAW,或在开发环境中安装 OpenFang CLI。"
|
||
.to_string(),
|
||
}
|
||
} else {
|
||
format!("运行 OpenFang 失败: {error}")
|
||
}
|
||
}
|
||
|
||
fn runtime_path_string(runtime: &OpenFangRuntime) -> String {
|
||
runtime.display_path.display().to_string()
|
||
}
|
||
|
||
fn binary_extension() -> &'static str {
|
||
if cfg!(target_os = "windows") {
|
||
".exe"
|
||
} else {
|
||
""
|
||
}
|
||
}
|
||
|
||
fn openfang_sidecar_filename() -> String {
|
||
format!("openfang-{}{}", env!("TARGET"), binary_extension())
|
||
}
|
||
|
||
fn openfang_plain_filename() -> String {
|
||
format!("openfang{}", binary_extension())
|
||
}
|
||
|
||
fn push_runtime_candidate(candidates: &mut Vec<OpenFangRuntime>, source: &str, executable: PathBuf) {
|
||
if candidates.iter().any(|candidate| candidate.display_path == executable) {
|
||
return;
|
||
}
|
||
|
||
candidates.push(OpenFangRuntime {
|
||
source: source.to_string(),
|
||
display_path: executable.clone(),
|
||
executable,
|
||
pre_args: Vec::new(),
|
||
});
|
||
}
|
||
|
||
/// Build binary runtime (OpenFang is a single binary, not npm package)
|
||
fn build_binary_runtime(source: &str, root_dir: &PathBuf) -> Option<OpenFangRuntime> {
|
||
// 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(OpenFangRuntime {
|
||
source: source.to_string(),
|
||
executable: binary_path.clone(),
|
||
pre_args: Vec::new(),
|
||
display_path: binary_path,
|
||
});
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// Get platform-specific binary names for OpenFang
|
||
fn get_platform_binary_names() -> Vec<String> {
|
||
let mut names = Vec::new();
|
||
|
||
if cfg!(target_os = "windows") {
|
||
names.push("openfang.exe".to_string());
|
||
names.push(format!("openfang-{}.exe", env!("TARGET")));
|
||
} else if cfg!(target_os = "macos") {
|
||
if cfg!(target_arch = "aarch64") {
|
||
names.push("openfang-aarch64-apple-darwin".to_string());
|
||
} else {
|
||
names.push("openfang-x86_64-apple-darwin".to_string());
|
||
}
|
||
names.push(format!("openfang-{}", env!("TARGET")));
|
||
names.push("openfang".to_string());
|
||
} else {
|
||
// Linux
|
||
if cfg!(target_arch = "aarch64") {
|
||
names.push("openfang-aarch64-unknown-linux-gnu".to_string());
|
||
} else {
|
||
names.push("openfang-x86_64-unknown-linux-gnu".to_string());
|
||
}
|
||
names.push(format!("openfang-{}", env!("TARGET")));
|
||
names.push("openfang".to_string());
|
||
}
|
||
|
||
names
|
||
}
|
||
|
||
/// Legacy: Build staged runtime using Node.js (for backward compatibility)
|
||
#[allow(dead_code)]
|
||
fn build_staged_runtime_legacy(source: &str, root_dir: PathBuf) -> Option<OpenFangRuntime> {
|
||
let node_executable = root_dir.join(if cfg!(target_os = "windows") {
|
||
"node.exe"
|
||
} else {
|
||
"node"
|
||
});
|
||
let entrypoint = root_dir
|
||
.join("node_modules")
|
||
.join("openfang")
|
||
.join("openfang.mjs");
|
||
|
||
if !node_executable.is_file() || !entrypoint.is_file() {
|
||
return None;
|
||
}
|
||
|
||
Some(OpenFangRuntime {
|
||
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<OpenFangRuntime> {
|
||
// 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<OpenFangRuntime>, 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<OpenFangRuntime> {
|
||
let mut candidates = Vec::new();
|
||
let sidecar_name = openfang_sidecar_filename();
|
||
let plain_name = openfang_plain_filename();
|
||
let platform_names = get_platform_binary_names();
|
||
|
||
if let Ok(resource_dir) = app.path().resource_dir() {
|
||
// Primary: openfang-runtime directory (contains binary + manifest)
|
||
push_staged_runtime_candidate(
|
||
&mut candidates,
|
||
"bundled",
|
||
resource_dir.join("openfang-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("openfang-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("openfang-runtime"),
|
||
);
|
||
|
||
for name in &platform_names {
|
||
push_runtime_candidate(
|
||
&mut candidates,
|
||
"development",
|
||
manifest_dir.join("binaries").join(name),
|
||
);
|
||
}
|
||
|
||
candidates
|
||
}
|
||
|
||
/// Resolve OpenFang runtime location
|
||
/// Priority: ZCLAW_OPENFANG_BIN env > bundled > system PATH
|
||
fn resolve_openfang_runtime(app: &AppHandle) -> OpenFangRuntime {
|
||
if let Ok(override_path) = std::env::var("ZCLAW_OPENFANG_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 OpenFangRuntime {
|
||
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;
|
||
}
|
||
|
||
OpenFangRuntime {
|
||
source: "system".to_string(),
|
||
display_path: PathBuf::from("openfang"),
|
||
executable: PathBuf::from("openfang"),
|
||
pre_args: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Resolve OpenFang config path (TOML format)
|
||
/// Priority: OPENFANG_HOME env > ~/.openfang/
|
||
fn resolve_openfang_config_path() -> Option<PathBuf> {
|
||
if let Ok(value) = std::env::var("OPENFANG_HOME") {
|
||
return Some(PathBuf::from(value).join("openfang.toml"));
|
||
}
|
||
|
||
if let Ok(value) = std::env::var("HOME") {
|
||
return Some(PathBuf::from(value).join(".openfang").join("openfang.toml"));
|
||
}
|
||
|
||
if let Ok(value) = std::env::var("USERPROFILE") {
|
||
return Some(PathBuf::from(value).join(".openfang").join("openfang.toml"));
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
/// Parse TOML config and extract gateway token
|
||
fn read_local_gateway_auth() -> Result<LocalGatewayAuth, String> {
|
||
let config_path = resolve_openfang_config_path()
|
||
.ok_or_else(|| "未找到 OpenFang 配置目录。".to_string())?;
|
||
let config_text = fs::read_to_string(&config_path)
|
||
.map_err(|error| format!("读取 OpenFang 配置失败: {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 OpenFang 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, format!("[gateway.controlUi]"));
|
||
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)
|
||
}
|
||
|
||
fn ensure_local_gateway_ready_for_tauri(app: &AppHandle) -> Result<LocalGatewayPrepareResult, String> {
|
||
let config_path = resolve_openfang_config_path()
|
||
.ok_or_else(|| "未找到 OpenFang 配置目录。".to_string())?;
|
||
let config_text = fs::read_to_string(&config_path)
|
||
.map_err(|error| format!("读取 OpenFang 配置失败: {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!("写入 OpenFang 配置失败: {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_openfang(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,
|
||
})
|
||
}
|
||
|
||
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_openfang(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 OpenFang 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_openfang(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()),
|
||
})
|
||
}
|
||
|
||
fn run_openfang(app: &AppHandle, args: &[&str]) -> Result<OpenFangCommandOutput, String> {
|
||
let runtime = resolve_openfang_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(OpenFangCommandOutput {
|
||
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!("OpenFang {:?} 执行失败: {}", args, output.status))
|
||
} else {
|
||
Err(message)
|
||
}
|
||
}
|
||
}
|
||
|
||
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())
|
||
}
|
||
|
||
fn unavailable_status(error: String, runtime: Option<&OpenFangRuntime>) -> 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!({}),
|
||
}
|
||
}
|
||
|
||
fn parse_gateway_status(raw: Value, runtime: &OpenFangRuntime) -> 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(OPENFANG_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,
|
||
}
|
||
}
|
||
|
||
fn read_gateway_status(app: &AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
match run_openfang(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_openfang_runtime(app);
|
||
Ok(unavailable_status(error, Some(&runtime)))
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Tauri Commands - OpenFang (with backward-compatible aliases)
|
||
// ============================================================================
|
||
|
||
/// Get OpenFang Kernel status
|
||
#[tauri::command]
|
||
fn openfang_status(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
read_gateway_status(&app)
|
||
}
|
||
|
||
/// Start OpenFang Kernel
|
||
#[tauri::command]
|
||
fn openfang_start(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
ensure_local_gateway_ready_for_tauri(&app)?;
|
||
run_openfang(&app, &["gateway", "start", "--json"])?;
|
||
thread::sleep(Duration::from_millis(800));
|
||
read_gateway_status(&app)
|
||
}
|
||
|
||
/// Stop OpenFang Kernel
|
||
#[tauri::command]
|
||
fn openfang_stop(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
run_openfang(&app, &["gateway", "stop", "--json"])?;
|
||
thread::sleep(Duration::from_millis(800));
|
||
read_gateway_status(&app)
|
||
}
|
||
|
||
/// Restart OpenFang Kernel
|
||
#[tauri::command]
|
||
fn openfang_restart(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
ensure_local_gateway_ready_for_tauri(&app)?;
|
||
run_openfang(&app, &["gateway", "restart", "--json"])?;
|
||
thread::sleep(Duration::from_millis(1200));
|
||
read_gateway_status(&app)
|
||
}
|
||
|
||
/// Get local auth token from OpenFang config
|
||
#[tauri::command]
|
||
fn openfang_local_auth() -> Result<LocalGatewayAuth, String> {
|
||
read_local_gateway_auth()
|
||
}
|
||
|
||
/// Prepare OpenFang for Tauri (update allowed origins)
|
||
#[tauri::command]
|
||
fn openfang_prepare_for_tauri(app: AppHandle) -> Result<LocalGatewayPrepareResult, String> {
|
||
ensure_local_gateway_ready_for_tauri(&app)
|
||
}
|
||
|
||
/// Approve device pairing request
|
||
#[tauri::command]
|
||
fn openfang_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 OpenFang doctor to diagnose issues
|
||
#[tauri::command]
|
||
fn openfang_doctor(app: AppHandle) -> Result<String, String> {
|
||
let result = run_openfang(&app, &["doctor", "--json"])?;
|
||
Ok(result.stdout)
|
||
}
|
||
|
||
// ============================================================================
|
||
// Process Monitoring Commands
|
||
// ============================================================================
|
||
|
||
/// Process information structure
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
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")]
|
||
struct ProcessListResponse {
|
||
processes: Vec<ProcessInfo>,
|
||
total_count: usize,
|
||
runtime_source: Option<String>,
|
||
}
|
||
|
||
/// Process logs response
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct ProcessLogsResponse {
|
||
pid: Option<u32>,
|
||
logs: String,
|
||
lines: usize,
|
||
runtime_source: Option<String>,
|
||
}
|
||
|
||
/// Version information response
|
||
#[derive(Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct VersionResponse {
|
||
version: String,
|
||
commit: Option<String>,
|
||
build_date: Option<String>,
|
||
runtime_source: Option<String>,
|
||
raw: Value,
|
||
}
|
||
|
||
/// List OpenFang processes
|
||
#[tauri::command]
|
||
fn openfang_process_list(app: AppHandle) -> Result<ProcessListResponse, String> {
|
||
let result = run_openfang(&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 OpenFang process logs
|
||
#[tauri::command]
|
||
fn openfang_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_openfang(&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 OpenFang version information
|
||
#[tauri::command]
|
||
fn openfang_version(app: AppHandle) -> Result<VersionResponse, String> {
|
||
let result = run_openfang(&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,
|
||
})
|
||
}
|
||
|
||
// ============================================================================
|
||
// Health Check Commands
|
||
// ============================================================================
|
||
|
||
/// Health status enum
|
||
#[derive(Debug, Clone, Serialize)]
|
||
#[serde(rename_all = "lowercase")]
|
||
enum HealthStatus {
|
||
Healthy,
|
||
Unhealthy,
|
||
Unknown,
|
||
}
|
||
|
||
/// Port check result
|
||
#[derive(Debug, Clone, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct PortCheckResult {
|
||
port: u16,
|
||
accessible: bool,
|
||
latency_ms: Option<u64>,
|
||
error: Option<String>,
|
||
}
|
||
|
||
/// Process health details
|
||
#[derive(Debug, Clone, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct ProcessHealthDetails {
|
||
pid: Option<u32>,
|
||
name: Option<String>,
|
||
status: Option<String>,
|
||
uptime_seconds: Option<u64>,
|
||
cpu_percent: Option<f64>,
|
||
memory_mb: Option<f64>,
|
||
}
|
||
|
||
/// Health check response
|
||
#[derive(Debug, Clone, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
struct HealthCheckResponse {
|
||
status: HealthStatus,
|
||
process: ProcessHealthDetails,
|
||
port_check: PortCheckResult,
|
||
last_check_timestamp: u64,
|
||
checks_performed: Vec<String>,
|
||
issues: Vec<String>,
|
||
runtime_source: Option<String>,
|
||
}
|
||
|
||
/// Check if a TCP port is accessible
|
||
fn check_port_accessibility(host: &str, port: u16, timeout_ms: u64) -> PortCheckResult {
|
||
let addr = format!("{}:{}", host, port);
|
||
|
||
// Resolve the address
|
||
let socket_addr = match addr.to_socket_addrs() {
|
||
Ok(mut addrs) => addrs.next(),
|
||
Err(e) => {
|
||
return PortCheckResult {
|
||
port,
|
||
accessible: false,
|
||
latency_ms: None,
|
||
error: Some(format!("Failed to resolve address: {}", e)),
|
||
};
|
||
}
|
||
};
|
||
|
||
let Some(socket_addr) = socket_addr else {
|
||
return PortCheckResult {
|
||
port,
|
||
accessible: false,
|
||
latency_ms: None,
|
||
error: Some("Failed to resolve address".to_string()),
|
||
};
|
||
};
|
||
|
||
// Try to connect with timeout
|
||
let start = Instant::now();
|
||
|
||
// Use a simple TCP connect with timeout simulation
|
||
let result = TcpStream::connect_timeout(&socket_addr, Duration::from_millis(timeout_ms));
|
||
|
||
match result {
|
||
Ok(_) => {
|
||
let latency = start.elapsed().as_millis() as u64;
|
||
PortCheckResult {
|
||
port,
|
||
accessible: true,
|
||
latency_ms: Some(latency),
|
||
error: None,
|
||
}
|
||
}
|
||
Err(e) => PortCheckResult {
|
||
port,
|
||
accessible: false,
|
||
latency_ms: None,
|
||
error: Some(format!("Connection failed: {}", e)),
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Get process uptime from status command
|
||
fn get_process_uptime(status: &LocalGatewayStatus) -> Option<u64> {
|
||
// Try to extract uptime from raw status data
|
||
status
|
||
.raw
|
||
.get("process")
|
||
.and_then(|p| p.get("uptimeSeconds"))
|
||
.and_then(Value::as_u64)
|
||
}
|
||
|
||
/// Perform comprehensive health check on OpenFang Kernel
|
||
#[tauri::command]
|
||
fn openfang_health_check(
|
||
app: AppHandle,
|
||
port: Option<u16>,
|
||
timeout_ms: Option<u64>,
|
||
) -> Result<HealthCheckResponse, String> {
|
||
let check_port = port.unwrap_or(OPENFANG_DEFAULT_PORT);
|
||
let timeout = timeout_ms.unwrap_or(3000);
|
||
let mut checks_performed = Vec::new();
|
||
let mut issues = Vec::new();
|
||
|
||
// Get current timestamp
|
||
let last_check_timestamp = SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.map(|d| d.as_secs())
|
||
.unwrap_or(0);
|
||
|
||
// 1. Check if OpenFang CLI is available
|
||
let runtime = resolve_openfang_runtime(&app);
|
||
let cli_available = runtime.executable.is_file();
|
||
|
||
if !cli_available {
|
||
return Ok(HealthCheckResponse {
|
||
status: HealthStatus::Unhealthy,
|
||
process: ProcessHealthDetails {
|
||
pid: None,
|
||
name: None,
|
||
status: None,
|
||
uptime_seconds: None,
|
||
cpu_percent: None,
|
||
memory_mb: None,
|
||
},
|
||
port_check: PortCheckResult {
|
||
port: check_port,
|
||
accessible: false,
|
||
latency_ms: None,
|
||
error: Some("OpenFang CLI not available".to_string()),
|
||
},
|
||
last_check_timestamp,
|
||
checks_performed: vec!["cli_availability".to_string()],
|
||
issues: vec![format!(
|
||
"OpenFang runtime not found at: {}",
|
||
runtime.display_path.display()
|
||
)],
|
||
runtime_source: Some(runtime.source),
|
||
});
|
||
}
|
||
checks_performed.push("cli_availability".to_string());
|
||
|
||
// 2. Get gateway status
|
||
let gateway_status = read_gateway_status(&app)?;
|
||
checks_performed.push("gateway_status".to_string());
|
||
|
||
// Check for configuration issues
|
||
if !gateway_status.config_ok {
|
||
issues.push("Gateway configuration has issues".to_string());
|
||
}
|
||
|
||
// 3. Check port accessibility
|
||
let port_check = check_port_accessibility("127.0.0.1", check_port, timeout);
|
||
checks_performed.push("port_accessibility".to_string());
|
||
|
||
if !port_check.accessible {
|
||
issues.push(format!(
|
||
"Port {} is not accessible: {}",
|
||
check_port,
|
||
port_check.error.as_deref().unwrap_or("unknown error")
|
||
));
|
||
}
|
||
|
||
// 4. Extract process information
|
||
let process_health = if !gateway_status.listener_pids.is_empty() {
|
||
// Get the first listener PID
|
||
let pid = gateway_status.listener_pids[0];
|
||
|
||
// Try to get detailed process info from process list
|
||
let process_info = run_openfang(&app, &["process", "list", "--json"])
|
||
.ok()
|
||
.and_then(|result| parse_json_output(&result.stdout).ok())
|
||
.and_then(|json| json.get("processes").and_then(Value::as_array).cloned());
|
||
|
||
let (cpu, memory, uptime) = if let Some(ref processes) = process_info {
|
||
let matching = processes
|
||
.iter()
|
||
.find(|p| p.get("pid").and_then(Value::as_u64) == Some(pid as u64));
|
||
|
||
matching.map_or((None, None, None), |p| {
|
||
(
|
||
p.get("cpuPercent").and_then(Value::as_f64),
|
||
p.get("memoryMb").and_then(Value::as_f64),
|
||
p.get("uptimeSeconds").and_then(Value::as_u64),
|
||
)
|
||
})
|
||
} else {
|
||
(None, None, get_process_uptime(&gateway_status))
|
||
};
|
||
|
||
ProcessHealthDetails {
|
||
pid: Some(pid),
|
||
name: Some("openfang".to_string()),
|
||
status: Some(
|
||
gateway_status
|
||
.service_status
|
||
.clone()
|
||
.unwrap_or_else(|| "running".to_string()),
|
||
),
|
||
uptime_seconds: uptime,
|
||
cpu_percent: cpu,
|
||
memory_mb: memory,
|
||
}
|
||
} else {
|
||
ProcessHealthDetails {
|
||
pid: None,
|
||
name: None,
|
||
status: gateway_status.service_status.clone(),
|
||
uptime_seconds: None,
|
||
cpu_percent: None,
|
||
memory_mb: None,
|
||
}
|
||
};
|
||
|
||
// Check if process is running but no listeners
|
||
if gateway_status.service_status.as_deref() == Some("running")
|
||
&& gateway_status.listener_pids.is_empty()
|
||
{
|
||
issues.push("Service reports running but no listener processes found".to_string());
|
||
}
|
||
|
||
// 5. Determine overall health status
|
||
let status = if !cli_available {
|
||
HealthStatus::Unhealthy
|
||
} else if !port_check.accessible {
|
||
HealthStatus::Unhealthy
|
||
} else if gateway_status.listener_pids.is_empty() {
|
||
HealthStatus::Unhealthy
|
||
} else if !issues.is_empty() {
|
||
// Has some issues but core functionality is working
|
||
HealthStatus::Healthy
|
||
} else {
|
||
HealthStatus::Healthy
|
||
};
|
||
|
||
Ok(HealthCheckResponse {
|
||
status,
|
||
process: process_health,
|
||
port_check,
|
||
last_check_timestamp,
|
||
checks_performed,
|
||
issues,
|
||
runtime_source: Some(runtime.source),
|
||
})
|
||
}
|
||
|
||
/// Quick ping to check if OpenFang is alive (lightweight check)
|
||
#[tauri::command]
|
||
fn openfang_ping(app: AppHandle) -> Result<bool, String> {
|
||
let port_check = check_port_accessibility("127.0.0.1", OPENFANG_DEFAULT_PORT, 1000);
|
||
|
||
if port_check.accessible {
|
||
return Ok(true);
|
||
}
|
||
|
||
// Fallback: check via status command
|
||
match run_openfang(&app, &["gateway", "status", "--json", "--no-probe"]) {
|
||
Ok(result) => {
|
||
if let Ok(status) = parse_json_output(&result.stdout) {
|
||
// Check if there are any listener PIDs
|
||
let has_listeners = status
|
||
.get("port")
|
||
.and_then(|p| p.get("listeners"))
|
||
.and_then(Value::as_array)
|
||
.map(|arr| !arr.is_empty())
|
||
.unwrap_or(false);
|
||
|
||
Ok(has_listeners)
|
||
} else {
|
||
Ok(false)
|
||
}
|
||
}
|
||
Err(_) => Ok(false),
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Backward-compatible aliases (OpenClaw naming)
|
||
// These delegate to OpenFang commands for backward compatibility
|
||
// ============================================================================
|
||
|
||
#[tauri::command]
|
||
fn gateway_status(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
openfang_status(app)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_start(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
openfang_start(app)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_stop(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
openfang_stop(app)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_restart(app: AppHandle) -> Result<LocalGatewayStatus, String> {
|
||
openfang_restart(app)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_local_auth() -> Result<LocalGatewayAuth, String> {
|
||
openfang_local_auth()
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_prepare_for_tauri(app: AppHandle) -> Result<LocalGatewayPrepareResult, String> {
|
||
openfang_prepare_for_tauri(app)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_approve_device_pairing(
|
||
app: AppHandle,
|
||
device_id: String,
|
||
public_key_base64: String,
|
||
url: Option<String>,
|
||
) -> Result<LocalGatewayPairingApprovalResult, String> {
|
||
openfang_approve_device_pairing(app, device_id, public_key_base64, url)
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn gateway_doctor(app: AppHandle) -> Result<String, String> {
|
||
openfang_doctor(app)
|
||
}
|
||
|
||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||
pub fn run() {
|
||
// Initialize browser state
|
||
let browser_state = browser::commands::BrowserState::new();
|
||
|
||
// Initialize memory store state
|
||
let memory_state: memory_commands::MemoryStoreState = std::sync::Arc::new(tokio::sync::Mutex::new(None));
|
||
|
||
// Initialize intelligence layer state
|
||
let heartbeat_state: intelligence::HeartbeatEngineState = std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()));
|
||
let reflection_state: intelligence::ReflectionEngineState = std::sync::Arc::new(tokio::sync::Mutex::new(intelligence::ReflectionEngine::new(None)));
|
||
let identity_state: intelligence::IdentityManagerState = std::sync::Arc::new(tokio::sync::Mutex::new(intelligence::AgentIdentityManager::new()));
|
||
|
||
tauri::Builder::default()
|
||
.plugin(tauri_plugin_opener::init())
|
||
.manage(browser_state)
|
||
.manage(memory_state)
|
||
.manage(heartbeat_state)
|
||
.manage(reflection_state)
|
||
.manage(identity_state)
|
||
.invoke_handler(tauri::generate_handler![
|
||
// OpenFang commands (new naming)
|
||
openfang_status,
|
||
openfang_start,
|
||
openfang_stop,
|
||
openfang_restart,
|
||
openfang_local_auth,
|
||
openfang_prepare_for_tauri,
|
||
openfang_approve_device_pairing,
|
||
openfang_doctor,
|
||
openfang_health_check,
|
||
// Process monitoring commands
|
||
openfang_process_list,
|
||
openfang_process_logs,
|
||
openfang_version,
|
||
// Health check commands
|
||
openfang_health_check,
|
||
openfang_ping,
|
||
// Backward-compatible aliases (OpenClaw naming)
|
||
gateway_status,
|
||
gateway_start,
|
||
gateway_stop,
|
||
gateway_restart,
|
||
gateway_local_auth,
|
||
gateway_prepare_for_tauri,
|
||
gateway_approve_device_pairing,
|
||
gateway_doctor,
|
||
// OpenViking CLI sidecar commands
|
||
viking_commands::viking_status,
|
||
viking_commands::viking_add,
|
||
viking_commands::viking_add_inline,
|
||
viking_commands::viking_find,
|
||
viking_commands::viking_grep,
|
||
viking_commands::viking_ls,
|
||
viking_commands::viking_read,
|
||
viking_commands::viking_remove,
|
||
viking_commands::viking_tree,
|
||
// Viking server management (local deployment)
|
||
viking_server::viking_server_status,
|
||
viking_server::viking_server_start,
|
||
viking_server::viking_server_stop,
|
||
viking_server::viking_server_restart,
|
||
// Memory extraction commands (supplement CLI)
|
||
memory::extractor::extract_session_memories,
|
||
memory::context_builder::estimate_content_tokens,
|
||
// LLM commands (for extraction)
|
||
llm::llm_complete,
|
||
// Browser automation commands (Fantoccini-based Browser Hand)
|
||
browser::commands::browser_create_session,
|
||
browser::commands::browser_close_session,
|
||
browser::commands::browser_list_sessions,
|
||
browser::commands::browser_get_session,
|
||
browser::commands::browser_navigate,
|
||
browser::commands::browser_back,
|
||
browser::commands::browser_forward,
|
||
browser::commands::browser_refresh,
|
||
browser::commands::browser_get_url,
|
||
browser::commands::browser_get_title,
|
||
browser::commands::browser_find_element,
|
||
browser::commands::browser_find_elements,
|
||
browser::commands::browser_click,
|
||
browser::commands::browser_type,
|
||
browser::commands::browser_get_text,
|
||
browser::commands::browser_get_attribute,
|
||
browser::commands::browser_wait_for_element,
|
||
browser::commands::browser_execute_script,
|
||
browser::commands::browser_screenshot,
|
||
browser::commands::browser_element_screenshot,
|
||
browser::commands::browser_get_source,
|
||
browser::commands::browser_scrape_page,
|
||
browser::commands::browser_fill_form,
|
||
// Secure storage commands (OS keyring/keychain)
|
||
secure_storage::secure_store_set,
|
||
secure_storage::secure_store_get,
|
||
secure_storage::secure_store_delete,
|
||
secure_storage::secure_store_is_available,
|
||
// Memory persistence commands (Phase 1 Intelligence Layer Migration)
|
||
memory_commands::memory_init,
|
||
memory_commands::memory_store,
|
||
memory_commands::memory_get,
|
||
memory_commands::memory_search,
|
||
memory_commands::memory_delete,
|
||
memory_commands::memory_delete_all,
|
||
memory_commands::memory_stats,
|
||
memory_commands::memory_export,
|
||
memory_commands::memory_import,
|
||
memory_commands::memory_db_path,
|
||
// Intelligence Layer commands (Phase 2-3)
|
||
// Heartbeat Engine
|
||
intelligence::heartbeat::heartbeat_init,
|
||
intelligence::heartbeat::heartbeat_start,
|
||
intelligence::heartbeat::heartbeat_stop,
|
||
intelligence::heartbeat::heartbeat_tick,
|
||
intelligence::heartbeat::heartbeat_get_config,
|
||
intelligence::heartbeat::heartbeat_update_config,
|
||
intelligence::heartbeat::heartbeat_get_history,
|
||
// Context Compactor
|
||
intelligence::compactor::compactor_estimate_tokens,
|
||
intelligence::compactor::compactor_estimate_messages_tokens,
|
||
intelligence::compactor::compactor_check_threshold,
|
||
intelligence::compactor::compactor_compact,
|
||
// Reflection Engine
|
||
intelligence::reflection::reflection_init,
|
||
intelligence::reflection::reflection_record_conversation,
|
||
intelligence::reflection::reflection_should_reflect,
|
||
intelligence::reflection::reflection_reflect,
|
||
intelligence::reflection::reflection_get_history,
|
||
intelligence::reflection::reflection_get_state,
|
||
// Agent Identity Manager
|
||
intelligence::identity::identity_get,
|
||
intelligence::identity::identity_get_file,
|
||
intelligence::identity::identity_build_prompt,
|
||
intelligence::identity::identity_update_user_profile,
|
||
intelligence::identity::identity_append_user_profile,
|
||
intelligence::identity::identity_propose_change,
|
||
intelligence::identity::identity_approve_proposal,
|
||
intelligence::identity::identity_reject_proposal,
|
||
intelligence::identity::identity_get_pending_proposals,
|
||
intelligence::identity::identity_update_file,
|
||
intelligence::identity::identity_get_snapshots,
|
||
intelligence::identity::identity_restore_snapshot,
|
||
intelligence::identity::identity_list_agents,
|
||
intelligence::identity::identity_delete_agent
|
||
])
|
||
.run(tauri::generate_context!())
|
||
.expect("error while running tauri application");
|
||
}
|