@mearl/client 2.20.1 → 2.20.3
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 +10 -0
- package/dist/actionTimeouts.js +1 -1
- package/dist/commands.d.ts +25 -5
- package/dist/commands.js +36 -6
- package/dist/generated-browser-action-protocol.d.ts +24 -1
- package/dist/generated-browser-action-protocol.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/pageEvalArguments.d.ts +3 -0
- package/dist/pageEvalArguments.js +47 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +108 -79
- package/dist/pluginPageBundle.d.ts +1 -0
- package/dist/pluginPageBundle.js +27 -62
- package/dist/pluginRegistry.d.ts +2 -0
- package/dist/pluginRegistry.js +14 -2
- package/dist/pythonPlugin.d.ts +1 -0
- package/dist/pythonPlugin.js +15 -3
- package/dist/socket.d.ts +7 -0
- package/dist/socket.js +74 -10
- package/dist/unified.d.ts +3 -5
- package/dist/unified.js +138 -15
- package/package.json +3 -3
package/dist/pluginPageBundle.js
CHANGED
|
@@ -29,6 +29,8 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
29
29
|
let port = null;
|
|
30
30
|
let requestSequence = 0;
|
|
31
31
|
const pending = new Map();
|
|
32
|
+
const pageEventListeners = new Map();
|
|
33
|
+
let subscriptionSequence = 0;
|
|
32
34
|
let resolveReady;
|
|
33
35
|
const ready = new Promise(resolve => { resolveReady = resolve; });
|
|
34
36
|
|
|
@@ -76,54 +78,25 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
76
78
|
};
|
|
77
79
|
|
|
78
80
|
const invoke = async (command, input = {}) => {
|
|
79
|
-
if (typeof command !== 'string' || !/^[a-z][a-z0-9-]*$/.test(command))
|
|
80
|
-
throw new Error('Invalid plugin command');
|
|
81
|
-
}
|
|
81
|
+
if (typeof command !== 'string' || (!/^[a-z][a-z0-9-]*$/.test(command) && !/^host\.[a-z][a-zA-Z]*(?:\.[a-z][a-zA-Z]*)*$/.test(command))) throw new Error('Invalid command');
|
|
82
82
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
83
83
|
throw new Error('Plugin command input must be an object');
|
|
84
84
|
}
|
|
85
85
|
return requestHost('invoke', { command, input });
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new Error('Invalid plugin operation id');
|
|
101
|
-
}
|
|
102
|
-
return requestHost('invoke-operation', { operationId });
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const runningOperations = async () => requestHost('invoke-operations');
|
|
106
|
-
|
|
107
|
-
const selectDirectory = async () => {
|
|
108
|
-
const path = await requestHost('select-directory');
|
|
109
|
-
if (path !== null && typeof path !== 'string') {
|
|
110
|
-
throw new Error('Directory selection returned an invalid path');
|
|
111
|
-
}
|
|
112
|
-
return path;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const copyText = async text => {
|
|
116
|
-
if (typeof text !== 'string' || text.length > 1000000) {
|
|
117
|
-
throw new Error('Clipboard text must be a string no longer than 1000000 characters');
|
|
118
|
-
}
|
|
119
|
-
await requestHost('clipboard-write', { text });
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const openExternalLink = async url => {
|
|
123
|
-
if (typeof url !== 'string' || !url || url.length > 8192) {
|
|
124
|
-
throw new Error('External link must be a non-empty URL no longer than 8192 characters');
|
|
125
|
-
}
|
|
126
|
-
await requestHost('open-external-link', { url });
|
|
88
|
+
const subscribe = async (source, target, listener) => {
|
|
89
|
+
if (source !== 'page' || !target || !Number.isSafeInteger(target.tabId) || target.tabId < 0 ||
|
|
90
|
+
typeof target.runtimeId !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(target.runtimeId) ||
|
|
91
|
+
typeof listener !== 'function') throw new Error('Invalid event subscription');
|
|
92
|
+
const subscriptionId = String(++subscriptionSequence);
|
|
93
|
+
pageEventListeners.set(subscriptionId, listener);
|
|
94
|
+
try { await requestHost('subscribe', { source, target, subscriptionId }); }
|
|
95
|
+
catch (error) { pageEventListeners.delete(subscriptionId); throw error; }
|
|
96
|
+
return () => {
|
|
97
|
+
if (!pageEventListeners.delete(subscriptionId)) return;
|
|
98
|
+
void requestHost('unsubscribe', { subscriptionId }).catch(() => {});
|
|
99
|
+
};
|
|
127
100
|
};
|
|
128
101
|
|
|
129
102
|
const handleExternalLinkClick = event => {
|
|
@@ -140,28 +113,11 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
140
113
|
}
|
|
141
114
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
|
|
142
115
|
event.preventDefault();
|
|
143
|
-
void
|
|
116
|
+
void invoke('host.link.open', { url: url.href }).catch(error => {
|
|
144
117
|
console.error('Failed to open external link', error);
|
|
145
118
|
});
|
|
146
119
|
};
|
|
147
120
|
|
|
148
|
-
const toast = async (message, options = {}) => {
|
|
149
|
-
if (typeof message !== 'string' || !message.trim() || message.length > 4000) {
|
|
150
|
-
throw new Error('Toast message must be a non-empty string no longer than 4000 characters');
|
|
151
|
-
}
|
|
152
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
153
|
-
throw new Error('Toast options must be an object');
|
|
154
|
-
}
|
|
155
|
-
const type = options.type ?? 'info';
|
|
156
|
-
const duration = options.duration ?? 3000;
|
|
157
|
-
if (!['success', 'error', 'info'].includes(type)) throw new Error('Invalid toast type');
|
|
158
|
-
if (!Number.isInteger(duration) || duration < 1000 || duration > 30000) {
|
|
159
|
-
throw new Error('Toast duration must be between 1000 and 30000 milliseconds');
|
|
160
|
-
}
|
|
161
|
-
await ready;
|
|
162
|
-
port.postMessage({ type: 'toast', message: message.trim(), toastType: type, duration });
|
|
163
|
-
};
|
|
164
|
-
|
|
165
121
|
const agentPanelLayout = options => {
|
|
166
122
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
167
123
|
throw new Error('Agent panel options must be an object');
|
|
@@ -441,7 +397,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
441
397
|
return query;
|
|
442
398
|
};
|
|
443
399
|
|
|
444
|
-
const api = {
|
|
400
|
+
const api = { invoke, subscribe, ready: () => ready, runAgentTask, setAgentGuides, setAgentPanelVisible, setQueryParam };
|
|
445
401
|
const ui = globalThis.__mearlToolboxUi;
|
|
446
402
|
if (ui !== undefined) {
|
|
447
403
|
if (!ui || typeof ui !== 'object' || ui.runtime !== 'preact') {
|
|
@@ -481,6 +437,10 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
481
437
|
port = nextPort;
|
|
482
438
|
port.onmessage = responseEvent => {
|
|
483
439
|
const response = responseEvent.data;
|
|
440
|
+
if (response?.type === 'event' && typeof response.subscriptionId === 'string') {
|
|
441
|
+
pageEventListeners.get(response.subscriptionId)?.(response.event, response.metadata);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
484
444
|
if (response?.type === 'context') {
|
|
485
445
|
applyContext(response.context);
|
|
486
446
|
return;
|
|
@@ -506,7 +466,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
506
466
|
if (!request) return;
|
|
507
467
|
pending.delete(response.requestId);
|
|
508
468
|
if (response.ok) request.resolve(response.result);
|
|
509
|
-
else request.reject(new Error(response.error || 'Plugin command failed'));
|
|
469
|
+
else request.reject(Object.assign(new Error(response.error || 'Plugin command failed'), { handledByHost: response.handledByHost === true }));
|
|
510
470
|
};
|
|
511
471
|
port.start();
|
|
512
472
|
resolveReady(context);
|
|
@@ -604,6 +564,11 @@ body {
|
|
|
604
564
|
}
|
|
605
565
|
button, input, select, textarea { font: inherit; }
|
|
606
566
|
</style>`;
|
|
567
|
+
export const PLUGIN_PAGE_RUNTIME_HASH = createHash('sha256')
|
|
568
|
+
.update(PAGE_BRIDGE_SCRIPT)
|
|
569
|
+
.update(PAGE_SECURITY_META)
|
|
570
|
+
.update(PAGE_THEME_STYLE)
|
|
571
|
+
.digest('hex');
|
|
607
572
|
function pluginRoot(entry) {
|
|
608
573
|
const entryDirectory = dirname(entry);
|
|
609
574
|
return basename(entryDirectory) === 'scripts' ? dirname(entryDirectory) : entryDirectory;
|
package/dist/pluginRegistry.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface PluginToolboxAgentConfig {
|
|
|
18
18
|
}
|
|
19
19
|
export interface PluginToolboxContribution {
|
|
20
20
|
version: 1;
|
|
21
|
+
displayMode?: 'popup' | 'tab' | 'both';
|
|
21
22
|
title: string;
|
|
22
23
|
description?: string;
|
|
23
24
|
icon?: string;
|
|
@@ -40,6 +41,7 @@ export interface PluginRegistration {
|
|
|
40
41
|
source?: {
|
|
41
42
|
entry: string;
|
|
42
43
|
page?: Record<string, string>;
|
|
44
|
+
pageRuntime?: string;
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
export declare function pluginRegistrationNeedsRefresh(registration: PluginRegistration, includePage?: boolean): boolean;
|
package/dist/pluginRegistry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, re
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { COMMAND_MAP } from './commands.js';
|
|
6
|
-
import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, } from './pluginPageBundle.js';
|
|
6
|
+
import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, PLUGIN_PAGE_RUNTIME_HASH, } from './pluginPageBundle.js';
|
|
7
7
|
import { pluginFileVersion } from './pluginSource.js';
|
|
8
8
|
const RESERVED_NAMES = new Set([
|
|
9
9
|
'check',
|
|
@@ -27,6 +27,8 @@ export function pluginRegistrationNeedsRefresh(registration, includePage = false
|
|
|
27
27
|
return true;
|
|
28
28
|
if (!includePage || !registration.toolbox)
|
|
29
29
|
return false;
|
|
30
|
+
if (registration.source.pageRuntime !== PLUGIN_PAGE_RUNTIME_HASH)
|
|
31
|
+
return true;
|
|
30
32
|
if (!registration.source.page)
|
|
31
33
|
return true;
|
|
32
34
|
return Object.entries(registration.source.page).some(([path, version]) => pluginFileVersion(path) !== version);
|
|
@@ -108,6 +110,10 @@ export function normalizePluginToolboxContribution(value) {
|
|
|
108
110
|
throw new Error('toolbox 必须是对象');
|
|
109
111
|
}
|
|
110
112
|
const candidate = value;
|
|
113
|
+
if (candidate.displayMode !== undefined &&
|
|
114
|
+
!['popup', 'tab', 'both'].includes(candidate.displayMode)) {
|
|
115
|
+
throw new Error('toolbox.displayMode 必须是 popup、tab 或 both');
|
|
116
|
+
}
|
|
111
117
|
const title = typeof candidate.title === 'string' ? candidate.title.trim() : '';
|
|
112
118
|
const description = typeof candidate.description === 'string' ? candidate.description.trim() : undefined;
|
|
113
119
|
const icon = typeof candidate.icon === 'string' ? candidate.icon.trim() : undefined;
|
|
@@ -216,6 +222,7 @@ export function normalizePluginToolboxContribution(value) {
|
|
|
216
222
|
return {
|
|
217
223
|
version: 1,
|
|
218
224
|
title,
|
|
225
|
+
...(candidate.displayMode !== undefined ? { displayMode: candidate.displayMode } : {}),
|
|
219
226
|
...(description ? { description } : {}),
|
|
220
227
|
...(icon ? { icon } : {}),
|
|
221
228
|
...(locales ? { locales } : {}),
|
|
@@ -281,7 +288,10 @@ export function registerPlugin(name, target, description, options = {}) {
|
|
|
281
288
|
}
|
|
282
289
|
const record = {
|
|
283
290
|
entry,
|
|
284
|
-
source: {
|
|
291
|
+
source: {
|
|
292
|
+
entry: entryVersion,
|
|
293
|
+
...(builtPage ? { page: builtPage.sources, pageRuntime: PLUGIN_PAGE_RUNTIME_HASH } : {}),
|
|
294
|
+
},
|
|
285
295
|
...(summary ? { description: summary } : {}),
|
|
286
296
|
...(managedBy ? { managedBy } : {}),
|
|
287
297
|
...(toolbox ? { toolbox } : {}),
|
|
@@ -344,6 +354,8 @@ export function findPluginRegistration(name) {
|
|
|
344
354
|
typeof source !== 'object' ||
|
|
345
355
|
Array.isArray(source) ||
|
|
346
356
|
typeof source.entry !== 'string' ||
|
|
357
|
+
(source.pageRuntime !== undefined &&
|
|
358
|
+
(typeof source.pageRuntime !== 'string' || !/^[a-f0-9]{64}$/.test(source.pageRuntime))) ||
|
|
347
359
|
(source.page !== undefined &&
|
|
348
360
|
(!source.page ||
|
|
349
361
|
typeof source.page !== 'object' ||
|
package/dist/pythonPlugin.d.ts
CHANGED
package/dist/pythonPlugin.js
CHANGED
|
@@ -53,6 +53,12 @@ class _App:
|
|
|
53
53
|
self.request_id = 0
|
|
54
54
|
|
|
55
55
|
def command(self, signature, handler=None, description=""):
|
|
56
|
+
return self._register_command(signature, handler, description, False)
|
|
57
|
+
|
|
58
|
+
def toolboxCommand(self, signature, handler=None, description=""):
|
|
59
|
+
return self._register_command(signature, handler, description, True)
|
|
60
|
+
|
|
61
|
+
def _register_command(self, signature, handler, description, toolbox_only):
|
|
56
62
|
if not self.registering:
|
|
57
63
|
raise RuntimeError("命令只能在入口注册阶段声明")
|
|
58
64
|
if not isinstance(signature, str):
|
|
@@ -67,6 +73,7 @@ class _App:
|
|
|
67
73
|
"signature": signature,
|
|
68
74
|
"description": description,
|
|
69
75
|
"handler": candidate,
|
|
76
|
+
"toolboxOnly": toolbox_only,
|
|
70
77
|
})
|
|
71
78
|
return candidate
|
|
72
79
|
|
|
@@ -163,7 +170,7 @@ try:
|
|
|
163
170
|
descriptor = {
|
|
164
171
|
"type": "ready",
|
|
165
172
|
"commands": [
|
|
166
|
-
{"signature": item["signature"], "description": item["description"]}
|
|
173
|
+
{"signature": item["signature"], "description": item["description"], "toolboxOnly": item["toolboxOnly"]}
|
|
167
174
|
for item in app.commands
|
|
168
175
|
],
|
|
169
176
|
}
|
|
@@ -223,10 +230,15 @@ function parseDescriptor(message) {
|
|
|
223
230
|
const commands = message.commands.map(value => {
|
|
224
231
|
if (!isObject(value) ||
|
|
225
232
|
typeof value.signature !== 'string' ||
|
|
226
|
-
typeof value.description !== 'string'
|
|
233
|
+
typeof value.description !== 'string' ||
|
|
234
|
+
typeof value.toolboxOnly !== 'boolean') {
|
|
227
235
|
throw new Error('Python 插件返回了无效的命令描述');
|
|
228
236
|
}
|
|
229
|
-
return {
|
|
237
|
+
return {
|
|
238
|
+
signature: value.signature,
|
|
239
|
+
description: value.description,
|
|
240
|
+
toolboxOnly: value.toolboxOnly,
|
|
241
|
+
};
|
|
230
242
|
});
|
|
231
243
|
if (message.name !== undefined && typeof message.name !== 'string') {
|
|
232
244
|
throw new Error('Python 插件返回了无效的 name');
|
package/dist/socket.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type ControlSource = 'local' | 'remote';
|
|
|
18
18
|
type CompatibleControlSource = ControlSource | 'cloud';
|
|
19
19
|
export interface InvokeOptions {
|
|
20
20
|
controlSessionId?: string;
|
|
21
|
+
/** Pin a known local endpoint for a sequence of transport requests. */
|
|
22
|
+
socketPath?: string;
|
|
21
23
|
/** 超时时间(秒),默认 15 */
|
|
22
24
|
timeoutSec?: number;
|
|
23
25
|
/** Force CDP transport instead of extension */
|
|
@@ -31,6 +33,11 @@ export interface InvokeOptions {
|
|
|
31
33
|
export declare function setDefaultBrowser(selector: string | undefined): void;
|
|
32
34
|
export declare function getDefaultBrowser(): string | undefined;
|
|
33
35
|
export declare function decorateBrowserListResult(result: Record<string, any>): Record<string, any>;
|
|
36
|
+
/** Resolve an existing local route without executing the action. */
|
|
37
|
+
export declare function resolveLocalActionTarget(action: string, data: Record<string, any>, options?: InvokeOptions): {
|
|
38
|
+
browser?: string;
|
|
39
|
+
socketPath?: string;
|
|
40
|
+
} | undefined;
|
|
34
41
|
/**
|
|
35
42
|
* 向 native host 发送一个 action 请求并等待响应。
|
|
36
43
|
*
|
package/dist/socket.js
CHANGED
|
@@ -18,6 +18,7 @@ import { readLiveProvider } from '@mearl/provider/runtime';
|
|
|
18
18
|
import { resolveFileRefs } from './fileRefs.js';
|
|
19
19
|
import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
|
|
20
20
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
21
|
+
import { isBrowserCapabilityAction } from './generated-browser-action-protocol.js';
|
|
21
22
|
const require = createRequire(import.meta.url);
|
|
22
23
|
export const CLIENT_VERSION = require('../package.json').version;
|
|
23
24
|
const MAX_SOCKET_BUFFER_SIZE = 64 * 1024 * 1024;
|
|
@@ -160,6 +161,24 @@ function supportsAction(browser, action, data) {
|
|
|
160
161
|
return (browser.transport !== 'provider' ||
|
|
161
162
|
browser.capabilities?.actions.some(capability => capability === action) === true);
|
|
162
163
|
}
|
|
164
|
+
function routableBrowsers(browsers, action, data, transport) {
|
|
165
|
+
return browsers.filter(browser => isConnectedBrowser(browser) &&
|
|
166
|
+
(action !== 'browser_list' || browser.transport !== 'provider') &&
|
|
167
|
+
!(transport === 'cdp' && browser.transport === 'provider') &&
|
|
168
|
+
supportsAction(browser, action, data));
|
|
169
|
+
}
|
|
170
|
+
/** Resolve an existing local route without executing the action. */
|
|
171
|
+
export function resolveLocalActionTarget(action, data, options = {}) {
|
|
172
|
+
const selector = options.browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
173
|
+
const socketPath = options.socketPath ?? process.env.MEARL_SOCKET_PATH?.trim();
|
|
174
|
+
if (socketPath && (options.socketPath || !selector))
|
|
175
|
+
return { socketPath };
|
|
176
|
+
const browsers = listLiveBrowsers();
|
|
177
|
+
const target = selector
|
|
178
|
+
? findBrowser(browsers, selector)
|
|
179
|
+
: routableBrowsers(browsers, action, data, options.transport)[0];
|
|
180
|
+
return target ? { browser: target.browserId } : undefined;
|
|
181
|
+
}
|
|
163
182
|
function selectBrowserResult(data) {
|
|
164
183
|
const selector = typeof data.browser === 'string' ? data.browser.trim() : '';
|
|
165
184
|
if (!selector) {
|
|
@@ -329,6 +348,17 @@ function removeStaleLegacySocket(identity) {
|
|
|
329
348
|
* legacy well-known socket,并在 socket 不存在或已残留时自动拉起 native host。
|
|
330
349
|
*/
|
|
331
350
|
export function invoke(action, data = {}, options = {}) {
|
|
351
|
+
if (action === '__resolve_action_target') {
|
|
352
|
+
if (!isBrowserCapabilityAction(data.action))
|
|
353
|
+
return Promise.reject(new Error('Invalid browser capability action'));
|
|
354
|
+
const target = resolveLocalActionTarget(data.action, data.data ?? {}, options);
|
|
355
|
+
const browser = target?.browser ??
|
|
356
|
+
(target?.socketPath
|
|
357
|
+
? listLiveBrowsers().find(item => item.socketPath === target.socketPath &&
|
|
358
|
+
supportsAction(item, data.action, data.data ?? {}))?.browserId
|
|
359
|
+
: undefined);
|
|
360
|
+
return Promise.resolve(browser ? { browser } : null);
|
|
361
|
+
}
|
|
332
362
|
if (action === 'browser_select_browser') {
|
|
333
363
|
try {
|
|
334
364
|
return Promise.resolve(selectBrowserResult(data));
|
|
@@ -338,14 +368,50 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
338
368
|
}
|
|
339
369
|
}
|
|
340
370
|
const { timeoutSec = resolveActionTimeoutSec(action, data), transport, browser, controlSource = 'local', } = options;
|
|
371
|
+
if (action === '__control_session_resolve') {
|
|
372
|
+
return resolveControlSession(data.sessionId, timeoutSec, controlSource);
|
|
373
|
+
}
|
|
341
374
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
342
375
|
const resolvedData = resolveFileRefs(data);
|
|
343
376
|
Object.defineProperty(resolvedData, SESSION_OPTION, { value: options.controlSessionId });
|
|
344
377
|
if (action === 'browser_list') {
|
|
345
|
-
return dispatchInvoke(action, resolvedData, timeoutSec, transport, undefined, controlSource).then(result => decorateBrowserListResult(transport === 'cdp' ? cdpOnlyBrowserList(result) : result));
|
|
378
|
+
return dispatchInvoke(action, resolvedData, timeoutSec, transport, undefined, controlSource, options.socketPath).then(result => decorateBrowserListResult(transport === 'cdp' ? cdpOnlyBrowserList(result) : result));
|
|
346
379
|
}
|
|
347
380
|
const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
348
|
-
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
|
|
381
|
+
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource, options.socketPath);
|
|
382
|
+
}
|
|
383
|
+
/** Session ownership lives in the control hosts and survives individual CLI connections. */
|
|
384
|
+
async function resolveControlSession(sessionId, timeoutSec, controlSource) {
|
|
385
|
+
const browsers = listLiveBrowsers();
|
|
386
|
+
const configuredSocketPath = process.env.MEARL_SOCKET_PATH?.trim();
|
|
387
|
+
const sockets = configuredSocketPath
|
|
388
|
+
? [configuredSocketPath]
|
|
389
|
+
: [
|
|
390
|
+
...new Set(browsers
|
|
391
|
+
.filter(browser => browser.transport !== 'provider')
|
|
392
|
+
.map(browser => browser.socketPath)),
|
|
393
|
+
];
|
|
394
|
+
const provider = configuredSocketPath
|
|
395
|
+
? undefined
|
|
396
|
+
: browsers.find(browser => browser.transport === 'provider');
|
|
397
|
+
if (!sockets.length && !provider)
|
|
398
|
+
sockets.push(SOCKET_PATH);
|
|
399
|
+
const requests = sockets.map(socketPath => invokeOnce('__control_session_resolve', { sessionId }, timeoutSec, undefined, socketPath, controlSource));
|
|
400
|
+
if (provider) {
|
|
401
|
+
requests.push(invokeOnce('__control_session_resolve', { sessionId }, timeoutSec, undefined, provider.socketPath, controlSource, provider.browserId));
|
|
402
|
+
}
|
|
403
|
+
const results = await Promise.allSettled(requests);
|
|
404
|
+
const sessions = results.flatMap(result => result.status === 'fulfilled' && result.value?.session?.sessionId === sessionId
|
|
405
|
+
? [result.value.session]
|
|
406
|
+
: []);
|
|
407
|
+
if (sessions.length > 1)
|
|
408
|
+
throw new Error('CONTROL_SESSION_AMBIGUOUS: multiple hosts own this id');
|
|
409
|
+
if (sessions.length)
|
|
410
|
+
return { session: sessions[0] };
|
|
411
|
+
const failure = results.find(result => result.status === 'rejected' && !isConnError(result.reason));
|
|
412
|
+
if (failure?.status === 'rejected')
|
|
413
|
+
throw failure.reason;
|
|
414
|
+
return { session: null };
|
|
349
415
|
}
|
|
350
416
|
function isConnError(error) {
|
|
351
417
|
const code = error?.code;
|
|
@@ -359,9 +425,9 @@ function isUnavailableConnection(error) {
|
|
|
359
425
|
const code = cause?.code;
|
|
360
426
|
return isConnError(cause) || isSocketSandboxError(cause) || code === 'EACCES';
|
|
361
427
|
}
|
|
362
|
-
async function dispatchInvoke(action, data, timeoutSec, transport, selector, controlSource) {
|
|
363
|
-
const configuredSocketPath = process.env.MEARL_SOCKET_PATH?.trim();
|
|
364
|
-
if (
|
|
428
|
+
async function dispatchInvoke(action, data, timeoutSec, transport, selector, controlSource, socketPath) {
|
|
429
|
+
const configuredSocketPath = socketPath ?? process.env.MEARL_SOCKET_PATH?.trim();
|
|
430
|
+
if (configuredSocketPath && (socketPath || !selector)) {
|
|
365
431
|
try {
|
|
366
432
|
return await invokeOnce(action, data, timeoutSec, transport, configuredSocketPath, controlSource);
|
|
367
433
|
}
|
|
@@ -447,11 +513,9 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
447
513
|
}
|
|
448
514
|
// No explicit target: prefer the most-recently-focused live browser; on a
|
|
449
515
|
// stale socket, fall through to the legacy path + auto-spawn.
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (routableBrowsers.length > 0) {
|
|
454
|
-
const target = routableBrowsers[0];
|
|
516
|
+
const candidates = routableBrowsers(browsers, action, data, transport);
|
|
517
|
+
if (candidates.length > 0) {
|
|
518
|
+
const target = candidates[0];
|
|
455
519
|
try {
|
|
456
520
|
return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
|
|
457
521
|
}
|
package/dist/unified.d.ts
CHANGED
|
@@ -30,12 +30,8 @@ interface BrowserTarget {
|
|
|
30
30
|
}
|
|
31
31
|
export declare function decodeBrowserTarget(selector: string | undefined): BrowserTarget | undefined;
|
|
32
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
|
-
};
|
|
38
33
|
export declare class UnifiedClient {
|
|
34
|
+
private readonly controlSessions;
|
|
39
35
|
private readonly options;
|
|
40
36
|
private defaultBrowser;
|
|
41
37
|
private defaultConnector;
|
|
@@ -55,6 +51,7 @@ export declare class UnifiedClient {
|
|
|
55
51
|
outputPath: string;
|
|
56
52
|
bytes: number;
|
|
57
53
|
}>;
|
|
54
|
+
private pinPageEvalTarget;
|
|
58
55
|
private invokeRouted;
|
|
59
56
|
private invokeExplicitLocal;
|
|
60
57
|
private invokeLaunchWithoutControlBrowser;
|
|
@@ -71,6 +68,7 @@ export declare class UnifiedClient {
|
|
|
71
68
|
private invokeLocal;
|
|
72
69
|
private invokeRemote;
|
|
73
70
|
private decorateActionResult;
|
|
71
|
+
private resolveControlSession;
|
|
74
72
|
private ensureConnected;
|
|
75
73
|
private connect;
|
|
76
74
|
private startHeartbeat;
|