@mearl/client 2.15.2 → 2.16.0
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 +5 -4
- package/dist/commands.d.ts +9 -5
- package/dist/commands.js +10 -2
- package/dist/generated-browser-action-protocol.d.ts +3 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +27 -15
- package/dist/pluginRegistry.d.ts +1 -0
- package/dist/pluginRegistry.js +17 -4
- package/dist/pythonPlugin.d.ts +17 -0
- package/dist/pythonPlugin.js +403 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -143,11 +143,10 @@ mearl yuque --help
|
|
|
143
143
|
mearl yuque --version
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
目录默认使用 `scripts/main.mjs
|
|
147
|
-
|
|
148
|
-
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。调用另一个插件时,payload 使用 `{ command, input }` envelope;嵌套调用继承宿主选项并拒绝循环依赖。CLI 默认只输出最终结果,传入
|
|
146
|
+
目录默认使用 `scripts/main.mjs` 或 `scripts/main.py`;两者同时存在时使用 JavaScript 入口。JavaScript 入口默认导出 `register(app)`,Python 入口导出 `register(app)` 函数;两种入口都可声明简短的 `name` 和 `description`,并通过 `app.command(signature, handler, description?)`
|
|
147
|
+
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。Python 的 `app.invoke` 是同步调用,handler 可以是普通函数或 `async def`。Python 插件要求 `python3 >= 3.9`,可用 `MEARL_PYTHON` 指定解释器路径;Mearl 不安装 Python 或执行 `pip install`。调用另一个插件时,payload 使用 `{ command, input }` envelope;嵌套调用继承宿主选项并拒绝循环依赖。CLI 默认只输出最终结果,传入
|
|
149
148
|
`--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
|
|
150
|
-
自动注册默认以 skill
|
|
149
|
+
自动注册默认以 skill 安装目录名作为命令名,也可在入口声明 `name` 指定短名称。
|
|
151
150
|
setup 会补齐缺失或低于自身版本的 client、native-host;安装参数透传给 Ali Skills,并固定全局共享安装。
|
|
152
151
|
|
|
153
152
|
需要单独检查插件时运行 `mearl <name> --version`。命令会解析注册记录并加载入口,但不连接浏览器;
|
|
@@ -183,6 +182,8 @@ MCP Server 通过固定工具 `mearl_plugin` 接受同样的 `plugin`、`command
|
|
|
183
182
|
`app.invoke('buc', { command: 'request', input })`;当前身份和人员资料分别使用 `whoami` 与 `profile`
|
|
184
183
|
子命令。BUC 插件按请求创建一次性凭据,只允许访问 HTTPS `alibaba-inc.com` 域名,过滤认证字段与
|
|
185
184
|
Cookie,并始终在运行 CLI、SDK 或 MCP Server 的机器上执行,不随浏览器或 connector 转发。
|
|
185
|
+
`buc request` 支持与 `send_request` 同结构的 `formData` 和 `files`;`files[].filePath` 从该插件执行
|
|
186
|
+
宿主读取,而不是浏览器 native-host,无法引用宿主路径时使用 Base64 内容与 `filename`。
|
|
186
187
|
|
|
187
188
|
## 支持的操作
|
|
188
189
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -421,7 +421,7 @@ export declare const COMMANDS: readonly [{
|
|
|
421
421
|
}, {
|
|
422
422
|
readonly name: "emulation";
|
|
423
423
|
readonly type: "object";
|
|
424
|
-
readonly description: "开 tab 即进入移动端模拟态(首屏即移动态、无需 reload)。字段同 set_device_emulation 去掉 enabled/reload
|
|
424
|
+
readonly description: "开 tab 即进入移动端模拟态(首屏即移动态、无需 reload)。字段同 set_device_emulation 去掉 enabled/reload;与 appProfile、deviceShell=true、orientation=landscape、splitScreen=true 组合可首屏进入左右分屏";
|
|
425
425
|
}, {
|
|
426
426
|
readonly name: "appProfile";
|
|
427
427
|
readonly type: "string";
|
|
@@ -435,7 +435,7 @@ export declare const COMMANDS: readonly [{
|
|
|
435
435
|
readonly type: "object";
|
|
436
436
|
readonly description: "与 appProfile 配合;声明式覆盖桥方法,形如 {methods:{api:{behavior|response|noop,silent?}}}";
|
|
437
437
|
}];
|
|
438
|
-
readonly examples: ["tab_open --payload '{\"url\":\"https://example.com\"}'", "tab_open --payload '{\"url\":\"https://example.com/page\",\"reuse\":\"prefer\",\"match\":{\"ignoreSearch\":true}}'", "tab_open --payload '{\"url\":\"https://m.example.com\",\"emulation\":{\"preset\":\"iphone-17-pro\"}}'", "tab_open --payload '{\"url\":\"https://market.m.taobao.com/app/trip/rx-home/pages/home\",\"appProfile\":\"fliggy\"}'", "tab_open --payload '{\"url\":\"https://example.com\",\"appProfile\":\"fliggy\",\"os\":\"android\"}'", "tab_open --payload '{\"url\":\"https://example.com\",\"group\":\"验证任务\"}'"];
|
|
438
|
+
readonly examples: ["tab_open --payload '{\"url\":\"https://example.com\"}'", "tab_open --payload '{\"url\":\"https://example.com/page\",\"reuse\":\"prefer\",\"match\":{\"ignoreSearch\":true}}'", "tab_open --payload '{\"url\":\"https://m.example.com\",\"emulation\":{\"preset\":\"iphone-17-pro\"}}'", "tab_open --payload '{\"url\":\"https://market.m.taobao.com/app/trip/rx-home/pages/home\",\"appProfile\":\"fliggy\"}'", "tab_open --payload '{\"url\":\"https://market.m.taobao.com/app/trip/rx-home/pages/home\",\"appProfile\":\"fliggy\",\"emulation\":{\"preset\":\"ipad-mini-a17-pro\",\"orientation\":\"landscape\",\"deviceShell\":true,\"splitScreen\":true}}'", "tab_open --payload '{\"url\":\"https://example.com\",\"appProfile\":\"fliggy\",\"os\":\"android\"}'", "tab_open --payload '{\"url\":\"https://example.com\",\"group\":\"验证任务\"}'"];
|
|
439
439
|
}, {
|
|
440
440
|
readonly name: "tab_close";
|
|
441
441
|
readonly description: "关闭指定标签页";
|
|
@@ -858,6 +858,10 @@ export declare const COMMANDS: readonly [{
|
|
|
858
858
|
readonly name: "deviceShell";
|
|
859
859
|
readonly type: "boolean";
|
|
860
860
|
readonly description: "是否使用 iframe WebView 并渲染顶部状态栏与底部操作区,默认 false";
|
|
861
|
+
}, {
|
|
862
|
+
readonly name: "splitScreen";
|
|
863
|
+
readonly type: "boolean";
|
|
864
|
+
readonly description: "是否将首页与业务页分置左右 WebView;仅在已启用 App 档案、deviceShell=true、横屏时生效,默认 false";
|
|
861
865
|
}, {
|
|
862
866
|
readonly name: "reload";
|
|
863
867
|
readonly type: "boolean";
|
|
@@ -868,7 +872,7 @@ export declare const COMMANDS: readonly [{
|
|
|
868
872
|
readonly required: true;
|
|
869
873
|
readonly description: "目标标签页 ID";
|
|
870
874
|
}];
|
|
871
|
-
readonly examples: ["set_device_emulation --payload '{\"enabled\":true,\"preset\":\"iphone-17-pro\"}'", "set_device_emulation --payload '{\"enabled\":true,\"preset\":\"pixel-11\",\"orientation\":\"landscape\"}'", "set_device_emulation --payload '{\"enabled\":false}'"];
|
|
875
|
+
readonly examples: ["set_device_emulation --payload '{\"enabled\":true,\"preset\":\"iphone-17-pro\"}'", "set_device_emulation --payload '{\"enabled\":true,\"preset\":\"pixel-11\",\"orientation\":\"landscape\"}'", "set_device_emulation --payload '{\"enabled\":true,\"preset\":\"ipad-mini-a17-pro\",\"orientation\":\"landscape\",\"deviceShell\":true,\"splitScreen\":true}'", "set_device_emulation --payload '{\"enabled\":false}'"];
|
|
872
876
|
}, {
|
|
873
877
|
readonly name: "set_app_profile";
|
|
874
878
|
readonly description: "设置 App 客户端环境与 JSBridge;传 emulation 时由同一个命令联动设备模拟,省略时不改变设备状态";
|
|
@@ -901,7 +905,7 @@ export declare const COMMANDS: readonly [{
|
|
|
901
905
|
}, {
|
|
902
906
|
readonly name: "emulation";
|
|
903
907
|
readonly type: "object";
|
|
904
|
-
readonly description: "可选设备联动参数,字段同 set_device_emulation 去掉 enabled/reload;字段覆盖档案推荐设备,deviceShell 默认 true。省略时不设置或清除设备";
|
|
908
|
+
readonly description: "可选设备联动参数,字段同 set_device_emulation 去掉 enabled/reload;字段覆盖档案推荐设备,deviceShell 默认 true,横屏时可传 splitScreen=true。省略时不设置或清除设备";
|
|
905
909
|
}, {
|
|
906
910
|
readonly name: "reload";
|
|
907
911
|
readonly type: "boolean";
|
|
@@ -912,7 +916,7 @@ export declare const COMMANDS: readonly [{
|
|
|
912
916
|
readonly required: true;
|
|
913
917
|
readonly description: "目标标签页 ID";
|
|
914
918
|
}];
|
|
915
|
-
readonly examples: ["set_app_profile --payload '{\"enabled\":true,\"profile\":\"fliggy\"}'", "set_app_profile --payload '{\"enabled\":true,\"profile\":\"fliggy\",\"emulation\":{}}'", "set_app_profile --payload '{\"enabled\":true,\"profile\":\"alipay\",\"emulation\":{\"preset\":\"pixel-11\",\"deviceShell\":true}}'", "set_app_profile --payload '{\"enabled\":false}'", "set_app_profile --payload '{\"enabled\":false,\"emulation\":{}}'"];
|
|
919
|
+
readonly examples: ["set_app_profile --payload '{\"enabled\":true,\"profile\":\"fliggy\"}'", "set_app_profile --payload '{\"enabled\":true,\"profile\":\"fliggy\",\"emulation\":{}}'", "set_app_profile --payload '{\"enabled\":true,\"profile\":\"alipay\",\"emulation\":{\"preset\":\"pixel-11\",\"deviceShell\":true}}'", "set_app_profile --payload '{\"enabled\":true,\"profile\":\"fliggy\",\"emulation\":{\"preset\":\"ipad-mini-a17-pro\",\"orientation\":\"landscape\",\"deviceShell\":true,\"splitScreen\":true}}'", "set_app_profile --payload '{\"enabled\":false}'", "set_app_profile --payload '{\"enabled\":false,\"emulation\":{}}'"];
|
|
916
920
|
}, {
|
|
917
921
|
readonly name: "set_timezone";
|
|
918
922
|
readonly description: "覆盖目标 tab 的时区,传空字符串恢复浏览器默认时区";
|
package/dist/commands.js
CHANGED
|
@@ -344,7 +344,7 @@ export const COMMANDS = [
|
|
|
344
344
|
{
|
|
345
345
|
name: 'emulation',
|
|
346
346
|
type: 'object',
|
|
347
|
-
description: '开 tab 即进入移动端模拟态(首屏即移动态、无需 reload)。字段同 set_device_emulation 去掉 enabled/reload
|
|
347
|
+
description: '开 tab 即进入移动端模拟态(首屏即移动态、无需 reload)。字段同 set_device_emulation 去掉 enabled/reload;与 appProfile、deviceShell=true、orientation=landscape、splitScreen=true 组合可首屏进入左右分屏',
|
|
348
348
|
},
|
|
349
349
|
{
|
|
350
350
|
name: 'appProfile',
|
|
@@ -367,6 +367,7 @@ export const COMMANDS = [
|
|
|
367
367
|
`tab_open --payload '{"url":"https://example.com/page","reuse":"prefer","match":{"ignoreSearch":true}}'`,
|
|
368
368
|
`tab_open --payload '{"url":"https://m.example.com","emulation":{"preset":"iphone-17-pro"}}'`,
|
|
369
369
|
`tab_open --payload '{"url":"https://market.m.taobao.com/app/trip/rx-home/pages/home","appProfile":"fliggy"}'`,
|
|
370
|
+
`tab_open --payload '{"url":"https://market.m.taobao.com/app/trip/rx-home/pages/home","appProfile":"fliggy","emulation":{"preset":"ipad-mini-a17-pro","orientation":"landscape","deviceShell":true,"splitScreen":true}}'`,
|
|
370
371
|
`tab_open --payload '{"url":"https://example.com","appProfile":"fliggy","os":"android"}'`,
|
|
371
372
|
`tab_open --payload '{"url":"https://example.com","group":"验证任务"}'`,
|
|
372
373
|
],
|
|
@@ -790,12 +791,18 @@ export const COMMANDS = [
|
|
|
790
791
|
type: 'boolean',
|
|
791
792
|
description: '是否使用 iframe WebView 并渲染顶部状态栏与底部操作区,默认 false',
|
|
792
793
|
},
|
|
794
|
+
{
|
|
795
|
+
name: 'splitScreen',
|
|
796
|
+
type: 'boolean',
|
|
797
|
+
description: '是否将首页与业务页分置左右 WebView;仅在已启用 App 档案、deviceShell=true、横屏时生效,默认 false',
|
|
798
|
+
},
|
|
793
799
|
{ name: 'reload', type: 'boolean', description: '启用后是否重载页面,默认 true' },
|
|
794
800
|
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
795
801
|
],
|
|
796
802
|
examples: [
|
|
797
803
|
`set_device_emulation --payload '{"enabled":true,"preset":"iphone-17-pro"}'`,
|
|
798
804
|
`set_device_emulation --payload '{"enabled":true,"preset":"pixel-11","orientation":"landscape"}'`,
|
|
805
|
+
`set_device_emulation --payload '{"enabled":true,"preset":"ipad-mini-a17-pro","orientation":"landscape","deviceShell":true,"splitScreen":true}'`,
|
|
799
806
|
`set_device_emulation --payload '{"enabled":false}'`,
|
|
800
807
|
],
|
|
801
808
|
},
|
|
@@ -825,7 +832,7 @@ export const COMMANDS = [
|
|
|
825
832
|
{
|
|
826
833
|
name: 'emulation',
|
|
827
834
|
type: 'object',
|
|
828
|
-
description: '可选设备联动参数,字段同 set_device_emulation 去掉 enabled/reload;字段覆盖档案推荐设备,deviceShell 默认 true。省略时不设置或清除设备',
|
|
835
|
+
description: '可选设备联动参数,字段同 set_device_emulation 去掉 enabled/reload;字段覆盖档案推荐设备,deviceShell 默认 true,横屏时可传 splitScreen=true。省略时不设置或清除设备',
|
|
829
836
|
},
|
|
830
837
|
{ name: 'reload', type: 'boolean', description: '应用/清除后是否重载页面,默认 true' },
|
|
831
838
|
{ name: 'tabId', type: 'number', required: true, description: '目标标签页 ID' },
|
|
@@ -834,6 +841,7 @@ export const COMMANDS = [
|
|
|
834
841
|
`set_app_profile --payload '{"enabled":true,"profile":"fliggy"}'`,
|
|
835
842
|
`set_app_profile --payload '{"enabled":true,"profile":"fliggy","emulation":{}}'`,
|
|
836
843
|
`set_app_profile --payload '{"enabled":true,"profile":"alipay","emulation":{"preset":"pixel-11","deviceShell":true}}'`,
|
|
844
|
+
`set_app_profile --payload '{"enabled":true,"profile":"fliggy","emulation":{"preset":"ipad-mini-a17-pro","orientation":"landscape","deviceShell":true,"splitScreen":true}}'`,
|
|
837
845
|
`set_app_profile --payload '{"enabled":false}'`,
|
|
838
846
|
`set_app_profile --payload '{"enabled":false,"emulation":{}}'`,
|
|
839
847
|
],
|
|
@@ -365,8 +365,10 @@ export interface BrowserEmulationOptions {
|
|
|
365
365
|
touch?: boolean;
|
|
366
366
|
/** 在 iframe WebView 外渲染设备系统栏;默认 false,保持纯 CDP 设备模拟。 */
|
|
367
367
|
deviceShell?: boolean;
|
|
368
|
+
/** App 容器横屏时将根页面与业务页面分置左右 WebView;仅与 deviceShell、App 档案同时生效。 */
|
|
369
|
+
splitScreen?: boolean;
|
|
368
370
|
}
|
|
369
|
-
export type AppBridgeConfigBehavior = 'navigate' | 'closeAll' | 'closeBackData' | 'postMessage' | 'notifySubscribe' | 'notifyUnsubscribe' | 'notifyPost' | 'sharedFileRead' | 'sharedFileWrite' | 'sharedDataGet' | 'sharedDataSet' | 'legacyBroadcastCreate' | 'legacyBroadcastPost' | 'legacyBroadcastClose' | 'memoryGet' | 'memorySet' | 'memoryDelete' | 'availabilityMap' | 'userInfo' | 'networkInfo' | 'geolocation' | 'permission' | 'prefetch' | 'fptTrack' | 'mtopStream' | 'previewFile' | 'toast' | 'unsupportedNativeInput' | 'payment' | 'userTrack' | 'calendar' | 'networkQualityMonitor' | 'beaconRequest';
|
|
371
|
+
export type AppBridgeConfigBehavior = 'navigate' | 'closeAll' | 'closeBackData' | 'postMessage' | 'notifySubscribe' | 'notifyUnsubscribe' | 'notifyPost' | 'sharedFileRead' | 'sharedFileWrite' | 'sharedDataGet' | 'sharedDataSet' | 'legacyBroadcastCreate' | 'legacyBroadcastPost' | 'legacyBroadcastClose' | 'memoryGet' | 'memorySet' | 'memoryDelete' | 'availabilityMap' | 'userInfo' | 'networkInfo' | 'geolocation' | 'permission' | 'prefetch' | 'fptTrack' | 'mtopStream' | 'previewFile' | 'toast' | 'unsupportedNativeInput' | 'payment' | 'userTrack' | 'calendar' | 'networkQualityMonitor' | 'beaconRequest' | 'voiceRecognition';
|
|
370
372
|
/** 单个桥方法的声明式覆盖;行为、固定回包、成功空实现三选一。 */
|
|
371
373
|
export interface AppBridgeMethodConfig {
|
|
372
374
|
behavior?: AppBridgeConfigBehavior;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
|
|
2
2
|
import type { UnifiedInvokeOptions } from './unified.js';
|
|
3
|
-
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, type PluginRegistrationOptions, } from './pluginRegistry.js';
|
|
3
|
+
export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, type PluginRegistrationOptions, } from './pluginRegistry.js';
|
|
4
4
|
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, type OfficialPluginDefinition, type OfficialPluginName, } from './officialPluginCatalog.js';
|
|
5
5
|
export type PluginOptions = UnifiedInvokeOptions;
|
|
6
6
|
export interface PluginInvocation {
|
|
@@ -21,7 +21,7 @@ export interface PluginApp {
|
|
|
21
21
|
progress(message: string): void;
|
|
22
22
|
}
|
|
23
23
|
export type PluginHandler = (input: Record<string, any>) => unknown | Promise<unknown>;
|
|
24
|
-
/**
|
|
24
|
+
/** 注册和执行复用同一宿主能力边界;仅 handler 执行期间开放浏览器调用。 */
|
|
25
25
|
export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'>, pluginName?: string): Promise<{
|
|
26
26
|
name: string | undefined;
|
|
27
27
|
description: string | undefined;
|
package/dist/plugin.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { COMMAND_MAP } from './commands.js';
|
|
6
6
|
import { findOfficialPlugin } from './officialPluginCatalog.js';
|
|
7
7
|
import { ensureOfficialPluginRegistered, } from './officialPluginInstaller.js';
|
|
8
8
|
import { findPluginRegistration, PluginError } from './pluginRegistry.js';
|
|
9
|
-
|
|
9
|
+
import { describePythonPlugin, runPythonPluginCommand } from './pythonPlugin.js';
|
|
10
|
+
export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
|
|
10
11
|
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, } from './officialPluginCatalog.js';
|
|
11
12
|
function readPluginVersion(entry) {
|
|
12
13
|
try {
|
|
@@ -36,7 +37,7 @@ function missingPluginEntryMessage(entry, pluginName) {
|
|
|
36
37
|
`卸载命令:npx @mearl/setup plugin uninstall ${name}`,
|
|
37
38
|
].join('\n');
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
+
/** 注册和执行复用同一宿主能力边界;仅 handler 执行期间开放浏览器调用。 */
|
|
40
41
|
export async function loadPlugin(entry, context, pluginName) {
|
|
41
42
|
let entryVersion;
|
|
42
43
|
try {
|
|
@@ -88,18 +89,29 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
88
89
|
},
|
|
89
90
|
});
|
|
90
91
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
if (extname(entry).toLowerCase() === '.py') {
|
|
93
|
+
const descriptor = await describePythonPlugin(entry, app.options);
|
|
94
|
+
name = descriptor.name;
|
|
95
|
+
description = descriptor.description;
|
|
96
|
+
for (const command of descriptor.commands) {
|
|
97
|
+
const commandName = command.signature.trim().split(/\s+/)[0];
|
|
98
|
+
app.command(command.signature, input => runPythonPluginCommand(entry, commandName, input, app), command.description);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const moduleUrl = pathToFileURL(entry);
|
|
103
|
+
moduleUrl.searchParams.set('mearl-version', entryVersion);
|
|
104
|
+
const module = await import(moduleUrl.href);
|
|
105
|
+
if (module.name !== undefined && typeof module.name !== 'string')
|
|
106
|
+
throw new Error('入口的 name 导出必须为字符串');
|
|
107
|
+
name = module.name;
|
|
108
|
+
if (module.description !== undefined && typeof module.description !== 'string')
|
|
109
|
+
throw new Error('入口的 description 导出必须为字符串');
|
|
110
|
+
description = module.description;
|
|
111
|
+
if (typeof module.default !== 'function')
|
|
112
|
+
throw new Error('入口必须默认导出 register(app) 函数');
|
|
113
|
+
await module.default(app);
|
|
114
|
+
}
|
|
103
115
|
if (commands.size === 0)
|
|
104
116
|
throw new Error('入口未注册任何命令');
|
|
105
117
|
}
|
package/dist/pluginRegistry.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface PluginRegistrationOptions {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function pluginDirectory(): string;
|
|
14
14
|
export declare function assertPluginName(name: string): void;
|
|
15
|
+
export declare function findPluginDirectoryEntry(directory: string): string | undefined;
|
|
15
16
|
export declare function resolvePluginEntry(target: string): string;
|
|
16
17
|
export declare function registerPlugin(name: string, target: string, description?: string, options?: PluginRegistrationOptions): string;
|
|
17
18
|
export declare function unregisterPlugin(name: string): boolean;
|
package/dist/pluginRegistry.js
CHANGED
|
@@ -26,18 +26,31 @@ export function assertPluginName(name) {
|
|
|
26
26
|
throw new PluginError('PLUGIN_INVALID_NAME', `不可用的插件名称: ${name}`);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
export function findPluginDirectoryEntry(directory) {
|
|
30
|
+
for (const filename of ['main.mjs', 'main.py']) {
|
|
31
|
+
const entry = join(directory, 'scripts', filename);
|
|
32
|
+
try {
|
|
33
|
+
if (statSync(entry).isFile())
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 没有约定入口的目录是普通 skill,调用方据此跳过自动注册。
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
29
42
|
export function resolvePluginEntry(target) {
|
|
30
43
|
const absolute = resolve(target);
|
|
31
44
|
try {
|
|
32
|
-
const entry = statSync(absolute).isDirectory()
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
const entry = statSync(absolute).isDirectory() ? findPluginDirectoryEntry(absolute) : absolute;
|
|
46
|
+
if (!entry)
|
|
47
|
+
throw new Error('not a file');
|
|
35
48
|
if (!statSync(entry).isFile())
|
|
36
49
|
throw new Error('not a file');
|
|
37
50
|
return entry;
|
|
38
51
|
}
|
|
39
52
|
catch {
|
|
40
|
-
throw new PluginError('PLUGIN_ENTRY_MISSING', `入口不存在: ${absolute}(目录默认使用 scripts/main.mjs)`);
|
|
53
|
+
throw new PluginError('PLUGIN_ENTRY_MISSING', `入口不存在: ${absolute}(目录默认使用 scripts/main.mjs 或 scripts/main.py)`);
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
export function registerPlugin(name, target, description, options = {}) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginOptions } from './plugin.js';
|
|
2
|
+
export interface PythonPluginCommand {
|
|
3
|
+
description: string;
|
|
4
|
+
signature: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PythonPluginDescriptor {
|
|
7
|
+
commands: PythonPluginCommand[];
|
|
8
|
+
description?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PythonPluginApp {
|
|
12
|
+
readonly options: Readonly<PluginOptions>;
|
|
13
|
+
invoke(action: string, payload?: Record<string, any>, options?: PluginOptions): Promise<any>;
|
|
14
|
+
progress(message: string): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function describePythonPlugin(entry: string, options: Readonly<PluginOptions>): Promise<PythonPluginDescriptor>;
|
|
17
|
+
export declare function runPythonPluginCommand(entry: string, command: string, input: Record<string, any>, app: PythonPluginApp): Promise<unknown>;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { PluginError } from './pluginRegistry.js';
|
|
4
|
+
const STDERR_LIMIT = 64 * 1024;
|
|
5
|
+
// Bootstrap 随 Node client 分发,Python 插件无需额外安装宿主 SDK。
|
|
6
|
+
const PYTHON_BOOTSTRAP = String.raw `
|
|
7
|
+
import asyncio
|
|
8
|
+
import importlib.util
|
|
9
|
+
import inspect
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
|
|
15
|
+
_protocol_output = sys.stdout
|
|
16
|
+
sys.stdout = sys.stderr
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _send(message):
|
|
20
|
+
encoded = json.dumps(message, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
|
|
21
|
+
_protocol_output.write(encoded + "\n")
|
|
22
|
+
_protocol_output.flush()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _receive():
|
|
26
|
+
line = sys.stdin.readline()
|
|
27
|
+
if not line:
|
|
28
|
+
raise RuntimeError("Mearl 宿主已断开")
|
|
29
|
+
message = json.loads(line)
|
|
30
|
+
if not isinstance(message, dict):
|
|
31
|
+
raise RuntimeError("Mearl 宿主发送了无效消息")
|
|
32
|
+
return message
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve(value):
|
|
36
|
+
if inspect.isawaitable(value):
|
|
37
|
+
return asyncio.run(value)
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _InvocationError(RuntimeError):
|
|
42
|
+
def __init__(self, message, code=None):
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
self.code = code
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _App:
|
|
48
|
+
def __init__(self, options):
|
|
49
|
+
self.options = MappingProxyType(options)
|
|
50
|
+
self.commands = []
|
|
51
|
+
self.registering = True
|
|
52
|
+
self.executing = False
|
|
53
|
+
self.request_id = 0
|
|
54
|
+
|
|
55
|
+
def command(self, signature, handler=None, description=""):
|
|
56
|
+
if not self.registering:
|
|
57
|
+
raise RuntimeError("命令只能在入口注册阶段声明")
|
|
58
|
+
if not isinstance(signature, str):
|
|
59
|
+
raise TypeError("命令签名必须为字符串")
|
|
60
|
+
if not isinstance(description, str):
|
|
61
|
+
raise TypeError("命令 description 必须为字符串")
|
|
62
|
+
|
|
63
|
+
def add(candidate):
|
|
64
|
+
if not callable(candidate):
|
|
65
|
+
raise TypeError("命令 handler 必须可调用")
|
|
66
|
+
self.commands.append({
|
|
67
|
+
"signature": signature,
|
|
68
|
+
"description": description,
|
|
69
|
+
"handler": candidate,
|
|
70
|
+
})
|
|
71
|
+
return candidate
|
|
72
|
+
|
|
73
|
+
if handler is None:
|
|
74
|
+
return add
|
|
75
|
+
add(handler)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def invoke(self, action, payload=None, options=None):
|
|
79
|
+
if not self.executing:
|
|
80
|
+
raise RuntimeError("app.invoke 只能在命令执行期间调用")
|
|
81
|
+
if not isinstance(action, str) or not action:
|
|
82
|
+
raise TypeError("app.invoke action 必须为非空字符串")
|
|
83
|
+
if payload is None:
|
|
84
|
+
payload = {}
|
|
85
|
+
if options is None:
|
|
86
|
+
options = {}
|
|
87
|
+
if not isinstance(payload, dict) or not isinstance(options, dict):
|
|
88
|
+
raise TypeError("app.invoke payload 和 options 必须为字典")
|
|
89
|
+
self.request_id += 1
|
|
90
|
+
request_id = self.request_id
|
|
91
|
+
_send({
|
|
92
|
+
"type": "invoke",
|
|
93
|
+
"id": request_id,
|
|
94
|
+
"action": action,
|
|
95
|
+
"payload": payload,
|
|
96
|
+
"options": options,
|
|
97
|
+
})
|
|
98
|
+
response = _receive()
|
|
99
|
+
if response.get("type") != "response" or response.get("id") != request_id:
|
|
100
|
+
raise RuntimeError("Mearl 宿主返回了无效的 invoke 响应")
|
|
101
|
+
error = response.get("error")
|
|
102
|
+
if isinstance(error, dict):
|
|
103
|
+
raise _InvocationError(str(error.get("message", "调用失败")), error.get("code"))
|
|
104
|
+
return response.get("result")
|
|
105
|
+
|
|
106
|
+
def progress(self, message):
|
|
107
|
+
if not self.executing:
|
|
108
|
+
raise RuntimeError("app.progress 只能在命令执行期间调用")
|
|
109
|
+
if not isinstance(message, str) or not message:
|
|
110
|
+
raise TypeError("app.progress 需要非空字符串")
|
|
111
|
+
_send({"type": "progress", "message": message})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _error_message(error):
|
|
115
|
+
if isinstance(error, _InvocationError):
|
|
116
|
+
return str(error)
|
|
117
|
+
detail = str(error)
|
|
118
|
+
return type(error).__name__ + (": " + detail if detail else "")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
phase = "load"
|
|
122
|
+
try:
|
|
123
|
+
if sys.version_info < (3, 9):
|
|
124
|
+
current = ".".join(str(part) for part in sys.version_info[:3])
|
|
125
|
+
raise RuntimeError("需要 Python 3.9 或更高版本,当前为 " + current)
|
|
126
|
+
|
|
127
|
+
initial = _receive()
|
|
128
|
+
mode = initial.get("mode")
|
|
129
|
+
options = initial.get("options", {})
|
|
130
|
+
if mode not in ("describe", "run") or not isinstance(options, dict):
|
|
131
|
+
raise RuntimeError("Mearl 宿主发送了无效的初始化消息")
|
|
132
|
+
|
|
133
|
+
entry = os.path.abspath(sys.argv[1])
|
|
134
|
+
entry_directory = os.path.dirname(entry)
|
|
135
|
+
if entry_directory not in sys.path:
|
|
136
|
+
sys.path.insert(0, entry_directory)
|
|
137
|
+
spec = importlib.util.spec_from_file_location("mearl_skill_plugin", entry)
|
|
138
|
+
if spec is None or spec.loader is None:
|
|
139
|
+
raise RuntimeError("无法加载 Python 插件入口")
|
|
140
|
+
module = importlib.util.module_from_spec(spec)
|
|
141
|
+
sys.modules[spec.name] = module
|
|
142
|
+
spec.loader.exec_module(module)
|
|
143
|
+
|
|
144
|
+
plugin_name = getattr(module, "name", None)
|
|
145
|
+
plugin_description = getattr(module, "description", None)
|
|
146
|
+
if plugin_name is not None and not isinstance(plugin_name, str):
|
|
147
|
+
raise TypeError("入口的 name 必须为字符串")
|
|
148
|
+
if plugin_description is not None and not isinstance(plugin_description, str):
|
|
149
|
+
raise TypeError("入口的 description 必须为字符串")
|
|
150
|
+
register = getattr(module, "register", None)
|
|
151
|
+
if not callable(register):
|
|
152
|
+
raise TypeError("入口必须导出 register(app) 函数")
|
|
153
|
+
|
|
154
|
+
app = _App(options)
|
|
155
|
+
_resolve(register(app))
|
|
156
|
+
app.registering = False
|
|
157
|
+
if not app.commands:
|
|
158
|
+
raise RuntimeError("入口未注册任何命令")
|
|
159
|
+
|
|
160
|
+
descriptor = {
|
|
161
|
+
"type": "ready",
|
|
162
|
+
"commands": [
|
|
163
|
+
{"signature": item["signature"], "description": item["description"]}
|
|
164
|
+
for item in app.commands
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
if plugin_name is not None:
|
|
168
|
+
descriptor["name"] = plugin_name
|
|
169
|
+
if plugin_description is not None:
|
|
170
|
+
descriptor["description"] = plugin_description
|
|
171
|
+
_send(descriptor)
|
|
172
|
+
|
|
173
|
+
if mode == "run":
|
|
174
|
+
acknowledgement = _receive()
|
|
175
|
+
if acknowledgement.get("type") != "ready_ack":
|
|
176
|
+
raise RuntimeError("Mearl 宿主未确认插件注册信息")
|
|
177
|
+
command_name = initial.get("command")
|
|
178
|
+
command_input = initial.get("input")
|
|
179
|
+
if not isinstance(command_name, str) or not isinstance(command_input, dict):
|
|
180
|
+
raise RuntimeError("Mearl 宿主发送了无效的命令")
|
|
181
|
+
selected = next(
|
|
182
|
+
(item for item in app.commands if item["signature"].strip().split()[0] == command_name),
|
|
183
|
+
None,
|
|
184
|
+
)
|
|
185
|
+
if selected is None:
|
|
186
|
+
raise RuntimeError("未知子命令: " + command_name)
|
|
187
|
+
phase = "run"
|
|
188
|
+
app.executing = True
|
|
189
|
+
try:
|
|
190
|
+
result = _resolve(selected["handler"](command_input))
|
|
191
|
+
finally:
|
|
192
|
+
app.executing = False
|
|
193
|
+
_send({"type": "result", "result": result})
|
|
194
|
+
except BaseException as error:
|
|
195
|
+
response = {"type": "error", "phase": phase, "message": _error_message(error)}
|
|
196
|
+
code = getattr(error, "code", None)
|
|
197
|
+
if isinstance(code, str):
|
|
198
|
+
response["code"] = code
|
|
199
|
+
try:
|
|
200
|
+
_send(response)
|
|
201
|
+
except BaseException as serialization_error:
|
|
202
|
+
_send({
|
|
203
|
+
"type": "error",
|
|
204
|
+
"phase": phase,
|
|
205
|
+
"message": "返回值无法 JSON 序列化: " + _error_message(serialization_error),
|
|
206
|
+
})
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
`;
|
|
209
|
+
function pythonExecutable() {
|
|
210
|
+
return process.env.MEARL_PYTHON?.trim() || 'python3';
|
|
211
|
+
}
|
|
212
|
+
function isObject(value) {
|
|
213
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
214
|
+
}
|
|
215
|
+
function parseDescriptor(message) {
|
|
216
|
+
if (!Array.isArray(message.commands))
|
|
217
|
+
throw new Error('Python 插件未返回命令列表');
|
|
218
|
+
const commands = message.commands.map(value => {
|
|
219
|
+
if (!isObject(value) ||
|
|
220
|
+
typeof value.signature !== 'string' ||
|
|
221
|
+
typeof value.description !== 'string') {
|
|
222
|
+
throw new Error('Python 插件返回了无效的命令描述');
|
|
223
|
+
}
|
|
224
|
+
return { signature: value.signature, description: value.description };
|
|
225
|
+
});
|
|
226
|
+
if (message.name !== undefined && typeof message.name !== 'string') {
|
|
227
|
+
throw new Error('Python 插件返回了无效的 name');
|
|
228
|
+
}
|
|
229
|
+
if (message.description !== undefined && typeof message.description !== 'string') {
|
|
230
|
+
throw new Error('Python 插件返回了无效的 description');
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
commands,
|
|
234
|
+
...(typeof message.name === 'string' ? { name: message.name } : {}),
|
|
235
|
+
...(typeof message.description === 'string' ? { description: message.description } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function bridgedError(message) {
|
|
239
|
+
const detail = typeof message.message === 'string' ? message.message : 'Python 插件执行失败';
|
|
240
|
+
if (typeof message.code === 'string' && /^PLUGIN_[A-Z0-9_]+$/.test(message.code)) {
|
|
241
|
+
return new PluginError(message.code, detail);
|
|
242
|
+
}
|
|
243
|
+
return new Error(detail);
|
|
244
|
+
}
|
|
245
|
+
function errorResponse(error) {
|
|
246
|
+
if (error instanceof PluginError) {
|
|
247
|
+
const prefix = `${error.code}: `;
|
|
248
|
+
return {
|
|
249
|
+
code: error.code,
|
|
250
|
+
message: error.message.startsWith(prefix)
|
|
251
|
+
? error.message.slice(prefix.length)
|
|
252
|
+
: error.message,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
256
|
+
}
|
|
257
|
+
async function runPythonWorker(entry, initial, app) {
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const executable = pythonExecutable();
|
|
260
|
+
const child = spawn(executable, ['-u', '-c', PYTHON_BOOTSTRAP, entry], {
|
|
261
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
262
|
+
windowsHide: true,
|
|
263
|
+
});
|
|
264
|
+
const lines = createInterface({ input: child.stdout });
|
|
265
|
+
let descriptor;
|
|
266
|
+
let outcome;
|
|
267
|
+
let protocolError;
|
|
268
|
+
let stderr = '';
|
|
269
|
+
const failProtocol = (error) => {
|
|
270
|
+
if (protocolError)
|
|
271
|
+
return;
|
|
272
|
+
protocolError = error instanceof Error ? error : new Error(String(error));
|
|
273
|
+
child.kill();
|
|
274
|
+
};
|
|
275
|
+
const write = (message) => {
|
|
276
|
+
try {
|
|
277
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
failProtocol(error);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
child.stderr.setEncoding('utf8');
|
|
284
|
+
child.stderr.on('data', chunk => {
|
|
285
|
+
stderr = `${stderr}${chunk}`.slice(-STDERR_LIMIT);
|
|
286
|
+
});
|
|
287
|
+
child.stdin.on('error', () => undefined);
|
|
288
|
+
child.once('error', error => {
|
|
289
|
+
if (error.code === 'ENOENT') {
|
|
290
|
+
reject(new Error(`未找到 ${executable},Python 插件需要 Python 3.9 或更高版本` +
|
|
291
|
+
'(可通过 MEARL_PYTHON 指定解释器)'));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
reject(error);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
lines.on('line', line => {
|
|
298
|
+
let message;
|
|
299
|
+
try {
|
|
300
|
+
const value = JSON.parse(line);
|
|
301
|
+
if (!isObject(value) || typeof value.type !== 'string') {
|
|
302
|
+
throw new Error('消息不是对象');
|
|
303
|
+
}
|
|
304
|
+
message = value;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
failProtocol(new Error(`Python 插件输出了无效协议消息: ${line.slice(0, 200)}`, error instanceof Error ? { cause: error } : undefined));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (message.type === 'ready') {
|
|
311
|
+
try {
|
|
312
|
+
if (descriptor)
|
|
313
|
+
throw new Error('Python 插件重复发送 ready');
|
|
314
|
+
descriptor = parseDescriptor(message);
|
|
315
|
+
if (initial.mode === 'run')
|
|
316
|
+
write({ type: 'ready_ack' });
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
failProtocol(error);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (message.type === 'progress') {
|
|
324
|
+
if (!app || typeof message.message !== 'string') {
|
|
325
|
+
failProtocol(new Error('Python 插件发送了无效的 progress 消息'));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
app.progress(message.message);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
failProtocol(error);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (message.type === 'invoke') {
|
|
337
|
+
if (!app ||
|
|
338
|
+
typeof message.id !== 'number' ||
|
|
339
|
+
typeof message.action !== 'string' ||
|
|
340
|
+
!isObject(message.payload) ||
|
|
341
|
+
!isObject(message.options)) {
|
|
342
|
+
failProtocol(new Error('Python 插件发送了无效的 invoke 消息'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
void app
|
|
346
|
+
.invoke(message.action, message.payload, message.options)
|
|
347
|
+
.then(result => {
|
|
348
|
+
write({
|
|
349
|
+
type: 'response',
|
|
350
|
+
id: message.id,
|
|
351
|
+
result: result === undefined ? null : result,
|
|
352
|
+
});
|
|
353
|
+
})
|
|
354
|
+
.catch(error => {
|
|
355
|
+
write({ type: 'response', id: message.id, error: errorResponse(error) });
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (message.type === 'result') {
|
|
360
|
+
outcome = { hasResult: true, result: message.result };
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (message.type === 'error') {
|
|
364
|
+
outcome = { error: bridgedError(message) };
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
failProtocol(new Error(`Python 插件发送了未知消息类型: ${message.type}`));
|
|
368
|
+
});
|
|
369
|
+
child.once('close', code => {
|
|
370
|
+
lines.close();
|
|
371
|
+
if (protocolError) {
|
|
372
|
+
reject(protocolError);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (outcome?.error) {
|
|
376
|
+
reject(outcome.error);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (code !== 0) {
|
|
380
|
+
reject(new Error(stderr.trim() || `Python 插件进程异常退出${code === null ? '' : ` (exit ${code})`}`));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!descriptor) {
|
|
384
|
+
reject(new Error('Python 插件未返回注册信息'));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (initial.mode === 'run' && !outcome?.hasResult) {
|
|
388
|
+
reject(new Error('Python 插件未返回执行结果'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
resolve({ descriptor, ...(outcome?.hasResult ? { result: outcome.result } : {}) });
|
|
392
|
+
});
|
|
393
|
+
write(initial);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
export async function describePythonPlugin(entry, options) {
|
|
397
|
+
const { descriptor } = await runPythonWorker(entry, { mode: 'describe', options });
|
|
398
|
+
return descriptor;
|
|
399
|
+
}
|
|
400
|
+
export async function runPythonPluginCommand(entry, command, input, app) {
|
|
401
|
+
const { result } = await runPythonWorker(entry, { mode: 'run', command, input, options: app.options }, app);
|
|
402
|
+
return result;
|
|
403
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
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/cloud-types": "2.
|
|
63
|
-
"@mearl/provider": "2.
|
|
62
|
+
"@mearl/cloud-types": "2.16.0",
|
|
63
|
+
"@mearl/provider": "2.16.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.9.1",
|