@evan_zhong/browser-attach 0.11.1 → 0.11.2

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
@@ -4,50 +4,54 @@
4
4
 
5
5
  # BrowserAttach
6
6
 
7
- 让 MCP 客户端操作你已经打开、已经登录的 Chrome / Edge 标签页。BrowserAttach 通过本机配对扩展连接浏览器,保留现有登录状态,不需要启动远程调试端口或单独的自动化浏览器。
7
+ <p align="center">
8
+ <strong>English</strong> · <a href="README.zh.md">简体中文</a>
9
+ </p>
10
+
11
+ Drive the Chrome / Edge tabs you already have open and already signed in from an MCP client. BrowserAttach connects to the browser through a locally paired extension, keeps your existing sign-in state, and needs neither a remote debugging port nor a separate automation browser.
8
12
 
9
- [npm](https://www.npmjs.com/package/@evan_zhong/browser-attach) · [使用指南](docs/USAGE.md) · [架构与设计](docs/ARCHITECTURE.md) · [验证报告](docs/TEST-REPORT.md) · [问题反馈](https://github.com/EvannZhongg/BrowserAttach/issues) · [贡献指南](CONTRIBUTING.md)
13
+ [npm](https://www.npmjs.com/package/@evan_zhong/browser-attach) · [Usage guide](docs/USAGE.md) · [Architecture](docs/ARCHITECTURE.md) · [Agent integration](docs/AGENT-INTEGRATION.md) · [Test report](docs/TEST-REPORT.md) · [Issues](https://github.com/EvannZhongg/BrowserAttach/issues) · [Contributing](CONTRIBUTING.md)
10
14
 
11
- ## 能做什么
15
+ ## What it does
12
16
 
13
- - 发现多个浏览器配置,精确选择并独占接管标签页,完成后释放。
14
- - 读取可访问性树、字段值和支持的编辑器模型,通过当前快照引用或严格 CSS/XPath 定位点击、替换填写和逐字输入。
15
- - 原生指针悬停、分步拖拽、条件等待;无需打开系统弹层即可选择 HTML 下拉选项。
16
- - 导航与历史前进/后退、打开或关闭标签页,处理 JavaScript 对话框,上传本机文件并截取整页或单个元素。
17
- - 在主页面执行 JavaScript 表达式,读取接管后的控制台、页面异常和网络生命周期日志;需要校验 XHR/fetch 时显式开启有界、脱敏的请求/响应详情。
18
- - 使用标准 stdio MCP,共提供 22 个工具。Hub 仅监听本机 loopback,通过随机密钥配对扩展。
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.
19
+ - Native pointer hover, stepped drags and conditional waits; select HTML options without opening a native popup.
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
+ - 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.
19
23
 
20
- ## 环境要求
24
+ ## Requirements
21
25
 
22
- - Node.js **22 或更新版本**,以及 npm。
23
- - Chrome / Edge **125 或更新版本**,且允许加载未打包扩展。推荐使用最新稳定版。
24
- - 支持 stdio MCP 的客户端,能从 `PATH` 找到 Node/npm。
26
+ - Node.js **22 or newer**, plus npm.
27
+ - Chrome / Edge **125 or newer** with permission to load unpacked extensions. Latest stable is recommended.
28
+ - An MCP client that speaks stdio MCP and can find Node/npm on `PATH`.
25
29
 
26
- ## 快速开始
30
+ ## Quick start
27
31
 
28
- ### 1. 安装与配对
32
+ ### 1. Install and pair
29
33
 
30
34
  ```sh
31
35
  npm install --global @evan_zhong/browser-attach
32
36
  browser-attach setup
33
37
  ```
34
38
 
35
- `setup` 会输出稳定的扩展目录并启动本地 Hub。扩展目录通常是:
39
+ `setup` prints a stable extension directory and starts the local Hub. The directory is usually:
36
40
 
37
- - Windows:`%USERPROFILE%\.browser-attach\extension`
38
- - macOS / Linux:`~/.local/share/browser-attach/extension`
41
+ - Windows: `%USERPROFILE%\.browser-attach\extension`
42
+ - macOS / Linux: `~/.local/share/browser-attach/extension`
39
43
 
40
- 打开 `chrome://extensions` 或 `edge://extensions`,启用开发者模式,选择“加载已解压的扩展程序”,加载该目录。每个需要接入的浏览器配置分别加载一次;扩展选项可设置容易辨认的配置名称。
44
+ Open `chrome://extensions` or `edge://extensions`, enable developer mode, choose "Load unpacked" and load that directory. Load it once for every browser profile you want to expose; the extension options page lets you set a recognizable profile name.
41
45
 
42
- 也可以不全局安装:
46
+ Or without a global install:
43
47
 
44
48
  ```sh
45
49
  npx --yes --package=@evan_zhong/browser-attach@latest browser-attach setup
46
50
  ```
47
51
 
48
- ### 2. 配置 MCP 客户端
52
+ ### 2. Configure the MCP client
49
53
 
50
- 将以下配置合并到客户端的 MCP 配置中,或参考 [mcp-config.json](mcp-config.json):
54
+ Merge the following into your client's MCP configuration, or see [mcp-config.json](mcp-config.json):
51
55
 
52
56
  ```json
53
57
  {
@@ -60,51 +64,51 @@ npx --yes --package=@evan_zhong/browser-attach@latest browser-attach setup
60
64
  }
61
65
  ```
62
66
 
63
- Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局安装时,也可以配置 `"command": "browser-attach", "args": ["mcp"]`。重启 MCP 客户端后发现工具。
67
+ 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.
64
68
 
65
- `@latest` 会在启动时解析 npm 最新版本。MCP 包与已加载扩展必须为同一版本;升级后重新运行 `browser-attach setup` 并在扩展页重新加载扩展,否则 Hub 会拒绝旧扩展连接。
69
+ `@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.
66
70
 
67
- Nosis 用户可运行 `browser-attach setup --nosis` 自动备份并更新配置,详见 [Nosis 集成](docs/NOSIS-INTEGRATION.md)。
71
+ Assembly steps for supported hosts are in [agent integration](docs/AGENT-INTEGRATION.md); other clients use the stdio entry above.
68
72
 
69
- ### 3. 开始操作
73
+ ### 3. Start driving
70
74
 
71
- 1. `browser_discover(kind="browsers")` → `browser_discover(kind="tabs")`,确认浏览器、标签页标题和 URL。
72
- 2. `browser_claim_tab`,用 `discover(kind="tabs")` 的 `tab_id` 接管(可选 `expected_url` / `expected_title` 精确校验),取得本连接的 `tab_handle`。
73
- 3. `browser_snapshot` 取得元素 `ref`,再执行点击、填写或其他动作。
74
- 4. 动作后重新观察,结束时调用 `browser_tab_control(action="release")`。
75
+ 1. `browser_discover(kind="browsers")` → `browser_discover(kind="tabs")` to confirm the browser, tab title and URL.
76
+ 2. `browser_claim_tab` with the `tab_id` from `discover(kind="tabs")` (optional `expected_url` / `expected_title` exact guards) to take control and receive this connection's `tab_handle`.
77
+ 3. `browser_snapshot` to get element `ref`s, then click, fill or run other actions.
78
+ 4. Observe again after each action, and finish with `browser_tab_control(action="release")`.
75
79
 
76
- 标题易变时,claim 可省略 `expected_title`;`expected_url` 也可选。点击/填写可改用每次重新解析的 `selector`。原生元素点击自动滚动;整页坐标点击使用 `coordinate_space: "document"`。求值结果使用 `cursor` 续读缓存,不会重新运行脚本。设计取舍见 [架构说明](docs/ARCHITECTURE.md#设计取舍)。
80
+ When titles change easily, claim can omit `expected_title`; `expected_url` is optional too. Click/fill can instead use a `selector` that is re-resolved on every call. Native element clicks scroll into view automatically; full-page coordinate clicks use `coordinate_space: "document"`. Evaluation results continue from the cache through `cursor` without re-running the script. Design trade-offs: [architecture notes](docs/ARCHITECTURE.md#设计取舍).
77
81
 
78
- 接管和普通输入不会主动激活标签或窗口;需要前台渲染时显式调用 `browser_tab_control(action="activate")`。操作结果未知时先重新观察,避免重复提交。完整参数、分页、上传与输入语义见 [使用指南](docs/USAGE.md)。
82
+ 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).
79
83
 
80
- ## 工具
84
+ ## Tools
81
85
 
82
- | 工具 | 用途 |
86
+ | Tool | Purpose |
83
87
  | --- | --- |
84
- | `browser_discover` | `kind=browsers/tabs`,发现浏览器配置、标签身份及控制状态 |
85
- | `browser_claim_tab` | 独占接管已发现的标签 |
86
- | `browser_open_tab` / `browser_tab_control` | 创建标签;显式 `release/activate/close` 释放控制、激活或关闭已接管的标签 |
87
- | `browser_snapshot` / `browser_read_text` | AX 快照、元素引用和正文 |
88
- | `browser_wait` | 只读等待元素、文本、URL 或网络空闲 |
89
- | `browser_click` / `browser_hover` / `browser_drag` / `browser_select_option` / `browser_type` / `browser_fill` / `browser_press_key` / `browser_scroll` | 原生/DOM 输入、指针悬停与拖拽、下拉选择、逐字输入与滚动 |
90
- | `browser_batch` | 单队列顺序执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,失败即停并返回部分结果 |
91
- | `browser_navigate` | HTTP(S) 导航与加载等待 |
92
- | `browser_handle_dialog` | alert、confirm、prompt |
93
- | `browser_upload_file` | 原生文件 input 或拦截的文件选择器 |
94
- | `browser_screenshot` | 视口或整页 PNG,附坐标映射 |
95
- | `browser_evaluate` | 主页面 JS 求值、Promise 等待、有界结果 |
96
- | `browser_logs` | 用 `kind=console/network` 选择控制台、异常或请求日志,支持过滤和游标续读 |
97
-
98
- ## 本机数据与升级
99
-
100
- | 平台 | 默认数据目录 |
88
+ | `browser_discover` | `kind=browsers/tabs`; discover browser profiles, tab identity and control status |
89
+ | `browser_claim_tab` | Exclusively claim a discovered tab |
90
+ | `browser_open_tab` / `browser_tab_control` | Create a tab; explicit `release/activate/close` of a claimed tab |
91
+ | `browser_snapshot` / `browser_read_text` | AX snapshot, element refs and page text |
92
+ | `browser_wait` | Read-only wait for an element, text, URL or network idle |
93
+ | `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 |
94
+ | `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 |
95
+ | `browser_navigate` | HTTP(S) navigation and load waiting |
96
+ | `browser_handle_dialog` | alert, confirm, prompt |
97
+ | `browser_upload_file` | Native file input or an intercepted file chooser |
98
+ | `browser_screenshot` | Viewport or full-page PNG with coordinate mapping |
99
+ | `browser_evaluate` | Main-page JS evaluation, promise awaiting, bounded results |
100
+ | `browser_logs` | Select console, error or request logs with `kind=console/network`; supports filters and cursor continuation |
101
+
102
+ ## Local data and upgrades
103
+
104
+ | Platform | Default data directory |
101
105
  | --- | --- |
102
106
  | Windows | `%USERPROFILE%/.browser-attach` |
103
107
  | macOS / Linux | `${XDG_DATA_HOME:-~/.local/share}/browser-attach` |
104
108
 
105
- 可通过绝对路径 `BROWSER_ATTACH_HOME` 显式覆盖。Windows 默认避开可能被打包宿主虚拟化的 AppData;不会搜索或自动合并其他宿主的私有缓存。目录包含配对配置、Hub 日志及稳定的扩展副本;不依赖 clone 位置、客户端工作目录或 npm 缓存。已有自定义目录请继续使用相同环境变量或 `--config`,不要在新默认目录意外创建另一套配对。
109
+ Override it explicitly with the absolute path `BROWSER_ATTACH_HOME`. The Windows default avoids AppData, which packaged hosts may virtualize; no other host's private cache is searched or merged automatically. The directory holds the pairing configuration, Hub logs and the stable extension copy, and depends on neither the clone location, the client working directory nor the npm cache. If you already use a custom directory, keep using the same environment variable or `--config` instead of accidentally creating a second pairing under the new default.
106
110
 
107
- `setup --nosis` 会备份配置并固定当前 Node、CLI 和实际配对文件的绝对路径,同时设置 MCP 的 `BROWSER_ATTACH_HOME`;支持与自定义 `--config` 一起使用。MCP 启动 Hub 时继续传入同一绝对配对路径及目录,避免不同启动环境重新推导出另一份配置。
111
+ 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).
108
112
 
109
113
  ```sh
110
114
  browser-attach status
@@ -113,27 +117,27 @@ npm install --global @evan_zhong/browser-attach
113
117
  browser-attach setup
114
118
  ```
115
119
 
116
- 升级后在浏览器扩展管理页重新加载扩展,再重启 MCP 连接。Hub 与扩展版本必须一致。首次使用本项目的新名称时,应停止之前运行的 Hub 并重新配对;默认端口为 `47831`,可用 `setup --port` 修改。
120
+ 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.
117
121
 
118
- ## 架构与使用边界
122
+ ## Architecture and boundaries
119
123
 
120
124
  ```text
121
- MCP 客户端 → Node.js stdio MCP → 127.0.0.1 Hub
122
- ↕ 本机配对
123
- Chrome / Edge MV3 扩展
125
+ MCP client → Node.js stdio MCP → 127.0.0.1 Hub
126
+ ↕ local pairing
127
+ Chrome / Edge MV3 extension
124
128
  ↓ chrome.debugger / CDP
125
- 现有 HTTP(S) 标签页
129
+ existing HTTP(S) tabs
126
130
  ```
127
131
 
128
- 每个 MCP 连接拥有独立控制权,句柄不能跨连接使用。断连或停止调试会释放控制。客户端端点要求 Bearer 密钥并拒绝网页 Origin;扩展验证固定 Origin、密钥和版本。`browser_evaluate` 可以使用页面现有登录态读写数据,执行后快照分页失效,原节点 refs 可继续解析;超时或报错前可能已产生作用,不应自动重放。未暴露通用 CDP 工具。
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; after it runs, snapshot pagination expires while refs to the original nodes still resolve; a timeout or error may already have had an effect and must not be replayed automatically. No generic CDP tool is exposed.
129
133
 
130
- 日志从接管开始采集,跨导航保留,释放时清除;每类最多保留 500 条、1,048,576 个 JSON 字符,超限淘汰会报告 `data_loss`。网络日志默认只记录生命周期元数据;显式 capture 才读取之后的 XHR/fetch 有界、脱敏 headers/正文,不改变缓存或拦截流量。完整参数及边界见[使用指南](docs/USAGE.md#诊断日志)。
134
+ Logs are collected from claim time, survive navigation and are cleared on release; each kind keeps at most 500 entries and 1,048,576 JSON characters, and eviction reports `data_loss`. 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. Full parameters and limits: [usage guide](docs/USAGE.md#诊断日志).
131
135
 
132
- 浏览器扩展具有调试权限,客户端操作可使用现有网站登录态;只应将其接入你信任的 MCP 客户端。网页内容不应被视为用户授权。配对密钥和生成的 `config.generated.js` 不应提交或发布,打包检查会拒绝包含该文件的包。
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.
133
137
 
134
- 目前仅支持 Chrome / Edge 的 HTTP(S) 页面。下载管理、网络拦截和 Firefox / WebKit 尚不支持。跨进程 iframe 使用递归 session 路由,覆盖快照、输入、上传和日志;透视变换的 iframe 点击会明确拒绝。原生指针输入要求活动且未暂停的标签及未最小化窗口,焦点和文档可见性只作诊断;不就绪时快速返回未派发及 tab_control(action=activate) 提示;不会自动抢焦点。后台截图可能因浏览器暂停渲染而超时;原生菜单和标签拖动也可能抑制输入。
138
+ Only HTTP(S) pages in Chrome / Edge are supported today. Download management, network interception and Firefox / WebKit are not supported. Cross-process iframes use recursive session routing covering snapshots, input, uploads and logs; clicks in perspective-transformed iframes are refused explicitly. Native pointer input requires an active, unsuspended tab in a non-minimized window, and focus and document visibility are diagnostic only; when the tab is not ready it returns without dispatch and suggests tab_control(action=activate) instead of stealing focus. Background screenshots may time out because the browser pauses rendering; native menus and tab drags may also suppress input.
135
139
 
136
- ## 本地开发
140
+ ## Local development
137
141
 
138
142
  ```sh
139
143
  git clone https://github.com/EvannZhongg/BrowserAttach.git
@@ -143,16 +147,16 @@ npm test
143
147
  npm run test:package
144
148
  ```
145
149
 
146
- 直接运行源码并配对仓库中的扩展:
150
+ Run the source directly and pair the extension in the repository:
147
151
 
148
152
  ```sh
149
153
  node bin/browser-attach.mjs setup --extension-dir ./extension
150
154
  node bin/browser-attach.mjs mcp
151
155
  ```
152
156
 
153
- 开发模式有意使用仓库扩展路径。一般安装请使用不带 `--extension-dir` 的 setup,确保移动仓库后仍可运行。Windows 也可运行 `./scripts/install.ps1`,或添加 `-ConfigureNosis`。
157
+ Development mode deliberately uses the repository extension path. For normal installs, use `setup` without `--extension-dir` so it keeps working after the repository moves. On Windows you can also run `./scripts/install.ps1`.
154
158
 
155
- 真实浏览器测试需要一个允许命令行加载扩展的 Chromium / Edge 可执行文件,路径由开发者传入:
159
+ Real-browser tests need a Chromium / Edge executable that allows loading extensions from the command line; the path is supplied by the developer:
156
160
 
157
161
  ```sh
158
162
  npm run test:live -- --executable /path/to/chromium --family chromium
@@ -160,10 +164,10 @@ npm run test:interaction -- --executable /path/to/chromium --family chromium
160
164
  npm run test:interaction -- --tab-indicators --executable /path/to/chromium --family chromium
161
165
  ```
162
166
 
163
- 这些测试使用临时配置和本地 fixture。测试覆盖与限制见 [验证报告](docs/TEST-REPORT.md),实现与设计取舍见 [架构与设计](docs/ARCHITECTURE.md)。
167
+ 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).
164
168
 
165
- ## 参与贡献
169
+ ## Contributing
166
170
 
167
- 欢迎通过 [Issues](https://github.com/EvannZhongg/BrowserAttach/issues) 提交可复现问题,或提交 Pull Request。请附上 Node、系统、浏览器与扩展版本,移除配对密钥和网页隐私数据。具体流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。
171
+ Reproducible issues via [Issues](https://github.com/EvannZhongg/BrowserAttach/issues), or Pull Requests. Please include Node, OS, browser and extension versions, and remove pairing keys and private page data. See [CONTRIBUTING.md](CONTRIBUTING.md) for the process.
168
172
 
169
- 本项目采用 [MIT License](LICENSE)。
173
+ Released under the [MIT License](LICENSE).
package/README.zh.md ADDED
@@ -0,0 +1,173 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/EvannZhongg/BrowserAttach/main/extension/icons/BrowserAttach.png" width="112" alt="BrowserAttach logo">
3
+ </p>
4
+
5
+ # BrowserAttach
6
+
7
+ <p align="center">
8
+ <a href="README.md">English</a> · <strong>简体中文</strong>
9
+ </p>
10
+
11
+ 让 MCP 客户端操作你已经打开、已经登录的 Chrome / Edge 标签页。BrowserAttach 通过本机配对扩展连接浏览器,保留现有登录状态,不需要启动远程调试端口或单独的自动化浏览器。
12
+
13
+ [npm](https://www.npmjs.com/package/@evan_zhong/browser-attach) · [使用指南](docs/USAGE.md) · [架构与设计](docs/ARCHITECTURE.md) · [Agent 集成](docs/AGENT-INTEGRATION.md) · [验证报告](docs/TEST-REPORT.md) · [问题反馈](https://github.com/EvannZhongg/BrowserAttach/issues) · [贡献指南](CONTRIBUTING.md)
14
+
15
+ ## 能做什么
16
+
17
+ - 发现多个浏览器配置,精确选择并独占接管标签页,完成后释放。
18
+ - 读取可访问性树、字段值和支持的编辑器模型,通过当前快照引用或严格 CSS/XPath 定位点击、替换填写和逐字输入。
19
+ - 原生指针悬停、分步拖拽、条件等待;无需打开系统弹层即可选择 HTML 下拉选项。
20
+ - 导航与历史前进/后退、打开或关闭标签页,处理 JavaScript 对话框,上传本机文件并截取整页或单个元素。
21
+ - 在主页面执行 JavaScript 表达式,读取接管后的控制台、页面异常和网络生命周期日志;需要校验 XHR/fetch 时显式开启有界、脱敏的请求/响应详情。
22
+ - 使用标准 stdio MCP,共提供 22 个工具。Hub 仅监听本机 loopback,通过随机密钥配对扩展。
23
+
24
+ ## 环境要求
25
+
26
+ - Node.js **22 或更新版本**,以及 npm。
27
+ - Chrome / Edge **125 或更新版本**,且允许加载未打包扩展。推荐使用最新稳定版。
28
+ - 支持 stdio MCP 的客户端,能从 `PATH` 找到 Node/npm。
29
+
30
+ ## 快速开始
31
+
32
+ ### 1. 安装与配对
33
+
34
+ ```sh
35
+ npm install --global @evan_zhong/browser-attach
36
+ browser-attach setup
37
+ ```
38
+
39
+ `setup` 会输出稳定的扩展目录并启动本地 Hub。扩展目录通常是:
40
+
41
+ - Windows:`%USERPROFILE%\.browser-attach\extension`
42
+ - macOS / Linux:`~/.local/share/browser-attach/extension`
43
+
44
+ 打开 `chrome://extensions` 或 `edge://extensions`,启用开发者模式,选择“加载已解压的扩展程序”,加载该目录。每个需要接入的浏览器配置分别加载一次;扩展选项可设置容易辨认的配置名称。
45
+
46
+ 也可以不全局安装:
47
+
48
+ ```sh
49
+ npx --yes --package=@evan_zhong/browser-attach@latest browser-attach setup
50
+ ```
51
+
52
+ ### 2. 配置 MCP 客户端
53
+
54
+ 将以下配置合并到客户端的 MCP 配置中,或参考 [mcp-config.json](mcp-config.json):
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "browser_attach": {
60
+ "command": "npx",
61
+ "args": ["--yes", "--package=@evan_zhong/browser-attach@latest", "browser-attach", "mcp"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ Windows 客户端若要求完整命令扩展名,使用 `npx.cmd`。已全局安装时,也可以配置 `"command": "browser-attach", "args": ["mcp"]`。重启 MCP 客户端后发现工具。
68
+
69
+ `@latest` 会在启动时解析 npm 最新版本。MCP 包与已加载扩展必须为同一版本;升级后重新运行 `browser-attach setup` 并在扩展页重新加载扩展,否则 Hub 会拒绝旧扩展连接。
70
+
71
+ 各宿主的装配步骤见 [Agent 集成](docs/AGENT-INTEGRATION.md);其他客户端直接使用上面的 stdio 配置。
72
+
73
+ ### 3. 开始操作
74
+
75
+ 1. `browser_discover(kind="browsers")` → `browser_discover(kind="tabs")`,确认浏览器、标签页标题和 URL。
76
+ 2. `browser_claim_tab`,用 `discover(kind="tabs")` 的 `tab_id` 接管(可选 `expected_url` / `expected_title` 精确校验),取得本连接的 `tab_handle`。
77
+ 3. `browser_snapshot` 取得元素 `ref`,再执行点击、填写或其他动作。
78
+ 4. 动作后重新观察,结束时调用 `browser_tab_control(action="release")`。
79
+
80
+ 标题易变时,claim 可省略 `expected_title`;`expected_url` 也可选。点击/填写可改用每次重新解析的 `selector`。原生元素点击自动滚动;整页坐标点击使用 `coordinate_space: "document"`。求值结果使用 `cursor` 续读缓存,不会重新运行脚本。设计取舍见 [架构说明](docs/ARCHITECTURE.md#设计取舍)。
81
+
82
+ 接管和普通输入不会主动激活标签或窗口;需要前台渲染时显式调用 `browser_tab_control(action="activate")`。操作结果未知时先重新观察,避免重复提交。完整参数、分页、上传与输入语义见 [使用指南](docs/USAGE.md)。
83
+
84
+ ## 工具
85
+
86
+ | 工具 | 用途 |
87
+ | --- | --- |
88
+ | `browser_discover` | `kind=browsers/tabs`,发现浏览器配置、标签身份及控制状态 |
89
+ | `browser_claim_tab` | 独占接管已发现的标签 |
90
+ | `browser_open_tab` / `browser_tab_control` | 创建标签;显式 `release/activate/close` 释放控制、激活或关闭已接管的标签 |
91
+ | `browser_snapshot` / `browser_read_text` | AX 快照、元素引用和正文 |
92
+ | `browser_wait` | 只读等待元素、文本、URL 或网络空闲 |
93
+ | `browser_click` / `browser_hover` / `browser_drag` / `browser_select_option` / `browser_type` / `browser_fill` / `browser_press_key` / `browser_scroll` | 原生/DOM 输入、指针悬停与拖拽、下拉选择、逐字输入与滚动 |
94
+ | `browser_batch` | 单队列顺序执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,失败即停并返回部分结果 |
95
+ | `browser_navigate` | HTTP(S) 导航与加载等待 |
96
+ | `browser_handle_dialog` | alert、confirm、prompt |
97
+ | `browser_upload_file` | 原生文件 input 或拦截的文件选择器 |
98
+ | `browser_screenshot` | 视口或整页 PNG,附坐标映射 |
99
+ | `browser_evaluate` | 主页面 JS 求值、Promise 等待、有界结果 |
100
+ | `browser_logs` | 用 `kind=console/network` 选择控制台、异常或请求日志,支持过滤和游标续读 |
101
+
102
+ ## 本机数据与升级
103
+
104
+ | 平台 | 默认数据目录 |
105
+ | --- | --- |
106
+ | Windows | `%USERPROFILE%/.browser-attach` |
107
+ | macOS / Linux | `${XDG_DATA_HOME:-~/.local/share}/browser-attach` |
108
+
109
+ 可通过绝对路径 `BROWSER_ATTACH_HOME` 显式覆盖。Windows 默认避开可能被打包宿主虚拟化的 AppData;不会搜索或自动合并其他宿主的私有缓存。目录包含配对配置、Hub 日志及稳定的扩展副本;不依赖 clone 位置、客户端工作目录或 npm 缓存。已有自定义目录请继续使用相同环境变量或 `--config`,不要在新默认目录意外创建另一套配对。
110
+
111
+ 宿主装配步骤写入客户端条目时,会固定当前 Node、CLI 和实际配对文件的绝对路径,并设置 MCP 的 `BROWSER_ATTACH_HOME`;支持与自定义 `--config` 一起使用。MCP 启动 Hub 时继续传入同一绝对配对路径及目录,避免不同启动环境重新推导出另一份配置。详见 [Agent 集成](docs/AGENT-INTEGRATION.md)。
112
+
113
+ ```sh
114
+ browser-attach status
115
+ browser-attach stop
116
+ npm install --global @evan_zhong/browser-attach
117
+ browser-attach setup
118
+ ```
119
+
120
+ 升级后在浏览器扩展管理页重新加载扩展,再重启 MCP 连接。Hub 与扩展版本必须一致。首次使用本项目的新名称时,应停止之前运行的 Hub 并重新配对;默认端口为 `47831`,可用 `setup --port` 修改。
121
+
122
+ ## 架构与使用边界
123
+
124
+ ```text
125
+ MCP 客户端 → Node.js stdio MCP → 127.0.0.1 Hub
126
+ ↕ 本机配对
127
+ Chrome / Edge MV3 扩展
128
+ ↓ chrome.debugger / CDP
129
+ 现有 HTTP(S) 标签页
130
+ ```
131
+
132
+ 每个 MCP 连接拥有独立控制权,句柄不能跨连接使用。断连或停止调试会释放控制。客户端端点要求 Bearer 密钥并拒绝网页 Origin;扩展验证固定 Origin、密钥和版本。`browser_evaluate` 可以使用页面现有登录态读写数据,执行后快照分页失效,原节点 refs 可继续解析;超时或报错前可能已产生作用,不应自动重放。未暴露通用 CDP 工具。
133
+
134
+ 日志从接管开始采集,跨导航保留,释放时清除;每类最多保留 500 条、1,048,576 个 JSON 字符,超限淘汰会报告 `data_loss`。网络日志默认只记录生命周期元数据;显式 capture 才读取之后的 XHR/fetch 有界、脱敏 headers/正文,不改变缓存或拦截流量。完整参数及边界见[使用指南](docs/USAGE.md#诊断日志)。
135
+
136
+ 浏览器扩展具有调试权限,客户端操作可使用现有网站登录态;只应将其接入你信任的 MCP 客户端。网页内容不应被视为用户授权。配对密钥和生成的 `config.generated.js` 不应提交或发布,打包检查会拒绝包含该文件的包。
137
+
138
+ 目前仅支持 Chrome / Edge 的 HTTP(S) 页面。下载管理、网络拦截和 Firefox / WebKit 尚不支持。跨进程 iframe 使用递归 session 路由,覆盖快照、输入、上传和日志;透视变换的 iframe 点击会明确拒绝。原生指针输入要求活动且未暂停的标签及未最小化窗口,焦点和文档可见性只作诊断;不就绪时快速返回未派发及 tab_control(action=activate) 提示;不会自动抢焦点。后台截图可能因浏览器暂停渲染而超时;原生菜单和标签拖动也可能抑制输入。
139
+
140
+ ## 本地开发
141
+
142
+ ```sh
143
+ git clone https://github.com/EvannZhongg/BrowserAttach.git
144
+ cd BrowserAttach
145
+ npm ci
146
+ npm test
147
+ npm run test:package
148
+ ```
149
+
150
+ 直接运行源码并配对仓库中的扩展:
151
+
152
+ ```sh
153
+ node bin/browser-attach.mjs setup --extension-dir ./extension
154
+ node bin/browser-attach.mjs mcp
155
+ ```
156
+
157
+ 开发模式有意使用仓库扩展路径。一般安装请使用不带 `--extension-dir` 的 setup,确保移动仓库后仍可运行。Windows 也可运行 `./scripts/install.ps1`。
158
+
159
+ 真实浏览器测试需要一个允许命令行加载扩展的 Chromium / Edge 可执行文件,路径由开发者传入:
160
+
161
+ ```sh
162
+ npm run test:live -- --executable /path/to/chromium --family chromium
163
+ npm run test:interaction -- --executable /path/to/chromium --family chromium
164
+ npm run test:interaction -- --tab-indicators --executable /path/to/chromium --family chromium
165
+ ```
166
+
167
+ 这些测试使用临时配置和本地 fixture。测试覆盖与限制见 [验证报告](docs/TEST-REPORT.md),实现与设计取舍见 [架构与设计](docs/ARCHITECTURE.md)。
168
+
169
+ ## 参与贡献
170
+
171
+ 欢迎通过 [Issues](https://github.com/EvannZhongg/BrowserAttach/issues) 提交可复现问题,或提交 Pull Request。请附上 Node、系统、浏览器与扩展版本,移除配对密钥和网页隐私数据。具体流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。
172
+
173
+ 本项目采用 [MIT License](LICENSE)。
@@ -0,0 +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,6 +1,6 @@
1
1
  # 架构与设计边界
2
2
 
3
- 当前实现为 **0.11.1**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
3
+ 当前实现为 **0.11.2**。本文说明 BrowserAttach 的组件、控制模型与设计取舍;工具参数见 [使用指南](USAGE.md)。
4
4
 
5
5
  ## 组件与数据流
6
6
 
@@ -88,7 +88,7 @@ claim 与普通输入都不激活标签或窗口,也不移动系统鼠标;
88
88
  - **OOPIF 坐标**:点击从顶层派发。逐层用 `DOM.getBoxModel` 把坐标换算到祖先文档,并用 `DOM.getNodeForLocation` 确认该位置确实落在 frame owner 上,防止祖先页面的遮挡被跳过。
89
89
  - **DOM 模式**:`mode: "dom"` 是显式的、受控的降级路径,用于原生菜单或标签拖动暂时捕获输入时操作普通 HTML 控件。它调用 `HTMLElement.click()` 或原生 value setter,事件非可信、无用户激活,且绝不会在原生失败后自动启用。
90
90
  - **对话框策略**:ask 仍为默认;单操作/单步骤的显式 accept/dismiss 在事件回调中解除 JS 模态阻塞,保留消息与处理结果,最多自动处理 10 个。beforeunload dismiss 快速取消本次导航等待,实际主文档变化仍会停止 batch。
91
- - **未知结果探测**:在可能写入的操作准备期安装目标 frame 的临时 MutationObserver,仅在不确定失败后做一次有界只读探测,回传对话框、导航、DOM 变动。观察不到变化不等于没有派发,不将证据升级成业务成功,不自动重放。
91
+ - **未知结果探测**:在可能写入的操作准备期安装目标 frame 的临时 MutationObserver,仅在不确定失败后做一次有界只读探测,回传对话框、导航、DOM 变动。assessment 为 observable_effects 或 no_observable_effects;观察不到变化不等于没有派发,不将证据升级成业务成功,不自动重放。
92
92
  - **元素截图/求值 ref**:复用既有截图 clip/元数据与 ref 注册/失效模型,分别增加元素 border box 截图和显式单 DOM Element 结果;不暴露 Runtime objectId。JSON 求值自动调用函数一次并等待 Promise,缓存续读不执行脚本。
93
93
  - **历史导航**:在已有 navigate 中选择 URL 或 back/forward,按 history entry 与主 frame 生命周期等待;同文档/BFCache 恢复不等新的加载事件,不用 evaluate 重放导航。
94
94
  - **不做通用 CDP 透传**:`browser_evaluate` 是页面表达式 API,不是 CDP 通道;没有暴露任意 domain/method 的工具。
@@ -1,11 +1,15 @@
1
1
  # 验证报告
2
2
 
3
- 当前版本 **0.11.1**(2026-10-22)。验证使用本地 fixture 和临时 profile,链路为扩展 → 本机 Hub → stdio MCP。
3
+ 当前版本 **0.11.2**(2026-10-22)。验证使用本地 fixture 和临时 profile,链路为扩展 → 本机 Hub → stdio MCP。
4
4
 
5
5
  ## 当前版本
6
6
 
7
- - `npm test`:141 项通过。覆盖独占控制、快照/ref 生命周期、严格 selector、原生/DOM 输入、逐字输入、hover、pointer/HTML5 拖拽、条件等待、HTML select、batch、历史导航、OOPIF、日志、上传、截图、对话框和未知结果不重放。
8
- - `npm run test:package`:仓库外含空格路径安装运行通过,22 个 MCP 工具、44 个发布文件;包含 `drag.js` / `wait.js` / `scroll.js` / `outcomes.js`,不包含配对密钥。版本为 0.11.1,示例配置使用 `@latest`。
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 默认行为后的可信回执、滚动边界后延迟增长和歧义候选。
10
+
11
+ ## 升级前的浏览器验证
12
+
9
13
  - Edge 155 headless `--primitives`:10 个场景通过,包含 hover 停留后点击子项、一次快照的 ref 连续输入并跨只读 evaluate 保持有效、替换节点拒绝、label/value/index 下拉选择、三种等待失败、pointer/HTML5 拖拽和目的元素滚入视口。
10
14
  - Edge 155 完整 stdio MCP 集成:192 个调用通过,包含嵌套 OOPIF、原生/DOM 输入、遮挡、上传、日志、按键、滚动与导航;扩展报告 `extension_version=0.10.0`。
11
15
  - Chromium 156、Edge 155 headless `--extended`:各 12 个场景通过,包含 ref 生命周期、选择框、拖拽、等待、历史导航和 OOPIF。
package/docs/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # BrowserAttach 使用指南
2
2
 
3
- 适用于 **0.11.1**:Node.js 22+、Chrome / Edge 125+、22 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
3
+ 适用于 **0.11.2**:Node.js 22+、Chrome / Edge 125+、22 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
4
4
 
5
5
  工具发现采用三层说明:初始化 `instructions` 提供跨工具规则,各工具 description 和参数 schema 说明自身行为与约束,本文保留完整边界和示例。客户端集成时应把 MCP 初始化响应中的 instructions 传入模型上下文;仅读取 tools/list 不会包含完整的通用调用规则。
6
6
 
@@ -130,7 +130,7 @@ ref 每次用 frame/session + backendNodeId 重新解析,并以局部 AX 查
130
130
 
131
131
  hint 仅为 HTML/ARIA 启发式判断,false 表示未找到候选,不能证明元素不可点击,也不能覆盖所有事件代理。它不会替换真正的鼠标命中节点或改变派发目标。坐标点击无法推断调用者的业务意图,DOM/网络变化也不能证明业务成功,需按具体任务观察。
132
132
 
133
- `selector` 可复用,但不是永久元素身份。输入准备阶段会等待匹配出现,受同一 timeout_ms 限制;省略 nth 时多个匹配立即报错。显式 nth 按当前文档顺序选择,尚未出现该索引时等待;行顺序变化后须重新确认目标。解析后元素被替换会安全失败,不自动重放。CSS/XPath 不穿透 Shadow DOM,那里继续使用 AX ref;iframe 用明确 `frame_id`。例如 `browser_fill({tab_handle, selector:'input[placeholder="公司名称"]', nth:1, text:"第二段经历", mode:"native"})`;XPath 同样支持 nth。
133
+ `selector` 可复用,但不是永久元素身份。输入准备阶段会等待匹配出现,受同一 timeout_ms 限制;省略 nth 时多个匹配立即报错,附带前 10 个候选的 nth、tag、最多 120 字符的 DOM 文本、disabled 和 visible,超出部分标明省略数量。显式 nth 按当前文档顺序选择,尚未出现该索引时等待;行顺序变化后须重新确认目标。解析后元素被替换会安全失败,不自动重放。CSS/XPath 不穿透 Shadow DOM,那里继续使用 AX ref;iframe 用明确 `frame_id`。例如 `browser_fill({tab_handle, selector:'input[placeholder="公司名称"]', nth:1, text:"第二段经历", mode:"native"})`;XPath 同样支持 nth。
134
134
 
135
135
  坐标命中 CSS 伪元素时,以其 originating element 做描述和事件校验,`hit_target.pseudo_element=true`。截图坐标仍按截图元数据换算;优先用 ref/selector 避免布局漂移。
136
136
 
@@ -154,7 +154,7 @@ drag 的 source / destination 各自接受 ref、严格 selector(可带 frame_
154
154
  {"tab_handle":"...","source":{"selector":"#card"},"destination":{"selector":"#column"},"mode":"html5","steps":20}
155
155
  ```
156
156
 
157
- pointer 发送 mousePressed → 多步 mouseMoved → mouseReleased;HTML5 模式先通过真实按下/移动触发 dragstart 并截获其 DragData,再派发 dragEnter/dragOver/drop,核验可信 dragstart/drop/dragend。不会凭空构造拖拽数据,不自动切换模式。目的元素在按住后调用已有滚入视口逻辑,包含嵌套容器,然后重新命中检查;这是目标驱动的滚动,不是持续的视口边缘滚动任务。固定坐标始终受视口边界约束,视口外坐标用 document 或改用元素目标。
157
+ pointer 发送 mousePressed → 多步 mouseMoved → mouseReleased,核验可信 pointerdown/pointermove/pointerup,因此控件取消 pointerdown 的默认行为、抑制兼容鼠标事件时仍可校验送达;HTML5 模式先通过真实按下/移动触发 dragstart 并截获其 DragData,再派发 dragEnter/dragOver/drop,核验可信 dragstart/drop/dragend。不会凭空构造拖拽数据,不自动切换模式。目的元素在按住后调用已有滚入视口逻辑,包含嵌套容器,然后重新命中检查;这是目标驱动的滚动,不是持续的视口边缘滚动任务。固定坐标始终受视口边界约束,视口外坐标用 document 或改用元素目标。
158
158
 
159
159
  成功返回 events_trusted=true、delivery_verified=true、application_result=not_checked;不保证应用已保存排序。任一步失败、主文档/相关 frame 变化或接管结束即停止,尝试取消/释放本次手势;不回滚、不重放。HTML5 CDP 路径仍受浏览器和站点原生拖拽支持限制。
160
160
 
@@ -244,7 +244,7 @@ timeout_ms 默认 15000,范围 100–30000,是整批准备、派发和校验
244
244
  {"tab_handle":"...","selector":"#list","until":{"text":"Row 0487","selector":"button"},"delta_y":200,"max_steps":100,"timeout_ms":15000}
245
245
  ```
246
246
 
247
- until 是显式 DOM 滚动搜索,不与 wheel 组合;省略 delta_y 时每次滚约 0.8 个容器高度。max_steps 默认 100、范围 1–500。搜索容器内渲染且有尺寸的最深匹配元素,文本为子串;重复匹配报错,使用 until.selector 消歧。每次滚动后有界轮询,导航、相关 frame 变化、边界、步数或超时即停止,不自动点击;返回 found/ref/frame_id/scroll_steps。滚动本身会改变页面状态,即使未找到也可能已触发加载。它不承诺读到服务端未提供的数据,也不把虚拟列表未渲染行当作已存在。batch 的 scroll 步骤使用同一实现。
247
+ until 是显式 DOM 滚动搜索,不与 wheel 组合;省略 delta_y 时每次滚约 0.8 个容器高度。max_steps 默认 100、范围 1–500,只计算实际 scrollBy 次数。搜索容器内渲染且有尺寸的最深匹配元素,文本为子串;重复匹配报错,使用 until.selector 消歧。滚动无位移时继续只读查找并观察容器位置、内容尺寸与视口尺寸,状态变化后才继续滚动;边界本身不证明内容已加载完毕。导航、相关 frame 变化、步数用尽或总预算超时即停止,不自动点击;返回 found/ref/frame_id/scroll_steps。未找到只表示本次有界搜索未命中,内容仍可能稍后加载。滚动本身会改变页面状态,即使未找到也可能已触发加载。它不承诺读到服务端未提供的数据,也不把虚拟列表未渲染行当作已存在。batch 的 scroll 步骤使用同一实现。
248
248
 
249
249
  ### 结果未知时
250
250
 
@@ -369,4 +369,4 @@ handle_dialog 返回原始 `dialog.type/message/default_prompt` 和 `accepted`
369
369
  - 后台操作要求浏览器运行且页面未冻结;网站自己弹窗或权限 UI 仍可能改变焦点。
370
370
  - `target_verified`、`delivery_verified`、`value_verified` 只证明事件送达或文本写入,不证明提交、付款、服务端上传等业务完成。
371
371
  - Hub 日志位于配对文件同目录的 `hub.log`;`browser-attach status` 查看连接状态,`browser-attach stop` 停止 Hub。
372
- - 宿主把大结果保存为 Artifact 时,长行可能被宿主截断;这是宿主读取行为,不是快照游标分页。相关说明见 [Nosis 集成](NOSIS-INTEGRATION.md)。
372
+ - 宿主把大结果保存为 Artifact 时,长行可能被宿主截断;这是宿主读取行为,不是快照游标分页。相关说明见 [Agent 集成](AGENT-INTEGRATION.md)。
@@ -12,7 +12,7 @@ import { createDiagnostics } from './diagnostics.js';
12
12
  import { createSessions } from './sessions.js';
13
13
  import { NAVIGATION_OBSERVATION_TIMEOUT_MS } from './limits.js';
14
14
  import { deadline } from './actionability.js';
15
- import { scrollOptions, scrollElement, findScrollText } from './scroll.js';
15
+ import { scrollOptions, scrollElement, findScrollText, scrollState } from './scroll.js';
16
16
  import { validateSteps } from './batch.js';
17
17
  import { waitFor } from './wait.js';
18
18
  import { boundedRead, installOutcomeObserver } from './outcomes.js';
@@ -566,7 +566,7 @@ export function createController(api, notify = () => {}) {
566
566
  const root = node || await resolveSelector(cdp, value, sessions, tabId, { selector: 'html' }, args.budget);
567
567
  const claim = claims.get(tabId), version = documents.get(tabId), frameVersion = frameStates.get(tabId)?.versions.get(root.frameId);
568
568
  let last;
569
- for (let step = 0; step <= maxSteps; step++) {
569
+ for (let step = 0; step <= maxSteps;) {
570
570
  if (claims.get(tabId) !== claim || documents.get(tabId) !== version || frameStates.get(tabId)?.versions.get(root.frameId) !== frameVersion) throw new Error('Document/frame/control changed during scroll search; observe before continuing');
571
571
  const found = await args.budget.run(() => cdp(root.target, 'Runtime.callFunctionOn', { objectId: root.objectId,
572
572
  functionDeclaration: findScrollText.toString(), arguments: [{ value: text }, { value: selector }], objectGroup: 'browser-attach-search' }));
@@ -576,13 +576,21 @@ export function createController(api, notify = () => {}) {
576
576
  await args.budget.run(() => cdp(root.target, 'Runtime.releaseObjectGroup', { objectGroup: 'browser-attach-search' }));
577
577
  return { found: true, ...result, scroll_steps: step, application_result: 'not_checked', observe_again: true };
578
578
  }
579
- if (step === maxSteps || last?.scrolled === false) break;
579
+ if (step === maxSteps) break;
580
+ if (last?.scrolled === false) {
581
+ // A boundary may precede lazy loading. Observe without repeating a
582
+ // no-op scroll until content grows, a match appears, or time runs out.
583
+ const current = value(await args.budget.run(() => cdp(root.target, 'Runtime.callFunctionOn', { objectId: root.objectId,
584
+ functionDeclaration: scrollState.toString(), returnByValue: true })));
585
+ if (JSON.stringify(current) === JSON.stringify(last.after)) { await args.budget.run(() => new Promise(resolve => setTimeout(resolve, 50))); continue; }
586
+ }
580
587
  args.budget.dispatch();
581
588
  last = value(await args.budget.run(() => cdp(root.target, 'Runtime.callFunctionOn', { objectId: root.objectId,
582
589
  functionDeclaration: scrollElement.toString(), arguments: [{ value: args.delta_x ?? 0 }, { value: args.delta_y ?? null }], returnByValue: true })));
590
+ step++;
583
591
  await args.budget.run(() => new Promise(resolve => setTimeout(resolve, 50)));
584
592
  }
585
- throw new Error(`Scroll search did not find ${JSON.stringify(text)} within ${maxSteps} steps or reached the scroll boundary; current=${JSON.stringify(last)}. Scrolling may already have occurred.`);
593
+ throw new Error(`Scroll search did not find ${JSON.stringify(text)} within ${maxSteps} steps; current=${JSON.stringify(last)}. Content may still load later. Scrolling may already have occurred.`);
586
594
  }
587
595
  if (command === 'scroll') {
588
596
  const mode = args.mode ?? 'dom';
@@ -648,7 +656,7 @@ export function createController(api, notify = () => {}) {
648
656
  error.post_action = { delivery: 'unknown', frame_id: observationFrame, frame_navigation_observed: frameChanged, dialog: dialogs.get(tabId) || null, dialogs: state.messages,
649
657
  navigation_observed: documents.get(tabId) !== version || !!current && current.url !== startTab.url,
650
658
  current_url: current?.url ?? null, dom_changed: dom?.dom_changed ?? null,
651
- assessment: dialogs.has(tabId) || state.messages.length || documents.get(tabId) !== version || frameChanged || dom?.dom_changed ? 'observable_effects' : 'no_effect_confirmed',
659
+ assessment: dialogs.has(tabId) || state.messages.length || documents.get(tabId) !== version || frameChanged || dom?.dom_changed ? 'observable_effects' : 'no_observable_effects',
652
660
  note: 'These observations do not prove business success or that an unchanged page received no input. Do not replay automatically.' };
653
661
  error.message += ` Post-action observation: ${JSON.stringify(error.post_action)}`;
654
662
  } else if (state.messages.length) error.message += ` Dialogs: ${JSON.stringify(state.messages)}`;
package/extension/drag.js CHANGED
@@ -27,7 +27,7 @@ export function createDrag(cdp, value, mouse) {
27
27
  const { type, rejected } = JSON.parse(params.payload);
28
28
  if (rejected) { operation.error = new Error('Drag target changed during dispatch; outcome is unknown. Observe before continuing.'); operation.reject(operation.error); }
29
29
  operation.seen.add(type);
30
- const required = operation.html5 ? ['mousedown', 'dragstart', 'drop', 'dragend'] : ['mousedown', 'mousemove', 'mouseup'];
30
+ const required = operation.html5 ? ['mousedown', 'dragstart', 'drop', 'dragend'] : ['pointerdown', 'pointermove', 'pointerup'];
31
31
  if (required.every(type => operation.seen.has(type))) operation.resolve();
32
32
  } catch (error) { operation.reject(error); }
33
33
  }
@@ -117,18 +117,21 @@ export function createDrag(cdp, value, mouse) {
117
117
 
118
118
  function installDragReceipt(name, stage, html5, expiryMs) {
119
119
  const target = this, win = target.ownerDocument.defaultView, report = win[name];
120
- const types = stage === 'source' ? ['mousedown', 'mousemove', 'mouseup', 'dragstart', 'dragend']
121
- : stage === 'source_boundary' ? ['mousedown'] : ['drop', 'mouseup'];
120
+ // Cancelling pointerdown suppresses compatibility mouse down/up, but does
121
+ // not prevent a pointer-based control from receiving the drag.
122
+ const down = html5 ? 'mousedown' : 'pointerdown';
123
+ const types = stage === 'source' ? (html5 ? ['mousedown', 'dragstart', 'dragend'] : ['pointerdown', 'pointermove', 'pointerup'])
124
+ : stage === 'source_boundary' ? [down] : (html5 ? ['drop'] : ['pointerup']);
122
125
  const listeners = [];
123
126
  function listener(event, expected) {
124
127
  if (!event.isTrusted) return;
125
128
  const hit = event.composedPath().includes(expected);
126
- if ((event.type === 'mousedown' || event.type === 'drop') && (!hit || !target.isConnected)) {
129
+ if ((event.type === down || event.type === 'drop') && (!hit || !target.isConnected)) {
127
130
  event.preventDefault(); event.stopImmediatePropagation(); report(JSON.stringify({ rejected: true })); return;
128
131
  }
129
132
  if (stage.endsWith('_boundary') || expected !== target) return;
130
133
  if (['dragstart', 'dragend'].includes(event.type) && !hit) return;
131
- if (event.type === 'mousemove' && event.buttons !== 1) return;
134
+ if (event.type === 'pointermove' && event.buttons !== 1) return;
132
135
  if (html5 && !['mousedown', 'dragstart', 'drop', 'dragend'].includes(event.type)) return;
133
136
  report(JSON.stringify({ type: event.type }));
134
137
  }
@@ -148,7 +151,7 @@ function installDragReceipt(name, stage, html5, expiryMs) {
148
151
  }
149
152
  // Pointer capture and movement can leave the source's closed shadow root.
150
153
  if (stage === 'source' && target.getRootNode().host && !html5) {
151
- const callback = event => listener(event, target), types = ['mousemove', 'mouseup'];
154
+ const callback = event => listener(event, target), types = ['pointermove', 'pointerup'];
152
155
  listeners.push({ receiver: win, callback, types });
153
156
  for (const type of types) win.addEventListener(type, callback, { capture: true, passive: false });
154
157
  }
@@ -17,17 +17,28 @@ export function targetOptions(args, { coordinates = false } = {}) {
17
17
 
18
18
  export function queryElement(selector, type = 'css', nth, waitForMatch = false) {
19
19
  if (nth != null && (!Number.isSafeInteger(nth) || nth < 0)) throw new Error('nth must be a nonnegative safe integer (zero-based)');
20
- let element, count;
20
+ let element, count, at;
21
21
  if (type === 'xpath') {
22
22
  const result = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
23
- count = result.snapshotLength; element = result.snapshotItem(nth ?? 0);
23
+ count = result.snapshotLength; at = index => result.snapshotItem(index);
24
24
  } else {
25
25
  const elements = document.querySelectorAll(selector);
26
- count = elements.length; element = elements[nth ?? 0];
26
+ count = elements.length; at = index => elements[index];
27
27
  }
28
28
  if (waitForMatch && (count === 0 || (nth != null && nth >= count))) return null;
29
- if (nth == null && count !== 1) throw new Error(`Selector must match exactly one element; matched ${count}. Scope the selector or provide an explicit zero-based nth.`);
29
+ if (nth == null && count !== 1) {
30
+ const candidates = Array.from({ length: Math.min(count, 10) }, (_, nth) => {
31
+ const node = at(nth), text = (node.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120);
32
+ if (node.nodeType !== 1) return { nth, node_type: node.nodeType, text };
33
+ const rect = node.getBoundingClientRect(), style = node.ownerDocument.defaultView.getComputedStyle(node);
34
+ return { nth, tag: node.tagName.toLowerCase(), text,
35
+ disabled: node.matches(':disabled') || !!node.closest('[inert], [aria-disabled="true"]'),
36
+ visible: rect.width > 0 && rect.height > 0 && style.visibility === 'visible' };
37
+ });
38
+ throw new Error(`Selector must match exactly one element; matched ${count}. Scope the selector or provide an explicit zero-based nth. Candidates: ${JSON.stringify(candidates)}${count > candidates.length ? ` (${count - candidates.length} more omitted)` : ''}`);
39
+ }
30
40
  if (nth != null && nth >= count) throw new Error(`Selector nth=${nth} is out of range; matched ${count}`);
41
+ element = at(nth ?? 0);
31
42
  if (element.nodeType !== 1) throw new Error('Selector must resolve to an element');
32
43
  return element;
33
44
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "BrowserAttach",
4
- "version": "0.11.1",
4
+ "version": "0.11.2",
5
5
  "description": "Connect existing Chrome and Edge tabs to a locally paired MCP client.",
6
6
  "minimum_chrome_version": "125",
7
7
  "permissions": [
@@ -14,10 +14,17 @@ export function scrollElement(dx, dy) {
14
14
  if (!this.isConnected) throw new Error('Scroll container is detached');
15
15
  const before = { x: this.scrollLeft, y: this.scrollTop };
16
16
  this.scrollBy({ left: dx, top: dy ?? Math.max(1, this.clientHeight * 0.8), behavior: 'instant' });
17
- const after = { x: this.scrollLeft, y: this.scrollTop };
17
+ const after = { x: this.scrollLeft, y: this.scrollTop, width: this.scrollWidth, height: this.scrollHeight,
18
+ client_width: this.clientWidth, client_height: this.clientHeight };
18
19
  return { scrolled: before.x !== after.x || before.y !== after.y, before, after, method: 'dom_scroll', scope: 'element' };
19
20
  }
20
21
 
22
+ export function scrollState() {
23
+ if (!this.isConnected) throw new Error('Scroll container is detached');
24
+ return { x: this.scrollLeft, y: this.scrollTop, width: this.scrollWidth, height: this.scrollHeight,
25
+ client_width: this.clientWidth, client_height: this.clientHeight };
26
+ }
27
+
21
28
  export function findScrollText(text, selector) {
22
29
  if (!this.isConnected) throw new Error('Scroll container detached');
23
30
  const elements = Array.from(this.querySelectorAll(selector || '*'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evan_zhong/browser-attach",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "MCP for existing signed-in Chrome and Edge tabs through a locally paired extension",
5
5
  "type": "module",
6
6
  "license": "MIT",
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.11.1';
18
+ export const VERSION = '0.11.2';
19
19
 
20
20
  export async function loadConfig(path = DEFAULT_CONFIG) {
21
21
  let config;
package/src/mcp.mjs CHANGED
@@ -149,7 +149,7 @@ export function createMcp(client) {
149
149
  ])).min(1).max(20),
150
150
  });
151
151
  tool('press_key', 'Send a native key/chord to current focus and verify keydown delivery. Supports F1-F12, navigation/editing keys and US ASCII punctuation. Examples: Control+A, Shift+/, Control++, Plus.', { ...handle, ...inputTimeout, key: z.string() });
152
- tool('scroll', 'Scroll an explicit element container or at viewport CSS x/y (default center); positive delta_y is down. DOM mode uses scrollBy without wheel events; wheel mode sends a native wheel for custom handlers/OOPIFs. until.text performs a bounded DOM scroll search and returns a fresh actionable ref; max_steps bounds the search. Wheel acknowledgement does not prove scrolling.', {
152
+ tool('scroll', 'Scroll an explicit element container or at viewport CSS x/y (default center); positive delta_y is down. DOM mode uses scrollBy without wheel events; wheel mode sends a native wheel for custom handlers/OOPIFs. until.text performs a bounded DOM search and returns a fresh ref; at an unchanged boundary it observes until content changes or timeout. max_steps bounds scroll calls. A search miss does not prove content will never load. Wheel acknowledgement does not prove scrolling.', {
153
153
  ...handle, ...inputTimeout, ...target, ...scrollFields,
154
154
  }, null, args => { scrollOptions(args); if (args.ref != null || args.selector != null || args.frame || args.frame_id) targetOptions(args); return args; });
155
155
  tool('navigate', 'Navigate to an HTTP(S) URL or back/forward history entry, wait until wait_until, and return a fresh snapshot by default. Observation has a separate 10s deadline with one transient-error retry; observation_error preserves navigation success.', {
@@ -1,53 +0,0 @@
1
- # Nosis 集成
2
-
3
- BrowserAttach 是标准 stdio MCP,也可以通过 `setup --nosis` 直接写入 Nosis 的用户配置。
4
-
5
- ## 写入内容
6
-
7
- ```sh
8
- browser-attach setup --nosis
9
- ```
10
-
11
- 该命令在完成普通配对后,备份并更新 `~/.nosis/agent_config.json`(备份名为 `agent_config.json.browser-attach-<时间戳>.bak`),写入:
12
-
13
- | 字段 | 值 |
14
- |---|---|
15
- | 服务器名 | `browser_attach` |
16
- | `command` | 执行 setup 的 Node 可执行文件绝对路径 |
17
- | `args` | 当前包 `bin/browser-attach.mjs` 绝对路径、`mcp`、`--config <绝对配对路径>` |
18
- | `env.BROWSER_ATTACH_HOME` | 配对文件所在目录 |
19
- | `startup_timeout_seconds` / `call_timeout_seconds` | `20` / `60` |
20
- | `tools.approval` | `always` |
21
-
22
- 同时会设置 `mcp.enabled = true`。命令与配对路径都固定为绝对路径,因此本机安装位置变化后需要重新执行 `setup --nosis`;请从长期保留的位置安装,而不是临时解压目录。
23
-
24
- 重启或重建 Nosis Runtime 后重新发现工具。
25
-
26
- ## 自定义运行目录
27
-
28
- Windows 默认目录是 `%USERPROFILE%/.browser-attach`,避开打包应用与普通宿主可能重定向到不同实际文件的 AppData;macOS / Linux 使用 XDG 用户数据目录。`BROWSER_ATTACH_HOME` 必须是绝对目录。
29
-
30
- 已有可共享目录可直接沿用:
31
-
32
- ```sh
33
- browser-attach setup --config /absolute/shared-directory/config.json --nosis
34
- ```
35
-
36
- setup 会把解析后的配对路径写进配置,MCP 与 Hub 后续都显式传递同一路径,不会在不同启动环境中重新推导出第二套密钥。setup 不会搜索或自动迁移其他应用的私有缓存;已有配对请显式保留原目录。
37
-
38
- ## 验证
39
-
40
- ```sh
41
- python scripts/verify-nosis.py --nosis-root /path/to/Nosis --family chrome
42
- python scripts/verify-nosis.py --nosis-root /path/to/Nosis --family edge
43
- ```
44
-
45
- 脚本读取本机 Nosis 配置,通过 Nosis ToolCatalog 发现 22 个工具,覆盖发现、标签控制、页面操作和结果封装,其中 `browser_logs` 分别覆盖 console/network,验证结果封装、截图到模型消息的图片适配以及 `isError` 后连接仍可用。它用 `browser_open_tab` 在选定配置中创建本地 fixture;同一 family 有多个 profile 时用 `--browser-id <精确 browser_id>` 指定,结束后释放控制。Python 仅用于该测试,不是运行依赖。
46
-
47
- ## 故障排查
48
-
49
- - 工具结果走 MCP 文本内容,截图走标准图片内容;能否进入模型取决于宿主的图片适配、大小预算与模型能力。
50
- - 失效句柄不能跨连接复用,错误后应重新 list + claim。
51
- - 文件上传只在浏览器本机读取路径,内容不经过 Hub 或模型。
52
- - 宿主把大结果保存为 Artifact 时,嵌套 JSON 长行可能被宿主的读取工具截断;这是宿主的分页行为,不能靠调大 Attach 的预算解决,也不等同于快照游标分页。
53
- - `Connection closed` 时依次检查:宿主的 stderr、配对文件是否可见、`hub.log`、以及 Hub 与扩展的版本是否一致(版本不一致会直接拒绝连接)。