@evan_zhong/browser-attach 0.23.4 → 0.23.5
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 +7 -11
- package/README.zh.md +9 -12
- package/docs/AGENT-INTEGRATION.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/TEST-REPORT.md +14 -2
- package/docs/USAGE.md +12 -11
- package/extension/manifest.json +1 -1
- package/extension/pointer.js +4 -2
- package/extension/upload.js +25 -17
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/config.mjs +1 -1
- package/src/mcp.mjs +2 -2
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Merge the following into your client's MCP configuration, or see [mcp-config.jso
|
|
|
70
70
|
|
|
71
71
|
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.
|
|
72
72
|
|
|
73
|
-
`@latest` selects the
|
|
73
|
+
`@latest` selects the package at launch; upgrading an existing pair is coordinated at startup. The first extension load, and pairs still on an older protocol, need one manual extension reload — see [local data and upgrades](#local-data-and-upgrades).
|
|
74
74
|
|
|
75
75
|
Assembly steps for supported hosts are in [agent integration](docs/AGENT-INTEGRATION.md); other clients use the stdio entry above.
|
|
76
76
|
|
|
@@ -81,7 +81,7 @@ Assembly steps for supported hosts are in [agent integration](docs/AGENT-INTEGRA
|
|
|
81
81
|
3. `browser_snapshot` to get element `ref`s, then click, fill or run other actions.
|
|
82
82
|
4. Observe again after each action, and finish with `browser_tab_control(action="release")`.
|
|
83
83
|
|
|
84
|
-
When titles change
|
|
84
|
+
When titles change often, omit `expected_title` and check the identity returned by claim instead. Elements may be addressed by `ref` or by a `selector` re-resolved on every call; design trade-offs: [architecture notes](docs/ARCHITECTURE.md#设计取舍).
|
|
85
85
|
|
|
86
86
|
Claiming and ordinary input never activate a tab or window; call `browser_tab_control(action="activate")` explicitly when foreground rendering is needed. When an outcome is unknown, observe again first and avoid duplicate submissions. Full parameters, pagination, uploads and input semantics: [usage guide](docs/USAGE.md).
|
|
87
87
|
|
|
@@ -115,13 +115,9 @@ Override it explicitly with the absolute path `BROWSER_ATTACH_HOME`. The Windows
|
|
|
115
115
|
|
|
116
116
|
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).
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
npm install --global @evan_zhong/browser-attach
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
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.
|
|
118
|
+
To upgrade, install again with the command from step 1 and reconnect the MCP client. With `@latest` and a pair on 0.17 or newer the restart is enough; a pair still on an older protocol needs `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, and an older MCP package cannot downgrade a newer Hub or installed extension.
|
|
123
119
|
|
|
124
|
-
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
|
|
120
|
+
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, and Hub and extension versions must match for page control. The default port is `47831`; choose another port with `setup --port` when creating a pairing.
|
|
125
121
|
|
|
126
122
|
## Architecture and boundaries
|
|
127
123
|
|
|
@@ -133,13 +129,13 @@ MCP client → Node.js stdio MCP → 127.0.0.1 Hub
|
|
|
133
129
|
existing HTTP(S) / authorized file:// tabs
|
|
134
130
|
```
|
|
135
131
|
|
|
136
|
-
Each MCP connection holds its own control and handles cannot be used across connections. Disconnecting or stopping debugging releases control. The client endpoint requires a Bearer key and rejects web page Origins; the extension verifies a fixed Origin, key and version. `browser_evaluate` can read and write data with the page's existing sign-in state;
|
|
132
|
+
Each MCP connection holds its own control and handles cannot be used across connections. Disconnecting or stopping debugging releases control. The client endpoint requires a Bearer key and rejects web page Origins; the extension verifies a fixed Origin, key and version. `browser_evaluate` can read and write data with the page's existing sign-in state; a timeout or error may already have had an effect and must not be replayed automatically. No generic CDP tool is exposed.
|
|
137
133
|
|
|
138
|
-
Logs are collected from claim time, survive navigation and are cleared on release
|
|
134
|
+
Logs are collected from claim time, survive navigation and are cleared on release, with bounded buffers that report `data_loss` on eviction. Network logs store lifecycle metadata only by default; an explicit capture reads bounded, redacted headers/bodies of subsequent XHR/fetch requests without changing caches or intercepting traffic. Buffer limits and parameters: [usage guide](docs/USAGE.md#诊断日志).
|
|
139
135
|
|
|
140
136
|
The extension holds debugger permission and client actions can use existing site sign-in state; only connect clients you trust. Web page content must not be treated as user authorization. The pairing key and the generated `config.generated.js` must not be committed or published; the packaging check rejects packages that contain that file.
|
|
141
137
|
|
|
142
|
-
Chrome / Edge HTTP(S) pages and existing ordinary `file://` tabs with extension file access enabled are supported
|
|
138
|
+
Chrome / Edge HTTP(S) pages and existing ordinary `file://` tabs with extension file access enabled are supported, and file access is checked during discovery, claim, operations and navigation. `open_tab` and `fetch` still accept HTTP(S) only. Browser internal pages, arbitrary extension pages, download management, network interception and Firefox / WebKit remain outside scope, and built-in PDF viewer content is browser-dependent. Native pointer input requires an unsuspended tab in a non-minimized window; an unmodified left single click may target an inactive tab, while hover, drag and other click variants require an active tab. Full limits and failure modes: [usage guide](docs/USAGE.md#限制与排查).
|
|
143
139
|
|
|
144
140
|
## Local development
|
|
145
141
|
|
package/README.zh.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
## 能做什么
|
|
16
16
|
|
|
17
17
|
- 发现多个浏览器配置,精确选择并独占接管标签页,完成后释放。
|
|
18
|
+
- 在浏览器扩展详情中为 BrowserAttach 开启 **允许访问文件网址** 后,可操作已打开的普通 `file://` 标签页;发现、接管、观察与输入工具完全相同。
|
|
18
19
|
- 读取可访问性树,按名称、value 或邻近字段 context 查询并取得可操作引用,检查字段值和编辑器模型;通过 ref 或严格 CSS/XPath、HTML label、placeholder、AX role/name 定位点击和填写,也可显式向当前聚焦字段逐字输入。
|
|
19
20
|
- 原生指针悬停、分步拖拽、条件等待;无需打开系统弹层即可选择 HTML 下拉选项。
|
|
20
21
|
- 导航与历史前进/后退、打开或关闭标签页,处理 JavaScript 对话框,上传本机文件并截取整页或单个元素。
|
|
@@ -67,7 +68,7 @@ npx --yes --package=@evan_zhong/browser-attach@latest browser-attach setup
|
|
|
67
68
|
|
|
68
69
|
Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局安装时,也可以配置 `"command": "browser-attach", "args": ["mcp"]`。重启 MCP 客户端后发现工具。
|
|
69
70
|
|
|
70
|
-
`@latest`
|
|
71
|
+
`@latest` 在启动时选择包版本;升级已有配对会在启动时协调。首次加载扩展,以及仍停留在更旧协议的配对,需要手动重载一次扩展,详见[本机数据与升级](#本机数据与升级)。
|
|
71
72
|
|
|
72
73
|
各宿主的装配步骤见 [Agent 集成](docs/AGENT-INTEGRATION.md);其他客户端直接使用上面的 stdio 配置。
|
|
73
74
|
|
|
@@ -78,7 +79,7 @@ Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局
|
|
|
78
79
|
3. `browser_snapshot` 取得元素 `ref`,再执行点击、填写或其他动作。
|
|
79
80
|
4. 动作后重新观察,结束时调用 `browser_tab_control(action="release")`。
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
标题易变时省略 `expected_title`,改为核对 claim 返回的当前身份。元素可用 `ref` 或每次重新解析的 `selector` 定位;设计取舍见 [架构说明](docs/ARCHITECTURE.md#设计取舍)。
|
|
82
83
|
|
|
83
84
|
接管和普通输入不会主动激活标签或窗口;需要前台渲染时显式调用 `browser_tab_control(action="activate")`。操作结果未知时先重新观察,避免重复提交。完整参数、分页、上传与输入语义见 [使用指南](docs/USAGE.md)。
|
|
84
85
|
|
|
@@ -94,7 +95,7 @@ Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局
|
|
|
94
95
|
| `browser_wait` | 只读等待元素、文本、URL 或网络空闲 |
|
|
95
96
|
| `browser_click` / `browser_hover` / `browser_drag` / `browser_select_option` / `browser_type` / `browser_fill` / `browser_press_key` / `browser_scroll` | 原生/DOM 输入、指针悬停与拖拽、下拉选择、逐字输入与滚动 |
|
|
96
97
|
| `browser_batch` | 单队列顺序执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,失败即停并返回部分结果 |
|
|
97
|
-
| `browser_navigate` | HTTP(S)
|
|
98
|
+
| `browser_navigate` | HTTP(S)/file 导航、后退前进与加载等待 |
|
|
98
99
|
| `browser_handle_dialog` | alert、confirm、prompt |
|
|
99
100
|
| `browser_upload_file` | 原生文件 input 或拦截的文件选择器 |
|
|
100
101
|
| `browser_screenshot` | 视口或整页 PNG,附坐标映射 |
|
|
@@ -112,13 +113,9 @@ Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局
|
|
|
112
113
|
|
|
113
114
|
宿主装配步骤写入客户端条目时,会固定当前 Node、CLI 和实际配对文件的绝对路径,并设置 MCP 的 `BROWSER_ATTACH_HOME`;支持与自定义 `--config` 一起使用。MCP 启动 Hub 时继续传入同一绝对配对路径及目录,避免不同启动环境重新推导出另一份配置。详见 [Agent 集成](docs/AGENT-INTEGRATION.md)。
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
npm install --global @evan_zhong/browser-attach
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
安装新版后重连 MCP。使用 `@latest` 时,在 Hub 与扩展已经运行 0.17 或更高版本后,重连 MCP 即可协调后续升级;显式运行 `setup` 或 `start` 也使用同一流程。从更旧版本升级时,需要先执行 `browser-attach stop`、重启 MCP,并手动重载一次扩展。自定义配对路径、端口和扩展目录会保留。未打开的浏览器 profile 在下次连接时完成更新。旧 MCP 包不会降级新版 Hub 或已安装扩展。
|
|
116
|
+
升级时用第 1 步的同一命令重新安装,然后重连 MCP。使用 `@latest` 且配对已运行 0.17 或更高版本时,重连即可;仍停留在更旧协议时需先执行 `browser-attach stop`、重启 MCP,并手动重载一次扩展。自定义配对路径、端口和扩展目录会保留,未打开的浏览器 profile 在下次连接时更新,旧 MCP 包不会降级新版 Hub 或已安装扩展。
|
|
120
117
|
|
|
121
|
-
|
|
118
|
+
升级会等待扩展清理,并在重载后确认新版本;清理或重载失败会明确报告,不重放页面动作。扩展设置页保留手动重新加载按钮用于恢复,正常页面控制仍要求 Hub 与扩展版本一致。默认端口为 `47831`,创建配对时可通过 `setup --port` 选择其他端口。
|
|
122
119
|
|
|
123
120
|
## 架构与使用边界
|
|
124
121
|
|
|
@@ -130,13 +127,13 @@ MCP 客户端 → Node.js stdio MCP → 127.0.0.1 Hub
|
|
|
130
127
|
现有 HTTP(S) 标签页
|
|
131
128
|
```
|
|
132
129
|
|
|
133
|
-
每个 MCP 连接拥有独立控制权,句柄不能跨连接使用。断连或停止调试会释放控制。客户端端点要求 Bearer 密钥并拒绝网页 Origin;扩展验证固定 Origin、密钥和版本。`browser_evaluate`
|
|
130
|
+
每个 MCP 连接拥有独立控制权,句柄不能跨连接使用。断连或停止调试会释放控制。客户端端点要求 Bearer 密钥并拒绝网页 Origin;扩展验证固定 Origin、密钥和版本。`browser_evaluate` 可以使用页面现有登录态读写数据;超时或报错前可能已产生作用,不应自动重放。未暴露通用 CDP 工具。
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
日志从接管开始采集,跨导航保留,释放时清除;缓冲区有上限,淘汰会报告 `data_loss`。网络日志默认只记录生命周期元数据;显式 capture 才读取之后的 XHR/fetch 有界、脱敏 headers/正文,不改变缓存或拦截流量。缓冲上限与参数见[使用指南](docs/USAGE.md#诊断日志)。
|
|
136
133
|
|
|
137
134
|
浏览器扩展具有调试权限,客户端操作可使用现有网站登录态;只应将其接入你信任的 MCP 客户端。网页内容不应被视为用户授权。配对密钥和生成的 `config.generated.js` 不应提交或发布,打包检查会拒绝包含该文件的包。
|
|
138
135
|
|
|
139
|
-
|
|
136
|
+
支持 Chrome / Edge 的 HTTP(S) 页面,以及开启扩展文件访问权限后已打开的普通 `file://` 标签页;文件权限在发现、接管、操作和导航时都会检查。`open_tab` 与 `fetch` 仍只接受 HTTP(S)。浏览器内部页、任意扩展页、下载管理、网络拦截和 Firefox / WebKit 不在范围内,内置 PDF 查看器内容取决于浏览器。原生指针输入要求未暂停的标签及未最小化窗口;无修饰键的左键单击可在非活动标签尝试,hover、drag 及其他点击变体仍要求活动标签。完整边界与排查见[使用指南](docs/USAGE.md#限制与排查)。
|
|
140
137
|
|
|
141
138
|
## 本地开发
|
|
142
139
|
|
|
@@ -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 即可,启动流程会协调升级;残留失败与手动重载见 [README:本机数据与升级](../README.md#本机数据与升级)。
|
|
114
114
|
|
|
115
115
|
## 新增宿主
|
|
116
116
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 架构与设计边界
|
|
2
2
|
|
|
3
|
-
当前实现为 **0.23.
|
|
3
|
+
当前实现为 **0.23.5**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
|
|
4
4
|
|
|
5
5
|
## 组件与数据流
|
|
6
6
|
|
|
@@ -114,7 +114,8 @@ HTTP 状态复用 network.js 的 Network.responseReceived 观察,按主文档
|
|
|
114
114
|
- **可操作性诊断**:区分 display_none、visibility_hidden、zero_size、disabled、inert、read_only 和 unstable_geometry;无可见落点单独返回 no_visible_click_point。失败时最多采集 8 层 composed 祖先的样式、滚动方向和 transition 属性/时长,成功路径保留原有几何就绪语义;不以 opacity=0 拒绝输入,不自动 hover、移除遮挡或等待推断出的组件状态。
|
|
115
115
|
- **原生下拉**:select_option 不要求可见或稳定几何,先只读核对节点、禁用/inert 祖先与唯一选项,再在一次派发中复查并用 selectedIndex setter 和合成 input/change 设置,核验保留结果并返回 events_trusted=false。单步和 batch 共用实现。native click 的 select 命中(含 UA shadow 内部节点和关联 label)在派发前拒绝。
|
|
116
116
|
- **命中提示**:普通命中保留实际 hit node,沿父元素、Shadow host、slot 查找最近的 HTML/ARIA 可点击候选,返回 clickable_target;hint=false 表示未识别候选,不代表无法响应事件。CSSPseudoElement 先解析为事件所属的 originating element,提示标记 pseudo_element=true,并使用该元素安装事件校验。语义候选不会被用于替换点击目标。
|
|
117
|
-
- **遮挡失败诊断**:当前 frame 的遮挡直接复用已读取的 hit_target;祖先 frame 遮挡额外用至多 200 ms 读取命中描述,诊断节点消失时保留原遮挡错误与 backend node ID
|
|
117
|
+
- **遮挡失败诊断**:当前 frame 的遮挡直接复用已读取的 hit_target;祖先 frame 遮挡额外用至多 200 ms 读取命中描述,诊断节点消失时保留原遮挡错误与 backend node ID。命中元素与最近可点击祖先均附 tag/id/classes,classes 至多 8 项、每项 100 字符,只作当次身份线索,不生成唯一 selector 或替换目标。错误包含命中点和受阻 frame。根据共享预算的派发状态区分“未派发”和“结果未知”,避免 drag 按住后准备目的节点时误报零输入。
|
|
118
|
+
- **上传证据**:非空文件通过 DOM.setFileInputFiles 设置;清空由 DOM 赋值并派发合成 input/change。完成后读回原 input 的 FileList 数量,数量不符或节点脱离时报告结果未知,读取失败也保留已派发事实,不补发设置或事件。files_set 仅表示当时保留的数量;非空路径的 events_trusted=null,不把 CDP 回执作为事件送达核验。清空路径 events_trusted=false,两者 application_result=not_checked,不推断组件登记或服务器接收。
|
|
118
119
|
- **送达不等于业务完成**:原生 click/hover 的 delivery_verified 表示可信事件送达;target_verified=true 只用于显式元素目标,纯坐标为 null,target_verification 标明 element/coordinate_hit 和准备时的 same_node/composed_descendant。hit_relation 按 composed 祖先遍历处理 slot/ShadowRoot,派发后仍由隔离世界事件路径核验。value_verified 表示字段文本写入;点击、按键、fill 和 batch 返回 `application_result: "not_checked"`。fill 不证明 React 状态、选项提交、blur 校验或保存完成。
|
|
119
120
|
- **有限多步输入**:batch 在现有标签队列内执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,共享至多 30 秒预算,每步可另设更短超时;不增加后台任务、循环或脚本引擎;元素目标使用每步重新解析的 selector,type 也接受 focused=true。所有参数先验证,selector 每步重解析,尚未出现的目标只在准备阶段等待;歧义立即报错。首个失败、主文档变化或接管结束后停止,返回此前结果与失败索引;不回滚、不重放、不自动激活或切换输入模式。start_at 显式跳过已完成前缀,返回 failed_step、next_unattempted_step、retry_failed_step_safe;没有后台作业或自动选择续跑点。
|
|
120
121
|
- **OOPIF 坐标**:点击从顶层派发。逐层用 `DOM.getBoxModel` 把坐标换算到祖先文档,并用 `DOM.getNodeForLocation` 确认该位置确实落在 frame owner 上,防止祖先页面的遮挡被跳过。
|
package/docs/TEST-REPORT.md
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
# 验证报告
|
|
2
2
|
|
|
3
|
-
当前版本 **0.23.
|
|
3
|
+
当前版本 **0.23.5**(2026-10-25)。浏览器验证使用本地 fixture 和临时 profile;测试链路在各项下说明。
|
|
4
4
|
|
|
5
5
|
## 当前版本
|
|
6
6
|
|
|
7
|
+
- read_text 的工具描述与使用指南明确:默认正文不包含 input/textarea 当前值;单字段使用 mode=value,整表使用 snapshot(profile=fields)。complete_source 表示来源覆盖,分页仍以 next_offset 为准。
|
|
8
|
+
- 上传完成后读回原 input 的 FileList 数量,files_set 不再直接回显请求数量;数量不符或节点脱离时报告 file_count_not_retained、已派发且结果未知。派发或读回异常均保留未知结果,不自动补发。非空路径返回 method=native、events_trusted=null;清空返回 method=dom、events_trusted=false;两者 application_result=not_checked,不声称事件送达、组件登记或上传完成。
|
|
9
|
+
- 命中元素及最近可点击祖先补充 id/classes;classes 至多 8 项,每项 100 字符,作为当前身份线索,沿用原有遮挡检查与派发边界。
|
|
10
|
+
- 文档整理:升级协调、日志上限与边界描述原先在 README、Agent 集成与架构文档中重复,现各自保留一处并按读者分层(用户步骤 / 宿主装配 / 协议实现);中文 README 与英文对齐 file:// 范围与后台单击规则;使用指南中的字段、frame、日志与上传示例改用中性占位值,不再引用测试 fixture 的字段名与业务数据。
|
|
11
|
+
- `npm test`:284 项通过。新增上传后页面重置/节点脱离与读回异常不重放的回归,补充清空结果、未检查的应用状态和有界 class 诊断断言。
|
|
12
|
+
- Chromium 156、Edge 155 headless 完整 stdio MCP `live-smoke`:各 388 次调用、36 项检查通过。新增 FileList 读回及结果语义核验,真实 change 处理器立即清空文件时返回未知结果且仅触发 1 次 change;跨 frame 遮挡保留 class/祖先身份且未误点。保留隐藏 input、选择器、清空、多文件、OOPIF 上传及其他既有回归。
|
|
13
|
+
- `npm run test:package`:包外安装与 stdio MCP 通过,23 个工具、62 个发布文件,无配对密钥。未增加工具、参数、依赖或权限,版本递增至 0.23.5。
|
|
14
|
+
- 本机部署:通过 stop/start 重启,Hub 为 0.23.5 / ready,Chrome、Edge 扩展均为 0.23.5 / ready;稳定扩展副本的 43 个文件已核对,manifest 版本/身份一致,其余非生成文件 SHA-256 一致。现有客户端需重连并重新发现、接管。
|
|
15
|
+
- 剩余边界:FileList 核验仅为当时数量,不核验内容或服务器持久化;页面已经消费文件也可能导致未知结果。class 线索不保证唯一或跨重渲染稳定。本轮未操作真实业务表单。
|
|
16
|
+
|
|
17
|
+
## 0.23.4 验证
|
|
18
|
+
|
|
7
19
|
- 补充 npm 包的 `mcpName` 和根目录 `server.json`,包含 Registry 名称、展示名称、描述、源码/文档链接及 npm stdio 启动信息;元数据通过官方 2025-12-11 JSON Schema 校验。
|
|
8
20
|
- `npm test`:282 项通过。仅调整发布元数据、版本与打包检查,未修改页面操作实现,未重复运行真实浏览器 fixture 测试。
|
|
9
21
|
- `npm run test:package`:包外安装与 stdio MCP 通过,23 个工具、62 个发布文件,无配对密钥。`server.json` 已纳入发布包;打包检查覆盖 Registry 名称、npm 包名及版本一致性。
|
|
10
22
|
- 本机部署:协调升级因现有 MCP 连接占用拒绝后,通过 stop/start 重启;Hub 为 0.23.4 / ready,Chrome、Edge 扩展均为 0.23.4。稳定扩展副本的 43 个文件已核对:manifest 版本/身份一致,其余非生成文件 SHA-256 一致。现有客户端需重连并重新发现、接管。
|
|
11
|
-
-
|
|
23
|
+
- 已发布 npm `@evan_zhong/browser-attach@0.23.4`(registry latest,含 `mcpName`),并以 GitHub 账号登录 `mcp-publisher` 发布 Registry 元数据:`io.github.EvannZhongg/browser-attach` 0.23.4 状态 active、`isLatest=true`,可通过 `GET /v0.1/servers?search=io.github.EvannZhongg/browser-attach` 检索。
|
|
12
24
|
|
|
13
25
|
## 0.23.3 验证
|
|
14
26
|
|
package/docs/USAGE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BrowserAttach 使用指南
|
|
2
2
|
|
|
3
|
-
适用于 **0.23.
|
|
3
|
+
适用于 **0.23.5**:Node.js 22+、Chrome / Edge 125+、23 个 MCP 工具。安装、配对与升级见 [README](../README.md),宿主配置见 [Agent 集成](AGENT-INTEGRATION.md),内部机制见 [架构说明](ARCHITECTURE.md)。
|
|
4
4
|
|
|
5
5
|
本文供按需查阅,不会自动通过 MCP 发送给 agent。MCP 初始化 `instructions` 提供通用规则,工具 description 与参数 schema 提供调用说明;宿主应把 instructions 纳入模型上下文。
|
|
6
6
|
|
|
@@ -173,8 +173,8 @@ ref 错误的 diagnostics.reason=stale_ref,cause 区分 snapshot_replaced、do
|
|
|
173
173
|
|
|
174
174
|
```json
|
|
175
175
|
{"tab_handle":"...","profile":"fields"}
|
|
176
|
-
{"tab_handle":"...","profile":"fields","query":"
|
|
177
|
-
{"tab_handle":"...","profile":"fields","include_hidden":true,"query":"
|
|
176
|
+
{"tab_handle":"...","profile":"fields","query":"邮箱"}
|
|
177
|
+
{"tab_handle":"...","profile":"fields","include_hidden":true,"query":"orderId"}
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
fields 视图用 `fields` 数组替代 snapshot 文本,保留重复标签和字段名,避免以标签作为键覆盖数据。`node_count/total_node_count` 与 max_nodes 计数字段,max_chars 仍按完整序列化项计算。`frames` 列出本次观察的 frame_id/URL,读取失败及变化见 omitted_frames。每行示例:
|
|
@@ -182,9 +182,9 @@ fields 视图用 `fields` 数组替代 snapshot 文本,保留重复标签和
|
|
|
182
182
|
```json
|
|
183
183
|
{
|
|
184
184
|
"ref":"token:1", "frame_id":"frame-id",
|
|
185
|
-
"labels":["
|
|
186
|
-
"ax":{"role":"textbox","name":"
|
|
187
|
-
"dom":{"tag":"input","id":"
|
|
185
|
+
"labels":["订单号"], "context":null,
|
|
186
|
+
"ax":{"role":"textbox","name":"订单号","value":"SO-0001","readonly":true},
|
|
187
|
+
"dom":{"tag":"input","id":"order-no","name":"orderNo","type":"text","value":"SO-0001","value_source":"input_value"}
|
|
188
188
|
}
|
|
189
189
|
```
|
|
190
190
|
|
|
@@ -197,7 +197,7 @@ fields 视图用 `fields` 数组替代 snapshot 文本,保留重复标签和
|
|
|
197
197
|
|
|
198
198
|
### browser_read_text
|
|
199
199
|
|
|
200
|
-
`browser_read_text({tab_handle})` 默认读取主 frame
|
|
200
|
+
`browser_read_text({tab_handle})` 默认读取主 frame 的可见正文,不包含 input/textarea 的当前值,不能据此判断表单为空。单字段用 `browser_read_text({tab_handle, selector:"#name", mode:"value"})`;整张表单用 `browser_snapshot({tab_handle, profile:"fields"})`,可加 query 筛选字段。可用 selector 限定正文范围,或用 frame/frame_id 选择子 frame。
|
|
201
201
|
|
|
202
202
|
| mode | 来源 |
|
|
203
203
|
|---|---|
|
|
@@ -231,12 +231,12 @@ browser_click({tab_handle, selector_type:"role", selector:"option", name:"2026"}
|
|
|
231
231
|
CSS/XPath/label/placeholder 不穿透 Shadow DOM;可用 AX role 或 ref。role 每次读取所选 frame 的完整 AX 树,不保证比简单 CSS 查询便宜。selector、read_text、evaluate 可用 frame 或 frame_id 指定范围,两者互斥:
|
|
232
232
|
|
|
233
233
|
```json
|
|
234
|
-
{"tab_handle":"...","selector":"
|
|
234
|
+
{"tab_handle":"...","selector":"#comment","frame":{"path":["iframe[title='Outer']","iframe[title='Inner']"]},"text":"填写内容"}
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
frame.url/title 精确匹配子 frame,path 是逐层且每层唯一的 CSS iframe 路径,可再用 url/title 校验最终 frame。路径每次重解析,歧义报错;ref 已携带 frame/session,不能另传 frame。
|
|
238
238
|
|
|
239
|
-
例如 iframe 的 id 为 `
|
|
239
|
+
例如 iframe 的 id 为 `payment_frame` 时,path 应写为 `["#payment_frame"]`。定位失败的 diagnostics 包含失败步骤和至多 10 个当前 frame 的 id、父 id、URL 及省略数量;快照也会列出 frame id/URL。frame_id 每次按当前 frame tree 校验,不是 ref;iframe 重建或导航后的 ID 应重新观察,不猜测替代目标。
|
|
240
240
|
|
|
241
241
|
## 输入:可操作性与两种模式
|
|
242
242
|
|
|
@@ -264,7 +264,7 @@ native 与 dom 由调用者显式选择。DOM click/fill 使用非可信事件
|
|
|
264
264
|
|
|
265
265
|
原生模式核验对应的 click/dblclick/auxclick/contextmenu,成功返回 delivery_verified=true、events_trusted=true。显式元素目标返回 target_verified=true;坐标调用返回 target_verified=null,仅证明事件送达该坐标命中的节点。目标被遮挡或派发中变化时失败;遮挡错误的 diagnostics 附带 hit_target、命中点及受阻 frame,前序输入可能已经送达。
|
|
266
266
|
|
|
267
|
-
hit_target 描述实际命中的 tag/id/text/role/disabled,以及所属文档视口 CSS 坐标的 rect、position、z_index、visibility;z_index 不代表全局遮挡顺序。clickable_hint
|
|
267
|
+
hit_target 描述实际命中的 tag/id/classes/text/role/disabled,以及所属文档视口 CSS 坐标的 rect、position、z_index、visibility;z_index 不代表全局遮挡顺序。classes 最多 8 项,每项最多 100 字符;最近的 clickable_target 也包含 tag/id/classes。这些是当次 DOM 身份线索,不保证唯一或稳定。clickable_hint 和 clickable_target 仅是启发式提示,不改变实际目标;false 不表示无法响应点击。伪元素命中标记 pseudo_element=true。返回 clicked 始终为顶层视口 CSS 坐标,跨进程 iframe 仍检查祖先遮挡;透视变换 frame 不支持。
|
|
268
268
|
|
|
269
269
|
DOM 模式调用一次 HTMLElement.click(),返回 method=dom、events_trusted=false。HTML select 的原生点击会在派发前拒绝,改用 select_option;系统弹层已打开时先按 Esc。
|
|
270
270
|
|
|
@@ -451,7 +451,8 @@ browser_screenshot 默认截图视口,full_page=true 截整页,或用 ref/se
|
|
|
451
451
|
|
|
452
452
|
- files 是浏览器本机的绝对可读文件路径,最多 100 个;支持中文、空格及空文件,内容不传给 MCP 或 Hub。
|
|
453
453
|
- 多文件要求 multiple;禁用控件、目录、webkitdirectory 和失效目标会被拒绝。accept 只是页面提示,网站可自行拒绝格式。
|
|
454
|
-
-
|
|
454
|
+
- 成功后读回原 input 的 FileList 数量,files_set 是当时观察到且与请求数量一致的值,cleared 表示该数量为 0;不核验文件内容或组件是否登记。数量不符或原 input 脱离文档时返回 file_count_not_retained 和未知结果;页面可能已经消费或重置选择,不代表上传失败,不自动重放。
|
|
455
|
+
- 非空文件通过 CDP 设置,返回 method=native、events_trusted=null:没有监听核验事件送达或信任状态。files=[] 清空指定 input 或结束待处理选择,显式派发合成 input/change,返回 method=dom、events_trusted=false。两者均返回 application_result=not_checked、observe_again=true,不证明组件接受、解析或服务端上传完成。
|
|
455
456
|
- 跨进程 iframe 的 file input 已支持;File System Access API 的 showOpenFilePicker() 不在范围内。
|
|
456
457
|
|
|
457
458
|
若网站解析文件后重载表单,先等待具体字段或就绪标志,重新观察,再用新的 selector/batch 补齐确认缺失的字段。upload_file 不等待应用解析完成,不自动重放上传或恢复字段;batch 在主文档变化后停止。
|
package/extension/manifest.json
CHANGED
package/extension/pointer.js
CHANGED
|
@@ -169,12 +169,14 @@ export function describeHit() {
|
|
|
169
169
|
if (parent.matches(':disabled,[inert],[aria-disabled="true"]')) disabled = true;
|
|
170
170
|
}
|
|
171
171
|
const rect = element.getBoundingClientRect(), style = element.ownerDocument.defaultView.getComputedStyle(element);
|
|
172
|
-
|
|
172
|
+
const identity = node => ({ tag: node.tagName.toLowerCase(), id: node.id || null,
|
|
173
|
+
classes: Array.from(node.classList).slice(0, 8).map(name => name.slice(0, 100)) });
|
|
174
|
+
return { ...identity(element), text: (element.innerText || element.textContent || '').slice(0, 300),
|
|
173
175
|
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, coordinate_space: 'frame_viewport_css_pixels',
|
|
174
176
|
position: style.position, z_index: style.zIndex, visibility: style.visibility,
|
|
175
177
|
role: element.getAttribute('role'), disabled, clickable_hint: !!clickable,
|
|
176
178
|
native_select: nativeSelect,
|
|
177
|
-
clickable_target: clickable ? {
|
|
179
|
+
clickable_target: clickable ? { ...identity(clickable), role: clickable.getAttribute('role'),
|
|
178
180
|
text: (clickable.innerText || clickable.textContent || '').slice(0, 300) } : null };
|
|
179
181
|
}
|
|
180
182
|
|
package/extension/upload.js
CHANGED
|
@@ -28,23 +28,31 @@ export function createUploads(cdp, value) {
|
|
|
28
28
|
if (resolved.exceptionDetails) value(resolved);
|
|
29
29
|
if (!resolved.result?.objectId) throw new Error('Could not resolve the file input');
|
|
30
30
|
args.budget.dispatch();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
try {
|
|
32
|
+
if (args.files.length === 0) {
|
|
33
|
+
// Chromium's native empty-path assignment does not emit input/change.
|
|
34
|
+
// Like Playwright's empty payload path, explicitly clear and notify the app.
|
|
35
|
+
value(await send(target.target, 'Runtime.callFunctionOn', { objectId: resolved.result.objectId, returnByValue: true,
|
|
36
|
+
functionDeclaration: `function() {
|
|
37
|
+
this.value = '';
|
|
38
|
+
const Event = this.ownerDocument.defaultView.Event;
|
|
39
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
40
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
41
|
+
return true;
|
|
42
|
+
}` }));
|
|
43
|
+
} else {
|
|
44
|
+
// Native CDP assignment emits input/change and preserves actual file metadata.
|
|
45
|
+
await send(target.target, 'DOM.setFileInputFiles', { objectId: resolved.result.objectId, files: args.files });
|
|
46
|
+
}
|
|
47
|
+
fileChoosers.delete(tabId);
|
|
48
|
+
const count = value(await send(target.target, 'Runtime.callFunctionOn', { objectId: resolved.result.objectId, returnByValue: true,
|
|
49
|
+
functionDeclaration: `function() { return this.isConnected ? this.files?.length : null; }` }));
|
|
50
|
+
if (count !== args.files.length) throw Object.assign(new Error('File input did not retain the requested file count. The page may have consumed or reset the selection; application outcome is unknown. Observe before continuing; do not replay automatically.'), {
|
|
51
|
+
outcome_unknown: true, diagnostics: { reason: 'file_count_not_retained', expected_count: args.files.length, observed_count: Number.isInteger(count) ? count : null },
|
|
52
|
+
});
|
|
53
|
+
return { files_set: count, cleared: count === 0, method: args.files.length ? 'native' : 'dom',
|
|
54
|
+
events_trusted: args.files.length ? null : false, application_result: 'not_checked', observe_again: true };
|
|
55
|
+
} catch (error) { error.outcome_unknown = true; throw error; }
|
|
48
56
|
}
|
|
49
57
|
return { upload,
|
|
50
58
|
opened(tabId, params, sessionId) {
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.EvannZhongg/browser-attach",
|
|
4
4
|
"title": "BrowserAttach",
|
|
5
5
|
"description": "Control existing signed-in Chrome and Edge tabs through a locally paired extension.",
|
|
6
|
-
"version": "0.23.
|
|
6
|
+
"version": "0.23.5",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/EvannZhongg/BrowserAttach",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "@evan_zhong/browser-attach",
|
|
16
|
-
"version": "0.23.
|
|
16
|
+
"version": "0.23.5",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|
package/src/config.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export const DATA_DIR = configuredHome
|
|
|
15
15
|
: process.platform === 'win32' ? join(homedir(), '.browser-attach')
|
|
16
16
|
: join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'browser-attach');
|
|
17
17
|
export const DEFAULT_CONFIG = join(DATA_DIR, 'config.json');
|
|
18
|
-
export const VERSION = '0.23.
|
|
18
|
+
export const VERSION = '0.23.5';
|
|
19
19
|
|
|
20
20
|
export async function loadConfig(path = DEFAULT_CONFIG) {
|
|
21
21
|
let config;
|
package/src/mcp.mjs
CHANGED
|
@@ -113,7 +113,7 @@ export function createMcp(client) {
|
|
|
113
113
|
query: z.string().min(1).max(2000).optional().describe('Case-insensitive literal substring of names, values or nearby context. fields also searches HTML labels, DOM id/name/placeholder and selected options. Zero matches is not an error.'),
|
|
114
114
|
verbose: z.boolean().default(false).describe('Include viewport and snapshot_id.'),
|
|
115
115
|
});
|
|
116
|
-
tool('read_text', 'Read rendered page/frame text, optionally scoped to a selector. mode=value
|
|
116
|
+
tool('read_text', 'Read rendered page/frame text, optionally scoped to a selector. Default visible text excludes input/textarea values: use mode=value with a selector for one field, or browser_snapshot profile=fields for structured form values. mode=editor reads a Monaco/CodeMirror 5 model. complete_source describes source coverage, not pagination; next_offset continues live text, which may change.', {
|
|
117
117
|
...handle, ...selector, ...frameScope, nth: target.nth, mode: z.enum(['visible', 'text_content', 'value', 'editor']).default('visible'),
|
|
118
118
|
offset: integerRange('offset', 0, Number.MAX_SAFE_INTEGER).default(0), max_chars: integerRange('max_chars', 1, 100000).default(20000),
|
|
119
119
|
});
|
|
@@ -196,7 +196,7 @@ export function createMcp(client) {
|
|
|
196
196
|
tool('handle_dialog', 'Accept/dismiss a JS dialog and return its original details and accepted.', {
|
|
197
197
|
...handle, accept: z.boolean(), prompt_text: z.string().optional().describe('Response sent to the website for a prompt dialog.'),
|
|
198
198
|
});
|
|
199
|
-
tool('upload_file', 'Set browser-host absolute file paths on a file-input/label ref, or omit ref for the pending intercepted chooser. [] clears/cancels; multiple files require a multiple input. Directories unsupported.', {
|
|
199
|
+
tool('upload_file', 'Set browser-host absolute file paths on a file-input/label ref, or omit ref for the pending intercepted chooser. [] clears/cancels; multiple files require a multiple input. Reads back the current FileList count only, not event delivery, application acceptance or upload completion. Directories unsupported.', {
|
|
200
200
|
...handle, ...inputTimeout, ...target, files: z.array(z.string()).max(100),
|
|
201
201
|
}, null, async args => ({ ...args, files: await validateFiles(args.files) }));
|
|
202
202
|
tool('screenshot', 'Capture viewport/full-page or one element as PNG with image-to-viewport CSS mapping. Full-page points may need scrolling before input. If background rendering stalls, activate the tab or use AX/text.', {
|