@mearl/client 3.0.0-beta.0 → 3.0.0-beta.10

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.
Files changed (74) hide show
  1. package/README.md +333 -255
  2. package/README.zh-CN.md +612 -0
  3. package/dist/actionTimeouts.js +0 -3
  4. package/dist/autoUpdate.js +3 -1
  5. package/dist/bin.js +12 -5
  6. package/dist/capabilityHelp.d.ts +4 -0
  7. package/dist/capabilityHelp.js +68 -0
  8. package/dist/check.d.ts +5 -6
  9. package/dist/check.js +2 -5
  10. package/dist/cli.js +29 -56
  11. package/dist/commandHelp.d.ts +3 -0
  12. package/dist/commandHelp.js +79 -0
  13. package/dist/commandSuggestions.d.ts +3 -0
  14. package/dist/commandSuggestions.js +16 -5
  15. package/dist/commands.d.ts +70 -65
  16. package/dist/commands.js +60 -55
  17. package/dist/guide.d.ts +46 -0
  18. package/dist/guide.js +80 -0
  19. package/dist/guides/api-browser-setup.md +93 -0
  20. package/dist/guides/api-page-automation.md +140 -0
  21. package/dist/guides/api-page-debugging.md +39 -0
  22. package/dist/guides/api-page-environment.md +37 -0
  23. package/dist/guides/api-request-debugging.md +54 -0
  24. package/dist/guides/browser-tool-cooperation.md +23 -0
  25. package/dist/guides/core.md +60 -0
  26. package/dist/guides/related-skills.md +51 -0
  27. package/dist/guides/scripting.md +21 -0
  28. package/dist/guides/troubleshooting.md +170 -0
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +8 -5
  31. package/dist/nativeHost.d.ts +2 -2
  32. package/dist/nativeHost.js +2 -4
  33. package/dist/plugins/clients/pluginHostConnection.d.ts +2 -0
  34. package/dist/plugins/clients/pluginHostConnection.js +3 -1
  35. package/dist/plugins/officialPluginInstaller.js +2 -1
  36. package/dist/plugins/pluginRegistry.d.ts +1 -1
  37. package/dist/plugins/pluginRegistry.js +21 -7
  38. package/dist/plugins/runtime/pluginCommandWorker.js +8 -6
  39. package/dist/plugins/runtime/pluginDiagnostics.js +1 -1
  40. package/dist/plugins/runtime/pluginExecutor.d.ts +5 -0
  41. package/dist/plugins/runtime/pluginExecutor.js +15 -2
  42. package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +4 -0
  43. package/dist/plugins/runtime/pluginExecutorSupervisor.js +2 -1
  44. package/dist/plugins/runtime/pluginHostServer.js +5 -1
  45. package/dist/plugins/runtime/pluginPageManager.js +2 -6
  46. package/dist/plugins/runtime/pluginRevision.js +0 -51
  47. package/dist/plugins/runtime/pluginStateManager.js +1 -1
  48. package/dist/plugins/runtime/pluginStateStore.js +3 -8
  49. package/dist/plugins/runtime/pluginTaskManager.js +5 -9
  50. package/dist/plugins/sdk/plugin.d.ts +9 -0
  51. package/dist/plugins/sdk/plugin.js +22 -9
  52. package/dist/plugins/sdk/pluginModelDefinition.js +0 -6
  53. package/dist/plugins/sdk/pluginPageDefinition.d.ts +0 -1
  54. package/dist/plugins/sdk/pluginPageDefinition.js +2 -18
  55. package/dist/plugins/sdk/pluginRecoveryDefinition.js +0 -3
  56. package/dist/plugins/sdk/pluginStateDefinition.d.ts +0 -1
  57. package/dist/plugins/sdk/pluginStateDefinition.js +0 -11
  58. package/dist/runtimePaths.d.ts +1 -0
  59. package/dist/runtimePaths.js +1 -0
  60. package/dist/script.d.ts +30 -0
  61. package/dist/script.js +261 -0
  62. package/dist/scriptCli.d.ts +1 -0
  63. package/dist/scriptCli.js +94 -0
  64. package/dist/scriptErrors.d.ts +8 -0
  65. package/dist/scriptErrors.js +21 -0
  66. package/dist/scriptWorker.d.ts +1 -0
  67. package/dist/scriptWorker.js +50 -0
  68. package/dist/sdkMethods.d.ts +3 -0
  69. package/dist/sdkMethods.js +9 -0
  70. package/dist/socket.js +33 -8
  71. package/dist/unified.js +11 -1
  72. package/dist/unifiedConfig.d.ts +4 -0
  73. package/dist/unifiedConfig.js +11 -0
  74. package/package.json +18 -11
@@ -146,12 +146,14 @@ export async function loadPlugin(entry, context, pluginName) {
146
146
  let registering = true;
147
147
  let name;
148
148
  let description;
149
+ let defaultCommand;
149
150
  let toolbox;
150
151
  let pageRuntime;
151
152
  let stateStorage;
152
153
  let taskRecovery;
153
154
  let appProfiles;
154
155
  let profileSources;
156
+ let setup;
155
157
  const registerCommand = (signature, handler, description = '', examples = [], toolboxOnly = false, runtimeOnly = false) => {
156
158
  if (!registering)
157
159
  throw new Error('命令只能在入口注册阶段声明');
@@ -312,6 +314,12 @@ export async function loadPlugin(entry, context, pluginName) {
312
314
  if (module.description !== undefined && typeof module.description !== 'string')
313
315
  throw new Error('入口的 description 导出必须为字符串');
314
316
  description = module.description;
317
+ if (module.setup !== undefined && typeof module.setup !== 'function')
318
+ throw new Error('入口的 setup 导出必须为函数');
319
+ setup = module.setup;
320
+ if (module.defaultCommand !== undefined && typeof module.defaultCommand !== 'string')
321
+ throw new Error('入口的 defaultCommand 导出必须为字符串');
322
+ defaultCommand = module.defaultCommand;
315
323
  toolbox = normalizePluginToolboxContribution(module.toolbox);
316
324
  pageRuntime = normalizePluginPageRuntime(module.pageRuntime);
317
325
  stateStorage = normalizePluginState(module.stateStorage);
@@ -326,6 +334,8 @@ export async function loadPlugin(entry, context, pluginName) {
326
334
  }
327
335
  if (commands.size === 0 && !appProfiles)
328
336
  throw new Error('入口未注册任何命令');
337
+ if (defaultCommand !== undefined && (!commands.has(defaultCommand) || commands.get(defaultCommand)?.toolboxOnly || commands.get(defaultCommand)?.runtimeOnly))
338
+ throw new Error('defaultCommand 必须指向公开命令');
329
339
  for (const command of Object.keys(taskRecovery?.commands ?? {})) {
330
340
  if (!commands.has(command) ||
331
341
  commands.get(command)?.runtimeOnly ||
@@ -401,12 +411,13 @@ export async function loadPlugin(entry, context, pluginName) {
401
411
  if (payload !== undefined && Object.keys(namedInput).length > 0) {
402
412
  throw new PluginError('PLUGIN_INVALID_INPUT', '插件业务选项与 --payload 调用不能混用');
403
413
  }
404
- if (payload !== undefined &&
405
- (!isObject(payload) || typeof payload.command !== 'string' || !isObject(payload.input))) {
414
+ const defaultPayload = defaultCommand && toolboxPage === false && isObject(payload) && !Object.hasOwn(payload, 'command');
415
+ if (payload !== undefined && !defaultPayload && !isPluginInvocation(payload)) {
406
416
  throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
407
417
  }
408
- const envelope = payload;
409
- const commandName = envelope?.command ?? positionals[0] ?? '';
418
+ const envelope = (defaultPayload ? { command: defaultCommand, input: payload } : payload);
419
+ const positionalOffset = defaultCommand && !commands.has(positionals[0]) && toolboxPage === false ? 0 : 1;
420
+ const commandName = envelope?.command ?? (positionalOffset === 0 ? defaultCommand : positionals[0] ?? '');
410
421
  const hidden = toolboxPage === 'recovery' || toolboxPage === 'migration';
411
422
  const selected = find(commandName, hidden ? false : toolboxPage);
412
423
  const command = selected.command;
@@ -426,7 +437,7 @@ export async function loadPlugin(entry, context, pluginName) {
426
437
  : Object.fromEntries(command.registeredArguments
427
438
  .map((argument, index) => [
428
439
  argument.name(),
429
- argument.variadic ? positionals.slice(index + 1) : positionals[index + 1],
440
+ argument.variadic ? positionals.slice(index + positionalOffset) : positionals[index + positionalOffset],
430
441
  ])
431
442
  .filter(([, value]) => value !== undefined));
432
443
  for (const name of Object.keys(namedInput)) {
@@ -437,7 +448,7 @@ export async function loadPlugin(entry, context, pluginName) {
437
448
  const input = { ...positionalInput, ...namedInput };
438
449
  if (!envelope &&
439
450
  !command.registeredArguments.some(argument => argument.variadic) &&
440
- positionals.length - 1 > command.registeredArguments.length) {
451
+ positionals.length - positionalOffset > command.registeredArguments.length) {
441
452
  throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
442
453
  }
443
454
  for (const argument of hidden ? [] : command.registeredArguments) {
@@ -528,7 +539,9 @@ export async function loadPlugin(entry, context, pluginName) {
528
539
  };
529
540
  return {
530
541
  name,
542
+ setup,
531
543
  description,
544
+ defaultCommand,
532
545
  toolbox,
533
546
  pageRuntime,
534
547
  stateStorage,
@@ -538,11 +551,11 @@ export async function loadPlugin(entry, context, pluginName) {
538
551
  version: readPluginVersion(entry),
539
552
  commandHelp(namespace) {
540
553
  return publicCommands()
541
- .map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`)
554
+ .map(({ command, signature }) => ` ${namespace} ${command.name() === defaultCommand ? signature.slice(defaultCommand.length).trim() : signature} ${command.description()}`)
542
555
  .join('\n');
543
556
  },
544
557
  help(namespace, name) {
545
- const selected = name ? [find(name)] : publicCommands();
558
+ const selected = name ? [find(defaultCommand && !commands.has(name) ? defaultCommand : name)] : publicCommands();
546
559
  if (!selected.length) {
547
560
  const location = toolbox
548
561
  ? `请在 Mearl ${toolbox.displayMode === 'popup' ? 'Popup' : '工具箱'} 中打开「${toolbox.title}」使用。`
@@ -552,7 +565,7 @@ export async function loadPlugin(entry, context, pluginName) {
552
565
  const examples = selected.flatMap(({ examples }) => examples);
553
566
  return [
554
567
  '用法:',
555
- ...selected.map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`),
568
+ ...selected.map(({ command, signature }) => ` ${namespace} ${command.name() === defaultCommand ? signature.slice(defaultCommand.length).trim() : signature} ${command.description()}`),
556
569
  ...(examples.length
557
570
  ? ['', '示例:', ...examples.map(example => ` ${namespace} ${example}`)]
558
571
  : []),
@@ -1,5 +1,4 @@
1
1
  import { CODEX_MODEL_CAPABILITY } from '@mearl/agent-runtime/capabilities';
2
- import { compilePluginStateSchema } from './pluginStateDefinition.js';
3
2
  export function pluginModelCapabilities() {
4
3
  return { providers: [CODEX_MODEL_CAPABILITY] };
5
4
  }
@@ -44,7 +43,6 @@ export function normalizePluginModel(value) {
44
43
  request.outputSchema.$async ||
45
44
  Buffer.byteLength(JSON.stringify(request.outputSchema)) > 64 * 1024)
46
45
  throw new Error('Invalid model output schema');
47
- compilePluginStateSchema(request.outputSchema);
48
46
  }
49
47
  return JSON.parse(JSON.stringify(request));
50
48
  }
@@ -65,10 +63,6 @@ export function validatePluginModelResult(request, value) {
65
63
  code: 'PLUGIN_MODEL_SCHEMA_INVALID',
66
64
  });
67
65
  }
68
- if (!compilePluginStateSchema(request.outputSchema)(output))
69
- throw Object.assign(new Error('Model result does not match the output schema'), {
70
- code: 'PLUGIN_MODEL_SCHEMA_INVALID',
71
- });
72
66
  return { ...result, output };
73
67
  }
74
68
  return result;
@@ -1,3 +1,2 @@
1
1
  import type { PluginPageRuntime } from '@mearl/browser-core';
2
2
  export declare function normalizePluginPageRuntime(value: unknown): PluginPageRuntime | undefined;
3
- export declare function compilePluginPageSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
@@ -1,7 +1,5 @@
1
- import { parse } from 'acorn';
2
- import { Ajv } from 'ajv';
1
+ import { Script } from 'node:vm';
3
2
  import { isPluginPageMatchPattern } from '@mearl/browser-core';
4
- const validators = new WeakMap();
5
3
  export function normalizePluginPageRuntime(value) {
6
4
  if (value === undefined)
7
5
  return undefined;
@@ -16,12 +14,7 @@ export function normalizePluginPageRuntime(value) {
16
14
  Buffer.byteLength(runtime.code) > 384 * 1024 ||
17
15
  Buffer.byteLength(JSON.stringify(runtime)) > 448 * 1024)
18
16
  throw new Error('Invalid plugin pageRuntime declaration');
19
- const program = parse(`(${runtime.code}\n)`, { ecmaVersion: 'latest' });
20
- const statement = program.body[0];
21
- if (program.body.length !== 1 ||
22
- statement.type !== 'ExpressionStatement' ||
23
- !['ArrowFunctionExpression', 'FunctionExpression'].includes(statement.expression?.type ?? ''))
24
- throw new Error('pageRuntime.code must be a self-contained function expression');
17
+ new Script(`(${runtime.code}\n)`, { filename: 'plugin-page-runtime.js' });
25
18
  if (runtime.events !== undefined &&
26
19
  (!runtime.events ||
27
20
  typeof runtime.events !== 'object' ||
@@ -46,17 +39,8 @@ export function normalizePluginPageRuntime(value) {
46
39
  event.maxPending > 64)) ||
47
40
  event.schema.$async)
48
41
  throw new Error(`Invalid page event binding: ${name}`);
49
- compilePluginPageSchema(event.schema);
50
42
  }
51
43
  if (runtime.initialize !== undefined && !/^[a-z][a-z0-9-]*$/.test(runtime.initialize))
52
44
  throw new Error('Invalid page runtime initialize command');
53
45
  return JSON.parse(JSON.stringify(runtime));
54
46
  }
55
- export function compilePluginPageSchema(schema) {
56
- let validate = validators.get(schema);
57
- if (!validate) {
58
- validate = new Ajv({ strict: true, allErrors: false }).compile(schema);
59
- validators.set(schema, validate);
60
- }
61
- return validate;
62
- }
@@ -1,5 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { compilePluginStateSchema } from './pluginStateDefinition.js';
3
2
  export function normalizePluginTaskRecovery(value) {
4
3
  if (value === undefined)
5
4
  return;
@@ -22,7 +21,6 @@ export function normalizePluginTaskRecovery(value) {
22
21
  Array.isArray(definition.schema) ||
23
22
  definition.schema.$async)
24
23
  throw new Error('Invalid task checkpoint definition');
25
- compilePluginStateSchema(definition.schema);
26
24
  if (definition.migrateFrom !== undefined) {
27
25
  if (!Array.isArray(definition.migrateFrom) ||
28
26
  definition.migrateFrom.length < 1 ||
@@ -40,7 +38,6 @@ export function normalizePluginTaskRecovery(value) {
40
38
  Array.isArray(source.schema) ||
41
39
  source.schema.$async)
42
40
  throw new Error('Checkpoint migrations require distinct older versions and schemas');
43
- compilePluginStateSchema(source.schema);
44
41
  versions.add(source.version);
45
42
  }
46
43
  }
@@ -1,4 +1,3 @@
1
1
  import type { PluginStateDeclaration } from '@mearl/browser-core';
2
- export declare function compilePluginStateSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
3
2
  export declare function normalizePluginState(value: unknown): PluginStateDeclaration | undefined;
4
3
  export declare function pluginStateSignature(collection: import('@mearl/browser-core').PluginStateCollection): string;
@@ -1,14 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { Ajv } from 'ajv';
3
- const validators = new WeakMap();
4
- export function compilePluginStateSchema(schema) {
5
- let validate = validators.get(schema);
6
- if (!validate) {
7
- validate = new Ajv({ strict: true }).compile(schema);
8
- validators.set(schema, validate);
9
- }
10
- return validate;
11
- }
12
2
  export function normalizePluginState(value) {
13
3
  if (value === undefined)
14
4
  return;
@@ -55,7 +45,6 @@ export function normalizePluginState(value) {
55
45
  throw new Error('State TTL must be between 1 ms and 7 days');
56
46
  if (collection.storage === 'persistent' && collection.ttlMs !== undefined)
57
47
  throw new Error('Persistent state does not accept TTL');
58
- compilePluginStateSchema(collection.schema);
59
48
  }
60
49
  return declaration;
61
50
  }
@@ -1,4 +1,5 @@
1
1
  export declare const CLIENT_VERSION: string;
2
2
  export declare const PLUGIN_TASK_HOST_PATH: string;
3
3
  export declare const PLUGIN_WORKER_PATH: string;
4
+ export declare const SCRIPT_WORKER_PATH: string;
4
5
  export declare const PLUGIN_MODEL_SESSION_WORKER_PATH: string;
@@ -5,4 +5,5 @@ const require = createRequire(import.meta.url);
5
5
  export const CLIENT_VERSION = require('../package.json').version;
6
6
  export const PLUGIN_TASK_HOST_PATH = fileURLToPath(new URL('./plugin-task-host.js', import.meta.url));
7
7
  export const PLUGIN_WORKER_PATH = fileURLToPath(new URL('./plugin-worker.js', import.meta.url));
8
+ export const SCRIPT_WORKER_PATH = fileURLToPath(new URL('./scriptWorker.js', import.meta.url));
8
9
  export const PLUGIN_MODEL_SESSION_WORKER_PATH = fileURLToPath(new URL('./pluginModelSessionWorker.js', import.meta.url));
@@ -0,0 +1,30 @@
1
+ import { type UnifiedInvokeOptions } from './unified.js';
2
+ export interface ScriptExecutionOptions extends UnifiedInvokeOptions {
3
+ serverUrl?: string;
4
+ /** Overall deadline, independent of individual action timeouts. Default: 60 seconds. */
5
+ executionTimeoutSec?: number;
6
+ /** Combined stdout/stderr limit in bytes. Default: 256 KiB; maximum: 1 MiB. */
7
+ maxOutputBytes?: number;
8
+ signal?: AbortSignal;
9
+ source?: 'cli' | 'mcp' | 'sdk';
10
+ }
11
+ export interface ScriptExecutionResult {
12
+ success: boolean;
13
+ stdout: string;
14
+ stderr: string;
15
+ session?: Record<string, unknown>;
16
+ error?: {
17
+ code: string;
18
+ message: string;
19
+ stack?: string;
20
+ details?: unknown;
21
+ stage?: string;
22
+ attempt?: unknown;
23
+ };
24
+ executionStopped: boolean;
25
+ /** Dispatched browser/plugin calls can finish after the script has stopped. */
26
+ mayHaveLateEffects: boolean;
27
+ pendingActions: string[];
28
+ }
29
+ /** Runs trusted agent code in a short-lived Node process, with one shared SDK client. */
30
+ export declare function executeScript(code: string, options?: ScriptExecutionOptions): Promise<ScriptExecutionResult>;
package/dist/script.js ADDED
@@ -0,0 +1,261 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { isBrowserCommandAction } from '@mearl/browser-core/browserActionProtocol';
3
+ import { exportArtifact } from './artifacts.js';
4
+ import { UnifiedClient } from './unified.js';
5
+ import { loadCloudServerConfig } from './unifiedConfig.js';
6
+ import { stopPluginProcessTree } from './plugins/runtime/pluginProcessTree.js';
7
+ import { SCRIPT_WORKER_PATH } from './runtimePaths.js';
8
+ import { SDK_METHOD_ACTIONS as METHOD_ACTIONS, SDK_OPTIONS_ONLY_METHODS as OPTIONS_ONLY, } from './sdkMethods.js';
9
+ import { getCommandHelp } from './commandHelp.js';
10
+ import { executeRegisteredPlugin } from './plugins/runtime/pluginExecutor.js';
11
+ import { findPluginRegistration } from './plugins/pluginRegistry.js';
12
+ import { findOfficialPlugin } from './plugins/officialPluginCatalog.js';
13
+ import { ensureOfficialPluginRegistered } from './plugins/officialPluginInstaller.js';
14
+ import { serializeScriptError as serializeError } from './scriptErrors.js';
15
+ const METHODS = [...METHOD_ACTIONS.keys(), 'invoke', 'invokePlugin', 'saveArtifact', 'help'];
16
+ const MAX_SCRIPT_BYTES = 1024 * 1024;
17
+ /** Runs trusted agent code in a short-lived Node process, with one shared SDK client. */
18
+ export async function executeScript(code, options = {}) {
19
+ if (typeof code !== 'string' || !code.trim() || Buffer.byteLength(code) > MAX_SCRIPT_BYTES)
20
+ throw new Error('Script must be non-empty JavaScript of at most 1 MiB');
21
+ const { serverUrl, executionTimeoutSec = 60, maxOutputBytes = 256 * 1024, signal, source = 'sdk', ...defaults } = options;
22
+ if (!Number.isFinite(executionTimeoutSec) ||
23
+ executionTimeoutSec <= 0 ||
24
+ executionTimeoutSec * 1000 > 2_147_483_647)
25
+ throw new Error('executionTimeoutSec must be positive and within the timer range');
26
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 1024 * 1024)
27
+ throw new Error('maxOutputBytes must be an integer between 1 and 1048576');
28
+ const client = new UnifiedClient({
29
+ serverUrl: serverUrl ?? process.env.MEARL_SERVER_URL ?? loadCloudServerConfig()?.server,
30
+ });
31
+ let session = defaults.session && defaults.session !== 'new' ? { sessionId: defaults.session } : undefined;
32
+ let sessionId = defaults.session;
33
+ let uncertainEffects = false;
34
+ const pending = new Map();
35
+ const controller = new AbortController();
36
+ const invoke = async (action, payload, overrides = {}) => {
37
+ controller.signal.throwIfAborted();
38
+ const result = await client.invoke(action, payload, {
39
+ ...defaults,
40
+ session: sessionId,
41
+ ...overrides,
42
+ });
43
+ if (result?.action?.stage === 'dispatch-uncertain' || result?.stage === 'dispatch-uncertain')
44
+ uncertainEffects = true;
45
+ if (result?.session?.sessionId &&
46
+ (!overrides.session || overrides.session === 'new' || overrides.session === sessionId)) {
47
+ session = result.session;
48
+ if (!overrides.session || overrides.session === 'new')
49
+ sessionId = result.session.sessionId;
50
+ }
51
+ return result;
52
+ };
53
+ try {
54
+ return await new Promise((resolve, reject) => {
55
+ if (signal?.aborted) {
56
+ resolve({
57
+ success: false,
58
+ stdout: '',
59
+ stderr: '',
60
+ ...(session ? { session } : {}),
61
+ error: { code: 'SCRIPT_CANCELLED', message: 'Script execution cancelled' },
62
+ executionStopped: true,
63
+ mayHaveLateEffects: false,
64
+ pendingActions: [],
65
+ });
66
+ return;
67
+ }
68
+ const child = spawn(process.execPath, [SCRIPT_WORKER_PATH], {
69
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
70
+ serialization: 'advanced',
71
+ detached: process.platform !== 'win32',
72
+ });
73
+ const output = { stdout: [], stderr: [] };
74
+ let outputBytes = 0;
75
+ let outcome;
76
+ let cleanup;
77
+ let cleanupError;
78
+ let pendingActions = [];
79
+ let closed = false;
80
+ const pluginCalls = new Set();
81
+ const stop = (error) => {
82
+ if (outcome !== undefined || closed)
83
+ return;
84
+ outcome = error ?? true;
85
+ pendingActions = [...pending.values()];
86
+ controller.abort();
87
+ client.disconnect();
88
+ if (child.pid) {
89
+ cleanup = stopPluginProcessTree(child.pid).catch(error => {
90
+ cleanupError = error;
91
+ child.kill('SIGKILL');
92
+ });
93
+ }
94
+ };
95
+ const abort = () => stop({ code: 'SCRIPT_CANCELLED', message: 'Script execution cancelled' });
96
+ const deadline = setTimeout(() => stop({ code: 'SCRIPT_TIMEOUT', message: 'Script execution deadline exceeded' }), executionTimeoutSec * 1000);
97
+ signal?.addEventListener('abort', abort, { once: true });
98
+ if (signal?.aborted)
99
+ abort();
100
+ const appendOutput = (stream, chunk) => {
101
+ const remaining = maxOutputBytes - outputBytes;
102
+ output[stream].push(chunk.subarray(0, Math.max(0, remaining)));
103
+ outputBytes += Math.min(chunk.length, Math.max(0, remaining));
104
+ if (chunk.length > remaining)
105
+ stop({
106
+ code: 'SCRIPT_OUTPUT_LIMIT',
107
+ message: `Script output exceeded ${maxOutputBytes} bytes`,
108
+ });
109
+ };
110
+ for (const stream of ['stdout', 'stderr']) {
111
+ child[stream].on('data', (chunk) => appendOutput(stream, chunk));
112
+ }
113
+ const runPlugin = async (name, invocation, overrides = {}, help) => {
114
+ if (!findPluginRegistration(name) && findOfficialPlugin(name))
115
+ await ensureOfficialPluginRegistered(name);
116
+ controller.signal.throwIfAborted();
117
+ const pluginOptions = { ...defaults, session: sessionId, ...overrides };
118
+ if (pluginOptions.session === 'new' && !help)
119
+ throw new Error('Create the control session with a browser action before calling a Skill command');
120
+ const operation = executeRegisteredPlugin(name, invocation, {
121
+ invoke: (action, payload = {}, options) => invoke(action, payload, options),
122
+ }, {
123
+ ...pluginOptions,
124
+ source,
125
+ signal: controller.signal,
126
+ cancellationGraceMs: 50,
127
+ onOutput: appendOutput,
128
+ ...(help ? { help } : {}),
129
+ });
130
+ pluginCalls.add(operation);
131
+ try {
132
+ return await operation;
133
+ }
134
+ finally {
135
+ pluginCalls.delete(operation);
136
+ }
137
+ };
138
+ const send = (message) => {
139
+ if (outcome !== undefined || !child.connected)
140
+ return;
141
+ child.send(message, error => {
142
+ if (error)
143
+ stop(serializeError(error));
144
+ });
145
+ };
146
+ child.on('message', (message) => {
147
+ if (outcome !== undefined || !message || typeof message !== 'object')
148
+ return;
149
+ if (message.type === 'done' || message.type === 'error') {
150
+ stop(message.type === 'error'
151
+ ? serializeError(message.error)
152
+ : pending.size
153
+ ? {
154
+ code: 'SCRIPT_PENDING_CALLS',
155
+ message: 'Script finished with unawaited SDK calls',
156
+ }
157
+ : undefined);
158
+ return;
159
+ }
160
+ if (message.type !== 'call')
161
+ return;
162
+ const { id, method, args } = message;
163
+ if (!Number.isSafeInteger(id) ||
164
+ pending.has(id) ||
165
+ !METHODS.includes(method) ||
166
+ !Array.isArray(args)) {
167
+ stop({ code: 'SCRIPT_PROTOCOL_ERROR', message: 'Invalid script SDK call' });
168
+ return;
169
+ }
170
+ if (method !== 'help' && sessionId === 'new' && pending.size) {
171
+ send({
172
+ type: 'response',
173
+ id,
174
+ error: {
175
+ code: 'SCRIPT_SESSION_INITIALIZING',
176
+ message: 'Await the first session call before starting concurrent calls',
177
+ },
178
+ });
179
+ return;
180
+ }
181
+ pending.set(id, method === 'invoke' || method === 'invokePlugin' ? String(args[0]) : method);
182
+ void Promise.resolve()
183
+ .then(async () => {
184
+ if (outcome !== undefined)
185
+ throw new Error('Script execution stopped');
186
+ if (method === 'help') {
187
+ const target = args[0];
188
+ if (target && typeof target === 'object') {
189
+ if (typeof target.plugin !== 'string' ||
190
+ !target.plugin.trim() ||
191
+ (target.command !== undefined && typeof target.command !== 'string'))
192
+ throw new Error('Plugin help requires { plugin, command? }');
193
+ return runPlugin(target.plugin, { command: '', input: {} }, {}, { namespace: `mearl ${target.plugin}`, command: target.command });
194
+ }
195
+ return getCommandHelp(target);
196
+ }
197
+ if (method === 'invoke') {
198
+ if (!isBrowserCommandAction(args[0]) && args[0] !== 'browser_select_browser')
199
+ throw new Error(`Unsupported browser action: ${args[0]}; use invokePlugin for registered Skill commands`);
200
+ return invoke(args[0], args[1] ?? {}, args[2]);
201
+ }
202
+ if (method === 'invokePlugin') {
203
+ return runPlugin(args[0], args[1], args[2]);
204
+ }
205
+ if (method === 'saveArtifact')
206
+ return exportArtifact({ invoke }, args[0], args[1], {
207
+ ...defaults,
208
+ session: sessionId,
209
+ ...args[2],
210
+ });
211
+ const optionsOnly = (OPTIONS_ONLY.has(method) && !(method === 'browserList' && args[0] &&
212
+ ('provider' in args[0] || 'launchForm' in args[0]))) || (method === 'browserRelease' && args.length < 2);
213
+ return invoke(METHOD_ACTIONS.get(method), optionsOnly ? {} : (args[0] ?? {}), optionsOnly ? args[0] : args[1]);
214
+ })
215
+ .then(result => send({ type: 'response', id, result, session }), error => send({ type: 'response', id, error: serializeError(error), session }))
216
+ .finally(() => pending.delete(id));
217
+ });
218
+ child.once('error', error => {
219
+ stop(serializeError(error));
220
+ });
221
+ child.once('exit', () => {
222
+ if (outcome === undefined)
223
+ stop({
224
+ code: 'SCRIPT_EXITED',
225
+ message: 'Script process exited before reporting completion',
226
+ });
227
+ });
228
+ child.once('close', () => {
229
+ closed = true;
230
+ clearTimeout(deadline);
231
+ signal?.removeEventListener('abort', abort);
232
+ void Promise.allSettled([cleanup, ...pluginCalls])
233
+ .then(outcomes => {
234
+ if (outcomes.some(result => result.status === 'rejected' &&
235
+ result.reason?.code === 'PLUGIN_EXECUTOR_CLEANUP_FAILED'))
236
+ cleanupError = new Error('Unable to confirm Skill process cleanup');
237
+ const error = cleanupError
238
+ ? serializeError(cleanupError, 'SCRIPT_CLEANUP_FAILED')
239
+ : outcome === true
240
+ ? undefined
241
+ : outcome;
242
+ resolve({
243
+ success: !error && outcome === true,
244
+ stdout: Buffer.concat(output.stdout).toString('utf8'),
245
+ stderr: Buffer.concat(output.stderr).toString('utf8'),
246
+ ...(session ? { session } : {}),
247
+ ...(error ? { error } : {}),
248
+ executionStopped: !cleanupError,
249
+ mayHaveLateEffects: pendingActions.length > 0 || uncertainEffects,
250
+ pendingActions,
251
+ });
252
+ })
253
+ .catch(reject);
254
+ });
255
+ send({ type: 'start', code, methods: METHODS, session });
256
+ });
257
+ }
258
+ finally {
259
+ client.disconnect();
260
+ }
261
+ }
@@ -0,0 +1 @@
1
+ export declare function runScriptCli(args: string[], commandName: string): Promise<number>;
@@ -0,0 +1,94 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { Command } from 'commander';
4
+ import { executeScript } from './script.js';
5
+ export async function runScriptCli(args, commandName) {
6
+ const program = new Command()
7
+ .name(`${commandName} run`)
8
+ .description('执行 Node.js 脚本;预注入 mearl SDK,支持顶层 await、条件和循环')
9
+ .option('--file <path>', '读取 JavaScript 文件;省略时从 stdin 读取')
10
+ .option('--session <new-or-id>', '首次建立或复用控制会话')
11
+ .option('--browser <id-or-name>', '指定浏览器')
12
+ .option('--connector <id-or-name>', '限制到远端 connector')
13
+ .option('--local', '仅使用本机浏览器')
14
+ .option('--cdp', '强制本机 CDP')
15
+ .option('--server <url>', '覆盖 cloud-server 地址')
16
+ .option('--timeout <seconds>', '整个脚本的执行时限(默认 60 秒)', '60')
17
+ .allowExcessArguments(false)
18
+ .exitOverride()
19
+ .addHelpText('after', `\n示例:\n ${commandName} run --file task.mjs\n ${commandName} run <<'JS'\n console.log(await mearl.tabList());\n JS\n\n脚本拥有当前宿主的 Node.js 权限;使用 await import() 加载模块。\n调用 Skill 命令:await mearl.invokePlugin('notes', { command: 'search', input: { query: 'project' } })。\n用 console.log(await mearl.help('pageClick')) 查询共享帮助,mearl.structure(value) 查看结果结构。\n每次运行重建 JS 变量;复用 session 和 tabId 可继续同一浏览器任务。\n`);
20
+ let source;
21
+ let options;
22
+ try {
23
+ program.parse(args, { from: 'user' });
24
+ options = program.opts();
25
+ const timeout = Number(options.timeout);
26
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout * 1000 > 2_147_483_647)
27
+ throw new Error('--timeout 必须是有效范围内的大于 0 的秒数');
28
+ if ((options.local || options.cdp) && options.connector)
29
+ throw new Error('--local/--cdp 不能与 --connector 同时使用');
30
+ if (options.file)
31
+ source = await readFile(resolve(options.file), 'utf8');
32
+ else {
33
+ if (process.stdin.isTTY)
34
+ throw new Error('通过 stdin 提交脚本,或使用 --file <path>');
35
+ const chunks = [];
36
+ let size = 0;
37
+ for await (const chunk of process.stdin) {
38
+ const buffer = Buffer.from(chunk);
39
+ size += buffer.length;
40
+ if (size > 1024 * 1024)
41
+ throw new Error('脚本不能超过 1 MiB');
42
+ chunks.push(buffer);
43
+ }
44
+ source = Buffer.concat(chunks).toString('utf8');
45
+ }
46
+ if (!source.trim() || Buffer.byteLength(source) > 1024 * 1024)
47
+ throw new Error('脚本必须是非空且不超过 1 MiB 的 JavaScript');
48
+ }
49
+ catch (error) {
50
+ if (error.code === 'commander.helpDisplayed')
51
+ return 0;
52
+ console.error(error instanceof Error ? error.message : String(error));
53
+ return 2;
54
+ }
55
+ const controller = new AbortController();
56
+ const abort = () => controller.abort();
57
+ process.once('SIGINT', abort);
58
+ process.once('SIGTERM', abort);
59
+ try {
60
+ const result = await executeScript(source, {
61
+ executionTimeoutSec: Number(options.timeout),
62
+ signal: controller.signal,
63
+ source: 'cli',
64
+ ...(options.session ? { session: options.session } : {}),
65
+ ...(options.browser ? { browser: options.browser } : {}),
66
+ ...(options.connector ? { connector: options.connector } : {}),
67
+ ...(options.local || options.cdp ? { local: true } : {}),
68
+ ...(options.cdp ? { transport: 'cdp' } : {}),
69
+ ...(options.server ? { serverUrl: options.server } : {}),
70
+ });
71
+ if (result.stdout)
72
+ process.stdout.write(result.stdout);
73
+ if (result.stderr)
74
+ process.stderr.write(result.stderr);
75
+ if (result.session)
76
+ console.error(JSON.stringify({ session: result.session }));
77
+ if (!result.success)
78
+ console.error(JSON.stringify({
79
+ error: result.error,
80
+ executionStopped: result.executionStopped,
81
+ mayHaveLateEffects: result.mayHaveLateEffects,
82
+ pendingActions: result.pendingActions,
83
+ }));
84
+ return result.success ? 0 : 1;
85
+ }
86
+ catch (error) {
87
+ console.error(error instanceof Error ? error.message : String(error));
88
+ return 1;
89
+ }
90
+ finally {
91
+ process.removeListener('SIGINT', abort);
92
+ process.removeListener('SIGTERM', abort);
93
+ }
94
+ }
@@ -0,0 +1,8 @@
1
+ export declare function serializeScriptError(error: unknown, fallback?: string): {
2
+ stack?: string | undefined;
3
+ attempt?: {} | null | undefined;
4
+ stage?: string | undefined;
5
+ details?: Record<string, unknown> | undefined;
6
+ code: string;
7
+ message: string;
8
+ };