@mearl/client 2.19.3 → 2.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +142 -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 +78 -23
- 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
|
@@ -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 anchoredAgentPanels.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);
|
|
@@ -172,36 +206,41 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
172
206
|
return context === undefined ? null : context.trim();
|
|
173
207
|
};
|
|
174
208
|
|
|
175
|
-
const
|
|
209
|
+
const anchoredAgentPanels = new Map();
|
|
176
210
|
|
|
177
|
-
const
|
|
211
|
+
const stopAgentAnchorTracking = key => {
|
|
178
212
|
if (key) {
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
anchoredAgentPanels.get(key)?.cleanup();
|
|
214
|
+
anchoredAgentPanels.delete(key);
|
|
181
215
|
return;
|
|
182
216
|
}
|
|
183
|
-
for (const tracked of
|
|
184
|
-
|
|
217
|
+
for (const tracked of anchoredAgentPanels.values()) tracked.cleanup();
|
|
218
|
+
anchoredAgentPanels.clear();
|
|
185
219
|
};
|
|
186
220
|
|
|
187
|
-
const
|
|
221
|
+
const agentPanelAnchor = (options, key, layout) => {
|
|
188
222
|
const anchor = options.anchor;
|
|
189
223
|
if (!(anchor instanceof Element)) {
|
|
190
|
-
throw new Error('
|
|
224
|
+
throw new Error('Agent panel anchor must be an element');
|
|
191
225
|
}
|
|
192
226
|
const rect = anchor.getBoundingClientRect();
|
|
193
227
|
if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) {
|
|
194
|
-
throw new Error('
|
|
228
|
+
throw new Error('Agent panel anchor returned invalid bounds');
|
|
195
229
|
}
|
|
196
230
|
return {
|
|
197
231
|
element: anchor,
|
|
198
232
|
key,
|
|
233
|
+
layout,
|
|
199
234
|
bounds: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
|
|
200
235
|
};
|
|
201
236
|
};
|
|
202
237
|
|
|
203
|
-
const
|
|
204
|
-
|
|
238
|
+
const trackAgentAnchor = presentation => {
|
|
239
|
+
if (presentation.layout === 'workspace' || [...anchoredAgentPanels.values()].some(item => item.presentation.layout === 'workspace')) {
|
|
240
|
+
stopAgentAnchorTracking();
|
|
241
|
+
} else {
|
|
242
|
+
stopAgentAnchorTracking(presentation.key);
|
|
243
|
+
}
|
|
205
244
|
let frame = null;
|
|
206
245
|
const update = () => {
|
|
207
246
|
frame = null;
|
|
@@ -247,7 +286,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
247
286
|
window.removeEventListener('resize', schedule);
|
|
248
287
|
document.removeEventListener('scroll', schedule, true);
|
|
249
288
|
};
|
|
250
|
-
|
|
289
|
+
anchoredAgentPanels.set(presentation.key, { presentation, cleanup });
|
|
251
290
|
};
|
|
252
291
|
|
|
253
292
|
const scrollInlineAgentPage = (presentation, deltaX, deltaY) => {
|
|
@@ -277,8 +316,11 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
277
316
|
};
|
|
278
317
|
|
|
279
318
|
const agentPanelPresentation = (options, layout, key) => {
|
|
280
|
-
if (layout
|
|
281
|
-
|
|
319
|
+
if (layout === 'inline') return agentPanelAnchor(options, key, layout);
|
|
320
|
+
if (layout === 'workspace' && options.anchor !== undefined) {
|
|
321
|
+
return agentPanelAnchor(options, key || 'toolbox-workspace', layout);
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
282
324
|
};
|
|
283
325
|
|
|
284
326
|
const setAgentPanelVisible = async (visible, options = {}) => {
|
|
@@ -298,16 +340,16 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
298
340
|
});
|
|
299
341
|
if (presentation) {
|
|
300
342
|
await ready;
|
|
301
|
-
|
|
343
|
+
trackAgentAnchor(presentation);
|
|
302
344
|
} else if (!visible && key) {
|
|
303
|
-
|
|
345
|
+
stopAgentAnchorTracking(key);
|
|
304
346
|
} else {
|
|
305
|
-
|
|
347
|
+
stopAgentAnchorTracking();
|
|
306
348
|
}
|
|
307
349
|
try {
|
|
308
350
|
await request;
|
|
309
351
|
} catch (error) {
|
|
310
|
-
if (presentation)
|
|
352
|
+
if (presentation) stopAgentAnchorTracking(presentation.key);
|
|
311
353
|
throw error;
|
|
312
354
|
}
|
|
313
355
|
};
|
|
@@ -375,14 +417,14 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
375
417
|
});
|
|
376
418
|
if (presentation) {
|
|
377
419
|
await ready;
|
|
378
|
-
|
|
420
|
+
trackAgentAnchor(presentation);
|
|
379
421
|
} else {
|
|
380
|
-
|
|
422
|
+
stopAgentAnchorTracking();
|
|
381
423
|
}
|
|
382
424
|
try {
|
|
383
425
|
await request;
|
|
384
426
|
} catch (error) {
|
|
385
|
-
if (presentation)
|
|
427
|
+
if (presentation) stopAgentAnchorTracking(presentation.key);
|
|
386
428
|
throw error;
|
|
387
429
|
}
|
|
388
430
|
};
|
|
@@ -446,14 +488,14 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
446
488
|
if (
|
|
447
489
|
response?.type === 'agent-panel-wheel' &&
|
|
448
490
|
typeof response.key === 'string' &&
|
|
449
|
-
|
|
491
|
+
anchoredAgentPanels.has(response.key) &&
|
|
450
492
|
typeof response.deltaX === 'number' &&
|
|
451
493
|
Number.isFinite(response.deltaX) &&
|
|
452
494
|
typeof response.deltaY === 'number' &&
|
|
453
495
|
Number.isFinite(response.deltaY)
|
|
454
496
|
) {
|
|
455
497
|
scrollInlineAgentPage(
|
|
456
|
-
|
|
498
|
+
anchoredAgentPanels.get(response.key).presentation,
|
|
457
499
|
response.deltaX,
|
|
458
500
|
response.deltaY,
|
|
459
501
|
);
|
|
@@ -469,6 +511,19 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
469
511
|
port.start();
|
|
470
512
|
resolveReady(context);
|
|
471
513
|
port.postMessage({ type: 'ready' });
|
|
514
|
+
// Dialogs in the sandbox cannot cover Agent overlays in the parent document.
|
|
515
|
+
syncPageModal();
|
|
516
|
+
const modalObserver = typeof MutationObserver === 'function'
|
|
517
|
+
? new MutationObserver(syncPageModal)
|
|
518
|
+
: null;
|
|
519
|
+
modalObserver?.observe(document.documentElement, {
|
|
520
|
+
attributes: true,
|
|
521
|
+
attributeFilter: ['open'],
|
|
522
|
+
childList: true,
|
|
523
|
+
subtree: true,
|
|
524
|
+
});
|
|
525
|
+
document.addEventListener('toggle', syncPageModal, true);
|
|
526
|
+
document.addEventListener('close', syncPageModal, true);
|
|
472
527
|
};
|
|
473
528
|
window.addEventListener('message', connect);
|
|
474
529
|
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) {
|