Files
zclaw_openfang/desktop/src-tauri/src/lib.rs
iven ef8f5cdb43 feat(intelligence): complete Phase 2-3 migration to Rust
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>
2026-03-21 00:52:44 +08:00

1445 lines
48 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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");
}