@evan_zhong/browser-attach 0.0.0-stage → 0.10.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/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +169 -3
- package/bin/browser-attach.mjs +43 -0
- package/docs/ARCHITECTURE.md +129 -0
- package/docs/NOSIS-INTEGRATION.md +53 -0
- package/docs/TEST-REPORT.md +39 -0
- package/docs/USAGE.md +370 -0
- package/extension/actionability.js +66 -0
- package/extension/background.js +98 -0
- package/extension/batch.js +32 -0
- package/extension/click-visuals.js +82 -0
- package/extension/click.js +308 -0
- package/extension/controller.js +812 -0
- package/extension/diagnostics.js +205 -0
- package/extension/drag.js +163 -0
- package/extension/evaluate.js +131 -0
- package/extension/icons/BrowserAttach.png +0 -0
- package/extension/input.js +318 -0
- package/extension/limits.js +6 -0
- package/extension/locator.js +115 -0
- package/extension/manifest.json +34 -0
- package/extension/navigation.js +65 -0
- package/extension/options.html +6 -0
- package/extension/options.js +15 -0
- package/extension/outcomes.js +15 -0
- package/extension/popup.html +5 -0
- package/extension/popup.js +10 -0
- package/extension/scroll.js +29 -0
- package/extension/sessions.js +100 -0
- package/extension/snapshot.js +75 -0
- package/extension/text.js +37 -0
- package/extension/ui.css +12 -0
- package/extension/visuals.js +93 -0
- package/extension/wait.js +81 -0
- package/mcp-config.json +8 -0
- package/package.json +54 -6
- package/scripts/check-package.mjs +20 -0
- package/src/client.mjs +91 -0
- package/src/config.mjs +36 -0
- package/src/hub.mjs +197 -0
- package/src/mcp.mjs +167 -0
- package/src/setup.mjs +59 -0
- package/src/upload.mjs +15 -0
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# BrowserAttach 使用指南
|
|
2
|
+
|
|
3
|
+
适用于 **0.10.1**:Node.js 22+、Chrome / Edge 125+、25 个 MCP 工具。安装、配对与升级见 [README](../README.md);内部实现与安全模型见 [架构说明](ARCHITECTURE.md)。
|
|
4
|
+
|
|
5
|
+
工具发现采用三层说明:初始化 `instructions` 提供跨工具规则,各工具 description 和参数 schema 说明自身行为与约束,本文保留完整边界和示例。客户端集成时应把 MCP 初始化响应中的 instructions 传入模型上下文;仅读取 tools/list 不会包含完整的通用调用规则。
|
|
6
|
+
|
|
7
|
+
## 基本流程
|
|
8
|
+
|
|
9
|
+
1. `browser_list_browsers` 取得 `browser_id`、浏览器族、版本和能力表。
|
|
10
|
+
2. `browser_list_tabs(browser_id)` 取得目标标签的 `tab_id`、精确标题 / URL 与控制状态。
|
|
11
|
+
3. `browser_claim_tab(...)` 独占接管,拿到本连接的 `tab_handle`。
|
|
12
|
+
4. `browser_snapshot(tab_handle)` 读取语义树与元素 `ref`。
|
|
13
|
+
5. 使用 ref 或 selector 执行动作;原节点仍存在时 ref 可复用,需要新信息时再观察。
|
|
14
|
+
6. `browser_release_tab(tab_handle)` 释放控制,页面保持打开。
|
|
15
|
+
|
|
16
|
+
`tab_id` 必须来自本连接最近一次 `list_tabs`。`expected_url` / `expected_title` 是可选的精确校验;提供时须与列表及接管时的实际标签逐字符一致。标题易变时只传 `expected_url`,或明确按 `tab_id` 接管并检查返回的当前身份。同一标签只允许一个 owner,`tab_handle` 不能跨连接使用。
|
|
17
|
+
|
|
18
|
+
## 工具速查
|
|
19
|
+
|
|
20
|
+
### 发现与标签管理
|
|
21
|
+
|
|
22
|
+
| 工具 | 主要参数 | 说明 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `browser_list_browsers` | — | 已连接的浏览器配置、`browser_id`、能力表 |
|
|
25
|
+
| `browser_list_tabs` | `browser_id` | `tab_id`、标题、URL、`active`、`group_id`、`controllable`、控制状态 |
|
|
26
|
+
| `browser_open_tab` | `browser_id`, `url`, `active=false` | 仅 HTTP(S);默认后台打开,返回 `tab_id`,加载后 list + claim |
|
|
27
|
+
| `browser_claim_tab` | `browser_id`, `tab_id`, 可选 `expected_url` / `expected_title` | 按列出的 ID 接管,可显式加身份校验 |
|
|
28
|
+
| `browser_release_tab` | `tab_handle` | 解除调试并释放控制,不关闭页面 |
|
|
29
|
+
| `browser_activate_tab` | `tab_handle`, `focus_window=true` | 显式选中标签并聚焦窗口;`false` 只选中标签 |
|
|
30
|
+
| `browser_close_tab` | `tab_handle` | 关闭标签并作废句柄,可能丢失未保存状态 |
|
|
31
|
+
|
|
32
|
+
### 观察
|
|
33
|
+
|
|
34
|
+
| 工具 | 主要参数 | 说明 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `browser_snapshot` | `max_nodes=600`, `max_chars=24000`, `cursor` | AX 语义树、元素 ref、frame 覆盖、待处理文件选择器 |
|
|
37
|
+
| `browser_wait` | `condition`, `selector`, `expected` / `previous`, `idle_ms=500`, `timeout_ms=5000` | 只读等待元素、文本、URL 或被动网络空闲 |
|
|
38
|
+
| `browser_read_text` | `mode`, `selector`, `offset=0`, `max_chars=20000` | 主 frame 的 DOM、字段值或支持的编辑器模型 |
|
|
39
|
+
|
|
40
|
+
### 输入
|
|
41
|
+
|
|
42
|
+
| 工具 | 主要参数 | 说明 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `browser_click` | `ref` / `selector` / `x`+`y`、`coordinate_space`、`button`、`click_count`、`modifiers`、`mode`、`timeout_ms` | 原生可信点击或显式 DOM 点击 |
|
|
45
|
+
| `browser_hover` | `ref` / `selector` / `x`+`y`, `coordinate_space`, `timeout_ms` | 只移动并保留原生指针,核验可信 mousemove |
|
|
46
|
+
| `browser_drag` | `source`, `destination`, `mode=pointer`, `steps=20`, `timeout_ms` | 原生指针或 HTML5 拖拽,目的节点自动滚入视口 |
|
|
47
|
+
| `browser_select_option` | `ref` / `selector`,`label` / `value` / `index` 三选一 | 设置一个 HTML select 选项,明确事件非可信 |
|
|
48
|
+
| `browser_type` | `ref` / `selector`, `text`, `delay_ms=0`, `timeout_ms` | 保留现有文本与选区,逐字符可信按键输入 |
|
|
49
|
+
| `browser_fill` | `ref` / `selector`, `text`, `mode`, `timeout_ms` | 文本 / textarea / contenteditable,写入后核验文本 |
|
|
50
|
+
| `browser_press_key` | `key`, `timeout_ms` | 单键或组合键,核验可信 keydown 送达 |
|
|
51
|
+
| `browser_batch` | `steps`, `timeout_ms=15000` | 单队列内顺序执行 1–20 步 click/fill/type/press_key/hover/scroll/select_option/wait,失败即停并返回部分结果 |
|
|
52
|
+
| `browser_scroll` | 可选目标、`delta_y`、`until.text`、`max_steps` | 容器/坐标滚动;有界查找文本并返回新 ref |
|
|
53
|
+
|
|
54
|
+
### 页面、文件与诊断
|
|
55
|
+
|
|
56
|
+
| 工具 | 主要参数 | 说明 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `browser_navigate` | `url` / `direction=back/forward`, `wait_until=domcontentloaded`, `timeout_ms=15000`, `include_snapshot=true` | 等待并返回新文档标题、URL、快照 |
|
|
59
|
+
| `browser_handle_dialog` | `accept`, `prompt_text` | alert / confirm / prompt |
|
|
60
|
+
| `browser_upload_file` | `files`, `ref`(可省略) | 原生 file input、隐藏 input 或已拦截的选择器 |
|
|
61
|
+
| `browser_screenshot` | `full_page=false`, `include_indicators=false` | viewport/full_page/元素 PNG + 坐标映射 |
|
|
62
|
+
| `browser_evaluate` | `expression`, `timeout_ms=10000`, `max_chars=20000` | 表达式/函数求值,等待 Promise;可选元素 ref |
|
|
63
|
+
| `browser_logs` | 必填 `kind=console/network`, `after_seq`, `limit`, `max_chars`, `level` / `url_contains` | 统一读取日志;network 可显式 capture 有界脱敏的 XHR/fetch 详情 |
|
|
64
|
+
|
|
65
|
+
普通结果是一份紧凑 JSON 文本;截图另外返回标准图片内容。失败通过 MCP `isError` 返回,错误文本即原因。
|
|
66
|
+
|
|
67
|
+
## 观察:快照与引用
|
|
68
|
+
|
|
69
|
+
`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`。
|
|
70
|
+
|
|
71
|
+
- 渲染时移除 `InlineTextBox`,合并纯文本列表项与段落名称,保留 `disabled`、`focused`、`expanded`、`checked`、`selected`、`required`、`invalid`、`level` 等状态,以及浏览器提供的 `value`;`[token:n]` 即后续动作使用的 `ref`。未提供 invalid/selected 等属性不代表有效或已提交;value 也可能只是下拉框的过滤文本。
|
|
72
|
+
- `max_nodes` 为 1–3000(默认 600);`max_chars` 为 1–2000000(默认 24000),按行序列化长度累计,与节点数是两个独立预算。
|
|
73
|
+
- `truncated: true` 时把 `next_cursor` 传回同一个工具继续读取同一份观察,refs 在续读期间保持有效;也可以调整预算。单个超长节点放不下时会明确报出所需预算,不静默丢尾部。
|
|
74
|
+
- 覆盖主 frame、同进程 iframe 与递归 OOPIF;读取失败、读取期间变化或附着失败的 frame 列入 `omitted_frames`。
|
|
75
|
+
|
|
76
|
+
失效规则:
|
|
77
|
+
|
|
78
|
+
| 事件 | 影响 |
|
|
79
|
+
|---|---|
|
|
80
|
+
| 输入 / `evaluate`(新表达式) | 快照 cursor 失效;原节点 refs 可继续解析;`evaluate` 的缓存续读不失效 |
|
|
81
|
+
| 新的 `browser_snapshot` | 旧 refs 失效 |
|
|
82
|
+
| 主文档导航、同文档导航、文档更新 | 全部 refs 与 cursor 失效 |
|
|
83
|
+
| 子 frame 导航或移除 | 仅该子树 refs 失效,其余保留 |
|
|
84
|
+
| `release_tab` / `close_tab` / 断连 | 全部失效 |
|
|
85
|
+
|
|
86
|
+
ref 每次用 frame/session + backendNodeId 重新解析,并以局部 AX 查询核对原 role/name;语义改变时要求新快照,不依赖旧坐标,也不以相似名称查找替代。节点脱离文档时失败;重新插入的同一节点可继续使用。
|
|
87
|
+
|
|
88
|
+
`browser_read_text` 默认读取主 frame,`max_chars` 为 1–100000(默认 20000)。`mode=visible` 使用 innerText;`text_content` 使用 textContent;`value` 要求用 selector 唯一选中 input、textarea 或 contenteditable;`editor` 要求 selector 对应支持的 Monaco 或 CodeMirror 5 实例并读取完整模型。Monaco 必须在页面暴露 `monaco.editor.getEditors()`,CodeMirror 5 必须在选中节点暴露实例。未知编辑器明确报错,使用站点专用 evaluate 适配;不猜测模块内部状态。
|
|
89
|
+
|
|
90
|
+
整页摘要/检索直接调用 `browser_read_text({tab_handle})`,不传 selector 即是 document.body.innerText;正文可用 next_offset 分段续读。可见正文包含页面当前渲染文本,不包含未加载的虚拟行、默认模式下的子 frame 或所有 Shadow DOM 内容,所以 complete_source=false 是覆盖边界,不代表分页丢失。
|
|
91
|
+
|
|
92
|
+
返回 `source`、`complete_source`、`total_chars`、`returned_chars`、`truncated`、`next_offset`。DOM 和 contenteditable 无法保证包含虚拟化内容,`complete_source=false`;字段 value / 支持的模型为 true。`next_offset` 用于下一次实时读取,内容变化可能导致重复或遗漏;需要一致结果时用 evaluate 一次提取并续读缓存。
|
|
93
|
+
|
|
94
|
+
### 可复用的 frame 寻址
|
|
95
|
+
|
|
96
|
+
除 frame_id 外,selector、read_text 和 evaluate 可传 frame;二者互斥。URL/title 精确匹配子 frame,多个匹配立即报错。path 是逐层 CSS iframe 选择器,每层须唯一;每次调用重解析,frame 被重新创建后仍可使用同一路径。未出现的 frame 只在准备预算内等待,歧义不重试。
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{"tab_handle":"...","selector":"input","frame":{"path":["iframe[title='Outer']","iframe[title='Inner']"]},"text":"填写内容"}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
也可传 `frame: {url:'https://example.com/form'}` 或 `frame: {title:'表单页面'}`;path 可再与 URL/title 组合校验最终 frame。ref 自带 frame/session,不能另传 frame。路径 CSS 不穿透 Shadow DOM;可操作 AX ref 和拦截的 file chooser 仍覆盖闭合 Shadow DOM。
|
|
103
|
+
|
|
104
|
+
## 输入:可操作性与两种模式
|
|
105
|
+
|
|
106
|
+
`click`、`fill`、`press_key` 共用一套等待逻辑:可见、启用、非 `inert`、几何位置连续稳定;`fill` 还要求可编辑且非只读,`press_key` 会从当前焦点逐层进入 OOPIF 找到接收方。`timeout_ms` 默认 5000、范围 100–30000,**一个截止时间**覆盖准备、派发与校验;准备阶段只轮询、不派发,派发只发生一次。原生 click/hover/press_key 派发与可信回执最多另等 1250 ms,清理最多 250 ms;这属于送达上限,长 timeout_ms 仍可用于等待目标出现。
|
|
107
|
+
|
|
108
|
+
### browser_click
|
|
109
|
+
|
|
110
|
+
| 参数 | 取值 | 说明 |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `ref` / `selector` / `x`,`y` | 三选一 | ref 来自最新快照;selector 每次重解析 |
|
|
113
|
+
| `selector_type` / `frame_id` | `css`(默认)或 `xpath` / 快照中的 frame ID | 仅 selector 使用;默认主 frame |
|
|
114
|
+
| `nth` | 从 0 开始的整数 | 仅 selector 使用;显式选择当前匹配的第 N 项,省略则要求唯一 |
|
|
115
|
+
| `coordinate_space` | `viewport`(默认)/ `document` | 仅坐标点击使用;document 自动滚动顶层文档后换算到视口 |
|
|
116
|
+
| `button` | `left`(默认)/ `middle` / `right` | |
|
|
117
|
+
| `click_count` | `1`(默认)/ `2` | 双击按顺序重复 down/up |
|
|
118
|
+
| `modifiers` | `Alt` / `Control` / `Meta` / `Shift` | 默认空数组 |
|
|
119
|
+
| `mode` | `native`(默认)/ `dom` | DOM 模式接受 ref 或 selector |
|
|
120
|
+
|
|
121
|
+
原生模式按序派发 move → down → up,显式设置 `buttons` 与修饰键,并在隔离世界安装命中校验:成功返回 `target_verified: true`、`application_result: "not_checked"`。左键单击 / 双击分别校验 `click` / `dblclick`,中键校验 `auxclick`,右键校验 `contextmenu`。目标被遮挡、在 hover 后移动或派发中变化时会明确报错并阻止不匹配事件;已送达的前序事件可能已有副作用,不能撤销或自动重试。
|
|
122
|
+
|
|
123
|
+
原生 click 在准备前、派发前分别检查顶层 tab 的 active/discarded/frozen、窗口是否最小化,并读取 `document.visibilityState` 和 `document.hasFocus()`;每次检查最多 500 ms,且受总输入截止时间限制。未选中的标签、暂停的页面或最小化窗口快速返回 `TAB_NOT_READY` 和 `No input was dispatched`,不自动调用 bringToFront。可显式调用 `browser_activate_tab`(默认 focus_window=true),重新 snapshot 后再 click。
|
|
124
|
+
|
|
125
|
+
成功结果的 `page_state` 包含 visibility_state、has_focus、window_focused。这些信号仅作诊断:活动标签所在窗口即使未处于 OS 前台,或者因遮挡报告 hidden,仍可尝试一次原生点击,并验证可信事件送达。不能据此保证任意后台页面都能交互;没有可信回执时仍报告未知结果,不重放、不自动切换 DOM 模式。
|
|
126
|
+
|
|
127
|
+
原生 ref/selector 点击已自动滚入视口;viewport 坐标保留严格视口边界。document 坐标针对顶层文档,嵌套滚动容器和动态布局优先用 selector/ref。`hit_target` 返回实际命中的 tag、最多 300 字符 text、role、disabled、clickable_hint,并由 clickable_target 给出最近的可点击候选祖先(含自身)的 tag/role/text。查找跨越 Shadow host 和 slot,例如 span 在 button 内时保留 tag=span,同时返回 clickable_hint=true、clickable_target.tag=button;disabled 同时观察祖先的禁用/inert/aria-disabled 状态。
|
|
128
|
+
|
|
129
|
+
hint 仅为 HTML/ARIA 启发式判断,false 表示未找到候选,不能证明元素不可点击,也不能覆盖所有事件代理。它不会替换真正的鼠标命中节点或改变派发目标。坐标点击无法推断调用者的业务意图,DOM/网络变化也不能证明业务成功,需按具体任务观察。
|
|
130
|
+
|
|
131
|
+
`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。
|
|
132
|
+
|
|
133
|
+
坐标命中 CSS 伪元素时,以其 originating element 做描述和事件校验,`hit_target.pseudo_element=true`。截图坐标仍按截图元数据换算;优先用 ref/selector 避免布局漂移。
|
|
134
|
+
|
|
135
|
+
`mode: "dom"` 只对 HTML 元素调用一次 `HTMLElement.click()`,返回 `method: "dom"`、`events_trusted: false`:没有 pointer 事件、没有用户激活,也不支持修饰键或坐标。自绘控件、拖拽、依赖 pointerdown 的功能仍需原生输入。两种模式都不会自动互相降级。
|
|
136
|
+
|
|
137
|
+
跨进程 iframe 会逐层检查父页面遮挡并换算坐标,最终从顶层派发;返回的 `clicked` 始终是顶层视口 CSS 坐标。透视变换的 iframe 会被明确拒绝。
|
|
138
|
+
|
|
139
|
+
### browser_hover / browser_drag
|
|
140
|
+
|
|
141
|
+
hover 接受与原生 click 相同的目标,执行一次 mouseMoved,不派发 down/up/click。工具清理不移动指针;后续直接 click 子项,不需要先移回父菜单。输入不移动系统鼠标,也不隐式激活标签。
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{"tab_handle":"...","selector":"#menu"}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
随后调用 `browser_click({tab_handle, selector:'#menu .child'})`。需要等待菜单出现时可组合 browser_wait。
|
|
148
|
+
|
|
149
|
+
drag 的 source / destination 各自接受 ref、严格 selector(可带 frame_id/nth)或 x/y(可带 coordinate_space)。mode 必须按页面交互选择,默认 pointer;steps 默认 20,范围 2–100,每步间隔约 16 ms。
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{"tab_handle":"...","source":{"selector":"#card"},"destination":{"selector":"#column"},"mode":"html5","steps":20}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
pointer 发送 mousePressed → 多步 mouseMoved → mouseReleased;HTML5 模式先通过真实按下/移动触发 dragstart 并截获其 DragData,再派发 dragEnter/dragOver/drop,核验可信 dragstart/drop/dragend。不会凭空构造拖拽数据,不自动切换模式。目的元素在按住后调用已有滚入视口逻辑,包含嵌套容器,然后重新命中检查;这是目标驱动的滚动,不是持续的视口边缘滚动任务。固定坐标始终受视口边界约束,视口外坐标用 document 或改用元素目标。
|
|
156
|
+
|
|
157
|
+
成功返回 events_trusted=true、delivery_verified=true、application_result=not_checked;不保证应用已保存排序。任一步失败、主文档/相关 frame 变化或接管结束即停止,尝试取消/释放本次手势;不回滚、不重放。HTML5 CDP 路径仍受浏览器和站点原生拖拽支持限制。
|
|
158
|
+
|
|
159
|
+
### browser_select_option
|
|
160
|
+
|
|
161
|
+
ref/selector 必须指向 HTML select。label/value 为精确匹配,index 从 0 开始,三者只传一个;重复 label/value 报错,使用明确 index 消歧。禁用 select/option/optgroup 被拒绝。设置一个选项;multiple 控件也只保留这个选项。
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{"tab_handle":"...","selector":"select[name=city]","label":"上海"}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
不打开系统弹层、不主动聚焦,使用原型 selectedIndex setter 后派发合成 input/change,并核对选项身份及最终索引。返回 method=dom、events_trusted=false、value_verified=true,不能满足网站的 isTrusted/用户激活要求。native click 命中 select、其 UA shadow 节点或关联 label 时在派发前提示使用 select_option。系统弹层已经打开时按 Esc 关闭;工具不接管操作系统 UI。
|
|
168
|
+
|
|
169
|
+
### browser_wait
|
|
170
|
+
|
|
171
|
+
condition 支持 attached(出现)、visible(非隐藏且有尺寸)、actionable(可见/启用/非 inert/稳定/命中无阻挡)、text(textContent 含 expected,或与 previous 不同)、url(主 frame URL 含 expected)、network_idle。元素条件使用严格 selector,可带 selector_type/frame_id/nth;歧义立即报错。visible 不要求启用;actionable 不自动滚动,元素须在视口内,并复用 native click 的 frame/祖先遮挡检查。
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{"tab_handle":"...","selector":"#toast","condition":"text","expected":"保存成功","timeout_ms":5000}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
url 需要 expected 子串;text 传 expected 子串,或传 previous 等待任意文本变化,两者只选一个。previous 必须是完整的旧 textContent,可先用 read_text 的 text_content 模式或 evaluate 读取。SPA 可组合 url 与目标元素条件。工具只读轮询,timeout_ms 为 100–30000;失败说明所等条件与最后观察状态:
|
|
178
|
+
|
|
179
|
+
- `Wait for attached selector="#missing" ... current={"satisfied":false,"state":"absent"}`
|
|
180
|
+
- `Wait for visible selector="#hidden" ... current={"ready":false,"reason":"hidden or zero size","satisfied":false}`
|
|
181
|
+
- `Wait for actionable selector="#disabled" ... current={"ready":false,"reason":"disabled","satisfied":false}`
|
|
182
|
+
|
|
183
|
+
network_idle 复用 claim 后的被动网络请求表,等待 inflight=0 且连续 idle_ms(默认 500,范围 100–10000)无请求活动,涵盖已接管页面与 iframe。它不能证明 claim 前的请求、独立 worker 或 WebSocket 消息空闲;长轮询请求会阻止满足条件。活跃表曾超过 500 项时 tracking_overflow=true,无法证明空闲而不会报告成功。页面通常仍需等待业务元素或标志。
|
|
184
|
+
|
|
185
|
+
### browser_fill
|
|
186
|
+
|
|
187
|
+
支持文本类 `input`(text / search / email / password / tel / url)、`textarea` 与 `contenteditable`;传当前 ref 或严格 selector。
|
|
188
|
+
|
|
189
|
+
- `native`(默认):聚焦、选中后发送原生 `insertText`。
|
|
190
|
+
- `dom`:用原型上的原生 value setter(contenteditable 用 `textContent`)写入,派发非可信 `input` / `change`,不主动聚焦;可通知 React/Vue 受控字段,不能保证所有自定义编辑器的内部状态。
|
|
191
|
+
- 两种模式都核对字段最终文本,成功返回 `value_verified: true`、`application_result: "not_checked"`;被吞、失焦或页面同步格式化导致不匹配时报错,且不回显字段内容。这不证明框架状态、选项提交、后续重渲染保留或业务保存。
|
|
192
|
+
- 不会自动 blur 或按 Enter。需要失焦校验时显式 press_key Tab 或点击其他明确目标;自定义年/月/城市选择器输入通常只是过滤,还需点击具体选项。Enter 的语义由站点决定。校验文案可能滞后,应同时观察选中项和字段值;添加行前先确认已提交选项,不能只看输入框里的文本。
|
|
193
|
+
|
|
194
|
+
### browser_type
|
|
195
|
+
|
|
196
|
+
fill 是替换文本,原生路径用 insertText,不制造逐字 keydown。自动补全/搜索框监听按键时用 type:只聚焦,不清空、不全选,在当前选区逐个 Unicode 字符派发原生 keyDown(含文本)/keyUp,由浏览器产生 keypress/input。页面 preventDefault 可阻止后续默认输入;不会为补事件再调用 insertText。ASCII 使用 US 键位,非 ASCII 的 code 为空,不模拟系统 IME 组合输入。
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{"tab_handle":"...","selector":"input[role=combobox]","text":"上海","delay_ms":50}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
delay_ms 为 0–1000,是字符之间的最小额外间隔,准备与协议开销会增加实际耗时;整串 text 最多 10000 个 UTF-16 单位,仍受同一 30 秒以内的总预算约束。换行/Tab/控制字符用显式 press_key。检查原字段连接与焦点,观察到失焦或替换后停止;失败报告已核验字符数,当前字符仍可能已送达,不能重放整串。delivery_verified 证明 keydown,不证明站点接受文本或保存。
|
|
203
|
+
|
|
204
|
+
### browser_batch
|
|
205
|
+
|
|
206
|
+
同一 tab_handle 内一次传入 1–20 个明确步骤,action 支持 click/fill/type/press_key/hover/scroll/select_option/wait;元素动作必须使用 selector,可带 selector_type、frame_id、nth 和 mode(默认 native)。click 还支持 button/click_count/modifiers。batch 只接受 selector,不接受 refs 或坐标;每步重新查询目标。
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{"tab_handle":"...","steps":[
|
|
210
|
+
{"action":"fill","selector":"input[placeholder=公司名称]","nth":1,"text":"公司名称"},
|
|
211
|
+
{"action":"press_key","key":"Tab"}
|
|
212
|
+
]}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
timeout_ms 默认 15000,范围 100–30000,是整批准备、派发和校验的总预算。每步可设 timeout_ms(100–30000),它与剩余总预算取更早的截止时间,失败后不会继续下一步。所有参数先预检,每一步复用原有输入流程,尚未渲染的选项可在准备阶段等待;不会自动猜选项或增加交互。步骤间不插入其他同标签命令。成功返回 completed=true、results;首个失败返回 MCP isError=true 以及 completed=false、results(此前完成结果)、failed_step(从 0 开始)、error。发生主文档变化、断开接管或弹窗也会停止。整批不具备事务或回滚语义,不要重放已完成步骤;失败步骤可能已产生副作用,先观察。
|
|
216
|
+
|
|
217
|
+
失败还返回 started_at、next_unattempted_step(无后续步骤时为 null)、retry_failed_step_safe。start_at 是显式的从 0 开始的续跑索引:重新提交同一 steps 并选择要执行的索引,前面的步骤只做参数验证,不再派发。未派发/只读等待失败可报告 retry_failed_step_safe=true;已派发或无法确认时为 false。先观察失败步骤是否生效,再决定重试该步还是跳到 next_unattempted_step;工具不自动选择、重试、回滚或保存后台任务。整批/每步均可指定 dialog_policy;各步骤返回自己的 dialogs,整批返回汇总。
|
|
218
|
+
|
|
219
|
+
用 batch 合并已知的“打开 → 原生输入过滤 → 点击具体选项”路径;不确定下一目标时先结束本批,snapshot/evaluate 观察后再继续。evaluate 中的 JavaScript 不能制造可信输入事件。
|
|
220
|
+
|
|
221
|
+
### browser_press_key
|
|
222
|
+
|
|
223
|
+
支持 F1–F12、Enter / Tab / Escape / Backspace、Insert / Delete、四个方向键、Home / End / PageUp / PageDown,以及 ASCII 字母、数字和 US 键盘标点。标点既可直接传字符,也可用物理键名:`Semicolon`、`Equal`、`Minus`、`Comma`、`Period`、`Slash`、`Backquote`、`BracketLeft`、`BracketRight`、`Backslash`、`Quote`、`Space`。
|
|
224
|
+
|
|
225
|
+
- `Shift+1` 生成 `!`,`Shift+/` 生成 `?`;直接传 `?`、`:`、`+` 会自动补 Shift。
|
|
226
|
+
- `+` 或 `Plus` 表示加号,组合键写 `Control++`;普通字母需要大写时显式使用 `Shift+A`。
|
|
227
|
+
- Control / Alt / Meta 组合不额外插入文本。
|
|
228
|
+
- 在隔离世界监听可信 keydown,成功返回 `delivery_verified: true`;无法建立校验的 frame 列入 `verification_omitted_frames`。浏览器保留的快捷键可能被浏览器截获。没有 DOM 快捷键替代模式。
|
|
229
|
+
|
|
230
|
+
### browser_scroll
|
|
231
|
+
|
|
232
|
+
- `x` / `y` 始终是顶层视口 CSS 坐标,省略时取视口中心;正 `delta_y` 向下,正 `delta_x` 向右,单位为 CSS 像素。
|
|
233
|
+
- `mode: "dom"`(默认)在指定位置逐层查找可滚动容器并调用 `scrollBy`,不触发 wheel;可进入闭合 Shadow DOM 与同源 iframe,跨源 iframe 会明确报错。
|
|
234
|
+
- `mode: "wheel"` 从顶层派发原生 wheel,由浏览器路由到容器或 OOPIF,遵守祖先页面遮挡;返回 `method: "native_wheel"`、`dispatched: true`,仅表示 CDP 已确认派发,不保证处理器执行或页面移动。
|
|
235
|
+
- DOM 模式返回 `scrolled`、`before` / `after`、`scope`(`document` 或 `element`)。
|
|
236
|
+
|
|
237
|
+
容器目标可传 ref/selector 与 frame;DOM 模式直接对这个容器 scrollBy,wheel 模式复用命中与 frame 换算后派发原生 wheel。无目标时保留原来的坐标/视口中心行为。
|
|
238
|
+
|
|
239
|
+
虚拟列表无需 evaluate 计算坐标,可在同一个标签队列和总预算内滚动查找,并返回找到元素的 ref:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{"tab_handle":"...","selector":"#list","until":{"text":"Row 0487","selector":"button"},"delta_y":200,"max_steps":100,"timeout_ms":15000}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
until 是显式 DOM 滚动搜索,不与 wheel 组合;省略 delta_y 时每次滚约 0.8 个容器高度。max_steps 默认 100、范围 1–500。搜索容器内渲染且有尺寸的最深匹配元素,文本为子串;重复匹配报错,使用 until.selector 消歧。每次滚动后有界轮询,导航、相关 frame 变化、边界、步数或超时即停止,不自动点击;返回 found/ref/frame_id/scroll_steps。滚动本身会改变页面状态,即使未找到也可能已触发加载。它不承诺读到服务端未提供的数据,也不把虚拟列表未渲染行当作已存在。batch 的 scroll 步骤使用同一实现。
|
|
246
|
+
|
|
247
|
+
### 结果未知时
|
|
248
|
+
|
|
249
|
+
不确定的输入/求值/导航错误会附带一次有界 Post-action observation:dialog/dialogs、navigation_observed、frame_navigation_observed、current_url、frame_id、dom_changed。DOM 信号由操作前在目标 frame 安装的临时 MutationObserver 提供,读取最多 300 ms、清理最多 100 ms;不读取整棵快照或重放动作。节点属性值、服务端状态及其他 frame 的变化未必触发该 DOM 信号。
|
|
250
|
+
|
|
251
|
+
观察到对话框、导航或 DOM 变动可报告 observable_effects;no_effect_confirmed 仅表示未确认效果,不表示未送达或未发生。delivery 仍为 unknown,业务成功仍需任务自己的标志或网络证据。确定的准备失败则保留“未派发”。
|
|
252
|
+
|
|
253
|
+
派发之后的超时会释放工具队列,但动作可能已经送达或稍后生效。此时先观察页面(快照、截图、日志),不要自动重放,也不要为了补一次而切换模式。
|
|
254
|
+
|
|
255
|
+
`TAB_NOT_READY` 是明确的派发前失败,可以在激活、重新观察后再点击;它不属于“结果未知”。即使前置检查通过,用户仍可能在派发瞬间切换标签,浏览器菜单也可能捕获输入。派发后没有回执仍须报告 Outcome is unknown,不能仅凭 visibility/focus 或“没有观察到变化”宣称零效果。
|
|
256
|
+
|
|
257
|
+
## 导航、对话框与截图
|
|
258
|
+
|
|
259
|
+
`browser_navigate` 传 url 或 direction=back/forward,二者只选一个;URL 和历史目的地都必须是不带凭据的 HTTP(S) 页面。历史使用 Page.navigateToHistoryEntry,并等待对应主 frame 的提交/生命周期;同文档历史和 BFCache 恢复无需等待新的 DOMContentLoaded。无历史条目时在派发前失败。
|
|
260
|
+
|
|
261
|
+
| 参数 | 默认 | 范围 / 说明 |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `wait_until` | `domcontentloaded` | `commit` 只等提交,`load` 等 load 事件;同文档 hash 导航不等待新的加载 |
|
|
264
|
+
| `timeout_ms` | 15000 | 100–40000 |
|
|
265
|
+
| `include_snapshot` | `true` | 关闭时只返回标题与 URL |
|
|
266
|
+
|
|
267
|
+
导航成功后另有一次 10000 ms 的观察预算;观察失败会返回导航结果加 `observation_error`、`observe_again`,不会把已成功的导航报告为失败。Hub 侧操作上限 60 秒、客户端 65 秒,覆盖最长导航加观察并保留传输余量。超时不代表导航已取消,SPA 在 `DOMContentLoaded` 之后仍可能异步更新。
|
|
268
|
+
|
|
269
|
+
观察遇到文档变化、执行上下文销毁等瞬时错误时,内部只重读一次,共享同一 10 秒预算;不会重新导航。权限错误和预算耗尽不重试。
|
|
270
|
+
|
|
271
|
+
输入、evaluate、navigate、wait 与 batch 可指定 dialog_policy=ask/accept/dismiss,默认 ask。accept/dismiss 在操作期间自动调用已有 JS 对话框接口,返回 dialogs 中的原消息、类型、accepted、handled;prompt 自动 accept 使用原默认文本。每个操作最多自动处理 10 个对话框,之后回到等待人工处理,不自动无限确认。它只覆盖页面 JS 对话框,不接管系统权限/文件 UI。
|
|
272
|
+
|
|
273
|
+
batch 遇到已自动处理的 alert/confirm 可以继续下一步;默认 ask 仍会停止。beforeunload=accept 继续等待导航,dismiss 返回 navigated=false、cancelled_by_dialog=true;浏览器若未弹出 beforeunload,工具不能强制它出现。主文档实际变化后 batch 仍按原规则停止,避免在新文档执行后续步骤。
|
|
274
|
+
|
|
275
|
+
页面弹出未自动处理的 JavaScript 对话框时,`browser_snapshot` 会返回 `dialog` 对象,其他动作(除 `release_tab`)被拒绝;用 `browser_handle_dialog` 接受或取消,`prompt_text` 只在 prompt 时使用。对话框打开期间仍可读取控制台和网络日志。
|
|
276
|
+
|
|
277
|
+
handle_dialog 返回原始 `dialog.type/message/default_prompt` 和 `accepted`。`default_button=null`,协议不提供浏览器默认按钮焦点。beforeunload 只有实际弹出时才能观察,浏览器可能因缺少用户激活而抑制它。DOM 值写入不证明应用已保存或 dirty=false;不存在跨框架可靠的通用 dirty 检测,导航前按任务检查保存标志或业务接口。
|
|
278
|
+
|
|
279
|
+
`browser_screenshot` 返回 PNG 与 `capture` 元数据:`scope`(`viewport` / `full_page`)、`viewport`、`css_width`、`css_height`、`image_origin_css`、`image_to_viewport_css`。像素到视口坐标的换算为 `css = 像素 × scale + offset`。超时为 5 秒;被遮挡或非活动的 Chromium 页面可能暂停渲染,此时先 `browser_activate_tab`,或改用 AX / 正文。整页截图超过 4000 万像素会被拒绝;整页图中的视口外位置需要先滚动。
|
|
280
|
+
|
|
281
|
+
元素截图传 ref 或严格 selector(可用 frame),与 full_page 互斥。截取当前渲染 border box 的外接矩形,不要求元素启用、不移动焦点;OOPIF 坐标逐层换算,保留页面遮挡,截图不是元素抠图。无渲染 box、负文档坐标、透视 frame 或超过 4000 万像素时报错。capture.scope=element,image_origin_css 是该元素的真实文档位置,沿用已有像素到视口换算。可分别请求元素和 full_page 获取视觉证据,不增加另一个截图/图片比较工具。
|
|
282
|
+
|
|
283
|
+
默认截图隐藏约 1.2 秒的原生点击反馈;`include_indicators: true` 可保留。截图本身不会激活标签或窗口。
|
|
284
|
+
|
|
285
|
+
## 文件上传
|
|
286
|
+
|
|
287
|
+
方式一,使用文件 input / 关联 label 的 ref 或严格 selector(可带 frame);files 数组已支持多文件,files: [] 清空:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{"tab_handle":"...","ref":"<最新快照中的文件输入框 ref>","files":["C:/Users/example/Documents/report.pdf"]}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
开放 Shadow DOM 可用 evaluate 显式返回元素 ref,闭合 Shadow DOM 中隐藏且没有 AX ref 的文件 input 优先通过真实上传按钮触发已拦截的 chooser;不用穿透 CSS 或开启系统文件框。清空派发合成 input/change,返回 events_trusted=false;非空 DOM.setFileInputFiles 的事件为原生,返回 events_trusted=true。这些字段不证明服务端已上传。
|
|
294
|
+
|
|
295
|
+
方式二,隐藏 input 或自定义上传按钮:
|
|
296
|
+
|
|
297
|
+
1. `browser_click` 点击“Choose file”之类的按钮。
|
|
298
|
+
2. 返回结果或下一次 snapshot 中出现 `file_chooser: { pending: true, ... }`。
|
|
299
|
+
3. 调用 `browser_upload_file(tab_handle, files)`,省略 ref,使用被拦截的真实 input。
|
|
300
|
+
|
|
301
|
+
`files: []` 清空指定 input 或结束待处理的选择,页面会收到 `input` / `change`。文件通过 `DOM.setFileInputFiles` 在浏览器本机设置,不读取内容到 MCP 或 Hub,也不弹系统文件框。
|
|
302
|
+
|
|
303
|
+
- 路径必须是浏览器所在机器的绝对可读路径,最多 100 个;支持单文件、多文件、中文 / 空格路径与空文件。
|
|
304
|
+
- 多文件要求 input 具有 `multiple`;禁用、失效 ref、目录路径和 `webkitdirectory` 会被拒绝。
|
|
305
|
+
- `accept` 只是网页提示,不做格式校验;网站仍可自行拒绝。
|
|
306
|
+
- 选择文件可能立即触发网站自动上传,只传任务指定的文件;选择成功不代表服务端上传完成。
|
|
307
|
+
- File System Access API 的 `showOpenFilePicker()` 与原生权限弹窗不在支持范围;跨进程 iframe 的 file input 已支持。
|
|
308
|
+
|
|
309
|
+
## JavaScript 求值
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{"tab_handle":"...","expression":"({title: document.title, count: document.querySelectorAll('a').length})"}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`expression` 最多 100000 字符,需要多条语句时使用 `(() => { ...; return result; })()`,异步使用 `(async () => { ... })()` 或直接返回 Promise。表达式默认在主 frame 的页面世界执行一次,可用 frame_id/frame 指定子 frame,可访问页面变量、DOM 和当前登录态,可产生写操作,但不授予用户激活;返回函数时自动调用一次并等待其 Promise;不会递归调用函数返回的另一个函数。
|
|
316
|
+
|
|
317
|
+
- `timeout_ms` 默认 10000、范围 100–30000;CDP 同步执行时间与 Promise 等待共用该预算。超时会释放工具队列,但不保证取消已调度的异步代码或网络请求。
|
|
318
|
+
- `max_chars` 默认 20000、范围 1–100000,限制每页 value 的 JSON 字符预算(不含元数据)。
|
|
319
|
+
- 每个被控标签最多缓存一份结果,捕获预算为 1000000 个 JSON 字符、50000 个节点、8 层;只读取自有可枚举数据属性,不调用 getter,数组按索引读取。捕获损失由 `capture_truncated` 标记;转义后超出捕获上限时返回结构化 `$type=truncated` 标记,不返回半截 JSON。
|
|
320
|
+
- 数组按完整项、对象按完整键值对、字符串按片段分页,返回 `result_kind`、`has_more`、`next_cursor`。下一页只传 `cursor`,不能再传 expression;续读只读缓存,不执行页面脚本,也不使当前 AX refs 失效。输入不改变已捕获结果;新求值、主文档导航/更新、释放或断连清空缓存。
|
|
321
|
+
- 单项放不下时返回 `budget_too_small`、`required_chars`、`next_cursor`,可提高预算续读,不必重新执行。单个嵌套值超过 100000 字符时应在表达式中提取更小的字段;分页不递归拆解嵌套对象。
|
|
322
|
+
- 特殊值用 `$type` 标记:`undefined`、`bigint`、`number`(NaN / Infinity / -0)、`date`、`error`、`function`、`symbol`、`accessor`、`circular_or_repeated`、`truncated`。Map、Set、DOM 等实例只遍历可枚举属性,需要稳定结果时返回普通对象或字符串。
|
|
323
|
+
- 执行新表达式后快照分页失效;仍连接的原节点 refs 可复用,文档/frame 变化仍使相关 refs 失效;报错或超时前可能已修改页面,先观察,不要自动重放。
|
|
324
|
+
|
|
325
|
+
需要可操作元素时显式指定 result_type=element:
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{"tab_handle":"...","expression":"() => document.querySelector('#target')","result_type":"element"}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
只接受所选 frame 的单个、仍连接的 DOM Element,不接受数组/JSON/null/Document,也不跨 frame 隐式猜测归属。返回 ref/frame_id/AX role/name,后续复用现有输入和上传工具;其文档/frame/session/语义失效规则与快照 refs 一致。每份当前观察最多追加 500 个元素 refs,新快照清空。原始 Runtime objectId 不暴露给客户端,生成 ref 后立即释放包装对象。result_type=json 为默认,分页只续读缓存,不执行函数;element 结果没有 JSON cursor。子 frame 求值的缓存也在该 frame 导航/移除时失效。
|
|
332
|
+
|
|
333
|
+
异步 IIFE 已能表达多步 JavaScript 流程,例如 `(async()=>{const r=await fetch('/api/state');return await r.json()})()`;它不能调用 MCP 原生输入工具。最多 30 秒,没有后台 job 或超时后的结果续接。长工作流按可观察检查点拆分;游标仅续读成功捕获的结果。超时不等于取消异步副作用。
|
|
334
|
+
|
|
335
|
+
## 诊断日志
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{"tab_handle":"...","kind":"console","level":"error","after_seq":0,"limit":100,"max_chars":24000}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
使用 `browser_logs` 读取日志,`kind` 必填且只能是 `console` 或 `network`,没有默认值或混合模式。返回包含 `kind`、`entries` 及续读元数据。两类日志各自拥有缓冲和序列,只能在同一标签、同一次 claim、同一个 kind 内续读:把返回的 `next_seq` 作为下一次的 `after_seq`,切换 kind 时使用该 kind 自己的续读位置。`limit` 为 1–200,`max_chars` 为 1000–100000(约束 `entries` 数组)。`has_more` 表示还有事件可扫描,`latest_seq` 是当前末尾;被过滤器跳过的事件也会推进游标,想在改过滤条件后回看旧事件应从 `after_seq: 0` 开始。
|
|
342
|
+
|
|
343
|
+
网络查询示例:`{"tab_handle":"...","kind":"network","url_contains":"/api/","after_seq":0}`。`level` 仅接受 console,`url_contains` 仅接受 network;传错类型明确报错,不会静默忽略。读取不消费缓冲,不使快照 refs 失效,JS 对话框打开时也可读取。
|
|
344
|
+
|
|
345
|
+
- 控制台:`level` 可选 `log` / `debug` / `info` / `warning` / `error`,收集 console、未捕获异常 / rejection 和浏览器 Log 消息。对象参数是浅层预览,没有 live handle;行号列号从 0 开始,`timestamp` 与 `capture_started_at` 是 Unix 毫秒。
|
|
346
|
+
- 网络:`url_contains`(最多 2000 字符)过滤 URL,事件为 `request` / `redirect` / `response` / `finished` / `failed` / `cache`。按 `session_id`、`request_id` 和重定向 `hop` 关联;404 属于 `response`,连接失败属于 `failed`。timestamp 是 CDP 单调时钟秒,`request` 另带 Unix 秒的 `wall_time`。
|
|
347
|
+
- 每次 claim 自动开启 Runtime / Log / Network 观察;过滤启用时会重放 claim 之后的历史控制台消息,已发生的网络请求无法追溯。导航不清空日志,`release`、detach、关闭或断连清理,不写入磁盘。
|
|
348
|
+
- 每类最多保留 500 条、1,048,576 个 JSON 字符,活跃请求关联表最多 500 项。淘汰时 `data_loss: true` 并由 `dropped_through_seq` 标识缺口;关联表溢出可能让完成事件缺少 URL / hop,但仍保留 `request_id`。单条字段超长时尾部显示 `…[truncated]`;单条放不进读取预算时返回 `truncated: true` 的预览并推进游标。
|
|
349
|
+
- 默认不保存 headers/正文。需要校验 XHR/fetch 时先调用 logs(kind=network, capture={enabled:true,max_body_bytes:65536}),再触发请求;随后读取 request/response/body 事件中的 method/status、headers、request_body/response_body。正文对象包含 text/format/bytes/truncated。url_contains 只过滤读取,不限制 capture 范围。capture.enabled=false 关闭后续捕获;之前的有界记录留到正常淘汰或 release,不落盘。
|
|
350
|
+
- Authorization、Proxy-Authorization、Cookie、Set-Cookie 默认且始终脱敏;JSON/form 的 password/token/authorization/cookie 等命名字段也会替换。普通业务文本仍可能含数据,脱敏不保证清除任意格式的个人信息。单正文上限 1024–262144 字节,默认 65536;浏览器每个 debugger session 总缓存 2 MiB,保留原日志 500 条/1 MiB 上限,最多 8 个并行正文读取。已知解压后超过上限的响应不读取,二进制正文省略;缓存不足、捕获前已发出、跨导航丢失或超过并发限额会明示 unavailable,不能当作空正文。大量正文请调低捕获上限,并在读取时提高 max_chars(最大 100000),否则沿用预算预览而非完整正文。
|
|
351
|
+
- 正文捕获不修改缓存策略、service worker 或拦截/重放请求;URL 与控制台内容仍可能包含网站数据。
|
|
352
|
+
- 覆盖主 target、同进程 frame 与递归附着的 OOPIF,日志带 `session_id`(主 session 为 null),解析出上下文时带 `frame_id`。独立 worker / service worker 与 WebSocket 消息不在覆盖范围。
|
|
353
|
+
|
|
354
|
+
## 标签栏控制标记
|
|
355
|
+
|
|
356
|
+
被控标签会临时进入一个**无标题的红色原生分组**(使用 `tabGroups` 权限),扩展按钮同时显示 `RUN` 徽标。分组不修改网页标题、favicon 或正文,也不注入页面脚本。
|
|
357
|
+
|
|
358
|
+
- 正常释放、MCP 断连或浏览器停止调试时移除标记并恢复原分组;原分组已因空组消失时按记录重建。
|
|
359
|
+
- 用户期间改名、改色或移动标签时保留用户的调整;恢复分组不保证原标签顺序。
|
|
360
|
+
- 固定标签不能加入分组,claim 会返回 `visual_warning`,扩展徽标仍可显示;需要分组标记时先取消固定。
|
|
361
|
+
|
|
362
|
+
## 限制与排查
|
|
363
|
+
|
|
364
|
+
- 仅支持 Chrome / Edge 的 HTTP(S) 页面;不含 `chrome://`、扩展页与本地文件。最低浏览器版本 125。
|
|
365
|
+
- 不支持操作系统原生 UI 接管、下载管理、网络拦截、测试运行器、Firefox / WebKit;独立 worker 与 WebSocket 消息不可见。
|
|
366
|
+
- 标签拖动或浏览器原生菜单可能捕获原生输入,CDP 返回成功不等于页面收到事件;结束该交互后再依赖原生输入。不要用 `Input.setIgnoreInputEvents(false)` 强行解除用户的菜单 / 拖动状态。
|
|
367
|
+
- 后台操作要求浏览器运行且页面未冻结;网站自己弹窗或权限 UI 仍可能改变焦点。
|
|
368
|
+
- `target_verified`、`delivery_verified`、`value_verified` 只证明事件送达或文本写入,不证明提交、付款、服务端上传等业务完成。
|
|
369
|
+
- Hub 日志位于配对文件同目录的 `hub.log`;`browser-attach status` 查看连接状态,`browser-attach stop` 停止 Hub。
|
|
370
|
+
- 宿主把大结果保存为 Artifact 时,长行可能被宿主截断;这是宿主读取行为,不是快照游标分页。相关说明见 [Nosis 集成](NOSIS-INTEGRATION.md)。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// One deadline covers preparation, waiting, dispatch and verification. Only
|
|
2
|
+
// preparation is retried; once input is sent its outcome must be observed.
|
|
3
|
+
export function deadline(timeout_ms = 5000, phase = 'input', parentEnd = Infinity) {
|
|
4
|
+
if (!Number.isInteger(timeout_ms) || timeout_ms < 100 || timeout_ms > 30000) throw new Error('timeout_ms must be 100..30000');
|
|
5
|
+
const end = Math.min(Date.now() + timeout_ms, parentEnd);
|
|
6
|
+
let dispatched = false, waiting;
|
|
7
|
+
function error() {
|
|
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 === 'wait') return Object.assign(new Error(`Condition was not satisfied within ${timeout_ms} ms`), { input_dispatched: false });
|
|
10
|
+
if (phase === 'observation') return new Error(`Navigation observation timed out within ${timeout_ms} ms; navigation already succeeded. Take a new snapshot.`);
|
|
11
|
+
const result = new Error(dispatched
|
|
12
|
+
? `Input did not finish with verified delivery within ${timeout_ms} ms. Outcome is unknown. Browser UI may be capturing input; press Esc to dismiss a native menu. Observe before retrying; never automatically replay or fall back.`
|
|
13
|
+
: `Target did not become actionable within ${timeout_ms} ms${waiting ? `; waiting for ${waiting}` : ''}; no input was dispatched.`);
|
|
14
|
+
result.input_dispatched = dispatched;
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
async function run(operation) {
|
|
18
|
+
const remaining = end - Date.now();
|
|
19
|
+
if (remaining <= 0) throw error();
|
|
20
|
+
let timer;
|
|
21
|
+
try { return await Promise.race([Promise.resolve().then(operation), new Promise((resolve, reject) => {
|
|
22
|
+
timer = setTimeout(() => reject(error()), remaining);
|
|
23
|
+
})]); } finally { clearTimeout(timer); }
|
|
24
|
+
}
|
|
25
|
+
return { run, has_dispatched: () => dispatched, child(ms, childPhase = phase) { return deadline(ms, childPhase, end); }, describe(detail) { waiting = detail; }, dispatch() { if (Date.now() >= end) throw error(); dispatched = true; }, remaining: () => Math.max(0, end - Date.now()) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function waitForTarget(cdp, value, target, node, budget, { editable = false } = {}) {
|
|
29
|
+
let previous;
|
|
30
|
+
while (true) {
|
|
31
|
+
const bounds = value(await budget.run(() => cdp(target, 'Runtime.callFunctionOn', {
|
|
32
|
+
objectId: node.objectId, functionDeclaration: inspectTarget.toString(), arguments: [{ value: editable }], returnByValue: true,
|
|
33
|
+
})));
|
|
34
|
+
if (bounds?.detached) throw new Error('Element is detached; take a new snapshot. No input was dispatched.');
|
|
35
|
+
if (bounds?.unsupported) throw new Error('Ref is not a supported editable field; no input was dispatched.');
|
|
36
|
+
if (bounds?.ready && previous && JSON.stringify(bounds) === JSON.stringify(previous)) return;
|
|
37
|
+
budget.describe?.(`visible, enabled, stable target; current=${JSON.stringify(bounds)}`);
|
|
38
|
+
previous = bounds?.ready ? bounds : null;
|
|
39
|
+
await budget.run(() => new Promise(resolve => setTimeout(resolve, 20)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function inspectTarget(editable) {
|
|
44
|
+
let element = this?.nodeType === 3 ? this.parentElement : this;
|
|
45
|
+
if (!element?.isConnected) return { detached: true };
|
|
46
|
+
const tag = element.tagName?.toLowerCase();
|
|
47
|
+
if (editable && ((!['input', 'textarea'].includes(tag) && !element.isContentEditable) ||
|
|
48
|
+
(tag === 'input' && !['text', 'search', 'email', 'password', 'tel', 'url', ''].includes(element.type)))) return { unsupported: true };
|
|
49
|
+
// Walk across shadow boundaries as inert can be inherited from a host.
|
|
50
|
+
for (let parent = element; parent; parent = parent.parentElement || parent.getRootNode().host) {
|
|
51
|
+
if (parent.matches(':disabled') || parent.hasAttribute('inert')) return { ready: false, reason: parent.matches(':disabled') ? 'disabled' : 'inert' };
|
|
52
|
+
}
|
|
53
|
+
const rect = element.getBoundingClientRect(), style = element.ownerDocument.defaultView.getComputedStyle(element);
|
|
54
|
+
if (style.visibility !== 'visible' || rect.width <= 0 || rect.height <= 0) return { ready: false, reason: 'hidden or zero size' };
|
|
55
|
+
if (editable && element.readOnly) return { ready: false, reason: 'read-only' };
|
|
56
|
+
return { ready: true,
|
|
57
|
+
x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A lost native receipt must free the queue promptly even with a long preparation budget.
|
|
61
|
+
export async function delivery(operation, ms = 1250) {
|
|
62
|
+
let timer;
|
|
63
|
+
try { return await Promise.race([operation, new Promise((resolve, reject) => {
|
|
64
|
+
timer = setTimeout(() => reject(new Error('Native input has no verified receipt. Outcome is unknown. Browser UI may be capturing input; press Esc to dismiss a native menu, then observe. Never automatically replay or fall back.')), ms);
|
|
65
|
+
})]); } finally { clearTimeout(timer); }
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { CONNECTION } from './config.generated.js';
|
|
2
|
+
import { createController } from './controller.js';
|
|
3
|
+
|
|
4
|
+
let socket;
|
|
5
|
+
let connecting = false;
|
|
6
|
+
let keepAlive;
|
|
7
|
+
let reconnect;
|
|
8
|
+
let status = 'disconnected';
|
|
9
|
+
let identity;
|
|
10
|
+
const controller = createController(chrome, event => {
|
|
11
|
+
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(event));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
async function getIdentity() {
|
|
15
|
+
const local = await chrome.storage.local.get(['instanceId', 'profileName']);
|
|
16
|
+
const session = await chrome.storage.session.get('runId');
|
|
17
|
+
const instanceId = local.instanceId || crypto.randomUUID();
|
|
18
|
+
const runId = session.runId || crypto.randomUUID();
|
|
19
|
+
if (!local.instanceId) await chrome.storage.local.set({ instanceId });
|
|
20
|
+
if (!session.runId) await chrome.storage.session.set({ runId });
|
|
21
|
+
const family = /Edg\//.test(navigator.userAgent) ? 'edge' : 'chrome';
|
|
22
|
+
return { instance_id: instanceId, run_id: runId, family,
|
|
23
|
+
profile_name: local.profileName || (family === 'edge' ? 'Edge' : 'Chrome'),
|
|
24
|
+
user_agent: navigator.userAgent, extension_id: chrome.runtime.id, extension_version: chrome.runtime.getManifest().version };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function connect() {
|
|
28
|
+
if (connecting || socket?.readyState === WebSocket.OPEN) return;
|
|
29
|
+
connecting = true;
|
|
30
|
+
try {
|
|
31
|
+
identity = await getIdentity();
|
|
32
|
+
const ws = new WebSocket(CONNECTION.url);
|
|
33
|
+
socket = ws;
|
|
34
|
+
ws.onopen = () => {
|
|
35
|
+
ws.send(JSON.stringify({ type: 'hello', token: CONNECTION.token, browser: identity }));
|
|
36
|
+
keepAlive = setInterval(() => {
|
|
37
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
|
|
38
|
+
}, 20000);
|
|
39
|
+
};
|
|
40
|
+
ws.onmessage = async event => {
|
|
41
|
+
let request;
|
|
42
|
+
try {
|
|
43
|
+
request = JSON.parse(event.data);
|
|
44
|
+
if (request.type === 'connected') {
|
|
45
|
+
status = 'connected';
|
|
46
|
+
await chrome.action.setBadgeText({ text: 'ON' });
|
|
47
|
+
await chrome.action.setBadgeBackgroundColor({ color: '#13755b' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!request.id) return;
|
|
51
|
+
const result = await controller.execute(request.owner, request.command, request.arguments || {});
|
|
52
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ id: request.id, result }));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (request?.id && ws.readyState === WebSocket.OPEN) {
|
|
55
|
+
ws.send(JSON.stringify({ id: request.id, error: error.message || String(error) }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
ws.onclose = async () => {
|
|
60
|
+
clearInterval(keepAlive);
|
|
61
|
+
status = 'disconnected';
|
|
62
|
+
connecting = false;
|
|
63
|
+
await controller.releaseAll();
|
|
64
|
+
await chrome.action.setBadgeText({ text: 'OFF' });
|
|
65
|
+
clearTimeout(reconnect);
|
|
66
|
+
reconnect = setTimeout(connect, 2000);
|
|
67
|
+
};
|
|
68
|
+
ws.onerror = () => ws.close();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
connecting = false;
|
|
71
|
+
status = error.message;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
chrome.runtime.onMessage.addListener((message, sender, respond) => {
|
|
76
|
+
if (sender.id !== chrome.runtime.id) return;
|
|
77
|
+
if (message.type === 'status') {
|
|
78
|
+
getIdentity().then(browser => respond({ status, browser, claims: controller.claimedCount() }));
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (message.type === 'reconnect') {
|
|
82
|
+
if (socket?.readyState === WebSocket.OPEN) socket.close();
|
|
83
|
+
else connect();
|
|
84
|
+
respond({ ok: true });
|
|
85
|
+
}
|
|
86
|
+
if (message.type === 'release_all') {
|
|
87
|
+
controller.releaseAll().then(() => respond({ ok: true }));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
chrome.alarms.onAlarm.addListener(alarm => { if (alarm.name === 'connect') connect(); });
|
|
92
|
+
chrome.runtime.onStartup.addListener(connect);
|
|
93
|
+
chrome.runtime.onInstalled.addListener(async () => {
|
|
94
|
+
await chrome.alarms.create('connect', { periodInMinutes: 0.5 });
|
|
95
|
+
connect();
|
|
96
|
+
});
|
|
97
|
+
chrome.alarms.create('connect', { periodInMinutes: 0.5 });
|
|
98
|
+
connect();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { targetOptions, frameOptions } from './locator.js';
|
|
2
|
+
import { clickOptions } from './click.js';
|
|
3
|
+
import { inputMode, keyParameters, selectOptions, typeOptions } from './input.js';
|
|
4
|
+
import { scrollOptions } from './scroll.js';
|
|
5
|
+
import { waitOptions } from './wait.js';
|
|
6
|
+
|
|
7
|
+
export function validateSteps(steps, startAt = 0) {
|
|
8
|
+
if (!Array.isArray(steps) || steps.length < 1 || steps.length > 20) throw new Error('steps must contain 1..20 actions');
|
|
9
|
+
if (!Number.isInteger(startAt) || startAt < 0 || startAt >= steps.length) throw new Error('start_at must be an existing zero-based step index');
|
|
10
|
+
for (const step of steps) {
|
|
11
|
+
if (!step || !['click', 'fill', 'type', 'press_key', 'hover', 'scroll', 'select_option', 'wait'].includes(step.action)) throw new Error('Unsupported batch action');
|
|
12
|
+
if (step.timeout_ms != null && (!Number.isInteger(step.timeout_ms) || step.timeout_ms < 100 || step.timeout_ms > 30000)) throw new Error('step timeout_ms must be 100..30000');
|
|
13
|
+
if (step.dialog_policy != null && !['ask', 'accept', 'dismiss'].includes(step.dialog_policy)) throw new Error('dialog_policy must be ask, accept or dismiss');
|
|
14
|
+
if (step.action === 'press_key') keyParameters(step.key);
|
|
15
|
+
else if (step.action === 'wait') waitOptions(step);
|
|
16
|
+
else if (step.action === 'scroll') {
|
|
17
|
+
if (step.ref != null) throw new Error('Batch scroll uses a fresh selector, not ref');
|
|
18
|
+
if (step.selector != null) targetOptions(step);
|
|
19
|
+
else if (step.frame || step.frame_id) throw new Error('scroll frame scope requires selector');
|
|
20
|
+
frameOptions(step);
|
|
21
|
+
scrollOptions(step);
|
|
22
|
+
} else {
|
|
23
|
+
if (!step.selector || step.ref != null) throw new Error('Batch element actions require selector');
|
|
24
|
+
targetOptions(step);
|
|
25
|
+
if (['click', 'fill'].includes(step.action)) inputMode(step);
|
|
26
|
+
if (step.action === 'click') clickOptions(step);
|
|
27
|
+
if (step.action === 'fill' && typeof step.text !== 'string') throw new Error('Batch fill requires text');
|
|
28
|
+
if (step.action === 'type') typeOptions(step);
|
|
29
|
+
if (step.action === 'select_option') selectOptions(step);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|