fix: v0.6.1 修复回复/推理期间偶发崩溃 — 浏览器回退窗口竞态 + 崩溃可观测性
CI / 类型检查 + Lint + 单元测试 (push) Failing after 5m41s
CI / 全量测试 (Electron ABI) (push) Failing after 5m21s
CI / 产物编译验证 (push) Successful in 10m1s

【根因(实证归因,非猜测)】
分析 userData/logs/main.log 全部 33 次启动会话,定位 3 处异常终止点
(07-25 ×2 / 08-22 ×1,启动标记前无 Database closed)。三处 100% 共享
同一模式:web_search 并行抓取 → 多个 web_fetch 同时进入浏览器回退 →
共享单例 BrowserWindowManager 中后到 open() 销毁前一个正在加载/执行
JS 的窗口。关键统计:56 次浏览器回退中 ERR_ABORTED(并发互毁的直接
证据)仅 3 次,而这 3 次恰好全部对应 3 个崩溃点;无并发销毁的 53 次
回退从未崩溃 —— 触发条件完全收敛。

缺陷链(三层叠加):
1. browserFetch 直接 open/evaluate 共享单例,无跨调用序列化 — 并发
   回退互相销毁窗口(ERR_ABORTED / "Object has been destroyed")
2. destroy() 对仍在使用中的 partition fire-and-forget
   clearStorageData/clearCache,与紧随其后的新窗口创建并发 —
   原生存储层竞态(崩溃引爆点)
3. ensureReady 检查与实际 executeJavaScript/loadURL 之间存在竞态窗口;
   loadURLWithTimeout 的 Race 落败方 rejection 无人处理

【修复(browser-window-manager.ts + web-fetch.ts + browser.ts)】
- 新增 fetchPageText:排队版页面抓取,串行化完整 open→等待→evaluate
  序列(与 open 共用单一操作链,destroy 只会在链上发生,跨链互毁彻底
  消除);web_fetch 浏览器回退改走此入口
- open() 拆分 openInternal(链内直调);open 与 fetchPageText 共用
  单一串行链,排队不分死锁
- destroy() 移除 session 存储清理(终态清理迁移至 close(),await 执行,
  不再与窗口创建并发)
- safeWebContents() 即时校验替代 racy 的 ensureReady;evaluate/extract/
  screenshot/click/type/scroll/waitForSelector 全部加固,消除对已销毁
  webContents 的调用
- loadURLWithTimeout 落败方 rejection 兜底(防 unhandledRejection)
- cleanupBrowser/cleanup/close 异步化适配(main.ts 退出链路 await)

【崩溃可观测性(此前崩溃无迹可查 — 日志无声截断)】
- process.on(uncaughtException/unhandledRejection) → [FATAL] 落盘
- app.on(render-process-gone/child-process-gone) → [FATAL] 落盘
- WindowManager: 每窗口 render-process-gone 日志 + 自动 reload 自愈
  (渲染进程 OOM/崩溃不再白屏卡死,可自动恢复)

【验证】
- lint 0/0;typecheck 双工程 0 错误;test:electron 252/252;build 通过
This commit is contained in:
2026-08-22 19:16:41 +08:00
parent 6b2b587c94
commit 80cf5b482c
7 changed files with 357 additions and 167 deletions
+18 -1
View File
@@ -86,7 +86,24 @@ export class WindowManager {
win.on('closed', () => {
this.windows.delete(id);
if (this.activeWindowId === id) {
this.activeWindowId = this.windows.size > 0 ? this.windows.keys().next().value ?? null : null;
this.activeWindowId =
this.windows.size > 0 ? (this.windows.keys().next().value ?? null) : null;
}
});
// 崩溃可观测性 + 自愈: 渲染进程崩溃(OOM/原生崩溃)时记录归因日志并自动重载,
// 替代静默白屏(用户此前感知为"应用崩溃"且无从恢复)
win.webContents.on('render-process-gone', (_event, details) => {
log.error(
`[WindowManager] render-process-gone (id=${win.id}): reason=${details.reason} exitCode=${details.exitCode}`,
);
if (!win.isDestroyed() && details.reason !== 'clean-exit') {
try {
win.webContents.reload();
log.info(`[WindowManager] Window ${id} reloaded after renderer crash`);
} catch (err) {
log.error(`[WindowManager] Reload after crash failed:`, err);
}
}
});