修复 Tauri 错误报告事件订阅拒绝

错误报告桥在浏览器和未初始化 WebView 中安全降级
优先复用全局 Tauri 事件桥并收口订阅拒绝
新增无 Tauri 环境回归测试
补充事件桥 fail-safe 排障记忆
This commit is contained in:
2026-09-02 11:32:31 +08:00
parent 97eabbf4c6
commit 04c5a92f2a
4 changed files with 53 additions and 6 deletions
@@ -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?.();
@@ -31,8 +31,33 @@ export function ackErrorReports(eventIds: string[]) {
export function subscribeErrorReportUpdates(
listener: () => void,
): Promise<UnlistenFn> {
return listen<ErrorReportUpdate>('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<ErrorReportUpdate>('error-report-updated', handleUpdate).catch(
() => () => {},
);
}
@@ -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();
});
});
@@ -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。