Files
zclaw_openfang/desktop/src-tauri/src/lib.rs
iven f4efc823e2 refactor(types): comprehensive TypeScript type system improvements
Major type system refactoring and error fixes across the codebase:

**Type System Improvements:**
- Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types
- Added GatewayPong interface for WebSocket pong responses
- Added index signature to MemorySearchOptions for Record compatibility
- Fixed RawApproval interface with hand_name, run_id properties

**Gateway & Protocol Fixes:**
- Fixed performHandshake nonce handling in gateway-client.ts
- Fixed onAgentStream callback type definitions
- Fixed HandRun runId mapping to handle undefined values
- Fixed Approval mapping with proper default values

**Memory System Fixes:**
- Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount)
- Replaced getByAgent with getAll method in vector-memory.ts
- Fixed MemorySearchOptions type compatibility

**Component Fixes:**
- Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent)
- Fixed SkillMarket suggestSkills async call arguments
- Fixed message-virtualization useRef generic type
- Fixed session-persistence messageCount type conversion

**Code Cleanup:**
- Removed unused imports and variables across multiple files
- Consolidated StoredError interface (removed duplicate)
- Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts)

**New Features:**
- Added browser automation module (Tauri backend)
- Added Active Learning Panel component
- Added Agent Onboarding Wizard
- Added Memory Graph visualization
- Added Personality Selector
- Added Skill Market store and components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 08:05:07 +08:00

1074 lines
35 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;
use serde::Serialize;
use serde_json::{json, Value};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::thread;
use std::time::Duration;
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,
})
}
// ============================================================================
// 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();
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(browser_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,
// Process monitoring commands
openfang_process_list,
openfang_process_logs,
openfang_version,
// 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
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}