@mearl/client 3.0.0-beta.1 → 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.
- package/README.md +333 -246
- package/README.zh-CN.md +612 -0
- package/dist/actionTimeouts.js +0 -3
- package/dist/autoUpdate.js +3 -1
- package/dist/bin.js +12 -5
- package/dist/capabilityHelp.d.ts +4 -0
- package/dist/capabilityHelp.js +68 -0
- package/dist/check.d.ts +5 -6
- package/dist/check.js +2 -5
- package/dist/cli.js +29 -56
- package/dist/commandHelp.d.ts +3 -0
- package/dist/commandHelp.js +79 -0
- package/dist/commandSuggestions.d.ts +3 -0
- package/dist/commandSuggestions.js +16 -5
- package/dist/commands.d.ts +70 -65
- package/dist/commands.js +60 -55
- package/dist/guide.d.ts +46 -0
- package/dist/guide.js +80 -0
- package/dist/guides/api-browser-setup.md +93 -0
- package/dist/guides/api-page-automation.md +140 -0
- package/dist/guides/api-page-debugging.md +39 -0
- package/dist/guides/api-page-environment.md +37 -0
- package/dist/guides/api-request-debugging.md +54 -0
- package/dist/guides/browser-tool-cooperation.md +23 -0
- package/dist/guides/core.md +60 -0
- package/dist/guides/related-skills.md +51 -0
- package/dist/guides/scripting.md +21 -0
- package/dist/guides/troubleshooting.md +170 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +8 -5
- package/dist/plugins/clients/pluginHostConnection.d.ts +2 -0
- package/dist/plugins/clients/pluginHostConnection.js +3 -1
- package/dist/plugins/officialPluginInstaller.js +2 -1
- package/dist/plugins/pluginRegistry.d.ts +1 -1
- package/dist/plugins/pluginRegistry.js +21 -7
- package/dist/plugins/runtime/pluginCommandWorker.js +8 -6
- package/dist/plugins/runtime/pluginDiagnostics.js +1 -1
- package/dist/plugins/runtime/pluginExecutor.d.ts +5 -0
- package/dist/plugins/runtime/pluginExecutor.js +15 -2
- package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +4 -0
- package/dist/plugins/runtime/pluginExecutorSupervisor.js +2 -1
- package/dist/plugins/runtime/pluginHostServer.js +5 -1
- package/dist/plugins/runtime/pluginPageManager.js +2 -6
- package/dist/plugins/runtime/pluginRevision.js +0 -51
- package/dist/plugins/runtime/pluginStateManager.js +1 -1
- package/dist/plugins/runtime/pluginStateStore.js +3 -8
- package/dist/plugins/runtime/pluginTaskManager.js +5 -9
- package/dist/plugins/sdk/plugin.d.ts +9 -0
- package/dist/plugins/sdk/plugin.js +22 -9
- package/dist/plugins/sdk/pluginModelDefinition.js +0 -6
- package/dist/plugins/sdk/pluginPageDefinition.d.ts +0 -1
- package/dist/plugins/sdk/pluginPageDefinition.js +2 -18
- package/dist/plugins/sdk/pluginRecoveryDefinition.js +0 -3
- package/dist/plugins/sdk/pluginStateDefinition.d.ts +0 -1
- package/dist/plugins/sdk/pluginStateDefinition.js +0 -11
- package/dist/runtimePaths.d.ts +1 -0
- package/dist/runtimePaths.js +1 -0
- package/dist/script.d.ts +30 -0
- package/dist/script.js +261 -0
- package/dist/scriptCli.d.ts +1 -0
- package/dist/scriptCli.js +94 -0
- package/dist/scriptErrors.d.ts +8 -0
- package/dist/scriptErrors.js +21 -0
- package/dist/scriptWorker.d.ts +1 -0
- package/dist/scriptWorker.js +50 -0
- package/dist/sdkMethods.d.ts +3 -0
- package/dist/sdkMethods.js +9 -0
- package/dist/socket.js +33 -8
- package/dist/unified.js +11 -1
- package/dist/unifiedConfig.d.ts +4 -0
- package/dist/unifiedConfig.js +11 -0
- package/package.json +14 -12
|
@@ -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
|
-
|
|
405
|
-
|
|
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
|
|
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 +
|
|
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 -
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
package/dist/runtimePaths.d.ts
CHANGED
package/dist/runtimePaths.js
CHANGED
|
@@ -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));
|
package/dist/script.d.ts
ADDED
|
@@ -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
|
+
};
|