@mearl/client 2.20.1 → 2.20.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 +8 -0
- package/dist/actionTimeouts.js +1 -1
- package/dist/commands.d.ts +7 -3
- package/dist/commands.js +9 -2
- package/dist/generated-browser-action-protocol.d.ts +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +107 -79
- package/dist/pluginPageBundle.d.ts +1 -0
- package/dist/pluginPageBundle.js +16 -1
- 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/package.json +3 -3
package/README.md
CHANGED
|
@@ -268,6 +268,14 @@ export default function register(app) {
|
|
|
268
268
|
}
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
+
`app.command(signature, handler, description?, examples?)` 注册 CLI、SDK 和 Agent 可调用的业务命令。
|
|
272
|
+
`app.toolboxCommand(signature, handler, description?, examples?)` 注册仅供工具页面调用的命令,参数和 handler 契约相同;
|
|
273
|
+
它不出现在 CLI 帮助中,CLI(包括 JSON payload)、SDK、MCP 及插件间的 `app.invoke` 都不能调用。
|
|
274
|
+
工具页面通过 `window.mearl.invoke` 或 `window.mearl.start` 执行;两类命令都必须显式列在 `toolbox.page.commands` 中才能供页面调用。
|
|
275
|
+
注册的 `toolboxCommand` 若没有列入该白名单,插件加载会失败。分块上传、分块读取和页面临时资源释放适合使用它;
|
|
276
|
+
对 Agent 有独立业务价值的查询、导入、压缩、比较等完整操作使用 `app.command`。
|
|
277
|
+
Python 入口也支持 `app.toolboxCommand(...)`,包括 `@app.toolboxCommand(..., description="...")` 装饰器。
|
|
278
|
+
|
|
271
279
|
声明 `agent` 后工具页可以把任务交给对话 Agent;`layout` 默认为 `drawer`,宿主会提供顶部入口。
|
|
272
280
|
页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace` / `inline`;
|
|
273
281
|
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";
|
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
|
{
|
|
@@ -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;
|
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'>>;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface PluginApp {
|
|
|
24
24
|
readonly options: Readonly<PluginOptions>;
|
|
25
25
|
/** Examples omit the `mearl <plugin>` prefix, which help adds when displayed. */
|
|
26
26
|
command(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
|
|
27
|
+
/** Registers a page-only command, also declared in toolbox.page.commands. */
|
|
28
|
+
toolboxCommand(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
|
|
27
29
|
invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
28
30
|
progress(progress: PluginProgress): void;
|
|
29
31
|
}
|
|
@@ -39,6 +41,7 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invo
|
|
|
39
41
|
commandHelp(namespace: string): string;
|
|
40
42
|
help(namespace: string, name?: string): string;
|
|
41
43
|
run(positionals: string[], payload?: unknown, namedInput?: Record<string, unknown>): Promise<unknown>;
|
|
44
|
+
runToolbox(invocation: PluginInvocation): Promise<unknown>;
|
|
42
45
|
}>;
|
|
43
46
|
export declare function loadRegisteredPlugin(name: string, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<{
|
|
44
47
|
name: string | undefined;
|
|
@@ -48,5 +51,8 @@ export declare function loadRegisteredPlugin(name: string, context: PluginExecut
|
|
|
48
51
|
commandHelp(namespace: string): string;
|
|
49
52
|
help(namespace: string, name?: string): string;
|
|
50
53
|
run(positionals: string[], payload?: unknown, namedInput?: Record<string, unknown>): Promise<unknown>;
|
|
54
|
+
runToolbox(invocation: PluginInvocation): Promise<unknown>;
|
|
51
55
|
}>;
|
|
52
56
|
export declare function invokeRegisteredPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
|
|
57
|
+
/** Entry point for the native toolbox service; nested plugin calls retain public-command access. */
|
|
58
|
+
export declare function invokeRegisteredToolboxPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
|
package/dist/plugin.js
CHANGED
|
@@ -133,31 +133,33 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
133
133
|
let name;
|
|
134
134
|
let description;
|
|
135
135
|
let toolbox;
|
|
136
|
+
const registerCommand = (signature, handler, description = '', examples = [], toolboxOnly = false) => {
|
|
137
|
+
if (!registering)
|
|
138
|
+
throw new Error('命令只能在入口注册阶段声明');
|
|
139
|
+
const [name, ...args] = signature.trim().split(/\s+/);
|
|
140
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name) ||
|
|
141
|
+
commands.has(name) ||
|
|
142
|
+
typeof handler !== 'function' ||
|
|
143
|
+
!Array.isArray(examples) ||
|
|
144
|
+
examples.some((example) => typeof example !== 'string' || !example.trim())) {
|
|
145
|
+
throw new Error(`无效或重复的命令: ${signature}`);
|
|
146
|
+
}
|
|
147
|
+
const command = new Command(name).description(description);
|
|
148
|
+
for (const argument of args) {
|
|
149
|
+
if (!/^(?:<[a-zA-Z][\w-]*(?:\.\.\.)?>|\[[a-zA-Z][\w-]*(?:\.\.\.)?\])$/.test(argument)) {
|
|
150
|
+
throw new Error(`无效的位置参数: ${argument}`);
|
|
151
|
+
}
|
|
152
|
+
command.argument(argument);
|
|
153
|
+
}
|
|
154
|
+
const names = command.registeredArguments.map(argument => argument.name());
|
|
155
|
+
if (new Set(names).size !== names.length)
|
|
156
|
+
throw new Error(`参数名称重复: ${signature}`);
|
|
157
|
+
commands.set(name, { command, signature, handler, examples: [...examples], toolboxOnly });
|
|
158
|
+
};
|
|
136
159
|
const app = Object.freeze({
|
|
137
160
|
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
|
-
},
|
|
161
|
+
command: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples),
|
|
162
|
+
toolboxCommand: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples, true),
|
|
161
163
|
invoke(action, payload = {}, options = {}) {
|
|
162
164
|
if (!executing)
|
|
163
165
|
throw new Error('app.invoke 只能在命令执行期间调用');
|
|
@@ -177,7 +179,8 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
177
179
|
toolbox = normalizePluginToolboxContribution(descriptor.toolbox);
|
|
178
180
|
for (const command of descriptor.commands) {
|
|
179
181
|
const commandName = command.signature.trim().split(/\s+/)[0];
|
|
180
|
-
|
|
182
|
+
const register = command.toolboxOnly ? app.toolboxCommand : app.command;
|
|
183
|
+
register(command.signature, input => runPythonPluginCommand(entry, commandName, input, app), command.description);
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
else {
|
|
@@ -198,6 +201,11 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
198
201
|
if (commands.size === 0)
|
|
199
202
|
throw new Error('入口未注册任何命令');
|
|
200
203
|
const toolboxCommands = toolbox?.page.commands ?? [];
|
|
204
|
+
for (const [name, command] of commands) {
|
|
205
|
+
if (command.toolboxOnly && !toolboxCommands.includes(name)) {
|
|
206
|
+
throw new Error(`toolboxCommand 未声明在 toolbox.page.commands 中: ${name}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
201
209
|
for (const command of toolboxCommands) {
|
|
202
210
|
if (!commands.has(command))
|
|
203
211
|
throw new Error(`toolbox 命令未注册: ${command}`);
|
|
@@ -209,24 +217,86 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
209
217
|
finally {
|
|
210
218
|
registering = false;
|
|
211
219
|
}
|
|
212
|
-
const find = (name) => {
|
|
220
|
+
const find = (name, toolboxPage = false) => {
|
|
213
221
|
const command = commands.get(name);
|
|
214
222
|
if (!command)
|
|
215
223
|
throw new PluginError('PLUGIN_COMMAND_NOT_FOUND', `未知子命令: ${name}`);
|
|
224
|
+
if (toolboxPage) {
|
|
225
|
+
if (!toolbox?.page.commands.includes(name)) {
|
|
226
|
+
throw new PluginError('PLUGIN_COMMAND_NOT_ALLOWED', `工具页面不允许调用命令: ${name}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (command.toolboxOnly) {
|
|
230
|
+
throw new PluginError('PLUGIN_COMMAND_TOOLBOX_ONLY', `命令仅供工具页面调用: ${name}`);
|
|
231
|
+
}
|
|
216
232
|
return command;
|
|
217
233
|
};
|
|
234
|
+
const publicCommands = () => [...commands.values()].filter(command => !command.toolboxOnly);
|
|
235
|
+
const run = async (positionals, payload, namedInput = {}, toolboxPage = false) => {
|
|
236
|
+
if (payload !== undefined && positionals.length > 0) {
|
|
237
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数调用与 --payload 调用不能混用');
|
|
238
|
+
}
|
|
239
|
+
if (payload !== undefined && Object.keys(namedInput).length > 0) {
|
|
240
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '插件业务选项与 --payload 调用不能混用');
|
|
241
|
+
}
|
|
242
|
+
if (payload !== undefined &&
|
|
243
|
+
(!isObject(payload) || typeof payload.command !== 'string' || !isObject(payload.input))) {
|
|
244
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
|
|
245
|
+
}
|
|
246
|
+
const envelope = payload;
|
|
247
|
+
const { command, handler } = find(envelope?.command ?? positionals[0] ?? '', toolboxPage);
|
|
248
|
+
const positionalInput = envelope
|
|
249
|
+
? { ...envelope.input }
|
|
250
|
+
: Object.fromEntries(command.registeredArguments
|
|
251
|
+
.map((argument, index) => [
|
|
252
|
+
argument.name(),
|
|
253
|
+
argument.variadic ? positionals.slice(index + 1) : positionals[index + 1],
|
|
254
|
+
])
|
|
255
|
+
.filter(([, value]) => value !== undefined));
|
|
256
|
+
for (const name of Object.keys(namedInput)) {
|
|
257
|
+
if (Object.hasOwn(positionalInput, name)) {
|
|
258
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `参数不能重复传入: ${name}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const input = { ...positionalInput, ...namedInput };
|
|
262
|
+
if (!envelope &&
|
|
263
|
+
!command.registeredArguments.some(argument => argument.variadic) &&
|
|
264
|
+
positionals.length - 1 > command.registeredArguments.length) {
|
|
265
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
|
|
266
|
+
}
|
|
267
|
+
for (const argument of command.registeredArguments) {
|
|
268
|
+
const value = Object.hasOwn(input, argument.name()) ? input[argument.name()] : undefined;
|
|
269
|
+
if (argument.required &&
|
|
270
|
+
(value === undefined ||
|
|
271
|
+
value === null ||
|
|
272
|
+
value === '' ||
|
|
273
|
+
(Array.isArray(value) && value.length === 0))) {
|
|
274
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `缺少必填参数: ${argument.name()}`);
|
|
275
|
+
}
|
|
276
|
+
if (argument.variadic && value !== undefined && !Array.isArray(value)) {
|
|
277
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `${argument.name()} 必须为数组`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
executing = true;
|
|
281
|
+
try {
|
|
282
|
+
return await handler(input);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
executing = false;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
218
288
|
return {
|
|
219
289
|
name,
|
|
220
290
|
description,
|
|
221
291
|
toolbox,
|
|
222
292
|
version: readPluginVersion(entry),
|
|
223
293
|
commandHelp(namespace) {
|
|
224
|
-
return
|
|
294
|
+
return publicCommands()
|
|
225
295
|
.map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`)
|
|
226
296
|
.join('\n');
|
|
227
297
|
},
|
|
228
298
|
help(namespace, name) {
|
|
229
|
-
const selected = name ? [find(name)] :
|
|
299
|
+
const selected = name ? [find(name)] : publicCommands();
|
|
230
300
|
const examples = selected.flatMap(({ examples }) => examples);
|
|
231
301
|
return [
|
|
232
302
|
'用法:',
|
|
@@ -236,58 +306,11 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
236
306
|
: []),
|
|
237
307
|
].join('\n');
|
|
238
308
|
},
|
|
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
|
-
}
|
|
309
|
+
run(positionals, payload, namedInput = {}) {
|
|
310
|
+
return run(positionals, payload, namedInput);
|
|
311
|
+
},
|
|
312
|
+
runToolbox(invocation) {
|
|
313
|
+
return run([], invocation, {}, true);
|
|
291
314
|
},
|
|
292
315
|
};
|
|
293
316
|
}
|
|
@@ -348,3 +371,8 @@ async function invokeRegisteredPluginWithState(name, invocation, context, option
|
|
|
348
371
|
export function invokeRegisteredPlugin(name, invocation, context, options = {}) {
|
|
349
372
|
return invokeRegisteredPluginWithState(name, invocation, context, options);
|
|
350
373
|
}
|
|
374
|
+
/** Entry point for the native toolbox service; nested plugin calls retain public-command access. */
|
|
375
|
+
export async function invokeRegisteredToolboxPlugin(name, invocation, context, options = {}) {
|
|
376
|
+
const plugin = await loadRegisteredPluginWithState(name, context, options);
|
|
377
|
+
return plugin.runToolbox(invocation);
|
|
378
|
+
}
|
package/dist/pluginPageBundle.js
CHANGED
|
@@ -103,6 +103,8 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
103
103
|
};
|
|
104
104
|
|
|
105
105
|
const runningOperations = async () => requestHost('invoke-operations');
|
|
106
|
+
const readClipboard = async () => requestHost('clipboard-read');
|
|
107
|
+
const getActiveTab = async () => requestHost('active-tab');
|
|
106
108
|
|
|
107
109
|
const selectDirectory = async () => {
|
|
108
110
|
const path = await requestHost('select-directory');
|
|
@@ -119,6 +121,14 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
119
121
|
await requestHost('clipboard-write', { text });
|
|
120
122
|
};
|
|
121
123
|
|
|
124
|
+
const copyImage = async data => {
|
|
125
|
+
if (typeof data !== 'string' || data.length > 16 * 1024 * 1024 ||
|
|
126
|
+
!/^data:image\/(png|jpeg|webp);base64,/.test(data)) {
|
|
127
|
+
throw new Error('Clipboard image must be a PNG, JPEG or WebP data URL within 16 MiB');
|
|
128
|
+
}
|
|
129
|
+
await requestHost('clipboard-image-write', { data });
|
|
130
|
+
};
|
|
131
|
+
|
|
122
132
|
const openExternalLink = async url => {
|
|
123
133
|
if (typeof url !== 'string' || !url || url.length > 8192) {
|
|
124
134
|
throw new Error('External link must be a non-empty URL no longer than 8192 characters');
|
|
@@ -441,7 +451,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
|
441
451
|
return query;
|
|
442
452
|
};
|
|
443
453
|
|
|
444
|
-
const api = { copyText, invoke, operation, ready: () => ready, runAgentTask, runningOperations, selectDirectory, setAgentGuides, setAgentPanelVisible, setQueryParam, start, toast };
|
|
454
|
+
const api = { copyImage, copyText, getActiveTab, invoke, operation, readClipboard, ready: () => ready, runAgentTask, runningOperations, selectDirectory, setAgentGuides, setAgentPanelVisible, setQueryParam, start, toast };
|
|
445
455
|
const ui = globalThis.__mearlToolboxUi;
|
|
446
456
|
if (ui !== undefined) {
|
|
447
457
|
if (!ui || typeof ui !== 'object' || ui.runtime !== 'preact') {
|
|
@@ -604,6 +614,11 @@ body {
|
|
|
604
614
|
}
|
|
605
615
|
button, input, select, textarea { font: inherit; }
|
|
606
616
|
</style>`;
|
|
617
|
+
export const PLUGIN_PAGE_RUNTIME_HASH = createHash('sha256')
|
|
618
|
+
.update(PAGE_BRIDGE_SCRIPT)
|
|
619
|
+
.update(PAGE_SECURITY_META)
|
|
620
|
+
.update(PAGE_THEME_STYLE)
|
|
621
|
+
.digest('hex');
|
|
607
622
|
function pluginRoot(entry) {
|
|
608
623
|
const entryDirectory = dirname(entry);
|
|
609
624
|
return basename(entryDirectory) === 'scripts' ? dirname(entryDirectory) : entryDirectory;
|
package/dist/pluginRegistry.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface PluginToolboxAgentConfig {
|
|
|
18
18
|
}
|
|
19
19
|
export interface PluginToolboxContribution {
|
|
20
20
|
version: 1;
|
|
21
|
+
displayMode?: 'popup' | 'tab' | 'both';
|
|
21
22
|
title: string;
|
|
22
23
|
description?: string;
|
|
23
24
|
icon?: string;
|
|
@@ -40,6 +41,7 @@ export interface PluginRegistration {
|
|
|
40
41
|
source?: {
|
|
41
42
|
entry: string;
|
|
42
43
|
page?: Record<string, string>;
|
|
44
|
+
pageRuntime?: string;
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
export declare function pluginRegistrationNeedsRefresh(registration: PluginRegistration, includePage?: boolean): boolean;
|
package/dist/pluginRegistry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, re
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { COMMAND_MAP } from './commands.js';
|
|
6
|
-
import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, } from './pluginPageBundle.js';
|
|
6
|
+
import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, PLUGIN_PAGE_RUNTIME_HASH, } from './pluginPageBundle.js';
|
|
7
7
|
import { pluginFileVersion } from './pluginSource.js';
|
|
8
8
|
const RESERVED_NAMES = new Set([
|
|
9
9
|
'check',
|
|
@@ -27,6 +27,8 @@ export function pluginRegistrationNeedsRefresh(registration, includePage = false
|
|
|
27
27
|
return true;
|
|
28
28
|
if (!includePage || !registration.toolbox)
|
|
29
29
|
return false;
|
|
30
|
+
if (registration.source.pageRuntime !== PLUGIN_PAGE_RUNTIME_HASH)
|
|
31
|
+
return true;
|
|
30
32
|
if (!registration.source.page)
|
|
31
33
|
return true;
|
|
32
34
|
return Object.entries(registration.source.page).some(([path, version]) => pluginFileVersion(path) !== version);
|
|
@@ -108,6 +110,10 @@ export function normalizePluginToolboxContribution(value) {
|
|
|
108
110
|
throw new Error('toolbox 必须是对象');
|
|
109
111
|
}
|
|
110
112
|
const candidate = value;
|
|
113
|
+
if (candidate.displayMode !== undefined &&
|
|
114
|
+
!['popup', 'tab', 'both'].includes(candidate.displayMode)) {
|
|
115
|
+
throw new Error('toolbox.displayMode 必须是 popup、tab 或 both');
|
|
116
|
+
}
|
|
111
117
|
const title = typeof candidate.title === 'string' ? candidate.title.trim() : '';
|
|
112
118
|
const description = typeof candidate.description === 'string' ? candidate.description.trim() : undefined;
|
|
113
119
|
const icon = typeof candidate.icon === 'string' ? candidate.icon.trim() : undefined;
|
|
@@ -216,6 +222,7 @@ export function normalizePluginToolboxContribution(value) {
|
|
|
216
222
|
return {
|
|
217
223
|
version: 1,
|
|
218
224
|
title,
|
|
225
|
+
...(candidate.displayMode !== undefined ? { displayMode: candidate.displayMode } : {}),
|
|
219
226
|
...(description ? { description } : {}),
|
|
220
227
|
...(icon ? { icon } : {}),
|
|
221
228
|
...(locales ? { locales } : {}),
|
|
@@ -281,7 +288,10 @@ export function registerPlugin(name, target, description, options = {}) {
|
|
|
281
288
|
}
|
|
282
289
|
const record = {
|
|
283
290
|
entry,
|
|
284
|
-
source: {
|
|
291
|
+
source: {
|
|
292
|
+
entry: entryVersion,
|
|
293
|
+
...(builtPage ? { page: builtPage.sources, pageRuntime: PLUGIN_PAGE_RUNTIME_HASH } : {}),
|
|
294
|
+
},
|
|
285
295
|
...(summary ? { description: summary } : {}),
|
|
286
296
|
...(managedBy ? { managedBy } : {}),
|
|
287
297
|
...(toolbox ? { toolbox } : {}),
|
|
@@ -344,6 +354,8 @@ export function findPluginRegistration(name) {
|
|
|
344
354
|
typeof source !== 'object' ||
|
|
345
355
|
Array.isArray(source) ||
|
|
346
356
|
typeof source.entry !== 'string' ||
|
|
357
|
+
(source.pageRuntime !== undefined &&
|
|
358
|
+
(typeof source.pageRuntime !== 'string' || !/^[a-f0-9]{64}$/.test(source.pageRuntime))) ||
|
|
347
359
|
(source.page !== undefined &&
|
|
348
360
|
(!source.page ||
|
|
349
361
|
typeof source.page !== 'object' ||
|
package/dist/pythonPlugin.d.ts
CHANGED
package/dist/pythonPlugin.js
CHANGED
|
@@ -53,6 +53,12 @@ class _App:
|
|
|
53
53
|
self.request_id = 0
|
|
54
54
|
|
|
55
55
|
def command(self, signature, handler=None, description=""):
|
|
56
|
+
return self._register_command(signature, handler, description, False)
|
|
57
|
+
|
|
58
|
+
def toolboxCommand(self, signature, handler=None, description=""):
|
|
59
|
+
return self._register_command(signature, handler, description, True)
|
|
60
|
+
|
|
61
|
+
def _register_command(self, signature, handler, description, toolbox_only):
|
|
56
62
|
if not self.registering:
|
|
57
63
|
raise RuntimeError("命令只能在入口注册阶段声明")
|
|
58
64
|
if not isinstance(signature, str):
|
|
@@ -67,6 +73,7 @@ class _App:
|
|
|
67
73
|
"signature": signature,
|
|
68
74
|
"description": description,
|
|
69
75
|
"handler": candidate,
|
|
76
|
+
"toolboxOnly": toolbox_only,
|
|
70
77
|
})
|
|
71
78
|
return candidate
|
|
72
79
|
|
|
@@ -163,7 +170,7 @@ try:
|
|
|
163
170
|
descriptor = {
|
|
164
171
|
"type": "ready",
|
|
165
172
|
"commands": [
|
|
166
|
-
{"signature": item["signature"], "description": item["description"]}
|
|
173
|
+
{"signature": item["signature"], "description": item["description"], "toolboxOnly": item["toolboxOnly"]}
|
|
167
174
|
for item in app.commands
|
|
168
175
|
],
|
|
169
176
|
}
|
|
@@ -223,10 +230,15 @@ function parseDescriptor(message) {
|
|
|
223
230
|
const commands = message.commands.map(value => {
|
|
224
231
|
if (!isObject(value) ||
|
|
225
232
|
typeof value.signature !== 'string' ||
|
|
226
|
-
typeof value.description !== 'string'
|
|
233
|
+
typeof value.description !== 'string' ||
|
|
234
|
+
typeof value.toolboxOnly !== 'boolean') {
|
|
227
235
|
throw new Error('Python 插件返回了无效的命令描述');
|
|
228
236
|
}
|
|
229
|
-
return {
|
|
237
|
+
return {
|
|
238
|
+
signature: value.signature,
|
|
239
|
+
description: value.description,
|
|
240
|
+
toolboxOnly: value.toolboxOnly,
|
|
241
|
+
};
|
|
230
242
|
});
|
|
231
243
|
if (message.name !== undefined && typeof message.name !== 'string') {
|
|
232
244
|
throw new Error('Python 插件返回了无效的 name');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.2",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"commander": "^15.0.0",
|
|
60
60
|
"fastest-levenshtein": "^1.0.16",
|
|
61
61
|
"ws": "^8.18.0",
|
|
62
|
-
"@mearl/
|
|
63
|
-
"@mearl/
|
|
62
|
+
"@mearl/provider": "2.20.2",
|
|
63
|
+
"@mearl/cloud-types": "2.20.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.9.1",
|