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:
@@ -84,6 +84,7 @@ export interface SaaSStateSlice {
|
||||
_consecutiveFailures: number;
|
||||
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
||||
_healthCheckTimer?: ReturnType<typeof setInterval>;
|
||||
_recoveryProbeTimer?: ReturnType<typeof setInterval>;
|
||||
|
||||
// === Billing State ===
|
||||
plans: BillingPlan[];
|
||||
@@ -141,6 +142,67 @@ function resolveInitialMode(sessionMeta: { saasUrl: string; account: SaaSAccount
|
||||
return sessionMeta ? 'saas' : 'tauri';
|
||||
}
|
||||
|
||||
// === SaaS Recovery Probe ===
|
||||
// When SaaS degrades to local mode, periodically probes SaaS reachability
|
||||
// with exponential backoff (2min → 3min → 4.5min → 6.75min → 10min cap).
|
||||
// On recovery, switches back to SaaS mode and notifies user via toast.
|
||||
|
||||
let _recoveryProbeInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let _recoveryBackoffMs = 2 * 60 * 1000; // Start at 2 minutes
|
||||
const RECOVERY_BACKOFF_CAP_MS = 10 * 60 * 1000; // Max 10 minutes
|
||||
const RECOVERY_BACKOFF_MULTIPLIER = 1.5;
|
||||
|
||||
function startRecoveryProbe() {
|
||||
if (_recoveryProbeInterval) return; // Already probing
|
||||
|
||||
_recoveryBackoffMs = 2 * 60 * 1000; // Reset backoff
|
||||
log.info('[SaaS Recovery] Starting recovery probe...');
|
||||
|
||||
const probe = async () => {
|
||||
try {
|
||||
await saasClient.deviceHeartbeat(DEVICE_ID);
|
||||
// SaaS is reachable again — recover
|
||||
log.info('[SaaS Recovery] SaaS reachable — switching back to SaaS mode');
|
||||
useSaaSStore.setState({
|
||||
saasReachable: true,
|
||||
connectionMode: 'saas',
|
||||
_consecutiveFailures: 0,
|
||||
} as unknown as Partial<SaaSStore>);
|
||||
saveConnectionMode('saas');
|
||||
|
||||
// Notify user via custom event (App.tsx listens)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('saas-recovered'));
|
||||
}
|
||||
|
||||
// Stop probing
|
||||
stopRecoveryProbe();
|
||||
} catch {
|
||||
// Still unreachable — increase backoff
|
||||
_recoveryBackoffMs = Math.min(
|
||||
_recoveryBackoffMs * RECOVERY_BACKOFF_MULTIPLIER,
|
||||
RECOVERY_BACKOFF_CAP_MS
|
||||
);
|
||||
log.debug(`[SaaS Recovery] Still unreachable, next probe in ${Math.round(_recoveryBackoffMs / 1000)}s`);
|
||||
|
||||
// Reschedule with new backoff
|
||||
if (_recoveryProbeInterval) {
|
||||
clearInterval(_recoveryProbeInterval);
|
||||
}
|
||||
_recoveryProbeInterval = setInterval(probe, _recoveryBackoffMs);
|
||||
}
|
||||
};
|
||||
|
||||
_recoveryProbeInterval = setInterval(probe, _recoveryBackoffMs);
|
||||
}
|
||||
|
||||
function stopRecoveryProbe() {
|
||||
if (_recoveryProbeInterval) {
|
||||
clearInterval(_recoveryProbeInterval);
|
||||
_recoveryProbeInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// === Store Implementation ===
|
||||
|
||||
export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
@@ -698,6 +760,8 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
||||
connectionMode: 'tauri',
|
||||
} as unknown as Partial<SaaSStore>);
|
||||
saveConnectionMode('tauri');
|
||||
// Start recovery probe with exponential backoff
|
||||
startRecoveryProbe();
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
Reference in New Issue
Block a user