@glittering0717/ai-browser 2.1.16 → 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 +1 -1
- package/src/main/index.js +40 -6
- package/src/main/page_manager.js +75 -33
- package/src/main/ws_server.js +3 -0
- package/src/shared/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glittering0717/ai-browser",
|
|
3
|
-
"version": "2.1.
|
|
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": {
|
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
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
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.
|
package/src/main/page_manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
3738
|
-
//
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
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
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
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();
|
package/src/main/ws_server.js
CHANGED
|
@@ -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;
|
package/src/shared/version.js
CHANGED
|
@@ -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.
|
|
5
|
+
export const VERSION = '2.1.18';
|