@mearl/client 2.19.2 → 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.
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
4
4
  import { isAbsolute, join, resolve } from 'node:path';
5
5
  import { COMMAND_MAP } from './commands.js';
6
6
  import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, } from './pluginPageBundle.js';
7
+ import { pluginFileVersion } from './pluginSource.js';
7
8
  const RESERVED_NAMES = new Set([
8
9
  'check',
9
10
  'help',
@@ -19,6 +20,17 @@ export class PluginError extends Error {
19
20
  this.code = code;
20
21
  }
21
22
  }
23
+ export function pluginRegistrationNeedsRefresh(registration, includePage = false) {
24
+ if (!registration.source)
25
+ return true;
26
+ if (pluginFileVersion(registration.entry) !== registration.source.entry)
27
+ return true;
28
+ if (!includePage || !registration.toolbox)
29
+ return false;
30
+ if (!registration.source.page)
31
+ return true;
32
+ return Object.entries(registration.source.page).some(([path, version]) => pluginFileVersion(path) !== version);
33
+ }
22
34
  export function pluginDirectory() {
23
35
  return resolve(process.env.MEARL_PLUGIN_DIR || join(homedir(), '.mearl', 'plugins'));
24
36
  }
@@ -242,6 +254,7 @@ export function resolvePluginEntry(target) {
242
254
  export function registerPlugin(name, target, description, options = {}) {
243
255
  assertPluginName(name);
244
256
  const entry = resolvePluginEntry(target);
257
+ const entryVersion = pluginFileVersion(entry);
245
258
  const directory = pluginDirectory();
246
259
  mkdirSync(directory, { recursive: true, mode: 0o700 });
247
260
  const file = join(directory, `${name}.json`);
@@ -268,6 +281,7 @@ export function registerPlugin(name, target, description, options = {}) {
268
281
  }
269
282
  const record = {
270
283
  entry,
284
+ source: { entry: entryVersion, ...(builtPage ? { page: builtPage.sources } : {}) },
271
285
  ...(summary ? { description: summary } : {}),
272
286
  ...(managedBy ? { managedBy } : {}),
273
287
  ...(toolbox ? { toolbox } : {}),
@@ -324,12 +338,26 @@ export function findPluginRegistration(name) {
324
338
  : undefined;
325
339
  const toolbox = normalizePluginToolboxContribution(record.toolbox);
326
340
  const pageBundle = toolbox ? normalizePageBundle(record.pageBundle) : undefined;
341
+ const source = record.source;
342
+ if (source !== undefined &&
343
+ (!source ||
344
+ typeof source !== 'object' ||
345
+ Array.isArray(source) ||
346
+ typeof source.entry !== 'string' ||
347
+ (source.page !== undefined &&
348
+ (!source.page ||
349
+ typeof source.page !== 'object' ||
350
+ Array.isArray(source.page) ||
351
+ Object.entries(source.page).some(([path, version]) => !isAbsolute(path) || typeof version !== 'string'))))) {
352
+ throw new Error('invalid source');
353
+ }
327
354
  return {
328
355
  entry: record.entry,
329
356
  ...(description ? { description } : {}),
330
357
  ...(managedBy ? { managedBy } : {}),
331
358
  ...(toolbox ? { toolbox } : {}),
332
359
  ...(pageBundle ? { pageBundle } : {}),
360
+ ...(source ? { source } : {}),
333
361
  };
334
362
  }
335
363
  catch {
@@ -0,0 +1,2 @@
1
+ /** 文件替换和同大小写入都需要使缓存失效;检测只读取文件元数据。 */
2
+ export declare function pluginFileVersion(path: string): string;
@@ -0,0 +1,8 @@
1
+ import { statSync } from 'node:fs';
2
+ /** 文件替换和同大小写入都需要使缓存失效;检测只读取文件元数据。 */
3
+ export function pluginFileVersion(path) {
4
+ const stat = statSync(path);
5
+ if (!stat.isFile())
6
+ throw new Error(`插件资源不是文件: ${path}`);
7
+ return `${stat.dev}-${stat.ino}-${stat.mtimeMs}-${stat.ctimeMs}-${stat.size}`;
8
+ }
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
@@ -96,13 +97,50 @@ function browserListFromProviderRecords(browsers) {
96
97
  browsers: providerBrowsers,
97
98
  };
98
99
  }
100
+ function withDiscoveryWarning(result, error) {
101
+ const cause = error.cause ?? error;
102
+ const code = cause?.code;
103
+ const detail = code ?? (error instanceof Error ? error.message.split('\n')[0] : String(error));
104
+ return {
105
+ ...result,
106
+ warnings: [
107
+ ...(Array.isArray(result.warnings) ? result.warnings : []),
108
+ `Local CDP discovery could not complete: ${detail}`,
109
+ ],
110
+ };
111
+ }
112
+ function cdpOnlyBrowserList(result) {
113
+ const browsers = Array.isArray(result.browsers) ? result.browsers : [];
114
+ const cdpBrowsers = browsers.filter(browser => browser.transport === 'cdp');
115
+ return {
116
+ ...result,
117
+ count: cdpBrowsers.length,
118
+ connectedCount: cdpBrowsers.filter(browser => browser.status === 'connected').length,
119
+ browsers: cdpBrowsers,
120
+ };
121
+ }
99
122
  function areInstalledProvidersRunning() {
100
123
  return listInstalledProviders().every(provider => readLiveProvider(provider.providerId));
101
124
  }
102
125
  function isConnectedBrowser(browser) {
103
126
  return browser.status === undefined || browser.status === 'connected';
104
127
  }
128
+ function isOrdinaryLocalBrowser(browser) {
129
+ return (!browser.browserId.startsWith('managed:') &&
130
+ (browser.transport === 'extension' || browser.transport === 'cdp'));
131
+ }
105
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
+ }
106
144
  if (action === 'get_versions')
107
145
  return true;
108
146
  if (action === 'get_browser_access_url') {
@@ -141,47 +179,62 @@ function selectBrowserResult(data) {
141
179
  }
142
180
  /** Deduplicates concurrent recovery attempts in long-lived clients such as the MCP server. */
143
181
  let persistentSpawnPromise = null;
182
+ let discoveryListPromise = null;
183
+ let discoveryListCache = null;
184
+ const DISCOVERY_STARTUP_TIMEOUT_MS = 3_000;
185
+ const DISCOVERY_RETRY_INTERVAL_MS = 50;
186
+ const DISCOVERY_MISS_CACHE_MS = 2_000;
187
+ function instanceSocketPath(pid) {
188
+ return process.platform === 'win32'
189
+ ? `\\\\.\\pipe\\mearl-agent-${pid}`
190
+ : path.join(os.tmpdir(), `mearl-agent-${pid}.sock`);
191
+ }
192
+ function discoveryCacheKey() {
193
+ const browsers = listLiveBrowsers()
194
+ .map(browser => [
195
+ browser.browserId,
196
+ browser.pid,
197
+ browser.socketPath,
198
+ browser.status,
199
+ browser.updatedAt,
200
+ ])
201
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0])));
202
+ const providers = listInstalledProviders()
203
+ .map(provider => provider.providerId)
204
+ .sort();
205
+ return JSON.stringify({ browsers, providers });
206
+ }
207
+ async function spawnNativeHost(args) {
208
+ const { spawn } = await import('node:child_process');
209
+ return new Promise((resolve, reject) => {
210
+ const child = spawn('mearl-native-host', args, {
211
+ detached: true,
212
+ stdio: 'ignore',
213
+ windowsHide: true,
214
+ });
215
+ child.once('error', reject);
216
+ child.once('spawn', () => {
217
+ child.unref();
218
+ resolve(child.pid);
219
+ });
220
+ });
221
+ }
144
222
  /**
145
223
  * Attempt to spawn `mearl-native-host` in the background (detached).
146
- * Browser discovery hosts probe the default local CDP endpoint for browser_list and only
147
- * persist when a real browser is available.
148
224
  * Returns true if a spawn was attempted (regardless of success).
149
225
  */
150
- async function trySpawnNativeHost(mode = 'control') {
151
- const persistent = mode === 'control';
152
- if (persistent && persistentSpawnPromise)
226
+ async function trySpawnNativeHost() {
227
+ if (persistentSpawnPromise)
153
228
  return persistentSpawnPromise;
154
229
  const spawnPromise = (async () => {
155
230
  try {
156
- const { spawn } = await import('node:child_process');
157
- return await new Promise(resolve => {
158
- const args = persistent ? ['--daemon'] : ['--daemon', '--discover-cdp'];
159
- const child = spawn('mearl-native-host', args, {
160
- detached: true,
161
- stdio: 'ignore',
162
- windowsHide: true,
163
- });
164
- let settled = false;
165
- const finish = (ok) => {
166
- if (settled)
167
- return;
168
- settled = true;
169
- resolve(ok);
170
- };
171
- // `spawn ENOENT` 等错误会通过事件异步抛出;必须显式监听避免进程崩溃。
172
- child.once('error', () => finish(false));
173
- child.once('spawn', () => {
174
- child.unref();
175
- finish(true);
176
- });
177
- });
231
+ await spawnNativeHost(['--daemon']);
232
+ return true;
178
233
  }
179
234
  catch {
180
235
  return false;
181
236
  }
182
237
  })();
183
- if (!persistent)
184
- return spawnPromise;
185
238
  persistentSpawnPromise = spawnPromise;
186
239
  try {
187
240
  return await spawnPromise;
@@ -191,6 +244,54 @@ async function trySpawnNativeHost(mode = 'control') {
191
244
  persistentSpawnPromise = null;
192
245
  }
193
246
  }
247
+ async function discoverLocalBrowserList(timeoutSec, controlSource) {
248
+ if (discoveryListPromise)
249
+ return discoveryListPromise;
250
+ const cacheKey = discoveryCacheKey();
251
+ if (discoveryListCache &&
252
+ discoveryListCache.key === cacheKey &&
253
+ Date.now() < discoveryListCache.until) {
254
+ if (discoveryListCache.error)
255
+ throw discoveryListCache.error;
256
+ return discoveryListCache.result;
257
+ }
258
+ discoveryListCache = null;
259
+ const request = (async () => {
260
+ const pid = await spawnNativeHost(['--daemon', '--discover-cdp']);
261
+ if (pid === undefined)
262
+ throw new Error('Native host did not report its process id');
263
+ const socketPath = instanceSocketPath(pid);
264
+ const deadline = Date.now() + Math.min(timeoutSec * 1_000, DISCOVERY_STARTUP_TIMEOUT_MS);
265
+ while (true) {
266
+ try {
267
+ return await invokeOnce('browser_list', {}, timeoutSec, undefined, socketPath, controlSource);
268
+ }
269
+ catch (error) {
270
+ if (!isConnError(error) || Date.now() >= deadline)
271
+ throw error;
272
+ await new Promise(resolve => setTimeout(resolve, DISCOVERY_RETRY_INTERVAL_MS));
273
+ }
274
+ }
275
+ })();
276
+ discoveryListPromise = request;
277
+ try {
278
+ const result = await request;
279
+ if (!result.browsers?.some((browser) => browser.transport === 'cdp' &&
280
+ browser.status === 'connected' &&
281
+ !browser.browserId?.startsWith('managed:'))) {
282
+ discoveryListCache = { key: cacheKey, result, until: Date.now() + DISCOVERY_MISS_CACHE_MS };
283
+ }
284
+ return result;
285
+ }
286
+ catch (error) {
287
+ discoveryListCache = { key: cacheKey, error, until: Date.now() + DISCOVERY_MISS_CACHE_MS };
288
+ throw error;
289
+ }
290
+ finally {
291
+ if (discoveryListPromise === request)
292
+ discoveryListPromise = null;
293
+ }
294
+ }
194
295
  function readOwnedLegacySocketIdentity() {
195
296
  if (process.platform === 'win32')
196
297
  return null;
@@ -239,8 +340,9 @@ export function invoke(action, data = {}, options = {}) {
239
340
  const { timeoutSec = resolveActionTimeoutSec(action, data), transport, browser, controlSource = 'local', } = options;
240
341
  // 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
241
342
  const resolvedData = resolveFileRefs(data);
343
+ Object.defineProperty(resolvedData, SESSION_OPTION, { value: options.controlSessionId });
242
344
  if (action === 'browser_list') {
243
- return dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(result));
345
+ return dispatchInvoke(action, resolvedData, timeoutSec, transport, undefined, controlSource).then(result => decorateBrowserListResult(transport === 'cdp' ? cdpOnlyBrowserList(result) : result));
244
346
  }
245
347
  const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
246
348
  return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
@@ -252,6 +354,11 @@ function isConnError(error) {
252
354
  function isSocketSandboxError(error) {
253
355
  return error?.code === 'EPERM';
254
356
  }
357
+ function isUnavailableConnection(error) {
358
+ const cause = error.cause ?? error;
359
+ const code = cause?.code;
360
+ return isConnError(cause) || isSocketSandboxError(cause) || code === 'EACCES';
361
+ }
255
362
  async function dispatchInvoke(action, data, timeoutSec, transport, selector, controlSource) {
256
363
  const configuredSocketPath = process.env.MEARL_SOCKET_PATH?.trim();
257
364
  if (!selector && configuredSocketPath) {
@@ -269,6 +376,44 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
269
376
  }
270
377
  const browsers = listLiveBrowsers();
271
378
  const controllableBrowsers = browsers.filter(isConnectedBrowser);
379
+ if (action === 'browser_list' && transport === 'cdp') {
380
+ const registeredCdp = controllableBrowsers.find(browser => browser.transport === 'cdp' && isOrdinaryLocalBrowser(browser));
381
+ if (registeredCdp) {
382
+ try {
383
+ return await invokeOnce(action, data, timeoutSec, transport, registeredCdp.socketPath, controlSource);
384
+ }
385
+ catch (error) {
386
+ if (!isConnError(error))
387
+ throw error;
388
+ }
389
+ }
390
+ try {
391
+ return await discoverLocalBrowserList(timeoutSec, controlSource);
392
+ }
393
+ catch (error) {
394
+ if (isUnavailableConnection(error) &&
395
+ browsers.some(browser => browser.transport === 'provider') &&
396
+ areInstalledProvidersRunning()) {
397
+ return withDiscoveryWarning(browserListFromProviderRecords(browsers), error);
398
+ }
399
+ throw error;
400
+ }
401
+ }
402
+ // A Provider record can keep the registry nonempty without representing a local
403
+ // browser transport. Let the discovery daemon probe CDP before returning that inventory.
404
+ if (action === 'browser_list' && !controllableBrowsers.some(isOrdinaryLocalBrowser)) {
405
+ try {
406
+ return await discoverLocalBrowserList(timeoutSec, controlSource);
407
+ }
408
+ catch (error) {
409
+ if (isUnavailableConnection(error) &&
410
+ browsers.some(browser => browser.transport === 'provider') &&
411
+ areInstalledProvidersRunning()) {
412
+ return withDiscoveryWarning(browserListFromProviderRecords(browsers), error);
413
+ }
414
+ throw error;
415
+ }
416
+ }
272
417
  // Explicit target: must match a live browser; do not silently fall back.
273
418
  if (selector) {
274
419
  const match = findBrowser(browsers, selector);
@@ -281,14 +426,15 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
281
426
  if (match.status === 'pairing' &&
282
427
  action !== 'get_versions' &&
283
428
  action !== 'browser_close' &&
284
- action !== 'browser_release') {
429
+ action !== 'browser_release' &&
430
+ action !== 'browser_control') {
285
431
  throw new Error(`Browser ${describeBrowser(match)} is waiting for device pairing`);
286
432
  }
287
433
  if (!supportsAction(match, action, data)) {
288
434
  throw new Error(`Browser ${describeBrowser(match)} does not support ${action}`);
289
435
  }
290
436
  try {
291
- return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.providerBrowserId);
437
+ return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.transport === 'provider' ? match.browserId : undefined);
292
438
  }
293
439
  catch (error) {
294
440
  if (isConnError(error) || isSocketSandboxError(error)) {
@@ -301,12 +447,13 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
301
447
  }
302
448
  // No explicit target: prefer the most-recently-focused live browser; on a
303
449
  // stale socket, fall through to the legacy path + auto-spawn.
304
- const routableBrowsers = controllableBrowsers.filter(browser => !(transport === 'cdp' && browser.transport === 'provider') &&
450
+ const routableBrowsers = controllableBrowsers.filter(browser => (action !== 'browser_list' || browser.transport !== 'provider') &&
451
+ !(transport === 'cdp' && browser.transport === 'provider') &&
305
452
  supportsAction(browser, action, data));
306
453
  if (routableBrowsers.length > 0) {
307
454
  const target = routableBrowsers[0];
308
455
  try {
309
- return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
456
+ return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
310
457
  }
311
458
  catch (error) {
312
459
  if (isSocketSandboxError(error)) {
@@ -318,10 +465,18 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
318
465
  throw error;
319
466
  }
320
467
  }
321
- if (action === 'browser_list' &&
322
- browsers.some(browser => browser.transport === 'provider') &&
323
- areInstalledProvidersRunning()) {
324
- return browserListFromProviderRecords(browsers);
468
+ if (action === 'browser_list') {
469
+ try {
470
+ return await discoverLocalBrowserList(timeoutSec, controlSource);
471
+ }
472
+ catch (error) {
473
+ if (isUnavailableConnection(error) &&
474
+ browsers.some(browser => browser.transport === 'provider') &&
475
+ areInstalledProvidersRunning()) {
476
+ return withDiscoveryWarning(browserListFromProviderRecords(browsers), error);
477
+ }
478
+ throw error;
479
+ }
325
480
  }
326
481
  return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
327
482
  }
@@ -337,21 +492,15 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
337
492
  if (shouldAttemptSpawn && !isRetry) {
338
493
  if (err.code === 'ECONNREFUSED')
339
494
  removeStaleLegacySocket(legacySocketIdentity);
340
- const spawned = await trySpawnNativeHost(action === 'browser_list' ? 'discovery' : 'control');
495
+ const spawned = await trySpawnNativeHost();
341
496
  if (spawned) {
342
- // Wait for the native host to start listening. Discovery retries the legacy socket directly
343
- // so the spawned host receives the probe even if another browser registers in the meantime.
344
497
  await new Promise(r => setTimeout(r, 1200));
345
- if (action === 'browser_list') {
346
- return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
347
- /* isRetry */ true);
348
- }
349
498
  const browsers = listLiveBrowsers();
350
499
  const target = browsers.find(browser => isConnectedBrowser(browser) &&
351
500
  !(transport === 'cdp' && browser.transport === 'provider') &&
352
501
  supportsAction(browser, action, data));
353
502
  if (target) {
354
- return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
503
+ return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
355
504
  }
356
505
  return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
357
506
  /* isRetry */ true);
@@ -362,7 +511,11 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
362
511
  throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
363
512
  }
364
513
  }
365
- function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerBrowserId) {
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
+ }
366
519
  const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
367
520
  return new Promise((resolve, reject) => {
368
521
  if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
@@ -401,10 +554,12 @@ function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSour
401
554
  version: CLIENT_VERSION,
402
555
  controlSource: toControlSourceWireValue(controlSource),
403
556
  };
557
+ if (data[SESSION_OPTION])
558
+ message.controlSessionId = data[SESSION_OPTION];
404
559
  if (transport)
405
560
  message.transport = transport;
406
- if (providerBrowserId)
407
- message.browserId = providerBrowserId;
561
+ if (providerTarget)
562
+ message.providerTarget = providerTarget;
408
563
  socket.write(JSON.stringify(message) + '\n');
409
564
  });
410
565
  socket.on('data', chunk => {
@@ -453,7 +608,8 @@ function formatConnectError(error, socketPath) {
453
608
  if (error.code === 'ENOENT') {
454
609
  return [
455
610
  `未找到 Socket: ${socketPath}`,
456
- 'native host 自动拉起失败,请手动排查:',
611
+ 'native host 自动拉起失败。先运行 mearl check --local;缺少命令时运行 npx @mearl/setup --local --yes。',
612
+ '自动拉起只使用默认 daemon 参数;自定义 CDP/Profile/浏览器参数需手工启动,参见 mearl-native-host --help。',
457
613
  '',
458
614
  '【方式一:浏览器插件模式】',
459
615
  `1. 确认已安装 Mearl 浏览器插件(${EXTENSION_INSTALL_URL})`,
@@ -469,7 +625,7 @@ function formatConnectError(error, socketPath) {
469
625
  if (error.code === 'ECONNREFUSED') {
470
626
  return [
471
627
  `Socket 连接被拒绝: ${socketPath}`,
472
- 'native host 自动恢复失败。',
628
+ '已尝试处理 stale socket,但 native host 自动恢复仍失败。请运行 mearl check --local 定位故障。',
473
629
  '请确认 Chrome 浏览器正在运行,可尝试在 chrome://extensions 重新加载插件。',
474
630
  '',
475
631
  installHint,
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;