From 04c5a92f2ab8a55f258260272d74d910a8cfae00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 2 Sep 2026 11:32:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Tauri=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E4=BA=8B=E4=BB=B6=E8=AE=A2=E9=98=85=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=20=E9=94=99=E8=AF=AF=E6=8A=A5=E5=91=8A=E6=A1=A5?= =?UTF-8?q?=E5=9C=A8=E6=B5=8F=E8=A7=88=E5=99=A8=E5=92=8C=E6=9C=AA=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=20WebView=20=E4=B8=AD=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=20=E4=BC=98=E5=85=88=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E5=85=A8=E5=B1=80=20Tauri=20=E4=BA=8B=E4=BB=B6=E6=A1=A5?= =?UTF-8?q?=E5=B9=B6=E6=94=B6=E5=8F=A3=E8=AE=A2=E9=98=85=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E6=97=A0=20Tauri=20=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E6=A1=A5=20fail-safe=20=E6=8E=92=E9=9A=9C?= =?UTF-8?q?=E8=AE=B0=E5=BF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/errorReporting.ts | 14 ++++++--- .../src/services/errorReportingBridge.ts | 29 +++++++++++++++++-- .../tests/errorReporting.test.ts | 9 ++++++ docs/project-memory/shared-memory/pitfalls.md | 7 +++++ 4 files changed, 53 insertions(+), 6 deletions(-) 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。