feat(openfang): add bundled OpenFang runtime support

- Add prepare-openfang-runtime.mjs script for cross-platform binary download
- Update lib.rs to support binary runtime (fallback to Node.js for legacy)
- Add openfang.cmd/sh launcher scripts
- Update runtime-manifest.json for binary-based runtime
- Add README documentation for bundled runtime architecture

OpenFang binary is downloaded during build, supporting:
- Windows x64/ARM64 (.zip)
- macOS Intel/Apple Silicon (.tar.gz)
- Linux x64/ARM64 (.tar.gz)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-13 18:03:43 +08:00
parent cfb06d7209
commit 4eb164764a
10 changed files with 1384 additions and 12 deletions

View File

@@ -1,14 +1,832 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-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)
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 greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
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)
}
// ============================================================================
// 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() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.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,
// 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
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}