diff --git a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx index 523378bfd..216f04b61 100644 --- a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx +++ b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx @@ -14,8 +14,11 @@ import { ThemedModal } from '../modal/ThemedModal'; type ErrorReportDialogProps = { open: boolean; onClose: () => void; + fallbackEvents?: ClientErrorEvent[]; }; +const emptyFallbackEvents: ClientErrorEvent[] = []; + function diagnosticLogLabel( logsReady: boolean, logsError: boolean, @@ -26,7 +29,11 @@ function diagnosticLogLabel( return `附加应用诊断日志(${logCount} 个文件)`; } -export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { +export function ErrorReportDialog({ + open, + onClose, + fallbackEvents = emptyFallbackEvents, +}: ErrorReportDialogProps) { const [events, setEvents] = useState([]); const [eventsReady, setEventsReady] = useState(false); const [eventsError, setEventsError] = useState(false); @@ -63,6 +70,15 @@ export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { }) .catch(() => { if (disposed) return; + if (fallbackEvents.length) { + setEvents(fallbackEvents); + setSelectedFingerprints( + new Set(fallbackEvents.map((event) => event.fingerprint)), + ); + setEventsReady(true); + setStatus('最新错误读取失败,已使用打开通知时的快照'); + return; + } setEventsError(true); setEventsReady(true); }); @@ -81,7 +97,7 @@ export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { return () => { disposed = true; }; - }, [open]); + }, [fallbackEvents, open]); const submit = useCallback(async () => { const selectedEvents = events.filter((event) => diff --git a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportNotice.tsx b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportNotice.tsx index 96f6fc11c..676cece87 100644 --- a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportNotice.tsx +++ b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportNotice.tsx @@ -12,6 +12,9 @@ export function ErrorReportNotice() { const [notificationEvents, setNotificationEvents] = useState< ClientErrorEvent[] >([]); + const [reportFallbackEvents, setReportFallbackEvents] = useState< + ClientErrorEvent[] + >([]); const [reportOpen, setReportOpen] = useState(false); const requestId = useRef(0); @@ -65,10 +68,16 @@ export function ErrorReportNotice() { }, [loadEvents]); const openReport = () => { + setReportFallbackEvents(notificationEvents); setReportOpen(true); setNotificationEvents([]); }; + const closeReport = () => { + setReportOpen(false); + setReportFallbackEvents([]); + }; + return ( <> {notificationEvents.length ? ( @@ -103,7 +112,8 @@ export function ErrorReportNotice() { ) : null} setReportOpen(false)} + fallbackEvents={reportFallbackEvents} + onClose={closeReport} /> ); diff --git a/apps/ai-game-creator-shell/tests/ErrorReportDialog.test.tsx b/apps/ai-game-creator-shell/tests/ErrorReportDialog.test.tsx index 33ca9afc1..cf8e05694 100644 --- a/apps/ai-game-creator-shell/tests/ErrorReportDialog.test.tsx +++ b/apps/ai-game-creator-shell/tests/ErrorReportDialog.test.tsx @@ -64,4 +64,18 @@ describe('ErrorReportDialog', () => { ).toBeTruthy(); expect(screen.queryByText('当前没有待报告的错误。')).toBeNull(); }); + + it('最新快照读取失败时使用打开通知时的事件快照', async () => { + mockState.getPending.mockRejectedValue(new Error('瞬时读取失败')); + mockState.readLogs.mockResolvedValue([]); + + render( + , + ); + + expect(await screen.findByText(event.message)).toBeTruthy(); + expect( + screen.getByText('最新错误读取失败,已使用打开通知时的快照'), + ).toBeTruthy(); + }); }); diff --git a/apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx b/apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx index 5bf74a1da..1fbe7b85d 100644 --- a/apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx +++ b/apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx @@ -1,6 +1,12 @@ /** @vitest-environment jsdom */ -import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; const mockState = vi.hoisted(() => ({ @@ -8,6 +14,10 @@ const mockState = vi.hoisted(() => ({ listeners: [] as Array<() => void>, })); +const dialogState = vi.hoisted(() => ({ + fallbackEvents: [] as (typeof event)[], +})); + vi.mock('../src/services/errorReporting', () => ({ getPendingClientErrorEvents: mockState.getPending, subscribeClientErrorEvents: (listener: () => void) => { @@ -21,7 +31,16 @@ vi.mock('../src/services/errorReporting', () => ({ })); vi.mock('../src/components/error-report/ErrorReportDialog', () => ({ - ErrorReportDialog: () => null, + ErrorReportDialog: ({ + fallbackEvents, + open, + }: { + fallbackEvents?: (typeof event)[]; + open: boolean; + }) => { + dialogState.fallbackEvents = fallbackEvents ?? []; + return open ?
: null; + }, })); import { ErrorReportNotice } from '../src/components/error-report/ErrorReportNotice'; @@ -64,4 +83,16 @@ describe('ErrorReportNotice', () => { expect(await screen.findByText('发现问题')).toBeTruthy(); expect(mockState.getPending).toHaveBeenCalledTimes(2); }); + + it('打开报告时保留通知快照作为对话框 fallback', async () => { + mockState.getPending.mockResolvedValue([event]); + + render(); + await screen.findByText('发现问题'); + + fireEvent.click(screen.getByRole('button', { name: '查看并报告' })); + + expect(await screen.findByTestId('error-report-dialog')).toBeTruthy(); + expect(dialogState.fallbackEvents).toEqual([event]); + }); }); diff --git a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md index 8d19e96ab..1b68df9fe 100644 --- a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md +++ b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md @@ -13,6 +13,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事 - Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。 - 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中当前快照中的全部事件,用户可取消不想提交的事件。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。 - 报告面板读取当前错误快照失败时,必须明确显示“错误事件暂不可用,请关闭后重试”,不能把失败误显示为“当前没有待报告的错误”。 +- 通知中的“查看并报告”打开面板时必须保留该次通知快照;最新快照读取瞬时失败时使用这份 fallback 继续展示和提交,不能因先清空通知而丢失用户刚看到的事件。 - Rust 是结构化错误队列的唯一真相源:`src-tauri/src/error_report/` 负责脱敏、调用点指纹、eventId、计数、100 条上限、5 秒聚合和 ack 生命周期;WebView 仅通过 Tauri bridge 上报、读取快照并保存短暂 React 展示状态,不维护第二份事件 Map。Runtime 错误上报统一使用 `source=agent-runtime`、`action=agent-runtime` 和 Agent ID 作为 page;包含 `kind=codex-app-server-*` 或 `codex-app-server-error:*` 的消息在入池前归一为稳定类别(例如 `codex-app-server-error:other`),不把 public summary 中的动态 fingerprint/长度作为分桶输入。Rust emit 只作为无状态唤醒,携带单调递增的 generation,不携带错误正文或事件 ID。 - 错误事件先在当前进程内存池按 fingerprint 合并,经过 5 秒聚合后只发出一次非阻塞存在性唤醒;WebView 挂载、收到唤醒、重新获得焦点或恢复可见时都查询完整未 ack 快照,并在桥接暂时失败时做有限退避重试。通知支持“查看并报告”和“忽略”,同一 fingerprint 仅在新增时唤醒一次。通知不直接打开阻塞式报告面板;用户打开报告面板后,面板再读取一次完整未 ack 快照,确保提交使用打开时的最新事件。忽略只关闭当前 UI,不删除事件;提交成功后由 bridge ack/delete 选中事件。 - 上传失败只在当前进程显示失败并允许用户再次提交,不跨重启恢复事件池,不后台自动重试。