@mearl/client 2.20.1 → 2.20.3
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 +10 -0
- package/dist/actionTimeouts.js +1 -1
- package/dist/commands.d.ts +25 -5
- package/dist/commands.js +36 -6
- package/dist/generated-browser-action-protocol.d.ts +24 -1
- package/dist/generated-browser-action-protocol.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/pageEvalArguments.d.ts +3 -0
- package/dist/pageEvalArguments.js +47 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +108 -79
- package/dist/pluginPageBundle.d.ts +1 -0
- package/dist/pluginPageBundle.js +27 -62
- package/dist/pluginRegistry.d.ts +2 -0
- package/dist/pluginRegistry.js +14 -2
- package/dist/pythonPlugin.d.ts +1 -0
- package/dist/pythonPlugin.js +15 -3
- package/dist/socket.d.ts +7 -0
- package/dist/socket.js +74 -10
- package/dist/unified.d.ts +3 -5
- package/dist/unified.js +138 -15
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ mearl browser_release
|
|
|
76
76
|
|
|
77
77
|
多步骤任务首次 `tab_open` 或 `page_snapshot` 使用 `--session new`,响应在原结果旁附带 `session`,后续用其 `sessionId` 自动路由,无需额外创建/查询 action。Session 绑定一个浏览器与传输;页面首次使用自动关联,复用页面与新建页面分别记录。同一页面只允许一个会话控制,旧调用不能绕过占用;Session 不隔离 Cookie 或存储。
|
|
78
78
|
|
|
79
|
+
`sessionId` 是形如 `s_aB3d_E-f` 的短 ID。会话及其浏览器、传输映射保存在常驻控制宿主的内存中;新的 CLI 进程会内部查询本机和远端宿主完成路由,无需调用方保存或传递路由信息,也不写入会话映射文件。控制宿主退出后会话失效。
|
|
80
|
+
|
|
79
81
|
旧调用不必传 `--session`,现有动作、`run_actions` 批次和上传 `filePaths` 参数继续可用。活动会话存在时,旧调用不能操作其占用页面、执行浏览器级释放/关闭或修改共享设置;旧批次中的开页/关闭浏览器需改为独立调用。CDP Mock 按标签页生效,跨页使用同一 Mock 时需分别配置。
|
|
80
82
|
|
|
81
83
|
```bash
|
|
@@ -268,6 +270,14 @@ export default function register(app) {
|
|
|
268
270
|
}
|
|
269
271
|
```
|
|
270
272
|
|
|
273
|
+
`app.command(signature, handler, description?, examples?)` 注册 CLI、SDK 和 Agent 可调用的业务命令。
|
|
274
|
+
`app.toolboxCommand(signature, handler, description?, examples?)` 注册仅供工具页面调用的命令,参数和 handler 契约相同;
|
|
275
|
+
它不出现在 CLI 帮助中,CLI(包括 JSON payload)、SDK、MCP 及插件间的 `app.invoke` 都不能调用。
|
|
276
|
+
工具页面通过 `window.mearl.invoke(command, input)` 或 `window.mearl.invoke('host.operation.start', { command, input })` 执行;两类命令都必须显式列在 `toolbox.page.commands` 中才能供页面调用。
|
|
277
|
+
注册的 `toolboxCommand` 若没有列入该白名单,插件加载会失败。分块上传、分块读取和页面临时资源释放适合使用它;
|
|
278
|
+
对 Agent 有独立业务价值的查询、导入、压缩、比较等完整操作使用 `app.command`。
|
|
279
|
+
Python 入口也支持 `app.toolboxCommand(...)`,包括 `@app.toolboxCommand(..., description="...")` 装饰器。
|
|
280
|
+
|
|
271
281
|
声明 `agent` 后工具页可以把任务交给对话 Agent;`layout` 默认为 `drawer`,宿主会提供顶部入口。
|
|
272
282
|
页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace` / `inline`;
|
|
273
283
|
Agent 对话会保存在 Codex 会话历史中。带固定
|
package/dist/actionTimeouts.js
CHANGED
|
@@ -21,7 +21,7 @@ export function resolveObservedActionTimeoutSec(data) {
|
|
|
21
21
|
}
|
|
22
22
|
export function resolveActionTimeoutSec(action, data, fallback = 15) {
|
|
23
23
|
if (action === 'send_request') {
|
|
24
|
-
const transferTimeoutMs = Array.isArray(data.files) && data.files.length > 0
|
|
24
|
+
const transferTimeoutMs = data.bodyFile !== undefined || (Array.isArray(data.files) && data.files.length > 0)
|
|
25
25
|
? SEND_REQUEST_FILE_TRANSFER_TIMEOUT_MS
|
|
26
26
|
: 0;
|
|
27
27
|
return Math.max(fallback, Math.ceil(((data.timeout ?? 30000) + transferTimeoutMs + 5000) / 1000));
|
package/dist/commands.d.ts
CHANGED
|
@@ -305,7 +305,7 @@ export declare const COMMANDS: readonly [{
|
|
|
305
305
|
readonly examples: ["get_rules"];
|
|
306
306
|
}, {
|
|
307
307
|
readonly name: "send_request";
|
|
308
|
-
readonly description: "发送 HTTP 请求,自动携带浏览器 Cookie,支持 multipart
|
|
308
|
+
readonly description: "发送 HTTP 请求,自动携带浏览器 Cookie,支持 multipart 和原始二进制上传";
|
|
309
309
|
readonly params: [{
|
|
310
310
|
readonly name: "url";
|
|
311
311
|
readonly type: "string";
|
|
@@ -327,6 +327,10 @@ export declare const COMMANDS: readonly [{
|
|
|
327
327
|
readonly name: "body";
|
|
328
328
|
readonly type: "any";
|
|
329
329
|
readonly description: "请求体(传对象时自动 JSON 序列化)";
|
|
330
|
+
}, {
|
|
331
|
+
readonly name: "bodyFile";
|
|
332
|
+
readonly type: "object";
|
|
333
|
+
readonly description: "原始二进制请求体:filePath 或 base64 二选一,可选 contentType(默认 application/octet-stream);与 body/formData/files 互斥";
|
|
330
334
|
}, {
|
|
331
335
|
readonly name: "formData";
|
|
332
336
|
readonly type: "object";
|
|
@@ -349,9 +353,9 @@ export declare const COMMANDS: readonly [{
|
|
|
349
353
|
readonly type: "number";
|
|
350
354
|
readonly description: "超时毫秒数,默认 30000";
|
|
351
355
|
}];
|
|
352
|
-
readonly notes: readonly ["仅用于非 BUC 请求;BUC 接口使用 mearl buc,不手工传 Token、DPoP 或 Ticket。", "body 不能与 formData/files 组合;multipart 不手工设置 Content-Type,由 Mearl 生成 boundary。", "filePath 属于 native-host 所在机器;无法引用远端路径时用 base64。BUC 上传的路径属于插件执行宿主。"];
|
|
356
|
+
readonly notes: readonly ["仅用于非 BUC 请求;BUC 接口使用 mearl buc,不手工传 Token、DPoP 或 Ticket。", "body 不能与 formData/files 组合;bodyFile 以原始字节发送,不包 multipart boundary,不能用于 GET/HEAD。", "bodyFile.filePath 属于 native-host,最大 40 MiB;base64 解码后最大 7 MiB。multipart 不手工设置 Content-Type,由 Mearl 生成 boundary。", "filePath 属于 native-host 所在机器;无法引用远端路径时用 base64。BUC 上传的路径属于插件执行宿主。"];
|
|
353
357
|
readonly result: readonly ["返回 status、statusText、headers、body、bodyType、attachedCookieCount;bodyType 为 json/text/base64,二进制 body 需解码后保存。"];
|
|
354
|
-
readonly examples: ["send_request --payload '{\"url\":\"https://api.example.com/data\",\"method\":\"GET\"}'", "send_request --payload '{\"url\":\"https://api.example.com/upload\",\"method\":\"POST\",\"formData\":{\"folder\":\"assets\"},\"files\":[{\"fieldName\":\"file\",\"filePath\":\"/absolute/path/image.png\"}]}'"];
|
|
358
|
+
readonly examples: ["send_request --payload '{\"url\":\"https://api.example.com/data\",\"method\":\"GET\"}'", "send_request --payload '{\"url\":\"https://api.example.com/upload\",\"method\":\"POST\",\"formData\":{\"folder\":\"assets\"},\"files\":[{\"fieldName\":\"file\",\"filePath\":\"/absolute/path/image.png\"}]}'", "send_request --payload '{\"url\":\"https://api.example.com/upload\",\"method\":\"POST\",\"bodyFile\":{\"filePath\":\"/absolute/path/image.png\",\"contentType\":\"image/png\"}}'"];
|
|
355
359
|
}, {
|
|
356
360
|
readonly name: "send_mtop_request";
|
|
357
361
|
readonly description: "使用目标浏览器 Cookie 发起 mtop 请求,自动处理签名和 token";
|
|
@@ -718,7 +722,19 @@ export declare const COMMANDS: readonly [{
|
|
|
718
722
|
readonly name: "expression";
|
|
719
723
|
readonly type: "string";
|
|
720
724
|
readonly required: true;
|
|
721
|
-
readonly description: "要执行的 JS
|
|
725
|
+
readonly description: "要执行的 JS 表达式;传 args 时须求值为函数";
|
|
726
|
+
}, {
|
|
727
|
+
readonly name: "args";
|
|
728
|
+
readonly type: "unknown[]";
|
|
729
|
+
readonly description: "传给页面函数的 JSON 参数;SDK/CLI 自动处理大参数分块,保持指定执行环境";
|
|
730
|
+
}, {
|
|
731
|
+
readonly name: "executionWorld";
|
|
732
|
+
readonly type: "string";
|
|
733
|
+
readonly description: "main(默认)读取网页 JS;isolated 使用命名隔离环境,扩展连接无需 debugger";
|
|
734
|
+
}, {
|
|
735
|
+
readonly name: "worldId";
|
|
736
|
+
readonly type: "string";
|
|
737
|
+
readonly description: "isolated 必填,小写字母开头,允许数字和连字符,最长 64 字符";
|
|
722
738
|
}, {
|
|
723
739
|
readonly name: "awaitPromise";
|
|
724
740
|
readonly type: "boolean";
|
|
@@ -949,6 +965,10 @@ export declare const COMMANDS: readonly [{
|
|
|
949
965
|
readonly name: "quality";
|
|
950
966
|
readonly type: "number";
|
|
951
967
|
readonly description: "JPEG 压缩质量(0-100),默认 80";
|
|
968
|
+
}, {
|
|
969
|
+
readonly name: "captureMode";
|
|
970
|
+
readonly type: "\"cdp\" | \"visible\"";
|
|
971
|
+
readonly description: "默认 cdp;visible 使用活动标签页截图,普通网页无需 debugger;设备模拟复用模拟会话并截取业务视口,不支持 selector";
|
|
952
972
|
}, {
|
|
953
973
|
readonly name: "tabId";
|
|
954
974
|
readonly type: "number";
|
|
@@ -956,7 +976,7 @@ export declare const COMMANDS: readonly [{
|
|
|
956
976
|
readonly description: "目标标签页 ID";
|
|
957
977
|
}];
|
|
958
978
|
readonly notes: readonly ["使用 --output <path> 保存图片;省略时 stdout 返回含 base64 data 的 JSON,避免直接将其加载为文本。"];
|
|
959
|
-
readonly result: readonly ["meta 包含 format、width、height、size、url、title
|
|
979
|
+
readonly result: readonly ["meta 包含 format、width、height、size、url、title;宽高为实际截图的物理像素。"];
|
|
960
980
|
readonly examples: ["page_screenshot --payload '{\"tabId\":12345}' --output ./screenshot.png", "page_screenshot --payload '{\"tabId\":12345,\"selector\":\".product-card\"}' --output ./card.png"];
|
|
961
981
|
}, {
|
|
962
982
|
readonly name: "set_device_emulation";
|
package/dist/commands.js
CHANGED
|
@@ -306,13 +306,18 @@ export const COMMANDS = [
|
|
|
306
306
|
// ── 网络代理 ──────────────────────────────────────────────────────────────
|
|
307
307
|
{
|
|
308
308
|
name: 'send_request',
|
|
309
|
-
description: '发送 HTTP 请求,自动携带浏览器 Cookie,支持 multipart
|
|
309
|
+
description: '发送 HTTP 请求,自动携带浏览器 Cookie,支持 multipart 和原始二进制上传',
|
|
310
310
|
params: [
|
|
311
311
|
{ name: 'url', type: 'string', required: true, description: '完整目标 URL' },
|
|
312
312
|
{ name: 'method', type: 'string', description: 'HTTP 方法,默认 GET' },
|
|
313
313
|
{ name: 'headers', type: 'object', description: '自定义请求头' },
|
|
314
314
|
{ name: 'params', type: 'object', description: 'URL 查询参数' },
|
|
315
315
|
{ name: 'body', type: 'any', description: '请求体(传对象时自动 JSON 序列化)' },
|
|
316
|
+
{
|
|
317
|
+
name: 'bodyFile',
|
|
318
|
+
type: 'object',
|
|
319
|
+
description: '原始二进制请求体:filePath 或 base64 二选一,可选 contentType(默认 application/octet-stream);与 body/formData/files 互斥',
|
|
320
|
+
},
|
|
316
321
|
{
|
|
317
322
|
name: 'formData',
|
|
318
323
|
type: 'object',
|
|
@@ -338,7 +343,8 @@ export const COMMANDS = [
|
|
|
338
343
|
],
|
|
339
344
|
notes: [
|
|
340
345
|
'仅用于非 BUC 请求;BUC 接口使用 mearl buc,不手工传 Token、DPoP 或 Ticket。',
|
|
341
|
-
'body 不能与 formData/files 组合;
|
|
346
|
+
'body 不能与 formData/files 组合;bodyFile 以原始字节发送,不包 multipart boundary,不能用于 GET/HEAD。',
|
|
347
|
+
'bodyFile.filePath 属于 native-host,最大 40 MiB;base64 解码后最大 7 MiB。multipart 不手工设置 Content-Type,由 Mearl 生成 boundary。',
|
|
342
348
|
'filePath 属于 native-host 所在机器;无法引用远端路径时用 base64。BUC 上传的路径属于插件执行宿主。',
|
|
343
349
|
],
|
|
344
350
|
result: [
|
|
@@ -347,6 +353,7 @@ export const COMMANDS = [
|
|
|
347
353
|
examples: [
|
|
348
354
|
`send_request --payload '{"url":"https://api.example.com/data","method":"GET"}'`,
|
|
349
355
|
`send_request --payload '{"url":"https://api.example.com/upload","method":"POST","formData":{"folder":"assets"},"files":[{"fieldName":"file","filePath":"/absolute/path/image.png"}]}'`,
|
|
356
|
+
`send_request --payload '{"url":"https://api.example.com/upload","method":"POST","bodyFile":{"filePath":"/absolute/path/image.png","contentType":"image/png"}}'`,
|
|
350
357
|
],
|
|
351
358
|
},
|
|
352
359
|
{
|
|
@@ -780,7 +787,27 @@ export const COMMANDS = [
|
|
|
780
787
|
name: 'page_eval',
|
|
781
788
|
description: '在页面上下文中执行任意 JavaScript 表达式',
|
|
782
789
|
params: [
|
|
783
|
-
{
|
|
790
|
+
{
|
|
791
|
+
name: 'expression',
|
|
792
|
+
type: 'string',
|
|
793
|
+
required: true,
|
|
794
|
+
description: '要执行的 JS 表达式;传 args 时须求值为函数',
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: 'args',
|
|
798
|
+
type: 'unknown[]',
|
|
799
|
+
description: '传给页面函数的 JSON 参数;SDK/CLI 自动处理大参数分块,保持指定执行环境',
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
name: 'executionWorld',
|
|
803
|
+
type: 'string',
|
|
804
|
+
description: 'main(默认)读取网页 JS;isolated 使用命名隔离环境,扩展连接无需 debugger',
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
name: 'worldId',
|
|
808
|
+
type: 'string',
|
|
809
|
+
description: 'isolated 必填,小写字母开头,允许数字和连字符,最长 64 字符',
|
|
810
|
+
},
|
|
784
811
|
{
|
|
785
812
|
name: 'awaitPromise',
|
|
786
813
|
type: 'boolean',
|
|
@@ -1042,14 +1069,17 @@ export const COMMANDS = [
|
|
|
1042
1069
|
{ name: 'selector', type: 'string', description: 'CSS 选择器,提供后只截取该元素区域' },
|
|
1043
1070
|
{ name: 'format', type: '"png" | "jpeg"', description: '图片格式,默认 png' },
|
|
1044
1071
|
{ name: 'quality', type: 'number', description: 'JPEG 压缩质量(0-100),默认 80' },
|
|
1072
|
+
{
|
|
1073
|
+
name: 'captureMode',
|
|
1074
|
+
type: '"cdp" | "visible"',
|
|
1075
|
+
description: '默认 cdp;visible 使用活动标签页截图,普通网页无需 debugger;设备模拟复用模拟会话并截取业务视口,不支持 selector',
|
|
1076
|
+
},
|
|
1045
1077
|
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
1046
1078
|
],
|
|
1047
1079
|
notes: [
|
|
1048
1080
|
'使用 --output <path> 保存图片;省略时 stdout 返回含 base64 data 的 JSON,避免直接将其加载为文本。',
|
|
1049
1081
|
],
|
|
1050
|
-
result: [
|
|
1051
|
-
'meta 包含 format、width、height、size、url、title;宽高为物理像素(CSS 尺寸 × DPR)。',
|
|
1052
|
-
],
|
|
1082
|
+
result: ['meta 包含 format、width、height、size、url、title;宽高为实际截图的物理像素。'],
|
|
1053
1083
|
examples: [
|
|
1054
1084
|
`page_screenshot --payload '{"tabId":12345}' --output ./screenshot.png`,
|
|
1055
1085
|
`page_screenshot --payload '{"tabId":12345,"selector":".product-card"}' --output ./card.png`,
|
|
@@ -83,12 +83,23 @@ export type SendRequestFile = SendRequestFileMetadata & ({
|
|
|
83
83
|
/** Multipart filename for the Base64 payload. */
|
|
84
84
|
filename: string;
|
|
85
85
|
});
|
|
86
|
+
export type SendRequestBinaryBody = {
|
|
87
|
+
contentType?: string;
|
|
88
|
+
} & ({
|
|
89
|
+
filePath: string;
|
|
90
|
+
base64?: never;
|
|
91
|
+
} | {
|
|
92
|
+
filePath?: never;
|
|
93
|
+
base64: string;
|
|
94
|
+
});
|
|
86
95
|
export interface SendRequestParams {
|
|
87
96
|
url: string;
|
|
88
97
|
method?: string;
|
|
89
98
|
headers?: Record<string, string>;
|
|
90
99
|
params?: Record<string, string>;
|
|
91
100
|
body?: unknown;
|
|
101
|
+
/** Raw bytes, mutually exclusive with body, formData and files. */
|
|
102
|
+
bodyFile?: SendRequestBinaryBody;
|
|
92
103
|
formData?: Record<string, SendRequestFormValue>;
|
|
93
104
|
files?: SendRequestFile[];
|
|
94
105
|
withCookies?: boolean;
|
|
@@ -426,6 +437,7 @@ export interface PageScreenshotParams {
|
|
|
426
437
|
selector?: string;
|
|
427
438
|
format?: 'png' | 'jpeg';
|
|
428
439
|
quality?: number;
|
|
440
|
+
captureMode?: 'cdp' | 'visible';
|
|
429
441
|
}
|
|
430
442
|
export declare const BROWSER_DEVICE_PRESET_NAMES: readonly ["iphone-duo-folded", "iphone-duo-unfolded", "iphone-17-pro", "iphone-17e", "pixel-11", "ipad-mini-a17-pro", "galaxy-s26", "pura-x-max", "iphone-15-pro", "iphone-se", "pixel-8", "ipad-mini", "galaxy-s23"];
|
|
431
443
|
export type BrowserDevicePresetName = (typeof BROWSER_DEVICE_PRESET_NAMES)[number];
|
|
@@ -604,11 +616,22 @@ export interface PageHoverParams {
|
|
|
604
616
|
export interface PageEvalParams {
|
|
605
617
|
tabId: number;
|
|
606
618
|
expression: string;
|
|
619
|
+
/** When provided, expression must evaluate to a function called with these JSON arguments. */
|
|
620
|
+
args?: unknown[];
|
|
607
621
|
frameId?: string;
|
|
608
622
|
awaitPromise?: boolean;
|
|
609
|
-
observe?: PageObservationWaitOptions;
|
|
623
|
+
observe?: PageObservationWaitOptions | false;
|
|
610
624
|
diagnostics?: PageDiagnosticsSetting;
|
|
625
|
+
executionWorld?: 'main' | 'isolated';
|
|
626
|
+
worldId?: string;
|
|
627
|
+
}
|
|
628
|
+
export declare function resolvePageEvalExpression(params: Pick<PageEvalParams, 'expression' | 'args'>): string;
|
|
629
|
+
export interface PageUserScriptParams {
|
|
630
|
+
tabId: number;
|
|
631
|
+
worldId: string;
|
|
632
|
+
code: string;
|
|
611
633
|
}
|
|
634
|
+
export declare function validatePageUserScriptParams(params: PageUserScriptParams): void;
|
|
612
635
|
export interface PageSnapshotQuery {
|
|
613
636
|
text?: string;
|
|
614
637
|
role?: string;
|
|
@@ -50,6 +50,26 @@ export const APP_PROFILE_NAMES = [
|
|
|
50
50
|
'didi',
|
|
51
51
|
'easybike',
|
|
52
52
|
];
|
|
53
|
+
export function resolvePageEvalExpression(params) {
|
|
54
|
+
if (typeof params.expression !== 'string' || !params.expression.trim())
|
|
55
|
+
throw new Error('Invalid expression');
|
|
56
|
+
if (params.args === undefined)
|
|
57
|
+
return params.expression;
|
|
58
|
+
if (!Array.isArray(params.args))
|
|
59
|
+
throw new Error('page_eval args must be an array');
|
|
60
|
+
return `(${params.expression})(...JSON.parse(${JSON.stringify(JSON.stringify(params.args))}))`;
|
|
61
|
+
}
|
|
62
|
+
export function validatePageUserScriptParams(params) {
|
|
63
|
+
if (!Number.isSafeInteger(params?.tabId) || params.tabId < 0) {
|
|
64
|
+
throw new Error('Invalid target tab');
|
|
65
|
+
}
|
|
66
|
+
if (typeof params.worldId !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(params.worldId)) {
|
|
67
|
+
throw new Error('Invalid user script worldId');
|
|
68
|
+
}
|
|
69
|
+
if (typeof params.code !== 'string' || !params.code.trim() || params.code.length > 256 * 1024) {
|
|
70
|
+
throw new Error('User script code must be non-empty and within 256 KiB characters');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
53
73
|
export const TAB_ID_REQUIRED_ACTIONS = [
|
|
54
74
|
'tab_checkpoint',
|
|
55
75
|
'capture_checkpoint',
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invok
|
|
|
8
8
|
export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
|
|
9
9
|
export type { PluginInvocation, PluginInvokeOptions, PluginProgress, StructuredPluginProgress, } from './plugin.js';
|
|
10
10
|
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';
|
|
11
|
-
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, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from './generated-browser-action-protocol.js';
|
|
11
|
+
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';
|
|
12
12
|
import { type UnifiedInvokeOptions } from './unified.js';
|
|
13
13
|
import type * as Protocol from './generated-browser-action-protocol.js';
|
|
14
14
|
export declare function tabCheckpoint(params: Protocol.CaptureCheckpointParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_checkpoint'>>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type PageEvalParams } from './generated-browser-action-protocol.js';
|
|
2
|
+
/** Serialize page_eval arguments within transport limits while retaining the caller's execution world. */
|
|
3
|
+
export declare function invokePageEvalWithArgs(invoke: (payload: Record<string, unknown>, transfer?: boolean) => Promise<any>, params: PageEvalParams): Promise<any>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { validatePageUserScriptParams, resolvePageEvalExpression, } from './generated-browser-action-protocol.js';
|
|
3
|
+
const CHUNK_CHARACTERS = 48 * 1024;
|
|
4
|
+
/** Serialize page_eval arguments within transport limits while retaining the caller's execution world. */
|
|
5
|
+
export async function invokePageEvalWithArgs(invoke, params) {
|
|
6
|
+
if (!Number.isSafeInteger(params.tabId) || params.tabId < 0)
|
|
7
|
+
throw new Error('Invalid target tab');
|
|
8
|
+
resolvePageEvalExpression({ expression: params.expression, args: [] });
|
|
9
|
+
if (!Array.isArray(params.args))
|
|
10
|
+
throw new Error('page_eval args must be an array');
|
|
11
|
+
if (params.executionWorld !== undefined && !['main', 'isolated'].includes(params.executionWorld))
|
|
12
|
+
throw new Error('Invalid page executionWorld');
|
|
13
|
+
if (params.executionWorld === 'isolated')
|
|
14
|
+
validatePageUserScriptParams({
|
|
15
|
+
tabId: params.tabId,
|
|
16
|
+
worldId: params.worldId ?? '',
|
|
17
|
+
code: params.expression,
|
|
18
|
+
});
|
|
19
|
+
const args = JSON.stringify(params.args);
|
|
20
|
+
if (args.length > 64 * 1024 * 1024)
|
|
21
|
+
throw new Error('Page script arguments must fit within 64 MiB characters');
|
|
22
|
+
const { args: _args, ...request } = params;
|
|
23
|
+
const evaluate = async (expression, transfer = false) => {
|
|
24
|
+
const payload = {
|
|
25
|
+
...request,
|
|
26
|
+
...(transfer ? { observe: false, diagnostics: false } : {}),
|
|
27
|
+
expression,
|
|
28
|
+
};
|
|
29
|
+
const result = await (transfer ? invoke(payload, true) : invoke(payload));
|
|
30
|
+
if (transfer && result?.action?.success === false)
|
|
31
|
+
throw new Error(result.action.error || 'page_eval failed');
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
if (args.length + params.expression.length <= CHUNK_CHARACTERS) {
|
|
35
|
+
return evaluate(resolvePageEvalExpression(params));
|
|
36
|
+
}
|
|
37
|
+
const key = JSON.stringify('__mearlPageScriptTransfer_' + randomUUID());
|
|
38
|
+
try {
|
|
39
|
+
for (let offset = 0; offset < args.length; offset += CHUNK_CHARACTERS) {
|
|
40
|
+
await evaluate(`void (window[${key}] = (window[${key}] || '') + ${JSON.stringify(args.slice(offset, offset + CHUNK_CHARACTERS))})`, true);
|
|
41
|
+
}
|
|
42
|
+
return await evaluate(`(() => { const args = JSON.parse(window[${key}]); delete window[${key}]; return (${params.expression})(...args); })()`);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await evaluate(`delete window[${key}]`, true).catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { invokePageEvalWithArgs } from './pageEvalArguments.js';
|
|
1
2
|
import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
|
|
2
3
|
import { type PluginToolboxContribution, type PluginRegistration } from './pluginRegistry.js';
|
|
3
4
|
import type { UnifiedInvokeOptions } from './unified.js';
|
|
@@ -24,6 +25,8 @@ export interface PluginApp {
|
|
|
24
25
|
readonly options: Readonly<PluginOptions>;
|
|
25
26
|
/** Examples omit the `mearl <plugin>` prefix, which help adds when displayed. */
|
|
26
27
|
command(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
|
|
28
|
+
/** Registers a page-only command, also declared in toolbox.page.commands. */
|
|
29
|
+
toolboxCommand(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
|
|
27
30
|
invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
28
31
|
progress(progress: PluginProgress): void;
|
|
29
32
|
}
|
|
@@ -39,6 +42,7 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invo
|
|
|
39
42
|
commandHelp(namespace: string): string;
|
|
40
43
|
help(namespace: string, name?: string): string;
|
|
41
44
|
run(positionals: string[], payload?: unknown, namedInput?: Record<string, unknown>): Promise<unknown>;
|
|
45
|
+
runToolbox(invocation: PluginInvocation): Promise<unknown>;
|
|
42
46
|
}>;
|
|
43
47
|
export declare function loadRegisteredPlugin(name: string, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<{
|
|
44
48
|
name: string | undefined;
|
|
@@ -48,5 +52,8 @@ export declare function loadRegisteredPlugin(name: string, context: PluginExecut
|
|
|
48
52
|
commandHelp(namespace: string): string;
|
|
49
53
|
help(namespace: string, name?: string): string;
|
|
50
54
|
run(positionals: string[], payload?: unknown, namedInput?: Record<string, unknown>): Promise<unknown>;
|
|
55
|
+
runToolbox(invocation: PluginInvocation): Promise<unknown>;
|
|
51
56
|
}>;
|
|
52
57
|
export declare function invokeRegisteredPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
|
|
58
|
+
/** Entry point for the native toolbox service; nested plugin calls retain public-command access. */
|
|
59
|
+
export declare function invokeRegisteredToolboxPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
|
package/dist/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { invokePageEvalWithArgs } from './pageEvalArguments.js';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, extname, resolve } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
@@ -133,31 +134,33 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
133
134
|
let name;
|
|
134
135
|
let description;
|
|
135
136
|
let toolbox;
|
|
137
|
+
const registerCommand = (signature, handler, description = '', examples = [], toolboxOnly = false) => {
|
|
138
|
+
if (!registering)
|
|
139
|
+
throw new Error('命令只能在入口注册阶段声明');
|
|
140
|
+
const [name, ...args] = signature.trim().split(/\s+/);
|
|
141
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name) ||
|
|
142
|
+
commands.has(name) ||
|
|
143
|
+
typeof handler !== 'function' ||
|
|
144
|
+
!Array.isArray(examples) ||
|
|
145
|
+
examples.some((example) => typeof example !== 'string' || !example.trim())) {
|
|
146
|
+
throw new Error(`无效或重复的命令: ${signature}`);
|
|
147
|
+
}
|
|
148
|
+
const command = new Command(name).description(description);
|
|
149
|
+
for (const argument of args) {
|
|
150
|
+
if (!/^(?:<[a-zA-Z][\w-]*(?:\.\.\.)?>|\[[a-zA-Z][\w-]*(?:\.\.\.)?\])$/.test(argument)) {
|
|
151
|
+
throw new Error(`无效的位置参数: ${argument}`);
|
|
152
|
+
}
|
|
153
|
+
command.argument(argument);
|
|
154
|
+
}
|
|
155
|
+
const names = command.registeredArguments.map(argument => argument.name());
|
|
156
|
+
if (new Set(names).size !== names.length)
|
|
157
|
+
throw new Error(`参数名称重复: ${signature}`);
|
|
158
|
+
commands.set(name, { command, signature, handler, examples: [...examples], toolboxOnly });
|
|
159
|
+
};
|
|
136
160
|
const app = Object.freeze({
|
|
137
161
|
options: Object.freeze({ ...context.options }),
|
|
138
|
-
command(signature, handler, description = '', examples = [])
|
|
139
|
-
|
|
140
|
-
throw new Error('命令只能在入口注册阶段声明');
|
|
141
|
-
const [name, ...args] = signature.trim().split(/\s+/);
|
|
142
|
-
if (!/^[a-z][a-z0-9-]*$/.test(name) ||
|
|
143
|
-
commands.has(name) ||
|
|
144
|
-
typeof handler !== 'function' ||
|
|
145
|
-
!Array.isArray(examples) ||
|
|
146
|
-
examples.some((example) => typeof example !== 'string' || !example.trim())) {
|
|
147
|
-
throw new Error(`无效或重复的命令: ${signature}`);
|
|
148
|
-
}
|
|
149
|
-
const command = new Command(name).description(description);
|
|
150
|
-
for (const argument of args) {
|
|
151
|
-
if (!/^(?:<[a-zA-Z][\w-]*(?:\.\.\.)?>|\[[a-zA-Z][\w-]*(?:\.\.\.)?\])$/.test(argument)) {
|
|
152
|
-
throw new Error(`无效的位置参数: ${argument}`);
|
|
153
|
-
}
|
|
154
|
-
command.argument(argument);
|
|
155
|
-
}
|
|
156
|
-
const names = command.registeredArguments.map(argument => argument.name());
|
|
157
|
-
if (new Set(names).size !== names.length)
|
|
158
|
-
throw new Error(`参数名称重复: ${signature}`);
|
|
159
|
-
commands.set(name, { command, signature, handler, examples: [...examples] });
|
|
160
|
-
},
|
|
162
|
+
command: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples),
|
|
163
|
+
toolboxCommand: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples, true),
|
|
161
164
|
invoke(action, payload = {}, options = {}) {
|
|
162
165
|
if (!executing)
|
|
163
166
|
throw new Error('app.invoke 只能在命令执行期间调用');
|
|
@@ -177,7 +180,8 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
177
180
|
toolbox = normalizePluginToolboxContribution(descriptor.toolbox);
|
|
178
181
|
for (const command of descriptor.commands) {
|
|
179
182
|
const commandName = command.signature.trim().split(/\s+/)[0];
|
|
180
|
-
|
|
183
|
+
const register = command.toolboxOnly ? app.toolboxCommand : app.command;
|
|
184
|
+
register(command.signature, input => runPythonPluginCommand(entry, commandName, input, app), command.description);
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
else {
|
|
@@ -198,6 +202,11 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
198
202
|
if (commands.size === 0)
|
|
199
203
|
throw new Error('入口未注册任何命令');
|
|
200
204
|
const toolboxCommands = toolbox?.page.commands ?? [];
|
|
205
|
+
for (const [name, command] of commands) {
|
|
206
|
+
if (command.toolboxOnly && !toolboxCommands.includes(name)) {
|
|
207
|
+
throw new Error(`toolboxCommand 未声明在 toolbox.page.commands 中: ${name}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
201
210
|
for (const command of toolboxCommands) {
|
|
202
211
|
if (!commands.has(command))
|
|
203
212
|
throw new Error(`toolbox 命令未注册: ${command}`);
|
|
@@ -209,24 +218,86 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
209
218
|
finally {
|
|
210
219
|
registering = false;
|
|
211
220
|
}
|
|
212
|
-
const find = (name) => {
|
|
221
|
+
const find = (name, toolboxPage = false) => {
|
|
213
222
|
const command = commands.get(name);
|
|
214
223
|
if (!command)
|
|
215
224
|
throw new PluginError('PLUGIN_COMMAND_NOT_FOUND', `未知子命令: ${name}`);
|
|
225
|
+
if (toolboxPage) {
|
|
226
|
+
if (!toolbox?.page.commands.includes(name)) {
|
|
227
|
+
throw new PluginError('PLUGIN_COMMAND_NOT_ALLOWED', `工具页面不允许调用命令: ${name}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (command.toolboxOnly) {
|
|
231
|
+
throw new PluginError('PLUGIN_COMMAND_TOOLBOX_ONLY', `命令仅供工具页面调用: ${name}`);
|
|
232
|
+
}
|
|
216
233
|
return command;
|
|
217
234
|
};
|
|
235
|
+
const publicCommands = () => [...commands.values()].filter(command => !command.toolboxOnly);
|
|
236
|
+
const run = async (positionals, payload, namedInput = {}, toolboxPage = false) => {
|
|
237
|
+
if (payload !== undefined && positionals.length > 0) {
|
|
238
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数调用与 --payload 调用不能混用');
|
|
239
|
+
}
|
|
240
|
+
if (payload !== undefined && Object.keys(namedInput).length > 0) {
|
|
241
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '插件业务选项与 --payload 调用不能混用');
|
|
242
|
+
}
|
|
243
|
+
if (payload !== undefined &&
|
|
244
|
+
(!isObject(payload) || typeof payload.command !== 'string' || !isObject(payload.input))) {
|
|
245
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
|
|
246
|
+
}
|
|
247
|
+
const envelope = payload;
|
|
248
|
+
const { command, handler } = find(envelope?.command ?? positionals[0] ?? '', toolboxPage);
|
|
249
|
+
const positionalInput = envelope
|
|
250
|
+
? { ...envelope.input }
|
|
251
|
+
: Object.fromEntries(command.registeredArguments
|
|
252
|
+
.map((argument, index) => [
|
|
253
|
+
argument.name(),
|
|
254
|
+
argument.variadic ? positionals.slice(index + 1) : positionals[index + 1],
|
|
255
|
+
])
|
|
256
|
+
.filter(([, value]) => value !== undefined));
|
|
257
|
+
for (const name of Object.keys(namedInput)) {
|
|
258
|
+
if (Object.hasOwn(positionalInput, name)) {
|
|
259
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `参数不能重复传入: ${name}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const input = { ...positionalInput, ...namedInput };
|
|
263
|
+
if (!envelope &&
|
|
264
|
+
!command.registeredArguments.some(argument => argument.variadic) &&
|
|
265
|
+
positionals.length - 1 > command.registeredArguments.length) {
|
|
266
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
|
|
267
|
+
}
|
|
268
|
+
for (const argument of command.registeredArguments) {
|
|
269
|
+
const value = Object.hasOwn(input, argument.name()) ? input[argument.name()] : undefined;
|
|
270
|
+
if (argument.required &&
|
|
271
|
+
(value === undefined ||
|
|
272
|
+
value === null ||
|
|
273
|
+
value === '' ||
|
|
274
|
+
(Array.isArray(value) && value.length === 0))) {
|
|
275
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `缺少必填参数: ${argument.name()}`);
|
|
276
|
+
}
|
|
277
|
+
if (argument.variadic && value !== undefined && !Array.isArray(value)) {
|
|
278
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `${argument.name()} 必须为数组`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
executing = true;
|
|
282
|
+
try {
|
|
283
|
+
return await handler(input);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
executing = false;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
218
289
|
return {
|
|
219
290
|
name,
|
|
220
291
|
description,
|
|
221
292
|
toolbox,
|
|
222
293
|
version: readPluginVersion(entry),
|
|
223
294
|
commandHelp(namespace) {
|
|
224
|
-
return
|
|
295
|
+
return publicCommands()
|
|
225
296
|
.map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`)
|
|
226
297
|
.join('\n');
|
|
227
298
|
},
|
|
228
299
|
help(namespace, name) {
|
|
229
|
-
const selected = name ? [find(name)] :
|
|
300
|
+
const selected = name ? [find(name)] : publicCommands();
|
|
230
301
|
const examples = selected.flatMap(({ examples }) => examples);
|
|
231
302
|
return [
|
|
232
303
|
'用法:',
|
|
@@ -236,58 +307,11 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
236
307
|
: []),
|
|
237
308
|
].join('\n');
|
|
238
309
|
},
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
throw new PluginError('PLUGIN_INVALID_INPUT', '插件业务选项与 --payload 调用不能混用');
|
|
245
|
-
}
|
|
246
|
-
if (payload !== undefined &&
|
|
247
|
-
(!isObject(payload) || typeof payload.command !== 'string' || !isObject(payload.input))) {
|
|
248
|
-
throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
|
|
249
|
-
}
|
|
250
|
-
const envelope = payload;
|
|
251
|
-
const { command, handler } = find(envelope?.command ?? positionals[0] ?? '');
|
|
252
|
-
const positionalInput = envelope
|
|
253
|
-
? { ...envelope.input }
|
|
254
|
-
: Object.fromEntries(command.registeredArguments
|
|
255
|
-
.map((argument, index) => [
|
|
256
|
-
argument.name(),
|
|
257
|
-
argument.variadic ? positionals.slice(index + 1) : positionals[index + 1],
|
|
258
|
-
])
|
|
259
|
-
.filter(([, value]) => value !== undefined));
|
|
260
|
-
for (const name of Object.keys(namedInput)) {
|
|
261
|
-
if (Object.hasOwn(positionalInput, name)) {
|
|
262
|
-
throw new PluginError('PLUGIN_INVALID_INPUT', `参数不能重复传入: ${name}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
const input = { ...positionalInput, ...namedInput };
|
|
266
|
-
if (!envelope &&
|
|
267
|
-
!command.registeredArguments.some(argument => argument.variadic) &&
|
|
268
|
-
positionals.length - 1 > command.registeredArguments.length) {
|
|
269
|
-
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
|
|
270
|
-
}
|
|
271
|
-
for (const argument of command.registeredArguments) {
|
|
272
|
-
const value = Object.hasOwn(input, argument.name()) ? input[argument.name()] : undefined;
|
|
273
|
-
if (argument.required &&
|
|
274
|
-
(value === undefined ||
|
|
275
|
-
value === null ||
|
|
276
|
-
value === '' ||
|
|
277
|
-
(Array.isArray(value) && value.length === 0))) {
|
|
278
|
-
throw new PluginError('PLUGIN_INVALID_INPUT', `缺少必填参数: ${argument.name()}`);
|
|
279
|
-
}
|
|
280
|
-
if (argument.variadic && value !== undefined && !Array.isArray(value)) {
|
|
281
|
-
throw new PluginError('PLUGIN_INVALID_INPUT', `${argument.name()} 必须为数组`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
executing = true;
|
|
285
|
-
try {
|
|
286
|
-
return await handler(input);
|
|
287
|
-
}
|
|
288
|
-
finally {
|
|
289
|
-
executing = false;
|
|
290
|
-
}
|
|
310
|
+
run(positionals, payload, namedInput = {}) {
|
|
311
|
+
return run(positionals, payload, namedInput);
|
|
312
|
+
},
|
|
313
|
+
runToolbox(invocation) {
|
|
314
|
+
return run([], invocation, {}, true);
|
|
291
315
|
},
|
|
292
316
|
};
|
|
293
317
|
}
|
|
@@ -348,3 +372,8 @@ async function invokeRegisteredPluginWithState(name, invocation, context, option
|
|
|
348
372
|
export function invokeRegisteredPlugin(name, invocation, context, options = {}) {
|
|
349
373
|
return invokeRegisteredPluginWithState(name, invocation, context, options);
|
|
350
374
|
}
|
|
375
|
+
/** Entry point for the native toolbox service; nested plugin calls retain public-command access. */
|
|
376
|
+
export async function invokeRegisteredToolboxPlugin(name, invocation, context, options = {}) {
|
|
377
|
+
const plugin = await loadRegisteredPluginWithState(name, context, options);
|
|
378
|
+
return plugin.runToolbox(invocation);
|
|
379
|
+
}
|