@evan_zhong/browser-attach 0.15.0 → 0.17.0

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
@@ -15,7 +15,7 @@ Drive the Chrome / Edge tabs you already have open and already signed in from an
15
15
  ## What it does
16
16
 
17
17
  - Discover multiple browser profiles, pick one precisely, claim a tab exclusively, and release it when done.
18
- - Read the accessibility tree, field values and supported editor models; click, replace-fill and type character by character through current snapshot refs or strict CSS/XPath locators.
18
+ - Read the accessibility tree, query it by literal name/value, inspect field values and supported editor models; click, replace-fill and type character by character through current snapshot refs or strict CSS/XPath locators.
19
19
  - Native pointer hover, stepped drags and conditional waits; select HTML options without opening a native popup.
20
20
  - Navigate and go back/forward, open or close tabs, handle JavaScript dialogs, upload local files, and capture the whole page or a single element.
21
21
  - Evaluate JavaScript expressions in the main page; read console, page-error and network lifecycle logs captured after claiming; opt in to bounded, redacted request/response details when XHR/fetch needs verification.
@@ -67,7 +67,7 @@ Merge the following into your client's MCP configuration, or see [mcp-config.jso
67
67
 
68
68
  On Windows clients that require a full command extension, use `npx.cmd`. With a global install you can also configure `"command": "browser-attach", "args": ["mcp"]`. Restart the MCP client to discover the tools.
69
69
 
70
- `@latest` resolves the newest npm version at launch. The MCP package and the loaded extension must be the same version; after an upgrade, rerun `browser-attach setup` and reload the extension on the extensions page, otherwise the Hub rejects the old extension connection.
70
+ `@latest` selects the npm package at launch. For a pair already running 0.17 or newer, startup coordinates an idle Hub upgrade, synchronizes the paired extension directory and asks older connected extensions to reload themselves. First-time extension loading and the first upgrade from a pre-0.17 pair still require one manual extension reload; a pre-0.17 Hub must be stopped once before the new MCP can start. If another MCP connection is using the Hub, close it and reconnect to retry the upgrade.
71
71
 
72
72
  Assembly steps for supported hosts are in [agent integration](docs/AGENT-INTEGRATION.md); other clients use the stdio entry above.
73
73
 
@@ -90,7 +90,7 @@ Claiming and ordinary input never activate a tab or window; call `browser_tab_co
90
90
  | `browser_claim_tab` | Exclusively claim a discovered tab |
91
91
  | `browser_open_tab` / `browser_tab_control` | Create a tab; explicit `release/activate/close` of a claimed tab |
92
92
  | `browser_fetch` | Read one HTTP(S) URL in a temporary background tab using the profile's sign-in; `text`/`html`/`json`, optional HTTP status and cursor continuation |
93
- | `browser_snapshot` / `browser_read_text` | AX snapshot, element refs and page text |
93
+ | `browser_snapshot` / `browser_read_text` | AX snapshot, literal name/value query with actionable refs, and page text |
94
94
  | `browser_wait` | Read-only wait for an element, text, URL or network idle |
95
95
  | `browser_click` / `browser_hover` / `browser_drag` / `browser_select_option` / `browser_type` / `browser_fill` / `browser_press_key` / `browser_scroll` | Native/DOM input, pointer hover and drag, option selection, character typing and scrolling |
96
96
  | `browser_batch` | Run 1–20 click/fill/type/press_key/hover/scroll/select_option/wait steps in one queue; stops on the first failure and returns partial results |
@@ -113,13 +113,12 @@ Override it explicitly with the absolute path `BROWSER_ATTACH_HOME`. The Windows
113
113
  A host assembly step that writes the client entry pins the absolute paths of the current Node, CLI and the actual pairing file and sets `BROWSER_ATTACH_HOME`; it works together with a custom `--config`. When the MCP launches the Hub it passes the same absolute pairing path and directory, so different launch environments cannot derive a second configuration. See [agent integration](docs/AGENT-INTEGRATION.md).
114
114
 
115
115
  ```sh
116
- browser-attach status
117
- browser-attach stop
118
116
  npm install --global @evan_zhong/browser-attach
119
- browser-attach setup
120
117
  ```
121
118
 
122
- After upgrading, reload the extension on the extensions page, then restart the MCP connection. Hub and extension versions must match. The first time you use this project's new name, stop any previously running Hub and pair again; the default port is `47831` and `setup --port` changes it.
119
+ Then restart the MCP connection. With `@latest`, restarting MCP is sufficient for later upgrades once Hub and extension are on 0.17 or newer; `setup` and `start` use the same coordination when run explicitly. Upgrading from an older pair first requires `browser-attach stop`, a restart and one manual extension reload. Custom pairing paths, ports and extension directories are retained. Closed browser profiles update when they next connect. An older MCP package cannot downgrade a newer Hub or installed extension.
120
+
121
+ An upgrade waits for extension cleanup and confirms the new version after reload; cleanup or reload failures are reported instead of replaying page actions. Manual reload remains available on the extension options page for recovery. Hub and extension versions must match for page control. The default port is `47831`; choose another port with `setup --port` when creating a pairing.
123
122
 
124
123
  ## Architecture and boundaries
125
124
 
@@ -164,6 +163,7 @@ Real-browser tests need a Chromium / Edge executable that allows loading extensi
164
163
  npm run test:live -- --executable /path/to/chromium --family chromium
165
164
  npm run test:interaction -- --executable /path/to/chromium --family chromium
166
165
  npm run test:interaction -- --tab-indicators --executable /path/to/chromium --family chromium
166
+ npm run test:upgrade -- --executable /path/to/chromium
167
167
  ```
168
168
 
169
169
  These tests use temporary profiles and local fixtures. Coverage and limits: [test report](docs/TEST-REPORT.md); implementation and design trade-offs: [architecture](docs/ARCHITECTURE.md).
package/README.zh.md CHANGED
@@ -15,7 +15,7 @@
15
15
  ## 能做什么
16
16
 
17
17
  - 发现多个浏览器配置,精确选择并独占接管标签页,完成后释放。
18
- - 读取可访问性树、字段值和支持的编辑器模型,通过当前快照引用或严格 CSS/XPath 定位点击、替换填写和逐字输入。
18
+ - 读取可访问性树,按字面名称或 value 查询并取得可操作引用,检查字段值和支持的编辑器模型,通过当前快照引用或严格 CSS/XPath 定位点击、替换填写和逐字输入。
19
19
  - 原生指针悬停、分步拖拽、条件等待;无需打开系统弹层即可选择 HTML 下拉选项。
20
20
  - 导航与历史前进/后退、打开或关闭标签页,处理 JavaScript 对话框,上传本机文件并截取整页或单个元素。
21
21
  - 在主页面执行 JavaScript 表达式,读取接管后的控制台、页面异常和网络生命周期日志;需要校验 XHR/fetch 时显式开启有界、脱敏的请求/响应详情。
@@ -67,7 +67,7 @@ npx --yes --package=@evan_zhong/browser-attach@latest browser-attach setup
67
67
 
68
68
  Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局安装时,也可以配置 `"command": "browser-attach", "args": ["mcp"]`。重启 MCP 客户端后发现工具。
69
69
 
70
- `@latest` 会在启动时解析 npm 最新版本。MCP 包与已加载扩展必须为同一版本;升级后重新运行 `browser-attach setup` 并在扩展页重新加载扩展,否则 Hub 会拒绝旧扩展连接。
70
+ `@latest` 在启动时选择 npm 包版本。当配对已经运行 0.17 或更高版本时,MCP 启动会协调空闲 Hub 升级、同步已配对的扩展目录,并通知较旧扩展自行重新加载。首次加载扩展,以及从 0.17 之前版本进行的第一次升级,仍需手动重载一次扩展;旧 Hub 必须先停止,新 MCP 才能启动。如果其他 MCP 连接正在使用 Hub,请关闭这些连接后重连,以重试升级。
71
71
 
72
72
  各宿主的装配步骤见 [Agent 集成](docs/AGENT-INTEGRATION.md);其他客户端直接使用上面的 stdio 配置。
73
73
 
@@ -90,7 +90,7 @@ Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局
90
90
  | `browser_claim_tab` | 独占接管已发现的标签 |
91
91
  | `browser_open_tab` / `browser_tab_control` | 创建标签;显式 `release/activate/close` 释放控制、激活或关闭已接管的标签 |
92
92
  | `browser_fetch` | 在临时后台标签中复用登录态读取一个 HTTP(S) URL;支持 `text`/`html`/`json`、可选 HTTP 状态和游标续读 |
93
- | `browser_snapshot` / `browser_read_text` | AX 快照、元素引用和正文 |
93
+ | `browser_snapshot` / `browser_read_text` | AX 快照、按字面名称/value 查询及可操作引用、正文 |
94
94
  | `browser_wait` | 只读等待元素、文本、URL 或网络空闲 |
95
95
  | `browser_click` / `browser_hover` / `browser_drag` / `browser_select_option` / `browser_type` / `browser_fill` / `browser_press_key` / `browser_scroll` | 原生/DOM 输入、指针悬停与拖拽、下拉选择、逐字输入与滚动 |
96
96
  | `browser_batch` | 单队列顺序执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,失败即停并返回部分结果 |
@@ -113,13 +113,12 @@ Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局
113
113
  宿主装配步骤写入客户端条目时,会固定当前 Node、CLI 和实际配对文件的绝对路径,并设置 MCP 的 `BROWSER_ATTACH_HOME`;支持与自定义 `--config` 一起使用。MCP 启动 Hub 时继续传入同一绝对配对路径及目录,避免不同启动环境重新推导出另一份配置。详见 [Agent 集成](docs/AGENT-INTEGRATION.md)。
114
114
 
115
115
  ```sh
116
- browser-attach status
117
- browser-attach stop
118
116
  npm install --global @evan_zhong/browser-attach
119
- browser-attach setup
120
117
  ```
121
118
 
122
- 升级后在浏览器扩展管理页重新加载扩展,再重启 MCP 连接。Hub 与扩展版本必须一致。首次使用本项目的新名称时,应停止之前运行的 Hub 并重新配对;默认端口为 `47831`,可用 `setup --port` 修改。
119
+ 安装新版后重连 MCP。使用 `@latest` 时,在 Hub 与扩展已经运行 0.17 或更高版本后,重连 MCP 即可协调后续升级;显式运行 `setup` 或 `start` 也使用同一流程。从更旧版本升级时,需要先执行 `browser-attach stop`、重启 MCP,并手动重载一次扩展。自定义配对路径、端口和扩展目录会保留。未打开的浏览器 profile 在下次连接时完成更新。旧 MCP 包不会降级新版 Hub 或已安装扩展。
120
+
121
+ 升级会等待扩展清理,并在重载后确认新版本;清理或重载失败会明确报告,不重放页面动作。扩展设置页保留手动重新加载按钮用于恢复。正常页面控制仍要求 Hub 与扩展版本一致。默认端口为 `47831`,创建配对时可通过 `setup --port` 选择其他端口。
123
122
 
124
123
  ## 架构与使用边界
125
124
 
@@ -164,6 +163,7 @@ node bin/browser-attach.mjs mcp
164
163
  npm run test:live -- --executable /path/to/chromium --family chromium
165
164
  npm run test:interaction -- --executable /path/to/chromium --family chromium
166
165
  npm run test:interaction -- --tab-indicators --executable /path/to/chromium --family chromium
166
+ npm run test:upgrade -- --executable /path/to/chromium
167
167
  ```
168
168
 
169
169
  这些测试使用临时配置和本地 fixture。测试覆盖与限制见 [验证报告](docs/TEST-REPORT.md),实现与设计取舍见 [架构与设计](docs/ARCHITECTURE.md)。
@@ -6,7 +6,7 @@ import { ensureHub, hubIsRunning, AttachClient } from '../src/client.mjs';
6
6
  try {
7
7
  const { values, positionals } = parseArgs({ allowPositionals: true, options: {
8
8
  config: { type: 'string', default: DEFAULT_CONFIG },
9
- 'extension-dir': { type: 'string' }, port: { type: 'string', default: '47831' },
9
+ 'extension-dir': { type: 'string' }, port: { type: 'string' },
10
10
  nosis: { type: 'boolean', default: false }, help: { type: 'boolean', short: 'h' },
11
11
  } });
12
12
  const action = positionals[0] || 'mcp', configPath = values.config;
@@ -20,10 +20,10 @@ try {
20
20
  const stop = () => hub.close(); process.once('SIGINT', stop); process.once('SIGTERM', stop);
21
21
  } else if (action === 'setup') {
22
22
  const { setup, configureNosis } = await import('../src/setup.mjs');
23
- const result = await setup({ configPath, extensionDir: values['extension-dir'], port: Number(values.port) });
23
+ const result = await setup({ configPath, extensionDir: values['extension-dir'], port: values.port === undefined ? undefined : Number(values.port) });
24
24
  console.log(`Extension folder: ${result.extensionDir}\nExtension ID: ${result.config.extension_id}\nPairing: ${result.configPath}`);
25
25
  if (values.nosis) console.log(JSON.stringify(await configureNosis(undefined, { configPath: result.configPath })));
26
- await ensureHub(result.configPath); console.log('Local hub started. Load/reload the extension in Chrome/Edge.');
26
+ await ensureHub(result.configPath); console.log('Local hub ready. Load the extension in each Chrome/Edge profile on first use; subsequent upgrades reload it automatically.');
27
27
  } else if (action === 'start') { await ensureHub(configPath); console.log('Local hub started.'); }
28
28
  else if (action === 'status') {
29
29
  const config = await loadConfig(configPath);
@@ -26,7 +26,7 @@ node <安装位置>/bin/browser-attach.mjs mcp --config <配对文件绝对路
26
26
 
27
27
  - `BROWSER_ATTACH_HOME` 指向配对文件所在目录。
28
28
  - Node 可执行文件、CLI 与配对文件一律使用绝对路径。宿主的启动环境(PATH、工作目录、AppData 重定向)不同,重新推导会得到第二份配对。
29
- - 安装位置变化或升级后重新执行 `setup` 并更新宿主条目。MCP 包与已加载扩展必须同版本,否则 Hub 直接拒绝连接。
29
+ - 安装位置变化后重新执行 `setup` 并更新宿主条目。Hub 与扩展已经运行 0.17 或更高版本时,安装新版后重连 MCP 会协调空闲 Hub 与扩展自动升级;从更旧版本首次升级需先停止 Hub、重启 MCP 并手动重载一次扩展。其他 MCP 仍连接时关闭它们后重试。正常页面控制要求同版本。
30
30
 
31
31
  工具前缀由宿主决定,原始工具名不变(`browser_discover`、`browser_claim_tab` 等)。装配后的最小验证在各宿主一致:`browser_discover(kind="browsers")` → `kind="tabs"` → `browser_claim_tab` → `browser_snapshot` → `browser_tab_control(action="release")`。
32
32
 
@@ -110,7 +110,7 @@ npm root -g # 全局安装目录;CLI 位于 <root>/@evan_zhong/browser-a
110
110
  - `web` profile 开启 HMR,修改条目会就地重连;`headless`、`sdk`、`acp` 关闭 HMR,需要重启 harness。
111
111
  - MCP 工具与原生工具一样经过 DSH 的权限检查与审批策略。
112
112
 
113
- 升级后重新加载扩展并重启 harness;包与扩展版本必须一致。
113
+ 安装新版后重启或重连 harness,由 MCP 启动流程协调 0.17+ 的后续 Hub 与扩展升级;首次加载扩展以及从更旧版本首次升级仍需手动重载。
114
114
 
115
115
  ## 新增宿主
116
116
 
@@ -122,4 +122,4 @@ npm root -g # 全局安装目录;CLI 位于 <root>/@evan_zhong/browser-a
122
122
  - 失效句柄不能跨连接复用,错误后应重新 discover + claim。
123
123
  - 文件上传只在浏览器本机读取路径,内容不经过 Hub 或模型。
124
124
  - 宿主把大结果保存为 Artifact 时,嵌套 JSON 长行可能被宿主的读取工具截断;这是宿主的分页行为,不能靠调大 Attach 的预算解决,也不等同于快照游标分页。
125
- - `Connection closed` 时依次检查:宿主的 stderr、配对文件是否可见、`hub.log`、以及 Hub 与扩展的版本是否一致(版本不一致会直接拒绝连接)。
125
+ - `Connection closed` 时依次检查:宿主的 stderr、配对文件是否可见、`hub.log`、是否有其他 MCP 连接阻止升级,以及扩展清理或自动重载是否失败。扩展设置页可手动重新加载以恢复。
@@ -1,6 +1,6 @@
1
1
  # 架构与设计边界
2
2
 
3
- 当前实现为 **0.15.0**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
3
+ 当前实现为 **0.17.0**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
4
4
 
5
5
  ## 组件与数据流
6
6
 
@@ -17,15 +17,17 @@ MCP 客户端
17
17
  |---|---|
18
18
  | `bin/browser-attach.mjs` | `setup` / `mcp` / `hub` / `status` / `stop` 入口 |
19
19
  | `src/config.mjs` | 数据目录、配对文件校验、版本常量 |
20
- | `src/setup.mjs` | 生成配对密钥、复制稳定扩展目录、写 Nosis 配置 |
20
+ | `src/setup.mjs` | 生成配对密钥、暂存并替换稳定扩展目录、写 Nosis 配置 |
21
21
  | `src/hub.mjs` | 浏览器与会话注册、独占 claim、每标签串行队列、超时 |
22
- | `src/client.mjs` | MCP 进程到 Hub 的 WebSocket 客户端,按需拉起 Hub |
22
+ | `src/client.mjs` | MCP 到 Hub 的 WebSocket 客户端、启动升级协调与就绪检查 |
23
23
  | `src/mcp.mjs` | 23 个工具的 schema、结果与图片封装 |
24
24
  | `extension/controller.js` | 操作实现;传输层从不暴露原始 CDP |
25
+ | `extension/connection.js` | 扩展连接、重连清理、升级维护与自行重载 |
25
26
  | `extension/sessions.js` | 扁平 debugger session 与 frame/OOPIF 归属 |
26
27
  | `extension/{click,input,actionability,scroll}.js` | 原生/DOM 输入、逐字按键、可操作性、容器滚动与有界查找 |
27
28
  | `extension/{drag,wait}.js` | 复用鼠标准备和严格定位的拖拽、只读条件等待;网络空闲复用被动请求表 |
28
29
  | `extension/batch.js` | 有限输入序列的参数预检;执行复用 controller 的单步流程与标签队列 |
30
+ | `extension/motion.js` | 无状态的可选落点/延迟/拖拽参数校验、区间采样及拖拽插值;不维护动作历史或改变焦点 |
29
31
  | `extension/outcomes.js` | 临时 DOM 变动观察器与有界只读探测,不推断业务成功 |
30
32
  | `extension/fetch.js` | 单页读取的参数、隔离世界采集与每 owner 有界缓存分页;临时页生命周期复用 controller 的 attach、导航和标签队列 |
31
33
  | `extension/{snapshot,navigation,evaluate,diagnostics}.js` | 观察、导航、有界求值结果缓存与分页、日志缓冲 |
@@ -67,6 +69,7 @@ HTTP 状态复用 diagnostics 的 Network.responseReceived 观察,按主文档
67
69
 
68
70
  - **AX 渲染**:从 `Accessibility.getFullAXTree` 读取,移除 `InlineTextBox`,合并纯文本列表项与段落名称,丢弃 `generic` / `none` 且无名称无状态的节点,只保留少量状态属性和浏览器提供的 value、invalid。每行 `[token:n] role "name"`,`token` 是本次观察的随机前缀;这些字段不证明应用已保存。
69
71
  - **交互视图**:可选 `profile=interactive` 在同一渲染器中仅保留 AX 控件角色、可聚焦/可编辑节点及其祖先链和控件内文本,不以 backendNodeId/ref 是否存在判断交互性。独立文本和合并正文可隐藏或截取预览,控件名称、值和 ref 的原始 AX 身份保留;默认 `full` 行为不变。筛选仍读取完整 AX 树,只降低输出与分页成本;视图参数绑定分页观察。
72
+ - **文本查询与精简输出**:`snapshot(query=...)` 复用同一次 AX 渲染,按当前视图中节点的完整名称(含合并正文)或 value 做不区分大小写的字面子串筛选,只输出匹配行和对应 frame 标题;不搜索 role/ref 或任意 DOM 属性,不新增索引缓存。新查询仍采集新观察并使旧 refs 失效,query 与分页绑定。默认省略 viewport、snapshot_id 和重复规则说明,`verbose=true` 返回前两项;frame 覆盖、视图、预算和游标语义保留。
70
73
  - **ref 解析**:ref 保存 `backendNodeId`、所属 frame/session 和观察时的 AX role/name;动作时重新 `DOM.resolveNode` 并用局部 AX 查询核对语义身份。输入与求值不清空 refs,只使快照分页失效;原节点仍连接时可复用,节点替换、role/name 改变、文档或 frame 变化时安全失败,不按 role/name 猜测替代节点。
71
74
  - **双预算**:`max_nodes` 限制节点数,`max_chars` 限制每行序列化后的累计字符数;两者独立,任一耗尽都会返回 `next_cursor`。
72
75
  - **续读**:续读沿用同一份 `state`(`entries`、`refs`、`nextOffset`),所以 refs 在分页过程中保持有效。单个节点放不进预算时直接报错要求提高预算,避免静默截断尾部。
@@ -90,9 +93,11 @@ HTTP 状态复用 diagnostics 的 Network.responseReceived 观察,按主文档
90
93
  - **焦点信号边界**:OS 窗口失焦、遮挡或 document.visibilityState=hidden 不一定阻止 CDP 输入;document.hasFocus() 在 iframe 导航后也可能为 false。这些值作为 page_state 诊断,不独立拒绝点击。实际送达仍由隔离世界的可信事件回执校验;没有回执时不自动重放。
91
94
  - **送达预算**:原生 click/hover/press_key 的派发及可信回执另有 1250 ms 上限,清理最多 250 ms;系统菜单捕获输入时快速释放队列,仍报告结果未知并提示 Esc,不自动重放。
92
95
  - **hover 与 drag**:hover 复用点击准备与隔离校验,仅派发一次 mouseMoved,清理不移走指针。drag 显式区分 pointer 与 HTML5;后者从真实 dragstart 截获 DragData,再用 CDP 派发 dragover/drop,校验可信 dragstart/drop/dragend。目的节点在按住后滚入视口;所有步骤共用标签队列和总预算,失败时只取消/释放本次手势。
96
+ - **可选输入参数**:显式 offset 在元素 content quad 上插值后沿用命中与 OOPIF 边界检查;settle_ms 在 click/drag 首次派发前等待并复查就绪。drag trajectory 只替换既有插值段,采用法向控制点、平滑进度及随机间隔权重;type 的 delay_ms 可逐间隔独立区间采样。所有等待共用总预算,超时后没有继续派发的链;省略参数时不使用随机数,沿用既有确定性路径。不新增 tab 级历史、冷却、自动激活或重放路径。
93
97
  - **条件等待**:wait 复用严格 selector、可操作性和命中检查,支持出现、可见、可操作、文本/URL 子串及网络空闲;只读轮询,错误附带条件和最后状态。
94
98
  - **原生下拉**:select_option 直接用 selectedIndex setter 和合成 input/change 设置一个 HTML 选项,核验保留结果并返回 events_trusted=false。native click 的 select 命中(含 UA shadow 内部节点和关联 label)在派发前拒绝。
95
99
  - **命中提示**:普通命中保留实际 hit node,沿父元素、Shadow host、slot 查找最近的 HTML/ARIA 可点击候选,返回 clickable_target;hint=false 表示未识别候选,不代表无法响应事件。CSSPseudoElement 先解析为事件所属的 originating element,提示标记 pseudo_element=true,并使用该元素安装事件校验。语义候选不会被用于替换点击目标。
100
+ - **遮挡失败诊断**:当前 frame 的遮挡直接复用已读取的 hit_target;祖先 frame 遮挡额外用至多 200 ms 读取命中描述,诊断节点消失时保留原遮挡错误与 backend node ID。错误包含命中点和受阻 frame。根据共享预算的派发状态区分“未派发”和“结果未知”,避免 drag 按住后准备目的节点时误报零输入。
96
101
  - **送达不等于业务完成**:`target_verified` / `delivery_verified` / `value_verified` 分别证明可信事件命中、keydown 送达、字段文本写入;点击、按键、fill 和 batch 返回 `application_result: "not_checked"`。fill 不证明 React 状态、选项提交、blur 校验或保存完成。
97
102
  - **有限多步输入**:batch 在现有标签队列内执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,共享至多 30 秒预算,每步可另设更短超时;不增加后台任务、循环或脚本引擎;batch 本身只接受每步重新解析的 selector。所有参数先验证,selector 每步重解析,尚未出现的目标只在准备阶段等待;歧义立即报错。首个失败、主文档变化或接管结束后停止,返回此前结果与失败索引;不回滚、不重放、不自动激活或切换输入模式。start_at 显式跳过已完成前缀,返回 failed_step、next_unattempted_step、retry_failed_step_safe;没有后台作业或自动选择续跑点。
98
103
  - **OOPIF 坐标**:点击从顶层派发。逐层用 `DOM.getBoxModel` 把坐标换算到祖先文档,并用 `DOM.getNodeForLocation` 确认该位置确实落在 frame owner 上,防止祖先页面的遮挡被跳过。
@@ -115,7 +120,7 @@ HTTP 状态复用 diagnostics 的 Network.responseReceived 观察,按主文档
115
120
  ## 安全与隐私
116
121
 
117
122
  - Hub 只监听 `127.0.0.1`;HTTP 端点拒绝任何带 `Origin` 的请求,客户端必须携带 Bearer 密钥。
118
- - 扩展连接固定校验 `chrome-extension://<extension_id>` Origin、随机 token 与版本号,三者任一不符即断开。
123
+ - 扩展连接固定校验 `chrome-extension://<extension_id>` Origin 与随机 token。同版本才注册为可控浏览器;已配对的较旧扩展仅进入维护连接,收到重载请求后校验本机文件、完成清理再自行重载。客户端握手同样要求版本一致。
119
124
  - 配对文件 `config.json` 与生成的 `extension/config.generated.js` 含密钥:目录在 Windows 上用当前用户 SID 收紧 ACL,在 POSIX 上为 `0700`/`0600`;发布包检查会拒绝包含 `config.generated.js`。
120
125
  - 扩展具备 `debugger` 与 `tabGroups` 权限,操作使用用户现有登录态;只应接入信任的 MCP 客户端。网页内容、控制台与网络日志均视为非可信数据。
121
126
  - 文件上传只传路径,Hub 与模型都不接触文件内容;选择文件可能立即把内容发给网站,因此只传任务明确指定的文件。
@@ -138,4 +143,6 @@ HTTP 状态复用 diagnostics 的 Network.responseReceived 观察,按主文档
138
143
 
139
144
  ## 版本与兼容
140
145
 
141
- `package.json`、`src/config.mjs`、`extension/manifest.json` 的版本必须一致;Hub 会拒绝版本不同的扩展,`hubIsRunning` 也会拒绝版本不同的 Hub。MCP 示例配置使用 `@latest`,升级流程:停止旧 Hub → `browser-attach setup` → 在扩展管理页重新加载扩展 → 重连 MCP。
146
+ `package.json`、`src/config.mjs`、`extension/manifest.json` 的版本必须一致。MCP 示例配置使用 `@latest`,MCP 启动、`start` 和 `setup` 共用升级协调:检查已配对 Hub → 空闲 Hub 暂停接入并确认扩展清理 → 旧 Hub 退出 → 新 Hub 独占端口后同步扩展 → 旧扩展自行重载 → 确认新版重连。该自动路径要求 Hub 与扩展已经具备 0.17 的 `connection.js` 维护协议;更旧版本的首次升级必须先停止 Hub、重启 MCP 并手动重载扩展(旧 Hub 没有升级端点,旧扩展也不处理重载命令)。MCP 在 stdio 就绪前即连接 Hub,尚未调用工具的 MCP 会话也会阻止升级。有客户端、断连清理或在途请求时返回占用错误,关闭其他 MCP 连接后重试;不强制打断页面动作。
147
+
148
+ Hub 的监听端口同时保证同一配对的文件同步只有一个执行者。扩展先完整暂存,保留稳定目录本身,逐文件替换并最后提交 manifest;替换失败恢复旧文件,进程在替换中退出时通过 `.previous` 副本恢复。配对密钥、端口和自定义目录保留,旧包不得降级新版 Hub 或扩展。扩展重载记录尝试,防止失败后无限循环;派发重载不代表升级成功,启动协调仍等待实际版本。未打开的 profile 在下次连接时处理;首次安装仍需手动加载扩展。此流程不热替换正在运行的 MCP 进程。
@@ -0,0 +1,27 @@
1
+ # 0.15.1 工具效率与设计取舍
2
+
3
+ 本次对照 `src/mcp.mjs`、`src/hub.mjs`、扩展实现与现有测试核查九项建议。结论是:返回内容和失败诊断存在可直接改进之处;等待、显式 frame、接管和输入结果边界主要是有意的可靠性策略。不能单凭一次调用的字符数或延迟判定策略有缺陷。题述 BA/PW 对照未在同一页面、版本和序列化口径下重跑,以下不把这些数值当作本次验收结果。
4
+
5
+ ## 逐项判断
6
+
7
+ | 建议 | 当前实现与性质 | 通用性、复杂度及本次选择 |
8
+ |---|---|---|
9
+ | 1. 默认精简、interactive、delta | full 同时服务阅读与交互,是覆盖面策略;每页重复规则和固定诊断元数据则是输出冗余。input/evaluate 使 cursor 失效,但仍连接且语义未变的 refs 可复用,并非每次输入都必须重新 snapshot。 | 精简通用且成本低,已移除重复说明,viewport/snapshot_id 改为 verbose 可选。保留 full 默认与结果验证字段。interactive 已有,按任务选择。delta 需处理新旧观察、删除/替换、祖先上下文和子 frame 失效,复杂度高,本次不引入。 |
10
+ | 2. AX 索引查询 | 当前只能把树传给客户端后查找,确有返回量缺口;read_text 不能替代可操作 ref。 | 已在现有 snapshot 加 query,复用渲染、ref、frame 和分页,工具仍为 23 个。查询新采集 AX,不引入可能过期的第二份索引缓存。仅支持字面子串;正则及独立 find 工具暂不增加。能省返回内容和一次往返,不能保证 AX 采集变快。 |
11
+ | 3. 日志摘要与明细 | 当前已有 after_seq、limit、max_chars、level、url_contains 与有界缓冲;默认不保存 headers/body,必须显式启用 capture。网络输出是 request/redirect/response/finished 等事件,并非一条完整请求对象。 | 请求摘要具有通用价值,但正确合并需要处理 session/request_id/hop、未完成请求、分页与淘汰;单次投影也不能自然变成完整请求的耗时列表。seq 明细可复用 logs 扩展,无须先增加工具。默认隐藏静态资源会漏掉 CSS/脚本故障,本次保留,先用现有过滤与预算。 |
12
+ | 4. 零命中立即失败、诊断原因 | resolveSelector 的零命中等待是支持异步渲染和 batch 前一步创建后一步目标的明确策略;多命中和语法错误已立即失败。deadline 已保留最后的 selector/可见性/尺寸等状态。遮挡分支却只给泛化错误,属于诊断不足。 | 已补实际 hit_target、点和受阻 frame,不增加准备重试或动作重放。普通点击明确未派发;drag 已按住后的目的节点准备失败仍报告结果未知。保留零命中等待,紧预算可用 timeout_ms(最小 100 ms),只读查询零命中不等待出现。 |
13
+ | 5. deep-css、自动跨 frame | 当前 CSS/XPath 明确只解析指定文档;ref 可操作 AX 暴露的 Shadow 节点;evaluate(result_type=element) 可显式选取 open shadow 元素,frame URL/title/path 已可逐次解析。属于范围和消歧策略。 | 显式 deep-css 对组件站点可能有用,但要定义复合选择器、遍历顺序、nth 和多 root 歧义,成本中等。跨 frame 再涉及 OOPIF、重复控件、未加载/分离文档和多层命中校验,成本更高。本次保留显式 frame 与现有 ref/element 路径。 |
14
+ | 6. batch 结果传递 | 当前 batch 是预检后的有限输入序列;没有 evaluate 或 drag 步骤,element 步骤只用 selector。因此单加占位符不能实现“evaluate 测量 → drag”。这是有限编排的边界,也确有限制。 | 动态数据流需增加结果生产步骤、类型/路径检查、替换后的再校验,以及部分完成的恢复规则,复杂度中高。原生输入仍不能由页面 JS 调用。常见“定位 → 输入”已可用 selector 或 evaluate 返回元素 ref;坐标测量保留独立检查点,本次不扩展 batch。 |
15
+ | 7. tabs 一站式 | MCP 已把浏览器/标签发现合并为 discover,把 release/activate/close 合并为 tab_control。发现后按本连接观察的 tab_id 独占 claim 是授权模型,激活则是另一个显式意图。 | 客户端可保存 profile 与连接内句柄减少重复 setup。再增加 tabs 聚合入口主要重复现有分支;若自动选标签或激活,反而改变边界。本次不改。工具入口数量与必须经过的授权步骤不能简单等同。 |
16
+ | 8. 渐进披露 schema | 描述已在 0.13.1 分层精简;本次修改前实际 tools/list 紧凑 JSON 为 36701 字符、36703 UTF-8 字节,格式化 JSON 为 75945 字节,正好对应题述约 75.9 KB,23 个工具。两种序列化是同一 schema,不意味着宿主实际省掉一半 token。是否每轮重新计入、是否缓存,取决于 MCP 宿主,仓库不能保证。 | 工具分组对更大工具集有价值,但会新增启动配置、发现差异和支持组合。当前不加 caps 开关;query 复用已有工具,新增少量 schema 描述以保持自描述。包内文档链接不能替代模型调用所需的关键约束。 |
17
+ | 9. 可回放执行记录 | 当前不持久化操作轨迹;batch 调用输入本身就是可保存的步骤 JSON,输出已有部分完成、failed_step/start_at 等恢复信息。缺少长期审计是能力缺口,不是当前执行可靠性缺陷。 | 审计记录与可重新执行的计划应区分:已送达的动作不能因“可回放”自动重发,ref 也不能跨观察/连接复用。默认回传完整 steps 又与节省输出冲突,持久化还会包含表单数据。需要明确的保存/导出需求后再做显式功能,本次不新增存储或回放引擎。 |
18
+
19
+ ## 实际改动
20
+
21
+ - `extension/snapshot.js`:query 在当前视图的完整 AX 名称(包括合并正文)或 value 上做大小写不敏感的字面子串匹配,只为匹配行生成 refs。复用原预算和游标;query 绑定观察,零命中返回空结果。
22
+ - `extension/controller.js`:查询仅输出有匹配行的 frame 标题;仍报告不可读或失效的 frame。每个新查询沿用新快照的采集与 ref 生命周期,续页不重复采集。
23
+ - 快照默认不再输出 viewport、snapshot_id 和重复说明;verbose 可恢复前两项。保留 profile/text/text_limit、frame 覆盖与分页字段。click/evaluate 的事件送达、分页或捕获损失等语义字段继续保留。
24
+ - `extension/click.js`:当前 frame 直接复用已有命中诊断;祖先 frame 最多额外读 200 ms,失败时保留原遮挡错误。读取预算已派发标记,防止复用准备逻辑的 drag 被误标为“无输入”。
25
+ - 版本按本次明确要求设为 **0.15.1**。没有新增工具、运行依赖、扩展权限、用户 UI、自动重放或独立状态管理层。
26
+
27
+ 使用示例和边界见 [使用指南](USAGE.md),验证结果见 [验证报告](TEST-REPORT.md)。
@@ -1,9 +1,33 @@
1
1
  # 验证报告
2
2
 
3
- 当前版本 **0.15.0**(2026-10-22)。验证使用本地 fixture 和临时 profile,链路为扩展 → 本机 Hub → stdio MCP。
3
+ 当前版本 **0.17.0**(2026-10-23)。浏览器验证使用本地 fixture 和临时 profile;测试链路在各项下说明。
4
4
 
5
5
  ## 当前版本
6
6
 
7
+ - `npm test`:196 项通过。新增并发升级与冷启动、无浏览器启动和离线 profile 迟到更新、尚未调用工具的 MCP 会话阻止升级、清理失败恢复、禁止降级、独占端口后才同步、文件中断恢复、自定义配对保留、维护期间拒绝页面动作、本机版本核验、重载循环抑制、detach 失败后禁止重连,以及旧配对缺少 `extension_dir` 时回落到同目录 `extension` 的兼容处理。
8
+ - Chromium 156 与 Edge 155:`npm run test:upgrade -- --executable <路径>` 均通过。临时 profile 开启开发者模式后,两个并发启动者将 Hub 和真实扩展从当前代码改版本号构造的兼容旧协议 0.16.9 升到 0.17.0;确认同一 profile、实际 runtime 重载后的版本,以及升级后发现/接管 → AX 查询 → DOM 点击 → 查询结果 → 释放。CDP 只用于临时 profile 的开发者模式设置,页面操作走扩展与 Hub。该脚本验证升级协议本身,不代表真实 0.16 扩展能自行重载;真实旧版本首次升级仍需手动重载。
9
+ - `npm run test:package`:仓库外安装运行通过,23 个工具、50 个发布文件;由已安装包中的 Hub 同步稳定扩展目录并运行 stdio MCP,不含配对密钥,无新增运行依赖或扩展权限。
10
+ - 本机部署:Hub 已从工作区重启并返回 0.17.0 / ready;`F:\.nosis\browser-attach\extension` 已同步,manifest 为 0.17.0。Chrome、Edge 均上报 0.17.0 并已重连。
11
+
12
+ ## 0.16.0 验证
13
+
14
+ - `npm test`:184 项通过。新增 offset 的变换/越界处理、延迟区间校验、默认路径不采样随机数、拖拽曲线与精确终点、batch 参数预检、真实 stdio MCP 参数转发,以及 settle/拖拽超时后停止和调试分离取消待点击的回归。
15
+ - Chromium 156 与 Edge 155 headless 输入集成:各 19 个场景通过(扩展 → 独立 Hub 客户端)。新增非中心可信点击、offset hover、settle、区间逐字输入、曲线 pointer 与 HTML5 拖拽;实际记录移动坐标、可信事件和按键时间,保留原有默认路径及跨屏拖拽验证。不据此声称能通过任意网站的行为验证。
16
+ - Chromium 156 完整 stdio MCP 集成:244 次调用通过,保留发现/接管、输入、导航、OOPIF、日志、分页及临时页清理覆盖。
17
+ - `npm run test:package`:仓库外安装运行通过,23 个工具、48 个发布文件,版本 0.16.0;无新增依赖或扩展权限,不包含本机配对密钥。
18
+ - 本机部署:Hub 已从工作区重启,健康接口返回 0.16.0;扩展同步至 `F:\.nosis\browser-attach\extension`,29 个源码/资源文件哈希一致,manifest 版本为 0.16.0。Chrome 与 Edge 均已重新连接并上报 0.16.0;MCP 客户端需刷新连接以获取新 schema。
19
+
20
+ ## 0.15.1 验证
21
+
22
+ - `npm test`:176 项通过。新增 AX 字面查询(名称/合并正文/value、预览前匹配、interactive 视图、零命中)、查询分页与新旧 ref 生命周期、默认精简/verbose 元数据、当前/祖先 frame 的遮挡诊断、诊断节点失效保留主错误,以及 drag 已派发预算不能误报“未派发”的回归。
23
+ - Chromium 156 与 Edge 155 headless 完整 stdio MCP 集成:各 244 次调用通过。新增真实 AX 查询返回的 ref 可用于原生 hover、查询分页和 verbose 续页、零命中及 ancestor frame 的实际遮挡文字;保留输入、导航、OOPIF、日志、分页和临时页关闭链路。
24
+ - 同一构造表单 AX(2 个字段、2 个按钮,固定 URL/ref 前缀)比较 JSON 字符数:0.15.0 默认 908,0.15.1 默认 443(减少 51.2%),query=save 为 332。这里只衡量固定 fixture 的序列化输出,不代表题述表单、token 数或 AX 采集耗时。
25
+ - 实际 MCP tools/list:仍为 23 个工具;紧凑 JSON 从 36703 增至 37116 UTF-8 字节(新增 query/verbose 参数及说明,约 +1.1%),格式化 JSON 从 75945 增至 76502 字节。未声称 schema 总量下降;不新增工具分组、依赖或扩展权限。
26
+ - `npm run test:package`:仓库外安装运行通过,23 个工具、47 个发布文件,版本为 0.15.1;发布包不含本机配对密钥。
27
+ - 本机部署:已把扩展同步至 `F:\.nosis\browser-attach\extension`,关键模块与源码一致,Hub 健康接口返回 0.15.1。临时 Chromium 加载该实际副本连接共享 Hub,AX 查询 → 原生点击 → 查询确认页面变化通过。验证结束时用户原有浏览器尚未重新连接;仍需在 Chrome/Edge 重新加载扩展,并刷新 MCP 连接以获取新 schema。
28
+
29
+ ## 0.15.0 验证
30
+
7
31
  - `npm test`:172 项通过。新增 commit / DOMContentLoaded、等待超时后的降级采集、主文档替换后跟随 loader、采集期间只读重试、等待完成后换文档的身份核对、HTTP 错误响应关联及空体判断,以及标签创建/关闭超时和迟到清理;保留断连、控制权变化与不重放导航的回归覆盖。
8
32
  - Chromium 156 与 Edge 155 headless 完整 stdio MCP 集成:各 238 次调用通过。新增阻塞 load 时的早期等待及超时降级、load 前 location.replace、SSE 阻塞 network_idle,以及空体 418/500/503 的状态码和正文来源;同时验证既有输入、导航、OOPIF、分页与临时页关闭链路。
9
33
  - Chromium 临时配置访问真实 URL:`https://httpbin.org/status/500` 与 `/status/503` 分别在约 689 ms、384 ms 返回对应状态、`ok=false`、空正文及 `http_error_response` 标记。`https://chatgpt.com/` 在约 2253 ms 返回 403 验证页的文档结果,未复现客户端跳转,不将该结果视为登录应用验收。
package/docs/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # BrowserAttach 使用指南
2
2
 
3
- 适用于 **0.15.0**:Node.js 22+、Chrome / Edge 125+、23 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
3
+ 适用于 **0.17.0**:Node.js 22+、Chrome / Edge 125+、23 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
4
4
 
5
5
  工具发现采用三层说明:初始化 `instructions` 提供跨工具规则;工具 description 说明用途、独有行为与结果语义;参数 schema 及其短说明提供类型、默认值和选择规则。本文保留完整原理、边界和示例。客户端集成时应把 MCP 初始化响应中的 instructions 传入模型上下文;仅读取 tools/list 不会包含完整的通用调用规则。
6
6
 
@@ -31,11 +31,13 @@
31
31
 
32
32
  `discover.kind` 与 `tab_control.action` 必填,无默认值。`browser_id` 仅在 `kind=tabs` 时使用且必填;`focus_window` 仅在 `action=activate` 时使用,省略时为 `true`。其他分支传入这些参数会在派发前报错。新建标签仍需加载后发现并显式接管,不会自动取得控制权。
33
33
 
34
+ `discover(kind="browsers")` 仅在 `browsers` 中列出同版本、可接受页面操作的扩展。0.17+ 的较旧扩展自动重载期间列在 `upgrades` 中,失败时附带 `error`;维护连接不提供可用于接管的浏览器身份。从 0.17 之前版本升级时,旧扩展不具备自动重载能力,需要手动重载一次。
35
+
34
36
  ### 观察
35
37
 
36
38
  | 工具 | 主要参数 | 说明 |
37
39
  |---|---|---|
38
- | `browser_snapshot` | `profile`, `text`, `text_limit`, `max_nodes=600`, `max_chars=24000`, `cursor` | AX 语义树、元素 ref、frame 覆盖、待处理文件选择器 |
40
+ | `browser_snapshot` | `profile`, `text`, `text_limit`, `query`, `verbose=false`, `max_nodes=600`, `max_chars=24000`, `cursor` | AX 语义树、按名称/value 查询、元素 ref、frame 覆盖、待处理文件选择器 |
39
41
  | `browser_wait` | `condition`, `selector`, `expected` / `previous`, `idle_ms=500`, `timeout_ms=5000` | 只读等待元素、文本、URL 或被动网络空闲 |
40
42
  | `browser_fetch` | `browser_id`, `url`, `wait_until=load`, `format=text`, `max_chars=20000`, `cursor` | 后台临时页读取一次,关闭后按缓存续读 |
41
43
  | `browser_read_text` | `mode`, `selector`, `offset=0`, `max_chars=20000` | 主 frame 的 DOM、字段值或支持的编辑器模型 |
@@ -94,7 +96,7 @@ browser_fetch({browser_id, cursor: next_cursor, max_chars: 20000})
94
96
 
95
97
  ## 观察:快照与引用
96
98
 
97
- `browser_snapshot` 返回 `snapshot` 文本(每行 `[token:n] role "name" 状态`)、`snapshot_id`、`node_count`、`total_node_count`、`truncated`、`truncation_reason`(`node_limit` / `text_budget`)、`next_cursor`、`viewport`、`omitted_frames`,以及当前 `title` / `url`。
99
+ `browser_snapshot` 返回 `snapshot` 文本(每行 `[token:n] role "name" 状态`)、`node_count`、`total_node_count`、`truncated`、`truncation_reason`(`node_limit` / `text_budget`)、`next_cursor`、`omitted_frames`,以及当前 `title` / `url`;`verbose: true` 时额外返回 `viewport` 和 `snapshot_id`。
98
100
 
99
101
  - 渲染时移除 `InlineTextBox`,合并纯文本列表项与段落名称,保留 `disabled`、`focused`、`expanded`、`checked`、`selected`、`required`、`invalid`、`level` 等状态,以及浏览器提供的 `value`;`[token:n]` 即后续动作使用的 `ref`。未提供 invalid/selected 等属性不代表有效或已提交;value 也可能只是下拉框的过滤文本。
100
102
  - `max_nodes` 为 1–3000(默认 600);`max_chars` 为 1–2000000(默认 24000),按行序列化长度累计,与节点数是两个独立预算。
@@ -102,9 +104,19 @@ browser_fetch({browser_id, cursor: next_cursor, max_chars: 20000})
102
104
  - `text` 默认 `true`;`false` 隐去独立文本和纯文本段落/列表项的合并正文,仍保留可访问名称及控件值。`text_limit` 为 0–10000,限制每个文本/合并正文名称并以 `…` 标明省略;`interactive` 默认 80,`full` 默认不截断。需要更多正文时重新请求 `full`,或显式提高 `text_limit`。控件名称、值及 ref 校验所用身份始终完整。
103
105
  - 返回 `profile` / `text` / `text_limit` 表明本次视图;`node_count` 和 `total_node_count` 统计筛选后的节点。`truncated` / `next_cursor` 只表示该视图尚有分页,不表示视图包含全部网页正文。筛选发生在完整 AX 树读取后,减少返回内容与分页次数,不减少 CDP 的 AX 树采集量。
104
106
  - `truncated: true` 时把 `next_cursor` 传回同一个工具继续读取同一份观察,refs 在续读期间保持有效;也可以调整预算。单个超长节点放不下时会明确报出所需预算,不静默丢尾部。
105
- - 续读可省略视图参数,沿用原 `profile` / `text` / `text_limit`;显式提供时必须与原观察相同。调整视图须请求新快照,并按下表使旧 ref 失效。
107
+ - 续读可省略视图参数,沿用原 `profile` / `text` / `text_limit` / `query`;显式提供时必须与原观察相同。调整视图须请求新快照,并按下表使旧 ref 失效。
106
108
  - 覆盖主 frame、同进程 iframe 与递归 OOPIF;读取失败、读取期间变化或附着失败的 frame 列入 `omitted_frames`。
107
109
 
110
+ 默认输出省略 viewport、snapshot_id 和重复的使用说明;需要视口几何与观察标识时传 `verbose: true`,也可在同一 cursor 的续页上开启。frame 覆盖、分页与截断字段始终保留。默认仍是 full 视图,阅读与交互任务可自行选择。
111
+
112
+ 已知目标文字时可直接查询,无需先返回整棵树:
113
+
114
+ ```json
115
+ {"tab_handle":"...","query":"保存","max_nodes":20}
116
+ ```
117
+
118
+ `query`(1–2000 字符,不能全为空白)在当前视图的 AX 完整名称(含合并正文)或 value 中匹配不区分大小写的字面子串,保留原始空格,不解释正则;不匹配 role、ref 或未暴露给 AX 的 DOM 内容。匹配发生在文本预览截取之前,返回匹配行及所属 frame 标题,有 backend node 的节点带可操作 ref。`total_node_count` 统计匹配节点;零命中正常返回空 snapshot,不等待目标出现。多命中按原有 max_nodes/max_chars 和 cursor 续读。每个新查询仍读取完整 AX 树、替换旧观察并使旧 refs 失效;只减少返回量,不能据此推断采集时间缩短。`profile=interactive` 会先筛掉正文,阅读查询保留默认 full。
119
+
108
120
  失效规则:
109
121
 
110
122
  | 事件 | 影响 |
@@ -148,6 +160,8 @@ ref 每次用 frame/session + backendNodeId 重新解析,并以局部 AX 查
148
160
  | `button` | `left`(默认)/ `middle` / `right` | |
149
161
  | `click_count` | `1`(默认)/ `2` | 双击按顺序重复 down/up |
150
162
  | `modifiers` | `Alt` / `Control` / `Meta` / `Shift` | 默认空数组 |
163
+ | `offset` | `{x_ratio, y_ratio}`,两项均为 0–1 | 原生元素目标的 content quad 内落点;省略沿用可见区域中心 |
164
+ | `settle_ms` | 0–1000 或 `[min,max]` | 原生点击准备后的派发前等待;区间每次均匀抽样,省略为 0 |
151
165
  | `mode` | `native`(默认)/ `dom` | DOM 模式接受 ref 或 selector |
152
166
 
153
167
  原生模式按序派发 move → down → up,显式设置 `buttons` 与修饰键,并在隔离世界安装命中校验:成功返回 `target_verified: true`、`application_result: "not_checked"`。左键单击 / 双击分别校验 `click` / `dblclick`,中键校验 `auxclick`,右键校验 `contextmenu`。目标被遮挡、在 hover 后移动或派发中变化时会明确报错并阻止不匹配事件;已送达的前序事件可能已有副作用,不能撤销或自动重试。
@@ -180,6 +194,18 @@ hover 接受与原生 click 相同的目标,执行一次 mouseMoved,不派
180
194
 
181
195
  drag 的 source / destination 各自接受 ref、严格 selector(可带 frame_id/nth)或 x/y(可带 coordinate_space)。mode 必须按页面交互选择,默认 pointer;steps 默认 20,范围 2–100,每步间隔约 16 ms。
182
196
 
197
+ 可选参数不改变默认输入路径:
198
+
199
+ - click、hover 以及 drag 的 source/destination 元素目标可指定 `offset: {x_ratio: 0.3, y_ratio: 0.65}`。比例沿 content quad 两个方向插值,支持旋转/倾斜元素;指定点不在视口或被遮挡时失败,不改投中心。多行元素按浏览器返回的 quad 顺序选择第一个落点在视口内的片段。坐标目标和 DOM click 不接受 offset。
200
+ - 原生 click 与 drag 可传 `settle_ms: [80,250]`,也可传固定毫秒数;在目标准备和校验器安装后、首次鼠标派发前等待,之后重新检查标签就绪。不引入自动激活。hover 不增加停留等待。
201
+ - drag 可传 `trajectory: {curve_px: 12, duration_ms: 600}`,并沿用 `steps` 控制采样数。curve_px 为二次曲线控制点相对路径法线的有符号偏移(−100–100 CSS px),实际曲线最大偏移约为其一半;duration_ms 为插值段等待总和(100–5000 ms)。采用平滑变速和独立随机权重分配间隔,最后一点严格等于目标。源点到起拖阈值的移动仍沿用现有实现,HTML5 仍使用真实截获的拖拽数据。总耗时还包含准备、CDP 和收尾;曲线离开视口时停止并释放本次手势。
202
+
203
+ ```json
204
+ {"tab_handle":"...","source":{"selector":"#card","offset":{"x_ratio":0.3,"y_ratio":0.6}},"destination":{"selector":"#column"},"steps":24,"settle_ms":[80,250],"trajectory":{"curve_px":12,"duration_ms":600}}
205
+ ```
206
+
207
+ 这些参数复用原有总 timeout、命中校验和失败清理,不保证满足网站的行为验证。省略时不采样随机数,保持原有落点、拖拽直线和 16 ms 间隔。batch 的 click/hover 支持对应 offset,click 支持 settle_ms。
208
+
183
209
  两端均为 document 坐标且能同时容纳于一个顶层视口时,在按下前统一滚动位置,保留两端的 x/y 差值。已在视口内的 document 坐标不重复居中;需要滚动时只调整越界的轴。跨屏目标仍在按住后滚入视口,因此返回的 from/to 是各自派发时的视口坐标,不能用两者之差推断跨屏的文档距离。
184
210
 
185
211
  ```json
@@ -233,7 +259,7 @@ fill 是替换文本,原生路径用 insertText,不制造逐字 keydown。
233
259
  {"tab_handle":"...","selector":"input[role=combobox]","text":"上海","delay_ms":50}
234
260
  ```
235
261
 
236
- delay_ms 为 0–1000,是字符之间的最小额外间隔,准备与协议开销会增加实际耗时;整串 text 最多 10000 个 UTF-16 单位,仍受同一 30 秒以内的总预算约束。换行/Tab/控制字符用显式 press_key。检查原字段连接与焦点,观察到失焦或替换后停止;失败报告已核验字符数,当前字符仍可能已送达,不能重放整串。delivery_verified 证明 keydown,不证明站点接受文本或保存。
262
+ delay_ms 接受固定的 0–1000 毫秒数(默认 0),或有序区间 `[min,max]`(两端均为 0–1000 整数),如 `[40,180]`;区间在每两个字符之间独立均匀抽样,末字符后不额外等待。它是字符之间的最小额外间隔,准备与协议开销会增加实际耗时;整串 text 最多 10000 个 UTF-16 单位,仍受同一 30 秒以内的总预算约束。batch 的 type 步骤接受相同参数。换行/Tab/控制字符用显式 press_key。检查原字段连接与焦点,观察到失焦或替换后停止;失败报告已核验字符数,当前字符仍可能已送达,不能重放整串。delivery_verified 证明 keydown,不证明站点接受文本或保存。
237
263
 
238
264
  ### browser_batch
239
265
 
@@ -400,5 +426,6 @@ handle_dialog 返回原始 `dialog.type/message/default_prompt` 和 `accepted`
400
426
  - 标签拖动或浏览器原生菜单可能捕获原生输入,CDP 返回成功不等于页面收到事件;结束该交互后再依赖原生输入。不要用 `Input.setIgnoreInputEvents(false)` 强行解除用户的菜单 / 拖动状态。
401
427
  - 后台操作要求浏览器运行且页面未冻结;网站自己弹窗或权限 UI 仍可能改变焦点。
402
428
  - `target_verified`、`delivery_verified`、`value_verified` 只证明事件送达或文本写入,不证明提交、付款、服务端上传等业务完成。
429
+ - 点击/hover 准备时发现遮挡会返回 `hit_target`、命中点及受阻 frame;祖先 frame 的节点描述若失效则保留 backend node ID。普通点击在此失败尚未派发输入;drag 若已经按住,再准备目的节点时遇到遮挡仍按结果未知处理。不可见/禁用/尺寸不稳定及 selector 尚未出现沿用准备等待与最后状态诊断。
403
430
  - Hub 日志位于配对文件同目录的 `hub.log`;`browser-attach status` 查看连接状态,`browser-attach stop` 停止 Hub。
404
431
  - 宿主把大结果保存为 Artifact 时,长行可能被宿主截断;这是宿主读取行为,不是快照游标分页。相关说明见 [Agent 集成](AGENT-INTEGRATION.md)。
@@ -1,98 +1,7 @@
1
- import { CONNECTION } from './config.generated.js';
2
- import { createController } from './controller.js';
3
-
4
- let socket;
5
- let connecting = false;
6
- let keepAlive;
7
- let reconnect;
8
- let status = 'disconnected';
9
- let identity;
10
- const controller = createController(chrome, event => {
11
- if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(event));
12
- });
13
-
14
- async function getIdentity() {
15
- const local = await chrome.storage.local.get(['instanceId', 'profileName']);
16
- const session = await chrome.storage.session.get('runId');
17
- const instanceId = local.instanceId || crypto.randomUUID();
18
- const runId = session.runId || crypto.randomUUID();
19
- if (!local.instanceId) await chrome.storage.local.set({ instanceId });
20
- if (!session.runId) await chrome.storage.session.set({ runId });
21
- const family = /Edg\//.test(navigator.userAgent) ? 'edge' : 'chrome';
22
- return { instance_id: instanceId, run_id: runId, family,
23
- profile_name: local.profileName || (family === 'edge' ? 'Edge' : 'Chrome'),
24
- user_agent: navigator.userAgent, extension_id: chrome.runtime.id, extension_version: chrome.runtime.getManifest().version };
25
- }
26
-
27
- async function connect() {
28
- if (connecting || socket?.readyState === WebSocket.OPEN) return;
29
- connecting = true;
30
- try {
31
- identity = await getIdentity();
32
- const ws = new WebSocket(CONNECTION.url);
33
- socket = ws;
34
- ws.onopen = () => {
35
- ws.send(JSON.stringify({ type: 'hello', token: CONNECTION.token, browser: identity }));
36
- keepAlive = setInterval(() => {
37
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
38
- }, 20000);
39
- };
40
- ws.onmessage = async event => {
41
- let request;
42
- try {
43
- request = JSON.parse(event.data);
44
- if (request.type === 'connected') {
45
- status = 'connected';
46
- await chrome.action.setBadgeText({ text: 'ON' });
47
- await chrome.action.setBadgeBackgroundColor({ color: '#13755b' });
48
- return;
49
- }
50
- if (!request.id) return;
51
- const result = await controller.execute(request.owner, request.command, request.arguments || {});
52
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ id: request.id, result }));
53
- } catch (error) {
54
- if (request?.id && ws.readyState === WebSocket.OPEN) {
55
- ws.send(JSON.stringify({ id: request.id, error: error.message || String(error) }));
56
- }
57
- }
58
- };
59
- ws.onclose = async () => {
60
- clearInterval(keepAlive);
61
- status = 'disconnected';
62
- connecting = false;
63
- await controller.releaseAll();
64
- await chrome.action.setBadgeText({ text: 'OFF' });
65
- clearTimeout(reconnect);
66
- reconnect = setTimeout(connect, 2000);
67
- };
68
- ws.onerror = () => ws.close();
69
- } catch (error) {
70
- connecting = false;
71
- status = error.message;
72
- }
73
- }
74
-
75
- chrome.runtime.onMessage.addListener((message, sender, respond) => {
76
- if (sender.id !== chrome.runtime.id) return;
77
- if (message.type === 'status') {
78
- getIdentity().then(browser => respond({ status, browser, claims: controller.claimedCount() }));
79
- return true;
80
- }
81
- if (message.type === 'reconnect') {
82
- if (socket?.readyState === WebSocket.OPEN) socket.close();
83
- else connect();
84
- respond({ ok: true });
85
- }
86
- if (message.type === 'release_all') {
87
- controller.releaseAll().then(() => respond({ ok: true }));
88
- return true;
89
- }
90
- });
91
- chrome.alarms.onAlarm.addListener(alarm => { if (alarm.name === 'connect') connect(); });
92
- chrome.runtime.onStartup.addListener(connect);
93
- chrome.runtime.onInstalled.addListener(async () => {
94
- await chrome.alarms.create('connect', { periodInMinutes: 0.5 });
95
- connect();
96
- });
97
- chrome.alarms.create('connect', { periodInMinutes: 0.5 });
98
- connect();
1
+ import { CONNECTION } from './config.generated.js';
2
+ import { createController } from './controller.js';
3
+ import { createConnection } from './connection.js';
4
+
5
+ let connection;
6
+ const controller = createController(chrome, event => connection?.send(event));
7
+ connection = createConnection(chrome, CONNECTION, controller);
@@ -3,6 +3,7 @@ import { clickOptions } from './click.js';
3
3
  import { inputMode, keyParameters, selectOptions, typeOptions } from './input.js';
4
4
  import { scrollOptions } from './scroll.js';
5
5
  import { waitOptions } from './wait.js';
6
+ import { offsetOption } from './motion.js';
6
7
 
7
8
  export function validateSteps(steps, startAt = 0) {
8
9
  if (!Array.isArray(steps) || steps.length < 1 || steps.length > 20) throw new Error('steps must contain 1..20 actions');
@@ -24,6 +25,7 @@ export function validateSteps(steps, startAt = 0) {
24
25
  targetOptions(step);
25
26
  if (['click', 'fill'].includes(step.action)) inputMode(step);
26
27
  if (step.action === 'click') clickOptions(step);
28
+ if (step.action === 'hover') offsetOption(step);
27
29
  if (step.action === 'fill' && typeof step.text !== 'string') throw new Error('Batch fill requires text');
28
30
  if (step.action === 'type') typeOptions(step);
29
31
  if (step.action === 'select_option') selectOptions(step);