mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
* Refactor core server helpers and enhance REPL dotenv loading - Removed unused functions for extracting namespaces and filtering documents by namespace from `helpers.rs`, streamlining the codebase. - Introduced dotenv loading functionality in `repl.rs`, allowing for environment variable management from a specified `.env` file. - Added utility functions for parsing dotenv values and resolving the dotenv file path, improving configuration handling in the REPL. - Enhanced logging for dotenv loading to provide better visibility into the process and any issues encountered. * Add AI RPC module and enhance core server dispatch functionality - Introduced a new `rpc` module within the `ai` namespace to handle various AI-related commands, including memory file operations and session management. - Updated the core server's dispatch logic to integrate the new AI RPC functionality, improving modularity and maintainability. - Removed outdated memory dispatch implementation and streamlined the overall dispatch structure for better performance and clarity. - Enhanced error handling and logging throughout the new functionalities to improve maintainability and user feedback. * Refactor project structure and enhance RPC functionality - Introduced a new `rpc` module to streamline JSON-RPC handling across various domains, improving code organization and maintainability. - Updated the core server and API modules to utilize the new `rpc` structure, enhancing modularity and reducing code duplication. - Added new models for authentication and socket management, improving the overall functionality of the API. - Removed outdated references to the previous `openhuman` RPC structure, ensuring a cleaner and more efficient codebase. * Update architecture documentation and refactor AI prompt paths - Updated references in architecture documentation to reflect the new directory structure for AI prompts, changing paths from `src/ai/prompts` to `src/openhuman/agent/prompts`. - Enhanced clarity in command documentation by aligning AI-related commands with the updated prompt paths. - Removed obsolete AI module and streamlined memory management references to improve code organization and maintainability. - Introduced new markdown files for agent prompts, establishing a foundation for OpenHuman's AI capabilities. * Update AI prompt paths and configuration references - Changed all references from `src/ai/prompts` to `src/openhuman/agent/prompts` in documentation and code files to reflect the new directory structure. - Updated Tauri configuration to include the new resource paths for AI prompts, ensuring proper access and functionality. - Enhanced the AI configuration commands to align with the updated paths, improving clarity and maintainability across the project. * Enhance core structure and introduce new RPC functionality - Added a new `core` module to centralize shared schemas and contracts for controllers, improving code organization and maintainability. - Introduced `jsonrpc` and `cli` modules within the `core` structure to handle JSON-RPC requests and command-line interactions, enhancing modularity. - Defined a `ControllerSchema` for transport-agnostic function contracts, allowing for consistent handling across RPC and CLI layers. - Established a new `all` module to manage registered controllers and their schemas, streamlining the invocation process. - Updated documentation in `CLAUDE.md` to reflect new controller schema contracts and module organization, improving clarity for developers. * Refactor core server structure and enhance RPC functionality - Removed the `core_server` module and integrated its functionalities into the `core` module, improving code organization and maintainability. - Introduced a new `dispatch` module to handle RPC requests, streamlining the invocation process for various commands. - Updated the CLI to utilize the new core structure, enhancing command handling and modularity. - Added comprehensive logging for RPC interactions, improving visibility and debugging capabilities. - Enhanced error handling across the core server, ensuring consistent feedback for users and developers. * Refactor CLI command handling and enhance JSON-RPC integration - Consolidated CLI command structure by removing the `CoreCli` and directly implementing command functions for `run`, `call`, and `namespace`. - Improved argument parsing for server commands, including port specification and help options, enhancing user experience. - Streamlined the invocation of JSON-RPC methods, ensuring consistent error handling and response formatting across commands. - Introduced a new `run_namespace_command` function to manage namespace-specific operations, improving modularity and clarity in command execution. * Refactor SkillsPanel and TauriCommandsPanel for improved integration handling - Removed unused integration-related functions and state management from SkillsPanel, simplifying the component's logic. - Updated TauriCommandsPanel to eliminate integration name input and associated commands, streamlining the user interface. - Introduced a new local AI memory management module to handle session and memory operations, enhancing overall functionality. - Refactored core RPC client to integrate local AI method dispatching, improving command handling consistency across the application. - Cleaned up tauriCommands utility functions to align with the new command structure, enhancing maintainability. * Refactor TauriCommandsPanel to streamline command handling - Removed the `openhumanModelsRefresh` function and replaced its usage with `openhumanDoctorReport`, simplifying the command logic. - Eliminated unused model refresh buttons from the UI, enhancing the user interface and reducing clutter. - Updated related utility functions in `tauriCommands.ts` to reflect the removal of model refresh functionality, improving maintainability. * Refactor JSON-RPC server integration and remove legacy server module - Updated the CLI to invoke the JSON-RPC server directly, enhancing command execution flow. - Introduced a new HTTP router in the `jsonrpc` module, consolidating route handling for health checks and RPC requests. - Removed the deprecated `server` module, streamlining the codebase and improving maintainability. - Adjusted tests to reflect the new routing structure, ensuring continued functionality and integration. * Remove submodule and update CLAUDE.md documentation - Deleted the `.gitmodules` file and removed the `skills` submodule, simplifying the project structure. - Updated the description in `CLAUDE.md` to reflect a broader focus on community assistance rather than just crypto, enhancing clarity. - Added sections on coding philosophy and controller migration checklist to improve developer guidance and maintainability. * Refactor controller registration and enhance schema management - Introduced a centralized registry for registered controllers, improving the organization and validation of controller schemas. - Updated the `all_registered_controllers` and `all_controller_schemas` functions across various modules to streamline controller management. - Added comprehensive validation for controller registration to ensure consistency and prevent duplicate entries. - Enhanced the CLI and JSON-RPC integration to utilize the new registry structure, improving command handling and modularity. - Updated documentation in `CLAUDE.md` to reflect changes in the skills registry and controller management processes, enhancing clarity for contributors. * Enhance autocomplete, config, and credentials modules with new schemas and controller registrations - Added support for autocomplete, config, and credentials functionalities by introducing new schemas and registered controllers. - Updated the `build_registered_controllers` and `build_declared_controller_schemas` functions to include new entries for autocomplete, config, and credentials. - Implemented new JSON-RPC tests for autocomplete and config methods, ensuring proper validation and error handling. - Refactored CLI tests to include checks for new commands related to autocomplete and configuration management, improving test coverage and reliability. - Introduced new modules for schemas in autocomplete, config, and credentials, enhancing code organization and maintainability. * Add local AI and migration modules with schemas and controller registrations - Introduced local AI and migration functionalities by adding new schemas and registered controllers. - Updated the `build_registered_controllers` and `build_declared_controller_schemas` functions to include entries for local AI and migration. - Implemented JSON-RPC tests for local AI and migration methods, ensuring proper validation and error handling. - Enhanced CLI tests to verify new commands related to local AI and migration, improving test coverage and reliability. - Organized code by creating dedicated modules for schemas in local AI and migration, enhancing maintainability. * Refactor controller schemas for config and auth modules - Updated controller schemas for config and auth functionalities, aligning namespaces and function names for consistency. - Changed function names in the JSON-RPC tests to reflect the new schema structure, ensuring proper invocation. - Enhanced CLI tests to verify updated commands related to config and auth, improving test coverage and reliability. - Organized code by consolidating related functionalities under appropriate namespaces, enhancing maintainability. * Add agent and screen intelligence modules with schemas and controller registrations - Introduced agent and screen intelligence functionalities by adding new schemas and registered controllers. - Updated the `build_registered_controllers` and `build_declared_controller_schemas` functions to include entries for agent and screen intelligence. - Created dedicated modules for schemas in agent, screen intelligence, skills, tools, tray, and workspace, enhancing code organization and maintainability. - Implemented initial controller schemas for agent and screen intelligence, providing a foundation for future functionality. - Enhanced the overall structure of the core module to accommodate new integrations, improving modularity and clarity. * Enhance autocomplete and namespace descriptions in core modules - Added a new function `namespace_description` to provide descriptions for various namespaces, improving user guidance in CLI commands. - Updated the CLI help output to include namespace descriptions, enhancing clarity for users. - Introduced a new `core` module for autocomplete functionalities, including various operations and structures related to inline autocomplete. - Refactored the `autocomplete` module to improve organization and maintainability, consolidating related functionalities under appropriate namespaces. - Implemented initial JSON-RPC operations for autocomplete, ensuring a robust interface for managing autocomplete features. * Refactor and clean up code across multiple modules - Removed unnecessary whitespace in `TauriCommandsPanel.tsx`, improving code readability. - Cleaned up imports and reorganized code structure in `localCoreAiMemory.ts`, enhancing maintainability. - Streamlined module imports in `mod.rs` files across various directories, ensuring consistency and clarity. - Deleted the obsolete `rpc.rs` file in the `cron` module, consolidating functionality and reducing clutter. - Updated function definitions in `ops.rs` to improve formatting and readability, enhancing overall code quality. * Refactor session management and enhance module organization - Changed `sessionIndex` from a mutable variable to a constant in `localCoreAiMemory.ts`, improving code clarity and immutability. - Introduced new `ops.rs` files in the `approval`, `providers`, `skills`, and `quickjs_libs` modules, consolidating related functionalities and enhancing code organization. - Streamlined module imports in `mod.rs` files across various directories, ensuring consistency and clarity in module structure. - Removed obsolete code and unnecessary comments, improving overall code readability and maintainability. * Refactor configuration schema organization and module structure - Moved the configuration schema definitions from `mod.rs` to a new `types.rs` file, enhancing modularity and clarity. - Updated module imports across various files to reflect the new structure, ensuring consistency in the codebase. - Cleaned up obsolete code and comments, improving overall readability and maintainability. * Remove obsolete configuration schema file and its associated modules - Deleted the `types.rs` file from the configuration schema, consolidating the codebase and removing unused components. - This change enhances maintainability by eliminating redundant code and streamlining the overall structure of the configuration management. * Fix config schema module exports * Add configuration schema types and enhance module exports - Introduced a new `types.rs` file to define the top-level configuration structure for `config.toml`, improving modularity and clarity. - Updated `mod.rs` to re-export all public types and configurations, ensuring a streamlined interface for the configuration schema. - Enhanced the organization of configuration components, making it easier to manage and extend in the future. * Update import path for AuthProfile and AuthProfileKind in tests module - Changed the import statement for `AuthProfile` and `AuthProfileKind` to use the correct path, ensuring proper module resolution and consistency in the codebase. * Refactor import statements in test files for consistency - Cleaned up import statements across multiple test files by removing unnecessary components and ensuring uniformity in module imports. - This change enhances code readability and maintainability by streamlining the import structure. * Add agent chat and REPL session handling with schemas - Introduced new schemas for agent chat and REPL session management, enhancing the functionality of the agent module. - Implemented handlers for chat and REPL session operations, allowing for more interactive and persistent user sessions. - Updated the memory store to include category handling for memory entries, improving data organization and retrieval. - Refactored memory query methods to support ranked results and category storage, enhancing the memory management capabilities. - Improved error handling in memory recall tools to ensure non-empty parameters, increasing robustness and user feedback. * Refactor JSON-RPC method names for consistency and clarity - Updated JSON-RPC method names in tests to follow a consistent naming convention, improving readability and maintainability. - Adjusted parameter names in the JSON payload to align with the updated method names, ensuring proper functionality and clarity in the API interactions. - Enhanced overall code organization by streamlining method calls in the test suite.
663 lines
23 KiB
Rust
663 lines
23 KiB
Rust
//! OpenHuman Desktop Application
|
|
//!
|
|
//! This is the Rust backend for the cross-platform crypto community platform.
|
|
//! It provides deep link handling, core process RPC relay, window management,
|
|
//! and AI configuration helpers.
|
|
|
|
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
|
compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supported.");
|
|
|
|
mod commands;
|
|
mod core_process;
|
|
mod core_rpc;
|
|
mod utils;
|
|
|
|
use aes_gcm::aead::{Aead, KeyInit};
|
|
use aes_gcm::{Aes256Gcm, Key, Nonce};
|
|
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
|
use commands::*;
|
|
use rand::TryRngCore;
|
|
use serde::Serialize;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use tauri::{AppHandle, Emitter, Manager, RunEvent};
|
|
use tokio::{
|
|
fs,
|
|
time::{interval, Duration},
|
|
};
|
|
|
|
#[cfg(any(windows, target_os = "linux"))]
|
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
|
|
/// Demo command - can be removed in production
|
|
#[tauri::command]
|
|
fn greet(name: &str) -> String {
|
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
|
}
|
|
|
|
fn derive_key(password: &str) -> [u8; 32] {
|
|
use sha2::{Digest, Sha256};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(password.as_bytes());
|
|
let hash = hasher.finalize();
|
|
let mut key = [0u8; 32];
|
|
key.copy_from_slice(&hash[..32]);
|
|
key
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct AIPreview {
|
|
soul: AIPreviewSoul,
|
|
tools: AIPreviewTools,
|
|
metadata: AIPreviewMetadata,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct AIPreviewSoul {
|
|
raw: String,
|
|
name: String,
|
|
description: String,
|
|
personality_preview: Vec<String>,
|
|
safety_rules_preview: Vec<String>,
|
|
loaded_at: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct AIPreviewTools {
|
|
raw: String,
|
|
total_tools: usize,
|
|
active_skills: usize,
|
|
skills_preview: Vec<String>,
|
|
loaded_at: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct AIPreviewMetadata {
|
|
loaded_at: i64,
|
|
loading_duration: i64,
|
|
has_fallbacks: bool,
|
|
sources: AIPreviewSources,
|
|
errors: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct AIPreviewSources {
|
|
soul: String,
|
|
tools: String,
|
|
}
|
|
|
|
fn now_ms() -> i64 {
|
|
chrono::Utc::now().timestamp_millis()
|
|
}
|
|
|
|
fn extract_section(raw: &str, heading: &str) -> String {
|
|
let marker = format!("## {heading}");
|
|
let Some(start) = raw.find(&marker) else {
|
|
return String::new();
|
|
};
|
|
let body = &raw[start + marker.len()..];
|
|
if let Some(next_idx) = body.find("\n## ") {
|
|
body[..next_idx].trim().to_string()
|
|
} else {
|
|
body.trim().to_string()
|
|
}
|
|
}
|
|
|
|
fn parse_soul_preview(raw: String, loaded_at: i64) -> AIPreviewSoul {
|
|
let name = raw
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("# ").map(|s| s.trim().to_string()))
|
|
.unwrap_or_else(|| "OpenHuman".to_string());
|
|
|
|
let description = raw
|
|
.lines()
|
|
.map(str::trim)
|
|
.find(|line| !line.is_empty() && !line.starts_with('#'))
|
|
.unwrap_or("AI assistant")
|
|
.to_string();
|
|
|
|
let personality_preview = extract_section(&raw, "Personality")
|
|
.lines()
|
|
.filter_map(|line| line.trim().strip_prefix("- **"))
|
|
.filter_map(|line| {
|
|
let mut parts = line.splitn(2, "**:");
|
|
let trait_name = parts.next()?.trim();
|
|
let detail = parts.next().unwrap_or("").trim();
|
|
Some(format!("{trait_name}: {detail}"))
|
|
})
|
|
.take(3)
|
|
.collect::<Vec<_>>();
|
|
|
|
let safety_rules_preview = extract_section(&raw, "Safety Rules")
|
|
.lines()
|
|
.filter_map(|line| {
|
|
let trimmed = line.trim();
|
|
let dot_idx = trimmed.find('.')?;
|
|
let (prefix, rest) = trimmed.split_at(dot_idx);
|
|
if prefix.chars().all(|c| c.is_ascii_digit()) {
|
|
Some(rest.trim_start_matches('.').trim().to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.take(3)
|
|
.collect::<Vec<_>>();
|
|
|
|
AIPreviewSoul {
|
|
raw,
|
|
name,
|
|
description,
|
|
personality_preview,
|
|
safety_rules_preview,
|
|
loaded_at,
|
|
}
|
|
}
|
|
|
|
fn parse_tools_preview(raw: String, loaded_at: i64) -> AIPreviewTools {
|
|
let mut current_skill = "General".to_string();
|
|
let mut skill_counts: HashMap<String, usize> = HashMap::new();
|
|
let mut total_tools = 0usize;
|
|
|
|
for line in raw.lines() {
|
|
let trimmed = line.trim();
|
|
if let Some(title) = trimmed.strip_prefix("### ") {
|
|
if let Some(skill_title) = title.strip_suffix(" Tools") {
|
|
current_skill = skill_title.trim().to_string();
|
|
skill_counts.entry(current_skill.clone()).or_insert(0);
|
|
}
|
|
continue;
|
|
}
|
|
if trimmed.starts_with("#### ") {
|
|
total_tools += 1;
|
|
*skill_counts.entry(current_skill.clone()).or_insert(0) += 1;
|
|
}
|
|
}
|
|
|
|
let mut skills = skill_counts.into_iter().collect::<Vec<_>>();
|
|
let active_skills = skills.len();
|
|
skills.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
|
let skills_preview = skills
|
|
.into_iter()
|
|
.take(6)
|
|
.map(|(name, count)| format!("{name} ({count})"))
|
|
.collect::<Vec<_>>();
|
|
|
|
AIPreviewTools {
|
|
raw,
|
|
total_tools,
|
|
active_skills,
|
|
skills_preview,
|
|
loaded_at,
|
|
}
|
|
}
|
|
|
|
fn resolve_ai_directory(app: &tauri::AppHandle) -> Option<(PathBuf, &'static str)> {
|
|
if let Ok(resource_dir) = app.path().resource_dir() {
|
|
if let Some(ai_dir) = utils::dev_paths::bundled_openclaw_prompts_dir(&resource_dir) {
|
|
return Some((ai_dir, "bundled"));
|
|
}
|
|
}
|
|
|
|
if let Ok(cwd) = std::env::current_dir() {
|
|
if let Some(path) = utils::dev_paths::repo_ai_prompts_dir(&cwd) {
|
|
return Some((path, "bundled"));
|
|
}
|
|
|
|
let fallback = cwd.join("ai");
|
|
if fallback.is_dir() {
|
|
return Some((fallback, "bundled"));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn build_ai_preview(app: &tauri::AppHandle) -> AIPreview {
|
|
let started = now_ms();
|
|
let loaded_at = now_ms();
|
|
let mut errors = Vec::new();
|
|
let mut soul_raw = String::new();
|
|
let mut tools_raw = String::new();
|
|
let mut source = "bundled".to_string();
|
|
|
|
if let Some((ai_dir, resolved_source)) = resolve_ai_directory(app) {
|
|
source = resolved_source.to_string();
|
|
let soul_path = ai_dir.join("SOUL.md");
|
|
let tools_path = ai_dir.join("TOOLS.md");
|
|
soul_raw = std::fs::read_to_string(&soul_path).unwrap_or_else(|e| {
|
|
errors.push(format!("Failed to read SOUL.md: {e}"));
|
|
String::new()
|
|
});
|
|
tools_raw = std::fs::read_to_string(&tools_path).unwrap_or_else(|e| {
|
|
errors.push(format!("Failed to read TOOLS.md: {e}"));
|
|
String::new()
|
|
});
|
|
} else {
|
|
errors.push("AI config directory not found".to_string());
|
|
}
|
|
|
|
let soul = parse_soul_preview(soul_raw, loaded_at);
|
|
let tools = parse_tools_preview(tools_raw, loaded_at);
|
|
let done = now_ms();
|
|
|
|
AIPreview {
|
|
soul,
|
|
tools,
|
|
metadata: AIPreviewMetadata {
|
|
loaded_at: done,
|
|
loading_duration: done - started,
|
|
has_fallbacks: false,
|
|
sources: AIPreviewSources {
|
|
soul: source.clone(),
|
|
tools: source,
|
|
},
|
|
errors,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn ai_get_config(app: tauri::AppHandle) -> Result<AIPreview, String> {
|
|
Ok(build_ai_preview(&app))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn ai_refresh_config(app: tauri::AppHandle) -> Result<AIPreview, String> {
|
|
Ok(build_ai_preview(&app))
|
|
}
|
|
|
|
/// Write AI configuration files to `src/openhuman/agent/prompts` in the repo (dev resolution from cwd).
|
|
#[tauri::command]
|
|
async fn write_ai_config_file(filename: String, content: String) -> Result<bool, String> {
|
|
use std::env;
|
|
|
|
// Determine runtime working directory
|
|
let current_dir =
|
|
env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
|
|
|
|
// Ensure filename is safe (only allow .md files)
|
|
if !filename.ends_with(".md") {
|
|
return Err("Only .md files are allowed".to_string());
|
|
}
|
|
|
|
// Prevent path traversal by checking for dangerous characters
|
|
if filename.contains("..") || filename.contains("/") || filename.contains("\\") {
|
|
return Err("Invalid filename: path traversal not allowed".to_string());
|
|
}
|
|
|
|
let ai_dir = utils::dev_paths::repo_ai_prompts_dir(¤t_dir)
|
|
.unwrap_or_else(|| {
|
|
current_dir
|
|
.join("src")
|
|
.join("openhuman")
|
|
.join("agent")
|
|
.join("prompts")
|
|
});
|
|
let file_path = ai_dir.join(&filename);
|
|
|
|
// Ensure ai directory exists
|
|
std::fs::create_dir_all(&ai_dir).map_err(|e| format!("Failed to create ai directory: {e}"))?;
|
|
|
|
// Write the file
|
|
std::fs::write(&file_path, content)
|
|
.map_err(|e| format!("Failed to write file {}: {e}", filename))?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
fn is_daemon_mode() -> bool {
|
|
std::env::args().any(|arg| arg == "daemon" || arg == "--daemon")
|
|
}
|
|
|
|
/// Watch daemon health file and bridge changes to frontend Tauri events
|
|
async fn watch_daemon_health_file(app_handle: AppHandle, data_dir: PathBuf) {
|
|
let state_file = data_dir.join("daemon_state.json");
|
|
let mut interval = interval(Duration::from_secs(2));
|
|
let mut last_modified: Option<std::time::SystemTime> = None;
|
|
|
|
log::info!(
|
|
"[openhuman] Watching daemon health file: {}",
|
|
state_file.display()
|
|
);
|
|
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
// Check if file exists and was modified
|
|
if let Ok(metadata) = fs::metadata(&state_file).await {
|
|
if let Ok(modified) = metadata.modified() {
|
|
if last_modified.map_or(true, |last| modified > last) {
|
|
last_modified = Some(modified);
|
|
|
|
// Read and parse health data
|
|
if let Ok(content) = fs::read_to_string(&state_file).await {
|
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&content)
|
|
{
|
|
log::debug!(
|
|
"[openhuman] Broadcasting health event from file: {:?}",
|
|
json_value
|
|
);
|
|
|
|
// Emit Tauri event to frontend (same as internal daemon)
|
|
if let Err(e) = app_handle.emit("openhuman:health", &json_value) {
|
|
log::error!(
|
|
"[openhuman] Failed to emit health event from file: {}",
|
|
e
|
|
);
|
|
} else {
|
|
log::debug!(
|
|
"[openhuman] Health event emitted successfully from file"
|
|
);
|
|
}
|
|
} else {
|
|
log::debug!(
|
|
"[openhuman] Failed to parse health file as JSON: {}",
|
|
state_file.display()
|
|
);
|
|
}
|
|
} else {
|
|
log::debug!(
|
|
"[openhuman] Failed to read health file: {}",
|
|
state_file.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// File doesn't exist yet - external daemon may not be writing yet
|
|
log::debug!(
|
|
"[openhuman] Health file not found yet: {}",
|
|
state_file.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn run() {
|
|
if let Err(err) = rustls::crypto::ring::default_provider().install_default() {
|
|
log::warn!(
|
|
"[app] rustls crypto provider not installed (already set?): {:?}",
|
|
err
|
|
);
|
|
} else {
|
|
log::info!("[app] rustls crypto provider installed (ring)");
|
|
}
|
|
|
|
let daemon_mode = is_daemon_mode();
|
|
|
|
// Initialize logger
|
|
{
|
|
use env_logger::fmt::style::{AnsiColor, Style};
|
|
use std::io::Write;
|
|
|
|
let default_filter = std::env::var("RUST_LOG")
|
|
.unwrap_or_else(|_| "info,tungstenite=warn,tokio_tungstenite=warn,reqwest=warn,rusqlite=warn,hyper=warn,h2=warn".to_string());
|
|
|
|
let write_style = std::env::var("RUST_LOG_STYLE")
|
|
.map(|v| match v.as_str() {
|
|
"never" => env_logger::fmt::WriteStyle::Never,
|
|
_ => env_logger::fmt::WriteStyle::Always,
|
|
})
|
|
.unwrap_or(env_logger::fmt::WriteStyle::Always);
|
|
|
|
let _ = env_logger::Builder::new()
|
|
.parse_filters(&default_filter)
|
|
.write_style(write_style)
|
|
.format(|buf, record| {
|
|
let timestamp = buf.timestamp_millis()
|
|
.to_string();
|
|
// Strip the date prefix, keep only HH:MM:SS.mmm
|
|
let time_only = timestamp.split('T')
|
|
.nth(1)
|
|
.and_then(|t| t.strip_suffix('Z'))
|
|
.unwrap_or(×tamp);
|
|
let level = record.level();
|
|
|
|
// Level colors
|
|
let level_style = match level {
|
|
log::Level::Error => Style::new().fg_color(Some(AnsiColor::Red.into())).bold(),
|
|
log::Level::Warn => Style::new().fg_color(Some(AnsiColor::Yellow.into())).bold(),
|
|
log::Level::Info => Style::new().fg_color(Some(AnsiColor::Green.into())),
|
|
log::Level::Debug => Style::new().fg_color(Some(AnsiColor::BrightBlack.into())),
|
|
log::Level::Trace => Style::new().fg_color(Some(AnsiColor::BrightBlack.into())),
|
|
};
|
|
|
|
let msg = format!("{}", record.args());
|
|
|
|
// Extract tag from message (e.g. "[socket-mgr]", "[skill:x]")
|
|
let (tag, rest) = if msg.starts_with('[') {
|
|
if let Some(end) = msg.find(']') {
|
|
let tag = &msg[..=end];
|
|
let rest = msg[end + 1..].trim_start();
|
|
(Some(tag.to_string()), rest.to_string())
|
|
} else {
|
|
(None, msg)
|
|
}
|
|
} else {
|
|
(None, msg)
|
|
};
|
|
|
|
// Tag-based colors
|
|
let tag_style = if let Some(ref t) = tag {
|
|
let t_lower = t.to_lowercase();
|
|
if t_lower.contains("socket") {
|
|
Style::new().fg_color(Some(AnsiColor::Blue.into())).bold()
|
|
} else if t_lower.contains("runtime") {
|
|
Style::new().fg_color(Some(AnsiColor::Cyan.into())).bold()
|
|
} else if t_lower.contains("skill") {
|
|
Style::new().fg_color(Some(AnsiColor::Green.into())).bold()
|
|
} else if t_lower.contains("ping") || t_lower.contains("cron") {
|
|
Style::new().fg_color(Some(AnsiColor::Yellow.into())).bold()
|
|
} else if t_lower.contains("app") {
|
|
Style::new().fg_color(Some(AnsiColor::White.into())).bold()
|
|
} else if t_lower.contains("ai") {
|
|
Style::new().fg_color(Some(AnsiColor::BrightMagenta.into())).bold()
|
|
} else {
|
|
Style::new().fg_color(Some(AnsiColor::BrightBlack.into()))
|
|
}
|
|
} else {
|
|
Style::new()
|
|
};
|
|
|
|
let dim = Style::new().fg_color(Some(AnsiColor::BrightBlack.into()));
|
|
|
|
if let Some(ref t) = tag {
|
|
writeln!(
|
|
buf,
|
|
"{dim}{time_only}{dim:#} {level_style}{level:<5}{level_style:#} {tag_style}{t}{tag_style:#} {rest}"
|
|
)
|
|
} else {
|
|
writeln!(
|
|
buf,
|
|
"{dim}{time_only}{dim:#} {level_style}{level:<5}{level_style:#} {rest}"
|
|
)
|
|
}
|
|
})
|
|
.try_init();
|
|
}
|
|
|
|
let mut builder = tauri::Builder::default()
|
|
// Plugins
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.plugin(tauri_plugin_os::init());
|
|
|
|
// Add desktop-only plugins (autostart, notification)
|
|
#[cfg(desktop)]
|
|
{
|
|
builder = builder
|
|
.plugin(tauri_plugin_autostart::init(
|
|
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
|
Some(vec!["--daemon"]),
|
|
))
|
|
.plugin(tauri_plugin_notification::init());
|
|
}
|
|
|
|
builder
|
|
// Setup
|
|
.setup(move |app| {
|
|
// Register deep link handlers (Windows/Linux)
|
|
#[cfg(any(windows, target_os = "linux"))]
|
|
{
|
|
app.deep_link().register_all()?;
|
|
}
|
|
|
|
// macOS-specific: Handle window close event to minimize to tray
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let app_handle = app.handle().clone();
|
|
window.on_window_event(move |event| {
|
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
|
// Prevent the window from closing, hide it instead
|
|
api.prevent_close();
|
|
if let Some(win) = app_handle.get_webview_window("main") {
|
|
let _ = win.hide();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Bridge external daemon health file and ensure core background service.
|
|
{
|
|
let data_dir = app.path().app_data_dir().unwrap_or_else(|_| {
|
|
dirs::home_dir()
|
|
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
|
.join(".openhuman")
|
|
});
|
|
let app_handle_for_watcher = app.handle().clone();
|
|
let data_dir_clone = data_dir.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
watch_daemon_health_file(app_handle_for_watcher, data_dir_clone).await;
|
|
});
|
|
tauri::async_runtime::spawn(async move {
|
|
match commands::core_relay::ensure_service_managed_core_running().await {
|
|
Ok(()) => {
|
|
log::info!("[openhuman] Core background service ensured via core RPC");
|
|
}
|
|
Err(e) => {
|
|
log::error!(
|
|
"[openhuman] Failed to ensure core background service: {e}"
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Start/ensure standalone core process for business logic RPC.
|
|
{
|
|
let core_run_mode = core_process::default_core_run_mode(daemon_mode);
|
|
let core_bin = if matches!(core_run_mode, core_process::CoreRunMode::ChildProcess) {
|
|
core_process::default_core_bin()
|
|
} else {
|
|
None
|
|
};
|
|
let core_handle = core_process::CoreProcessHandle::new(
|
|
core_process::default_core_port(),
|
|
core_bin,
|
|
core_run_mode,
|
|
);
|
|
std::env::set_var("OPENHUMAN_CORE_RPC_URL", core_handle.rpc_url());
|
|
app.manage(core_handle.clone());
|
|
tauri::async_runtime::spawn(async move {
|
|
if let Err(err) = core_handle.ensure_running().await {
|
|
log::error!("[core] failed to start core process: {err}");
|
|
} else {
|
|
log::info!("[core] core process ready");
|
|
}
|
|
});
|
|
}
|
|
|
|
if daemon_mode {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.hide();
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
// Register all commands (desktop build lists handlers explicitly below).
|
|
.invoke_handler({
|
|
#[cfg(desktop)]
|
|
{
|
|
tauri::generate_handler![
|
|
greet,
|
|
// AI config file writing
|
|
write_ai_config_file,
|
|
ai_get_config,
|
|
ai_refresh_config,
|
|
core_rpc_relay,
|
|
show_window,
|
|
hide_window,
|
|
toggle_window,
|
|
is_window_visible,
|
|
minimize_window,
|
|
maximize_window,
|
|
close_window,
|
|
set_window_title,
|
|
// OpenHuman local host commands (core RPC uses core_rpc_relay)
|
|
openhuman_get_daemon_host_config,
|
|
openhuman_set_daemon_host_config,
|
|
openhuman_service_install,
|
|
openhuman_service_start,
|
|
openhuman_service_stop,
|
|
openhuman_service_status,
|
|
openhuman_service_uninstall,
|
|
]
|
|
}
|
|
})
|
|
.build({
|
|
let mut context = tauri::generate_context!();
|
|
if daemon_mode {
|
|
context.config_mut().app.windows.clear();
|
|
}
|
|
context
|
|
})
|
|
.expect("error while building tauri application")
|
|
.run(move |app_handle, event| {
|
|
match event {
|
|
// Handle macOS Dock icon click (reopen event)
|
|
#[cfg(target_os = "macos")]
|
|
RunEvent::Reopen { .. } => {
|
|
if !daemon_mode {
|
|
if let Some(window) = app_handle.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.unminimize();
|
|
let _ = window.set_focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gracefully shut down background services before process exit.
|
|
RunEvent::Exit => {
|
|
log::info!("[app] Exit event received, shutting down");
|
|
|
|
let _ = app_handle;
|
|
}
|
|
|
|
_ => {
|
|
let _ = app_handle;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn run_core_from_args(args: &[String]) -> anyhow::Result<()> {
|
|
let core_bin = crate::core_process::default_core_bin()
|
|
.ok_or_else(|| anyhow::anyhow!("openhuman core binary not found"))?;
|
|
let status = std::process::Command::new(core_bin)
|
|
.args(args)
|
|
.status()
|
|
.map_err(|e| anyhow::anyhow!("failed to execute core binary: {e}"))?;
|
|
if !status.success() {
|
|
anyhow::bail!("core binary exited with status {status}");
|
|
}
|
|
Ok(())
|
|
}
|