@mearl/client 2.14.0 → 2.15.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/dist/socket.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * 通过 Unix Domain Socket (macOS/Linux) 或 Named Pipe (Windows) 与 native host 进程通信。
5
5
  * 每次调用新建连接,收到响应后关闭,简单可靠。
6
6
  *
7
- * 当 Socket 文件不存在(ENOENT)时,自动尝试拉起 native host,稍后重试连接。
7
+ * 当 Socket 不存在,或 legacy Socket 是无人监听的残留文件时,
8
+ * 自动尝试拉起 native host,稍后重试连接。
8
9
  */
9
10
  import net from 'node:net';
10
11
  import fs from 'node:fs';
@@ -138,46 +139,85 @@ function selectBrowserResult(data) {
138
139
  setDefaultBrowser(match.browserId);
139
140
  return { selected: { browserId: match.browserId, name: match.browserName } };
140
141
  }
141
- /** Whether this process has already attempted to auto-spawn a persistent native-host. */
142
- let spawnAttempted = false;
142
+ /** Deduplicates concurrent recovery attempts in long-lived clients such as the MCP server. */
143
+ let persistentSpawnPromise = null;
143
144
  /**
144
145
  * Attempt to spawn `mearl-native-host` in the background (detached).
145
146
  * Browser discovery hosts probe the default local CDP endpoint for browser_list and only
146
147
  * persist when a real browser is available.
147
148
  * Returns true if a spawn was attempted (regardless of success).
148
149
  */
149
- async function trySpawnNativeHost(mode = 'control', retryPreviousAttempt = false) {
150
+ async function trySpawnNativeHost(mode = 'control') {
150
151
  const persistent = mode === 'control';
151
- if (persistent && spawnAttempted && !retryPreviousAttempt)
152
- return false;
153
- if (persistent)
154
- spawnAttempted = true;
155
- 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);
152
+ if (persistent && persistentSpawnPromise)
153
+ return persistentSpawnPromise;
154
+ const spawnPromise = (async () => {
155
+ 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
+ });
176
177
  });
177
- });
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ })();
183
+ if (!persistent)
184
+ return spawnPromise;
185
+ persistentSpawnPromise = spawnPromise;
186
+ try {
187
+ return await spawnPromise;
188
+ }
189
+ finally {
190
+ if (persistentSpawnPromise === spawnPromise)
191
+ persistentSpawnPromise = null;
192
+ }
193
+ }
194
+ function readOwnedLegacySocketIdentity() {
195
+ if (process.platform === 'win32')
196
+ return null;
197
+ try {
198
+ const stat = fs.lstatSync(SOCKET_PATH);
199
+ if (!stat.isSocket())
200
+ return null;
201
+ if (process.getuid && stat.uid !== process.getuid())
202
+ return null;
203
+ return { dev: stat.dev, ino: stat.ino };
178
204
  }
179
205
  catch {
180
- return false;
206
+ return null;
207
+ }
208
+ }
209
+ /** Remove only the same-user socket inode whose failed connection proved it stale. */
210
+ function removeStaleLegacySocket(identity) {
211
+ if (!identity)
212
+ return;
213
+ try {
214
+ const current = fs.lstatSync(SOCKET_PATH);
215
+ if (!current.isSocket() || current.dev !== identity.dev || current.ino !== identity.ino)
216
+ return;
217
+ fs.unlinkSync(SOCKET_PATH);
218
+ }
219
+ catch {
220
+ // Spawning and the final retry provide the authoritative recovery result.
181
221
  }
182
222
  }
183
223
  /**
@@ -185,7 +225,7 @@ async function trySpawnNativeHost(mode = 'control', retryPreviousAttempt = false
185
225
  *
186
226
  * 目标解析优先级:显式 options.browser → setDefaultBrowser → 环境变量
187
227
  * MEARL_BROWSER → 自动(注册表中最近聚焦的浏览器)。注册表为空时回退到
188
- * legacy well-known socket,并在 socket 不存在(ENOENT)时自动拉起 native host。
228
+ * legacy well-known socket,并在 socket 不存在或已残留时自动拉起 native host。
189
229
  */
190
230
  export function invoke(action, data = {}, options = {}) {
191
231
  if (action === 'browser_select_browser') {
@@ -284,16 +324,19 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
284
324
  }
285
325
  return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
286
326
  }
287
- /** Legacy well-known socket, auto-spawning a native-host on ENOENT and retrying once. */
327
+ /** Legacy well-known socket, repairing a missing or stale endpoint and retrying once. */
288
328
  async function invokeViaLegacy(action, data, timeoutSec, transport, controlSource, isRetry) {
329
+ const legacySocketIdentity = readOwnedLegacySocketIdentity();
289
330
  try {
290
331
  return await invokeOnce(action, data, timeoutSec, transport, SOCKET_PATH, controlSource);
291
332
  }
292
333
  catch (error) {
293
334
  const err = error;
294
- const shouldAttemptSpawn = err.code === 'ENOENT' || (action === 'browser_release' && err.code === 'ECONNREFUSED');
335
+ const shouldAttemptSpawn = err.code === 'ENOENT' || err.code === 'ECONNREFUSED';
295
336
  if (shouldAttemptSpawn && !isRetry) {
296
- const spawned = await trySpawnNativeHost(action === 'browser_list' ? 'discovery' : 'control', action === 'browser_release');
337
+ if (err.code === 'ECONNREFUSED')
338
+ removeStaleLegacySocket(legacySocketIdentity);
339
+ const spawned = await trySpawnNativeHost(action === 'browser_list' ? 'discovery' : 'control');
297
340
  if (spawned) {
298
341
  // Wait for the native host to start listening. Discovery retries the legacy socket directly
299
342
  // so the spawned host receives the probe even if another browser registers in the meantime.
@@ -425,7 +468,7 @@ function formatConnectError(error, socketPath) {
425
468
  if (error.code === 'ECONNREFUSED') {
426
469
  return [
427
470
  `Socket 连接被拒绝: ${socketPath}`,
428
- 'native host 可能已退出。',
471
+ 'native host 自动恢复失败。',
429
472
  '请确认 Chrome 浏览器正在运行,可尝试在 chrome://extensions 重新加载插件。',
430
473
  '',
431
474
  installHint,
package/dist/unified.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type CloudConnectorListResult } from '@mearl/cloud-types';
2
+ import type { PluginInvocation, PluginInvokeOptions } from './plugin.js';
2
3
  export interface UnifiedInvokeOptions {
3
4
  timeoutSec?: number;
4
5
  /** Global browser id, source-local browser id, or unique browser name. */
@@ -36,6 +37,7 @@ export declare class UnifiedClient {
36
37
  private closing;
37
38
  constructor(options?: UnifiedClientOptions);
38
39
  disconnect(): void;
40
+ invokePlugin(name: string, invocation: PluginInvocation, options?: PluginInvokeOptions): Promise<unknown>;
39
41
  invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
40
42
  private invokeExplicitLocal;
41
43
  private invokeLaunchWithoutControlBrowser;
@@ -61,5 +63,6 @@ export declare class UnifiedClient {
61
63
  private rejectAllPending;
62
64
  }
63
65
  export declare function invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
66
+ export declare function invokePlugin(name: string, invocation: PluginInvocation, options?: PluginInvokeOptions): Promise<unknown>;
64
67
  export declare function listCloudConnectors(): Promise<CloudConnectorListResult>;
65
68
  export {};
package/dist/unified.js CHANGED
@@ -13,8 +13,10 @@ function encodeBrowserTarget(target) {
13
13
  }
14
14
  function omitTransientBrowserSecrets(browser) {
15
15
  const safeBrowser = { ...browser };
16
- delete safeBrowser.agentbayAccessUrl;
17
- delete safeBrowser.accessUrl;
16
+ for (const key of Object.keys(safeBrowser)) {
17
+ if (/accessUrl$/i.test(key))
18
+ delete safeBrowser[key];
19
+ }
18
20
  return safeBrowser;
19
21
  }
20
22
  export function decodeBrowserTarget(selector) {
@@ -117,6 +119,12 @@ export class UnifiedClient {
117
119
  this.ws?.close();
118
120
  this.ws = null;
119
121
  }
122
+ async invokePlugin(name, invocation, options = {}) {
123
+ const { invokeRegisteredPlugin } = await import('./plugin.js');
124
+ return invokeRegisteredPlugin(name, invocation, {
125
+ invoke: (action, payload, invokeOptions) => this.invoke(action, payload, invokeOptions),
126
+ }, options);
127
+ }
120
128
  async invoke(action, data = {}, options = {}) {
121
129
  const timeoutSec = options.timeoutSec ?? resolveActionTimeoutSec(action, data, this.options.requestTimeoutSec);
122
130
  if (action === 'connector_list')
@@ -569,6 +577,20 @@ export async function invoke(action, data = {}, options = {}) {
569
577
  client.disconnect();
570
578
  }
571
579
  }
580
+ export async function invokePlugin(name, invocation, options = {}) {
581
+ const serverUrl = process.env.MEARL_SERVER_URL ?? loadCloudServerConfig()?.server;
582
+ const client = new UnifiedClient({
583
+ ...(serverUrl ? { serverUrl } : {}),
584
+ defaultBrowser,
585
+ defaultConnector,
586
+ });
587
+ try {
588
+ return await client.invokePlugin(name, invocation, options);
589
+ }
590
+ finally {
591
+ client.disconnect();
592
+ }
593
+ }
572
594
  export function listCloudConnectors() {
573
595
  return invoke('connector_list', {});
574
596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/client",
3
- "version": "2.14.0",
3
+ "version": "2.15.1",
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.14.0",
63
- "@mearl/provider": "2.14.0"
62
+ "@mearl/cloud-types": "2.15.1",
63
+ "@mearl/provider": "2.15.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^24.9.1",