From dcde9f8f71636db140af418fedb0eaa1de30a7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=92=9F=E6=98=8E=E5=8E=9F?= Date: Tue, 17 Mar 2026 15:55:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A6=96=E6=AC=A1=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E6=AD=A5=E9=AA=A4=E7=99=BD?= =?UTF-8?q?=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ModelCheckStep 的 onDone 引用不稳定导致 useEffect 反复执行 checkModels - configured 时 return null 导致父容器可见但内容为空(白屏) - 添加 AppErrorBoundary 防止未捕获错误导致白屏 --- src/renderer/src/App.tsx | 55 ++++++++++++++++++- .../components/onboarding/onboarding-flow.tsx | 30 +++++++--- 2 files changed, 74 insertions(+), 11 deletions(-) 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 (