diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 0278ffa..4d031dc 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from 'react' +import { Component, useCallback, useEffect, useState } from 'react' +import type { ErrorInfo, ReactNode } from 'react' import { useApp } from '@/store/app-context' import { AppProvider } from '@/store/app-context' import { LanguageProvider } from '@/i18n' @@ -55,9 +56,13 @@ function AppRoot() { if (state.gatewayConnected && !everConnected) setEverConnected(true) }, [state.gatewayConnected, everConnected]) + // 稳定引用,避免 gateway 事件导致 AppRoot re-render 时传给 OnboardingFlow 的 onDone 引用变化, + // 进而触发 ModelCheckStep 内 useEffect 反复执行 checkModels + const handleOnboardingDone = useCallback(() => setOnboardingDone(true), []) + // 新用户:走完整 onboarding(gateway loading → profile setup) if (!onboardingDone) { - return setOnboardingDone(true)} /> + return } // 老用户:未连接过 → 显示启动封面(品牌 Logo + 连接进度 + 日志) @@ -68,6 +73,48 @@ function AppRoot() { return } +// ── ErrorBoundary: 防止未捕获的 React 错误导致白屏 ────────────────────────── + +interface ErrorBoundaryState { + error: Error | null +} + +class AppErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryState> { + state: ErrorBoundaryState = { error: null } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error } + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error('[AppErrorBoundary]', error, info.componentStack) + } + + render(): ReactNode { + if (this.state.error) { + return ( +
+
+

Something went wrong

+

{this.state.error.message}

+ +
+
+ ) + } + return this.props.children + } +} + export default function App() { useAppUpdate() @@ -75,7 +122,9 @@ export default function App() { - + + + diff --git a/src/renderer/src/components/onboarding/onboarding-flow.tsx b/src/renderer/src/components/onboarding/onboarding-flow.tsx index d7dbac8..cfafe6d 100644 --- a/src/renderer/src/components/onboarding/onboarding-flow.tsx +++ b/src/renderer/src/components/onboarding/onboarding-flow.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" // 剥离 ANSI 颜色/控制转义码,避免日志乱码 const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g @@ -519,6 +519,10 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen const [checking, setChecking] = useState(true) const [configured, setConfigured] = useState(false) + // 用 ref 持有最新的 onDone,避免 onDone 引用变化触发 useEffect 重复执行 + const onDoneRef = useRef(onDone) + onDoneRef.current = onDone + const checkModels = useCallback(async () => { setChecking(true) try { @@ -529,10 +533,13 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen const hasPrimary = !!result.defaults?.primary if (hasProviders && hasPrimary) { setConfigured(true) + setChecking(false) return true } } - } catch { /* ignore */ } + } catch (err) { + console.warn('[ModelCheckStep] checkModels failed:', err) + } setConfigured(false) setChecking(false) return false @@ -540,16 +547,16 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen useEffect(() => { checkModels().then((ok) => { - if (ok) onDone() + if (ok) onDoneRef.current() }) - }, [checkModels, onDone]) + }, [checkModels]) // Re-check when settings dialog closes const recheck = useCallback(() => { checkModels().then((ok) => { - if (ok) onDone() + if (ok) onDoneRef.current() }) - }, [checkModels, onDone]) + }, [checkModels]) // Expose recheck for parent to call after settings close useEffect(() => { @@ -557,7 +564,7 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen return () => { delete (window as unknown as Record).__modelCheckRecheck } }, [recheck]) - if (checking && !configured) { + if (checking) { return (
@@ -565,7 +572,14 @@ function ModelCheckStep({ onDone, onOpenSettings }: { onDone: () => void; onOpen ) } - if (configured) return null + // 模型已配置时也显示 spinner(onDone 会很快切走整个 onboarding) + if (configured) { + return ( +
+ +
+ ) + } return (