diff --git a/apps/ai-game-creator-shell/src/services/errorReporting.ts b/apps/ai-game-creator-shell/src/services/errorReporting.ts index 5f8ed9bed..0a1648098 100644 --- a/apps/ai-game-creator-shell/src/services/errorReporting.ts +++ b/apps/ai-game-creator-shell/src/services/errorReporting.ts @@ -143,10 +143,16 @@ export function subscribeClientErrorEvents(listener: () => void) { let unlisten: (() => void) | undefined; void subscribeErrorReportUpdates(() => { if (!disposed) listener(); - }).then((stop) => { - if (disposed) stop(); - else unlisten = stop; - }); + }) + .then((stop) => { + if (disposed) stop(); + else unlisten = stop; + }) + .catch(() => { + // Browser previews and partially initialized native webviews do not have + // an event bridge; snapshots and focus/visibility refreshes remain the + // source of truth in those environments. + }); return () => { disposed = true; unlisten?.(); diff --git a/apps/ai-game-creator-shell/src/services/errorReportingBridge.ts b/apps/ai-game-creator-shell/src/services/errorReportingBridge.ts index afbb503d3..1e36fb9ee 100644 --- a/apps/ai-game-creator-shell/src/services/errorReportingBridge.ts +++ b/apps/ai-game-creator-shell/src/services/errorReportingBridge.ts @@ -31,8 +31,33 @@ export function ackErrorReports(eventIds: string[]) { export function subscribeErrorReportUpdates( listener: () => void, ): Promise { - return listen('error-report-updated', (event) => { + const handleUpdate = (event: { payload: ErrorReportUpdate }) => { void event.payload; listener(); - }); + }; + + // 客户端也会运行在浏览器预览和 Vitest 的 jsdom 中。Tauri API 模块默认 + // `__TAURI_INTERNALS__` 已存在,缺失时会拒绝,因此原生 WebView 之外保持 + // 桥接为空操作。启用 `withGlobalTauri` 时优先使用全局桥接,让测试替身与 + // 客户端其它事件订阅复用同一入口。 + if (typeof window !== 'undefined') { + const globalListen = window.__TAURI__?.event?.listen; + if (globalListen) { + return globalListen('error-report-updated', handleUpdate).catch( + () => () => {}, + ); + } + + const internals = (window as Window & { __TAURI_INTERNALS__?: unknown }) + .__TAURI_INTERNALS__; + if (!internals) { + return Promise.resolve(() => {}); + } + } else { + return Promise.resolve(() => {}); + } + + return listen('error-report-updated', handleUpdate).catch( + () => () => {}, + ); } diff --git a/apps/ai-game-creator-shell/tests/errorReporting.test.ts b/apps/ai-game-creator-shell/tests/errorReporting.test.ts index 7554032b0..4fb1747c9 100644 --- a/apps/ai-game-creator-shell/tests/errorReporting.test.ts +++ b/apps/ai-game-creator-shell/tests/errorReporting.test.ts @@ -89,6 +89,7 @@ import { resetClientErrorEventsForTests, shouldCaptureClientError, submitErrorReportBatch, + subscribeClientErrorEvents, } from '../src/services/errorReporting'; describe('客户端错误报告池', () => { @@ -184,4 +185,12 @@ describe('客户端错误报告池', () => { uninstall(); consoleInfo.mockRestore(); }); + + it('浏览器预览中订阅错误事件不会产生未处理拒绝', async () => { + delete window.__TAURI__; + + const unsubscribe = subscribeClientErrorEvents(vi.fn()); + unsubscribe(); + await Promise.resolve(); + }); }); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 459cafb65..e2e02cb8c 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -2,6 +2,13 @@ > 当前口径:本文件保留可复用的排障经验;历史条目的旧路由、旧版本和已删除文档仅作根因背景,不得据此恢复退役入口。当前命令、路由和 schema 以代码与 `docs/README.md` 为准。 +## 2026-09-02 Tauri 事件桥在浏览器预览中必须 fail-safe + +- **现象**:Vitest/jsdom 挂载 AGC 客户端时,错误报告通知调用 `@tauri-apps/api/event.listen`,因缺少 `window.__TAURI_INTERNALS__` 产生未处理拒绝;测试断言虽通过,CI 仍以 unhandled errors 失败。 +- **原因**:错误报告订阅是非阻塞唤醒通道,不能假定所有渲染环境都已初始化 Tauri IPC;模块级 `listen` 在 API 调用前就会访问 `transformCallback`,仅在调用方包一层 `.then` 无法消除该环境差异。 +- **处理**:订阅桥先复用 `window.__TAURI__.event.listen`(含 globalTauri/native shim),其次仅在 `__TAURI_INTERNALS__` 存在时调用模块 API;浏览器预览或订阅失败统一返回 no-op,并在消费层收口 rejection。错误快照读取、焦点和可见性刷新仍是权威路径。 +- **验证**:`errorReporting.test.ts` 覆盖无 Tauri 环境无未处理拒绝;`appSurface.test.ts` 全部 385 条用例通过且无 Vitest unhandled errors。 + ## 2026-08-27 Provider 成功 handoff 失败时需要保留本地私有原始响应 - **现象**:Provider 已返回响应,但 tool-plan handoff 因绝对路径或其它内容安全校验失败,Runtime 只留下 `failureKind`、哈希和被压平的 JSON pointer;排障时无法确认实际工具名和完整 arguments。