feat: error boundary (#9462)

* build: 添加 react-error-boundary 依赖

添加 react-error-boundary 包以增强 React 应用的错误处理能力

* feat(组件): 添加ErrorBoundary组件用于错误边界处理

* feat(home): 为HomeTabs和Chat组件添加错误边界处理

* refactor(ErrorBoundary): 移除多余的ErrorContainer包装并优化结构

* feat(ErrorBoundary): 添加重新加载按钮并优化错误边界样式

添加重新加载功能按钮,方便用户快速恢复应用
调整错误边界容器的布局样式,使其居中显示

* style(ErrorBoundary): 移除ErrorContainer的固定高度以改善布局灵活性

* test(ErrorBoundary): 添加测试错误边界组件的功能按钮

添加一个用于测试错误边界组件功能的按钮组件,该按钮点击后会抛出错误以验证错误边界是否正常工作。此组件仅用于测试,合并前需要删除。

* feat(路由): 为路由组件添加错误边界处理

在Router组件中包裹ErrorBoundary以捕获并处理子组件中的错误

* fix(ErrorBoundary): 修复错误边界中翻译键的拼写错误

* feat(i18n): 添加边界错误处理和主题不存在错误的多语言支持

* refactor(ErrorBoundary): 移除用于测试的ThrowError组件
This commit is contained in:
Phantom 2025-08-24 18:49:14 +08:00 committed by GitHub
parent 6d102ccef8
commit 107c01913d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 178 additions and 25 deletions

View File

@ -227,6 +227,7 @@
"proxy-agent": "^6.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",
"react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0",

View File

@ -4,6 +4,7 @@ import { FC, useMemo } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import Sidebar from './components/app/Sidebar'
import { ErrorBoundary } from './components/ErrorBoundary'
import TabsContainer from './components/Tab/TabContainer'
import NavigationHandler from './handler/NavigationHandler'
import { useNavbarPosition } from './hooks/useSettings'
@ -23,18 +24,20 @@ const Router: FC = () => {
const routes = useMemo(() => {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/code" element={<CodeToolsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
<ErrorBoundary>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/code" element={<CodeToolsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
</ErrorBoundary>
)
}, [])

View File

@ -0,0 +1,57 @@
import { formatErrorMessage } from '@renderer/utils/error'
import { Alert, Button, Space } from 'antd'
import { ComponentType, ReactNode } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): ReactNode => {
const { t } = useTranslation()
const { error } = props
const debug = async () => {
await window.api.devTools.toggle()
}
const reload = async () => {
await window.api.reload()
}
return (
<ErrorContainer>
<Alert
message={t('error.boundary.default.message')}
showIcon
description={formatErrorMessage(error)}
type="error"
action={
<Space>
<Button size="small" danger onClick={debug}>
{t('error.boundary.default.devtools')}
</Button>
<Button size="small" danger onClick={reload}>
{t('error.boundary.default.reload')}
</Button>
</Space>
}
/>
</ErrorContainer>
)
}
const ErrorBoundaryCustomized = ({
children,
fallbackComponent
}: {
children: ReactNode
fallbackComponent?: ComponentType<FallbackProps>
}) => {
return <ErrorBoundary FallbackComponent={fallbackComponent ?? DefaultFallback}>{children}</ErrorBoundary>
}
const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px;
`
export { ErrorBoundaryCustomized as ErrorBoundary }

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "Backup file format error"
},
"boundary": {
"default": {
"devtools": "Open debug panel",
"message": "It seems that something went wrong...",
"reload": "Reload"
}
},
"chat": {
"chunk": {
"non_json": "Returned an invalid data format"

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "バックアップファイルの形式エラー"
},
"boundary": {
"default": {
"devtools": "デバッグパネルを開く",
"message": "何か問題が発生したようです...",
"reload": "再読み込み"
}
},
"chat": {
"chunk": {
"non_json": "無効なデータ形式が返されました"

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "Ошибка формата файла резервной копии"
},
"boundary": {
"default": {
"devtools": "Открыть панель отладки",
"message": "Похоже, возникла какая-то проблема...",
"reload": "Перезагрузить"
}
},
"chat": {
"chunk": {
"non_json": "Вернулся недопустимый формат данных"

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "备份文件格式错误"
},
"boundary": {
"default": {
"devtools": "打开调试面板",
"message": "似乎出现了一些问题...",
"reload": "重新加载"
}
},
"chat": {
"chunk": {
"non_json": "返回了无效的数据格式"

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "備份檔案格式錯誤"
},
"boundary": {
"default": {
"devtools": "打開除錯面板",
"message": "似乎出現了一些問題...",
"reload": "重新載入"
}
},
"chat": {
"chunk": {
"non_json": "返回了無效的資料格式"

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "Λάθος μορφή αρχείου που επιστρέφεται"
},
"boundary": {
"default": {
"devtools": "Άνοιγμα πίνακα αποσφαλμάτωσης",
"message": "Φαίνεται ότι προέκυψε κάποιο πρόβλημα...",
"reload": "Επαναφόρτωση"
}
},
"chat": {
"chunk": {
"non_json": "Επέστρεψε μη έγκυρη μορφή δεδομένων"
@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Συνεχίστε το συνομιλημένο",
"error": {
"topic_not_found": "Το θέμα δεν υπάρχει"
},
"locate": {
"message": "Εφαρμογή στο μήνυμα"
},

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "Formato de archivo de copia de seguridad incorrecto"
},
"boundary": {
"default": {
"devtools": "Abrir el panel de depuración",
"message": "Parece que ha surgido un problema...",
"reload": "Recargar"
}
},
"chat": {
"chunk": {
"non_json": "Devuelve un formato de datos no válido"
@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Continuar chat",
"error": {
"topic_not_found": "El tema no existe"
},
"locate": {
"message": "Localizar mensaje"
},

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "Le format du fichier de sauvegarde est incorrect"
},
"boundary": {
"default": {
"devtools": "Ouvrir le panneau de débogage",
"message": "Il semble que quelques problèmes soient survenus...",
"reload": "Recharger"
}
},
"chat": {
"chunk": {
"non_json": "a renvoyé un format de données invalide"
@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Continuer la conversation",
"error": {
"topic_not_found": "Le sujet n'existe pas"
},
"locate": {
"message": "Localiser le message"
},

View File

@ -808,6 +808,13 @@
"backup": {
"file_format": "Formato do arquivo de backup está incorreto"
},
"boundary": {
"default": {
"devtools": "Abrir o painel de depuração",
"message": "Parece que ocorreu um problema...",
"reload": "Recarregar"
}
},
"chat": {
"chunk": {
"non_json": "Devolveu um formato de dados inválido"
@ -888,6 +895,9 @@
},
"history": {
"continue_chat": "Continuar conversando",
"error": {
"topic_not_found": "Tópico inexistente"
},
"locate": {
"message": "Localizar mensagem"
},

View File

@ -1,3 +1,4 @@
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
@ -100,20 +101,24 @@ const HomePage: FC = () => {
)}
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
<ErrorBoundary>
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
</ErrorBoundary>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
<ErrorBoundary>
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
</ErrorBoundary>
</ContentContainer>
</Container>
)

View File

@ -8538,6 +8538,7 @@ __metadata:
proxy-agent: "npm:^6.5.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
react-error-boundary: "npm:^6.0.0"
react-hotkeys-hook: "npm:^4.6.1"
react-i18next: "npm:^14.1.2"
react-infinite-scroll-component: "npm:^6.1.0"
@ -19026,6 +19027,17 @@ __metadata:
languageName: node
linkType: hard
"react-error-boundary@npm:^6.0.0":
version: 6.0.0
resolution: "react-error-boundary@npm:6.0.0"
dependencies:
"@babel/runtime": "npm:^7.12.5"
peerDependencies:
react: ">=16.13.1"
checksum: 10c0/1914d600dee95a14f14af4afe9867b0d35c26c4f7826d23208800ba2a99728659029aad60a6ef95e13430b4d79c2c4c9b3585f50bf508450478760d2e4e732d8
languageName: node
linkType: hard
"react-hotkeys-hook@npm:^4.6.1":
version: 4.6.2
resolution: "react-hotkeys-hook@npm:4.6.2"