Files
awoooi/apps/web/src/components/shared/auto-healing-error-boundary.tsx
OG T 4d46e6b9a7
Some checks failed
E2E Health Check / e2e-health (push) Successful in 17s
CD Pipeline / build-and-deploy (push) Has been cancelled
style(web): 全站 font-mono → font-body (DM Mono 設計系統套用)
45 個 component + 6 個 page 統一從舊 font-mono 遷移到
font-body (DM Mono),確保設計系統一致性。

font-body = DM Mono (等寬),視覺效果相同但走新設計 token。
保留: font-heading (Syne)、font-dot-matrix (VT323/DSEG7)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:37:03 +08:00

148 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/* eslint-disable no-console -- Error boundary requires console for crash logging */
import type { ErrorInfo, ReactNode } from 'react';
import React, { Component } from 'react'
import { useTranslations } from 'next-intl'
import { useAgentStore } from '@/stores/agent.store'
interface ErrorBoundaryTranslations {
systemFailure: string;
criticalError: string;
escalating: string;
forceRestart: string;
detectingAnomaly: string;
autoHealingAttempt: (attempt: number) => string;
}
interface InnerProps {
children: ReactNode;
fallbackMessage?: string;
translations: ErrorBoundaryTranslations;
}
interface State {
hasError: boolean;
retryCount: number;
}
class AutoHealingErrorBoundaryInner extends Component<InnerProps, State> {
public state: State = {
hasError: false,
retryCount: 0
};
public static getDerivedStateFromError(_: Error): Partial<State> {
// 只設定 hasError保留 retryCount 防止無限重試
return { hasError: true };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[AWOOOI] Frontend component crashed:', error, errorInfo);
// 檢查是否已達重試上限
if (this.state.retryCount < 3) {
this.attemptAutoHealing();
}
}
private attemptAutoHealing = () => {
const { retryCount } = this.state;
if (retryCount >= 3) {
console.error('[AWOOOI] Auto-healing failed after 3 attempts. Escalating to L3 AIOps.');
return;
}
// L1 Auto-Healing Logic
console.log(`[AWOOOI] Attempting auto-healing (Attempt ${retryCount + 1}/3)...`);
// 1. Clear toxic stored state
if (typeof window !== 'undefined') {
try {
localStorage.clear();
sessionStorage.clear();
} catch (e) {
console.warn('Failed to clear storage:', e);
}
}
// 2. Reset Zustand store explicitly outside of React render cycle
try {
if (typeof useAgentStore.getState().reset === 'function') {
useAgentStore.getState().reset();
}
} catch (e) {
console.warn('[AWOOOI] Failed to reset Agent Store during auto-healing', e);
}
// 3. Exponential Backoff remount
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
setTimeout(() => {
console.log('[AWOOOI] Remounting component...');
this.setState({ hasError: false, retryCount: retryCount + 1 });
}, delay);
}
public render() {
const { translations } = this.props;
if (this.state.hasError) {
if (this.state.retryCount >= 3) {
return (
<div className="flex flex-col items-center justify-center p-8 bg-[#111111] text-white border border-red-500/30 rounded-lg max-w-lg mx-auto mt-12">
<h2 className="text-xl font-body text-red-500 mb-4">{translations.systemFailure}</h2>
<p className="font-body text-sm text-gray-400 mb-4">{this.props.fallbackMessage || translations.criticalError}</p>
<p className="font-body text-xs text-purple-400 mb-6">{translations.escalating}</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-2 bg-white text-black font-body text-sm rounded-full hover:bg-gray-200 transition-colors"
>
{translations.forceRestart}
</button>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center p-12 space-y-4 animate-pulse">
<div className="text-red-500 font-body text-sm font-bold tracking-widest">{translations.detectingAnomaly}</div>
<div className="text-gray-400 font-body text-xs">{translations.autoHealingAttempt(this.state.retryCount + 1)}</div>
<div className="w-48 h-1 bg-gray-800 rounded overflow-hidden mt-4">
<div className="h-full bg-white opacity-50 animate-ping"></div>
</div>
</div>
);
}
return this.props.children;
}
}
// Wrapper component that provides translations via hook
interface Props {
children: ReactNode;
fallbackMessage?: string;
}
export function AutoHealingErrorBoundary({ children, fallbackMessage }: Props) {
const t = useTranslations('errorBoundary');
const translations: ErrorBoundaryTranslations = {
systemFailure: t('systemFailure'),
criticalError: t('criticalError'),
escalating: t('escalating'),
forceRestart: t('forceRestart'),
detectingAnomaly: t('detectingAnomaly'),
autoHealingAttempt: (attempt: number) => t('autoHealingAttempt', { attempt }),
};
return (
<AutoHealingErrorBoundaryInner
translations={translations}
fallbackMessage={fallbackMessage}
>
{children}
</AutoHealingErrorBoundaryInner>
);
}