cc-haha/desktop/src/components/ErrorBoundary.tsx
程序员阿江(Relakkes) 7ae885a114 fix: add safe Doctor rescue path for desktop upgrades
Online upgrades can strand users on stale desktop UI state or malformed local persistence. This adds a deny-by-default Doctor path that resets only regenerable desktop UI state, reports protected local files with redacted metadata, and keeps protected repair as a dry-run no-op until a reviewed backup-first flow exists.

Constraint: Chat transcripts, model/provider config, Skills, MCP, IM bindings, adapter sessions, OAuth tokens, plugins, and team/session records are user-owned protected state.
Rejected: Automatically rewrite malformed protected JSON | unsafe without schema-specific migrations and backups.
Rejected: Continue relying only on startup migrations | users need an explicit recovery action after a white screen.
Confidence: high
Scope-risk: moderate
Directive: Keep Doctor repair deny-by-default; do not mutate protected state without an explicit reviewed backup-first manual repair flow.
Tested: bun test src/server/__tests__/doctor-service.test.ts
Tested: cd desktop && bun run test src/components/ErrorBoundary.test.tsx src/lib/doctorRepair.test.ts src/__tests__/diagnosticsSettings.test.tsx src/components/layout/StartupErrorView.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:server
Tested: bun run verify (failed only existing agent-utils coverage baseline; changed-lines coverage 97.62%)
Not-tested: Packaged desktop manual Doctor click path.
2026-05-07 00:04:06 +08:00

55 lines
1.5 KiB
TypeScript

import React from 'react'
import { t } from '../i18n'
import { reportReactError } from '../lib/diagnosticsCapture'
import { Button } from './shared/Button'
import { DoctorPanel } from './doctor/DoctorPanel'
type Props = {
children: React.ReactNode
}
type State = {
hasError: boolean
}
export class ErrorBoundary extends React.Component<Props, State> {
state: State = { hasError: false }
static getDerivedStateFromError(): State {
return { hasError: true }
}
componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
void reportReactError(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <ErrorBoundaryFallback />
}
return this.props.children
}
}
function ErrorBoundaryFallback() {
return (
<div className="h-screen w-screen bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex items-center justify-center p-6">
<div className="max-w-md w-full text-center">
<div className="text-base font-semibold">{t('errorBoundary.title')}</div>
<div className="mt-2 text-sm text-[var(--color-text-tertiary)]">
{t('errorBoundary.description')}
</div>
<div className="mt-4 flex justify-center">
<Button type="button" variant="secondary" size="sm" onClick={() => window.location.reload()}>
{t('common.retry')}
</Button>
</div>
<div className="mt-4 text-left">
<DoctorPanel compact />
</div>
</div>
</div>
)
}