@evan_zhong/browser-attach 0.11.2 → 0.13.1

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
@@ -19,7 +19,7 @@ Drive the Chrome / Edge tabs you already have open and already signed in from an
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.
22
- - Standard stdio MCP with 22 tools. The Hub listens on loopback only and pairs with the extension through a random key.
22
+ - Standard stdio MCP with 23 tools. The Hub listens on loopback only and pairs with the extension through a random key.
23
23
 
24
24
  ## Requirements
25
25
 
package/README.zh.md CHANGED
@@ -19,7 +19,7 @@
19
19
  - 原生指针悬停、分步拖拽、条件等待;无需打开系统弹层即可选择 HTML 下拉选项。
20
20
  - 导航与历史前进/后退、打开或关闭标签页,处理 JavaScript 对话框,上传本机文件并截取整页或单个元素。
21
21
  - 在主页面执行 JavaScript 表达式,读取接管后的控制台、页面异常和网络生命周期日志;需要校验 XHR/fetch 时显式开启有界、脱敏的请求/响应详情。
22
- - 使用标准 stdio MCP,共提供 22 个工具。Hub 仅监听本机 loopback,通过随机密钥配对扩展。
22
+ - 使用标准 stdio MCP,共提供 23 个工具。Hub 仅监听本机 loopback,通过随机密钥配对扩展。
23
23
 
24
24
  ## 环境要求
25
25
 
@@ -1,125 +1,125 @@
1
- # Agent 集成
2
-
3
- BrowserAttach 是标准 stdio MCP,共 22 个工具;任何能启动子进程并发现 MCP 工具的 Agent 宿主都可以接入。本文按宿主给出装配步骤;配对与路径要求先读「通用装配」。
4
-
5
- | 宿主 | 装配方式 | 配置位置 |
6
- | --- | --- | --- |
7
- | Nosis | `browser-attach setup --nosis` 自动写入 | `~/.nosis/agent_config.json` |
8
- | DeepSeek Harness | 手动追加 patch 条目 | `$DSH_HOME/profiles/<name>/cordis.patch.yml` |
9
-
10
- ## 通用装配
11
-
12
- 先完成配对:
13
-
14
- ```sh
15
- npm install --global @evan_zhong/browser-attach
16
- browser-attach setup
17
- ```
18
-
19
- `setup` 打印稳定的扩展目录并启动本机 Hub。按 [README](../README.md) 第 1 步在 `chrome://extensions` 或 `edge://extensions` 加载该目录;每个要暴露的浏览器 profile 各加载一次。
20
-
21
- 宿主侧只需启动 stdio 进程:
22
-
23
- ```sh
24
- node <安装位置>/bin/browser-attach.mjs mcp --config <配对文件绝对路径>
25
- ```
26
-
27
- - `BROWSER_ATTACH_HOME` 指向配对文件所在目录。
28
- - Node 可执行文件、CLI 与配对文件一律使用绝对路径。宿主的启动环境(PATH、工作目录、AppData 重定向)不同,重新推导会得到第二份配对。
29
- - 安装位置变化或升级后重新执行 `setup` 并更新宿主条目。MCP 包与已加载扩展必须同版本,否则 Hub 直接拒绝连接。
30
-
31
- 工具前缀由宿主决定,原始工具名不变(`browser_discover`、`browser_claim_tab` 等)。装配后的最小验证在各宿主一致:`browser_discover(kind="browsers")` → `kind="tabs"` → `browser_claim_tab` → `browser_snapshot` → `browser_tab_control(action="release")`。
32
-
33
- ## Nosis
34
-
35
- `setup --nosis` 要求 `~/.nosis/agent_config.json` 已存在(Nosis 已初始化)。该命令在完成普通配对后备份并更新此文件(备份名 `agent_config.json.browser-attach-<时间戳>.bak`),写入:
36
-
37
- | 字段 | 值 |
38
- |---|---|
39
- | 服务器名 | `browser_attach` |
40
- | `command` | 执行 setup 的 Node 可执行文件绝对路径 |
41
- | `args` | 当前包 `bin/browser-attach.mjs` 绝对路径、`mcp`、`--config <绝对配对路径>` |
42
- | `env.BROWSER_ATTACH_HOME` | 配对文件所在目录 |
43
- | `startup_timeout_seconds` / `call_timeout_seconds` | `20` / `60` |
44
- | `tools.approval` | `always` |
45
-
46
- 同时设置 `mcp.enabled = true`。命令与配对路径都固定为绝对路径,因此本机安装位置变化后需要重新执行 `setup --nosis`;请从长期保留的位置安装,而不是临时解压目录。重启或重建 Nosis Runtime 后重新发现工具。
47
-
48
- 自定义运行目录:
49
-
50
- ```sh
51
- browser-attach setup --config /absolute/shared-directory/config.json --nosis
52
- ```
53
-
54
- `setup` 会把解析后的配对路径写进配置,MCP 与 Hub 后续都显式传递同一路径,不会在不同启动环境中重新推导出第二套密钥。`setup` 不会搜索或自动迁移其他应用的私有缓存;已有配对请显式保留原目录。
55
-
56
- Windows 安装脚本可直接带该开关:`./scripts/install.ps1 -ConfigureNosis`。
57
-
58
- 验证:
59
-
60
- ```sh
61
- python scripts/verify-nosis.py --nosis-root /path/to/Nosis --family chrome
62
- python scripts/verify-nosis.py --nosis-root /path/to/Nosis --family edge
63
- ```
64
-
65
- 脚本读取本机 Nosis 配置,通过 Nosis ToolCatalog 发现 22 个工具,覆盖发现、标签控制、页面操作和结果封装,其中 `browser_logs` 分别覆盖 console/network,验证结果封装、截图到模型消息的图片适配以及 `isError` 后连接仍可用。它用 `browser_open_tab` 在选定配置中创建本地 fixture;同一 family 有多个 profile 时用 `--browser-id <精确 browser_id>` 指定,结束后释放控制。Python 仅用于该测试,不是运行依赖。
66
-
67
- ## DeepSeek Harness
68
-
69
- DSH 通过 `@deepseek-ai/dsh-mcp-client` 条目接入 stdio MCP:一个服务器一个条目,工具暴露为 `mcp__<serverName>__<工具名>`。客户端插件随 harness 提供,不需要为 BrowserAttach 额外安装包。
70
-
71
- 把条目追加到 patch 层:
72
-
73
- - 单个 profile:`$DSH_HOME/profiles/<name>/cordis.patch.yml`;`dsh web` 对应 `web`。
74
- - 本机所有 profile:`$DSH_HOME/cordis.patch.yml`。
75
- - 临时试用:任意 patch 文件,用 `dsh web --patch ./browser-attach.cordis.yml` 叠加。
76
-
77
- `$DSH_HOME` 未设置时为 `~/.dsh`。patch 文件是 patch 列表,直接追加 `insert` 条目,不要覆盖其中已有的用户 patch。
78
-
79
- ```yaml
80
- - insert:
81
- - id: browser-attach
82
- name: '@deepseek-ai/dsh-mcp-client'
83
- config:
84
- serverName: browser_attach
85
- transport: stdio
86
- command: 'C:\Program Files\nodejs\node.exe'
87
- args:
88
- - 'C:\Users\<用户>\AppData\Roaming\npm\node_modules\@evan_zhong\browser-attach\bin\browser-attach.mjs'
89
- - mcp
90
- - --config
91
- - 'C:\Users\<用户>\.browser-attach\config.json'
92
- env:
93
- BROWSER_ATTACH_HOME: 'C:\Users\<用户>\.browser-attach'
94
- toolCallTimeoutMs: 60000
95
- ```
96
-
97
- 三个路径按本机实际值填写:
98
-
99
- ```sh
100
- where node # Windows;macOS / Linux 用 which node
101
- npm root -g # 全局安装目录;CLI 位于 <root>/@evan_zhong/browser-attach/bin/browser-attach.mjs
102
- ```
103
-
104
- 开发检出可直接指向 `<仓库>/bin/browser-attach.mjs`。macOS / Linux 把示例中的 Windows 路径换成对应的绝对路径,字段不变。
105
-
106
- - 子进程启动前,DSH 会删除名字匹配 `KEY|PASSWORD|SECRET|TOKEN` 的环境变量以及全部 `DSH_*`,其余环境变量继承;显式写在 `env` 里的值保留。配对路径已由 `--config` 固定,不依赖工作目录。
107
- - `toolCallTimeoutMs` 默认 `60000`,与 BrowserAttach 推荐的调用超时一致;单个调用确实更慢时再调大。
108
- - `failOnStartupError` 默认 `false`:连接失败只记日志、工具不出现,harness 照常启动。排查连接问题时设为 `true` 可让启动失败直接暴露。
109
- - 工具发现是异步的;启动后等 `mcp__browser_attach__browser_discover` 出现再发第一条指令。
110
- - `web` profile 开启 HMR,修改条目会就地重连;`headless`、`sdk`、`acp` 关闭 HMR,需要重启 harness。
111
- - MCP 工具与原生工具一样经过 DSH 的权限检查与审批策略。
112
-
113
- 升级后重新加载扩展并重启 harness;包与扩展版本必须一致。
114
-
115
- ## 新增宿主
116
-
117
- 追加小节时至少覆盖:配置位置与可粘贴片段、启动命令的三个绝对路径(Node、CLI、配对文件)、`BROWSER_ATTACH_HOME`、调用与启动超时、审批方式、工具前缀、验证步骤,以及升级后如何重新连接。配对与安全边界只在「通用装配」维护,不逐宿主重复。
118
-
119
- ## 故障排查
120
-
121
- - 工具结果走 MCP 文本内容,截图走标准图片内容;能否进入模型取决于宿主的图片适配、大小预算与模型能力。
122
- - 失效句柄不能跨连接复用,错误后应重新 discover + claim。
123
- - 文件上传只在浏览器本机读取路径,内容不经过 Hub 或模型。
124
- - 宿主把大结果保存为 Artifact 时,嵌套 JSON 长行可能被宿主的读取工具截断;这是宿主的分页行为,不能靠调大 Attach 的预算解决,也不等同于快照游标分页。
125
- - `Connection closed` 时依次检查:宿主的 stderr、配对文件是否可见、`hub.log`、以及 Hub 与扩展的版本是否一致(版本不一致会直接拒绝连接)。
1
+ # Agent 集成
2
+
3
+ BrowserAttach 是标准 stdio MCP,共 23 个工具;任何能启动子进程并发现 MCP 工具的 Agent 宿主都可以接入。本文按宿主给出装配步骤;配对与路径要求先读「通用装配」。
4
+
5
+ | 宿主 | 装配方式 | 配置位置 |
6
+ | --- | --- | --- |
7
+ | Nosis | `browser-attach setup --nosis` 自动写入 | `~/.nosis/agent_config.json` |
8
+ | DeepSeek Harness | 手动追加 patch 条目 | `$DSH_HOME/profiles/<name>/cordis.patch.yml` |
9
+
10
+ ## 通用装配
11
+
12
+ 先完成配对:
13
+
14
+ ```sh
15
+ npm install --global @evan_zhong/browser-attach
16
+ browser-attach setup
17
+ ```
18
+
19
+ `setup` 打印稳定的扩展目录并启动本机 Hub。按 [README](../README.md) 第 1 步在 `chrome://extensions` 或 `edge://extensions` 加载该目录;每个要暴露的浏览器 profile 各加载一次。
20
+
21
+ 宿主侧只需启动 stdio 进程:
22
+
23
+ ```sh
24
+ node <安装位置>/bin/browser-attach.mjs mcp --config <配对文件绝对路径>
25
+ ```
26
+
27
+ - `BROWSER_ATTACH_HOME` 指向配对文件所在目录。
28
+ - Node 可执行文件、CLI 与配对文件一律使用绝对路径。宿主的启动环境(PATH、工作目录、AppData 重定向)不同,重新推导会得到第二份配对。
29
+ - 安装位置变化或升级后重新执行 `setup` 并更新宿主条目。MCP 包与已加载扩展必须同版本,否则 Hub 直接拒绝连接。
30
+
31
+ 工具前缀由宿主决定,原始工具名不变(`browser_discover`、`browser_claim_tab` 等)。装配后的最小验证在各宿主一致:`browser_discover(kind="browsers")` → `kind="tabs"` → `browser_claim_tab` → `browser_snapshot` → `browser_tab_control(action="release")`。
32
+
33
+ ## Nosis
34
+
35
+ `setup --nosis` 要求 `~/.nosis/agent_config.json` 已存在(Nosis 已初始化)。该命令在完成普通配对后备份并更新此文件(备份名 `agent_config.json.browser-attach-<时间戳>.bak`),写入:
36
+
37
+ | 字段 | 值 |
38
+ |---|---|
39
+ | 服务器名 | `browser_attach` |
40
+ | `command` | 执行 setup 的 Node 可执行文件绝对路径 |
41
+ | `args` | 当前包 `bin/browser-attach.mjs` 绝对路径、`mcp`、`--config <绝对配对路径>` |
42
+ | `env.BROWSER_ATTACH_HOME` | 配对文件所在目录 |
43
+ | `startup_timeout_seconds` / `call_timeout_seconds` | `20` / `60` |
44
+ | `tools.approval` | `always` |
45
+
46
+ 同时设置 `mcp.enabled = true`。命令与配对路径都固定为绝对路径,因此本机安装位置变化后需要重新执行 `setup --nosis`;请从长期保留的位置安装,而不是临时解压目录。重启或重建 Nosis Runtime 后重新发现工具。
47
+
48
+ 自定义运行目录:
49
+
50
+ ```sh
51
+ browser-attach setup --config /absolute/shared-directory/config.json --nosis
52
+ ```
53
+
54
+ `setup` 会把解析后的配对路径写进配置,MCP 与 Hub 后续都显式传递同一路径,不会在不同启动环境中重新推导出第二套密钥。`setup` 不会搜索或自动迁移其他应用的私有缓存;已有配对请显式保留原目录。
55
+
56
+ Windows 安装脚本可直接带该开关:`./scripts/install.ps1 -ConfigureNosis`。
57
+
58
+ 验证:
59
+
60
+ ```sh
61
+ python scripts/verify-nosis.py --nosis-root /path/to/Nosis --family chrome
62
+ python scripts/verify-nosis.py --nosis-root /path/to/Nosis --family edge
63
+ ```
64
+
65
+ 脚本读取本机 Nosis 配置,通过 Nosis ToolCatalog 发现 23 个工具,覆盖发现、标签控制、页面操作和结果封装,其中 `browser_logs` 分别覆盖 console/network,验证结果封装、截图到模型消息的图片适配以及 `isError` 后连接仍可用。它用 `browser_open_tab` 在选定配置中创建本地 fixture;同一 family 有多个 profile 时用 `--browser-id <精确 browser_id>` 指定,结束后释放控制。Python 仅用于该测试,不是运行依赖。
66
+
67
+ ## DeepSeek Harness
68
+
69
+ DSH 通过 `@deepseek-ai/dsh-mcp-client` 条目接入 stdio MCP:一个服务器一个条目,工具暴露为 `mcp__<serverName>__<工具名>`。客户端插件随 harness 提供,不需要为 BrowserAttach 额外安装包。
70
+
71
+ 把条目追加到 patch 层:
72
+
73
+ - 单个 profile:`$DSH_HOME/profiles/<name>/cordis.patch.yml`;`dsh web` 对应 `web`。
74
+ - 本机所有 profile:`$DSH_HOME/cordis.patch.yml`。
75
+ - 临时试用:任意 patch 文件,用 `dsh web --patch ./browser-attach.cordis.yml` 叠加。
76
+
77
+ `$DSH_HOME` 未设置时为 `~/.dsh`。patch 文件是 patch 列表,直接追加 `insert` 条目,不要覆盖其中已有的用户 patch。
78
+
79
+ ```yaml
80
+ - insert:
81
+ - id: browser-attach
82
+ name: '@deepseek-ai/dsh-mcp-client'
83
+ config:
84
+ serverName: browser_attach
85
+ transport: stdio
86
+ command: 'C:\Program Files\nodejs\node.exe'
87
+ args:
88
+ - 'C:\Users\<用户>\AppData\Roaming\npm\node_modules\@evan_zhong\browser-attach\bin\browser-attach.mjs'
89
+ - mcp
90
+ - --config
91
+ - 'C:\Users\<用户>\.browser-attach\config.json'
92
+ env:
93
+ BROWSER_ATTACH_HOME: 'C:\Users\<用户>\.browser-attach'
94
+ toolCallTimeoutMs: 60000
95
+ ```
96
+
97
+ 三个路径按本机实际值填写:
98
+
99
+ ```sh
100
+ where node # Windows;macOS / Linux 用 which node
101
+ npm root -g # 全局安装目录;CLI 位于 <root>/@evan_zhong/browser-attach/bin/browser-attach.mjs
102
+ ```
103
+
104
+ 开发检出可直接指向 `<仓库>/bin/browser-attach.mjs`。macOS / Linux 把示例中的 Windows 路径换成对应的绝对路径,字段不变。
105
+
106
+ - 子进程启动前,DSH 会删除名字匹配 `KEY|PASSWORD|SECRET|TOKEN` 的环境变量以及全部 `DSH_*`,其余环境变量继承;显式写在 `env` 里的值保留。配对路径已由 `--config` 固定,不依赖工作目录。
107
+ - `toolCallTimeoutMs` 默认 `60000`,与 BrowserAttach 推荐的调用超时一致;单个调用确实更慢时再调大。
108
+ - `failOnStartupError` 默认 `false`:连接失败只记日志、工具不出现,harness 照常启动。排查连接问题时设为 `true` 可让启动失败直接暴露。
109
+ - 工具发现是异步的;启动后等 `mcp__browser_attach__browser_discover` 出现再发第一条指令。
110
+ - `web` profile 开启 HMR,修改条目会就地重连;`headless`、`sdk`、`acp` 关闭 HMR,需要重启 harness。
111
+ - MCP 工具与原生工具一样经过 DSH 的权限检查与审批策略。
112
+
113
+ 升级后重新加载扩展并重启 harness;包与扩展版本必须一致。
114
+
115
+ ## 新增宿主
116
+
117
+ 追加小节时至少覆盖:配置位置与可粘贴片段、启动命令的三个绝对路径(Node、CLI、配对文件)、`BROWSER_ATTACH_HOME`、调用与启动超时、审批方式、工具前缀、验证步骤,以及升级后如何重新连接。配对与安全边界只在「通用装配」维护,不逐宿主重复。
118
+
119
+ ## 故障排查
120
+
121
+ - 工具结果走 MCP 文本内容,截图走标准图片内容;能否进入模型取决于宿主的图片适配、大小预算与模型能力。
122
+ - 失效句柄不能跨连接复用,错误后应重新 discover + claim。
123
+ - 文件上传只在浏览器本机读取路径,内容不经过 Hub 或模型。
124
+ - 宿主把大结果保存为 Artifact 时,嵌套 JSON 长行可能被宿主的读取工具截断;这是宿主的分页行为,不能靠调大 Attach 的预算解决,也不等同于快照游标分页。
125
+ - `Connection closed` 时依次检查:宿主的 stderr、配对文件是否可见、`hub.log`、以及 Hub 与扩展的版本是否一致(版本不一致会直接拒绝连接)。
@@ -1,6 +1,6 @@
1
1
  # 架构与设计边界
2
2
 
3
- 当前实现为 **0.11.2**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
3
+ 当前实现为 **0.13.1**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
4
4
 
5
5
  ## 组件与数据流
6
6
 
@@ -20,13 +20,14 @@ MCP 客户端
20
20
  | `src/setup.mjs` | 生成配对密钥、复制稳定扩展目录、写 Nosis 配置 |
21
21
  | `src/hub.mjs` | 浏览器与会话注册、独占 claim、每标签串行队列、超时 |
22
22
  | `src/client.mjs` | MCP 进程到 Hub 的 WebSocket 客户端,按需拉起 Hub |
23
- | `src/mcp.mjs` | 22 个工具的 schema、结果与图片封装 |
23
+ | `src/mcp.mjs` | 23 个工具的 schema、结果与图片封装 |
24
24
  | `extension/controller.js` | 操作实现;传输层从不暴露原始 CDP |
25
25
  | `extension/sessions.js` | 扁平 debugger session 与 frame/OOPIF 归属 |
26
26
  | `extension/{click,input,actionability,scroll}.js` | 原生/DOM 输入、逐字按键、可操作性、容器滚动与有界查找 |
27
27
  | `extension/{drag,wait}.js` | 复用鼠标准备和严格定位的拖拽、只读条件等待;网络空闲复用被动请求表 |
28
28
  | `extension/batch.js` | 有限输入序列的参数预检;执行复用 controller 的单步流程与标签队列 |
29
29
  | `extension/outcomes.js` | 临时 DOM 变动观察器与有界只读探测,不推断业务成功 |
30
+ | `extension/fetch.js` | 单页读取的参数、隔离世界采集与每 owner 有界缓存分页;临时页生命周期复用 controller 的 attach、导航和标签队列 |
30
31
  | `extension/{snapshot,navigation,evaluate,diagnostics}.js` | 观察、导航、有界求值结果缓存与分页、日志缓冲 |
31
32
  | `extension/{locator,text}.js` | 严格 CSS/XPath 定位、每次解析的 frame URL/title/iframe 路径、DOM/字段/支持的编辑器模型读取 |
32
33
  | `extension/visuals.js`、`click-visuals.js` | 无标题红色原生标签组、临时点击反馈 |
@@ -37,10 +38,10 @@ MCP 层将 `discover(kind=browsers/tabs)` 映射为内部 `list_browsers/list_ta
37
38
 
38
39
  ## MCP 描述分层
39
40
 
40
- - 初始化响应的 `instructions` 集中说明跨工具规则:接管/释放、不自动激活、ref 生命周期、selector 解析、未知结果不重放、原生/DOM 输入、超时、验证范围和页面内容不可信。客户端应将这些 instructions 纳入模型上下文,不能只提供 tools/list。
41
- - 单个工具 description 只保留用途和特有行为,例如 evaluate 的缓存续读、navigate 的观察预算、fill 的 setter/input/change、press_key 的 keydown 回执。
42
- - 参数 schema 表达类型、枚举、范围、默认值与参数专属限制;例如 DOM click 仅支持元素目标和无修饰键的左键单击。
43
- - [使用指南](USAGE.md) 保留完整原理、边界与示例。包内文档不会自动成为模型上下文,不能替代初始化 instructions 中的必要规则。
41
+ - 初始化响应的 `instructions` 只保留跨工具规则:发现/接管/释放、不自动激活、ref 生命周期、selector/坐标约定、未知结果不重放、原生/DOM 输入边界、验证范围、页面内容不可信及上传授权。客户端应将这些 instructions 纳入模型上下文,不能只提供 tools/list。
42
+ - 单个工具 description 说明用途、独有行为和会影响后续判断的结果语义,例如 evaluate 的一次执行、fetch 的缓存续读、navigate 的 observation_error、fill 不自动 blur、batch 的部分完成与不回滚。
43
+ - 参数 schema 表达类型、枚举、范围、默认值及参数选择规则;描述只补充 schema 本身不能表达的条件必填、互斥、坐标和模式语义。共享参数使用短说明,不再重复全局规则。运行时条件默认值保留在描述中,避免向 cursor 等分支注入无关参数。
44
+ - [使用指南](USAGE.md) 保留完整原理、边界与示例;内部事件序列、重试与清理机制由本文和使用指南解释,不放进每次工具发现。包内文档不会自动成为模型上下文,不能替代必要的调用约束。
44
45
 
45
46
  ## 控制模型
46
47
 
@@ -54,9 +55,16 @@ MCP 层将 `discover(kind=browsers/tabs)` 映射为内部 `list_browsers/list_ta
54
55
 
55
56
  claim 与普通输入都不激活标签或窗口,也不移动系统鼠标;需要前台渲染时显式调用 `tab_control(action="activate")`。
56
57
 
58
+ ## 单页读取
59
+
60
+ `browser_fetch` 是一次 URL 读取:Hub 按显式 `browser_id` 把请求交给配对扩展;controller 创建后台 `about:blank` 临时标签,在其队列内复用 attach/session 初始化后只导航一次,按 load 或 load + network_idle 等待,在隔离世界读取主文档,再于 finally 关闭。它不接管已有页面,不创建浏览器或窗口。临时标签在扩展内记录 owner,并从标签发现列表排除;即使关闭或初始化失败,也不能被其他连接接管。关闭失败保留临时页记录并报告,release_owner/releaseAll 再次尝试关闭;正常已有标签的 detach 边界保持不变。
61
+
62
+ 正文按 owner 保留一份最多 1000000 字符的不可变缓存,分页不执行脚本、不导航,临时标签关闭不会删除它;新 fetch、读完、owner 断连或扩展断连清除。读取期间文档或控制权变化会丢弃结果,断连后的迟到结果不能复活缓存。格式、采集上限与分页语义见使用指南。
63
+
57
64
  ## 观察模型
58
65
 
59
66
  - **AX 渲染**:从 `Accessibility.getFullAXTree` 读取,移除 `InlineTextBox`,合并纯文本列表项与段落名称,丢弃 `generic` / `none` 且无名称无状态的节点,只保留少量状态属性和浏览器提供的 value、invalid。每行 `[token:n] role "name"`,`token` 是本次观察的随机前缀;这些字段不证明应用已保存。
67
+ - **交互视图**:可选 `profile=interactive` 在同一渲染器中仅保留 AX 控件角色、可聚焦/可编辑节点及其祖先链和控件内文本,不以 backendNodeId/ref 是否存在判断交互性。独立文本和合并正文可隐藏或截取预览,控件名称、值和 ref 的原始 AX 身份保留;默认 `full` 行为不变。筛选仍读取完整 AX 树,只降低输出与分页成本;视图参数绑定分页观察。
60
68
  - **ref 解析**:ref 保存 `backendNodeId`、所属 frame/session 和观察时的 AX role/name;动作时重新 `DOM.resolveNode` 并用局部 AX 查询核对语义身份。输入与求值不清空 refs,只使快照分页失效;原节点仍连接时可复用,节点替换、role/name 改变、文档或 frame 变化时安全失败,不按 role/name 猜测替代节点。
61
69
  - **双预算**:`max_nodes` 限制节点数,`max_chars` 限制每行序列化后的累计字符数;两者独立,任一耗尽都会返回 `next_cursor`。
62
70
  - **续读**:续读沿用同一份 `state`(`entries`、`refs`、`nextOffset`),所以 refs 在分页过程中保持有效。单个节点放不进预算时直接报错要求提高预算,避免静默截断尾部。
@@ -1,12 +1,24 @@
1
1
  # 验证报告
2
2
 
3
- 当前版本 **0.11.2**(2026-10-22)。验证使用本地 fixture 和临时 profile,链路为扩展 → 本机 Hub → stdio MCP。
3
+ 当前版本 **0.13.1**(2026-10-22)。验证使用本地 fixture 和临时 profile,链路为扩展 → 本机 Hub → stdio MCP。
4
4
 
5
5
  ## 当前版本
6
6
 
7
- - `npm test`:144 项通过。覆盖独占控制、快照/ref 生命周期、严格 selector 与有界歧义候选、原生/DOM 输入、逐字输入、hover、pointer/HTML5 拖拽、滚动边界后的延迟加载、条件等待、HTML select、batch、历史导航、OOPIF、日志、上传、截图、对话框和未知结果不重放。
8
- - `npm run test:package`:仓库外含空格路径安装运行通过,22 个 MCP 工具、45 个发布文件;包含 `drag.js` / `wait.js` / `scroll.js` / `outcomes.js`,不包含配对密钥。版本为 0.11.2,示例配置使用 `@latest`。
9
- - Edge 155 headless `--primitives`:13 个场景通过,包含普通 pointer/HTML5 拖拽、取消 pointerdown 默认行为后的可信回执、滚动边界后延迟增长和歧义候选。
7
+ - 描述精简:实际 MCP 初始化与 tools/list 的描述文本从 18716 降至 11469 字符,减少 38.7%(不含 schema 结构,不等于 token 数);instructions / 工具描述 / 参数说明分别为 1893 / 3949 / 5627 字符。跨工具规则、工具语义、参数选择规则与文档实现细节各自归位。
8
+ - 对比精简前后的实际 MCP 输出:23 个工具的名称与输入 schema 在移除 description 后完全一致;类型、枚举、默认值、范围与 required 均未改变。
9
+ - `npm test`:159 项通过;`npm run test:package`:仓库外安装运行通过,23 个工具、46 个发布文件,版本为 0.13.1。本次只调整描述与版本,未修改浏览器操作实现,未重复运行真实浏览器用例。
10
+
11
+ ## 0.13.0 验证
12
+
13
+ - `npm test`:159 项通过。新增单页 fetch 参数、三种格式、采集上限、缓存分页、owner 隔离、断连清理、初始化与导航超时、文档/控制权变化、关闭失败保留所有权和迟到回执不续跑;保留原有 149 项回归覆盖。
14
+ - `npm run test:package`:仓库外含空格路径安装运行通过,23 个 MCP 工具、46 个发布文件;包含 `fetch.js`,不包含配对密钥,版本为 0.13.0。
15
+ - Chromium 156 与 Edge 155 headless 完整 stdio MCP 集成:各 219 个调用通过。新增后台临时页不激活、单次导航、重定向与 HttpOnly 登录 Cookie、网络空闲等待延迟内容、无链接爬取、text/HTML/JSON、不可变分页、1000000 字符采集上限、非法 JSON 和导航超时后关闭;同时覆盖原有输入、OOPIF、上传、日志、截图与导航链路。
16
+
17
+ ## 0.12.0 验证
18
+
19
+ - `npm test`:149 项通过。覆盖 interactive 快照的控件/祖先筛选、文本预览与原始 ref 身份、视图绑定分页、独占控制、快照/ref 生命周期、严格 selector 与有界歧义候选、原生/DOM 输入、逐字输入、hover、pointer/HTML5 拖拽、document 拖拽坐标差值与按轴滚动、滚动边界后的延迟加载、条件等待、HTML select、batch、历史导航、OOPIF、日志、上传、截图、对话框和未知结果不重放。
20
+ - `npm run test:package`:仓库外含空格路径安装运行通过,22 个 MCP 工具、45 个发布文件;包含 `drag.js` / `wait.js` / `scroll.js` / `outcomes.js`,不包含配对密钥。版本为 0.12.0,示例配置使用 `@latest`。
21
+ - Chromium headless `--primitives`:16 个场景通过,包含 interactive 快照、document 坐标纵向 pointer/HTML5 拖拽、普通 pointer/HTML5 拖拽、取消 pointerdown 默认行为后的可信回执、滚动边界后延迟增长和歧义候选。含 200 段正文的 fixture 中,默认 full 首屏 23302 字符且仍需续页,interactive 为 947 字符且无需续页;该比较只衡量返回内容,不代表 AX 采集提速。
10
22
 
11
23
  ## 升级前的浏览器验证
12
24
 
package/docs/USAGE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # BrowserAttach 使用指南
2
2
 
3
- 适用于 **0.11.2**:Node.js 22+、Chrome / Edge 125+、22 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
3
+ 适用于 **0.13.1**:Node.js 22+、Chrome / Edge 125+、23 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
4
4
 
5
- 工具发现采用三层说明:初始化 `instructions` 提供跨工具规则,各工具 description 和参数 schema 说明自身行为与约束,本文保留完整边界和示例。客户端集成时应把 MCP 初始化响应中的 instructions 传入模型上下文;仅读取 tools/list 不会包含完整的通用调用规则。
5
+ 工具发现采用三层说明:初始化 `instructions` 提供跨工具规则;工具 description 说明用途、独有行为与结果语义;参数 schema 及其短说明提供类型、默认值和选择规则。本文保留完整原理、边界和示例。客户端集成时应把 MCP 初始化响应中的 instructions 传入模型上下文;仅读取 tools/list 不会包含完整的通用调用规则。
6
6
 
7
7
  ## 基本流程
8
8
 
@@ -35,8 +35,9 @@
35
35
 
36
36
  | 工具 | 主要参数 | 说明 |
37
37
  |---|---|---|
38
- | `browser_snapshot` | `max_nodes=600`, `max_chars=24000`, `cursor` | AX 语义树、元素 ref、frame 覆盖、待处理文件选择器 |
38
+ | `browser_snapshot` | `profile`, `text`, `text_limit`, `max_nodes=600`, `max_chars=24000`, `cursor` | AX 语义树、元素 ref、frame 覆盖、待处理文件选择器 |
39
39
  | `browser_wait` | `condition`, `selector`, `expected` / `previous`, `idle_ms=500`, `timeout_ms=5000` | 只读等待元素、文本、URL 或被动网络空闲 |
40
+ | `browser_fetch` | `browser_id`, `url`, `wait_until=load`, `format=text`, `max_chars=20000`, `cursor` | 后台临时页读取一次,关闭后按缓存续读 |
40
41
  | `browser_read_text` | `mode`, `selector`, `offset=0`, `max_chars=20000` | 主 frame 的 DOM、字段值或支持的编辑器模型 |
41
42
 
42
43
  ### 输入
@@ -66,13 +67,36 @@
66
67
 
67
68
  普通结果是一份紧凑 JSON 文本;截图另外返回标准图片内容。失败通过 MCP `isError` 返回,错误文本即原因。
68
69
 
70
+ ## 读取一个 URL
71
+
72
+ 先用 `browser_discover(kind="browsers")` 选择配置,再调用:
73
+
74
+ ```js
75
+ browser_fetch({browser_id, url: "https://example.com/article", wait_until: "network_idle", format: "text", max_chars: 20000})
76
+ // → {url, title, contentType, format, body, capture_truncated, truncated, next_cursor}
77
+ browser_fetch({browser_id, cursor: next_cursor, max_chars: 20000})
78
+ ```
79
+
80
+ `browser_fetch` 在选定配置中新建 `active=false` 的临时标签,复用该配置的登录态和导航链路,采集后关闭;不需要对临时页显式 discover/claim。它不操作已有标签,不创建浏览器或窗口,不跟随页面链接。后台标签会短暂出现在标签栏,不能保证浏览器完全不可见。网页脚本、子资源、重定向仍照常执行,打开 URL 也可能产生网站状态变化;“读一页”指工具范围,不代表 HTTP 请求无副作用。
81
+
82
+ - `wait_until=load` 为默认;`network_idle` 在 load 后再等被动请求表中连续 500 ms 无活动请求,复用 `browser_wait` 的网络空闲条件,不保证应用内容全部完成。`timeout_ms` 为 100–30000,默认 15000;调试初始化、导航和读取共享预算,标签创建和关闭受 Hub 超时约束。
83
+ - 只读当前主文档。`text` 使用 `body.innerText`;`html` 返回执行脚本后的当前 DOM `outerHTML`,并非原始 HTTP 响应;`json` 从浏览器呈现的 JSON 正文解析、校验后序列化,不另发 fetch/XHR,也不返回响应头或 HTTP 状态。`contentType` 来自 `document.contentType`,`url` 为采集时的最终地址。非 HTTP(S) 最终文档报错。
84
+ - `body` 始终为字符串,`max_chars` 为 1–100000,默认 20000,按 JavaScript 字符串长度计算。JSON 分页也为字符串片段,应先按顺序拼接再 `JSON.parse`。
85
+ - 一次最多保留 1000000 字符。`next_cursor` 非空表示缓存尚有后续片段;`truncated` 表示尚未返回全部内容或采集有损。`capture_truncated=true` 表示文本/HTML 超出采集上限,丢失的尾部不能通过 cursor 取回;JSON 无法完整捕获或解析时直接报错。
86
+ - 每个连接、每个浏览器配置最多保留一份缓存;新 fetch 替换旧缓存,读完或断连清除。续读只接受 `browser_id`、`cursor` 和可选 `max_chars`,不再打开页面。缓存不因临时页关闭而失效,也不能跨连接读取。
87
+ - 超时、解析错误或断连会清理临时页,不自动重新导航。关闭失败会明确报错并保留清理责任,在断连时再次尝试关闭;不会静默移交给其他连接。临时页不出现在 `discover(kind="tabs")` 的结果中。
88
+
69
89
  ## 观察:快照与引用
70
90
 
71
91
  `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`。
72
92
 
73
93
  - 渲染时移除 `InlineTextBox`,合并纯文本列表项与段落名称,保留 `disabled`、`focused`、`expanded`、`checked`、`selected`、`required`、`invalid`、`level` 等状态,以及浏览器提供的 `value`;`[token:n]` 即后续动作使用的 `ref`。未提供 invalid/selected 等属性不代表有效或已提交;value 也可能只是下拉框的过滤文本。
74
94
  - `max_nodes` 为 1–3000(默认 600);`max_chars` 为 1–2000000(默认 24000),按行序列化长度累计,与节点数是两个独立预算。
95
+ - `profile` 默认为 `full`。`interactive` 保留 AX 控件角色、可聚焦/可编辑节点及其祖先链和控件内的文本,略去无关正文;禁用控件仍保留。仅按是否有 ref 筛选无法有效压缩,因为段落、列表项等也可能有 ref。自定义控件未提供可访问角色或可聚焦/可编辑属性时可能被省略,需用 `full` 观察。
96
+ - `text` 默认 `true`;`false` 隐去独立文本和纯文本段落/列表项的合并正文,仍保留可访问名称及控件值。`text_limit` 为 0–10000,限制每个文本/合并正文名称并以 `…` 标明省略;`interactive` 默认 80,`full` 默认不截断。需要更多正文时重新请求 `full`,或显式提高 `text_limit`。控件名称、值及 ref 校验所用身份始终完整。
97
+ - 返回 `profile` / `text` / `text_limit` 表明本次视图;`node_count` 和 `total_node_count` 统计筛选后的节点。`truncated` / `next_cursor` 只表示该视图尚有分页,不表示视图包含全部网页正文。筛选发生在完整 AX 树读取后,减少返回内容与分页次数,不减少 CDP 的 AX 树采集量。
75
98
  - `truncated: true` 时把 `next_cursor` 传回同一个工具继续读取同一份观察,refs 在续读期间保持有效;也可以调整预算。单个超长节点放不下时会明确报出所需预算,不静默丢尾部。
99
+ - 续读可省略视图参数,沿用原 `profile` / `text` / `text_limit`;显式提供时必须与原观察相同。调整视图须请求新快照,并按下表使旧 ref 失效。
76
100
  - 覆盖主 frame、同进程 iframe 与递归 OOPIF;读取失败、读取期间变化或附着失败的 frame 列入 `omitted_frames`。
77
101
 
78
102
  失效规则:
@@ -150,6 +174,8 @@ hover 接受与原生 click 相同的目标,执行一次 mouseMoved,不派
150
174
 
151
175
  drag 的 source / destination 各自接受 ref、严格 selector(可带 frame_id/nth)或 x/y(可带 coordinate_space)。mode 必须按页面交互选择,默认 pointer;steps 默认 20,范围 2–100,每步间隔约 16 ms。
152
176
 
177
+ 两端均为 document 坐标且能同时容纳于一个顶层视口时,在按下前统一滚动位置,保留两端的 x/y 差值。已在视口内的 document 坐标不重复居中;需要滚动时只调整越界的轴。跨屏目标仍在按住后滚入视口,因此返回的 from/to 是各自派发时的视口坐标,不能用两者之差推断跨屏的文档距离。
178
+
153
179
  ```json
154
180
  {"tab_handle":"...","source":{"selector":"#card"},"destination":{"selector":"#column"},"mode":"html5","steps":20}
155
181
  ```
@@ -6,6 +6,7 @@ export function deadline(timeout_ms = 5000, phase = 'input', parentEnd = Infinit
6
6
  let dispatched = false, waiting;
7
7
  function error() {
8
8
  if (phase === 'evaluation') return Object.assign(new Error(dispatched ? 'JavaScript evaluation timed out. Page effects may already have occurred. Observe before continuing. Do not replay automatically.' : 'Evaluation preparation timed out; no expression was executed.'), { input_dispatched: dispatched });
9
+ if (phase === 'fetch') return new Error(`Fetch did not finish within ${timeout_ms} ms; the temporary tab will be closed. Do not automatically replay the request.`);
9
10
  if (phase === 'wait') return Object.assign(new Error(`Condition was not satisfied within ${timeout_ms} ms`), { input_dispatched: false });
10
11
  if (phase === 'observation') return new Error(`Navigation observation timed out within ${timeout_ms} ms; navigation already succeeded. Take a new snapshot.`);
11
12
  const result = new Error(dispatched
@@ -67,9 +67,14 @@ export function createClick(cdp, value, viewport, position, sessions, getTab, ge
67
67
  view = await budget.run(() => viewport(target));
68
68
  if (args.coordinate_space === 'document') {
69
69
  if (!Number.isFinite(args.x) || !Number.isFinite(args.y) || args.x < 0 || args.y < 0) throw new Error('Document coordinates must be finite and nonnegative');
70
- value(await send('Runtime.evaluate', { returnByValue: true,
71
- expression: `scrollTo({left:${Math.max(0, args.x - view.width / 2)},top:${Math.max(0, args.y - view.height / 2)},behavior:'instant'})` }));
72
- view = await budget.run(() => viewport(target));
70
+ const x = args.x - view.scroll_x, y = args.y - view.scroll_y;
71
+ if (args.scroll !== false && (x < 0 || y < 0 || x >= view.width || y >= view.height)) {
72
+ const left = x < 0 || x >= view.width ? Math.max(0, args.x - view.width / 2) : view.scroll_x;
73
+ const top = y < 0 || y >= view.height ? Math.max(0, args.y - view.height / 2) : view.scroll_y;
74
+ value(await send('Runtime.evaluate', { returnByValue: true,
75
+ expression: `scrollTo({left:${left},top:${top},behavior:'instant'})` }));
76
+ view = await budget.run(() => viewport(target));
77
+ }
73
78
  point = position(view, args.x - view.scroll_x, args.y - view.scroll_y);
74
79
  } else point = position(view, args.x, args.y);
75
80
  }