@mearl/client 2.19.3 → 2.20.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 +23 -0
- package/dist/actionTimeouts.js +2 -0
- package/dist/artifacts.d.ts +17 -0
- package/dist/artifacts.js +80 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +27 -2
- package/dist/commands.d.ts +65 -2
- package/dist/commands.js +78 -1
- package/dist/generated-browser-action-protocol.d.ts +115 -6
- package/dist/generated-browser-action-protocol.js +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +15 -3
- package/dist/pluginPageBundle.js +47 -0
- package/dist/socket.d.ts +1 -0
- package/dist/socket.js +28 -8
- package/dist/unified.d.ts +17 -0
- package/dist/unified.js +117 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -72,6 +72,29 @@ mearl browser_list
|
|
|
72
72
|
mearl browser_release
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
### 控制会话与文件
|
|
76
|
+
|
|
77
|
+
多步骤任务首次 `tab_open` 或 `page_snapshot` 使用 `--session new`,响应在原结果旁附带 `session`,后续用其 `sessionId` 自动路由,无需额外创建/查询 action。Session 绑定一个浏览器与传输;页面首次使用自动关联,复用页面与新建页面分别记录。同一页面只允许一个会话控制,旧调用不能绕过占用;Session 不隔离 Cookie 或存储。
|
|
78
|
+
|
|
79
|
+
旧调用不必传 `--session`,现有动作、`run_actions` 批次和上传 `filePaths` 参数继续可用。活动会话存在时,旧调用不能操作其占用页面、执行浏览器级释放/关闭或修改共享设置;旧批次中的开页/关闭浏览器需改为独立调用。CDP Mock 按标签页生效,跨页使用同一 Mock 时需分别配置。
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mearl tab_open --session new --payload '{"url":"https://example.com"}'
|
|
83
|
+
mearl page_snapshot --session '<sessionId>' --payload '{"tabId":12345}'
|
|
84
|
+
mearl browser_control --session '<sessionId>' --payload '{"operation":"handoff","reason":"请完成扫码"}'
|
|
85
|
+
mearl page_download --session '<sessionId>' --payload '{"tabId":12345,"trigger":{"selector":"#export"}}' --output /absolute/path/report.csv
|
|
86
|
+
mearl page_upload --session '<sessionId>' --payload '{"tabId":12345,"selector":"input[type=file]","files":[{"filePath":"/absolute/path/report.csv"}]}'
|
|
87
|
+
mearl browser_release --session '<sessionId>' --payload '{"closeCreatedTabs":true,"keepTabIds":[12345]}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
用户在 Popup 的浏览器管理页接管、交还或结束会话。交接后排队旧请求和批次后续步骤停止;已派发动作可能仍然生效。交还后重新取快照。CDP-only 环境可在用户明确确认后用同一 `browser_control` 的 `operation:"resume"` 交还,无需额外 action。会话跨 CLI 连接保留,宿主进程退出后失效;不会根据空闲时间清理用户正在操作的会话。
|
|
91
|
+
|
|
92
|
+
带 Session 的释放只影响该会话,默认保留页面及环境;`closeCreatedTabs` 只关闭新建页,`keepTabIds` 保留结果页,复用的用户页面始终保留。`preserveEnvironment:false` 清理这些页面的环境。活动会话存在时,浏览器级释放与关闭会被拒绝。
|
|
93
|
+
|
|
94
|
+
`page_download` 先监听再点击,返回浏览器文件产物;CLI `--output` 或 SDK `saveArtifact(artifact, absolutePath, { session })` 以 256 KiB 分块导出到调用方。`page_upload.files` 接受调用方绝对路径 `{filePath}` 或浏览器 `{artifactId}`,`filePaths` 仍指浏览器宿主路径。单文件最多 256 MiB;保存到调用方的结果独立于临时产物。会话释放清理临时产物,`keepArtifactIds` 可保留指定产物;无会话产物空闲一小时回收,宿主退出清理暂存。
|
|
95
|
+
|
|
96
|
+
扩展捕获下载需要匹配来源页 referrer 或明确的预期 `url`,无法归属时不会认领最近下载。CDP 只在托管浏览器中使用临时下载目录,共享浏览器保持默认目录,Chrome 未返回完成文件路径时明确报错。下载超时可能已经触发,需检查现场再决定下一步。当前会话与下载适配 Extension/CDP;直接处理页面动作的外部 Provider 对不支持的能力明确报错。
|
|
97
|
+
|
|
75
98
|
### 外部浏览器 Provider
|
|
76
99
|
|
|
77
100
|
Provider 浏览器与扩展、CDP 浏览器一起出现在 `browser_list` 中,并使用同一个
|
package/dist/actionTimeouts.js
CHANGED
|
@@ -29,6 +29,8 @@ export function resolveActionTimeoutSec(action, data, fallback = 15) {
|
|
|
29
29
|
if (action === 'send_mtop_request') {
|
|
30
30
|
return Math.max(fallback, Math.ceil(((data.timeout ?? 10000) + 5000) / 1000));
|
|
31
31
|
}
|
|
32
|
+
if (action === 'page_download')
|
|
33
|
+
return Math.max(fallback, Math.ceil((Number(data.timeout ?? 30000) + 10000) / 1000));
|
|
32
34
|
if (action === 'page_wait') {
|
|
33
35
|
const waitMs = data.time ?? data.timeout ?? 10000;
|
|
34
36
|
return Math.max(fallback, Math.ceil((Number(waitMs) + 5000) / 1000));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BrowserArtifact } from './generated-browser-action-protocol.js';
|
|
2
|
+
import type { UnifiedInvokeOptions } from './unified.js';
|
|
3
|
+
interface Invoker {
|
|
4
|
+
invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
5
|
+
}
|
|
6
|
+
export declare function stageFile(client: Invoker, filePath: string, options: UnifiedInvokeOptions): Promise<{
|
|
7
|
+
artifactId: string;
|
|
8
|
+
options: UnifiedInvokeOptions;
|
|
9
|
+
}>;
|
|
10
|
+
/** Atomically export browser-host data to the calling machine without large JSON responses. */
|
|
11
|
+
export declare function exportArtifact(client: Invoker, artifact: BrowserArtifact & {
|
|
12
|
+
browserId?: string;
|
|
13
|
+
}, outputPath: string, options?: UnifiedInvokeOptions): Promise<{
|
|
14
|
+
outputPath: string;
|
|
15
|
+
bytes: number;
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { open, mkdir, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
const CHUNK_BYTES = 256 * 1024;
|
|
5
|
+
export async function stageFile(client, filePath, options) {
|
|
6
|
+
if (!path.isAbsolute(filePath))
|
|
7
|
+
throw new Error('Caller-local file paths must be absolute');
|
|
8
|
+
const source = await open(filePath, 'r');
|
|
9
|
+
let id;
|
|
10
|
+
let pinned = options;
|
|
11
|
+
try {
|
|
12
|
+
const metadata = await source.stat();
|
|
13
|
+
if (!metadata.isFile() || metadata.size > 256 * 1024 * 1024)
|
|
14
|
+
throw new Error('Upload requires a regular file of at most 256 MiB');
|
|
15
|
+
const created = await client.invoke('__artifact_init', { filename: path.basename(filePath) }, options);
|
|
16
|
+
pinned = { ...options, browser: created.browserId };
|
|
17
|
+
id = created.artifact.artifactId;
|
|
18
|
+
let offset = 0;
|
|
19
|
+
for await (const chunk of source.createReadStream({
|
|
20
|
+
highWaterMark: CHUNK_BYTES,
|
|
21
|
+
autoClose: false,
|
|
22
|
+
})) {
|
|
23
|
+
await client.invoke('__artifact_write', { artifactId: id, offset, base64: chunk.toString('base64') }, pinned);
|
|
24
|
+
offset += chunk.length;
|
|
25
|
+
}
|
|
26
|
+
await client.invoke('__artifact_complete', { artifactId: id, bytes: offset }, pinned);
|
|
27
|
+
return { artifactId: id, options: pinned };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (id)
|
|
31
|
+
await client.invoke('__artifact_remove', { artifactId: id }, pinned).catch(() => { });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await source.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Atomically export browser-host data to the calling machine without large JSON responses. */
|
|
39
|
+
export async function exportArtifact(client, artifact, outputPath, options = {}) {
|
|
40
|
+
if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0)
|
|
41
|
+
throw new Error('Invalid artifact size');
|
|
42
|
+
if (!path.isAbsolute(outputPath))
|
|
43
|
+
throw new Error('Artifact output paths must be absolute');
|
|
44
|
+
if (!options.session && !artifact.browserId && !options.browser)
|
|
45
|
+
throw new Error('An artifact requires its originating browser');
|
|
46
|
+
const pinned = { ...options, ...(artifact.browserId ? { browser: artifact.browserId } : {}) };
|
|
47
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
48
|
+
const temporary = `${outputPath}.${randomUUID()}.part`;
|
|
49
|
+
const target = await open(temporary, 'wx', 0o600);
|
|
50
|
+
try {
|
|
51
|
+
let offset = 0;
|
|
52
|
+
if (artifact.bytes === 0) {
|
|
53
|
+
const empty = await client.invoke('__artifact_read', { artifactId: artifact.artifactId, offset: 0 }, pinned);
|
|
54
|
+
if (empty.totalBytes !== 0 || empty.bytes !== 0 || empty.base64 !== '')
|
|
55
|
+
throw new Error('Artifact export size mismatch');
|
|
56
|
+
}
|
|
57
|
+
while (offset < artifact.bytes) {
|
|
58
|
+
const chunk = await client.invoke('__artifact_read', { artifactId: artifact.artifactId, offset }, pinned);
|
|
59
|
+
const bytes = Buffer.from(chunk.base64, 'base64');
|
|
60
|
+
if (!bytes.length ||
|
|
61
|
+
bytes.length !== chunk.bytes ||
|
|
62
|
+
chunk.totalBytes !== artifact.bytes ||
|
|
63
|
+
offset + bytes.length > artifact.bytes)
|
|
64
|
+
throw new Error('Invalid artifact chunk or truncated transfer');
|
|
65
|
+
await target.write(bytes);
|
|
66
|
+
offset += bytes.length;
|
|
67
|
+
}
|
|
68
|
+
await target.sync();
|
|
69
|
+
await target.close();
|
|
70
|
+
if ((await stat(temporary)).size !== artifact.bytes)
|
|
71
|
+
throw new Error('Artifact export size mismatch');
|
|
72
|
+
await rename(temporary, outputPath);
|
|
73
|
+
return { outputPath, bytes: artifact.bytes };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
await target.close().catch(() => { });
|
|
77
|
+
await rm(temporary, { force: true });
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ const CATEGORIES = [
|
|
|
38
38
|
'page_wait',
|
|
39
39
|
'page_navigate',
|
|
40
40
|
'page_upload',
|
|
41
|
+
'page_download',
|
|
41
42
|
'page_eval',
|
|
42
43
|
],
|
|
43
44
|
},
|
|
@@ -48,7 +49,13 @@ const CATEGORIES = [
|
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
51
|
title: '浏览器',
|
|
51
|
-
names: [
|
|
52
|
+
names: [
|
|
53
|
+
'browser_list',
|
|
54
|
+
'browser_launch',
|
|
55
|
+
'browser_release',
|
|
56
|
+
'browser_close',
|
|
57
|
+
'browser_control',
|
|
58
|
+
],
|
|
52
59
|
},
|
|
53
60
|
];
|
|
54
61
|
const BUILTIN_COMMANDS = [
|
|
@@ -119,6 +126,7 @@ function printUsage(commandName, version) {
|
|
|
119
126
|
' --timeout <seconds> 超时时间(默认按 action 推导)',
|
|
120
127
|
' --compact stdout 输出紧凑格式 JSON',
|
|
121
128
|
' --output <path> 保存输出或截图',
|
|
129
|
+
' --session <new|id> 首次建立或复用控制会话',
|
|
122
130
|
' --browser <id|名称> 指定 browser_list 返回的浏览器',
|
|
123
131
|
'',
|
|
124
132
|
'其他选项:',
|
|
@@ -422,6 +430,7 @@ export function parseClientArgs(args, plugin = false) {
|
|
|
422
430
|
.option('--json')
|
|
423
431
|
.option('--output <path>')
|
|
424
432
|
.option('--browser <id-or-name>')
|
|
433
|
+
.option('--session <new-or-id>')
|
|
425
434
|
.option('--connector <id-or-name>')
|
|
426
435
|
.option('--local')
|
|
427
436
|
.option('--updates')
|
|
@@ -484,6 +493,7 @@ export function parseClientArgs(args, plugin = false) {
|
|
|
484
493
|
updates: options.updates === true,
|
|
485
494
|
...(outputPath ? { outputPath } : {}),
|
|
486
495
|
...(options.browser ? { browser: options.browser } : {}),
|
|
496
|
+
...(options.session ? { session: options.session } : {}),
|
|
487
497
|
...(connector ? { connector } : {}),
|
|
488
498
|
...resolveServer(options.server),
|
|
489
499
|
...(plugin
|
|
@@ -512,6 +522,7 @@ async function runPluginCli(args, commandName) {
|
|
|
512
522
|
const invokeOptions = {
|
|
513
523
|
...(parsed.timeoutOption ? { timeoutSec: parsed.timeoutSec } : {}),
|
|
514
524
|
...(parsed.browser ? { browser: parsed.browser } : {}),
|
|
525
|
+
...(parsed.session ? { session: parsed.session } : {}),
|
|
515
526
|
...(parsed.connector ? { connector: parsed.connector } : {}),
|
|
516
527
|
...(parsed.local ? { local: true } : {}),
|
|
517
528
|
...(parsed.forceCdp ? { transport: 'cdp' } : {}),
|
|
@@ -665,6 +676,7 @@ export async function runMearlCli(args, options = {}) {
|
|
|
665
676
|
...(parsed.serverUrl ? { serverUrl: parsed.serverUrl } : {}),
|
|
666
677
|
...(parsed.serverUrlSource ? { serverUrlSource: parsed.serverUrlSource } : {}),
|
|
667
678
|
...(parsed.browser ? { browser: parsed.browser } : {}),
|
|
679
|
+
...(parsed.session ? { session: parsed.session } : {}),
|
|
668
680
|
...(parsed.connector ? { connector: parsed.connector } : {}),
|
|
669
681
|
...(parsed.local ? { local: true } : {}),
|
|
670
682
|
...(parsed.forceCdp ? { transport: 'cdp' } : {}),
|
|
@@ -684,12 +696,25 @@ export async function runMearlCli(args, options = {}) {
|
|
|
684
696
|
const result = await client.invoke(parsed.action, parsed.payload, {
|
|
685
697
|
timeoutSec: parsed.timeoutSec,
|
|
686
698
|
...(parsed.browser ? { browser: parsed.browser } : {}),
|
|
699
|
+
...(parsed.session ? { session: parsed.session } : {}),
|
|
687
700
|
...(parsed.connector ? { connector: parsed.connector } : {}),
|
|
688
701
|
...(parsed.local ? { local: true } : {}),
|
|
689
702
|
...(parsed.forceCdp ? { transport: 'cdp' } : {}),
|
|
690
703
|
});
|
|
691
|
-
if (parsed.outputPath) {
|
|
704
|
+
if (parsed.outputPath && parsed.action === 'page_download') {
|
|
705
|
+
await client.saveArtifact(result.download, resolve(parsed.outputPath), {
|
|
706
|
+
...(parsed.session ? { session: parsed.session } : {}),
|
|
707
|
+
});
|
|
708
|
+
process.stdout.write(JSON.stringify({ ...result, outputPath: resolve(parsed.outputPath) }) + '\n');
|
|
709
|
+
if (!parsed.session)
|
|
710
|
+
await client.invoke('__artifact_remove', { artifactId: result.download.artifactId }, { browser: result.download.browserId });
|
|
711
|
+
}
|
|
712
|
+
else if (parsed.outputPath) {
|
|
692
713
|
writeOutput(parsed.action, result, parsed.outputPath);
|
|
714
|
+
if (parsed.action === 'page_screenshot' && result?.session) {
|
|
715
|
+
const { data: _image, ...metadata } = result;
|
|
716
|
+
process.stdout.write(JSON.stringify({ ...metadata, outputPath: resolve(parsed.outputPath) }) + '\n');
|
|
717
|
+
}
|
|
693
718
|
}
|
|
694
719
|
else {
|
|
695
720
|
process.stdout.write(`${parsed.compact ? JSON.stringify(result) : JSON.stringify(result, null, 2)}\n`);
|
package/dist/commands.d.ts
CHANGED
|
@@ -23,6 +23,48 @@ export interface CommandDef {
|
|
|
23
23
|
result?: readonly string[];
|
|
24
24
|
}
|
|
25
25
|
export declare const COMMANDS: readonly [{
|
|
26
|
+
readonly name: "browser_control";
|
|
27
|
+
readonly description: "把控制会话交给用户;用户在 Mearl 浏览器管理页交还后重新观察页面";
|
|
28
|
+
readonly params: [{
|
|
29
|
+
readonly name: "operation";
|
|
30
|
+
readonly type: "\"handoff\" | \"resume\"";
|
|
31
|
+
readonly required: true;
|
|
32
|
+
readonly description: "交给用户操作;resume 仅在用户明确交还后使用";
|
|
33
|
+
}, {
|
|
34
|
+
readonly name: "tabId";
|
|
35
|
+
readonly type: "number";
|
|
36
|
+
readonly description: "可选的目标页面";
|
|
37
|
+
}, {
|
|
38
|
+
readonly name: "reason";
|
|
39
|
+
readonly type: "string";
|
|
40
|
+
readonly description: "用户需要处理的事项";
|
|
41
|
+
}];
|
|
42
|
+
readonly notes: readonly ["必须携带 --session;接管停止后续派发,已经发出的操作可能仍然生效。"];
|
|
43
|
+
}, {
|
|
44
|
+
readonly name: "page_download";
|
|
45
|
+
readonly description: "先监听再点击,等待下载完成并创建浏览器文件产物";
|
|
46
|
+
readonly outputFlag: true;
|
|
47
|
+
readonly params: [{
|
|
48
|
+
readonly name: "tabId";
|
|
49
|
+
readonly type: "number";
|
|
50
|
+
readonly required: true;
|
|
51
|
+
readonly description: "下载来源页面";
|
|
52
|
+
}, {
|
|
53
|
+
readonly name: "trigger";
|
|
54
|
+
readonly type: "object";
|
|
55
|
+
readonly required: true;
|
|
56
|
+
readonly description: "page_click 参数,不含 tabId";
|
|
57
|
+
}, {
|
|
58
|
+
readonly name: "url";
|
|
59
|
+
readonly type: "string";
|
|
60
|
+
readonly description: "预期下载 URL;扩展无法通过 referrer 归属时需显式指定";
|
|
61
|
+
}, {
|
|
62
|
+
readonly name: "timeout";
|
|
63
|
+
readonly type: "number";
|
|
64
|
+
readonly description: "下载等待毫秒,默认 30000,范围 1000–300000";
|
|
65
|
+
}];
|
|
66
|
+
readonly notes: readonly ["--output 把文件分块保存到调用方机器;未指定时返回 artifactId,可供 page_upload 复用。", "共享 CDP 浏览器保持原下载目录;Chrome 未返回文件路径时明确报错。超时不能直接重复点击。"];
|
|
67
|
+
}, {
|
|
26
68
|
readonly name: "tab_checkpoint";
|
|
27
69
|
readonly description: "为目标标签页建立日志、请求和埋点增量边界";
|
|
28
70
|
readonly params: [{
|
|
@@ -354,6 +396,24 @@ export declare const COMMANDS: readonly [{
|
|
|
354
396
|
}, {
|
|
355
397
|
readonly name: "browser_release";
|
|
356
398
|
readonly description: "释放当前浏览器的调试控制和会话级临时效果(含设备/时区模拟);不用于普通任务收尾,Provider 实例的行为以 mearl <providerId> 为准";
|
|
399
|
+
readonly params: [{
|
|
400
|
+
readonly name: "closeCreatedTabs";
|
|
401
|
+
readonly type: "boolean";
|
|
402
|
+
readonly description: "带 --session 时关闭会话创建的过程页,默认 false";
|
|
403
|
+
}, {
|
|
404
|
+
readonly name: "keepTabIds";
|
|
405
|
+
readonly type: "number[]";
|
|
406
|
+
readonly description: "保留的结果页面;复用的用户页面始终保留";
|
|
407
|
+
}, {
|
|
408
|
+
readonly name: "preserveEnvironment";
|
|
409
|
+
readonly type: "boolean";
|
|
410
|
+
readonly description: "带 --session 时默认 true,保留 Mock 与模拟现场;false 清理这些页面的环境";
|
|
411
|
+
}, {
|
|
412
|
+
readonly name: "keepArtifactIds";
|
|
413
|
+
readonly type: "string[]";
|
|
414
|
+
readonly description: "保留在浏览器宿主的文件产物;其余会话临时文件删除";
|
|
415
|
+
}];
|
|
416
|
+
readonly notes: readonly ["带 --session 时可用于任务收尾,只释放该会话;未带会话时保留原浏览器级释放行为,活动会话存在时拒绝全局释放。"];
|
|
357
417
|
readonly summary: "释放浏览器调试控制和会话级临时效果";
|
|
358
418
|
readonly examples: ["browser_release", "browser_release --browser chrome-a"];
|
|
359
419
|
}, {
|
|
@@ -808,6 +868,10 @@ export declare const COMMANDS: readonly [{
|
|
|
808
868
|
readonly name: "page_upload";
|
|
809
869
|
readonly description: "向 <input type=\"file\"> 元素上传一个或多个文件";
|
|
810
870
|
readonly params: [{
|
|
871
|
+
readonly name: "files";
|
|
872
|
+
readonly type: "object[]";
|
|
873
|
+
readonly description: "调用方绝对路径 {filePath} 或浏览器产物 {artifactId};与 filePaths 二选一";
|
|
874
|
+
}, {
|
|
811
875
|
readonly name: "selector";
|
|
812
876
|
readonly type: "string";
|
|
813
877
|
readonly required: true;
|
|
@@ -815,8 +879,7 @@ export declare const COMMANDS: readonly [{
|
|
|
815
879
|
}, {
|
|
816
880
|
readonly name: "filePaths";
|
|
817
881
|
readonly type: "string[]";
|
|
818
|
-
readonly
|
|
819
|
-
readonly description: "本地文件路径数组";
|
|
882
|
+
readonly description: "浏览器宿主上的绝对路径数组;与 files 二选一";
|
|
820
883
|
}, ParamDef, ParamDef, {
|
|
821
884
|
readonly name: "tabId";
|
|
822
885
|
readonly type: "number";
|
package/dist/commands.js
CHANGED
|
@@ -36,6 +36,49 @@ const PAGE_ACTION_RESULT = [
|
|
|
36
36
|
'显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。',
|
|
37
37
|
];
|
|
38
38
|
export const COMMANDS = [
|
|
39
|
+
{
|
|
40
|
+
name: 'browser_control',
|
|
41
|
+
description: '把控制会话交给用户;用户在 Mearl 浏览器管理页交还后重新观察页面',
|
|
42
|
+
params: [
|
|
43
|
+
{
|
|
44
|
+
name: 'operation',
|
|
45
|
+
type: '"handoff" | "resume"',
|
|
46
|
+
required: true,
|
|
47
|
+
description: '交给用户操作;resume 仅在用户明确交还后使用',
|
|
48
|
+
},
|
|
49
|
+
{ name: 'tabId', type: 'number', description: '可选的目标页面' },
|
|
50
|
+
{ name: 'reason', type: 'string', description: '用户需要处理的事项' },
|
|
51
|
+
],
|
|
52
|
+
notes: ['必须携带 --session;接管停止后续派发,已经发出的操作可能仍然生效。'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'page_download',
|
|
56
|
+
description: '先监听再点击,等待下载完成并创建浏览器文件产物',
|
|
57
|
+
outputFlag: true,
|
|
58
|
+
params: [
|
|
59
|
+
{ name: 'tabId', type: 'number', required: true, description: '下载来源页面' },
|
|
60
|
+
{
|
|
61
|
+
name: 'trigger',
|
|
62
|
+
type: 'object',
|
|
63
|
+
required: true,
|
|
64
|
+
description: 'page_click 参数,不含 tabId',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'url',
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: '预期下载 URL;扩展无法通过 referrer 归属时需显式指定',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'timeout',
|
|
73
|
+
type: 'number',
|
|
74
|
+
description: '下载等待毫秒,默认 30000,范围 1000–300000',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
notes: [
|
|
78
|
+
'--output 把文件分块保存到调用方机器;未指定时返回 artifactId,可供 page_upload 复用。',
|
|
79
|
+
'共享 CDP 浏览器保持原下载目录;Chrome 未返回文件路径时明确报错。超时不能直接重复点击。',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
39
82
|
// ── API 调试 ──────────────────────────────────────────────────────────────
|
|
40
83
|
{
|
|
41
84
|
name: 'tab_checkpoint',
|
|
@@ -337,6 +380,31 @@ export const COMMANDS = [
|
|
|
337
380
|
{
|
|
338
381
|
name: 'browser_release',
|
|
339
382
|
description: '释放当前浏览器的调试控制和会话级临时效果(含设备/时区模拟);不用于普通任务收尾,Provider 实例的行为以 mearl <providerId> 为准',
|
|
383
|
+
params: [
|
|
384
|
+
{
|
|
385
|
+
name: 'closeCreatedTabs',
|
|
386
|
+
type: 'boolean',
|
|
387
|
+
description: '带 --session 时关闭会话创建的过程页,默认 false',
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'keepTabIds',
|
|
391
|
+
type: 'number[]',
|
|
392
|
+
description: '保留的结果页面;复用的用户页面始终保留',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'preserveEnvironment',
|
|
396
|
+
type: 'boolean',
|
|
397
|
+
description: '带 --session 时默认 true,保留 Mock 与模拟现场;false 清理这些页面的环境',
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'keepArtifactIds',
|
|
401
|
+
type: 'string[]',
|
|
402
|
+
description: '保留在浏览器宿主的文件产物;其余会话临时文件删除',
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
notes: [
|
|
406
|
+
'带 --session 时可用于任务收尾,只释放该会话;未带会话时保留原浏览器级释放行为,活动会话存在时拒绝全局释放。',
|
|
407
|
+
],
|
|
340
408
|
summary: '释放浏览器调试控制和会话级临时效果',
|
|
341
409
|
examples: ['browser_release', 'browser_release --browser chrome-a'],
|
|
342
410
|
},
|
|
@@ -882,13 +950,22 @@ export const COMMANDS = [
|
|
|
882
950
|
name: 'page_upload',
|
|
883
951
|
description: '向 <input type="file"> 元素上传一个或多个文件',
|
|
884
952
|
params: [
|
|
953
|
+
{
|
|
954
|
+
name: 'files',
|
|
955
|
+
type: 'object[]',
|
|
956
|
+
description: '调用方绝对路径 {filePath} 或浏览器产物 {artifactId};与 filePaths 二选一',
|
|
957
|
+
},
|
|
885
958
|
{
|
|
886
959
|
name: 'selector',
|
|
887
960
|
type: 'string',
|
|
888
961
|
required: true,
|
|
889
962
|
description: '原生 CSS 选择器,或 page_snapshot / 页面动作 observation 返回的 @eN ref(必须带 @ 前缀),必须指向 input[type=file]',
|
|
890
963
|
},
|
|
891
|
-
{
|
|
964
|
+
{
|
|
965
|
+
name: 'filePaths',
|
|
966
|
+
type: 'string[]',
|
|
967
|
+
description: '浏览器宿主上的绝对路径数组;与 files 二选一',
|
|
968
|
+
},
|
|
892
969
|
PAGE_OBSERVE_PARAM,
|
|
893
970
|
PAGE_DIAGNOSTICS_PARAM,
|
|
894
971
|
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
@@ -173,7 +173,7 @@ export interface BrowserInfoBase {
|
|
|
173
173
|
}
|
|
174
174
|
export type BrowserScreenshotKind = 'native' | 'reconstructed' | 'none';
|
|
175
175
|
/** Browser-targeted actions that an external provider may implement. */
|
|
176
|
-
export type BrowserCapabilityAction = ExtensionBrowserAction;
|
|
176
|
+
export type BrowserCapabilityAction = ExtensionBrowserAction | 'page_download';
|
|
177
177
|
/** Capabilities advertised by an external browser-control provider. */
|
|
178
178
|
export interface BrowserCapabilities {
|
|
179
179
|
actions: BrowserCapabilityAction[];
|
|
@@ -222,6 +222,52 @@ export interface BrowserReleaseResult {
|
|
|
222
222
|
affectedTabIds: number[];
|
|
223
223
|
failures?: BrowserReleaseFailure[];
|
|
224
224
|
}
|
|
225
|
+
export interface ControlSessionInfo {
|
|
226
|
+
sessionId: string;
|
|
227
|
+
browserId: string;
|
|
228
|
+
transport: BrowserTransport;
|
|
229
|
+
status: 'active' | 'user' | 'releasing' | 'released';
|
|
230
|
+
generation: number;
|
|
231
|
+
tabs: Array<{
|
|
232
|
+
tabId: number;
|
|
233
|
+
source: 'created' | 'attached';
|
|
234
|
+
title?: string;
|
|
235
|
+
url?: string;
|
|
236
|
+
}>;
|
|
237
|
+
reason?: string;
|
|
238
|
+
}
|
|
239
|
+
export interface BrowserReleaseParams {
|
|
240
|
+
/** Close only session-created pages; attached user pages are protected. */
|
|
241
|
+
closeCreatedTabs?: boolean;
|
|
242
|
+
keepTabIds?: number[];
|
|
243
|
+
/** Preserve the browser environment by default for a scoped release. */
|
|
244
|
+
preserveEnvironment?: boolean;
|
|
245
|
+
keepArtifactIds?: string[];
|
|
246
|
+
}
|
|
247
|
+
export interface BrowserControlParams {
|
|
248
|
+
operation: 'handoff' | 'resume';
|
|
249
|
+
tabId?: number;
|
|
250
|
+
reason?: string;
|
|
251
|
+
}
|
|
252
|
+
export interface BrowserArtifact {
|
|
253
|
+
artifactId: string;
|
|
254
|
+
filename: string;
|
|
255
|
+
bytes: number;
|
|
256
|
+
location: 'browser';
|
|
257
|
+
/** Global browser route is attached by the unified client. */
|
|
258
|
+
browserId?: string;
|
|
259
|
+
}
|
|
260
|
+
export interface PageDownloadParams {
|
|
261
|
+
tabId: number;
|
|
262
|
+
trigger: Omit<PageClickParams, 'tabId'>;
|
|
263
|
+
/** Restrict the expected download URL; useful when the browser cannot identify its source. */
|
|
264
|
+
url?: string;
|
|
265
|
+
timeout?: number;
|
|
266
|
+
}
|
|
267
|
+
export interface PageDownloadResult {
|
|
268
|
+
download: BrowserArtifact;
|
|
269
|
+
action: unknown;
|
|
270
|
+
}
|
|
225
271
|
export interface CookieCopyScope {
|
|
226
272
|
/** Maintained authentication-domain presets to include. */
|
|
227
273
|
presets?: Array<'mearl-services'>;
|
|
@@ -595,7 +641,14 @@ export interface PageNavigateParams {
|
|
|
595
641
|
export interface PageUploadParams {
|
|
596
642
|
tabId: number;
|
|
597
643
|
selector: string;
|
|
598
|
-
|
|
644
|
+
/** Absolute paths available to the browser host. */
|
|
645
|
+
filePaths?: string[];
|
|
646
|
+
/** SDK/CLI stages caller-local files before dispatch. Mutually exclusive with filePaths. */
|
|
647
|
+
files?: Array<{
|
|
648
|
+
filePath: string;
|
|
649
|
+
} | {
|
|
650
|
+
artifactId: string;
|
|
651
|
+
}>;
|
|
599
652
|
observe?: PageObservationWaitOptions | false;
|
|
600
653
|
diagnostics?: PageDiagnosticsSetting;
|
|
601
654
|
}
|
|
@@ -719,9 +772,19 @@ export interface BrowserActionMap {
|
|
|
719
772
|
response: SelectBrowserResult;
|
|
720
773
|
};
|
|
721
774
|
browser_release: {
|
|
722
|
-
request:
|
|
775
|
+
request: BrowserReleaseParams;
|
|
723
776
|
response: BrowserReleaseResult;
|
|
724
777
|
};
|
|
778
|
+
browser_control: {
|
|
779
|
+
request: BrowserControlParams;
|
|
780
|
+
response: {
|
|
781
|
+
session: ControlSessionInfo;
|
|
782
|
+
};
|
|
783
|
+
};
|
|
784
|
+
page_download: {
|
|
785
|
+
request: PageDownloadParams;
|
|
786
|
+
response: PageDownloadResult;
|
|
787
|
+
};
|
|
725
788
|
browser_launch: {
|
|
726
789
|
request: BrowserLaunchParams;
|
|
727
790
|
response: ManagedBrowserInfo;
|
|
@@ -833,10 +896,12 @@ export interface BrowserActionMap {
|
|
|
833
896
|
}
|
|
834
897
|
export type BrowserAction = keyof BrowserActionMap;
|
|
835
898
|
export type BrowserActionRequest<A extends BrowserAction> = BrowserActionMap[A]['request'];
|
|
836
|
-
export type BrowserActionResponse<A extends BrowserAction> = BrowserActionMap[A]['response']
|
|
899
|
+
export type BrowserActionResponse<A extends BrowserAction> = BrowserActionMap[A]['response'] & {
|
|
900
|
+
session?: ControlSessionInfo;
|
|
901
|
+
};
|
|
837
902
|
export type BrowserCommandAction = Exclude<BrowserAction, 'browser_select_browser'>;
|
|
838
|
-
export type ExtensionBrowserAction = Exclude<BrowserCommandAction, 'browser_list' | 'browser_launch' | 'browser_close'>;
|
|
839
|
-
export declare const TAB_ID_REQUIRED_ACTIONS: readonly ["tab_checkpoint", "capture_checkpoint", "get_requests", "get_logs", "get_events", "set_mock", "get_mocks", "page_screenshot", "tab_close", "page_click", "page_drag", "page_type", "page_scroll", "page_hover", "page_eval", "page_inspect", "page_snapshot", "page_press", "page_wait", "page_navigate", "page_upload", "page_frames", "set_device_emulation", "set_app_profile", "set_timezone", "run_actions"];
|
|
903
|
+
export type ExtensionBrowserAction = Exclude<BrowserCommandAction, 'browser_list' | 'browser_launch' | 'browser_close' | 'browser_control' | 'page_download'>;
|
|
904
|
+
export declare const TAB_ID_REQUIRED_ACTIONS: readonly ["tab_checkpoint", "capture_checkpoint", "get_requests", "get_logs", "get_events", "set_mock", "get_mocks", "page_screenshot", "tab_close", "page_click", "page_drag", "page_type", "page_scroll", "page_hover", "page_eval", "page_inspect", "page_snapshot", "page_press", "page_wait", "page_navigate", "page_upload", "page_download", "page_frames", "set_device_emulation", "set_app_profile", "set_timezone", "run_actions"];
|
|
840
905
|
export type TabIdRequiredAction = (typeof TAB_ID_REQUIRED_ACTIONS)[number];
|
|
841
906
|
export declare function actionRequiresTabId(action: unknown): action is TabIdRequiredAction;
|
|
842
907
|
export declare function validateRequiredActionTabId(action: string, data: unknown): void;
|
|
@@ -857,6 +922,8 @@ export declare const BROWSER_ACTIONS: {
|
|
|
857
922
|
readonly browser_list: true;
|
|
858
923
|
readonly browser_select_browser: true;
|
|
859
924
|
readonly browser_release: true;
|
|
925
|
+
readonly browser_control: true;
|
|
926
|
+
readonly page_download: true;
|
|
860
927
|
readonly browser_launch: true;
|
|
861
928
|
readonly browser_close: true;
|
|
862
929
|
readonly page_screenshot: true;
|
|
@@ -899,6 +966,8 @@ export declare const BROWSER_COMMAND_ACTIONS: {
|
|
|
899
966
|
readonly send_mtop_request: true;
|
|
900
967
|
readonly browser_list: true;
|
|
901
968
|
readonly browser_release: true;
|
|
969
|
+
readonly browser_control: true;
|
|
970
|
+
readonly page_download: true;
|
|
902
971
|
readonly browser_launch: true;
|
|
903
972
|
readonly browser_close: true;
|
|
904
973
|
readonly page_screenshot: true;
|
|
@@ -966,6 +1035,46 @@ export declare const EXTENSION_BROWSER_ACTIONS: {
|
|
|
966
1035
|
readonly request_domain_permission: true;
|
|
967
1036
|
readonly run_actions: true;
|
|
968
1037
|
};
|
|
1038
|
+
export declare const BROWSER_CAPABILITY_ACTIONS: {
|
|
1039
|
+
readonly page_download: true;
|
|
1040
|
+
readonly tab_checkpoint: true;
|
|
1041
|
+
readonly capture_checkpoint: true;
|
|
1042
|
+
readonly get_requests: true;
|
|
1043
|
+
readonly get_logs: true;
|
|
1044
|
+
readonly get_events: true;
|
|
1045
|
+
readonly set_mock: true;
|
|
1046
|
+
readonly get_mocks: true;
|
|
1047
|
+
readonly set_rule: true;
|
|
1048
|
+
readonly get_rules: true;
|
|
1049
|
+
readonly send_request: true;
|
|
1050
|
+
readonly send_mtop_request: true;
|
|
1051
|
+
readonly browser_release: true;
|
|
1052
|
+
readonly page_screenshot: true;
|
|
1053
|
+
readonly tab_open: true;
|
|
1054
|
+
readonly tab_close: true;
|
|
1055
|
+
readonly tab_list: true;
|
|
1056
|
+
readonly tab_group: true;
|
|
1057
|
+
readonly page_click: true;
|
|
1058
|
+
readonly page_drag: true;
|
|
1059
|
+
readonly page_type: true;
|
|
1060
|
+
readonly page_scroll: true;
|
|
1061
|
+
readonly page_hover: true;
|
|
1062
|
+
readonly page_eval: true;
|
|
1063
|
+
readonly page_inspect: true;
|
|
1064
|
+
readonly page_snapshot: true;
|
|
1065
|
+
readonly page_press: true;
|
|
1066
|
+
readonly page_wait: true;
|
|
1067
|
+
readonly page_navigate: true;
|
|
1068
|
+
readonly page_upload: true;
|
|
1069
|
+
readonly page_frames: true;
|
|
1070
|
+
readonly set_device_emulation: true;
|
|
1071
|
+
readonly set_app_profile: true;
|
|
1072
|
+
readonly set_timezone: true;
|
|
1073
|
+
readonly get_cookie: true;
|
|
1074
|
+
readonly set_cookie: true;
|
|
1075
|
+
readonly request_domain_permission: true;
|
|
1076
|
+
readonly run_actions: true;
|
|
1077
|
+
};
|
|
969
1078
|
export declare function isBrowserAction(value: unknown): value is BrowserAction;
|
|
970
1079
|
export declare function isBrowserCommandAction(value: unknown): value is BrowserCommandAction;
|
|
971
1080
|
export declare function isBrowserCapabilityAction(value: unknown): value is BrowserCapabilityAction;
|
|
@@ -72,6 +72,7 @@ export const TAB_ID_REQUIRED_ACTIONS = [
|
|
|
72
72
|
'page_wait',
|
|
73
73
|
'page_navigate',
|
|
74
74
|
'page_upload',
|
|
75
|
+
'page_download',
|
|
75
76
|
'page_frames',
|
|
76
77
|
'set_device_emulation',
|
|
77
78
|
'set_app_profile',
|
|
@@ -128,6 +129,8 @@ export const BROWSER_ACTIONS = {
|
|
|
128
129
|
browser_list: true,
|
|
129
130
|
browser_select_browser: true,
|
|
130
131
|
browser_release: true,
|
|
132
|
+
browser_control: true,
|
|
133
|
+
page_download: true,
|
|
131
134
|
browser_launch: true,
|
|
132
135
|
browser_close: true,
|
|
133
136
|
page_screenshot: true,
|
|
@@ -170,6 +173,8 @@ export const BROWSER_COMMAND_ACTIONS = {
|
|
|
170
173
|
send_mtop_request: true,
|
|
171
174
|
browser_list: true,
|
|
172
175
|
browser_release: true,
|
|
176
|
+
browser_control: true,
|
|
177
|
+
page_download: true,
|
|
173
178
|
browser_launch: true,
|
|
174
179
|
browser_close: true,
|
|
175
180
|
page_screenshot: true,
|
|
@@ -237,6 +242,10 @@ export const EXTENSION_BROWSER_ACTIONS = {
|
|
|
237
242
|
request_domain_permission: true,
|
|
238
243
|
run_actions: true,
|
|
239
244
|
};
|
|
245
|
+
export const BROWSER_CAPABILITY_ACTIONS = {
|
|
246
|
+
...EXTENSION_BROWSER_ACTIONS,
|
|
247
|
+
page_download: true,
|
|
248
|
+
};
|
|
240
249
|
export function isBrowserAction(value) {
|
|
241
250
|
return typeof value === 'string' && Object.hasOwn(BROWSER_ACTIONS, value);
|
|
242
251
|
}
|
|
@@ -244,7 +253,7 @@ export function isBrowserCommandAction(value) {
|
|
|
244
253
|
return typeof value === 'string' && Object.hasOwn(BROWSER_COMMAND_ACTIONS, value);
|
|
245
254
|
}
|
|
246
255
|
export function isBrowserCapabilityAction(value) {
|
|
247
|
-
return
|
|
256
|
+
return typeof value === 'string' && Object.hasOwn(BROWSER_CAPABILITY_ACTIONS, value);
|
|
248
257
|
}
|
|
249
258
|
export function isExtensionBrowserAction(value) {
|
|
250
259
|
return typeof value === 'string' && Object.hasOwn(EXTENSION_BROWSER_ACTIONS, value);
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
|
|
8
8
|
export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
|
|
9
9
|
export type { PluginInvocation, PluginInvokeOptions, PluginProgress, StructuredPluginProgress, } from './plugin.js';
|
|
10
|
-
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
10
|
+
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
11
11
|
export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from './generated-browser-action-protocol.js';
|
|
12
12
|
import { type UnifiedInvokeOptions } from './unified.js';
|
|
13
13
|
import type * as Protocol from './generated-browser-action-protocol.js';
|
|
@@ -24,6 +24,7 @@ export declare function sendMtopRequest(params: Protocol.SendMtopRequestParams,
|
|
|
24
24
|
export declare function browserList(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
|
|
25
25
|
export declare function selectBrowser(params?: Protocol.SelectBrowserParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_select_browser'>>;
|
|
26
26
|
export declare function browserRelease(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
|
|
27
|
+
export declare function browserRelease(params: Protocol.BrowserReleaseParams, options: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
|
|
27
28
|
export declare function browserLaunch(params: Protocol.BrowserLaunchParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_launch'>>;
|
|
28
29
|
export declare function browserClose(params: Protocol.BrowserCloseParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_close'>>;
|
|
29
30
|
export declare function setRule(params: Protocol.SetRuleParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_rule'>>;
|
|
@@ -52,3 +53,11 @@ export declare function setTimezone(params: Protocol.SetTimezoneParams, options?
|
|
|
52
53
|
export declare function getCookie(params: Protocol.GetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_cookie'>>;
|
|
53
54
|
export declare function setCookie(params: Protocol.SetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_cookie'>>;
|
|
54
55
|
export declare function runActions(params: Protocol.RunActionsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'run_actions'>>;
|
|
56
|
+
export declare function browserControl(params: Protocol.BrowserControlParams, options?: UnifiedInvokeOptions): Promise<any>;
|
|
57
|
+
export declare function pageDownload(params: Protocol.PageDownloadParams, options?: UnifiedInvokeOptions): Promise<any>;
|
|
58
|
+
export declare function saveArtifact(artifact: Protocol.BrowserArtifact & {
|
|
59
|
+
browserId?: string;
|
|
60
|
+
}, outputPath: string, options?: UnifiedInvokeOptions): Promise<{
|
|
61
|
+
outputPath: string;
|
|
62
|
+
bytes: number;
|
|
63
|
+
}>;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* then routes actions using the global browser ids returned by browser_list.
|
|
6
6
|
*/
|
|
7
7
|
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
|
|
8
|
-
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
8
|
+
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
9
9
|
import { invoke } from './unified.js';
|
|
10
10
|
export function tabCheckpoint(params, options) {
|
|
11
11
|
return invoke('tab_checkpoint', params, options);
|
|
@@ -41,8 +41,10 @@ export function browserList(options) {
|
|
|
41
41
|
export function selectBrowser(params = {}, options) {
|
|
42
42
|
return invoke('browser_select_browser', params, options);
|
|
43
43
|
}
|
|
44
|
-
export function browserRelease(options) {
|
|
45
|
-
return
|
|
44
|
+
export function browserRelease(paramsOrOptions = {}, options) {
|
|
45
|
+
return options
|
|
46
|
+
? invoke('browser_release', paramsOrOptions, options)
|
|
47
|
+
: invoke('browser_release', {}, paramsOrOptions);
|
|
46
48
|
}
|
|
47
49
|
export function browserLaunch(params, options) {
|
|
48
50
|
return invoke('browser_launch', params, options);
|
|
@@ -128,3 +130,13 @@ export function setCookie(params, options) {
|
|
|
128
130
|
export function runActions(params, options) {
|
|
129
131
|
return invoke('run_actions', params, options);
|
|
130
132
|
}
|
|
133
|
+
export function browserControl(params, options) {
|
|
134
|
+
return invoke('browser_control', params, options);
|
|
135
|
+
}
|
|
136
|
+
export function pageDownload(params, options) {
|
|
137
|
+
return invoke('page_download', params, options);
|
|
138
|
+
}
|
|
139
|
+
export async function saveArtifact(artifact, outputPath, options = {}) {
|
|
140
|
+
const { exportArtifact } = await import('./artifacts.js');
|
|
141
|
+
return exportArtifact({ invoke }, artifact, outputPath, options);
|
|
142
|
+
}
|
package/dist/pluginPageBundle.js
CHANGED
|
@@ -32,6 +32,40 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
32
32
|
let resolveReady;
|
|
33
33
|
const ready = new Promise(resolve => { resolveReady = resolve; });
|
|
34
34
|
|
|
35
|
+
let pageModalOpen = null;
|
|
36
|
+
let modalRestoreFrame = null;
|
|
37
|
+
const syncPageModal = () => {
|
|
38
|
+
if (!port) return;
|
|
39
|
+
const open = Boolean(document.querySelector?.('dialog:modal'));
|
|
40
|
+
if (open && modalRestoreFrame !== null) {
|
|
41
|
+
cancelAnimationFrame(modalRestoreFrame);
|
|
42
|
+
modalRestoreFrame = null;
|
|
43
|
+
}
|
|
44
|
+
if (open === pageModalOpen) return;
|
|
45
|
+
if (!open && pageModalOpen === true) {
|
|
46
|
+
if (modalRestoreFrame !== null) return;
|
|
47
|
+
// Closing a dialog restores focus and can scroll the anchors before the next paint.
|
|
48
|
+
modalRestoreFrame = requestAnimationFrame(() => {
|
|
49
|
+
modalRestoreFrame = null;
|
|
50
|
+
if (document.querySelector?.('dialog:modal')) return;
|
|
51
|
+
const positions = [];
|
|
52
|
+
for (const { presentation } of inlineAgentPanels.values()) {
|
|
53
|
+
if (!presentation.element.isConnected) continue;
|
|
54
|
+
const rect = presentation.element.getBoundingClientRect();
|
|
55
|
+
if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) continue;
|
|
56
|
+
const bounds = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
|
|
57
|
+
presentation.bounds = bounds;
|
|
58
|
+
positions.push({ key: presentation.key, bounds });
|
|
59
|
+
}
|
|
60
|
+
pageModalOpen = false;
|
|
61
|
+
port.postMessage({ type: 'page-modal', open: false, positions });
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
pageModalOpen = open;
|
|
66
|
+
port.postMessage({ type: 'page-modal', open });
|
|
67
|
+
};
|
|
68
|
+
|
|
35
69
|
const requestHost = async (type, payload = {}) => {
|
|
36
70
|
await ready;
|
|
37
71
|
const requestId = String(++requestSequence);
|
|
@@ -469,6 +503,19 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
469
503
|
port.start();
|
|
470
504
|
resolveReady(context);
|
|
471
505
|
port.postMessage({ type: 'ready' });
|
|
506
|
+
// Dialogs in the sandbox cannot cover Agent overlays in the parent document.
|
|
507
|
+
syncPageModal();
|
|
508
|
+
const modalObserver = typeof MutationObserver === 'function'
|
|
509
|
+
? new MutationObserver(syncPageModal)
|
|
510
|
+
: null;
|
|
511
|
+
modalObserver?.observe(document.documentElement, {
|
|
512
|
+
attributes: true,
|
|
513
|
+
attributeFilter: ['open'],
|
|
514
|
+
childList: true,
|
|
515
|
+
subtree: true,
|
|
516
|
+
});
|
|
517
|
+
document.addEventListener('toggle', syncPageModal, true);
|
|
518
|
+
document.addEventListener('close', syncPageModal, true);
|
|
472
519
|
};
|
|
473
520
|
window.addEventListener('message', connect);
|
|
474
521
|
window.parent.postMessage({ type: 'mearl-plugin-ready' }, '*');
|
package/dist/socket.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare const SOCKET_PATH: string;
|
|
|
17
17
|
export type ControlSource = 'local' | 'remote';
|
|
18
18
|
type CompatibleControlSource = ControlSource | 'cloud';
|
|
19
19
|
export interface InvokeOptions {
|
|
20
|
+
controlSessionId?: string;
|
|
20
21
|
/** 超时时间(秒),默认 15 */
|
|
21
22
|
timeoutSec?: number;
|
|
22
23
|
/** Force CDP transport instead of extension */
|
package/dist/socket.js
CHANGED
|
@@ -13,7 +13,7 @@ import os from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
import { StringDecoder } from 'node:string_decoder';
|
|
16
|
-
import { listInstalledProviders } from '@mearl/provider/host';
|
|
16
|
+
import { ensureProviderControlHost, listInstalledProviders } from '@mearl/provider/host';
|
|
17
17
|
import { readLiveProvider } from '@mearl/provider/runtime';
|
|
18
18
|
import { resolveFileRefs } from './fileRefs.js';
|
|
19
19
|
import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
|
|
@@ -32,6 +32,7 @@ export const SOCKET_PATH = process.platform === 'win32'
|
|
|
32
32
|
function toControlSourceWireValue(source) {
|
|
33
33
|
return source === 'remote' || source === 'cloud' ? 'cloud' : 'local';
|
|
34
34
|
}
|
|
35
|
+
const SESSION_OPTION = Symbol('control-session');
|
|
35
36
|
/**
|
|
36
37
|
* Process-wide default browser selector. Lets a long-lived host (e.g. the MCP
|
|
37
38
|
* server) pin a target once via `select_browser` without threading it through
|
|
@@ -129,6 +130,17 @@ function isOrdinaryLocalBrowser(browser) {
|
|
|
129
130
|
(browser.transport === 'extension' || browser.transport === 'cdp'));
|
|
130
131
|
}
|
|
131
132
|
function supportsAction(browser, action, data) {
|
|
133
|
+
if (browser.transport === 'provider') {
|
|
134
|
+
if (action === 'browser_control' ||
|
|
135
|
+
(action === 'browser_release' && data[SESSION_OPTION]))
|
|
136
|
+
return true;
|
|
137
|
+
if (action.startsWith('__artifact_'))
|
|
138
|
+
return (browser.capabilities?.actions.includes('page_upload') === true ||
|
|
139
|
+
((action === '__artifact_read' || action === '__artifact_remove') &&
|
|
140
|
+
browser.capabilities?.actions.includes('page_download') === true));
|
|
141
|
+
if (action === 'run_actions' && data[SESSION_OPTION])
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
132
144
|
if (action === 'get_versions')
|
|
133
145
|
return true;
|
|
134
146
|
if (action === 'get_browser_access_url') {
|
|
@@ -328,6 +340,7 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
328
340
|
const { timeoutSec = resolveActionTimeoutSec(action, data), transport, browser, controlSource = 'local', } = options;
|
|
329
341
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
330
342
|
const resolvedData = resolveFileRefs(data);
|
|
343
|
+
Object.defineProperty(resolvedData, SESSION_OPTION, { value: options.controlSessionId });
|
|
331
344
|
if (action === 'browser_list') {
|
|
332
345
|
return dispatchInvoke(action, resolvedData, timeoutSec, transport, undefined, controlSource).then(result => decorateBrowserListResult(transport === 'cdp' ? cdpOnlyBrowserList(result) : result));
|
|
333
346
|
}
|
|
@@ -413,14 +426,15 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
413
426
|
if (match.status === 'pairing' &&
|
|
414
427
|
action !== 'get_versions' &&
|
|
415
428
|
action !== 'browser_close' &&
|
|
416
|
-
action !== 'browser_release'
|
|
429
|
+
action !== 'browser_release' &&
|
|
430
|
+
action !== 'browser_control') {
|
|
417
431
|
throw new Error(`Browser ${describeBrowser(match)} is waiting for device pairing`);
|
|
418
432
|
}
|
|
419
433
|
if (!supportsAction(match, action, data)) {
|
|
420
434
|
throw new Error(`Browser ${describeBrowser(match)} does not support ${action}`);
|
|
421
435
|
}
|
|
422
436
|
try {
|
|
423
|
-
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.
|
|
437
|
+
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.transport === 'provider' ? match.browserId : undefined);
|
|
424
438
|
}
|
|
425
439
|
catch (error) {
|
|
426
440
|
if (isConnError(error) || isSocketSandboxError(error)) {
|
|
@@ -439,7 +453,7 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
439
453
|
if (routableBrowsers.length > 0) {
|
|
440
454
|
const target = routableBrowsers[0];
|
|
441
455
|
try {
|
|
442
|
-
return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.
|
|
456
|
+
return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
|
|
443
457
|
}
|
|
444
458
|
catch (error) {
|
|
445
459
|
if (isSocketSandboxError(error)) {
|
|
@@ -486,7 +500,7 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
|
|
|
486
500
|
!(transport === 'cdp' && browser.transport === 'provider') &&
|
|
487
501
|
supportsAction(browser, action, data));
|
|
488
502
|
if (target) {
|
|
489
|
-
return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.
|
|
503
|
+
return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
|
|
490
504
|
}
|
|
491
505
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
|
|
492
506
|
/* isRetry */ true);
|
|
@@ -497,7 +511,11 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
|
|
|
497
511
|
throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
|
|
498
512
|
}
|
|
499
513
|
}
|
|
500
|
-
function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource,
|
|
514
|
+
async function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerTarget) {
|
|
515
|
+
if (providerTarget) {
|
|
516
|
+
const host = await ensureProviderControlHost(Math.min(timeoutSec * 1000, 10_000));
|
|
517
|
+
socketPath = host.socketPath;
|
|
518
|
+
}
|
|
501
519
|
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
502
520
|
return new Promise((resolve, reject) => {
|
|
503
521
|
if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
|
|
@@ -536,10 +554,12 @@ function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSour
|
|
|
536
554
|
version: CLIENT_VERSION,
|
|
537
555
|
controlSource: toControlSourceWireValue(controlSource),
|
|
538
556
|
};
|
|
557
|
+
if (data[SESSION_OPTION])
|
|
558
|
+
message.controlSessionId = data[SESSION_OPTION];
|
|
539
559
|
if (transport)
|
|
540
560
|
message.transport = transport;
|
|
541
|
-
if (
|
|
542
|
-
message.
|
|
561
|
+
if (providerTarget)
|
|
562
|
+
message.providerTarget = providerTarget;
|
|
543
563
|
socket.write(JSON.stringify(message) + '\n');
|
|
544
564
|
});
|
|
545
565
|
socket.on('data', chunk => {
|
package/dist/unified.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { BrowserArtifact } from './generated-browser-action-protocol.js';
|
|
1
2
|
import { type CloudConnectorListResult } from '@mearl/cloud-types';
|
|
2
3
|
import type { PluginInvocation, PluginInvokeOptions } from './plugin.js';
|
|
3
4
|
export interface UnifiedInvokeOptions {
|
|
5
|
+
/** Use new on the first tab_open/page_snapshot, then reuse the returned id. */
|
|
6
|
+
session?: string;
|
|
7
|
+
/** Source-local session id used by transport adapters. */
|
|
8
|
+
controlSessionId?: string;
|
|
4
9
|
timeoutSec?: number;
|
|
5
10
|
/** Global browser id, source-local browser id, or unique browser name. */
|
|
6
11
|
browser?: string;
|
|
@@ -25,6 +30,11 @@ interface BrowserTarget {
|
|
|
25
30
|
}
|
|
26
31
|
export declare function decodeBrowserTarget(selector: string | undefined): BrowserTarget | undefined;
|
|
27
32
|
export declare function normalizeBrowserSelector(selector: string): string;
|
|
33
|
+
export declare function decodeControlSessionId(id: string): {
|
|
34
|
+
browser: string;
|
|
35
|
+
id: string;
|
|
36
|
+
transport: 'extension' | 'cdp' | 'provider';
|
|
37
|
+
};
|
|
28
38
|
export declare class UnifiedClient {
|
|
29
39
|
private readonly options;
|
|
30
40
|
private defaultBrowser;
|
|
@@ -39,6 +49,13 @@ export declare class UnifiedClient {
|
|
|
39
49
|
disconnect(): void;
|
|
40
50
|
invokePlugin(name: string, invocation: PluginInvocation, options?: PluginInvokeOptions): Promise<unknown>;
|
|
41
51
|
invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
52
|
+
saveArtifact(artifact: BrowserArtifact & {
|
|
53
|
+
browserId?: string;
|
|
54
|
+
}, outputPath: string, options?: UnifiedInvokeOptions): Promise<{
|
|
55
|
+
outputPath: string;
|
|
56
|
+
bytes: number;
|
|
57
|
+
}>;
|
|
58
|
+
private invokeRouted;
|
|
42
59
|
private invokeExplicitLocal;
|
|
43
60
|
private invokeLaunchWithoutControlBrowser;
|
|
44
61
|
private selectDefaultBrowser;
|
package/dist/unified.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stageFile, exportArtifact } from './artifacts.js';
|
|
1
2
|
import WebSocket from 'ws';
|
|
2
3
|
import { DEFAULT_CONNECT_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_REQUEST_TIMEOUT, MAX_BUFFER_SIZE, isHeartbeatMessage, resolveCloudConnectorSelector, } from '@mearl/cloud-types';
|
|
3
4
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
@@ -48,6 +49,16 @@ export function normalizeBrowserSelector(selector) {
|
|
|
48
49
|
const decodedTarget = decodeBrowserTarget(selector);
|
|
49
50
|
return (decodedTarget ? encodeBrowserTarget(decodedTarget) : selector.trim()).toLowerCase();
|
|
50
51
|
}
|
|
52
|
+
export function decodeControlSessionId(id) {
|
|
53
|
+
const match = /^mearl-session:([^:]+):(cs_[a-f0-9-]+):(extension|cdp|provider)$/.exec(id);
|
|
54
|
+
if (!match)
|
|
55
|
+
throw new Error('Invalid session id; reuse session.sessionId from the first response');
|
|
56
|
+
return {
|
|
57
|
+
browser: decodeURIComponent(match[1]),
|
|
58
|
+
id: match[2],
|
|
59
|
+
transport: match[3],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
51
62
|
function decorateBrowser(browser, source) {
|
|
52
63
|
const safeBrowser = omitTransientBrowserSecrets(browser);
|
|
53
64
|
const originalBrowserId = String(browser.browserId);
|
|
@@ -127,6 +138,67 @@ export class UnifiedClient {
|
|
|
127
138
|
}, options);
|
|
128
139
|
}
|
|
129
140
|
async invoke(action, data = {}, options = {}) {
|
|
141
|
+
if (options.session && options.session !== 'new') {
|
|
142
|
+
const session = decodeControlSessionId(options.session);
|
|
143
|
+
const selected = decodeBrowserTarget(options.browser);
|
|
144
|
+
const pinned = decodeBrowserTarget(session.browser);
|
|
145
|
+
if (options.browser &&
|
|
146
|
+
(!selected ||
|
|
147
|
+
selected.browser !== pinned?.browser ||
|
|
148
|
+
selected.connector !== pinned?.connector)) {
|
|
149
|
+
throw new Error('The selected browser does not match the control session');
|
|
150
|
+
}
|
|
151
|
+
if (options.transport && session.transport !== options.transport)
|
|
152
|
+
throw new Error('A control session cannot change transport');
|
|
153
|
+
options = {
|
|
154
|
+
...options,
|
|
155
|
+
browser: session.browser,
|
|
156
|
+
controlSessionId: session.id,
|
|
157
|
+
...(session.transport === 'cdp' && !pinned?.connector ? { transport: 'cdp' } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
else if (options.session === 'new') {
|
|
161
|
+
if (!['tab_open', 'page_snapshot', 'page_screenshot'].includes(action))
|
|
162
|
+
throw new Error('--session new requires tab_open, page_snapshot or page_screenshot');
|
|
163
|
+
options = { ...options, controlSessionId: 'new' };
|
|
164
|
+
}
|
|
165
|
+
if (action === 'page_upload' &&
|
|
166
|
+
data.files !== undefined &&
|
|
167
|
+
(!Array.isArray(data.files) ||
|
|
168
|
+
!data.files.length ||
|
|
169
|
+
data.files.some((file) => !file || (typeof file.filePath === 'string') === (typeof file.artifactId === 'string'))))
|
|
170
|
+
throw new Error('files requires exactly one filePath or artifactId per entry');
|
|
171
|
+
if (action === 'page_upload' &&
|
|
172
|
+
Array.isArray(data.files) &&
|
|
173
|
+
data.files.some((file) => file.filePath)) {
|
|
174
|
+
if (data.filePaths)
|
|
175
|
+
throw new Error('Use files or filePaths, not both');
|
|
176
|
+
const staged = [];
|
|
177
|
+
const temporary = [];
|
|
178
|
+
try {
|
|
179
|
+
for (const file of data.files) {
|
|
180
|
+
if (file.filePath) {
|
|
181
|
+
const uploaded = await stageFile(this, file.filePath, options);
|
|
182
|
+
options = uploaded.options;
|
|
183
|
+
temporary.push(uploaded.artifactId);
|
|
184
|
+
staged.push({ artifactId: uploaded.artifactId });
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
staged.push(file);
|
|
188
|
+
}
|
|
189
|
+
return await this.invokeRouted(action, { ...data, files: staged }, options);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
for (const id of temporary)
|
|
193
|
+
await this.invoke('__artifact_remove', { artifactId: id }, options).catch(() => { });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return this.invokeRouted(action, data, options);
|
|
197
|
+
}
|
|
198
|
+
saveArtifact(artifact, outputPath, options = {}) {
|
|
199
|
+
return exportArtifact(this, artifact, outputPath, options);
|
|
200
|
+
}
|
|
201
|
+
async invokeRouted(action, data = {}, options = {}) {
|
|
130
202
|
validateRequiredActionTabId(action, data);
|
|
131
203
|
const timeoutSec = options.timeoutSec ?? resolveActionTimeoutSec(action, data, this.options.requestTimeoutSec);
|
|
132
204
|
if (action === 'connector_list')
|
|
@@ -162,7 +234,7 @@ export class UnifiedClient {
|
|
|
162
234
|
if (options.connector) {
|
|
163
235
|
throw new Error('A local browser cannot be used together with --connector.');
|
|
164
236
|
}
|
|
165
|
-
return this.invokeLocal(action, requestData, timeoutSec, target.browser);
|
|
237
|
+
return this.invokeLocal(action, requestData, timeoutSec, target.browser, undefined, options.controlSessionId);
|
|
166
238
|
}
|
|
167
239
|
if (connector) {
|
|
168
240
|
const selected = target?.connector && !options.connector && !this.defaultConnector
|
|
@@ -171,10 +243,10 @@ export class UnifiedClient {
|
|
|
171
243
|
if (target?.connector && selected.connectorId !== target.connector) {
|
|
172
244
|
throw new Error('The selected browser belongs to a different cloud connector.');
|
|
173
245
|
}
|
|
174
|
-
return this.invokeRemote(action, requestData, timeoutSec, browser, selected);
|
|
246
|
+
return this.invokeRemote(action, requestData, timeoutSec, browser, selected, options.controlSessionId);
|
|
175
247
|
}
|
|
176
248
|
if (!this.options.serverUrl) {
|
|
177
|
-
return this.invokeLocal(action, requestData, timeoutSec, options.browser ?? this.defaultBrowser);
|
|
249
|
+
return this.invokeLocal(action, requestData, timeoutSec, options.browser ?? this.defaultBrowser, undefined, options.controlSessionId);
|
|
178
250
|
}
|
|
179
251
|
const selector = options.browser ??
|
|
180
252
|
this.defaultBrowser ??
|
|
@@ -204,7 +276,7 @@ export class UnifiedClient {
|
|
|
204
276
|
}
|
|
205
277
|
const browser = optionTarget?.browser ?? defaultTarget?.browser ?? options.browser ?? this.defaultBrowser;
|
|
206
278
|
const requestData = closeTarget ? { ...data, browser: closeTarget.browser } : data;
|
|
207
|
-
return this.invokeLocal(action, requestData, timeoutSec, browser, options.transport);
|
|
279
|
+
return this.invokeLocal(action, requestData, timeoutSec, browser, options.transport, options.controlSessionId);
|
|
208
280
|
}
|
|
209
281
|
async invokeLaunchWithoutControlBrowser(data, timeoutSec) {
|
|
210
282
|
const local = await this.tryLocalBrowserList(timeoutSec);
|
|
@@ -410,17 +482,18 @@ export class UnifiedClient {
|
|
|
410
482
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
411
483
|
});
|
|
412
484
|
}
|
|
413
|
-
async invokeLocal(action, data, timeoutSec, browser, transport) {
|
|
485
|
+
async invokeLocal(action, data, timeoutSec, browser, transport, controlSessionId) {
|
|
414
486
|
const result = await invokeLocalBrowser(action, data, {
|
|
415
487
|
timeoutSec,
|
|
416
488
|
...(browser ? { browser } : {}),
|
|
417
489
|
...(transport ? { transport } : {}),
|
|
418
490
|
controlSource: 'local',
|
|
491
|
+
...(controlSessionId ? { controlSessionId } : {}),
|
|
419
492
|
});
|
|
420
493
|
return this.decorateActionResult(action, result, { kind: 'local' });
|
|
421
494
|
}
|
|
422
|
-
async invokeRemote(action, data, timeoutSec, browser, connector) {
|
|
423
|
-
const result = await this.requestRemote(action, data, timeoutSec, browser, connector.connectorId);
|
|
495
|
+
async invokeRemote(action, data, timeoutSec, browser, connector, controlSessionId) {
|
|
496
|
+
const result = await this.requestRemote(action, data, timeoutSec, browser, connector.connectorId, controlSessionId);
|
|
424
497
|
return this.decorateActionResult(action, result, {
|
|
425
498
|
kind: 'remote',
|
|
426
499
|
connectorId: connector.connectorId,
|
|
@@ -428,6 +501,40 @@ export class UnifiedClient {
|
|
|
428
501
|
});
|
|
429
502
|
}
|
|
430
503
|
decorateActionResult(action, result, source) {
|
|
504
|
+
if (result?.session?.sessionId && !result.session.sessionId.startsWith('mearl-session:')) {
|
|
505
|
+
const browserId = encodeBrowserTarget({
|
|
506
|
+
browser: result.session.browserId,
|
|
507
|
+
...(source.connectorId ? { connector: source.connectorId } : {}),
|
|
508
|
+
});
|
|
509
|
+
result = {
|
|
510
|
+
...result,
|
|
511
|
+
session: {
|
|
512
|
+
...result.session,
|
|
513
|
+
browserId,
|
|
514
|
+
sessionId: `mearl-session:${encodeURIComponent(browserId)}:${result.session.sessionId}:${result.session.transport}`,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (typeof result?.browserId === 'string' && action.startsWith('__artifact_')) {
|
|
519
|
+
return {
|
|
520
|
+
...result,
|
|
521
|
+
browserId: encodeBrowserTarget({
|
|
522
|
+
browser: result.browserId,
|
|
523
|
+
...(source.connectorId ? { connector: source.connectorId } : {}),
|
|
524
|
+
}),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (result?.download?.browserId)
|
|
528
|
+
result = {
|
|
529
|
+
...result,
|
|
530
|
+
download: {
|
|
531
|
+
...result.download,
|
|
532
|
+
browserId: encodeBrowserTarget({
|
|
533
|
+
browser: result.download.browserId,
|
|
534
|
+
...(source.connectorId ? { connector: source.connectorId } : {}),
|
|
535
|
+
}),
|
|
536
|
+
},
|
|
537
|
+
};
|
|
431
538
|
if (action !== 'browser_launch' || typeof result?.browserId !== 'string')
|
|
432
539
|
return result;
|
|
433
540
|
return decorateBrowser(result, source);
|
|
@@ -526,7 +633,7 @@ export class UnifiedClient {
|
|
|
526
633
|
else
|
|
527
634
|
pending.reject(new Error(response.error || 'Unknown error from cloud-server'));
|
|
528
635
|
}
|
|
529
|
-
async requestRemote(action, data, timeoutSec, browser, connector) {
|
|
636
|
+
async requestRemote(action, data, timeoutSec, browser, connector, controlSessionId) {
|
|
530
637
|
await this.ensureConnected();
|
|
531
638
|
const ws = this.ws;
|
|
532
639
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
@@ -537,6 +644,8 @@ export class UnifiedClient {
|
|
|
537
644
|
message.browser = browser;
|
|
538
645
|
if (connector)
|
|
539
646
|
message.connector = connector;
|
|
647
|
+
if (controlSessionId)
|
|
648
|
+
message.controlSessionId = controlSessionId;
|
|
540
649
|
const serialized = `${JSON.stringify(message)}\n`;
|
|
541
650
|
const bytes = Buffer.byteLength(serialized, 'utf8');
|
|
542
651
|
if (bytes > MAX_BUFFER_SIZE) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"commander": "^15.0.0",
|
|
60
60
|
"fastest-levenshtein": "^1.0.16",
|
|
61
61
|
"ws": "^8.18.0",
|
|
62
|
-
"@mearl/cloud-types": "2.
|
|
63
|
-
"@mearl/provider": "2.
|
|
62
|
+
"@mearl/cloud-types": "2.20.0",
|
|
63
|
+
"@mearl/provider": "2.20.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.9.1",
|