@mearl/client 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -15
- package/dist/actionTimeouts.js +4 -0
- package/dist/cli.js +5 -2
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +143 -15
- package/dist/index.d.ts +98 -7
- package/dist/index.js +13 -0
- package/dist/socket.d.ts +2 -0
- package/dist/socket.js +70 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ mearl browser_launch --payload '{"name":"account-b","query":"test_account"}'
|
|
|
58
58
|
mearl browser_launch --payload '{"name":"cookie-copy","copyCookieDomains":["example.com"]}'
|
|
59
59
|
mearl browser_launch --payload '{"name":"figma","userAgentMode":"desktop","copyCookieDomains":["figma.com"]}'
|
|
60
60
|
mearl browser_list
|
|
61
|
+
mearl browser_release
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
**通用选项:**
|
|
@@ -69,29 +70,34 @@ mearl browser_list
|
|
|
69
70
|
| `--timeout <seconds>` | 请求超时时间 |
|
|
70
71
|
| `--compact` | 紧凑输出 |
|
|
71
72
|
| `--output <path>` | 将结果写入文件(如截图) |
|
|
73
|
+
| `--browser <id\|名称>` | 指定目标浏览器 |
|
|
72
74
|
|
|
73
75
|
## 支持的操作
|
|
74
76
|
|
|
75
|
-
| 分类 | Actions
|
|
76
|
-
| ----------- |
|
|
77
|
-
| API 调试 | `get_requests` `get_logs` `get_events` `get_api_schema`
|
|
78
|
-
| Mock & 规则 | `set_mock` `get_mocks` `set_rule` `get_rules`
|
|
79
|
-
| 网络代理 | `send_request` `send_mtop_request`
|
|
80
|
-
| 标签页 | `tab_open` `tab_close` `tab_list`
|
|
81
|
-
| 页面操作 | `page_click` `page_type` `page_scroll` `page_eval` `page_press` `page_wait` `page_navigate` `page_upload`
|
|
82
|
-
| 页面感知 | `page_snapshot` `page_screenshot` `page_selected_element` `page_frames`
|
|
83
|
-
|
|
|
84
|
-
| 用户信息 | `get_user_info`
|
|
85
|
-
| 录制 | `record_start` `record_stop`
|
|
86
|
-
| TDBank | `tdbank_account`
|
|
87
|
-
| 浏览器 | `browser_list` `browser_launch` `browser_close`
|
|
77
|
+
| 分类 | Actions |
|
|
78
|
+
| ----------- | --------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| API 调试 | `get_requests` `get_logs` `get_events` `get_api_schema` |
|
|
80
|
+
| Mock & 规则 | `set_mock` `get_mocks` `set_rule` `get_rules` |
|
|
81
|
+
| 网络代理 | `send_request` `send_mtop_request` |
|
|
82
|
+
| 标签页 | `tab_open` `tab_close` `tab_list` |
|
|
83
|
+
| 页面操作 | `page_click` `page_type` `page_scroll` `page_eval` `page_press` `page_wait` `page_navigate` `page_upload` |
|
|
84
|
+
| 页面感知 | `page_snapshot` `page_screenshot` `page_selected_element` `page_frames` |
|
|
85
|
+
| 环境与状态 | `set_device_emulation` `set_timezone` `get_cookie` `set_cookie` |
|
|
86
|
+
| 用户信息 | `get_user_info` |
|
|
87
|
+
| 录制 | `record_start` `record_stop` |
|
|
88
|
+
| TDBank | `tdbank_account` |
|
|
89
|
+
| 浏览器 | `browser_list` `browser_release` `browser_launch` `browser_close` |
|
|
88
90
|
|
|
89
|
-
页面交互动作(`page_click` / `page_type` / `page_hover` / `page_scroll` / `page_press` / `page_upload`)默认内置观察:同一次调用内执行动作并等待异步稳定,返回 `{ action, observation }`;传 `observe: false` 可关闭观察仅执行裸动作。观察结果不替代完整页面理解:`mode: "delta"` 只返回主文档中的 `effects.notifications`、`effects.interactives` 和 `effects.focus` 等高置信度信号,并明确携带 `scope: "main-document"
|
|
91
|
+
页面交互动作(`page_click` / `page_type` / `page_hover` / `page_scroll` / `page_press` / `page_upload`)默认内置观察:同一次调用内执行动作并等待异步稳定,返回 `{ action, observation }`;传 `observe: false` 可关闭观察仅执行裸动作。观察结果不替代完整页面理解:`mode: "delta"` 只返回主文档中的 `effects.notifications`、`effects.interactives` 和 `effects.focus` 等高置信度信号,并明确携带 `scope: "main-document"`;可交互节点会尽量携带真实 backend `node.ref`,后续动作优先使用 ref,缺少 ref 时使用 `node.selector`。`mode: "navigation"` 且 `ready: true` 时,页面已通过网络静默、骨架状态或保守的内容稳定判定,可在新页面重建快照。通常仅在 `fullSnapshotRecommended` 为 true 时根据 `snapshotReasons` 回退;滚动后若下一步需要读取新视口内容,可按需获取 viewport 快照。`page_eval` 默认裸执行,只有显式传入 `observe` 对象时才启用观察。
|
|
90
92
|
|
|
91
|
-
`page_snapshot` 默认返回完整 AX Tree;长列表可传 `mode: "viewport"
|
|
93
|
+
`page_snapshot` 默认返回完整 AX Tree;长列表可传 `mode: "viewport"`,只需要当前视口内的控件时传 `mode: "interactive"`,已知 CSS 区域时传 `rootSelector`,已有 ref 时传 `rootRef`(可用 `ancestorDepth` 向上补充上下文),只查找特定文案或角色时传 `query`。视口内缺少 AX 控件语义时,`interactive` 会自动回退到 viewport,并返回 `fallbackMode: "viewport"`。`maxNodes` / `maxChars` 截断会同时保留首尾内容。
|
|
94
|
+
|
|
95
|
+
重复文本点击可用 `page_click.scope` 限定 CSS / ref 子树;ref 指向滚动容器内的子节点时,可用 `page_scroll.containerPolicy: "nearest"` 自动解析最近可滚动祖先。`page_click.clickMode` 默认 `auto`:可见桌面页派发可信 mouse,移动模拟页派发可信 touch;隐藏页使用 DOM fallback,返回 `dispatchMode: "dom"` 和 `fallbackReason: "page-hidden"`,且不切换标签或还原窗口。可用 `dom` / `mouse` / `touch` 覆盖自动策略;可信输入返回实际 `pointerType`。点击结果的 `resolvedTarget` 和滚动结果的前后位置、边界字段可用于诊断实际派发目标与滚动效果。
|
|
92
96
|
|
|
93
97
|
`browser_list` 统一列出普通浏览器和托管浏览器。`type` 区分 `regular` / `managed`,`status` 区分 `connected` / `running_disconnected` / `stopped`;只有 `connected` 的浏览器可作为操作目标。
|
|
94
98
|
|
|
99
|
+
`browser_release` 释放当前目标浏览器的 debugger/CDP 控制和会话级临时状态,保留浏览器及全部标签页;后续浏览器操作可自动重新建立控制。
|
|
100
|
+
|
|
95
101
|
托管浏览器使用独立 Profile 和动态 CDP 端口。控制浏览器需先登录 TDBank;生成的 SSO 地址在本地内部传递,新实例可以使用 `headless: true`(默认)完成测试账号登录。`copyCookieDomains` 可把控制浏览器指定域的 Cookie 复制到新实例,Cookie 值不会出现在命令结果或日志中;`userAgentMode: "desktop"` 可让实例使用匹配本机 Chrome 版本的桌面 UA。完整设计见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。
|
|
96
102
|
|
|
97
103
|
## 架构
|
package/dist/actionTimeouts.js
CHANGED
|
@@ -33,6 +33,10 @@ export function resolveActionTimeoutSec(action, data, fallback = 15) {
|
|
|
33
33
|
const waitMs = data.time ?? data.timeout ?? 10000;
|
|
34
34
|
return Math.max(fallback, Math.ceil((Number(waitMs) + 5000) / 1000));
|
|
35
35
|
}
|
|
36
|
+
if (action === 'tab_open')
|
|
37
|
+
return Math.max(fallback, 40);
|
|
38
|
+
if (action === 'set_cookie' && data.reload === true)
|
|
39
|
+
return Math.max(fallback, 40);
|
|
36
40
|
if (action === 'browser_launch')
|
|
37
41
|
return Math.max(fallback, 90);
|
|
38
42
|
if (action === 'browser_close' || action === 'tdbank_account')
|
package/dist/cli.js
CHANGED
|
@@ -41,13 +41,16 @@ const CATEGORIES = [
|
|
|
41
41
|
title: '页面感知',
|
|
42
42
|
names: ['page_snapshot', 'page_screenshot', 'page_selected_element', 'page_frames'],
|
|
43
43
|
},
|
|
44
|
-
{
|
|
44
|
+
{
|
|
45
|
+
title: '环境与 Cookie',
|
|
46
|
+
names: ['set_device_emulation', 'set_timezone', 'get_cookie', 'set_cookie'],
|
|
47
|
+
},
|
|
45
48
|
{ title: '用户信息', names: ['get_user_info'] },
|
|
46
49
|
{ title: '录制', names: ['record_start', 'record_stop'] },
|
|
47
50
|
{ title: 'TDBank', names: ['tdbank_account'] },
|
|
48
51
|
{
|
|
49
52
|
title: '浏览器',
|
|
50
|
-
names: ['browser_list', 'browser_launch', 'browser_close'],
|
|
53
|
+
names: ['browser_list', 'browser_release', 'browser_launch', 'browser_close'],
|
|
51
54
|
},
|
|
52
55
|
];
|
|
53
56
|
function printUsage() {
|
package/dist/commands.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export interface CommandDef {
|
|
|
14
14
|
examples?: string[];
|
|
15
15
|
}
|
|
16
16
|
export declare const COMMANDS: CommandDef[];
|
|
17
|
-
export type KnownAction = 'get_requests' | 'get_logs' | 'get_events' | 'get_api_schema' | 'set_mock' | 'get_mocks' | 'set_rule' | 'get_rules' | 'send_request' | 'send_mtop_request' | 'tab_open' | 'tab_close' | 'tab_list' | 'page_click' | 'page_type' | 'page_hover' | 'page_scroll' | 'page_eval' | 'page_press' | 'page_wait' | 'page_navigate' | 'page_upload' | 'page_snapshot' | 'page_screenshot' | 'page_selected_element' | 'page_frames' | 'set_device_emulation' | 'set_timezone' | 'record_start' | 'record_stop' | 'request_domain_permission' | 'get_user_info' | 'browser_list' | 'browser_launch' | 'browser_close' | 'run_actions';
|
|
17
|
+
export type KnownAction = 'get_requests' | 'get_logs' | 'get_events' | 'get_api_schema' | 'set_mock' | 'get_mocks' | 'set_rule' | 'get_rules' | 'send_request' | 'send_mtop_request' | 'tab_open' | 'tab_close' | 'tab_list' | 'page_click' | 'page_type' | 'page_hover' | 'page_scroll' | 'page_eval' | 'page_press' | 'page_wait' | 'page_navigate' | 'page_upload' | 'page_snapshot' | 'page_screenshot' | 'page_selected_element' | 'page_frames' | 'set_device_emulation' | 'set_timezone' | 'get_cookie' | 'set_cookie' | 'record_start' | 'record_stop' | 'request_domain_permission' | 'get_user_info' | 'browser_list' | 'browser_release' | 'browser_launch' | 'browser_close' | 'run_actions';
|
|
18
18
|
export declare const COMMAND_MAP: Map<string, CommandDef>;
|
package/dist/commands.js
CHANGED
|
@@ -23,6 +23,7 @@ export const COMMANDS = [
|
|
|
23
23
|
name: 'get_logs',
|
|
24
24
|
description: '获取浏览器控制台日志',
|
|
25
25
|
params: [
|
|
26
|
+
{ name: 'tabId', type: 'number', description: '目标标签页 ID,从 tab_list 获取' },
|
|
26
27
|
{ name: 'limit', type: 'number', description: '返回条数,默认 20' },
|
|
27
28
|
{ name: 'level', type: 'string', description: '按日志级别过滤:log/info/warn/error' },
|
|
28
29
|
{ name: 'filter', type: 'string', description: '按内容过滤,支持正则' },
|
|
@@ -43,9 +44,15 @@ export const COMMANDS = [
|
|
|
43
44
|
{ name: 'filter', type: 'string', description: '按 name 或 url 过滤,支持正则' },
|
|
44
45
|
{ name: 'since', type: 'number', description: '只返回最近 N 秒内的事件' },
|
|
45
46
|
{ name: 'limit', type: 'number', description: '返回条数,默认 20' },
|
|
47
|
+
{
|
|
48
|
+
name: 'includeRaw',
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
description: '返回完整 URL、_raw 和 _context;默认 false',
|
|
51
|
+
},
|
|
46
52
|
],
|
|
47
53
|
examples: [
|
|
48
54
|
`get_events --payload '{"source":"aplus","event_type":"CLK","limit":10}'`,
|
|
55
|
+
`get_events --payload '{"filter":"order","includeRaw":true,"limit":1}'`,
|
|
49
56
|
`get_events --payload '{"tabId":12345,"source":"all","limit":20}'`,
|
|
50
57
|
],
|
|
51
58
|
},
|
|
@@ -63,6 +70,7 @@ export const COMMANDS = [
|
|
|
63
70
|
name: 'set_mock',
|
|
64
71
|
description: '设置 API mock 数据',
|
|
65
72
|
params: [
|
|
73
|
+
{ name: 'tabId', type: 'number', description: '目标标签页 ID,从 tab_list 获取' },
|
|
66
74
|
{ name: 'apiName', type: 'string', required: true, description: 'API 名称或正则表达式' },
|
|
67
75
|
{ name: 'mockData', type: 'object', description: '完整 mock 响应数据(与 fields 二选一)' },
|
|
68
76
|
{
|
|
@@ -94,6 +102,10 @@ export const COMMANDS = [
|
|
|
94
102
|
{
|
|
95
103
|
name: 'get_mocks',
|
|
96
104
|
description: '查看当前生效的 mock 列表',
|
|
105
|
+
params: [
|
|
106
|
+
{ name: 'tabId', type: 'number', description: '目标标签页 ID,从 tab_list 获取' },
|
|
107
|
+
{ name: 'apiName', type: 'string', description: '按 API 名称或正则表达式过滤' },
|
|
108
|
+
],
|
|
97
109
|
examples: ['get_mocks'],
|
|
98
110
|
},
|
|
99
111
|
{
|
|
@@ -141,6 +153,11 @@ export const COMMANDS = [
|
|
|
141
153
|
name: 'tdbank_account',
|
|
142
154
|
description: 'TDBank 账号操作:获取当前账号、账号列表、切换账号、快速借用并切换账号',
|
|
143
155
|
params: [
|
|
156
|
+
{
|
|
157
|
+
name: 'tabId',
|
|
158
|
+
type: 'number',
|
|
159
|
+
description: 'CDP 模式下可指定用于读取 Cookie 的标签页 ID',
|
|
160
|
+
},
|
|
144
161
|
{
|
|
145
162
|
name: 'operation',
|
|
146
163
|
type: '"current" | "list" | "switch" | "borrow"',
|
|
@@ -165,6 +182,11 @@ export const COMMANDS = [
|
|
|
165
182
|
description: '列出所有浏览器及其连接状态、类型和托管实例信息',
|
|
166
183
|
examples: ['browser_list'],
|
|
167
184
|
},
|
|
185
|
+
{
|
|
186
|
+
name: 'browser_release',
|
|
187
|
+
description: '释放 Agent 对当前浏览器的调试控制,保留浏览器和全部标签页',
|
|
188
|
+
examples: ['browser_release', 'browser_release --browser chrome-a'],
|
|
189
|
+
},
|
|
168
190
|
{
|
|
169
191
|
name: 'browser_launch',
|
|
170
192
|
description: '启动独立 Chrome,可在新实例中通过 TDBank 登录指定账号',
|
|
@@ -285,12 +307,12 @@ export const COMMANDS = [
|
|
|
285
307
|
// ── 页面操作 ──────────────────────────────────────────────────────────────
|
|
286
308
|
{
|
|
287
309
|
name: 'page_click',
|
|
288
|
-
description: '点击页面元素,支持 selector / text / point
|
|
310
|
+
description: '点击页面元素,支持 selector / text / point 三种定位方式;auto 在可见页按设备模拟状态使用可信 mouse/touch,隐藏页使用 DOM fallback',
|
|
289
311
|
params: [
|
|
290
312
|
{
|
|
291
313
|
name: 'selector',
|
|
292
314
|
type: 'string',
|
|
293
|
-
description: '原生 CSS 选择器,或
|
|
315
|
+
description: '原生 CSS 选择器,或 page_snapshot / 页面动作 observation 返回的 @eN ref(必须带 @ 前缀)。不支持 :has-text/:visible 等 Playwright 私有伪类',
|
|
294
316
|
},
|
|
295
317
|
{
|
|
296
318
|
name: 'text',
|
|
@@ -313,9 +335,14 @@ export const COMMANDS = [
|
|
|
313
335
|
description: '配合 text 使用,默认 true 精确匹配;false 时按子串匹配',
|
|
314
336
|
},
|
|
315
337
|
{
|
|
316
|
-
name: '
|
|
317
|
-
type: '
|
|
318
|
-
description: '
|
|
338
|
+
name: 'scope',
|
|
339
|
+
type: 'string',
|
|
340
|
+
description: '配合 text 使用,把文本匹配限制在指定 CSS 元素或 @eN ref 子树内,适合消除重复文案歧义',
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'clickMode',
|
|
344
|
+
type: '"auto" | "dom" | "mouse" | "touch"',
|
|
345
|
+
description: '点击模式;auto 在可见页按设备模拟状态选择可信 mouse/touch、隐藏页走 DOM,checkbox/radio 校验状态;其他值强制指定派发方式',
|
|
319
346
|
},
|
|
320
347
|
{
|
|
321
348
|
name: 'observe',
|
|
@@ -327,8 +354,10 @@ export const COMMANDS = [
|
|
|
327
354
|
examples: [
|
|
328
355
|
`page_click --payload '{"text":"提交"}'`,
|
|
329
356
|
`page_click --payload '{"text":"退款明细","role":"button"}'`,
|
|
357
|
+
`page_click --payload '{"text":"确定","scope":"@e12"}'`,
|
|
330
358
|
`page_click --payload '{"selector":"@e3"}'`,
|
|
331
359
|
`page_click --payload '{"selector":".submit-btn"}'`,
|
|
360
|
+
`page_click --payload '{"selector":".gesture-btn","clickMode":"touch"}'`,
|
|
332
361
|
`page_click --payload '{"point":{"x":336,"y":117}}'`,
|
|
333
362
|
],
|
|
334
363
|
},
|
|
@@ -340,7 +369,7 @@ export const COMMANDS = [
|
|
|
340
369
|
name: 'selector',
|
|
341
370
|
type: 'string',
|
|
342
371
|
required: true,
|
|
343
|
-
description: '原生 CSS
|
|
372
|
+
description: '原生 CSS 选择器,或 page_snapshot / 页面动作 observation 返回的 @eN ref(必须带 @ 前缀)',
|
|
344
373
|
},
|
|
345
374
|
{ name: 'text', type: 'string', required: true, description: '要填写的文本' },
|
|
346
375
|
{ name: 'clearFirst', type: 'boolean', description: '是否先清空原有内容,默认 true' },
|
|
@@ -361,7 +390,7 @@ export const COMMANDS = [
|
|
|
361
390
|
name: 'selector',
|
|
362
391
|
type: 'string',
|
|
363
392
|
required: true,
|
|
364
|
-
description: '原生 CSS
|
|
393
|
+
description: '原生 CSS 选择器,或 page_snapshot / 页面动作 observation 返回的 @eN ref(必须带 @ 前缀)',
|
|
365
394
|
},
|
|
366
395
|
{
|
|
367
396
|
name: 'observe',
|
|
@@ -374,7 +403,7 @@ export const COMMANDS = [
|
|
|
374
403
|
},
|
|
375
404
|
{
|
|
376
405
|
name: 'page_scroll',
|
|
377
|
-
description: '
|
|
406
|
+
description: '滚动页面或指定容器,默认观察懒加载等滚动后变化',
|
|
378
407
|
params: [
|
|
379
408
|
{
|
|
380
409
|
name: 'direction',
|
|
@@ -382,7 +411,17 @@ export const COMMANDS = [
|
|
|
382
411
|
required: true,
|
|
383
412
|
description: '滚动方向',
|
|
384
413
|
},
|
|
385
|
-
{ name: 'distance', type: 'number', description: '滚动距离(px),默认
|
|
414
|
+
{ name: 'distance', type: 'number', description: '滚动距离(px),默认 600' },
|
|
415
|
+
{
|
|
416
|
+
name: 'selector',
|
|
417
|
+
type: 'string',
|
|
418
|
+
description: '目标滚动容器的原生 CSS 选择器,或 page_snapshot 返回的 @eN ref;不传时滚动页面',
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'containerPolicy',
|
|
422
|
+
type: '"self" | "nearest"',
|
|
423
|
+
description: '默认 self,要求 selector 本身可滚动;nearest 会在其不可滚动时使用最近的可滚动祖先',
|
|
424
|
+
},
|
|
386
425
|
{
|
|
387
426
|
name: 'observe',
|
|
388
427
|
type: 'object | false',
|
|
@@ -393,6 +432,8 @@ export const COMMANDS = [
|
|
|
393
432
|
examples: [
|
|
394
433
|
`page_scroll --payload '{"direction":"down"}'`,
|
|
395
434
|
`page_scroll --payload '{"direction":"bottom"}'`,
|
|
435
|
+
`page_scroll --payload '{"selector":".virtual-list","direction":"down","distance":800}'`,
|
|
436
|
+
`page_scroll --payload '{"selector":"@e8","containerPolicy":"nearest","direction":"bottom"}'`,
|
|
396
437
|
],
|
|
397
438
|
},
|
|
398
439
|
{
|
|
@@ -405,6 +446,11 @@ export const COMMANDS = [
|
|
|
405
446
|
type: 'boolean',
|
|
406
447
|
description: '表达式返回 Promise 时等待完成,默认 true',
|
|
407
448
|
},
|
|
449
|
+
{
|
|
450
|
+
name: 'observe',
|
|
451
|
+
type: 'object',
|
|
452
|
+
description: '仅在表达式会修改页面且需要观察结果时显式传入;不传保持轻量裸执行',
|
|
453
|
+
},
|
|
408
454
|
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
409
455
|
],
|
|
410
456
|
examples: [`page_eval --payload '{"expression":"document.title"}'`],
|
|
@@ -456,19 +502,29 @@ export const COMMANDS = [
|
|
|
456
502
|
},
|
|
457
503
|
{
|
|
458
504
|
name: 'page_navigate',
|
|
459
|
-
description: '
|
|
505
|
+
description: '在当前标签页内导航到 URL、刷新页面,或沿浏览历史后退/前进',
|
|
460
506
|
params: [
|
|
461
|
-
{
|
|
507
|
+
{
|
|
508
|
+
name: 'url',
|
|
509
|
+
type: 'string',
|
|
510
|
+
description: '要导航到的 URL;与 refresh/history 互斥',
|
|
511
|
+
},
|
|
462
512
|
{
|
|
463
513
|
name: 'refresh',
|
|
464
514
|
type: 'boolean',
|
|
465
|
-
description: '为 true
|
|
515
|
+
description: '为 true 时直接重新加载当前页面;与 url/history 互斥',
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: 'history',
|
|
519
|
+
type: '"back" | "forward"',
|
|
520
|
+
description: '沿当前标签页浏览历史后退或前进;与 url/refresh 互斥',
|
|
466
521
|
},
|
|
467
522
|
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
468
523
|
],
|
|
469
524
|
examples: [
|
|
470
525
|
`page_navigate --payload '{"tabId":12345,"url":"https://example.com/page2"}'`,
|
|
471
526
|
`page_navigate --payload '{"tabId":12345,"refresh":true}'`,
|
|
527
|
+
`page_navigate --payload '{"tabId":12345,"history":"back"}'`,
|
|
472
528
|
],
|
|
473
529
|
},
|
|
474
530
|
{
|
|
@@ -479,7 +535,7 @@ export const COMMANDS = [
|
|
|
479
535
|
name: 'selector',
|
|
480
536
|
type: 'string',
|
|
481
537
|
required: true,
|
|
482
|
-
description: '原生 CSS
|
|
538
|
+
description: '原生 CSS 选择器,或 page_snapshot / 页面动作 observation 返回的 @eN ref(必须带 @ 前缀),必须指向 input[type=file]',
|
|
483
539
|
},
|
|
484
540
|
{ name: 'filePaths', type: 'string[]', required: true, description: '本地文件路径数组' },
|
|
485
541
|
{
|
|
@@ -502,12 +558,27 @@ export const COMMANDS = [
|
|
|
502
558
|
{
|
|
503
559
|
name: 'mode',
|
|
504
560
|
type: '"full" | "interactive" | "viewport"',
|
|
505
|
-
description: '输出范围,默认 full
|
|
561
|
+
description: '输出范围,默认 full;interactive 只保留当前视口内的语义控件;长列表优先使用 viewport',
|
|
506
562
|
},
|
|
507
563
|
{
|
|
508
564
|
name: 'rootSelector',
|
|
509
565
|
type: 'string',
|
|
510
|
-
description: '只获取指定 CSS 元素的 AX 子树,可与 mode
|
|
566
|
+
description: '只获取指定 CSS 元素的 AX 子树,可与 mode 组合;与 rootRef 互斥',
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: 'rootRef',
|
|
570
|
+
type: 'string',
|
|
571
|
+
description: '以已有 @eN ref 为根获取局部 AX 子树;与 rootSelector 互斥',
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'ancestorDepth',
|
|
575
|
+
type: 'number',
|
|
576
|
+
description: 'rootRef 向上扩展的 DOM 祖先层数,默认 0,最大 20',
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: 'query',
|
|
580
|
+
type: '{ text?: string; role?: string; exact?: boolean }',
|
|
581
|
+
description: '服务端过滤 AX 树,只保留按 accessibility name / role 命中的节点及祖先路径',
|
|
511
582
|
},
|
|
512
583
|
{ name: 'maxNodes', type: 'number', description: '最多输出节点数,0 表示不限制' },
|
|
513
584
|
{
|
|
@@ -521,6 +592,8 @@ export const COMMANDS = [
|
|
|
521
592
|
'page_snapshot',
|
|
522
593
|
`page_snapshot --payload '{"tabId":1,"mode":"viewport"}'`,
|
|
523
594
|
`page_snapshot --payload '{"tabId":1,"rootSelector":"[role=dialog]"}'`,
|
|
595
|
+
`page_snapshot --payload '{"tabId":1,"rootRef":"@e8","ancestorDepth":1}'`,
|
|
596
|
+
`page_snapshot --payload '{"tabId":1,"query":{"text":"保存","role":"button"}}'`,
|
|
524
597
|
],
|
|
525
598
|
},
|
|
526
599
|
{
|
|
@@ -573,11 +646,13 @@ export const COMMANDS = [
|
|
|
573
646
|
{
|
|
574
647
|
name: 'record_start',
|
|
575
648
|
description: '开始录制用户操作',
|
|
649
|
+
params: [{ name: 'tabId', type: 'number', description: '目标标签页 ID,从 tab_list 获取' }],
|
|
576
650
|
examples: ['record_start'],
|
|
577
651
|
},
|
|
578
652
|
{
|
|
579
653
|
name: 'record_stop',
|
|
580
654
|
description: '停止录制并返回完整结果(操作 + 关联请求);未在录制时不报错,返回 wasRecording: false',
|
|
655
|
+
params: [{ name: 'tabId', type: 'number', description: '目标标签页 ID,从 tab_list 获取' }],
|
|
581
656
|
examples: ['record_stop'],
|
|
582
657
|
},
|
|
583
658
|
// ── 移动端模拟 ────────────────────────────────────────────────────────────
|
|
@@ -628,6 +703,59 @@ export const COMMANDS = [
|
|
|
628
703
|
`set_timezone --payload '{"tabId":12345,"timezone":""}'`,
|
|
629
704
|
],
|
|
630
705
|
},
|
|
706
|
+
{
|
|
707
|
+
name: 'get_cookie',
|
|
708
|
+
description: '读取当前标签页的一个指定非分区 Cookie;拒绝返回 HttpOnly Cookie',
|
|
709
|
+
params: [
|
|
710
|
+
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
711
|
+
{
|
|
712
|
+
name: 'name',
|
|
713
|
+
type: 'string',
|
|
714
|
+
required: true,
|
|
715
|
+
description: '要读取的 Cookie 名称;不支持列举全部 Cookie',
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
name: 'domain',
|
|
719
|
+
type: 'string',
|
|
720
|
+
description: '可选精确域过滤,只允许当前 host 或其父域',
|
|
721
|
+
},
|
|
722
|
+
{ name: 'path', type: 'string', description: '可选精确 path 过滤' },
|
|
723
|
+
],
|
|
724
|
+
examples: [
|
|
725
|
+
`get_cookie --payload '{"tabId":12345,"name":"feature_flag"}'`,
|
|
726
|
+
`get_cookie --payload '{"tabId":12345,"name":"edith_switch_br.rx-vehicle-search_home","domain":"taobao.com"}'`,
|
|
727
|
+
],
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: 'set_cookie',
|
|
731
|
+
description: '设置当前标签页域下的一个指定非分区 session Cookie,可选刷新页面',
|
|
732
|
+
params: [
|
|
733
|
+
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
734
|
+
{
|
|
735
|
+
name: 'name',
|
|
736
|
+
type: 'string',
|
|
737
|
+
required: true,
|
|
738
|
+
description: 'Cookie 名称',
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: 'value',
|
|
742
|
+
type: 'string',
|
|
743
|
+
required: true,
|
|
744
|
+
description: 'Cookie 值,可传空字符串;空字符串不会删除 Cookie',
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: 'domain',
|
|
748
|
+
type: 'string',
|
|
749
|
+
description: 'Cookie 域,默认当前 host;只允许当前 host 或其父域',
|
|
750
|
+
},
|
|
751
|
+
{ name: 'path', type: 'string', description: 'Cookie path,默认 /' },
|
|
752
|
+
{ name: 'reload', type: 'boolean', description: '设置后是否刷新页面,默认 false' },
|
|
753
|
+
],
|
|
754
|
+
examples: [
|
|
755
|
+
`set_cookie --payload '{"tabId":12345,"name":"feature_flag","value":"enabled"}'`,
|
|
756
|
+
`set_cookie --payload '{"tabId":12345,"name":"edith_switch_br.rx-vehicle-search_home","value":"3.6.63","domain":"taobao.com","reload":true}'`,
|
|
757
|
+
],
|
|
758
|
+
},
|
|
631
759
|
// ── 其他 ──────────────────────────────────────────────────────────────────
|
|
632
760
|
{
|
|
633
761
|
name: 'page_frames',
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface GetRequestsParams {
|
|
|
31
31
|
}
|
|
32
32
|
export declare function getRequests(params?: GetRequestsParams): Promise<any>;
|
|
33
33
|
export interface GetLogsParams {
|
|
34
|
+
tabId?: number;
|
|
34
35
|
level?: 'log' | 'warn' | 'error' | 'info' | 'all';
|
|
35
36
|
since?: number;
|
|
36
37
|
limit?: number;
|
|
@@ -44,9 +45,12 @@ export interface GetEventsParams {
|
|
|
44
45
|
filter?: string;
|
|
45
46
|
since?: number;
|
|
46
47
|
limit?: number;
|
|
48
|
+
/** Include full tracking URL, raw payload, and parsed context. Defaults to false. */
|
|
49
|
+
includeRaw?: boolean;
|
|
47
50
|
}
|
|
48
51
|
export declare function getEvents(params?: GetEventsParams): Promise<any>;
|
|
49
52
|
export interface SetMockParams {
|
|
53
|
+
tabId?: number;
|
|
50
54
|
apiName: string;
|
|
51
55
|
mockData?: any;
|
|
52
56
|
fields?: Array<{
|
|
@@ -61,6 +65,7 @@ export interface SetMockParams {
|
|
|
61
65
|
}
|
|
62
66
|
export declare function setMock(params: SetMockParams): Promise<any>;
|
|
63
67
|
export interface GetMocksParams {
|
|
68
|
+
tabId?: number;
|
|
64
69
|
apiName?: string;
|
|
65
70
|
}
|
|
66
71
|
export declare function getMocks(params?: GetMocksParams): Promise<any>;
|
|
@@ -152,6 +157,23 @@ export interface SelectBrowserResult {
|
|
|
152
157
|
note?: string;
|
|
153
158
|
}
|
|
154
159
|
export declare function selectBrowser(params?: SelectBrowserParams): Promise<SelectBrowserResult>;
|
|
160
|
+
export interface BrowserReleaseFailure {
|
|
161
|
+
tabId: number;
|
|
162
|
+
stage: string;
|
|
163
|
+
error: string;
|
|
164
|
+
}
|
|
165
|
+
export interface BrowserReleaseResult {
|
|
166
|
+
released: boolean;
|
|
167
|
+
hadActiveControl: boolean;
|
|
168
|
+
transports: Array<'extension' | 'cdp'>;
|
|
169
|
+
affectedTabIds: number[];
|
|
170
|
+
failures?: BrowserReleaseFailure[];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 释放当前目标浏览器的 debugger/CDP 控制,保留浏览器和全部标签页。
|
|
174
|
+
* 下一次浏览器操作仍可自动重新建立控制。
|
|
175
|
+
*/
|
|
176
|
+
export declare function browserRelease(): Promise<BrowserReleaseResult>;
|
|
155
177
|
export interface BrowserLaunchParams {
|
|
156
178
|
name: string;
|
|
157
179
|
headless?: boolean;
|
|
@@ -245,6 +267,7 @@ export declare function tabList(): Promise<any>;
|
|
|
245
267
|
* 页面交互动作(click/type/hover/scroll/press/upload)默认内置观察:
|
|
246
268
|
* 同一次调用内执行动作并等待异步稳定,返回 `{ action, observation }`。
|
|
247
269
|
* 传 `observe: false` 关闭观察,仅执行裸动作并直接返回其结果。
|
|
270
|
+
* page_eval 默认裸执行,显式传 observe 对象时才进入同一观察链路。
|
|
248
271
|
*/
|
|
249
272
|
export interface PageObserveOptions {
|
|
250
273
|
quietMs?: number;
|
|
@@ -257,7 +280,7 @@ export interface PageObserveOptions {
|
|
|
257
280
|
}
|
|
258
281
|
export interface PageClickParams {
|
|
259
282
|
tabId: number;
|
|
260
|
-
/** 原生 CSS
|
|
283
|
+
/** 原生 CSS 选择器,或 snapshot / 页面动作 observation 返回的 @eN ref。 */
|
|
261
284
|
selector?: string;
|
|
262
285
|
/** 按可见文本(AX name)定位;可配合 role/exact 消歧。 */
|
|
263
286
|
text?: string;
|
|
@@ -270,11 +293,14 @@ export interface PageClickParams {
|
|
|
270
293
|
role?: string;
|
|
271
294
|
/** 配合 text,默认 true(精确匹配),false 时子串匹配。 */
|
|
272
295
|
exact?: boolean;
|
|
296
|
+
/** 配合 text,把 AX 文本匹配限制在指定 CSS 元素或 @eN ref 子树内。 */
|
|
297
|
+
scope?: string;
|
|
273
298
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
299
|
+
* 点击模式。auto 会在可见页按设备模拟状态选择可信 mouse/touch,在隐藏页
|
|
300
|
+
* 使用 DOM fallback;checkbox/radio 使用带状态校验的 DOM 路径。
|
|
301
|
+
* dom/mouse/touch 可强制指定实际派发方式。
|
|
276
302
|
*/
|
|
277
|
-
|
|
303
|
+
clickMode?: 'auto' | 'dom' | 'mouse' | 'touch';
|
|
278
304
|
/** 观察选项;传 false 关闭内置观察。 */
|
|
279
305
|
observe?: PageObserveOptions | false;
|
|
280
306
|
}
|
|
@@ -290,8 +316,12 @@ export interface PageTypeParams {
|
|
|
290
316
|
export declare function pageType(params: PageTypeParams): Promise<any>;
|
|
291
317
|
export interface PageScrollParams {
|
|
292
318
|
tabId: number;
|
|
293
|
-
direction:
|
|
319
|
+
direction: 'up' | 'down' | 'top' | 'bottom';
|
|
294
320
|
distance?: number;
|
|
321
|
+
/** 原生 CSS 选择器或 page_snapshot 返回的 @eN ref;不传时滚动页面。 */
|
|
322
|
+
selector?: string;
|
|
323
|
+
/** selector 指向子节点时,是否自动使用最近的可滚动祖先。默认 self。 */
|
|
324
|
+
containerPolicy?: 'self' | 'nearest';
|
|
295
325
|
/** 观察选项;传 false 关闭内置观察。 */
|
|
296
326
|
observe?: PageObserveOptions | false;
|
|
297
327
|
}
|
|
@@ -308,14 +338,32 @@ export interface PageEvalParams {
|
|
|
308
338
|
expression: string;
|
|
309
339
|
frameId?: string;
|
|
310
340
|
awaitPromise?: boolean;
|
|
341
|
+
/** 显式传入时观察表达式引发的页面变化;不传保持轻量裸执行。 */
|
|
342
|
+
observe?: PageObserveOptions;
|
|
311
343
|
}
|
|
312
344
|
export declare function pageEval(params: PageEvalParams): Promise<any>;
|
|
345
|
+
export interface PageSnapshotQuery {
|
|
346
|
+
/** 按 accessibility name 匹配。 */
|
|
347
|
+
text?: string;
|
|
348
|
+
/** 按 AX role 匹配。 */
|
|
349
|
+
role?: string;
|
|
350
|
+
/** 默认 true 精确匹配;false 时按子串匹配 text。 */
|
|
351
|
+
exact?: boolean;
|
|
352
|
+
}
|
|
313
353
|
export interface PageSnapshotParams {
|
|
314
354
|
tabId: number;
|
|
315
355
|
depth?: number;
|
|
316
356
|
maxChars?: number;
|
|
317
357
|
maxNodes?: number;
|
|
358
|
+
/** CSS 根元素;与 rootRef 互斥。 */
|
|
318
359
|
rootSelector?: string;
|
|
360
|
+
/** 以已有 @eN ref 为根获取局部 AX 子树;与 rootSelector 互斥。 */
|
|
361
|
+
rootRef?: string;
|
|
362
|
+
/** rootRef 向上扩展的 DOM 祖先层数,默认 0,最大 20。 */
|
|
363
|
+
ancestorDepth?: number;
|
|
364
|
+
/** 服务端过滤 AX 树,只保留命中节点及其祖先路径。 */
|
|
365
|
+
query?: PageSnapshotQuery;
|
|
366
|
+
/** full 为完整树;interactive 为当前视口内的语义控件;viewport 为当前可视区域。 */
|
|
319
367
|
mode?: 'full' | 'interactive' | 'viewport';
|
|
320
368
|
}
|
|
321
369
|
export declare function pageSnapshot(params: PageSnapshotParams): Promise<any>;
|
|
@@ -339,10 +387,12 @@ export interface PageWaitParams {
|
|
|
339
387
|
export declare function pageWait(params: PageWaitParams): Promise<any>;
|
|
340
388
|
export interface PageNavigateParams {
|
|
341
389
|
tabId: number;
|
|
342
|
-
/** 要导航到的 URL
|
|
390
|
+
/** 要导航到的 URL;与 refresh/history 互斥。 */
|
|
343
391
|
url?: string;
|
|
344
|
-
/** 为 true
|
|
392
|
+
/** 为 true 时直接重新加载当前页面;与 url/history 互斥。 */
|
|
345
393
|
refresh?: boolean;
|
|
394
|
+
/** 沿当前标签页的浏览历史后退或前进;与 url/refresh 互斥。 */
|
|
395
|
+
history?: 'back' | 'forward';
|
|
346
396
|
}
|
|
347
397
|
export declare function pageNavigate(params: PageNavigateParams): Promise<any>;
|
|
348
398
|
export interface PageUploadParams {
|
|
@@ -388,6 +438,47 @@ export interface SetTimezoneResult {
|
|
|
388
438
|
timezone: string | null;
|
|
389
439
|
}
|
|
390
440
|
export declare function setTimezone(params: SetTimezoneParams): Promise<SetTimezoneResult>;
|
|
441
|
+
export interface GetCookieParams {
|
|
442
|
+
tabId: number;
|
|
443
|
+
/** Exact non-partitioned cookie name. HttpOnly cookies cannot be read. */
|
|
444
|
+
name: string;
|
|
445
|
+
/** Optional exact domain filter. */
|
|
446
|
+
domain?: string;
|
|
447
|
+
/** Optional exact path filter. */
|
|
448
|
+
path?: string;
|
|
449
|
+
}
|
|
450
|
+
export interface GetCookieResult {
|
|
451
|
+
tabId?: number;
|
|
452
|
+
url: string;
|
|
453
|
+
name: string;
|
|
454
|
+
found: boolean;
|
|
455
|
+
value?: string;
|
|
456
|
+
domain?: string;
|
|
457
|
+
path?: string;
|
|
458
|
+
}
|
|
459
|
+
export declare function getCookie(params: GetCookieParams): Promise<GetCookieResult>;
|
|
460
|
+
export interface SetCookieParams {
|
|
461
|
+
tabId: number;
|
|
462
|
+
/** Cookie name. */
|
|
463
|
+
name: string;
|
|
464
|
+
/** Cookie value. An empty value does not delete the cookie. */
|
|
465
|
+
value: string;
|
|
466
|
+
/** Cookie domain; defaults to the current host. */
|
|
467
|
+
domain?: string;
|
|
468
|
+
/** Cookie path; defaults to "/". */
|
|
469
|
+
path?: string;
|
|
470
|
+
/** Reload the page after setting the cookie. Defaults to false. */
|
|
471
|
+
reload?: boolean;
|
|
472
|
+
}
|
|
473
|
+
export interface SetCookieResult {
|
|
474
|
+
tabId?: number;
|
|
475
|
+
url: string;
|
|
476
|
+
name: string;
|
|
477
|
+
domain: string;
|
|
478
|
+
path: string;
|
|
479
|
+
reloaded: boolean;
|
|
480
|
+
}
|
|
481
|
+
export declare function setCookie(params: SetCookieParams): Promise<SetCookieResult>;
|
|
391
482
|
export interface GetUserInfoParams {
|
|
392
483
|
detail?: boolean;
|
|
393
484
|
}
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,13 @@ export function browserList() {
|
|
|
50
50
|
export function selectBrowser(params = {}) {
|
|
51
51
|
return invoke('browser_select_browser', params);
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* 释放当前目标浏览器的 debugger/CDP 控制,保留浏览器和全部标签页。
|
|
55
|
+
* 下一次浏览器操作仍可自动重新建立控制。
|
|
56
|
+
*/
|
|
57
|
+
export function browserRelease() {
|
|
58
|
+
return invoke('browser_release', {});
|
|
59
|
+
}
|
|
53
60
|
export function browserLaunch(params) {
|
|
54
61
|
return invoke('browser_launch', params);
|
|
55
62
|
}
|
|
@@ -117,6 +124,12 @@ export function setDeviceEmulation(params) {
|
|
|
117
124
|
export function setTimezone(params) {
|
|
118
125
|
return invoke('set_timezone', params);
|
|
119
126
|
}
|
|
127
|
+
export function getCookie(params) {
|
|
128
|
+
return invoke('get_cookie', params);
|
|
129
|
+
}
|
|
130
|
+
export function setCookie(params) {
|
|
131
|
+
return invoke('set_cookie', params);
|
|
132
|
+
}
|
|
120
133
|
export function getUserInfo(params = {}) {
|
|
121
134
|
return invoke('get_user_info', params);
|
|
122
135
|
}
|
package/dist/socket.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface InvokeOptions {
|
|
|
20
20
|
transport?: 'cdp';
|
|
21
21
|
/** Target a specific browser by id / name (see the browser registry). */
|
|
22
22
|
browser?: string;
|
|
23
|
+
/** Identifies whether browser control originated locally or through the cloud connector. */
|
|
24
|
+
controlSource?: 'local' | 'cloud';
|
|
23
25
|
}
|
|
24
26
|
/** Pin the default target browser for subsequent invokes (undefined clears it). */
|
|
25
27
|
export declare function setDefaultBrowser(selector: string | undefined): void;
|
package/dist/socket.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 当 Socket 文件不存在(ENOENT)时,自动尝试拉起 native host,稍后重试连接。
|
|
8
8
|
*/
|
|
9
9
|
import net from 'node:net';
|
|
10
|
+
import fs from 'node:fs';
|
|
10
11
|
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { createRequire } from 'node:module';
|
|
@@ -15,6 +16,7 @@ import { listLiveBrowsers, findBrowser, describeBrowser } from './browserRegistr
|
|
|
15
16
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
export const CLIENT_VERSION = require('../package.json').version;
|
|
19
|
+
const MAX_SOCKET_BUFFER_SIZE = 64 * 1024 * 1024;
|
|
18
20
|
/**
|
|
19
21
|
* Legacy well-known socket. New native-hosts publish per-browser sockets in the
|
|
20
22
|
* registry and we address those; this path is a fallback for pre-registry hosts
|
|
@@ -79,8 +81,8 @@ let spawnAttempted = false;
|
|
|
79
81
|
* Inventory-only hosts serve one browser_list request without publishing a fake CDP target.
|
|
80
82
|
* Returns true if a spawn was attempted (regardless of success).
|
|
81
83
|
*/
|
|
82
|
-
async function trySpawnNativeHost(registerBrowser = true) {
|
|
83
|
-
if (registerBrowser && spawnAttempted)
|
|
84
|
+
async function trySpawnNativeHost(registerBrowser = true, retryPreviousAttempt = false) {
|
|
85
|
+
if (registerBrowser && spawnAttempted && !retryPreviousAttempt)
|
|
84
86
|
return false;
|
|
85
87
|
if (registerBrowser)
|
|
86
88
|
spawnAttempted = true;
|
|
@@ -128,20 +130,37 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
128
130
|
return Promise.reject(error);
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
|
-
const { timeoutSec = resolveActionTimeoutSec(action, data), transport, browser } = options;
|
|
133
|
+
const { timeoutSec = resolveActionTimeoutSec(action, data), transport, browser, controlSource = 'local', } = options;
|
|
132
134
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
133
135
|
const resolvedData = resolveFileRefs(data);
|
|
134
136
|
if (action === 'browser_list') {
|
|
135
|
-
return dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined).then(decorateBrowserListResult);
|
|
137
|
+
return dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(decorateBrowserListResult);
|
|
136
138
|
}
|
|
137
139
|
const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
138
|
-
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector);
|
|
140
|
+
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
|
|
139
141
|
}
|
|
140
142
|
function isConnError(error) {
|
|
141
143
|
const code = error?.code;
|
|
142
144
|
return code === 'ENOENT' || code === 'ECONNREFUSED';
|
|
143
145
|
}
|
|
144
|
-
|
|
146
|
+
function isSocketSandboxError(error) {
|
|
147
|
+
return error?.code === 'EPERM';
|
|
148
|
+
}
|
|
149
|
+
async function dispatchInvoke(action, data, timeoutSec, transport, selector, controlSource) {
|
|
150
|
+
const configuredSocketPath = process.env.MEARL_SOCKET_PATH?.trim();
|
|
151
|
+
if (!selector && configuredSocketPath) {
|
|
152
|
+
try {
|
|
153
|
+
return await invokeOnce(action, data, timeoutSec, transport, configuredSocketPath, controlSource);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (isConnError(error) || isSocketSandboxError(error)) {
|
|
157
|
+
throw new Error(formatConnectError(error, configuredSocketPath), {
|
|
158
|
+
cause: error,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
145
164
|
const browsers = listLiveBrowsers();
|
|
146
165
|
// Explicit target: must match a live browser; do not silently fall back.
|
|
147
166
|
if (selector) {
|
|
@@ -150,10 +169,10 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector) {
|
|
|
150
169
|
throw new Error(formatNoMatchError(selector, browsers));
|
|
151
170
|
}
|
|
152
171
|
try {
|
|
153
|
-
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath);
|
|
172
|
+
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource);
|
|
154
173
|
}
|
|
155
174
|
catch (error) {
|
|
156
|
-
if (isConnError(error)) {
|
|
175
|
+
if (isConnError(error) || isSocketSandboxError(error)) {
|
|
157
176
|
throw new Error(formatConnectError(error, match.socketPath), {
|
|
158
177
|
cause: error,
|
|
159
178
|
});
|
|
@@ -165,42 +184,56 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector) {
|
|
|
165
184
|
// stale socket, fall through to the legacy path + auto-spawn.
|
|
166
185
|
if (browsers.length > 0) {
|
|
167
186
|
try {
|
|
168
|
-
return await invokeOnce(action, data, timeoutSec, transport, browsers[0].socketPath);
|
|
187
|
+
return await invokeOnce(action, data, timeoutSec, transport, browsers[0].socketPath, controlSource);
|
|
169
188
|
}
|
|
170
189
|
catch (error) {
|
|
190
|
+
if (isSocketSandboxError(error)) {
|
|
191
|
+
throw new Error(formatConnectError(error, browsers[0].socketPath), { cause: error });
|
|
192
|
+
}
|
|
171
193
|
if (!isConnError(error))
|
|
172
194
|
throw error;
|
|
173
195
|
}
|
|
174
196
|
}
|
|
175
|
-
return invokeViaLegacy(action, data, timeoutSec, transport, /* isRetry */ false);
|
|
197
|
+
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
|
|
176
198
|
}
|
|
177
199
|
/** Legacy well-known socket, auto-spawning a native-host on ENOENT and retrying once. */
|
|
178
|
-
async function invokeViaLegacy(action, data, timeoutSec, transport, isRetry) {
|
|
200
|
+
async function invokeViaLegacy(action, data, timeoutSec, transport, controlSource, isRetry) {
|
|
179
201
|
try {
|
|
180
|
-
return await invokeOnce(action, data, timeoutSec, transport, SOCKET_PATH);
|
|
202
|
+
return await invokeOnce(action, data, timeoutSec, transport, SOCKET_PATH, controlSource);
|
|
181
203
|
}
|
|
182
204
|
catch (error) {
|
|
183
205
|
const err = error;
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
const shouldAttemptSpawn = err.code === 'ENOENT' || (action === 'browser_release' && err.code === 'ECONNREFUSED');
|
|
207
|
+
if (shouldAttemptSpawn && !isRetry) {
|
|
208
|
+
const spawned = await trySpawnNativeHost(action !== 'browser_list', action === 'browser_release');
|
|
186
209
|
if (spawned) {
|
|
187
210
|
// Wait for the native host to start listening; persistent hosts also publish their registry record.
|
|
188
211
|
await new Promise(r => setTimeout(r, 1200));
|
|
189
212
|
const browsers = listLiveBrowsers();
|
|
190
213
|
if (browsers.length > 0) {
|
|
191
|
-
return invokeOnce(action, data, timeoutSec, transport, browsers[0].socketPath);
|
|
214
|
+
return invokeOnce(action, data, timeoutSec, transport, browsers[0].socketPath, controlSource);
|
|
192
215
|
}
|
|
193
|
-
return invokeViaLegacy(action, data, timeoutSec, transport,
|
|
216
|
+
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
|
|
217
|
+
/* isRetry */ true);
|
|
194
218
|
}
|
|
195
219
|
}
|
|
196
|
-
if (err instanceof Error && !isConnError(err))
|
|
220
|
+
if (err instanceof Error && !isConnError(err) && !isSocketSandboxError(err))
|
|
197
221
|
throw err;
|
|
198
222
|
throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
|
|
199
223
|
}
|
|
200
224
|
}
|
|
201
|
-
function invokeOnce(action, data, timeoutSec, transport, socketPath) {
|
|
225
|
+
function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource) {
|
|
202
226
|
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
203
227
|
return new Promise((resolve, reject) => {
|
|
228
|
+
if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
|
|
229
|
+
const ownerUid = fs.statSync(socketPath).uid;
|
|
230
|
+
if (ownerUid !== process.getuid()) {
|
|
231
|
+
const error = new Error(`拒绝连接不属于当前用户的 Socket: ${socketPath}`);
|
|
232
|
+
Object.assign(error, { code: 'EACCES' });
|
|
233
|
+
reject(error);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
204
237
|
const socket = net.createConnection(socketPath);
|
|
205
238
|
let settled = false;
|
|
206
239
|
let buffer = '';
|
|
@@ -220,13 +253,23 @@ function invokeOnce(action, data, timeoutSec, transport, socketPath) {
|
|
|
220
253
|
settle(new Error(`请求超时,已等待 ${timeoutSec} 秒`));
|
|
221
254
|
}, timeoutSec * 1000);
|
|
222
255
|
socket.on('connect', () => {
|
|
223
|
-
const message = {
|
|
256
|
+
const message = {
|
|
257
|
+
id,
|
|
258
|
+
action,
|
|
259
|
+
data,
|
|
260
|
+
version: CLIENT_VERSION,
|
|
261
|
+
controlSource,
|
|
262
|
+
};
|
|
224
263
|
if (transport)
|
|
225
264
|
message.transport = transport;
|
|
226
265
|
socket.write(JSON.stringify(message) + '\n');
|
|
227
266
|
});
|
|
228
267
|
socket.on('data', chunk => {
|
|
229
268
|
buffer += chunk.toString('utf-8');
|
|
269
|
+
if (Buffer.byteLength(buffer, 'utf-8') > MAX_SOCKET_BUFFER_SIZE) {
|
|
270
|
+
settle(new Error(`Socket 响应超过 ${MAX_SOCKET_BUFFER_SIZE} 字节限制`));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
230
273
|
let newlineIndex;
|
|
231
274
|
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
232
275
|
const line = buffer.slice(0, newlineIndex);
|
|
@@ -289,6 +332,14 @@ function formatConnectError(error, socketPath) {
|
|
|
289
332
|
installHint,
|
|
290
333
|
].join('\n');
|
|
291
334
|
}
|
|
335
|
+
if (error.code === 'EPERM') {
|
|
336
|
+
return [
|
|
337
|
+
`Socket 连接被执行环境拦截: ${socketPath}`,
|
|
338
|
+
'当前进程没有访问该 Mearl Unix Socket 的权限(EPERM)。',
|
|
339
|
+
'如果命令运行在 Agent 沙箱中,请为这个 Socket 配置精确的 Unix Socket 白名单后重试。',
|
|
340
|
+
'无需重新安装浏览器插件,也不要使用 sudo 或改写 Socket 权限。',
|
|
341
|
+
].join('\n');
|
|
342
|
+
}
|
|
292
343
|
return [`Socket 连接失败: ${error.message}`, '', installHint].join('\n');
|
|
293
344
|
}
|
|
294
345
|
/** Error message when an explicit --browser selector matches no live browser. */
|