@glittering0717/ai-browser 2.1.18 → 2.1.19

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/README.md CHANGED
@@ -724,7 +724,7 @@ not bugs. The full write-up, including why the first measurement of this was wro
724
724
  ## Test & dev
725
725
 
726
726
  ```bash
727
- npm test # vitest — 411 tests across 27 files
727
+ npm test # vitest — 970 tests across 69 files
728
728
  npm run smoke # Electron contract layer, offline fixture — 31 checks
729
729
  npm run capabilities # text fields, canvas, page source, network, DOM editing — 54 checks
730
730
  npm run richtext # rich-text editors, nested menus, forms, errors, tags, upload — 117 checks
@@ -736,8 +736,14 @@ npm run audit # every advertised action must exist; wait/downloads/permis
736
736
  npm run nav # address bar + load-failure error page (the "white screen" report) — 19 checks
737
737
  npm run focus # no focus stealing + full capability while backgrounded — 32 checks
738
738
  npm run interact # trusted drag / key press / wheel (pointer gestures) — 31 checks
739
- npm run view # viewport repaint + snapshot/diff verification — 28 checks
740
- npm run e2e:all # 一次跑完上面全部套件(每个独立端口)+ 可信汇总 — 428 checks
739
+ npm run view # viewport repaint budget + snapshot/diff verification — 29 checks
740
+ npm run dialog # browser-level dialogs (native sheet blocks the renderer) — 15 checks
741
+ npm run wrapper # "in the tree but won't click" = matched a container, not a link — 13 checks
742
+ npm run paint # blank-page self-check: verdicts, bounded ladder, no false alarms — 23 checks
743
+ npm run layout # geometry invariants + "no twitching" (zero extra repaints on nav) — 25 checks
744
+ npm run flicker # frame-level: no frame may render the page shrunk (needs screen recording) — 6 checks
745
+ npm run e2e:all # 一次跑完上面全部 19 个套件(每个独立端口)+ 可信汇总 — 636 checks
746
+ npm run verify:app # boot the packaged /Applications app and assert on it — 22 checks
741
747
  npm run realsites # navigate/read/act against a matrix of real sites
742
748
  ```
743
749
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittering0717/ai-browser",
3
- "version": "2.1.18",
3
+ "version": "2.1.19",
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": {
@@ -84,7 +84,8 @@
84
84
  "verify:app": "node e2e/shipped_ws.cjs",
85
85
  "wrapper": "node e2e/wrapper_ws.cjs",
86
86
  "paint": "node e2e/paint_ws.cjs",
87
- "layout": "node e2e/layout_ws.cjs"
87
+ "layout": "node e2e/layout_ws.cjs",
88
+ "flicker": "node e2e/flicker_ws.cjs"
88
89
  },
89
90
  "dependencies": {
90
91
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -1040,7 +1040,18 @@ class PageManager {
1040
1040
  captureBeyondViewport: o.fullPage,
1041
1041
  };
1042
1042
  if (o.quality !== undefined) params.quality = o.quality;
1043
- if (o.clip) params.clip = o.clip;
1043
+ // clip.scale < 1("给我一张缩略概览")**绝不交给合成器**:带 clip.scale 的
1044
+ // 截图会让 Chromium 按该比例临时重建 surface,窗口恰好在合成时就会贴上那张
1045
+ // 缩小的图 1–3 帧(见 SPEC「取帧两条铁律」,2026-10-25 实锤)。
1046
+ // 现在按 scale=1 抓、用 nativeImage 在我们自己这边缩 —— 输出尺寸一致,
1047
+ // 但不再碰合成器。scale>1(放大取值)仍交给 Chromium:本地放大得不到像素。
1048
+ let localScale = 1;
1049
+ if (o.clip && typeof o.clip.scale === 'number' && o.clip.scale > 0 && o.clip.scale < 1) {
1050
+ localScale = o.clip.scale;
1051
+ params.clip = { ...o.clip, scale: 1 };
1052
+ } else if (o.clip) {
1053
+ params.clip = o.clip;
1054
+ }
1044
1055
  const shot = await withDeadline(
1045
1056
  dbg.sendCommand('Page.captureScreenshot', params),
1046
1057
  20000, 'captureScreenshot'
@@ -1048,6 +1059,21 @@ class PageManager {
1048
1059
  if (!shot || typeof shot.data !== 'string') {
1049
1060
  return { ok: false, error: 'empty screenshot result', tab: tid };
1050
1061
  }
1062
+ // 需要缩小就自己做(见上:不把缩放交给合成器)。
1063
+ let imgData = shot.data;
1064
+ if (localScale !== 1) {
1065
+ try {
1066
+ const img0 = nativeImage.createFromBuffer(Buffer.from(shot.data, 'base64'));
1067
+ if (!img0.isEmpty()) {
1068
+ const sz = img0.getSize();
1069
+ const w = Math.max(1, Math.round(sz.width * localScale));
1070
+ const resized = img0.resize({ width: w });
1071
+ imgData = o.format === 'jpeg'
1072
+ ? resized.toJPEG(typeof o.quality === 'number' ? o.quality : 80).toString('base64')
1073
+ : resized.toPNG().toString('base64');
1074
+ }
1075
+ } catch (_) { /* 缩失败就用原图(宁可图大,不要空图) */ }
1076
+ }
1051
1077
 
1052
1078
  const mime = 'image/' + (o.format === 'jpeg' ? 'jpeg' : o.format);
1053
1079
  // 一次 evaluate 拿两样东西:可见密码框(截图不脱敏,泄密了得让调用方
@@ -1070,7 +1096,7 @@ class PageManager {
1070
1096
  const summary = screenshotOpts.describeShot({
1071
1097
  format: o.format,
1072
1098
  mime,
1073
- bytes: Buffer.byteLength(shot.data, 'base64'),
1099
+ bytes: Buffer.byteLength(imgData, 'base64'),
1074
1100
  width: meta.width,
1075
1101
  height: meta.height,
1076
1102
  url: (this._getView(tid) && this._getView(tid).webContents.getURL()) || undefined,
@@ -1082,7 +1108,7 @@ class PageManager {
1082
1108
  if (o.path) {
1083
1109
  try {
1084
1110
  fs.mkdirSync(path.dirname(o.path), { recursive: true });
1085
- fs.writeFileSync(o.path, Buffer.from(shot.data, 'base64'));
1111
+ fs.writeFileSync(o.path, Buffer.from(imgData, 'base64'));
1086
1112
  filePath = o.path;
1087
1113
  } catch (e) {
1088
1114
  return { ok: false, tab: tid, error: 'screenshot captured but writing to ' + o.path + ' failed: ' + ((e && e.message) || e), ...summary };
@@ -1095,7 +1121,7 @@ class PageManager {
1095
1121
  ok: true,
1096
1122
  tab: tid,
1097
1123
  // data 是 base64 裸数据(不带 data: 前缀)—— 调用方按需自己拼。
1098
- data: shot.data,
1124
+ data: imgData,
1099
1125
  mime,
1100
1126
  ...(filePath ? { path: filePath } : {}),
1101
1127
  ...(ignored.length ? { ignored_params: ignored } : {}),
@@ -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.18';
5
+ export const VERSION = '2.1.19';