@glittering0717/ai-browser 2.1.17 → 2.1.18

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.17",
3
+ "version": "2.1.18",
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": {
@@ -3727,41 +3727,46 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
3727
3727
  let stats = null;
3728
3728
  let frameError = null;
3729
3729
  try {
3730
- // 位图来源优先走 CDP(和 ui.screenshot 同一条路),而不是
3731
- // webContents.capturePage()。实测(2026-10-25,e2e/paint 与真机):
3732
- // capturePage 对**被其它窗口盖住**的窗口返回空图(isEmpty()===true),
3733
- // 而"窗口被盖住"恰恰是本产品最常见的状态(不抢焦点原则)。用 capturePage
3734
- // 就等于在最需要观测的场景下失明 —— 判出来是 no-frame,什么都没看见。
3735
- // CDP 的 Page.captureScreenshot 走渲染器自己的 surface,被遮挡也拿得到。
3730
+ // 位图来源,优先级从"最不容易打扰合成器"往下排:
3736
3731
  //
3737
- // clip.scale=0.25:判"是不是白的"不需要细节,缩到 1/4 后像素数从 350 万
3738
- // 降到 22 万,统计从几十毫秒降到几毫秒,而且和用户看到的是同一帧内容。
3739
- const w = bounds && bounds.width ? Math.max(1, Math.round(bounds.width)) : 0;
3740
- const h = bounds && bounds.height ? Math.max(1, Math.round(bounds.height)) : 0;
3741
- let shot = null;
3742
- if (w && h) {
3743
- try {
3744
- this._ensureCdp(tid, view, 'Page');
3745
- shot = await withDeadline(
3746
- view.webContents.debugger.sendCommand('Page.captureScreenshot', {
3747
- format: 'png',
3748
- fromSurface: true,
3749
- clip: { x: 0, y: 0, width: w, height: h, scale: 0.25 },
3750
- }),
3751
- 4000, 'paint_shot'
3752
- );
3753
- } catch (_) { /* 落到 capturePage 兜底 */ }
3754
- }
3755
- if (shot && shot.data) {
3756
- const buf = Buffer.from(shot.data, 'base64');
3757
- const img = nativeImage.createFromBuffer(buf);
3758
- if (img && !img.isEmpty()) stats = blankPage.statsFromBitmap(img.toBitmap());
3759
- }
3732
+ // 1) webContents.capturePage() —— 不碰 CDP 的 Page 域、没有 capture 模式,
3733
+ // 只是把视图自己的 surface 拷一份。窗口被别的窗口盖住时会返回空图
3734
+ // (isEmpty()),这时跳过、交给下一档,绝不据此判"页面是白的"。
3735
+ // 2) CDP Page.captureScreenshot,**不带 clip / scale**。
3736
+ //
3737
+ // ⚠️ 2026-10-25 血泪:这里原本是
3738
+ // clip: {x:0,y:0,width:w,height:h, scale:0.25}
3739
+ // 图省事把统计用的图直接缩到 1/4。带 clip.scale 的截图会让 Chromium
3740
+ // **按该比例临时重建 surface**;自检又恰好在 did-finish-load+450ms 跑,
3741
+ // 那几十毫秒里窗口往往正在重新合成,于是把这张 1/4 的图贴上去 1–3 帧,
3742
+ // 用户看到的就是"每次加载先小一下再恢复"。实测抓到的那一帧内容宽度
3743
+ // 正好是 25%,与 clip.scale 一致。
3744
+ // 结论:**缩放只许在我们自己这边做**(nativeImage.resize),不许交给合成器。
3745
+ try {
3746
+ const img0 = await withDeadline(wc.capturePage(), 3000, 'capturePage');
3747
+ if (img0 && typeof img0.isEmpty === 'function' && !img0.isEmpty()) {
3748
+ const small0 = img0.resize({ width: 240 });
3749
+ stats = blankPage.statsFromBitmap(small0.toBitmap());
3750
+ }
3751
+ } catch (_) { /* 视图被遮挡/正在销毁:交给 CDP 档 */ }
3760
3752
  if (!stats) {
3761
- const img = await withDeadline(wc.capturePage(), 4000, 'capturePage');
3762
- if (img && typeof img.isEmpty === 'function' && !img.isEmpty()) {
3763
- const small = img.resize({ width: 240 });
3764
- stats = blankPage.statsFromBitmap(small.toBitmap());
3753
+ const shot = await withDeadline(
3754
+ (() => {
3755
+ this._ensureCdp(tid, view, 'Page');
3756
+ return view.webContents.debugger.sendCommand('Page.captureScreenshot', {
3757
+ format: 'jpeg',
3758
+ quality: 70,
3759
+ fromSurface: true,
3760
+ });
3761
+ })(),
3762
+ 5000, 'paint_shot'
3763
+ ).catch(() => null);
3764
+ if (shot && shot.data) {
3765
+ const img = nativeImage.createFromBuffer(Buffer.from(shot.data, 'base64'));
3766
+ if (img && !img.isEmpty()) {
3767
+ const small = img.resize({ width: 240 });
3768
+ stats = blankPage.statsFromBitmap(small.toBitmap());
3769
+ }
3765
3770
  }
3766
3771
  }
3767
3772
  } catch (e) {
@@ -3869,6 +3874,43 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
3869
3874
  || actual.x !== expected.x
3870
3875
  || actual.y !== expected.y;
3871
3876
  if (!bad) return { ok: true, expected, actual };
3877
+ // 不一致时先复核一次再动手:窗口刚变化的那一两百毫秒里,getContentBounds 与
3878
+ // 视图 bounds 会短暂对不上,此刻"纠正"等于把视图改成中间尺寸 —— 那正是
3879
+ // "页面缩成左上角一小块"的成因之一。宁可晚 120ms 修,也不要修错。
3880
+ return this._confirmAndFixLayout(tabId, expected, actual);
3881
+ }
3882
+
3883
+ _confirmAndFixLayout(tabId, expected, actual) {
3884
+ const view = this._getView(tabId);
3885
+ if (!view) return { ok: false, expected, actual, fixed: false };
3886
+ const t = setTimeout(() => {
3887
+ try {
3888
+ const v = this._getView(tabId);
3889
+ if (!v || !v.webContents || v.webContents.isDestroyed()) return;
3890
+ let cb;
3891
+ try { cb = this.window.getContentBounds(); } catch (_) { return; }
3892
+ const exp = {
3893
+ x: 0,
3894
+ y: config.tabBarHeight,
3895
+ width: cb.width,
3896
+ height: Math.max(0, cb.height - config.tabBarHeight - (this._bottomInset || 0)),
3897
+ };
3898
+ let act;
3899
+ try { act = v.getBounds(); } catch (_) { return; }
3900
+ const stillBad = act.width < exp.width - 2
3901
+ || Math.abs(act.height - exp.height) > 2
3902
+ || act.x !== exp.x || act.y !== exp.y;
3903
+ if (!stillBad) return; // 只是中间态,已经自己好了
3904
+ try {
3905
+ this._layoutView(v);
3906
+ v.webContents.invalidate();
3907
+ } catch (_) { /* 视图正在销毁 */ }
3908
+ recordError('layout_mismatch', { tab: tabId, expected: exp, actual: act });
3909
+ this._broadcast('layout_mismatch', { tab: tabId, expected: exp, actual: act });
3910
+ } catch (_) { /* 忽略 */ }
3911
+ }, 120);
3912
+ if (t.unref) t.unref();
3913
+ return { ok: false, expected, actual, fixed: 'deferred' };
3872
3914
  try {
3873
3915
  this._layoutView(view);
3874
3916
  view.webContents.invalidate();
@@ -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.17';
5
+ export const VERSION = '2.1.18';