@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 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 会话历史中。带固定
@@ -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));
@@ -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;宽高为物理像素(CSS 尺寸 × DPR)。"];
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 组合;multipart 不手工设置 Content-Type,由 Mearl 生成 boundary。',
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
- { name: 'expression', type: 'string', required: true, description: '要执行的 JS 表达式' },
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
- if (!registering)
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
- app.command(command.signature, input => runPythonPluginCommand(entry, commandName, input, app), command.description);
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 [...commands.values()]
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)] : [...commands.values()];
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
- async run(positionals, payload, namedInput = {}) {
240
- if (payload !== undefined && positionals.length > 0) {
241
- throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数调用与 --payload 调用不能混用');
242
- }
243
- if (payload !== undefined && Object.keys(namedInput).length > 0) {
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
+ }
@@ -6,4 +6,5 @@ export interface BuiltPluginPageBundle {
6
6
  chunks: number;
7
7
  sources: Record<string, string>;
8
8
  }
9
+ export declare const PLUGIN_PAGE_RUNTIME_HASH: string;
9
10
  export declare function buildPluginPageBundle(entry: string, pageEntry: string): BuiltPluginPageBundle;