Skip to content

Commit 37f142a

Browse files
committed
fix(issue-1287): [bug]-windows-免安装桌面端进入通知/agent-设置可能黑屏
1 parent 60d8211 commit 37f142a

6 files changed

Lines changed: 185 additions & 27 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Component } from 'react';
2+
import type { ErrorInfo, ReactNode } from 'react';
3+
import { InlineAlert } from '../common';
4+
import { cn } from '../../utils/cn';
5+
6+
interface SettingsPanelErrorBoundaryProps {
7+
title: string;
8+
children: ReactNode;
9+
resetKey?: string | number;
10+
className?: string;
11+
}
12+
13+
interface SettingsPanelErrorBoundaryState {
14+
hasError: boolean;
15+
errorMessage: string;
16+
}
17+
18+
export class SettingsPanelErrorBoundary extends Component<
19+
SettingsPanelErrorBoundaryProps,
20+
SettingsPanelErrorBoundaryState
21+
> {
22+
override state: SettingsPanelErrorBoundaryState = {
23+
hasError: false,
24+
errorMessage: '',
25+
};
26+
27+
static getDerivedStateFromError(error: unknown): SettingsPanelErrorBoundaryState {
28+
return {
29+
hasError: true,
30+
errorMessage: error instanceof Error ? error.message : '未知前端运行时异常',
31+
};
32+
}
33+
34+
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
35+
console.error(`Settings panel runtime error: ${this.props.title}`, error, errorInfo);
36+
}
37+
38+
override componentDidUpdate(prevProps: SettingsPanelErrorBoundaryProps) {
39+
if (this.state.hasError && prevProps.resetKey !== this.props.resetKey) {
40+
this.setState({ hasError: false, errorMessage: '' });
41+
}
42+
}
43+
44+
override render() {
45+
if (!this.state.hasError) {
46+
return this.props.children;
47+
}
48+
49+
return (
50+
<div className={cn('rounded-[1.5rem] border settings-border bg-card/94 p-5 shadow-soft-card-strong backdrop-blur-sm', this.props.className)}>
51+
<InlineAlert
52+
title={`${this.props.title}加载失败`}
53+
variant="danger"
54+
message={(
55+
<div className="space-y-2">
56+
<p>
57+
该设置区域发生前端运行时异常,页面其他设置仍可继续使用。请查看并提供桌面端日志
58+
<code className="mx-1 rounded bg-background/45 px-1 py-0.5 font-mono text-xs">desktop.log</code>
59+
,同时补充 release 版本、Windows 版本和触发入口。
60+
</p>
61+
{this.state.errorMessage ? (
62+
<p className="break-words font-mono text-xs opacity-80">
63+
错误摘要:{this.state.errorMessage}
64+
</p>
65+
) : null}
66+
</div>
67+
)}
68+
/>
69+
</div>
70+
);
71+
}
72+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import type { ReactElement } from 'react';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { SettingsPanelErrorBoundary } from '../SettingsPanelErrorBoundary';
5+
6+
function ThrowingPanel(): ReactElement {
7+
throw new Error('mock settings panel crash');
8+
}
9+
10+
describe('SettingsPanelErrorBoundary', () => {
11+
beforeEach(() => {
12+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
13+
});
14+
15+
afterEach(() => {
16+
vi.restoreAllMocks();
17+
});
18+
19+
it('renders a diagnostic desktop-log fallback when a settings panel throws', () => {
20+
render(
21+
<SettingsPanelErrorBoundary title="通知设置" resetKey="notification">
22+
<ThrowingPanel />
23+
</SettingsPanelErrorBoundary>
24+
);
25+
26+
expect(screen.getByRole('alert')).toBeInTheDocument();
27+
expect(screen.getByText('通知设置加载失败')).toBeInTheDocument();
28+
expect(screen.getByText('desktop.log')).toBeInTheDocument();
29+
expect(screen.getByText(/release Windows /)).toBeInTheDocument();
30+
expect(screen.getByText(/mock settings panel crash/)).toBeInTheDocument();
31+
});
32+
33+
it('resets after resetKey changes so the panel can render again', async () => {
34+
const { rerender } = render(
35+
<SettingsPanelErrorBoundary title="Agent 设置" resetKey="agent:v1">
36+
<ThrowingPanel />
37+
</SettingsPanelErrorBoundary>
38+
);
39+
40+
expect(screen.getByText('Agent 设置加载失败')).toBeInTheDocument();
41+
42+
rerender(
43+
<SettingsPanelErrorBoundary title="Agent 设置" resetKey="agent:v2">
44+
<div>Agent 设置已恢复</div>
45+
</SettingsPanelErrorBoundary>
46+
);
47+
48+
await waitFor(() => {
49+
expect(screen.getByText('Agent 设置已恢复')).toBeInTheDocument();
50+
});
51+
expect(screen.queryByText('Agent 设置加载失败')).not.toBeInTheDocument();
52+
});
53+
});

apps/dsa-web/src/components/settings/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './NotificationTestPanel';
66
export * from './SettingsField';
77
export * from './SettingsHelpButton';
88
export * from './SettingsLoading';
9+
export * from './SettingsPanelErrorBoundary';
910
export * from './SettingsSectionCard';
1011
export * from './SettingsCategoryNav';
1112
export * from './AuthSettingsCard';

apps/dsa-web/src/pages/SettingsPage.tsx

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
SettingsAlert,
1515
SettingsField,
1616
SettingsLoading,
17+
SettingsPanelErrorBoundary,
1718
SettingsSectionCard,
1819
} from '../components/settings';
1920
import { WEB_BUILD_INFO } from '../utils/constants';
@@ -495,6 +496,31 @@ const SettingsPage: React.FC = () => {
495496
};
496497

497498
const desktopUpdateNotice = getDesktopUpdateNotice(desktopUpdateState);
499+
const shouldGuardActiveConfigPanel = activeCategory === 'notification' || activeCategory === 'agent';
500+
const activeConfigPanelErrorTitle = activeCategory === 'agent' ? 'Agent 设置' : '通知设置';
501+
const activeConfigPanel = activeItems.length ? (
502+
<SettingsSectionCard
503+
title="当前分类配置项"
504+
description={getCategoryDescriptionZh(activeCategory as SystemConfigCategory, '') || '使用统一字段卡片维护当前分类的系统配置。'}
505+
>
506+
{activeItems.map((item) => (
507+
<SettingsField
508+
key={item.key}
509+
item={item}
510+
value={item.value}
511+
disabled={isSaving}
512+
onChange={setDraftValue}
513+
issues={issueByKey[item.key] || []}
514+
/>
515+
))}
516+
</SettingsSectionCard>
517+
) : (
518+
<EmptyState
519+
title="当前分类下暂无配置项"
520+
description="当前分类没有可编辑字段;可切换左侧分类继续查看其它系统配置。"
521+
className="settings-surface-panel settings-border-strong border-none bg-transparent shadow-none"
522+
/>
523+
);
498524

499525
return (
500526
<div className="settings-page min-h-full px-4 pb-6 pt-4 md:px-6">
@@ -754,35 +780,25 @@ const SettingsPage: React.FC = () => {
754780
<ChangePasswordCard />
755781
) : null}
756782
{activeCategory === 'notification' ? (
757-
<NotificationTestPanel
758-
items={rawActiveItems.map((item) => ({ key: item.key, value: String(item.value ?? '') }))}
759-
maskToken={maskToken}
760-
disabled={isSaving || isLoading}
761-
/>
783+
<SettingsPanelErrorBoundary
784+
title="通知测试"
785+
resetKey={`notification-test:${configVersion}`}
786+
>
787+
<NotificationTestPanel
788+
items={rawActiveItems.map((item) => ({ key: item.key, value: String(item.value ?? '') }))}
789+
maskToken={maskToken}
790+
disabled={isSaving || isLoading}
791+
/>
792+
</SettingsPanelErrorBoundary>
762793
) : null}
763-
{activeItems.length ? (
764-
<SettingsSectionCard
765-
title="当前分类配置项"
766-
description={getCategoryDescriptionZh(activeCategory as SystemConfigCategory, '') || '使用统一字段卡片维护当前分类的系统配置。'}
794+
{shouldGuardActiveConfigPanel && activeItems.length ? (
795+
<SettingsPanelErrorBoundary
796+
title={activeConfigPanelErrorTitle}
797+
resetKey={`${activeCategory}:${configVersion}`}
767798
>
768-
{activeItems.map((item) => (
769-
<SettingsField
770-
key={item.key}
771-
item={item}
772-
value={item.value}
773-
disabled={isSaving}
774-
onChange={setDraftValue}
775-
issues={issueByKey[item.key] || []}
776-
/>
777-
))}
778-
</SettingsSectionCard>
779-
) : (
780-
<EmptyState
781-
title="当前分类下暂无配置项"
782-
description="当前分类没有可编辑字段;可切换左侧分类继续查看其它系统配置。"
783-
className="settings-surface-panel settings-border-strong border-none bg-transparent shadow-none"
784-
/>
785-
)}
799+
{activeConfigPanel}
800+
</SettingsPanelErrorBoundary>
801+
) : activeConfigPanel}
786802
</section>
787803
</div>
788804
)}

apps/dsa-web/src/pages/__tests__/SettingsPage.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
applyPartialUpdate,
2222
refreshAfterExternalSave,
2323
refreshStatus,
24+
settingsPanelErrorBoundary,
2425
useAuthMock,
2526
useSystemConfigMock,
2627
webBuildInfoMock,
@@ -41,6 +42,7 @@ const {
4142
applyPartialUpdate: vi.fn(),
4243
refreshAfterExternalSave: vi.fn(),
4344
refreshStatus: vi.fn(),
45+
settingsPanelErrorBoundary: vi.fn(),
4446
useAuthMock: vi.fn(),
4547
useSystemConfigMock: vi.fn(),
4648
webBuildInfoMock: {
@@ -141,6 +143,16 @@ vi.mock('../../components/settings', () => ({
141143
),
142144
SettingsField: ({ item }: { item: { key: string } }) => <div>{item.key}</div>,
143145
SettingsLoading: () => <div>loading</div>,
146+
SettingsPanelErrorBoundary: ({
147+
title,
148+
children,
149+
}: {
150+
title: string;
151+
children: React.ReactNode;
152+
}) => {
153+
settingsPanelErrorBoundary(title);
154+
return <>{children}</>;
155+
},
144156
SettingsSectionCard: ({
145157
title,
146158
description,
@@ -578,6 +590,7 @@ describe('SettingsPage', () => {
578590
expect(screen.getByText('AGENT_ORCHESTRATOR_TIMEOUT_S')).toBeInTheDocument();
579591
expect(screen.getByText('AGENT_DEEP_RESEARCH_BUDGET')).toBeInTheDocument();
580592
expect(screen.getByText('AGENT_EVENT_MONITOR_ENABLED')).toBeInTheDocument();
593+
expect(settingsPanelErrorBoundary).toHaveBeenCalledWith('Agent 设置');
581594
});
582595

583596
it('reset button semantic: discards local changes without network request', () => {
@@ -633,6 +646,8 @@ describe('SettingsPage', () => {
633646

634647
expect(screen.getByText('通知测试面板:WECHAT_WEBHOOK_URL')).toBeInTheDocument();
635648
expect(screen.getByText('WECHAT_WEBHOOK_URL')).toBeInTheDocument();
649+
expect(settingsPanelErrorBoundary).toHaveBeenCalledWith('通知测试');
650+
expect(settingsPanelErrorBoundary).toHaveBeenCalledWith('通知设置');
636651
});
637652

638653
it('renders env backup actions outside desktop runtime', () => {

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
4040
- [修复] 修正分析报告 API 构建策略点位时数值字段未归一为字符串的问题,避免策略价格触发响应 DTO 类型校验失败。
4141
- [修复] Docker 启动入口自动修复 `data` / `logs` / `reports` 挂载目录权限并降权运行,文档化的 Compose `exec` 手动命令显式使用 `dsa` 用户,避免普通部署需要手动 `chown` / `chmod`
4242
- [修复] Web 首页大盘复盘结果改由主内容滚动区承载,避免 loading 切换到长结果后下方报告区域被截断或无法继续滚动。
43+
- [修复] Web 设置页为通知测试与 Agent/通知配置区域增加局部运行时错误兜底,异常时提示提供 Windows 桌面端 `desktop.log`,避免整页黑屏。
4344

4445
## [3.16.0] - 2026-05-10
4546

0 commit comments

Comments
 (0)