fix(intelligence): Heartbeat 统一健康系统 — 6处断链修复 + 健康面板 + SaaS自动恢复
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行, 单文件版是活跃代码)
This commit is contained in:
iven
2026-04-15 23:19:24 +08:00
parent 043824c722
commit 215c079d29
19 changed files with 1184 additions and 1678 deletions

View File

@@ -21,6 +21,7 @@ import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/
import { LoginPage } from './components/LoginPage';
import { useOnboarding } from './lib/use-onboarding';
import { intelligenceClient } from './lib/intelligence-client';
import { safeListen } from './lib/safe-tauri';
import { loadEmbeddingConfig, loadEmbeddingApiKey } from './lib/embedding-client';
import { invoke } from '@tauri-apps/api/core';
import { useProposalNotifications, ProposalNotificationHandler } from './lib/useProposalNotifications';
@@ -54,6 +55,7 @@ function App() {
const [showOnboarding, setShowOnboarding] = useState(false);
const [showDetailDrawer, setShowDetailDrawer] = useState(false);
const statsSyncRef = useRef<ReturnType<typeof setInterval> | null>(null);
const alertUnlistenRef = useRef<(() => void) | null>(null);
// Hand Approval state
const [pendingApprovalRun, setPendingApprovalRun] = useState<HandRun | null>(null);
@@ -155,6 +157,11 @@ function App() {
useEffect(() => {
let mounted = true;
// SaaS recovery listener (defined at useEffect scope for cleanup access)
const handleSaasRecovered = () => {
toast('SaaS 服务已恢复连接', 'success');
};
const bootstrap = async () => {
// 未登录时不启动 bootstrap直接结束 loading
if (!useSaaSStore.getState().isLoggedIn) {
@@ -208,7 +215,9 @@ function App() {
// Step 4.5: Auto-start heartbeat engine for self-evolution
try {
const defaultAgentId = 'zclaw-main';
await intelligenceClient.heartbeat.init(defaultAgentId, {
// Restore config from localStorage (Rust side also restores from VikingStorage)
const savedConfig = localStorage.getItem('zclaw-heartbeat-config');
const heartbeatConfig = savedConfig ? JSON.parse(savedConfig) : {
enabled: true,
interval_minutes: 30,
quiet_hours_start: '22:00',
@@ -216,7 +225,8 @@ function App() {
notify_channel: 'ui',
proactivity_level: 'standard',
max_alerts_per_tick: 5,
});
};
await intelligenceClient.heartbeat.init(defaultAgentId, heartbeatConfig);
// Sync memory stats to heartbeat engine
try {
@@ -236,6 +246,21 @@ function App() {
await intelligenceClient.heartbeat.start(defaultAgentId);
log.debug('Heartbeat engine started for self-evolution');
// Listen for real-time heartbeat alerts and show as toast notifications
const unlistenAlerts = await safeListen<Array<{ title: string; content: string; urgency: string }>>(
'heartbeat:alert',
(alerts) => {
for (const alert of alerts) {
const alertType = alert.urgency === 'high' ? 'error'
: alert.urgency === 'medium' ? 'warning'
: 'info';
toast(`[${alert.title}] ${alert.content}`, alertType as 'info' | 'warning' | 'error');
}
}
);
// Store unlisten for cleanup
alertUnlistenRef.current = unlistenAlerts;
// Set up periodic memory stats sync (every 5 minutes)
const MEMORY_STATS_SYNC_INTERVAL = 5 * 60 * 1000;
const statsSyncInterval = setInterval(async () => {
@@ -261,6 +286,9 @@ function App() {
// Non-critical, continue without heartbeat
}
// Listen for SaaS recovery events (from saasStore recovery probe)
window.addEventListener('saas-recovered', handleSaasRecovered);
// Step 5: Restore embedding config to Rust backend (Tauri-only)
if (isTauriRuntime()) {
try {
@@ -339,6 +367,12 @@ function App() {
if (statsSyncRef.current) {
clearInterval(statsSyncRef.current);
}
// Clean up heartbeat alert listener
if (alertUnlistenRef.current) {
alertUnlistenRef.current();
}
// Clean up SaaS recovery event listener
window.removeEventListener('saas-recovered', handleSaasRecovered);
};
}, [connect, onboardingNeeded, onboardingLoading, isLoggedIn]);