@glittering0717/ai-browser 2.1.15 → 2.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittering0717/ai-browser",
3
- "version": "2.1.15",
3
+ "version": "2.1.17",
4
4
  "description": "AI-native browser — renders pixels for humans, structured semantic-tree API for agents. No screenshots, no OCR, no coordinate guessing.",
5
5
  "main": "src/main/index.js",
6
6
  "bin": {
package/src/main/index.js CHANGED
@@ -257,14 +257,44 @@ app.on('child-process-gone', (_event, details) => {
257
257
  exit_code: details && details.exitCode,
258
258
  service: details && details.serviceName,
259
259
  });
260
+ // GPU 崩溃自动降级(见下面 disableHardwareAcceleration 的说明)。
261
+ // 只在"开着硬件加速"时计数;已经降级过就不再折腾。
262
+ const type = details && details.type;
263
+ const reason = details && details.reason;
264
+ if (!gpuDisabled && type === 'GPU' && reason && reason !== 'clean-exit') {
265
+ const now = Date.now();
266
+ gpuCrashes = gpuCrashes.filter((t) => now - t < 120000).concat(now);
267
+ if (gpuCrashes.length >= 2) {
268
+ try { fs.writeFileSync(gpuOffMarker, new Date().toISOString() + ' gpu crash fallback\n'); } catch (e) {}
269
+ recordError('gpu_fallback', { crashes: gpuCrashes.length, reason });
270
+ try { app.relaunch(); } catch (e) {}
271
+ try { app.exit(0); } catch (e) {}
272
+ }
273
+ }
260
274
  });
275
+ let gpuCrashes = [];
261
276
 
262
- // Disable GPU compositing to avoid a whole class of GPU driver crashes
263
- // (exit_code=6) that take down the whole app on some systems. This only turns
264
- // off hardware acceleration — the window itself stays a real, visible window
265
- // and a human can drive it with mouse/keyboard at any time (login, QR scan,
266
- // captcha, correcting the agent).
267
- app.disableHardwareAcceleration();
277
+ // === 硬件加速:默认开,GPU 连续崩溃自动退回软件合成 ===
278
+ //
279
+ // 历史:2026-10 为了让某些机器上的 GPU 崩溃(exit_code=6)别把整个 app 拖死,
280
+ // 这里无条件 disableHardwareAcceleration()。代价是**所有页面都走软件合成**,
281
+ // 而"整窗内容被缩放一帧""刷新时先缩成一小块再恢复""内容缩在左上角"这类绘制异常
282
+ // 几乎都长在软件合成上 —— 合成器手里的 surface 尺寸和图层几何脱节时,软件路径会
283
+ // 把那张图**缩放**着贴出来,GPU 路径则按图层几何重新栅格化。
284
+ //
285
+ // 现在的策略:
286
+ // - 默认开硬件加速(Apple Silicon 上更快,也不出这类缩放异常)
287
+ // - 连续两次 GPU 进程崩溃(2 分钟内)→ 写标记 + 自动重启到软件合成,
288
+ // 之后一直用软的(删掉 ~/.ai-browser/.gpu-off 可恢复)。绝不因为一次偶发崩溃
289
+ // 就把所有人降级到软件渲染。
290
+ // - AI_BROWSER_GPU=on|off 可强制;off 等价于旧行为。
291
+ const gpuOffMarker = path.join(config.userDataDir, '.gpu-off');
292
+ const gpuPref = String(process.env.AI_BROWSER_GPU || 'auto').toLowerCase();
293
+ let gpuDisabled = gpuPref === 'off';
294
+ if (!gpuDisabled && gpuPref !== 'on') {
295
+ try { gpuDisabled = fs.existsSync(gpuOffMarker); } catch (e) { gpuDisabled = false; }
296
+ }
297
+ if (gpuDisabled) app.disableHardwareAcceleration();
268
298
 
269
299
  // Disable Chromium sandbox — required when launched from restricted
270
300
  // environments (e.g. TRAE IDE shell) where the sandbox helper cannot
@@ -398,6 +428,10 @@ function createWindow() {
398
428
 
399
429
  // Init PageManager
400
430
  pageManager = PageManager.getInstance(mainWindow);
431
+ // 渲染路径是"硬件合成"还是"软件合成",直接决定绘制异常的面貌(见
432
+ // disableHardwareAcceleration 一段)。让外部能查到这一位:ui.server_info 会带出来,
433
+ // 否则"页面上这一帧的问题"和"这条路的问题"永远分不开。
434
+ pageManager.renderPath = { gpu: !gpuDisabled, reason: gpuPref === 'off' ? 'env' : (gpuDisabled ? 'fallback' : 'default') };
401
435
  pageManager._preloadPath = path.join(__dirname, '../preload/bridge.cjs');
402
436
 
403
437
  // Create first tab.
@@ -707,7 +707,10 @@ class PageManager {
707
707
  } catch (e) {
708
708
  return this._navigateFailure(e, view, tid, waitUntil);
709
709
  }
710
- this.repaint(tid);
710
+ // 这里**不做**重绘(2026-10-25 修)。以前每次 navigate 结束都调 repaint(),
711
+ // 而 repaint() 走 webContents.invalidate() —— 在新页面刚出现的那一刻强制整页
712
+ // 重栅格化,合成器会先把画面按初始尺寸画一遍再缩放,用户看到的就是
713
+ // "先缩成一小块、闪一下、然后正常"。导航本身会正常出帧,不需要我们推。
711
714
  if (waitUntil === 'load') return { ok: true, waited_for: 'load' };
712
715
  // load 还没来是**正常**的(SPA 本来就不 fire load)。如实说明,并给出
713
716
  // 下一步:调用方需要等某个具体内容时该用 ui.wait,而不是等 load。
@@ -3434,19 +3437,20 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
3434
3437
  // so did-finish-load has already fired and repainted an empty shell.
3435
3438
  // did-navigate — any cross-document navigation, including programmatic
3436
3439
  // location.href assignment the app does on its own.
3437
- if (url) {
3438
- const repaint = () => { self._scheduleRepaint(tabId); };
3439
- view.webContents.on('dom-ready', repaint);
3440
- view.webContents.on('did-finish-load', repaint);
3441
- view.webContents.on('did-navigate', repaint);
3442
- view.webContents.on('did-navigate-in-page', repaint);
3443
- view.webContents.on('did-frame-finish-load', repaint);
3444
- } else {
3445
- // No URL at construction time (new_tab with no url, then navigate):
3446
- // register the same hooks now, or the later navigate() would never
3447
- // repaint — which was one of the paths that showed a stale view.
3448
- this._attachRepaintHooks(tabId, view);
3449
- }
3440
+ // ⚠️ 导航事件**不挂任何重绘**(2026-10-25 修)。
3441
+ //
3442
+ // 历史:这条钩子是为了对付"页面已导航但窗口还显示旧画面",手段是
3443
+ // removeBrowserView + addBrowserView(后来换成 invalidate)。但两者都有代价:
3444
+ // remove/add 会闪一帧白;invalidate() 更麻烦 —— 它强制整页重新栅格化,
3445
+ // 而在导航刚开始的那一刻,图层的尺寸还是初始值,于是合成器会先把新页面画进
3446
+ // 一个小尺寸 surface 再缩放显示:用户看到的就是"先缩成左上角一小块,闪一下,
3447
+ // 然后正常"(2026-10-25 用户报"刷新 baidu 总是一闪一闪")。
3448
+ //
3449
+ // 现代 Chromium 在正常情况下不会留旧画面;真留住了也有兜底:
3450
+ // - 白屏/缩放自检(_auditPaint)会判 scale-mismatch 并**定向**重绘
3451
+ // - ui.repaint 显式重绘
3452
+ // - 几何变化由 afterGeometryChange 负责收敛
3453
+ // 所以这里什么都不挂。回归:npm run layout 的 G5 断言"一次导航不引发任何重绘"。
3450
3454
 
3451
3455
  this._attachLoadFailureHooks(tabId, view);
3452
3456
  // "画出来了没有"只能主动看(没有对应事件),"渲染进程崩了"要自己救
@@ -3536,27 +3540,12 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
3536
3540
  * Idempotent per tab: re-registering would schedule redundant remove/add
3537
3541
  * cycles, so a WeakSet of hooked views guards it.
3538
3542
  */
3539
- _attachRepaintHooks(tabId, view) {
3540
- if (!view || !view.webContents || view.webContents.isDestroyed()) return;
3541
- if (!this._repaintHooked) this._repaintHooked = new WeakSet();
3542
- if (this._repaintHooked.has(view)) return;
3543
- this._repaintHooked.add(view);
3544
- const repaint = () => { this._scheduleRepaint(tabId); };
3545
- view.webContents.on('dom-ready', repaint);
3546
- view.webContents.on('did-finish-load', repaint);
3547
- view.webContents.on('did-navigate', repaint);
3548
- view.webContents.on('did-navigate-in-page', repaint);
3549
- view.webContents.on('did-frame-finish-load', repaint);
3550
- }
3551
-
3552
3543
  /**
3553
- * Coalesce repaint requests. A single page load fires dom-ready +
3554
- * did-navigate + did-navigate-in-page + did-frame-finish-load within a few
3555
- * hundred ms; a load like ComfyUI's fires dozens of did-frame-finish-load.
3556
- * Without coalescing that is dozens of remove/add cycles — visible flicker
3557
- * and wasted compositing work. Debounce ~120ms: late enough to collapse the
3558
- * burst, early enough that the user sees the new view essentially at once.
3544
+ * 已废弃:以前用来给"后来才导航的 tab"补挂重绘钩子。现在导航事件一律不重绘
3545
+ * (原因见 newTab 里的长注释),这里只留一个空实现,避免调用点报错。
3559
3546
  */
3547
+ _attachRepaintHooks() {}
3548
+
3560
3549
  _scheduleRepaint(tabId, delay = 120) {
3561
3550
  const prev = this._repaintTimers.get(tabId);
3562
3551
  if (prev) clearTimeout(prev);
@@ -317,6 +317,9 @@ export function startWSServer(pageManager, port = config.wsPort, onQuit = null)
317
317
  started_at: serverStartedAt,
318
318
  ws_port: port,
319
319
  platform: process.platform,
320
+ // 渲染路径:{gpu:true|false, reason:'default'|'env'|'fallback'}。
321
+ // 白屏/缩放类缺陷的复现与"这条路有没有关系"都要靠它。
322
+ ...(pageManager.renderPath ? { render_path: pageManager.renderPath } : {}),
320
323
  },
321
324
  });
322
325
  break;
@@ -2,4 +2,4 @@
2
2
  // Kept in JS (not imported from package.json) to avoid Node import-attribute
3
3
  // (JSON module) compatibility issues inside Electron. package.json "version"
4
4
  // is manually synced to this value; tests assert they match.
5
- export const VERSION = '2.1.15';
5
+ export const VERSION = '2.1.17';