Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Rust 后端 (heartbeat.rs): - 告警实时推送: OnceLock<AppHandle> + Tauri emit heartbeat:alert - 动态间隔: tokio::select! + Notify 替代不可变 interval - Config 持久化: update_config 写入 VikingStorage - heartbeat_init 从 VikingStorage 恢复 config - 移除 dead code (subscribe, HeartbeatCheckFn) - Memory stats fallback 分层处理 新增 health_snapshot.rs: - HealthSnapshot Tauri 命令 — 按需查询引擎/记忆状态 - 注册到 lib.rs invoke_handler 前端修复: - HeartbeatConfig handleSave 同步到 Rust 后端 - App.tsx 读 localStorage 持久化配置 + heartbeat:alert 监听 + toast - saasStore 降级后指数退避探测恢复 + saas-recovered 事件 - 新增 HealthPanel.tsx 只读健康面板 (4卡片 + 告警列表) - SettingsLayout 添加 health 导航入口 清理: - 删除 intelligence-client/ 目录版 (9文件 -1640行, 单文件版是活跃代码)
127 lines
4.2 KiB
Rust
127 lines
4.2 KiB
Rust
//! Health Snapshot — on-demand query for all subsystem health status
|
|
//!
|
|
//! Provides a single Tauri command that aggregates health data from:
|
|
//! - Intelligence Heartbeat engine (running state, config, alerts)
|
|
//! - Memory pipeline (entries count, storage size)
|
|
//!
|
|
//! Connection and SaaS status are managed by frontend stores and not included here.
|
|
|
|
use serde::Serialize;
|
|
use super::heartbeat::{HeartbeatConfig, HeartbeatEngineState, HeartbeatResult};
|
|
|
|
/// Aggregated health snapshot from Rust backend
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct HealthSnapshot {
|
|
pub timestamp: String,
|
|
pub intelligence: IntelligenceHealth,
|
|
pub memory: MemoryHealth,
|
|
}
|
|
|
|
/// Intelligence heartbeat engine status
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct IntelligenceHealth {
|
|
pub engine_running: bool,
|
|
pub config: HeartbeatConfig,
|
|
pub last_tick: Option<String>,
|
|
pub alert_count_24h: usize,
|
|
pub total_checks: usize,
|
|
}
|
|
|
|
/// Memory pipeline status
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct MemoryHealth {
|
|
pub total_entries: usize,
|
|
pub storage_size_bytes: u64,
|
|
pub last_extraction: Option<String>,
|
|
}
|
|
|
|
/// Query a unified health snapshot for an agent
|
|
// @connected
|
|
#[tauri::command]
|
|
pub async fn health_snapshot(
|
|
agent_id: String,
|
|
heartbeat_state: tauri::State<'_, HeartbeatEngineState>,
|
|
) -> Result<HealthSnapshot, String> {
|
|
let engines = heartbeat_state.lock().await;
|
|
|
|
let engine = engines
|
|
.get(&agent_id)
|
|
.ok_or_else(|| format!("Heartbeat engine not initialized for agent: {}", agent_id))?;
|
|
|
|
let engine_running = engine.is_running().await;
|
|
let config = engine.get_config().await;
|
|
let history: Vec<HeartbeatResult> = engine.get_history(100).await;
|
|
|
|
// Calculate alert count in the last 24 hours
|
|
let now = chrono::Utc::now();
|
|
let twenty_four_hours_ago = now - chrono::Duration::hours(24);
|
|
let alert_count_24h = history
|
|
.iter()
|
|
.filter(|r| {
|
|
r.timestamp.parse::<chrono::DateTime<chrono::Utc>>()
|
|
.map(|t| t > twenty_four_hours_ago)
|
|
.unwrap_or(false)
|
|
})
|
|
.flat_map(|r| r.alerts.iter())
|
|
.count();
|
|
|
|
let last_tick = history.first().map(|r| r.timestamp.clone());
|
|
|
|
// Memory health from cached stats (fallback to zeros)
|
|
// Read cache in a separate scope to ensure RwLockReadGuard is dropped before any .await
|
|
let cached_stats: Option<super::heartbeat::MemoryStatsCache> = {
|
|
let cache = super::heartbeat::get_memory_stats_cache();
|
|
match cache.read() {
|
|
Ok(c) => c.get(&agent_id).cloned(),
|
|
Err(_) => None,
|
|
}
|
|
}; // RwLockReadGuard dropped here
|
|
|
|
let memory = match cached_stats {
|
|
Some(s) => MemoryHealth {
|
|
total_entries: s.total_entries,
|
|
storage_size_bytes: s.storage_size_bytes as u64,
|
|
last_extraction: s.last_updated,
|
|
},
|
|
None => {
|
|
// Fallback: try to query VikingStorage directly
|
|
match crate::viking_commands::get_storage().await {
|
|
Ok(storage) => {
|
|
match zclaw_growth::VikingStorage::find_by_prefix(&*storage, &format!("mem:{}", agent_id)).await {
|
|
Ok(entries) => MemoryHealth {
|
|
total_entries: entries.len(),
|
|
storage_size_bytes: 0,
|
|
last_extraction: None,
|
|
},
|
|
Err(_) => MemoryHealth {
|
|
total_entries: 0,
|
|
storage_size_bytes: 0,
|
|
last_extraction: None,
|
|
},
|
|
}
|
|
}
|
|
Err(_) => MemoryHealth {
|
|
total_entries: 0,
|
|
storage_size_bytes: 0,
|
|
last_extraction: None,
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(HealthSnapshot {
|
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
intelligence: IntelligenceHealth {
|
|
engine_running,
|
|
config,
|
|
last_tick,
|
|
alert_count_24h,
|
|
total_checks: 5, // Fixed: 5 built-in checks
|
|
},
|
|
memory,
|
|
})
|
|
}
|