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
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:
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user