@glittering0717/ai-browser 2.1.14 → 2.1.16
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 +13 -5
- package/src/main/page_manager.js +128 -73
- package/src/main/ws_server.js +3 -1
- 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.16",
|
|
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
|
@@ -524,17 +524,25 @@ function createWindow() {
|
|
|
524
524
|
// 触发点选这几个:窗口缩放、进出全屏(macOS 全屏会换 display)、显示器参数变化
|
|
525
525
|
// (分辨率/缩放/插拔外接屏)、系统从睡眠唤醒 —— 这四类都会让"窗口几何"和
|
|
526
526
|
// "合成器手里的 surface 尺寸"脱节,是最常见的来源。
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
527
|
+
// 几何变化后不能只重画:还要确认"渲染视口真的跟上了新尺寸"(见 afterGeometryChange
|
|
528
|
+
// 的注释 —— 视图 800 而页面还按 1280 排版,就是用户看到的"大一下小一下")。
|
|
529
|
+
const afterGeometry = () => { try { pageManager.afterGeometryChange(); } catch (e) {} };
|
|
530
|
+
let geomTimer = null;
|
|
531
|
+
const afterGeometrySoon = (ms = 150) => {
|
|
532
|
+
if (geomTimer) clearTimeout(geomTimer);
|
|
533
|
+
geomTimer = setTimeout(afterGeometry, ms);
|
|
534
|
+
};
|
|
535
|
+
mainWindow.on('resize', () => afterGeometrySoon(150));
|
|
536
|
+
mainWindow.on('enter-full-screen', () => { layoutWithBanner(); afterGeometrySoon(120); });
|
|
537
|
+
mainWindow.on('leave-full-screen', () => { layoutWithBanner(); afterGeometrySoon(120); });
|
|
538
|
+
const onDisplayChange = () => { try { layoutWithBanner(); } catch (e) {} afterGeometrySoon(80); };
|
|
531
539
|
try {
|
|
532
540
|
screen.on('display-metrics-changed', onDisplayChange);
|
|
533
541
|
screen.on('display-added', onDisplayChange);
|
|
534
542
|
screen.on('display-removed', onDisplayChange);
|
|
535
543
|
} catch (e) { /* 无 screen 模块(极老版本) */ }
|
|
536
544
|
try {
|
|
537
|
-
powerMonitor.on('resume', () => { onDisplayChange();
|
|
545
|
+
powerMonitor.on('resume', () => { onDisplayChange(); afterGeometrySoon(400); });
|
|
538
546
|
} catch (e) { /* 无 powerMonitor */ }
|
|
539
547
|
mainWindow.on('app-command-added', (_e, cmd) => {
|
|
540
548
|
// macOS window-management commands (zoom / minimize / fullscreen).
|
package/src/main/page_manager.js
CHANGED
|
@@ -216,6 +216,10 @@ class PageManager {
|
|
|
216
216
|
this._openDialogs = new Map();
|
|
217
217
|
this._axDiffCache = new Map(); // tabId -> lite interactive snapshot (get_tree {mode:'diff'})
|
|
218
218
|
this._repaintTimers = new Map();// tabId -> 正在排队/已调度的重绘(合并同一批导航事件)
|
|
219
|
+
// 重绘计数:soft = 只 invalidate;hard = 额外 1px 微调(会重排两次,肉眼可见)。
|
|
220
|
+
// 高频路径(导航完成/窗口焦点)只许走 soft —— 这条要被回归钉住。
|
|
221
|
+
this._repaintCount = 0;
|
|
222
|
+
this._hardRepaintCount = 0;
|
|
219
223
|
// === 白屏自检(paint audit)===
|
|
220
224
|
// tabId -> {verdict, blank, white_ratio, unique, action, at, url}:最近一次自检的结论。
|
|
221
225
|
// 为什么必须有它:白屏是唯一一种"所有事件都成功、DOM 也正常、但用户看到的是空"的
|
|
@@ -703,7 +707,10 @@ class PageManager {
|
|
|
703
707
|
} catch (e) {
|
|
704
708
|
return this._navigateFailure(e, view, tid, waitUntil);
|
|
705
709
|
}
|
|
706
|
-
|
|
710
|
+
// 这里**不做**重绘(2026-10-25 修)。以前每次 navigate 结束都调 repaint(),
|
|
711
|
+
// 而 repaint() 走 webContents.invalidate() —— 在新页面刚出现的那一刻强制整页
|
|
712
|
+
// 重栅格化,合成器会先把画面按初始尺寸画一遍再缩放,用户看到的就是
|
|
713
|
+
// "先缩成一小块、闪一下、然后正常"。导航本身会正常出帧,不需要我们推。
|
|
707
714
|
if (waitUntil === 'load') return { ok: true, waited_for: 'load' };
|
|
708
715
|
// load 还没来是**正常**的(SPA 本来就不 fire load)。如实说明,并给出
|
|
709
716
|
// 下一步:调用方需要等某个具体内容时该用 ui.wait,而不是等 load。
|
|
@@ -3430,19 +3437,20 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3430
3437
|
// so did-finish-load has already fired and repainted an empty shell.
|
|
3431
3438
|
// did-navigate — any cross-document navigation, including programmatic
|
|
3432
3439
|
// location.href assignment the app does on its own.
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
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 断言"一次导航不引发任何重绘"。
|
|
3446
3454
|
|
|
3447
3455
|
this._attachLoadFailureHooks(tabId, view);
|
|
3448
3456
|
// "画出来了没有"只能主动看(没有对应事件),"渲染进程崩了"要自己救
|
|
@@ -3532,27 +3540,12 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3532
3540
|
* Idempotent per tab: re-registering would schedule redundant remove/add
|
|
3533
3541
|
* cycles, so a WeakSet of hooked views guards it.
|
|
3534
3542
|
*/
|
|
3535
|
-
_attachRepaintHooks(tabId, view) {
|
|
3536
|
-
if (!view || !view.webContents || view.webContents.isDestroyed()) return;
|
|
3537
|
-
if (!this._repaintHooked) this._repaintHooked = new WeakSet();
|
|
3538
|
-
if (this._repaintHooked.has(view)) return;
|
|
3539
|
-
this._repaintHooked.add(view);
|
|
3540
|
-
const repaint = () => { this._scheduleRepaint(tabId); };
|
|
3541
|
-
view.webContents.on('dom-ready', repaint);
|
|
3542
|
-
view.webContents.on('did-finish-load', repaint);
|
|
3543
|
-
view.webContents.on('did-navigate', repaint);
|
|
3544
|
-
view.webContents.on('did-navigate-in-page', repaint);
|
|
3545
|
-
view.webContents.on('did-frame-finish-load', repaint);
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
3543
|
/**
|
|
3549
|
-
*
|
|
3550
|
-
*
|
|
3551
|
-
* hundred ms; a load like ComfyUI's fires dozens of did-frame-finish-load.
|
|
3552
|
-
* Without coalescing that is dozens of remove/add cycles — visible flicker
|
|
3553
|
-
* and wasted compositing work. Debounce ~120ms: late enough to collapse the
|
|
3554
|
-
* burst, early enough that the user sees the new view essentially at once.
|
|
3544
|
+
* 已废弃:以前用来给"后来才导航的 tab"补挂重绘钩子。现在导航事件一律不重绘
|
|
3545
|
+
* (原因见 newTab 里的长注释),这里只留一个空实现,避免调用点报错。
|
|
3555
3546
|
*/
|
|
3547
|
+
_attachRepaintHooks() {}
|
|
3548
|
+
|
|
3556
3549
|
_scheduleRepaint(tabId, delay = 120) {
|
|
3557
3550
|
const prev = this._repaintTimers.get(tabId);
|
|
3558
3551
|
if (prev) clearTimeout(prev);
|
|
@@ -3571,43 +3564,83 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3571
3564
|
* (only the active view is normally attached, so re-adding an inactive one
|
|
3572
3565
|
* would show it — guard by re-asserting the active view afterwards).
|
|
3573
3566
|
*/
|
|
3574
|
-
|
|
3567
|
+
/**
|
|
3568
|
+
* 窗口几何变过之后调一次:重排 → 软重绘 → 160ms 后确认渲染视口真的跟上了。
|
|
3569
|
+
*
|
|
3570
|
+
* 为什么不能只 setBounds:实测(e2e/layout_ws 的 G2,2026-10-25)窗口从
|
|
3571
|
+
* 1280 缩到 800 之后,视图 bounds 已经是 800,但页面 `innerWidth` 还是 1280 ——
|
|
3572
|
+
* 也就是渲染器没接到/没应用这次尺寸变化。此时合成器会把 1280 宽的内容画进
|
|
3573
|
+
* 800 的框里,内容整体缩小或错位;等它某一刻突然追上,就是用户说的
|
|
3574
|
+
* "一闪一闪、大一下小一下"。
|
|
3575
|
+
* 确认的方式和判定口径一致:innerWidth 必须等于视图宽度。不等就做一次 hard
|
|
3576
|
+
* 重绘(1px 微调会强制 Chromium 重算 surface 与布局)—— 这时代价是值得的,
|
|
3577
|
+
* 因为画面本来就是错的。
|
|
3578
|
+
*/
|
|
3579
|
+
afterGeometryChange() {
|
|
3580
|
+
const tid = this.activeTab;
|
|
3581
|
+
const view = this._getView(tid);
|
|
3582
|
+
if (!view || !view.webContents || view.webContents.isDestroyed()) return;
|
|
3583
|
+
try { this._layoutView(view); } catch (_) { return; }
|
|
3584
|
+
this._forceRepaint(view); // soft:不可见
|
|
3585
|
+
const timer = setTimeout(async () => {
|
|
3586
|
+
try {
|
|
3587
|
+
const v = this._getView(tid);
|
|
3588
|
+
if (!v || !v.webContents || v.webContents.isDestroyed()) return;
|
|
3589
|
+
const b = v.getBounds();
|
|
3590
|
+
const raw = await withDeadline(this.evaluate('(function(){return String(window.innerWidth);})()', tid), 1500, 'geom_iw');
|
|
3591
|
+
const iw = Number((raw && typeof raw === 'object' && 'value' in raw) ? raw.value : raw) || 0;
|
|
3592
|
+
if (iw > 0 && b.width > 0 && Math.abs(iw - b.width) > 2) {
|
|
3593
|
+
recordError('layout_mismatch', {
|
|
3594
|
+
tab: tid, phase: 'after-resize',
|
|
3595
|
+
expected: { width: b.width }, actual: { innerWidth: iw },
|
|
3596
|
+
});
|
|
3597
|
+
this._forceRepaint(v, { hard: true });
|
|
3598
|
+
}
|
|
3599
|
+
} catch (_) { /* renderer 正在忙/已销毁:下一次 resize 会再来一次 */ }
|
|
3600
|
+
}, 160);
|
|
3601
|
+
if (timer.unref) timer.unref();
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
/**
|
|
3605
|
+
* 让合成器重新画一次,分两档。
|
|
3606
|
+
*
|
|
3607
|
+
* soft(默认):只 webContents.invalidate() —— 不改几何、不重排、不可见。
|
|
3608
|
+
* hard:额外做 1px 微调(width+1 再 width),因为**只有尺寸变化才会让 surface
|
|
3609
|
+
* 重新栅格化**(尺寸不变时 setBounds 被 Chromium 直接忽略)。
|
|
3610
|
+
*
|
|
3611
|
+
* 为什么要分档(2026-10-25 用户报"一闪一闪、大一下小一下"):
|
|
3612
|
+
* 1px 微调必然触发两次重排,文字会肉眼可见地跳一下;再叠 removeBrowserView /
|
|
3613
|
+
* addBrowserView 还会闪一帧白。把这套放在"每次导航完成 / 每次窗口焦点变化"
|
|
3614
|
+
* 这种高频路径上,就变成了用户看到的抽搐。
|
|
3615
|
+
* 所以高频路径一律 soft;只有"确认 surface 尺寸脱节"(scale-mismatch、白屏阶梯)
|
|
3616
|
+
* 才付 hard 的代价。
|
|
3617
|
+
*
|
|
3618
|
+
* 也不再 remove/add:那是过去没有 invalidate 时的唯一手段,代价是闪一帧白,
|
|
3619
|
+
* 而现在 invalidate 覆盖了它的作用。
|
|
3620
|
+
*/
|
|
3621
|
+
_forceRepaint(view, opts = {}) {
|
|
3622
|
+
const hard = opts && opts.hard === true;
|
|
3575
3623
|
const tabId = this._tabIdOfView(view);
|
|
3576
|
-
|
|
3624
|
+
if (tabId === null) return false;
|
|
3625
|
+
const wc = view.webContents;
|
|
3626
|
+
if (!wc || (typeof wc.isDestroyed === 'function' && wc.isDestroyed())) return false;
|
|
3577
3627
|
try {
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
// remove+add 只改变合成树里的位置,**不保证 surface 重新栅格化**:
|
|
3582
|
-
// 如果 surface 还是旧尺寸(窗口从大到小之后很常见),合成器会把那张大图
|
|
3583
|
-
// 缩小/裁到新 bounds 里 —— 用户看到的就是"内容缩成左上角一小块、其余全白"。
|
|
3584
|
-
// 真正的重栅格化要靠 invalidate();再叠一次 1px 微调,是因为
|
|
3585
|
-
// setBounds 尺寸没变时 Chromium 会直接忽略(不会重算 surface)。
|
|
3586
|
-
try { view.webContents.invalidate(); } catch (_) {}
|
|
3587
|
-
const b = view.getBounds();
|
|
3588
|
-
try {
|
|
3589
|
-
view.setBounds({ ...b, width: b.width + 1 });
|
|
3590
|
-
view.setBounds(b);
|
|
3591
|
-
} catch (_) {}
|
|
3592
|
-
} catch(e) { return false; }
|
|
3593
|
-
// Cumulative counter, reported by ui.repaint. Not just a debug aid: it is
|
|
3594
|
-
// the only externally observable proof that a repaint actually happened.
|
|
3595
|
-
// "The window looks stale" has no other symptom to check, and a caller
|
|
3596
|
-
// cannot poll for it — polling calls repaint itself, which *consumes* the
|
|
3597
|
-
// queued debounce timer and destroys the very evidence being looked for.
|
|
3598
|
-
this._repaintCount = (this._repaintCount || 0) + 1;
|
|
3599
|
-
// If we just re-added an inactive view it is now on top of the stack —
|
|
3600
|
-
// put the real active view back so the user sees the tab they selected.
|
|
3601
|
-
if (!wasActive) {
|
|
3602
|
-
const active = this._getView(this.activeTab);
|
|
3603
|
-
if (active) {
|
|
3604
|
-
try {
|
|
3605
|
-
this.window.removeBrowserView(view);
|
|
3606
|
-
this.window.addBrowserView(active);
|
|
3607
|
-
this._layoutView(active);
|
|
3608
|
-
} catch(e) {}
|
|
3609
|
-
}
|
|
3628
|
+
wc.invalidate();
|
|
3629
|
+
} catch (e) {
|
|
3630
|
+
return false;
|
|
3610
3631
|
}
|
|
3632
|
+
// Cumulative counter, reported by ui.repaint / ui.window_bounds. Not just a
|
|
3633
|
+
// debug aid: it is the only externally observable proof that a repaint
|
|
3634
|
+
// actually happened, and (for hard) that no geometry nudge leaked into a
|
|
3635
|
+
// hot path.
|
|
3636
|
+
this._repaintCount = (this._repaintCount || 0) + 1;
|
|
3637
|
+
if (!hard) return true;
|
|
3638
|
+
try {
|
|
3639
|
+
const b = view.getBounds();
|
|
3640
|
+
view.setBounds({ ...b, width: b.width + 1 });
|
|
3641
|
+
view.setBounds(b);
|
|
3642
|
+
this._hardRepaintCount = (this._hardRepaintCount || 0) + 1;
|
|
3643
|
+
} catch (e2) { /* 视图正在销毁 */ }
|
|
3611
3644
|
return true;
|
|
3612
3645
|
}
|
|
3613
3646
|
|
|
@@ -3741,9 +3774,28 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3741
3774
|
// 用 zoomFactor 把两者分开:页面缩放有明确的 zoom 值,surface 脱节时 zoom 仍是 1。
|
|
3742
3775
|
let zoom = null;
|
|
3743
3776
|
try { zoom = typeof wc.getZoomFactor === 'function' ? wc.getZoomFactor() : null; } catch (_) {}
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3777
|
+
let iw = Number(dom && dom.iw) || 0;
|
|
3778
|
+
let mismatched = bounds && bounds.width > 0 && iw > 0 && Math.abs(iw - bounds.width) > 2
|
|
3779
|
+
&& (zoom === null || Math.abs(zoom - 1) < 0.01);
|
|
3780
|
+
if (mismatched) {
|
|
3781
|
+
// 刚 resize 完的那一两帧里 innerWidth 会短暂滞后(布局还没跟上新尺寸),
|
|
3782
|
+
// 直接判 scale-mismatch 就会误触发 hard 重绘 —— 而 hard 会重排两次,
|
|
3783
|
+
// 用户看到的就是"大一下小一下"(2026-10-25 真机就这么抖的)。
|
|
3784
|
+
// 所以等 120ms 再读一次:还是不一致才算 surface 真的脱节。
|
|
3785
|
+
await new Promise((r) => { const t = setTimeout(r, 120); if (t.unref) t.unref(); });
|
|
3786
|
+
let iw2 = 0;
|
|
3787
|
+
try {
|
|
3788
|
+
const raw2 = await withDeadline(this.evaluate(PageManager.PAINT_DOM_JS, tid), 2000, 'paint_dom2');
|
|
3789
|
+
const s2 = (raw2 && typeof raw2 === 'object' && 'value' in raw2) ? raw2.value : raw2;
|
|
3790
|
+
const parsed2 = typeof s2 === 'string' ? JSON.parse(s2) : null;
|
|
3791
|
+
iw2 = Number(parsed2 && parsed2.iw) || 0;
|
|
3792
|
+
} catch (_) { /* 读不到就按原判断 */ }
|
|
3793
|
+
if (iw2 > 0) {
|
|
3794
|
+
if (Math.abs(iw2 - bounds.width) <= 2) mismatched = false;
|
|
3795
|
+
else iw = iw2;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
if (mismatched) {
|
|
3747
3799
|
cls = {
|
|
3748
3800
|
verdict: 'scale-mismatch',
|
|
3749
3801
|
blank: cls.blank,
|
|
@@ -3889,7 +3941,9 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
3889
3941
|
if (!view || !view.webContents || view.webContents.isDestroyed()) return rec;
|
|
3890
3942
|
if (action === 'repaint') {
|
|
3891
3943
|
this._paintSteps.set(tabId, steps + 1);
|
|
3892
|
-
|
|
3944
|
+
// 这里必须 hard:被判成白屏/scale-mismatch 时,surface 很可能就是旧尺寸,
|
|
3945
|
+
// 只 invalidate 不会重新栅格化。用户已经看到坏画面了,1px 重排的代价无所谓。
|
|
3946
|
+
this._forceRepaint(view, { hard: true });
|
|
3893
3947
|
await new Promise((r) => { const t = setTimeout(r, 320); if (t.unref) t.unref(); });
|
|
3894
3948
|
return this._auditPaintNow(tabId);
|
|
3895
3949
|
}
|
|
@@ -4007,7 +4061,7 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
4007
4061
|
* explicit "I know this window looks wrong, redraw it" call rather than a
|
|
4008
4062
|
* heuristic.
|
|
4009
4063
|
*/
|
|
4010
|
-
repaint(tabId) {
|
|
4064
|
+
repaint(tabId, opts = {}) {
|
|
4011
4065
|
const tid = tabId !== undefined && tabId !== null ? tabId : this.activeTab;
|
|
4012
4066
|
const view = this._getView(tid);
|
|
4013
4067
|
if (!view) return { ok: false, error: 'no-such-tab', tab: tid };
|
|
@@ -4019,7 +4073,7 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
4019
4073
|
// to ask, the browser already scheduled one" — i.e. the window was stale
|
|
4020
4074
|
// for ~120ms and has since caught up on its own.
|
|
4021
4075
|
if (pending) { clearTimeout(pending); this._repaintTimers.delete(tid); }
|
|
4022
|
-
const ok = this._forceRepaint(view);
|
|
4076
|
+
const ok = this._forceRepaint(view, { hard: opts && opts.hard === true });
|
|
4023
4077
|
return { ok, tab: tid, was_active: tid === this.activeTab, was_pending: !!pending, repaint_count: this._repaintCount || 0 };
|
|
4024
4078
|
}
|
|
4025
4079
|
|
|
@@ -4216,6 +4270,7 @@ return {ok:true,name:file.name,size:file.size,via:via,host_sel:hostSel,clicked:c
|
|
|
4216
4270
|
const out = { ok: true };
|
|
4217
4271
|
try { out.bounds = this.window.getBounds(); } catch (_) { /* 窗口已销毁 */ }
|
|
4218
4272
|
try { out.content = this.window.getContentBounds(); } catch (_) { /* 窗口已销毁 */ }
|
|
4273
|
+
out.repaints = { soft: this._repaintCount || 0, hard: this._hardRepaintCount || 0 };
|
|
4219
4274
|
const views = {};
|
|
4220
4275
|
for (const [id, view] of this.tabs) {
|
|
4221
4276
|
try { views[id] = view.getBounds(); } catch (_) { /* 视图已销毁 */ }
|
package/src/main/ws_server.js
CHANGED
|
@@ -181,7 +181,9 @@ export function startWSServer(pageManager, port = config.wsPort, onQuit = null)
|
|
|
181
181
|
// the DOM tree / evaluate already report the NEW page while the
|
|
182
182
|
// screen shows the OLD one. So the agent's only handle on it is an
|
|
183
183
|
// explicit "this window looks wrong, redraw it".
|
|
184
|
-
|
|
184
|
+
// hard:true 才做 1px 微调(会让页面重排两次,肉眼可见)。
|
|
185
|
+
// 默认 soft:只 invalidate —— 高频调用不会把窗口刷成幻灯片。
|
|
186
|
+
const r = pageManager.repaint(tabId, { hard: params && params.hard === true });
|
|
185
187
|
send({ jsonrpc: '2.0', id, result: r });
|
|
186
188
|
break;
|
|
187
189
|
}
|
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.16';
|