Repository navigation
Releases: Glittering/ai-browser
Release list
v2.4.1 —— 输入路径的三处静默故障
v2.4.1 —— 输入路径的三处静默故障
这一版没有新功能。它修的是**"读得到、打不进"**这一类问题:ui.get_tree 明明给出了
一个 id,ui.act 却告诉你"找不到这个元素",或者更糟 —— 动作返回成功,其实什么都没发生。
来源是一次三站点联调(Notion / Miro / Figma)。三个站点各有各的形态,而 Notion 暴露了
输入链路上三个互不相关的缺陷,每一个都把错误推给下一个。
1. 句柄来自元素原生 id,查找却只认 data-ai-id
抽取器在没有 data-ai-id 时用元素自己的 id 当句柄(existingAiId || nativeId)。
而 data-ai-id 只是页面里的一个普通属性 —— React 类应用重渲染那个子树时会把它一起抹掉,
元素本身还在。
实测(Notion 空段落):id=":r6:",data-ai-id 刚写就被抹空。于是 ui.get_tree 报
:r6:,ui.act 报 Target not found for ref: :r6:。
修法:属性查不到时按 getElementById 兜底,放在"穿透 shadow 的同一递归"里
(ShadowRoot 也有这个方法)。它不会把"不存在"变成"乱命中" —— 有阴性对照钉住。
2. focus() 不等于"可输入"
contenteditable 调 focus() 之后 document.activeElement 确实等于它 ——
所以"焦点坐上没有"这个判据看不出任何问题 —— 但 Notion 的块编辑器没进入编辑态,
随后所有受信按键全部落空,校验读到空,报 Input not verified。
真人点一下之后,activeElement 会交给编辑器自己的容器,按键才生效。
修法:contenteditable 一律先来一次真实指针点击。input/textarea 不加这一步 ——
原生控件 focus 就够,多点一下反而会把光标挪到中段,破坏 type 的 at:'end' 语义。
3. 校验读错对象 → 假阴性
清空 Notion 的空段落之后,它把焦点交还给页面(activeElement 变成 BODY),
校验读到的是整页文本,于是明明成功却报失败。
这个方向最危险:agent 看到失败会重试,对 type 就是写入两份重复内容。
修法:失败时再按"解析出来的那个可编辑元素"复核一次,命中就如实给成功并标
verified_by: 'target-element'。它不掩盖真错位 —— 文字若落进别的输入框,
目标元素里就没有它。
顺带修掉的(同一轮)
- GPU 画布上
canvas_changed:false是假阴性。Figma 的可见画布是 WebGPU,
DOM 里 2D 画布 0 张,而绘制钩子照样记到帧 —— 那些来自页面自己建的离屏 2D 画布。
一次真实的"宽度 100→200"改动,指纹一动不动,回执却说"证明没变"。
现在钩子自述g=1,上层把canvas_changed/canvas_idle一律降级成null(不可判)。
null意味着"无法判断",不是"什么都没发生" —— 这句话现在也写进了
ui.capabilities的说明里。 data-ai-id优先级:元素已有data-ai-id时必须优先沿用,不能被元素自己的id
改写。两条路径(AX 与 DOM 抽取)轮流改写同一个属性时,各自报出的 ref 都会被对方作废。
验证
- 单测 1128 / 81 文件
- e2e 23 套件 789 项全绿(richtext 117、readcov 99、capabilities 54、canvas 41…)
- 真站点:Notion 写入→回读→还原;Miro
create → get → sync → remove;
Figma 属性写入 → Mirror DOM 自述 "Dimensions 150 100" → 还原
全部写入都是可逆的,且已还原到原状。
v2.4.0 — canvas 应用终于可验证了;窗口显示的自述不再撒谎
v2.4.0 — canvas 应用终于可验证了;窗口显示的自述不再撒谎
这一版解决的是同一类问题的两个面:工具说了假话。一处是把"没看见"说成"没变",
另一处是把"我等到了页面"说成事实而其实等到的只是我们自己的占位页。
1. 纯 canvas 应用的写入,现在能被证明(也终于能被证伪)
用 Excalidraw(无需登录、全球最典型的 canvas 白板)实测:拖出一个矩形,
ui.act 之所以报 page_changed:true,靠的是"选中图元后右侧属性面板出现"(DOM +48 节点)。
这是运气 —— 动作只要不产生面板副作用(移动已有图元、空白处涂色、连续调参),
DOM 就零字节变化,回执会说 page_changed:false + "payload most likely never landed",
把一次成功的写入说成失败。而 ui.diff 的差异条目里,一条来自画布的都没有。
现在注入页面的绘制记录器会累积两个值:
| 量 | 它回答什么 | 受采样相位影响 |
|---|---|---|
fp(自上次清屏以来的绘制序列哈希) |
画到什么程度了 | 是(落在帧中间就是前缀) |
last(最近一个完整帧的哈希) |
上一帧长什么样 | 否 |
ui.act 的回执因此新增三个字段:
| 页面形态 | canvas_changed |
canvas_idle |
canvas_repeat |
|---|---|---|---|
| 静止(空闲期没画) | true / false | true | false |
| 在重绘,但每帧内容相同 | true / false | false | true |
| 每帧都不一样(时钟/光标/直播) | null(判不了) |
false | false |
| 证据不足(无完整帧 / 后台被节流) | null |
null |
false |
请这样子读:false 是"有证据的没变",null 是"这一页判不了"。
把 null 当成 false 是最常见的误读 —— 它会让你重复执行一个其实已经成功的写入。
实测证据(夹具:每 16ms 重绘相同内容、只在第一次指针按下时多画一个方块):
第一次拖 → canvas_changed:true repeat:true 页面自述图元 1 → 2
第二次拖 → canvas_changed:false repeat:true 页面自述仍是 2 ← 以前这里只能是 null
也就是说,在一直动的画布上,工具现在能说出"你这一手真的没反应"。
2. ui.window_shown 不再把"我的占位页"说成"目标页面"
新标签页会先加载 loading.html 占位页(把白屏换成「正在打开 」),
而占位页自己也会发 dom-ready。于是记录里写着 show_reason:"dom-ready" +
waited_for_dom:true,而此刻目标文档一个字节都没到(外网不通时实测 body 恒为 578 字符)。
现在 show_reason 会如实给出 placeholder,waited_for_dom 只在目标文档就绪时为 true,
并新增 show_url(那一刻 webContents 上是哪一页 —— 占位页 URL 里带着目标地址,
正是最有用的那一半)。另外 fail-load 不再被算成"等到了 DOM"。
显示时机没有改变:占位页一就绪就显示是 2026-10-24 的决定(人先看到
「正在打开 」比看白屏好)。这次只让记录说真话。
3. 顺手修掉的两个缺陷
ui.capabilities的 endpoints 混进了别的标签页的请求({tabId}传给只读
filter.tab的函数,过滤静默失效)。实测:只打开 excalidraw 的实例里出现了
百度的/ztbox、/sugrec。agent 会照着这份清单去 fetch 与本页无关的接口。- 一个只在特定环境下才暴露的测试误报:
smoke用url.includes("/a")找标签页,
会命中占位页的file:///.../projects/ai-browser/...(路径里恰好含/a),
于是它切到占位页、读到「正在打开…」,红信息却指向"set_active_tab 坏了"。
验收
- 单测 1114 / 80 文件
- e2e 23 套件 789 项全绿(canvas 41、capabilities 54、nav 21、smoke 31)
仍然做不到(写在这里,免得被上面的好消息盖过)
- 读不出画了什么。帧指纹只回答"这一帧和上一帧一样不一样"。Excalidraw 实测是 2D
且一次fillText都没有(整帧clearRect+Path2D+ 离屏合成),所以
"2D canvas 就能读文字"不成立,ui.canvas_read对这类渲染器的texts[]是空的 ——
空不代表画布是空的。 - Figma / Miro / Notion 这类站点仍未真正测到(只拿到落地页或登录墙),
它们的唯一出路是应用数据模型层(React fiber / Yjs 运行时),需要可复现样本才动手。 - 浏览器内的远程桌面/VM 控制台(帧流 + 坐标点击)没有可验证路径,本版不含。
v2.3.1 — 修 2.3.0 的打包版事故:新标签页停在「正在打开…」
v2.3.1 — 修 2.3.0 的打包版事故:新标签页停在「正在打开…」
请立刻升级。 2.3.0 的打包版(/Applications 里那个、以及 dmg)有一个只在打包后
才出现的回归:任何带地址的新标签页都会永久停在占位页(「正在打开…」),打不开目标站。
源码/dev 树不受影响 —— 这正是它逃过全部 865 条测试的原因。
原因
上一版为了让"打开的目标页别被延迟的占位页导航盖回首页",在 newTab 的延迟导航里加了
一句判据:
const stillMine = !cur || cur === 'about:blank' || cur === placeholderUrl;而 placeholderUrl 是手拼的:
'file://' + pagePath + '?' + q // pagePath 里有空格:/Applications/AI Browser.app/...Chromium 报回来的 getURL() 是 AI%20Browser.app,我们手里是 AI Browser.app ——
字符串相等在打包后恒为 false,于是那句判据把"还在占位页上"误判成"已经被导航走了",
真正的目标再也不加载。开发树路径没有空格,所以 e2e 全绿。
修法
两处,都在 src/shared/placeholder.cjs(新文件,纯函数):
- 不再手拼 file URL:
pathToFileURL(),空格/中文/#/?由它负责转义。 - 不再用字符串相等判断"是不是同一页":按文件名判据(
basename === 'loading.html'),
并且http(s)页面一律不算占位页(站点里正好有个 loading.html 不会被误判)。
新增 tests/test_placeholder.js(7 条),其中 PH-4 直接钉住"打包路径含空格"这一条 ——
它在 v2.3.0 会是红的。
顺带
- 产物验收(
npm run verify:app)在找不到按钮时会打印那个标签页的 url 与 state。
2.3.0 那次它只会说"no clickable after 8s",而真正的原因(state=pending、
url 还停在 loading.html)藏在没有输出的地方。 tools/browser.cjs(e2e 客户端)不再永久等:连接断开 / 回包超时都唤醒成一条错误。
以前一次掉线会让套件挂到 150s 看门狗且零线索。
验证
- vitest 74 文件 / 1031 条;e2e:all 22 套件 / 745 断言全绿。
npm run verify:app在装好的 2.3.1 上 22/22(这条才是这次事故的验收标准)。
v2.3.0 — 每个 agent 一块自己的地方
v2.3.0 — 每个 agent 一块自己的地方
上一版让每个连接有了自己的目标,这一版把它补成自己的空间:一个组、一个上限、一个
标签栏上看得见的位置。
为什么还要再改一次
用户的原话:"我看 agent 还是不知道去操作其它标签,只会弄当前激活的显示的那个标签。"
这是三个原因叠在一起,只修一个都不够:
- 默认行为:会话还没有绑定时,目标回落到
activeTab—— 也就是人正在看的那一页。
单 agent 时它碰巧等于"agent 该干的活",多 agent 时就是灾难。 - 没人告诉它:工具描述和技能里只有一句"省略 tab 就是你自己的页",而"你自己还没有页"
这件事从来没有被说出来过。 - 习惯:模型在别处学到的浏览器操作就是"操作当前页",没有理由自己想到去开一页。
所以这一版把"给自己开一页"从要求变成事实。
自动工作区
MCP / CLI 建连后立刻自报家门(ui.hello)。声明自己是 agent 的连接,第一次要动页面而还没
指定目标时,浏览器给它开一页空白标签、归进它自己的组,并在这次结果里附一次提示:
{"workspace": {"tab": 7, "group": "Claude", "created": true, "hint": "..."}}Agent 于是永远不会默认开在人眼前那一页上。想看人的页面仍然便宜:list 里
group: "human" 那条,显式传 tab —— 读一下不会把它抢走。
四条不触发:① 没发 ui.hello(裸 WS / e2e / 脚本保持老行为);② 方法本身在管标签页;
③ 显式点了 tab 或已有绑定;④ AI_BROWSER_AUTO_WORKSPACE=0。CLI 发
auto_workspace:false —— 人坐在终端前敲命令时,"不传 tab 就打当前页"才是他要的。
组与上限
group = 'human' | 'agent:<会话>'
每组默认 8 页(AI_BROWSER_TAB_LIMIT)。new_tab 撞上限时拒绝,不回"悄悄挤掉一页":
挤掉是破坏性的(agent 可能正在那一页跑流程),拒绝是一次必须做的决定 —— 这正是上限的
理由:后台运行时没人看得见它的标签页。
-32001 your tab group is full (8/8) — close one before opening another
data: { reason: "group-full", limit: 8, tabs: [{tab, url}, ...] }
页面自己弹的窗口没法拒绝(站点脚本在等它),只能牺牲:本组里"最久没用、不在人眼前、
没有挂着对话框"的那一页,并在 list 的 evicted 里回采一次。
上限只约束声明过自己是 agent 的连接。裸 WS 拿到的组只是归属标签,不限量 —— 否则那些
一次性开二十个标签的回归套件会突然全红。
标签栏
人的标签页平铺;每个 agent 组折叠成一个 pill(组色 + 名字 + 页数,满额变暖色)。悬停展开,
点击钉住;正在看的那一组强制展开,否则人切过去就找不到自己在看什么。点开组里的页只是
"你看",不改 agent 的目标。
没有做成"竖着掉下来的下拉菜单":标签栏是固定高度的 BrowserView,往下弹会被自己裁掉;要能弹
就得每次悬停把标签栏长高、把页面下推 —— 那正是本项目历史上闪烁/几何脱节的来源。就地展开
拿到同样的折叠效果,不动布局。
验证
- 新增
tests/test_tab_group.js(12 条)与tests/test_tab_bar_layout.js(10 条):
上限判定、弹窗牺牲顺序(不许牺牲正在看的、正在被开的、挂着对话框的)、折叠状态机。
交互逻辑抽成纯函数才测得了 —— 靠人肉把鼠标移上去验证等于没验证。 npm run multiagent从 32 条断言扩到 46 条:M7 自动工作区(含"人的页面没被换掉")、
M8 上限与拒绝回执、M9 组间互不影响。- 全量:vitest 73 文件 / 1024 条;e2e:all 22 套件 / 743 断言。
顺手修掉的一个老 bug:打开的目标页会自己跳回首页
npm run background 这次开始稳定失败,追下去发现不是新代码的问题 ——
在 v2.2.0 的 tag 上跑同样稳定失败:
新标签页的占位页(loading.html)后面挂着一个"垫片装好后就去加载目标地址"的延迟
动作。垫片安装偶尔会挂十几秒,这期间 agent 已经把这一页导航走了;等它终于 resolve,
那个延迟动作又把启动页加载回来 —— 现象是"打开的是目标页,下一刻自己跳回首页",
读到的树也成了首页的。
修法是一句判据:延迟动作执行前先看"现在还在占位页上吗",不在就什么都不做。
(page_manager.newTab 的 proceed()。)
升级注意
- 以前"不传 tab 就打当前页"的 agent 行为变了:第一次调用会得到一页空白工作页。
这是刻意的(隔离的前提),回执里有提示;要恢复老行为设AI_BROWSER_AUTO_WORKSPACE=0。 - 一个 agent 同时开超过 8 页会被拒绝,需要先关。
v2.2.0 — 多 agent 并行,窗口可以不出现
v2.2.0 — 多 agent 并行,窗口可以不出现
这一版解决两件事,它们其实是同一个问题的两面:agent 该在哪一页上干活,以及
浏览器必须长什么样。
1. 几个 agent 同时用,各干各的
以前默认目标是全局 activeTab,也就是"人眼看的那一页"。单 agent 时它恰好等于
"agent 正在干的活",看不出问题。多个 agent 同时用时立刻崩:
- A 开一个标签页 → 所有人的默认目标都变成它;
- B 只说"点一下提交",点在了 A 的页面上;
- 人切一下标签页看热闹 → 把所有 agent 的目标一起切走。
共同点:动作落在哪个页面,取决于别人做了什么。回执 success、页面真的变了,
但不是该变的那个 —— 比直接失败难查得多。
现在每条连接(每个 MCP 进程)有自己的工作标签页,判据只有一处
(shared/tab_route.cjs):
点名的 tab > 你自己的 tab > 人正在看的 > 第一个
browse_tabs {"op":"new"}不顶掉用户正在看的那一页(show:true才显示)。
唯一例外:人眼前还是没被动过的启动页 —— 否则用户会盯着百度首页看 agent 干活。browse_tabs {"op":"list"}在你自己那条上标mine: true。- 你的页被别人关掉 → 下一次调用明确报
-32001,绝不静默落到别人的页面上。
自己关自己的页 → 照浏览器惯例落回可见页。 - 点链接弹出新页:只把目标挪过去;只有打开者本身是可见页时才挪视野。
(agent 流程里登录/预览/下载弹窗极多,每弹一个抢一次视野等于人没法用电脑。)
2. 窗口不显示,活照样干完
用户的原话:"必须可以不在前台显示的情况下,agent 可以完成所有操作 … 跟无头浏览器
一样无感知运行,并且随时可以点开,使用它,还不影响 agent 进行任何任务。"
这以前做不到,三处硬伤:
- 隐藏不是稳定状态:任何动作都会把隐藏/最小化的窗口
showInactive回来 ——
agent 每操作一次,窗口就自己爬出来一次。 - 没挂进窗口的标签页没有合成表面:读树/点击/输入/导航都正常,但
screenshot
返回 0 字节、paint_check直接no-frame。截图正是 agent 判断"这一页画出什么"
的唯一手段 —— 后台标签页变成一个看不见也拍不到的黑盒。 - 窗口不可见时仍在做白屏判定:取帧要么空要么陈旧,而阶梯会据此
reload——
后台无人值守时等于自己制造一场重载风暴。
现在的行为:
AI_BROWSER_START_HIDDEN=1 ... # 进程起来但不出窗口
browse_tabs {"op":"window","visible":false} # 中途退到后台
browse_tabs {"op":"window","visible":true} # 要人登录/扫码时打开- 可见性只由人(Dock 图标 →
activate)和显式调用决定;AI_BROWSER_FOCUS=auto
不再碰窗口任何状态。 - 所有标签页都挂进窗口、用 z 序决定谁在上面(
setTopBrowserView),因此后台标签页有
真实表面:能截图、能自检、能录 canvas。每个标签页从出生起backgroundThrottling=false。 - 窗口不可见时跳过白屏判定;人把窗口点开时补一次自检(兜住"agent 在后台把页面搞白了,
人一打开正好看到白屏")。 - 隐藏期间全部能力可用:读树 / 点击 / 输入 / 滚动 / 上传 / 下载 / 应答对话框 / 抓包 /
截图 / 导航。npm run background用 37 条断言钉住,其中启动就是START_HIDDEN=1
——窗口从头到尾没出现过。
已知边界
- 两个 agent 往同一个输入框里同时打字不做协调,值校验会如实报
input not verified;
不同标签页之间完全并行。 - 每个标签页都占一份合成资源(和 Chrome 多标签一个量级),不是"免费"。
验证
- 单元:
npm test— 71 文件 / 1001 条。 - 端到端:
npm run e2e:all— 22 套件 / 696 断言,新增两个:npm run multiagent(32 断言):两条连接对同一个浏览器,各自的目标、并行动作不串台、
被关掉要报错、switch只换目标不换视野。npm run background(37 断言):启动即隐藏,跑完整能力切片,再显示回去验证可见页没变。
e2e/run_all.cjs顺带补了一条:套件没有摘要时打印它最后几行输出。
以前只写"无摘要(未知)",等于把唯一的线索扔了。
v2.1.21 — 点「证书下载」不再卡在「正在打开…」
v2.1.21 — 点「证书下载」不再卡在「正在打开…」
你看到的
沧州市教师继续教育网 →「证书下载」→ 标签页停在一句 正在打开… 和一大串 png 地址上,
永远不动。文件其实下载成功了(~/.ai-browser/downloads/<tab>/),但界面上零回执,
所以只能判断成"卡住了"。
为什么会这样
那个地址回的是 Content-Type: image/png + Content-Disposition: attachment,
站点又用新标签打开它。浏览器把"attachment"理解成"存下来",于是这次导航
永远不会提交 —— 标签页停在加载占位页上,占位页尽职地把目标地址显示了出来。
再加上整条下载路径只有落盘、没有任何界面表达,两个问题叠在一起就是"别扭"。
现在的行为(按浏览器的既有语义,只认 Content-Type)
| 点了什么 | 会发生什么 |
|---|---|
| 图片 / 视频 / 音频 / PDF(哪怕服务端要求下载) | 直接显示。工具条右侧出现「↓」,点一下存进下载目录 |
| zip / dmg / exe / docx … | 正常下载;那个"为它而开的"标签页变成下载结果页:文件名、大小、类型、图片缩略图、进度条,「打开」「在 Finder 中显示」「复制路径」 |
| 页面里点"导出 CSV"之类 | 页面不动,工具条给一条「↓ 已下载 xxx」回执(8 秒),点一下在 Finder 中显示 |
判据只有一条:Content-Type 是不是人能直接看的东西(image/*、video/*、
audio/*、application/pdf)。不看后缀、不针对任何站点。
- 刻意不含
text/plain/text/csv/application/json—— 点"导出"的语义
就是要文件,把 CSV 铺满一屏是帮倒忙。 - 刻意不改 Content-Type —— 站点把 png 说成
application/octet-stream时,
硬改类型去渲染,坏了就是一片碎图;那种情况让它下载才可靠。 - 下载时用的是同一个会话(Cookie / Referer / 临时签名照旧),所以不会出现
"浏览器里能看、存下来是 403 页面"。
顺手补上的两个洞
ui.download:显示出来的图片/视频/PDF 也要能留下文件,否则等于把"下载"
从这类页面上拿掉了。工具条那个「↓」走的就是它。dl:*通道的门:结果页要「打开 / 在 Finder 中显示」,但 preload 挂在每个
标签页上(含任意站点)。主进程校验 sender 必须是我们自己的界面页,且路径必须落在
下载目录之内 —— 否则任意网站都能让浏览器打开、高亮你下载过的文件。
验证
npm run download(新增,23 断言):图片带 attachment 必须显示且不产生下载记录、
zip 必须下载且标签页变结果页、就地下载不许顶掉页面、本来就 inline 的图片照旧显示、
ui.download能把正在显示的图片存成文件。- 负向验证:
AI_BROWSER_MEDIA_INLINE=0跑同一套件,前四条全红
(cert.png变成下载、导航不提交)。不做负向验证的回归只是心理安慰。 - 全量:
npm test988/988;npm run e2e:all20 套件 / 659 断言全绿。
关掉这条钩子的开关是 AI_BROWSER_MEDIA_INLINE=0(回到"图片也当文件下载"的旧行为),
用于二分"某个站点的显示问题是不是它造成的"。
v2.1.20 — 把并行线并进产物(启动布局修正 + Windows 打包)
v2.1.20 — 把并行线并进产物(启动布局修正 + Windows 打包)
2.1.19 发布时另一条线正往 GitHub main 推 Windows 打包与启动布局修正,赶上了推送冲突。
合并之后 main 上多了它们、而 npm 与应用包里没有 —— 按本项目的规矩("任何已修复都必须
以产物同步为前提"),再发一版把渠道对齐。
本版包含(2.1.19 之后合并进来的)
- 启动后补一次重排(
src/main/index.js):修窗口显示时视图底部溢出 26px。
这条是真正的产品修复,此前只存在于 main,没进过任何产物。 - Windows 打包:
scripts/build_win.cjs(便携包)+scripts/build_installer.cjs
(Squirrel 单文件安装器),npm run build:win/build:installer。 - e2e 在 Win10 上的可移植性与时序:spawn guard、多个套件的等待策略。
- network 套件:空白启动页,隔离启动 tab 的全局 body 配额噪声。
- view 套件 R-01/R-04 对齐「导航不主动重绘」契约(与 2.1.19 的修正同一结论)。
2.1.19 的内容(npm 上已有,这里一并带上)
- 新增
npm run flicker帧级回归:avfoundation60fps 录内容区,逐帧判"有没有一帧
把页面画成缩小的"。负向验证过(把clip.scale加回去 3/3 全红,tiny=24.3%)。 ui.screenshot的clip.scale < 1改为按 1 抓、本地nativeImage.resize。e2e:all收进 dialog / wrapper / paint / layout,19 套件 / 636 断言。
验证
合并后逐项复跑:layout 25/25、view 29/29、paint 23/23、flicker 6/6(零缩小帧),
打包产物验收 22/22。
v2.1.19 — 把「加载时缩一下」钉成回归用例
v2.1.19 — 把"加载时缩一下"钉成回归用例
上一版(2.1.18)抓到了真因:白屏自检用 clip.scale=0.25 截图,Chromium 会按该比例
临时重建 surface,自检又恰好跑在 did-finish-load+450ms,于是那张 1/4 的图被贴上
1–3 帧。但抓它用的是一次性手工取证(60fps 录屏 + 手算裁切),下次再有人把缩放交给
合成器,没人会重跑那一套。
这一版把取证固化成套件,并清掉最后一条同类路径。
新增:npm run flicker(帧级回归)
e2e/flicker_ws.cjs + e2e/_win_capture.py:起 App → 加载铺满视口、上下带
每次换色通栏色带的 fixture → 触发 reload → avfoundation 固定 60fps 录内容区 →
逐帧判定。
两条断言:
- 采集有效:帧间差异峰值 > 1.0。整段画面一帧没变 = 根本没拍到目标窗口,
此时的"通过"是假的。 - 零缩小帧:任意一帧内容宽度 ≥ 60%(正常 97–100%,故障 24–25%)。
负向验证:把 clip.scale 加回去,3 轮全部 FAIL、tiny=24.3%、连续 3–4 帧;
恢复后 3/3 全绿,每轮恰好 1 帧可见变化。没通过这一步的回归不算回归。
修复:ui.screenshot 的 clip.scale < 1 不再交给合成器
按 scale=1 抓、本地 nativeImage.resize() 缩,输出尺寸与语义不变。
这是最后一条会把缩放交给合成器的自动路径。
(clip.scale > 1 仍交给 Chromium —— 本地放大得不到像素,且只在调用方显式要放大值时发生。)
修正:view 套件的两条断言写反了
R-01 / R-04 一直在要求"导航事件必须触发重绘",而那正是 v2.1.16 为了消灭
"先缩成左上角一小块、闪一下"而撤掉的行为。断言现在反过来钉住新不变量:
导航/输入这类"动作"路径上不许重绘。e2e:all 从 v2.1.16 起就是红的,只是没人跑。
补漏:e2e:all 收进 dialog / wrapper / paint / layout
这四个套件此前只在自己文件里,全量验收从没跑过 —— 加了套件但没进全量等于没加。
现在 npm run e2e:all 是 19 个套件 / 636 断言。
文档
docs/SPEC.md 新增「帧级回归」一节,含五个制造过假阴性的坑(变帧率录屏、
写死裁切坐标、CGDisplayPixelsWide 当 backing scale、固定时长等待、量非白像素 bbox);
README 的测试清单按实际断言数校准。
v2.1.18 Windows (portable)
Windows x64 portable build of AI Browser 2.1.18.
Includes Windows-specific e2e fixes (portable userData path, cross-platform teardown, timing hardening) and a startup layout fix (view no longer overflows the content area by 26px on first show).
Unzip and run AI Browser.exe.
v2.1.18
v2.1.19 — 每次加载"闪一下 1/4 大小":真因是自检截图的 clip.scale
这次抓到了实锤,不是推测。
证据
系统录屏是变帧率编码,一帧(16ms)的闪现会被直接丢掉——这就是前几轮我"录不到"的原因。
换成 avfoundation 固定 60fps 后,一次就抓到了:
帧 137–139 / 223–225 / 325–327 / 426–428 (每组 3 帧 ≈ 50ms)
内容宽度 = 25% ← 正好 1/4,页面被画进左上角
每次刷新出现一组,时机都在"刷新后约 500ms"
真因
白屏自检(v2.1.13 引入)为了省 CPU,把缩放参数交给了合成器:
Page.captureScreenshot({ clip: { x:0, y:0, width, height, scale: 0.25 } })带 clip.scale 的截图会让 Chromium 按该比例临时重建 surface。自检恰好跑在
did-finish-load + 450ms,那几十毫秒里窗口往往正在重新合成 —— 于是把这张 1/4 的图
贴上去 1–3 帧,再恢复。尺度(25%)和时机(+500ms)两处都对得上,而且这个自检
正是 v2.1.13 加的,"加载时闪一下"也是从那时开始出现。
改法
- 取帧优先
webContents.capturePage()—— 不碰 CDP 的 Page 域、没有 capture 模式;
拿不到(窗口被遮挡会返回空图)再用不带 clip/scale 的captureScreenshot兜底;
两者都拿不到 =no-frame,不判缺陷。 - 缩放只许我们自己用
nativeImage.resize({width:240})做,绝不把缩放参数交给合成器。 - 加固
_auditLayout:几何不一致时先等 120ms 复核,仍不一致才纠正——窗口刚变化的那
一两百毫秒里两个值会短暂对不上,此刻"纠正"等于把视图改成中间尺寸。
验证
npm run paint 23/23(blank-with-text 仍能报出来)、npm run layout 25/25、单测 970。
渠道
GitHub Release(含 dmg)· npm · MCP registry · /Applications —— 全部 2.1.19。