@mearl/client 2.22.1 → 2.22.2
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 +71 -0
- package/dist/cli.js +13 -41
- package/dist/commandHelp.d.ts +3 -0
- package/dist/commandHelp.js +79 -0
- package/dist/commands.d.ts +7 -7
- package/dist/commands.js +1 -1
- package/dist/generated/data-structure.d.ts +8 -0
- package/dist/generated/data-structure.js +58 -0
- package/dist/generated/plugin-host-build.d.ts +1 -1
- package/dist/generated/plugin-host-build.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugins/pluginRegistry.js +1 -0
- package/dist/plugins/runtime/pluginCommandWorker.js +8 -6
- 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/runtimePaths.d.ts +1 -0
- package/dist/runtimePaths.js +1 -0
- package/dist/script.d.ts +30 -0
- package/dist/script.js +260 -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/package.json +10 -5
package/README.md
CHANGED
|
@@ -72,6 +72,77 @@ mearl browser_list
|
|
|
72
72
|
mearl browser_release
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
### Agent 脚本
|
|
76
|
+
|
|
77
|
+
`mearl run` 从 stdin 或 `--file <path>` 读取 JavaScript,预注入 `mearl` SDK,支持顶层 `await`、条件、循环和数据处理,无需在任务目录安装依赖:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
mearl run --session new <<'JS'
|
|
81
|
+
const tab = await mearl.tabOpen({ url: 'https://example.com', reuse: 'prefer' });
|
|
82
|
+
console.log({ tabId: tab.tabId, session: mearl.session });
|
|
83
|
+
console.log(await mearl.pageSnapshot({ tabId: tab.tabId, mode: 'viewport' }));
|
|
84
|
+
JS
|
|
85
|
+
|
|
86
|
+
mearl run --session '<sessionId>' --file /absolute/path/next-step.mjs
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
方法名与 SDK 一致,例如 `mearl.pageClick(params, options?)`、`mearl.getRequests(params, options?)`、
|
|
90
|
+
`mearl.tabList(options?)`,通用调用使用 `mearl.invoke(action, payload, options?)`。
|
|
91
|
+
参数和返回值沿用普通 action,可查 `mearl <action> --help`;动作失败仍以 `action.success`、
|
|
92
|
+
`stage` 和 `attempt` 表达,由脚本判断后续逻辑。`console.log` 输出必要结果,`console.error` 输出诊断;
|
|
93
|
+
返回数据不会自动打印。`run_actions` 适合无需判断的紧密序列;脚本适合结果可由代码判断的组合流程。
|
|
94
|
+
需要模型理解页面时,输出最小观察并在下一轮继续。
|
|
95
|
+
|
|
96
|
+
脚本用 `help` 查询现有命令定义,返回文本由调用方按需输出;CLI `--help` 与脚本共用同一份参数、结果说明、约束和示例。
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
console.log(await mearl.help()); // 方法与已注册插件清单
|
|
100
|
+
console.log(await mearl.help('pageClick')); // 也接受 page_click
|
|
101
|
+
console.log(await mearl.help({ plugin: 'yuque', command: 'read' }));
|
|
102
|
+
|
|
103
|
+
const result = await mearl.invokePlugin('yuque', { command: 'read', input: { url: '文档链接' } });
|
|
104
|
+
console.log(mearl.structure(result));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`structure(value, options?)` 同步提取已取得结果的结构,叶子值替换为类型;默认深度 5、每对象 50 个字段、
|
|
108
|
+
每数组 1 个样本、总计 2000 个节点,可通过 `maxDepth`、`maxKeys`、`arraySamples`、`maxNodes` 调整。
|
|
109
|
+
输出包含数组总长度与样本,遇到循环引用、访问器或资源限制会标记;样本不保证覆盖所有可选字段或不同数组元素,
|
|
110
|
+
业务含义以命令帮助为准。普通 SDK 同样导出 `structure`,它与请求 `keysOnly` 使用相同的提取逻辑;
|
|
111
|
+
`keysOnly` 也遵循上述默认字段和节点上限,超出部分会标记截断。
|
|
112
|
+
插件帮助在隔离进程中加载注册信息,不执行所查询的业务命令。
|
|
113
|
+
|
|
114
|
+
已注册 Skill 命令也可直接组合,命令名和输入来自该插件的 `--help`:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
const result = await mearl.invokePlugin('yuque', {
|
|
118
|
+
command: 'read',
|
|
119
|
+
input: { url: 'https://aliyuque.antfin.com/group/book/doc' },
|
|
120
|
+
});
|
|
121
|
+
console.log(result);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
脚本的 `--browser`、`--session`、`--connector`、`--local`、`--cdp` 由浏览器 API 和插件调用继承;
|
|
125
|
+
单次调用的 `options` 可覆盖。`--server` 指定 cloud-server。`--session new` 成功建立会话后,
|
|
126
|
+
后续调用自动使用返回的 ID,`mearl.session` 和 CLI stderr 同时提供会话信息。
|
|
127
|
+
先 await 建立会话的浏览器调用,再执行并发或 Skill 命令。脚本结束断开 SDK 连接,控制会话保留;
|
|
128
|
+
按任务生命周期显式调用 `mearl.browserRelease`。每轮重建 JS 变量,使用已返回的 `sessionId` 和 `tabId` 继续任务。
|
|
129
|
+
|
|
130
|
+
每次脚本在独立 Node.js 进程运行,继承调用方的工作目录和环境,拥有该宿主的 Node.js 权限;
|
|
131
|
+
它不是安全沙箱。用 `await import('node:fs/promises')` 等动态 import 加载模块;
|
|
132
|
+
`--file` 的内容也作为脚本函数执行,不支持静态 `import` / `export`。本地模块建议使用绝对文件 URL。
|
|
133
|
+
`window` / `document` 仅在 `mearl.pageEval` 的页面表达式中可用。插件 registry、本地文件和身份认证均属于运行 CLI 的宿主。
|
|
134
|
+
|
|
135
|
+
`--timeout <seconds>` 限制整个脚本,默认 60 秒;单次 API 的 `options.timeoutSec` 限制该调用。
|
|
136
|
+
stdout 与 stderr 合计最多 256 KiB,源码最多 1 MiB。await 所有 SDK 调用;漏写 await、超时、取消或脚本异常以退出码 1 结束,
|
|
137
|
+
输入错误以 2 结束。取消和超时停止脚本及其进程组,并向仍在等待的 Skill 调用传递取消信号;已派发的浏览器或业务操作可能继续生效,
|
|
138
|
+
错误中的 `pendingActions` / `mayHaveLateEffects` 用于判断是否需要核验现场,不能直接重跑整段脚本。
|
|
139
|
+
Skill 命令使用既有隔离执行器,stdout/stderr 计入脚本输出上限;异常诊断也会按大小裁剪。
|
|
140
|
+
执行结果的 `success` 仅表示脚本正常完成,动作结果与业务成功仍需由代码检查。
|
|
141
|
+
|
|
142
|
+
SDK 宿主可从 `@mearl/client/script` 导入 `executeScript(code, options)`,得到
|
|
143
|
+
`{ success, stdout, stderr, session?, error?, executionStopped, mayHaveLateEffects, pendingActions }`。
|
|
144
|
+
它支持 `signal` 取消和 `executionTimeoutSec`,与 CLI、MCP 共用执行器。
|
|
145
|
+
|
|
75
146
|
### 控制会话与文件
|
|
76
147
|
|
|
77
148
|
多步骤任务首次 `tab_open` 或 `page_snapshot` 使用 `--session new`,响应在原结果旁附带 `session`,后续用其 `sessionId` 自动路由,无需额外创建/查询 action。Session 绑定一个浏览器与传输;页面首次使用自动关联,复用页面与新建页面分别记录。同一页面只允许一个会话控制,旧调用不能绕过占用;Session 不隔离 Cookie 或存储。
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
|
6
6
|
import { runUnifiedCheck, parseCheckTimeout } from './check.js';
|
|
7
7
|
import { commandInput, findCommandSuggestions, formatCommandSuggestions, } from './commandSuggestions.js';
|
|
8
8
|
import { COMMANDS, COMMAND_MAP } from './commands.js';
|
|
9
|
+
import { formatCommandHelp } from './commandHelp.js';
|
|
9
10
|
import { CLIENT_VERSION } from './socket.js';
|
|
10
11
|
import { UnifiedClient } from './unified.js';
|
|
11
12
|
import { findPluginEntry, findPluginRegistration, findOfficialPlugin, listPluginNames, listOfficialPluginNames, loadRegisteredPlugin, PluginError, } from './plugins/sdk/plugin.js';
|
|
@@ -59,6 +60,11 @@ const CATEGORIES = [
|
|
|
59
60
|
},
|
|
60
61
|
];
|
|
61
62
|
const BUILTIN_COMMANDS = [
|
|
63
|
+
{
|
|
64
|
+
name: 'run',
|
|
65
|
+
description: '执行预注入 mearl SDK 的 Node.js 脚本',
|
|
66
|
+
examples: ['run --file task.mjs'],
|
|
67
|
+
},
|
|
62
68
|
{
|
|
63
69
|
name: 'check',
|
|
64
70
|
description: '检查指定浏览器,未指定时检查全部已连接浏览器',
|
|
@@ -120,6 +126,7 @@ function printUsage(commandName, version) {
|
|
|
120
126
|
` ${commandName} <action> [选项]`,
|
|
121
127
|
` ${commandName} <action> --help`,
|
|
122
128
|
` ${commandName} check [选项]`,
|
|
129
|
+
` ${commandName} run [--file <path>] [选项]`,
|
|
123
130
|
'',
|
|
124
131
|
'常用选项:',
|
|
125
132
|
' --payload <json> 直接传入 JSON 参数',
|
|
@@ -135,6 +142,7 @@ function printUsage(commandName, version) {
|
|
|
135
142
|
' -v, --version 查看版本',
|
|
136
143
|
'',
|
|
137
144
|
'内置命令:',
|
|
145
|
+
` ${'run'.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}执行 Node.js 脚本,支持浏览器 API 与 Skill 命令`,
|
|
138
146
|
` ${'check'.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}检查指定浏览器,未指定时检查全部已连接浏览器`,
|
|
139
147
|
];
|
|
140
148
|
for (const category of CATEGORIES) {
|
|
@@ -300,46 +308,6 @@ function printAdvancedCommandHelp(commandName, action) {
|
|
|
300
308
|
'结果包含 count 与 connectors;每项包含 ID、名称、可选版本、连接时间和最后心跳。',
|
|
301
309
|
].join('\n'));
|
|
302
310
|
}
|
|
303
|
-
function printCommandHelp(commandName, command) {
|
|
304
|
-
const payloadUsage = command.params?.length ? ' [--payload <json> | --payload-file <path>]' : '';
|
|
305
|
-
const lines = [
|
|
306
|
-
`${command.name}`,
|
|
307
|
-
` ${command.description}`,
|
|
308
|
-
'',
|
|
309
|
-
'用法:',
|
|
310
|
-
` ${commandName} ${command.name}${payloadUsage} [常用选项]`,
|
|
311
|
-
'',
|
|
312
|
-
];
|
|
313
|
-
if (command.params?.length) {
|
|
314
|
-
lines.push('参数(--payload JSON 字段):');
|
|
315
|
-
for (const parameter of command.params) {
|
|
316
|
-
const required = parameter.required ? '必填' : '可选';
|
|
317
|
-
lines.push(` ${parameter.name.padEnd(22)}${required.padEnd(6)}${parameter.type.padEnd(28)} ${parameter.description}`);
|
|
318
|
-
for (const detail of parameter.details ?? [])
|
|
319
|
-
lines.push(` ${detail}`);
|
|
320
|
-
}
|
|
321
|
-
lines.push('');
|
|
322
|
-
}
|
|
323
|
-
if (command.name === 'browser_list') {
|
|
324
|
-
lines.push('目标范围(按需):', ' --local 只列出当前机器上的浏览器', ' --connector <id|名称> 只列出指定远端 connector 的浏览器', '');
|
|
325
|
-
}
|
|
326
|
-
else if (command.name === 'browser_launch') {
|
|
327
|
-
lines.push('宿主选择(本机与远端同时可用时):', ' --local 在当前机器启动', ' --connector <id|名称> 在指定远端 connector 启动', ' --browser <id|名称> 在控制浏览器所在宿主启动,并可作为 Cookie/UA 来源', '');
|
|
328
|
-
}
|
|
329
|
-
if (command.notes?.length) {
|
|
330
|
-
lines.push('约束与行为:', ...command.notes.map(note => ` ${note}`), '');
|
|
331
|
-
}
|
|
332
|
-
if (command.result?.length) {
|
|
333
|
-
lines.push('结果:', ...command.result.map(line => ` ${line}`), '');
|
|
334
|
-
}
|
|
335
|
-
if (command.examples?.length) {
|
|
336
|
-
lines.push('示例:');
|
|
337
|
-
for (const example of command.examples)
|
|
338
|
-
lines.push(` ${commandName} ${example}`);
|
|
339
|
-
}
|
|
340
|
-
lines.push('', `运行 ${commandName} --help 查看常用选项。`);
|
|
341
|
-
console.error(lines.join('\n'));
|
|
342
|
-
}
|
|
343
311
|
function printUnknownCommand(commandName, args) {
|
|
344
312
|
const input = commandInput(args) || args[0] || '';
|
|
345
313
|
const suggestions = findCommandSuggestions(args, SUGGESTABLE_COMMANDS);
|
|
@@ -605,6 +573,10 @@ function writeOutput(action, result, outputPath) {
|
|
|
605
573
|
export async function runMearlCli(args, options = {}) {
|
|
606
574
|
const commandName = options.commandName ?? 'mearl';
|
|
607
575
|
const clientVersion = options.clientVersion ?? CLIENT_VERSION;
|
|
576
|
+
if (args[0] === 'run') {
|
|
577
|
+
const { runScriptCli } = await import('./scriptCli.js');
|
|
578
|
+
return runScriptCli(args.slice(1), commandName);
|
|
579
|
+
}
|
|
608
580
|
if (args[0] === 'capture_checkpoint')
|
|
609
581
|
args = ['tab_checkpoint', ...args.slice(1)];
|
|
610
582
|
const provider = installedProvider(args[0]);
|
|
@@ -641,7 +613,7 @@ export async function runMearlCli(args, options = {}) {
|
|
|
641
613
|
if (args.includes('--help') || args.includes('-h')) {
|
|
642
614
|
const command = COMMAND_MAP.get(args[0]);
|
|
643
615
|
if (command)
|
|
644
|
-
|
|
616
|
+
console.error(formatCommandHelp(commandName, command));
|
|
645
617
|
else if (args[0] === 'check')
|
|
646
618
|
printCheckHelp(commandName);
|
|
647
619
|
else if (ADVANCED_BUILTIN_COMMANDS.has(args[0])) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { COMMANDS, COMMAND_MAP } from './commands.js';
|
|
2
|
+
import { SDK_METHOD_ACTIONS, SDK_OPTIONS_ONLY_METHODS } from './sdkMethods.js';
|
|
3
|
+
import { listPluginNames } from './plugins/pluginRegistry.js';
|
|
4
|
+
const HELPER_HELP = {
|
|
5
|
+
invoke: 'invoke(action, payload?, options?) — 调用浏览器 action,返回普通 action 的原始结果',
|
|
6
|
+
invokePlugin: 'invokePlugin(name, { command, input }, options?) — 调用已注册 Skill 命令,返回该命令的原始结果',
|
|
7
|
+
help: 'help(methodOrAction?) / help({ plugin, command? }) — 返回共享命令帮助文本;无参数时列出方法;用 console.log 输出',
|
|
8
|
+
structure: 'structure(value, { maxDepth=5, maxKeys=50, arraySamples=1, maxNodes=2000 }?) — 同步返回有界结构样本;叶子值替换为类型,标注数组样本、循环引用和截断;不能证明未采样内容或业务语义',
|
|
9
|
+
saveArtifact: 'saveArtifact(artifact, absolutePath, options?) — 保存浏览器文件产物,返回保存结果',
|
|
10
|
+
selectBrowser: 'selectBrowser({ browser? }, options?) — 选择默认目标;返回 { selected };browser 使用 browserList 返回的 ID 或名称,空字符串清除默认目标',
|
|
11
|
+
};
|
|
12
|
+
export function getCommandHelp(target) {
|
|
13
|
+
if (!target)
|
|
14
|
+
return [
|
|
15
|
+
...COMMANDS.map((command) => `${[...SDK_METHOD_ACTIONS].find(([, action]) => action === command.name)?.[0] ?? command.name} (${command.name}) — ${command.summary ?? command.description}`),
|
|
16
|
+
...Object.values(HELPER_HELP),
|
|
17
|
+
...listPluginNames().map(plugin => `Skill ${plugin} — help({ plugin: '${plugin}' })`),
|
|
18
|
+
].join('\n');
|
|
19
|
+
if (Object.hasOwn(HELPER_HELP, target))
|
|
20
|
+
return HELPER_HELP[target];
|
|
21
|
+
const resolved = SDK_METHOD_ACTIONS.get(target) ?? target;
|
|
22
|
+
const action = resolved === 'capture_checkpoint' ? 'tab_checkpoint' : resolved;
|
|
23
|
+
if (action === 'browser_select_browser')
|
|
24
|
+
return HELPER_HELP.selectBrowser;
|
|
25
|
+
const command = COMMAND_MAP.get(action);
|
|
26
|
+
if (!command)
|
|
27
|
+
throw new Error(`Unknown command or SDK method: ${target}`);
|
|
28
|
+
return formatCommandHelp('mearl', command);
|
|
29
|
+
}
|
|
30
|
+
export function formatCommandHelp(commandName, command) {
|
|
31
|
+
const payloadUsage = command.params?.length ? ' [--payload <json> | --payload-file <path>]' : '';
|
|
32
|
+
const lines = [
|
|
33
|
+
`${command.name}`,
|
|
34
|
+
` ${command.description}`,
|
|
35
|
+
'',
|
|
36
|
+
'用法:',
|
|
37
|
+
` ${commandName} ${command.name}${payloadUsage} [常用选项]`,
|
|
38
|
+
'',
|
|
39
|
+
];
|
|
40
|
+
const method = [...SDK_METHOD_ACTIONS].find(([, action]) => action === command.name)?.[0];
|
|
41
|
+
if (method)
|
|
42
|
+
lines.push('SDK:', ...(method === 'browserRelease'
|
|
43
|
+
? [
|
|
44
|
+
' await mearl.browserRelease(options?)',
|
|
45
|
+
' await mearl.browserRelease(params, options)',
|
|
46
|
+
]
|
|
47
|
+
: [
|
|
48
|
+
` await mearl.${method}(${SDK_OPTIONS_ONLY_METHODS.has(method) ? 'options?' : 'params, options?'})`,
|
|
49
|
+
]), '');
|
|
50
|
+
if (command.params?.length) {
|
|
51
|
+
lines.push('参数(--payload JSON 字段):');
|
|
52
|
+
for (const parameter of command.params) {
|
|
53
|
+
const required = parameter.required ? '必填' : '可选';
|
|
54
|
+
lines.push(` ${parameter.name.padEnd(22)}${required.padEnd(6)}${parameter.type.padEnd(28)} ${parameter.description}`);
|
|
55
|
+
for (const detail of parameter.details ?? [])
|
|
56
|
+
lines.push(` ${detail}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
if (command.name === 'browser_list') {
|
|
61
|
+
lines.push('目标范围(按需):', ' --local 只列出当前机器上的浏览器', ' --connector <id|名称> 只列出指定远端 connector 的浏览器', '');
|
|
62
|
+
}
|
|
63
|
+
else if (command.name === 'browser_launch') {
|
|
64
|
+
lines.push('宿主选择(本机与远端同时可用时):', ' --local 在当前机器启动', ' --connector <id|名称> 在指定远端 connector 启动', ' --browser <id|名称> 在控制浏览器所在宿主启动,并可作为 Cookie/UA 来源', '');
|
|
65
|
+
}
|
|
66
|
+
if (command.notes?.length) {
|
|
67
|
+
lines.push('约束与行为:', ...command.notes.map(note => ` ${note}`), '');
|
|
68
|
+
}
|
|
69
|
+
if (command.result?.length) {
|
|
70
|
+
lines.push('结果:', ...command.result.map(line => ` ${line}`), '');
|
|
71
|
+
}
|
|
72
|
+
if (command.examples?.length) {
|
|
73
|
+
lines.push('示例:');
|
|
74
|
+
for (const example of command.examples)
|
|
75
|
+
lines.push(` ${commandName} ${example}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('', `运行 ${commandName} --help 查看常用选项。`);
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -613,7 +613,7 @@ export declare const COMMANDS: readonly [{
|
|
|
613
613
|
readonly required: true;
|
|
614
614
|
readonly description: "目标标签页 ID";
|
|
615
615
|
}];
|
|
616
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
616
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。"];
|
|
617
617
|
readonly notes: readonly ["selector / text / point 必须且只能提供一个。隐藏页 auto 使用 DOM;只有明确依赖真实用户手势时才激活页面。", "原生 select 可按 option 文本点击;checkbox/radio 返回 controlState.checkedBefore/checkedAfter。"];
|
|
618
618
|
readonly examples: ["page_click --payload '{\"tabId\":12345,\"text\":\"提交\"}'", "page_click --payload '{\"tabId\":12345,\"text\":\"退款明细\",\"role\":\"button\"}'", "page_click --payload '{\"tabId\":12345,\"text\":\"确定\",\"scope\":\"@e12\"}'", "page_click --payload '{\"tabId\":12345,\"selector\":\"@e3\"}'", "page_click --payload '{\"tabId\":12345,\"selector\":\".submit-btn\"}'", "page_click --payload '{\"tabId\":12345,\"selector\":\".gesture-btn\",\"clickMode\":\"touch\"}'", "page_click --payload '{\"tabId\":12345,\"point\":{\"x\":336,\"y\":117}}'", "page_click --payload '{\"tabId\":12345,\"point\":{\"x\":196,\"y\":240},\"coordinateSpace\":\"active-frame\"}'"];
|
|
619
619
|
}, {
|
|
@@ -643,7 +643,7 @@ export declare const COMMANDS: readonly [{
|
|
|
643
643
|
readonly required: true;
|
|
644
644
|
readonly description: "目标标签页 ID";
|
|
645
645
|
}];
|
|
646
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
646
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。"];
|
|
647
647
|
readonly notes: readonly ["起点会自动滚入视口,终点必须同时位于当前视口;否则先滚动页面。隐藏页 auto 使用 DOM,不切换活动页。"];
|
|
648
648
|
readonly examples: ["page_drag --payload '{\"tabId\":12345,\"from\":{\"x\":350,\"y\":80},\"to\":{\"x\":80,\"y\":80}}'", "page_drag --payload '{\"tabId\":12345,\"from\":\"@e3\",\"to\":\"#drop-zone\",\"dragMode\":\"mouse\"}'"];
|
|
649
649
|
}, {
|
|
@@ -669,7 +669,7 @@ export declare const COMMANDS: readonly [{
|
|
|
669
669
|
readonly required: true;
|
|
670
670
|
readonly description: "目标标签页 ID";
|
|
671
671
|
}];
|
|
672
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
672
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。"];
|
|
673
673
|
readonly examples: ["page_type --payload '{\"tabId\":12345,\"selector\":\"@e6\",\"text\":\"搜索关键词\"}'"];
|
|
674
674
|
}, {
|
|
675
675
|
readonly name: "page_hover";
|
|
@@ -685,7 +685,7 @@ export declare const COMMANDS: readonly [{
|
|
|
685
685
|
readonly required: true;
|
|
686
686
|
readonly description: "目标标签页 ID";
|
|
687
687
|
}];
|
|
688
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
688
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。"];
|
|
689
689
|
readonly examples: ["page_hover --payload '{\"tabId\":12345,\"selector\":\".nav-menu\"}'"];
|
|
690
690
|
}, {
|
|
691
691
|
readonly name: "page_scroll";
|
|
@@ -713,7 +713,7 @@ export declare const COMMANDS: readonly [{
|
|
|
713
713
|
readonly required: true;
|
|
714
714
|
readonly description: "目标标签页 ID";
|
|
715
715
|
}];
|
|
716
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
716
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。", "action.data 返回 moved、atTop/atBottom、scrollYBefore/After、maxScrollY、resolvedContainer 和 ancestorDistance;自动发现的内部容器提供可复用 containerSelector。"];
|
|
717
717
|
readonly examples: ["page_scroll --payload '{\"tabId\":12345,\"direction\":\"down\"}'", "page_scroll --payload '{\"tabId\":12345,\"direction\":\"bottom\"}'", "page_scroll --payload '{\"tabId\":12345,\"selector\":\".virtual-list\",\"direction\":\"down\",\"distance\":800}'", "page_scroll --payload '{\"tabId\":12345,\"selector\":\"@e8\",\"containerPolicy\":\"nearest\",\"direction\":\"bottom\"}'"];
|
|
718
718
|
}, {
|
|
719
719
|
readonly name: "page_runtime";
|
|
@@ -821,7 +821,7 @@ export declare const COMMANDS: readonly [{
|
|
|
821
821
|
readonly description: "目标标签页 ID";
|
|
822
822
|
}];
|
|
823
823
|
readonly notes: readonly ["隐藏页 DOM 模式支持 Tab 焦点移动、原生控件 Enter/Space 激活和输入框 Enter 提交;编辑、选择、方向键和页面滚动等原生行为需可见页与 pressMode=keyboard。"];
|
|
824
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
824
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。", "action.data.dispatchMode / fallbackReason / defaultAction 说明实际输入路径。"];
|
|
825
825
|
readonly examples: ["page_press --payload '{\"tabId\":12345,\"key\":\"Enter\"}'"];
|
|
826
826
|
}, {
|
|
827
827
|
readonly name: "page_wait";
|
|
@@ -911,7 +911,7 @@ export declare const COMMANDS: readonly [{
|
|
|
911
911
|
readonly required: true;
|
|
912
912
|
readonly description: "目标标签页 ID";
|
|
913
913
|
}];
|
|
914
|
-
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
914
|
+
readonly result: readonly ["默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。", "observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。", "包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。", "observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。", "effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。", "fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。", "显式开启的诊断通道在 diagnostics 中返回动作期间增量,与 observe 独立。"];
|
|
915
915
|
readonly notes: readonly ["直接写入 input[type=file],不依赖 file chooser;隐藏的文件节点也可上传。外部 Provider 的文件中转能力查询 mearl <providerId>。"];
|
|
916
916
|
readonly examples: ["page_upload --payload '{\"tabId\":12345,\"selector\":\"#file-input\",\"filePaths\":[\"/Users/me/photo.jpg\"]}'"];
|
|
917
917
|
}, {
|
package/dist/commands.js
CHANGED
|
@@ -30,7 +30,7 @@ const PAGE_OBSERVE_PARAM = {
|
|
|
30
30
|
const PAGE_ACTION_RESULT = [
|
|
31
31
|
'默认返回 { action, observation };action.success 只表示动作成功,业务效果需结合 observation 和目标状态判断。',
|
|
32
32
|
'observe=false 且未开启 diagnostics 时返回裸动作结果(如 { clicked: true }),没有 action/observation;开启 diagnostics 时返回 { action, diagnostics }。',
|
|
33
|
-
'包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt
|
|
33
|
+
'包装结果中的 action.stage=precondition 表示未派发,省略 observation/diagnostics;dispatch-uncertain 表示可能已派发,先查看 action.attempt 与可用观察或最小快照核验,避免重复提交。CLI 单次调用失败以退出码 1 结束;脚本需检查 action.success。',
|
|
34
34
|
'observation.effects 返回通知、控件和焦点增量,可直接复用 node.ref/selector;changed=true 且 effects 为空不表示失败。',
|
|
35
35
|
'effects.texts 按需返回普通可见文本的 selector/text,不代表可点击控件或业务成功;超限或覆盖不完整时仍提示补快照。',
|
|
36
36
|
'fullSnapshotRecommended/snapshotReasons 提示是否补充快照;openedTabs 返回可继续操作的 tabId 和 ready。navigation 模式的 ready 表示新页面就绪。',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface StructureOptions {
|
|
2
|
+
maxDepth?: number;
|
|
3
|
+
maxKeys?: number;
|
|
4
|
+
arraySamples?: number;
|
|
5
|
+
maxNodes?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
|
|
8
|
+
export declare function getStructure(data: unknown, options?: StructureOptions): unknown;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
|
|
2
|
+
export function getStructure(data, options = {}) {
|
|
3
|
+
const { maxDepth = 5, maxKeys = 50, arraySamples = 1, maxNodes = 2000 } = options;
|
|
4
|
+
for (const [name, value, limit] of [
|
|
5
|
+
['maxDepth', maxDepth, 20],
|
|
6
|
+
['maxKeys', maxKeys, 1000],
|
|
7
|
+
['arraySamples', arraySamples, 20],
|
|
8
|
+
['maxNodes', maxNodes, 10000],
|
|
9
|
+
]) {
|
|
10
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > limit)
|
|
11
|
+
throw new Error(`${name} must be an integer between 1 and ${limit}`);
|
|
12
|
+
}
|
|
13
|
+
const ancestors = new WeakSet();
|
|
14
|
+
let nodes = 0;
|
|
15
|
+
const visit = (value, depth) => {
|
|
16
|
+
if (++nodes > maxNodes)
|
|
17
|
+
return '<max nodes reached>';
|
|
18
|
+
if (depth >= maxDepth)
|
|
19
|
+
return '<max depth reached>';
|
|
20
|
+
if (value === null)
|
|
21
|
+
return 'null';
|
|
22
|
+
if (typeof value !== 'object')
|
|
23
|
+
return typeof value;
|
|
24
|
+
if (ancestors.has(value))
|
|
25
|
+
return '<circular>';
|
|
26
|
+
ancestors.add(value);
|
|
27
|
+
try {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
if (!value.length)
|
|
30
|
+
return 'array[0]';
|
|
31
|
+
const samples = Array.from({ length: Math.min(value.length, arraySamples) }, (_, index) => {
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
33
|
+
return descriptor && 'value' in descriptor
|
|
34
|
+
? visit(descriptor.value, depth + 1)
|
|
35
|
+
: descriptor
|
|
36
|
+
? '<accessor>'
|
|
37
|
+
: 'undefined';
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
_type: `array[${value.length}]`,
|
|
41
|
+
...(arraySamples === 1 ? { _sample: samples[0] } : { _samples: samples }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const keys = Object.keys(value);
|
|
45
|
+
const entries = keys.slice(0, maxKeys).map(key => {
|
|
46
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
47
|
+
return [key, 'value' in descriptor ? visit(descriptor.value, depth + 1) : '<accessor>'];
|
|
48
|
+
});
|
|
49
|
+
if (keys.length > maxKeys)
|
|
50
|
+
entries.push(['<truncated keys>', keys.length - maxKeys]);
|
|
51
|
+
return Object.fromEntries(entries);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
ancestors.delete(value);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return visit(data, 0);
|
|
58
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_HOST_BUILD_ID = "
|
|
1
|
+
export declare const PLUGIN_HOST_BUILD_ID = "e5c7f6cfb7a597a523293c0ea2929b4970aa9eb86e7aeb672f85a37c317bc258";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/generate-plugin-host-build.mjs.
|
|
2
|
-
export const PLUGIN_HOST_BUILD_ID = '
|
|
2
|
+
export const PLUGIN_HOST_BUILD_ID = 'e5c7f6cfb7a597a523293c0ea2929b4970aa9eb86e7aeb672f85a37c317bc258';
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSE
|
|
|
16
16
|
export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestBinaryBody, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from './generated/browser-action-protocol.js';
|
|
17
17
|
import { type UnifiedInvokeOptions } from './unified.js';
|
|
18
18
|
import type * as Protocol from './generated/browser-action-protocol.js';
|
|
19
|
+
export { getStructure as structure, type StructureOptions } from './generated/data-structure.js';
|
|
19
20
|
export declare function tabCheckpoint(params: Protocol.CaptureCheckpointParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_checkpoint'>>;
|
|
20
21
|
/** Compatibility wrapper for tabCheckpoint. */
|
|
21
22
|
export declare function captureCheckpoint(params: Protocol.CaptureCheckpointParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_checkpoint'>>;
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { executeRegisteredPlugin, } from './plugins/sdk/plugin.js';
|
|
|
11
11
|
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
|
|
12
12
|
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated/browser-action-protocol.js';
|
|
13
13
|
import { invoke } from './unified.js';
|
|
14
|
+
export { getStructure as structure } from './generated/data-structure.js';
|
|
14
15
|
export function tabCheckpoint(params, options) {
|
|
15
16
|
return invoke('tab_checkpoint', params, options);
|
|
16
17
|
}
|
|
@@ -31,7 +31,7 @@ async function run(message) {
|
|
|
31
31
|
: message.access === 'toolbox'
|
|
32
32
|
? invokeRegisteredToolboxPlugin
|
|
33
33
|
: invokeRegisteredPlugin;
|
|
34
|
-
const declaration = message.describe
|
|
34
|
+
const declaration = message.describe || message.help
|
|
35
35
|
? await loadPlugin(message.entry, {
|
|
36
36
|
options: {},
|
|
37
37
|
progress: () => { },
|
|
@@ -63,11 +63,13 @@ async function run(message) {
|
|
|
63
63
|
const value = migration
|
|
64
64
|
? await migration.runMigration(message.invocation)
|
|
65
65
|
: declaration
|
|
66
|
-
?
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
? message.help
|
|
67
|
+
? declaration.help(message.help.namespace, message.help.command)
|
|
68
|
+
: {
|
|
69
|
+
pageRuntime: declaration.pageRuntime,
|
|
70
|
+
stateStorage: declaration.stateStorage,
|
|
71
|
+
taskRecovery: declaration.taskRecovery,
|
|
72
|
+
}
|
|
71
73
|
: message.model
|
|
72
74
|
? await (await import('@mearl/agent-runtime/codex')).runCodexModel(message.model, {
|
|
73
75
|
signal: controller.signal,
|
|
@@ -6,6 +6,11 @@ export interface PluginExecutorDependencies {
|
|
|
6
6
|
registration?: PluginRegistration;
|
|
7
7
|
}
|
|
8
8
|
export interface PluginExecutorOptions extends PluginInvokeOptions {
|
|
9
|
+
help?: {
|
|
10
|
+
namespace: string;
|
|
11
|
+
command?: string;
|
|
12
|
+
};
|
|
13
|
+
onOutput?: (stream: 'stdout' | 'stderr', chunk: Buffer) => void;
|
|
9
14
|
describe?: boolean;
|
|
10
15
|
model?: PluginModelRequest;
|
|
11
16
|
recovery?: boolean;
|
|
@@ -8,7 +8,7 @@ import { stopPluginProcessTree } from './pluginProcessTree.js';
|
|
|
8
8
|
import { processIdentity } from '@mearl/daemon-core';
|
|
9
9
|
/** Executes registered trusted code in a process whose lifetime belongs to this invocation. */
|
|
10
10
|
export async function executeRegisteredPlugin(plugin, invocation, context, options = {}, dependencies = {}) {
|
|
11
|
-
const { signal, onProgress, source, access = 'public', executionTimeoutMs, cancellationGraceMs = 1000, workerPath, cwd, environment, describe, model, recovery, migration, expectedRecovery, onExecutorStarted, onExecutorStopped, onDiagnostic, ...invokeOptions } = options;
|
|
11
|
+
const { signal, onProgress, source, access = 'public', executionTimeoutMs, cancellationGraceMs = 1000, workerPath, cwd, environment, describe, help, onOutput, model, recovery, migration, expectedRecovery, onExecutorStarted, onExecutorStopped, onDiagnostic, ...invokeOptions } = options;
|
|
12
12
|
for (const [name, value] of [
|
|
13
13
|
['executionTimeoutMs', executionTimeoutMs],
|
|
14
14
|
['cancellationGraceMs', cancellationGraceMs],
|
|
@@ -25,7 +25,7 @@ export async function executeRegisteredPlugin(plugin, invocation, context, optio
|
|
|
25
25
|
const entryVersion = pluginFileVersion(registration.entry);
|
|
26
26
|
return new Promise((resolve, reject) => {
|
|
27
27
|
const child = fork(workerPath ?? PLUGIN_WORKER_PATH, [], {
|
|
28
|
-
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
28
|
+
stdio: ['ignore', onOutput ? 'pipe' : 'ignore', 'pipe', 'ipc'],
|
|
29
29
|
serialization: 'advanced',
|
|
30
30
|
execArgv: [],
|
|
31
31
|
detached: process.platform !== 'win32',
|
|
@@ -46,6 +46,18 @@ export async function executeRegisteredPlugin(plugin, invocation, context, optio
|
|
|
46
46
|
child.stderr?.on('data', chunk => {
|
|
47
47
|
stderr = `${stderr}${chunk}`.slice(-65536);
|
|
48
48
|
});
|
|
49
|
+
if (onOutput) {
|
|
50
|
+
for (const stream of ['stdout', 'stderr']) {
|
|
51
|
+
child[stream]?.on('data', chunk => {
|
|
52
|
+
try {
|
|
53
|
+
onOutput(stream, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
stop(error instanceof Error ? error : new Error(String(error)));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
49
61
|
const send = (message) => {
|
|
50
62
|
if (!child.connected || exited)
|
|
51
63
|
return;
|
|
@@ -209,6 +221,7 @@ export async function executeRegisteredPlugin(plugin, invocation, context, optio
|
|
|
209
221
|
entryVersion,
|
|
210
222
|
access,
|
|
211
223
|
describe,
|
|
224
|
+
help,
|
|
212
225
|
model,
|
|
213
226
|
recovery,
|
|
214
227
|
migration,
|
|
@@ -13,6 +13,10 @@ export type PluginWorkerRequest = {
|
|
|
13
13
|
access: 'public' | 'toolbox' | 'runtime';
|
|
14
14
|
registration?: PluginRegistration;
|
|
15
15
|
describe?: boolean;
|
|
16
|
+
help?: {
|
|
17
|
+
namespace: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
};
|
|
16
20
|
model?: PluginModelRequest;
|
|
17
21
|
recovery?: boolean;
|
|
18
22
|
migration?: boolean;
|
|
@@ -3,13 +3,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
/** Ownership monitoring runs outside the event loop that executes plugin code. */
|
|
4
4
|
export function supervisePluginExecutor(workerUrl) {
|
|
5
5
|
const child = fork(fileURLToPath(workerUrl), ['--command'], {
|
|
6
|
-
stdio: ['ignore', '
|
|
6
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
7
7
|
serialization: 'advanced',
|
|
8
8
|
execArgv: [],
|
|
9
9
|
});
|
|
10
10
|
let ending = false;
|
|
11
11
|
let forwarded = Promise.resolve();
|
|
12
12
|
child.stderr?.on('data', chunk => process.stderr.write(chunk));
|
|
13
|
+
child.stdout?.on('data', chunk => process.stdout.write(chunk));
|
|
13
14
|
const terminate = () => {
|
|
14
15
|
if (process.platform === 'win32') {
|
|
15
16
|
try {
|
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,260 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isBrowserCommandAction } from './generated/browser-action-protocol.js';
|
|
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 === 'browserRelease' && args.length < 2);
|
|
212
|
+
return invoke(METHOD_ACTIONS.get(method), optionsOnly ? {} : (args[0] ?? {}), optionsOnly ? args[0] : args[1]);
|
|
213
|
+
})
|
|
214
|
+
.then(result => send({ type: 'response', id, result, session }), error => send({ type: 'response', id, error: serializeError(error), session }))
|
|
215
|
+
.finally(() => pending.delete(id));
|
|
216
|
+
});
|
|
217
|
+
child.once('error', error => {
|
|
218
|
+
stop(serializeError(error));
|
|
219
|
+
});
|
|
220
|
+
child.once('exit', () => {
|
|
221
|
+
if (outcome === undefined)
|
|
222
|
+
stop({
|
|
223
|
+
code: 'SCRIPT_EXITED',
|
|
224
|
+
message: 'Script process exited before reporting completion',
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
child.once('close', () => {
|
|
228
|
+
closed = true;
|
|
229
|
+
clearTimeout(deadline);
|
|
230
|
+
signal?.removeEventListener('abort', abort);
|
|
231
|
+
void Promise.allSettled([cleanup, ...pluginCalls])
|
|
232
|
+
.then(outcomes => {
|
|
233
|
+
if (outcomes.some(result => result.status === 'rejected' &&
|
|
234
|
+
result.reason?.code === 'PLUGIN_EXECUTOR_CLEANUP_FAILED'))
|
|
235
|
+
cleanupError = new Error('Unable to confirm Skill process cleanup');
|
|
236
|
+
const error = cleanupError
|
|
237
|
+
? serializeError(cleanupError, 'SCRIPT_CLEANUP_FAILED')
|
|
238
|
+
: outcome === true
|
|
239
|
+
? undefined
|
|
240
|
+
: outcome;
|
|
241
|
+
resolve({
|
|
242
|
+
success: !error && outcome === true,
|
|
243
|
+
stdout: Buffer.concat(output.stdout).toString('utf8'),
|
|
244
|
+
stderr: Buffer.concat(output.stderr).toString('utf8'),
|
|
245
|
+
...(session ? { session } : {}),
|
|
246
|
+
...(error ? { error } : {}),
|
|
247
|
+
executionStopped: !cleanupError,
|
|
248
|
+
mayHaveLateEffects: pendingActions.length > 0 || uncertainEffects,
|
|
249
|
+
pendingActions,
|
|
250
|
+
});
|
|
251
|
+
})
|
|
252
|
+
.catch(reject);
|
|
253
|
+
});
|
|
254
|
+
send({ type: 'start', code, methods: METHODS, session });
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
client.disconnect();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -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('yuque', { command: 'read', input: { url: '...' } })。\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
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { serializePluginExecutionError } from './plugins/runtime/pluginExecutorProtocol.js';
|
|
2
|
+
export function serializeScriptError(error, fallback = 'SCRIPT_ERROR') {
|
|
3
|
+
const value = error;
|
|
4
|
+
const diagnostic = serializePluginExecutionError({
|
|
5
|
+
message: typeof value?.message === 'string' ? value.message : String(error),
|
|
6
|
+
code: typeof value?.code === 'string' ? value.code : fallback,
|
|
7
|
+
details: {
|
|
8
|
+
...value?.details,
|
|
9
|
+
...(value?.attempt !== undefined ? { attempt: value.attempt } : {}),
|
|
10
|
+
},
|
|
11
|
+
...(typeof value?.stage === 'string' ? { stage: value.stage } : {}),
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
code: diagnostic.code ?? fallback,
|
|
15
|
+
message: diagnostic.message,
|
|
16
|
+
...(diagnostic.details ? { details: diagnostic.details } : {}),
|
|
17
|
+
...(typeof diagnostic.metadata?.stage === 'string' ? { stage: diagnostic.metadata.stage } : {}),
|
|
18
|
+
...(diagnostic.details?.attempt !== undefined ? { attempt: diagnostic.details.attempt } : {}),
|
|
19
|
+
...(typeof value?.stack === 'string' ? { stack: value.stack.slice(0, 16384) } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getStructure } from './generated/data-structure.js';
|
|
2
|
+
import { serializeScriptError } from './scriptErrors.js';
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
let sequence = 0;
|
|
5
|
+
let session;
|
|
6
|
+
const call = (method, args) => new Promise((resolve, reject) => {
|
|
7
|
+
const id = ++sequence;
|
|
8
|
+
pending.set(id, { resolve, reject });
|
|
9
|
+
process.send({ type: 'call', id, method, args });
|
|
10
|
+
});
|
|
11
|
+
process.on('disconnect', () => process.exit(1));
|
|
12
|
+
process.on('message', async (message) => {
|
|
13
|
+
if (message.type === 'response') {
|
|
14
|
+
const entry = pending.get(message.id);
|
|
15
|
+
if (!entry)
|
|
16
|
+
return;
|
|
17
|
+
pending.delete(message.id);
|
|
18
|
+
session = message.session ?? session;
|
|
19
|
+
if (message.error)
|
|
20
|
+
entry.reject(Object.assign(new Error(message.error.message), message.error));
|
|
21
|
+
else
|
|
22
|
+
entry.resolve(message.result);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (message.type !== 'start')
|
|
26
|
+
return;
|
|
27
|
+
session = message.session;
|
|
28
|
+
const api = Object.fromEntries(message.methods.map((method) => [method, (...args) => call(method, args)]));
|
|
29
|
+
api.structure = getStructure;
|
|
30
|
+
Object.defineProperty(api, 'session', { get: () => session, enumerable: true });
|
|
31
|
+
Object.defineProperty(globalThis, 'mearl', { value: Object.freeze(api), configurable: true });
|
|
32
|
+
let failure;
|
|
33
|
+
try {
|
|
34
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
35
|
+
const fn = new AsyncFunction('mearl', '"use strict";\n' + message.code + '\n//# sourceURL=mearl-script.js');
|
|
36
|
+
await fn(api);
|
|
37
|
+
if (pending.size)
|
|
38
|
+
throw Object.assign(new Error('Script finished with unawaited SDK calls'), {
|
|
39
|
+
code: 'SCRIPT_PENDING_CALLS',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
failure = serializeScriptError(error);
|
|
44
|
+
}
|
|
45
|
+
await Promise.all([
|
|
46
|
+
new Promise(resolve => process.stdout.write('', resolve)),
|
|
47
|
+
new Promise(resolve => process.stderr.write('', resolve)),
|
|
48
|
+
]);
|
|
49
|
+
process.send(failure ? { type: 'error', error: failure } : { type: 'done' });
|
|
50
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BROWSER_COMMAND_ACTIONS } from './generated/browser-action-protocol.js';
|
|
2
|
+
/** Method names and signature exceptions shared by script dispatch and command help. */
|
|
3
|
+
export const SDK_METHOD_ACTIONS = new Map(Object.keys(BROWSER_COMMAND_ACTIONS).map(action => [
|
|
4
|
+
action.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
|
|
5
|
+
action,
|
|
6
|
+
]));
|
|
7
|
+
SDK_METHOD_ACTIONS.set('selectBrowser', 'browser_select_browser');
|
|
8
|
+
SDK_METHOD_ACTIONS.set('captureCheckpoint', 'tab_checkpoint');
|
|
9
|
+
export const SDK_OPTIONS_ONLY_METHODS = new Set(['browserList', 'tabList', 'getRules']);
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.2",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
+
"./script": {
|
|
10
|
+
"types": "./dist/script.d.ts",
|
|
11
|
+
"import": "./dist/script.js"
|
|
12
|
+
},
|
|
9
13
|
".": {
|
|
10
14
|
"types": "./dist/index.d.ts",
|
|
11
15
|
"import": "./dist/index.js"
|
|
@@ -63,14 +67,15 @@
|
|
|
63
67
|
"commander": "^15.0.0",
|
|
64
68
|
"fastest-levenshtein": "^1.0.16",
|
|
65
69
|
"ws": "^8.18.0",
|
|
66
|
-
"@mearl/cloud-types": "2.22.
|
|
67
|
-
"@mearl/
|
|
68
|
-
"@mearl/
|
|
69
|
-
"@mearl/
|
|
70
|
+
"@mearl/cloud-types": "2.22.2",
|
|
71
|
+
"@mearl/provider": "2.22.2",
|
|
72
|
+
"@mearl/agent-runtime": "2.22.2",
|
|
73
|
+
"@mearl/daemon-core": "2.22.2"
|
|
70
74
|
},
|
|
71
75
|
"devDependencies": {
|
|
72
76
|
"@types/node": "^24.9.1",
|
|
73
77
|
"@types/ws": "^8.18.1",
|
|
78
|
+
"esbuild": "^0.25.12",
|
|
74
79
|
"typescript": "^5.8.3"
|
|
75
80
|
},
|
|
76
81
|
"scripts": {
|