@mearl/client 2.15.0 → 2.15.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 +7 -12
- package/dist/cli.js +20 -3
- package/dist/officialPluginCatalog.d.ts +79 -0
- package/dist/officialPluginCatalog.js +80 -0
- package/dist/officialPluginInstaller.d.ts +3 -0
- package/dist/officialPluginInstaller.js +130 -0
- package/dist/plugin.d.ts +4 -7
- package/dist/plugin.js +45 -25
- package/dist/socket.d.ts +3 -2
- package/dist/socket.js +79 -36
- package/package.json +3 -3
- package/dist/buc.d.ts +0 -65
- package/dist/buc.js +0 -533
package/README.md
CHANGED
|
@@ -134,6 +134,7 @@ Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通
|
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
npx @mearl/setup plugin install trip/mearl --skill yuque-doc-fetch --yes
|
|
137
|
+
npx @mearl/setup plugin install trip/mearl --skill buc-request --yes
|
|
137
138
|
npx @mearl/setup plugin yuque /absolute/path/to/yuque-doc-fetch
|
|
138
139
|
npx @mearl/setup plugin uninstall yuque
|
|
139
140
|
mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
|
|
@@ -144,7 +145,7 @@ mearl yuque --version
|
|
|
144
145
|
|
|
145
146
|
目录默认使用 `scripts/main.mjs`。入口默认导出 `register(app)`,可导出简短的 `description`
|
|
146
147
|
显示在 `mearl` 插件列表中,并通过 `app.command(signature, handler, description?)`
|
|
147
|
-
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)`
|
|
148
|
+
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。调用另一个插件时,payload 使用 `{ command, input }` envelope;嵌套调用继承宿主选项并拒绝循环依赖。CLI 默认只输出最终结果,传入
|
|
148
149
|
`--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
|
|
149
150
|
自动注册默认以 skill 安装目录名作为命令名,也可在入口导出 `name` 指定短名称。
|
|
150
151
|
setup 会补齐缺失或低于自身版本的 client、native-host;安装参数透传给 Ali Skills,并固定全局共享安装。
|
|
@@ -168,7 +169,7 @@ const result = await invokePlugin(
|
|
|
168
169
|
);
|
|
169
170
|
```
|
|
170
171
|
|
|
171
|
-
MCP Server 通过固定工具 `mearl_plugin` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP
|
|
172
|
+
MCP Server 通过固定工具 `mearl_plugin` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP 不接受任意入口路径;调用官方目录中尚未注册的插件时,会通过匹配当前 client 版本的 setup 自动安装并重试一次。未知名称仍返回 `PLUGIN_NOT_REGISTERED`。
|
|
172
173
|
|
|
173
174
|
注册记录在 `~/.mearl/plugins/<name>.json`,保存入口路径和可选的简短描述;skill 业务代码原地更新直接生效,换路径或更新描述后重新注册。
|
|
174
175
|
`plugin uninstall <name>` 会对 `~/.agents/skills` 中的全局 Skill 调用 `ali-skills remove --global --yes`,
|
|
@@ -178,16 +179,10 @@ MCP Server 通过固定工具 `mearl_plugin` 接受同样的 `plugin`、`command
|
|
|
178
179
|
卸载清理命令。其他错误按实际原因处理。
|
|
179
180
|
编写规范见 [Mearl Skill Creator 的 Skill 插件规范](../skills/mearl-skill-creator/references/cli-plugin.md)。
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
`
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`app.buc.whoami()` 返回当前身份的
|
|
186
|
-
`empId` 和姓名,并在 Agent 身份场景返回 `agentId`;仅在确实需要部门等详细字段时使用
|
|
187
|
-
`app.buc.profile()`,它通过一次性 `alineiwai` Ticket
|
|
188
|
-
建立内存会话、校验资料属于当前员工,并只返回白名单字段,Ticket 与 Cookie 均不暴露给插件。
|
|
189
|
-
缺少 BUC CLI 时 Mearl 会从官方 CLI Hub 安装后重试。这些方法与 `app.invoke` 一样,只能在插件 handler
|
|
190
|
-
执行期间调用。BUC 请求始终在运行 CLI、SDK 或 MCP Server 的机器上执行,不会随浏览器或 connector 转发到远端浏览器宿主。
|
|
182
|
+
需要 BUC 身份的内部 HTTP 接口通过官方 `buc` 插件调用。CLI 使用 `mearl buc request`,业务插件使用
|
|
183
|
+
`app.invoke('buc', { command: 'request', input })`;当前身份和人员资料分别使用 `whoami` 与 `profile`
|
|
184
|
+
子命令。BUC 插件按请求创建一次性凭据,只允许访问 HTTPS `alibaba-inc.com` 域名,过滤认证字段与
|
|
185
|
+
Cookie,并始终在运行 CLI、SDK 或 MCP Server 的机器上执行,不随浏览器或 connector 转发。
|
|
191
186
|
|
|
192
187
|
## 支持的操作
|
|
193
188
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { commandInput, findCommandSuggestions, formatCommandSuggestions, } from
|
|
|
8
8
|
import { COMMANDS, COMMAND_MAP } from './commands.js';
|
|
9
9
|
import { CLIENT_VERSION } from './socket.js';
|
|
10
10
|
import { UnifiedClient } from './unified.js';
|
|
11
|
-
import { findPluginEntry, findPluginRegistration, listPluginNames, loadRegisteredPlugin, PluginError, } from './plugin.js';
|
|
11
|
+
import { findPluginEntry, findPluginRegistration, findOfficialPlugin, listPluginNames, listOfficialPluginNames, loadRegisteredPlugin, PluginError, } from './plugin.js';
|
|
12
12
|
import { getCloudServerConfigPath, loadCloudServerConfig } from './unifiedConfig.js';
|
|
13
13
|
const TOP_LEVEL_COMMAND_NAME_WIDTH = 22;
|
|
14
14
|
const CATEGORIES = [
|
|
@@ -59,7 +59,16 @@ const BUILTIN_COMMANDS = [
|
|
|
59
59
|
examples: ['check'],
|
|
60
60
|
},
|
|
61
61
|
];
|
|
62
|
-
const
|
|
62
|
+
const OFFICIAL_PLUGIN_COMMANDS = listOfficialPluginNames().map(name => ({
|
|
63
|
+
name,
|
|
64
|
+
description: findOfficialPlugin(name).description,
|
|
65
|
+
examples: [name],
|
|
66
|
+
}));
|
|
67
|
+
const SUGGESTABLE_COMMANDS = [
|
|
68
|
+
...COMMANDS,
|
|
69
|
+
...BUILTIN_COMMANDS,
|
|
70
|
+
...OFFICIAL_PLUGIN_COMMANDS,
|
|
71
|
+
];
|
|
63
72
|
const ADVANCED_BUILTIN_COMMANDS = new Set(['connector_list']);
|
|
64
73
|
function printUsage(commandName, version) {
|
|
65
74
|
const lines = [
|
|
@@ -113,6 +122,14 @@ function printUsage(commandName, version) {
|
|
|
113
122
|
});
|
|
114
123
|
lines.push('', '已注册插件:', ...entries);
|
|
115
124
|
}
|
|
125
|
+
const installed = new Set(plugins);
|
|
126
|
+
const available = listOfficialPluginNames().filter(name => !installed.has(name));
|
|
127
|
+
if (available.length) {
|
|
128
|
+
lines.push('', '官方插件(首次调用自动安装):', ...available.map(name => {
|
|
129
|
+
const description = findOfficialPlugin(name).description;
|
|
130
|
+
return ` ${name.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}${description}`;
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
116
133
|
}
|
|
117
134
|
catch (error) {
|
|
118
135
|
lines.push('', error instanceof Error ? error.message : String(error));
|
|
@@ -519,7 +536,7 @@ export async function runMearlCli(args, options = {}) {
|
|
|
519
536
|
!COMMAND_MAP.has(args[0])) {
|
|
520
537
|
try {
|
|
521
538
|
const entry = findPluginEntry(args[0]);
|
|
522
|
-
if (entry)
|
|
539
|
+
if (entry || findOfficialPlugin(args[0]))
|
|
523
540
|
return await runPluginCli(args, commandName);
|
|
524
541
|
}
|
|
525
542
|
catch (error) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare const OFFICIAL_PLUGIN_MANAGER = "@mearl/setup:official";
|
|
2
|
+
export interface OfficialPluginDefinition {
|
|
3
|
+
description: string;
|
|
4
|
+
installArgs: readonly string[];
|
|
5
|
+
setup?: 'alibaba';
|
|
6
|
+
skill: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const OFFICIAL_PLUGINS: {
|
|
9
|
+
readonly buc: {
|
|
10
|
+
readonly description: "使用 BUC 身份请求内部服务";
|
|
11
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "buc-request", "--yes"];
|
|
12
|
+
readonly setup: "alibaba";
|
|
13
|
+
readonly skill: "buc-request";
|
|
14
|
+
};
|
|
15
|
+
readonly tdbank: {
|
|
16
|
+
readonly description: "查询、借用和切换 TDBank 测试账号";
|
|
17
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "tdbank-account", "--yes"];
|
|
18
|
+
readonly setup: "alibaba";
|
|
19
|
+
readonly skill: "tdbank-account";
|
|
20
|
+
};
|
|
21
|
+
readonly agentforce: {
|
|
22
|
+
readonly description: "管理 AgentForce 数字员工";
|
|
23
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "agentforce-manage", "--yes"];
|
|
24
|
+
readonly skill: "agentforce-manage";
|
|
25
|
+
};
|
|
26
|
+
readonly aliway: {
|
|
27
|
+
readonly description: "读取和管理 Aliway 内容";
|
|
28
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "aliway-doc-fetch", "--yes"];
|
|
29
|
+
readonly skill: "aliway-doc-fetch";
|
|
30
|
+
};
|
|
31
|
+
readonly ata: {
|
|
32
|
+
readonly description: "读取和管理 ATA 内容";
|
|
33
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "ata-doc-fetch", "--yes"];
|
|
34
|
+
readonly skill: "ata-doc-fetch";
|
|
35
|
+
};
|
|
36
|
+
readonly code: {
|
|
37
|
+
readonly description: "读取 code.alibaba-inc.com 代码";
|
|
38
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "code-file-fetch", "--yes"];
|
|
39
|
+
readonly skill: "code-file-fetch";
|
|
40
|
+
};
|
|
41
|
+
readonly dingtalk: {
|
|
42
|
+
readonly description: "读取钉钉文档与表格";
|
|
43
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "dingtalk-doc-fetch", "--yes"];
|
|
44
|
+
readonly skill: "dingtalk-doc-fetch";
|
|
45
|
+
};
|
|
46
|
+
readonly figma: {
|
|
47
|
+
readonly description: "提取 Figma 设计数据与素材";
|
|
48
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "figma-design-fetch", "--yes"];
|
|
49
|
+
readonly skill: "figma-design-fetch";
|
|
50
|
+
};
|
|
51
|
+
readonly flyeye: {
|
|
52
|
+
readonly description: "查询 FlyEye 分布式链路日志";
|
|
53
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "flyeye-trace-query", "--yes"];
|
|
54
|
+
readonly skill: "flyeye-trace-query";
|
|
55
|
+
};
|
|
56
|
+
readonly jarvis: {
|
|
57
|
+
readonly description: "查询和处理 Jarvis 机票退改";
|
|
58
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "jarvis-order-operations", "--yes"];
|
|
59
|
+
readonly skill: "jarvis-order-operations";
|
|
60
|
+
};
|
|
61
|
+
readonly loginsight: {
|
|
62
|
+
readonly description: "分析 LogInsight 全链路日志";
|
|
63
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "loginsight-analyze", "--yes"];
|
|
64
|
+
readonly skill: "loginsight-analyze";
|
|
65
|
+
};
|
|
66
|
+
readonly tps: {
|
|
67
|
+
readonly description: "上传 TPS 图片并获取 CDN 地址";
|
|
68
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "tps-image-upload", "--yes"];
|
|
69
|
+
readonly skill: "tps-image-upload";
|
|
70
|
+
};
|
|
71
|
+
readonly yuque: {
|
|
72
|
+
readonly description: "读取和管理阿里语雀文档";
|
|
73
|
+
readonly installArgs: readonly ["trip/mearl", "--skill", "yuque-doc-fetch", "--yes"];
|
|
74
|
+
readonly skill: "yuque-doc-fetch";
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export type OfficialPluginName = keyof typeof OFFICIAL_PLUGINS;
|
|
78
|
+
export declare function findOfficialPlugin(name: string): OfficialPluginDefinition | undefined;
|
|
79
|
+
export declare function listOfficialPluginNames(setup?: OfficialPluginDefinition['setup']): string[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const OFFICIAL_PLUGIN_MANAGER = '@mearl/setup:official';
|
|
2
|
+
export const OFFICIAL_PLUGINS = {
|
|
3
|
+
buc: {
|
|
4
|
+
description: '使用 BUC 身份请求内部服务',
|
|
5
|
+
installArgs: ['trip/mearl', '--skill', 'buc-request', '--yes'],
|
|
6
|
+
setup: 'alibaba',
|
|
7
|
+
skill: 'buc-request',
|
|
8
|
+
},
|
|
9
|
+
tdbank: {
|
|
10
|
+
description: '查询、借用和切换 TDBank 测试账号',
|
|
11
|
+
installArgs: ['trip/mearl', '--skill', 'tdbank-account', '--yes'],
|
|
12
|
+
setup: 'alibaba',
|
|
13
|
+
skill: 'tdbank-account',
|
|
14
|
+
},
|
|
15
|
+
agentforce: {
|
|
16
|
+
description: '管理 AgentForce 数字员工',
|
|
17
|
+
installArgs: ['trip/mearl', '--skill', 'agentforce-manage', '--yes'],
|
|
18
|
+
skill: 'agentforce-manage',
|
|
19
|
+
},
|
|
20
|
+
aliway: {
|
|
21
|
+
description: '读取和管理 Aliway 内容',
|
|
22
|
+
installArgs: ['trip/mearl', '--skill', 'aliway-doc-fetch', '--yes'],
|
|
23
|
+
skill: 'aliway-doc-fetch',
|
|
24
|
+
},
|
|
25
|
+
ata: {
|
|
26
|
+
description: '读取和管理 ATA 内容',
|
|
27
|
+
installArgs: ['trip/mearl', '--skill', 'ata-doc-fetch', '--yes'],
|
|
28
|
+
skill: 'ata-doc-fetch',
|
|
29
|
+
},
|
|
30
|
+
code: {
|
|
31
|
+
description: '读取 code.alibaba-inc.com 代码',
|
|
32
|
+
installArgs: ['trip/mearl', '--skill', 'code-file-fetch', '--yes'],
|
|
33
|
+
skill: 'code-file-fetch',
|
|
34
|
+
},
|
|
35
|
+
dingtalk: {
|
|
36
|
+
description: '读取钉钉文档与表格',
|
|
37
|
+
installArgs: ['trip/mearl', '--skill', 'dingtalk-doc-fetch', '--yes'],
|
|
38
|
+
skill: 'dingtalk-doc-fetch',
|
|
39
|
+
},
|
|
40
|
+
figma: {
|
|
41
|
+
description: '提取 Figma 设计数据与素材',
|
|
42
|
+
installArgs: ['trip/mearl', '--skill', 'figma-design-fetch', '--yes'],
|
|
43
|
+
skill: 'figma-design-fetch',
|
|
44
|
+
},
|
|
45
|
+
flyeye: {
|
|
46
|
+
description: '查询 FlyEye 分布式链路日志',
|
|
47
|
+
installArgs: ['trip/mearl', '--skill', 'flyeye-trace-query', '--yes'],
|
|
48
|
+
skill: 'flyeye-trace-query',
|
|
49
|
+
},
|
|
50
|
+
jarvis: {
|
|
51
|
+
description: '查询和处理 Jarvis 机票退改',
|
|
52
|
+
installArgs: ['trip/mearl', '--skill', 'jarvis-order-operations', '--yes'],
|
|
53
|
+
skill: 'jarvis-order-operations',
|
|
54
|
+
},
|
|
55
|
+
loginsight: {
|
|
56
|
+
description: '分析 LogInsight 全链路日志',
|
|
57
|
+
installArgs: ['trip/mearl', '--skill', 'loginsight-analyze', '--yes'],
|
|
58
|
+
skill: 'loginsight-analyze',
|
|
59
|
+
},
|
|
60
|
+
tps: {
|
|
61
|
+
description: '上传 TPS 图片并获取 CDN 地址',
|
|
62
|
+
installArgs: ['trip/mearl', '--skill', 'tps-image-upload', '--yes'],
|
|
63
|
+
skill: 'tps-image-upload',
|
|
64
|
+
},
|
|
65
|
+
yuque: {
|
|
66
|
+
description: '读取和管理阿里语雀文档',
|
|
67
|
+
installArgs: ['trip/mearl', '--skill', 'yuque-doc-fetch', '--yes'],
|
|
68
|
+
skill: 'yuque-doc-fetch',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export function findOfficialPlugin(name) {
|
|
72
|
+
return Object.hasOwn(OFFICIAL_PLUGINS, name)
|
|
73
|
+
? OFFICIAL_PLUGINS[name]
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
export function listOfficialPluginNames(setup) {
|
|
77
|
+
return Object.entries(OFFICIAL_PLUGINS)
|
|
78
|
+
.filter(([, definition]) => setup === undefined || ('setup' in definition && definition.setup === setup))
|
|
79
|
+
.map(([name]) => name);
|
|
80
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { findOfficialPlugin } from './officialPluginCatalog.js';
|
|
6
|
+
import { findPluginRegistration, pluginDirectory } from './pluginRegistry.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const CLIENT_VERSION = require('../package.json').version;
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 180_000;
|
|
10
|
+
const INSTALL_LOCK_STALE_MS = 5 * 60_000;
|
|
11
|
+
const MAX_INSTALL_OUTPUT = 1024 * 1024;
|
|
12
|
+
const activeInstallations = new Map();
|
|
13
|
+
function delay(milliseconds) {
|
|
14
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
15
|
+
}
|
|
16
|
+
function installLockPath(name) {
|
|
17
|
+
return join(pluginDirectory(), `.official-${name}.install.lock`);
|
|
18
|
+
}
|
|
19
|
+
async function acquireInstallLock(name) {
|
|
20
|
+
const directory = pluginDirectory();
|
|
21
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
22
|
+
const lockPath = installLockPath(name);
|
|
23
|
+
const deadline = Date.now() + INSTALL_TIMEOUT_MS;
|
|
24
|
+
while (true) {
|
|
25
|
+
let descriptor;
|
|
26
|
+
try {
|
|
27
|
+
descriptor = openSync(lockPath, 'wx', 0o600);
|
|
28
|
+
writeFileSync(descriptor, `${process.pid}\n`);
|
|
29
|
+
closeSync(descriptor);
|
|
30
|
+
return () => rmSync(lockPath, { force: true });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (descriptor !== undefined)
|
|
34
|
+
closeSync(descriptor);
|
|
35
|
+
if (error.code !== 'EEXIST')
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (findPluginRegistration(name))
|
|
39
|
+
return () => undefined;
|
|
40
|
+
try {
|
|
41
|
+
if (Date.now() - statSync(lockPath).mtimeMs > INSTALL_LOCK_STALE_MS) {
|
|
42
|
+
rmSync(lockPath, { force: true });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code === 'ENOENT')
|
|
48
|
+
continue;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (Date.now() >= deadline) {
|
|
52
|
+
throw new Error(`等待安装官方插件 ${name} 超时`);
|
|
53
|
+
}
|
|
54
|
+
await delay(250);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function appendOutput(current, chunk) {
|
|
58
|
+
if (current.length >= MAX_INSTALL_OUTPUT)
|
|
59
|
+
return current;
|
|
60
|
+
return (current + chunk.toString('utf8')).slice(0, MAX_INSTALL_OUTPUT);
|
|
61
|
+
}
|
|
62
|
+
function installWithSetup(name) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
65
|
+
const child = spawn(command, ['-y', `@mearl/setup@${CLIENT_VERSION}`, 'plugin', 'ensure', name], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
66
|
+
let stdout = '';
|
|
67
|
+
let stderr = '';
|
|
68
|
+
let settled = false;
|
|
69
|
+
const finish = (error) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
settled = true;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
if (error)
|
|
75
|
+
reject(error);
|
|
76
|
+
else
|
|
77
|
+
resolve();
|
|
78
|
+
};
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
child.kill('SIGTERM');
|
|
81
|
+
finish(new Error(`安装官方插件 ${name} 超时`));
|
|
82
|
+
}, INSTALL_TIMEOUT_MS);
|
|
83
|
+
child.stdout.on('data', chunk => (stdout = appendOutput(stdout, chunk)));
|
|
84
|
+
child.stderr.on('data', chunk => (stderr = appendOutput(stderr, chunk)));
|
|
85
|
+
child.once('error', error => finish(error));
|
|
86
|
+
child.once('exit', code => {
|
|
87
|
+
if (code === 0) {
|
|
88
|
+
finish();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const detail = (stderr || stdout)
|
|
92
|
+
.replace(/[\r\n]+/g, ' ')
|
|
93
|
+
.trim()
|
|
94
|
+
.slice(-1000);
|
|
95
|
+
finish(new Error(detail
|
|
96
|
+
? `安装官方插件 ${name} 失败 (exit ${code}): ${detail}`
|
|
97
|
+
: `安装官方插件 ${name} 失败 (exit ${code})`));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function installOfficialPlugin(name, progress) {
|
|
102
|
+
if (findPluginRegistration(name))
|
|
103
|
+
return;
|
|
104
|
+
if (!findOfficialPlugin(name))
|
|
105
|
+
throw new Error(`未知官方插件: ${name}`);
|
|
106
|
+
const releaseLock = await acquireInstallLock(name);
|
|
107
|
+
try {
|
|
108
|
+
if (findPluginRegistration(name))
|
|
109
|
+
return;
|
|
110
|
+
progress?.(`正在安装官方插件 ${name}...`);
|
|
111
|
+
await installWithSetup(name);
|
|
112
|
+
if (!findPluginRegistration(name)) {
|
|
113
|
+
throw new Error(`官方插件 ${name} 安装完成后仍未注册`);
|
|
114
|
+
}
|
|
115
|
+
progress?.(`官方插件 ${name} 已安装。`);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
releaseLock();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export const ensureOfficialPluginRegistered = (name, progress) => {
|
|
122
|
+
const active = activeInstallations.get(name);
|
|
123
|
+
if (active)
|
|
124
|
+
return active;
|
|
125
|
+
const installation = installOfficialPlugin(name, progress).finally(() => {
|
|
126
|
+
activeInstallations.delete(name);
|
|
127
|
+
});
|
|
128
|
+
activeInstallations.set(name, installation);
|
|
129
|
+
return installation;
|
|
130
|
+
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
|
|
2
2
|
import type { UnifiedInvokeOptions } from './unified.js';
|
|
3
|
-
export { type BucApi, type BucIdentity, type BucProfile, type BucRequestAuth, type BucRequestFormValue, type BucRequestParams, type BucRequestResult, } from './buc.js';
|
|
4
3
|
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, type PluginRegistrationOptions, } from './pluginRegistry.js';
|
|
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 {
|
|
7
7
|
command: string;
|
|
@@ -11,21 +11,18 @@ export interface PluginInvokeOptions extends UnifiedInvokeOptions {
|
|
|
11
11
|
onProgress?: (message: string) => void;
|
|
12
12
|
}
|
|
13
13
|
export interface PluginExecutionContext {
|
|
14
|
-
|
|
14
|
+
ensurePlugin?: OfficialPluginInstaller;
|
|
15
15
|
invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
16
16
|
}
|
|
17
17
|
export interface PluginApp {
|
|
18
18
|
readonly options: Readonly<PluginOptions>;
|
|
19
|
-
readonly buc: BucApi;
|
|
20
19
|
command(signature: string, handler: PluginHandler, description?: string): void;
|
|
21
20
|
invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
22
21
|
progress(message: string): void;
|
|
23
22
|
}
|
|
24
23
|
export type PluginHandler = (input: Record<string, any>) => unknown | Promise<unknown>;
|
|
25
24
|
/** 注册和执行共享一个 app;仅 handler 执行期间开放浏览器调用。 */
|
|
26
|
-
export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'
|
|
27
|
-
buc?: BucApi;
|
|
28
|
-
}, pluginName?: string): Promise<{
|
|
25
|
+
export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'>, pluginName?: string): Promise<{
|
|
29
26
|
name: string | undefined;
|
|
30
27
|
description: string | undefined;
|
|
31
28
|
version: string | undefined;
|
package/dist/plugin.js
CHANGED
|
@@ -2,9 +2,12 @@ import { readFileSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
-
import {
|
|
5
|
+
import { COMMAND_MAP } from './commands.js';
|
|
6
|
+
import { findOfficialPlugin } from './officialPluginCatalog.js';
|
|
7
|
+
import { ensureOfficialPluginRegistered, } from './officialPluginInstaller.js';
|
|
6
8
|
import { findPluginRegistration, PluginError } from './pluginRegistry.js';
|
|
7
9
|
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
|
|
10
|
+
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, } from './officialPluginCatalog.js';
|
|
8
11
|
function readPluginVersion(entry) {
|
|
9
12
|
try {
|
|
10
13
|
const content = readFileSync(resolve(dirname(entry), '..', 'SKILL.md'), 'utf8');
|
|
@@ -19,6 +22,9 @@ function readPluginVersion(entry) {
|
|
|
19
22
|
function isObject(value) {
|
|
20
23
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
24
|
}
|
|
25
|
+
function isPluginInvocation(value) {
|
|
26
|
+
return isObject(value) && typeof value.command === 'string' && isObject(value.input);
|
|
27
|
+
}
|
|
22
28
|
function missingPluginEntryMessage(entry, pluginName) {
|
|
23
29
|
const name = pluginName ?? '<name>';
|
|
24
30
|
return [
|
|
@@ -49,23 +55,6 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
49
55
|
let description;
|
|
50
56
|
const app = Object.freeze({
|
|
51
57
|
options: Object.freeze({ ...context.options }),
|
|
52
|
-
buc: Object.freeze({
|
|
53
|
-
request(params) {
|
|
54
|
-
if (!executing)
|
|
55
|
-
throw new Error('app.buc.request 只能在命令执行期间调用');
|
|
56
|
-
return (context.buc ?? buc).request(params);
|
|
57
|
-
},
|
|
58
|
-
whoami() {
|
|
59
|
-
if (!executing)
|
|
60
|
-
throw new Error('app.buc.whoami 只能在命令执行期间调用');
|
|
61
|
-
return (context.buc ?? buc).whoami();
|
|
62
|
-
},
|
|
63
|
-
profile() {
|
|
64
|
-
if (!executing)
|
|
65
|
-
throw new Error('app.buc.profile 只能在命令执行期间调用');
|
|
66
|
-
return (context.buc ?? buc).profile();
|
|
67
|
-
},
|
|
68
|
-
}),
|
|
69
58
|
command(signature, handler, description = '') {
|
|
70
59
|
if (!registering)
|
|
71
60
|
throw new Error('命令只能在入口注册阶段声明');
|
|
@@ -194,21 +183,46 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
194
183
|
},
|
|
195
184
|
};
|
|
196
185
|
}
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
async function loadRegisteredPluginWithState(name, context, options = {}, state = { stack: [] }) {
|
|
187
|
+
if (state.stack.includes(name)) {
|
|
188
|
+
throw new PluginError('PLUGIN_INVOCATION_CYCLE', `插件调用形成循环: ${[...state.stack, name].join(' -> ')}`);
|
|
189
|
+
}
|
|
190
|
+
let registration = findPluginRegistration(name);
|
|
191
|
+
if (!registration && findOfficialPlugin(name)) {
|
|
192
|
+
await (context.ensurePlugin ?? ensureOfficialPluginRegistered)(name, options.onProgress);
|
|
193
|
+
registration = findPluginRegistration(name);
|
|
194
|
+
}
|
|
199
195
|
if (!registration) {
|
|
200
196
|
throw new PluginError('PLUGIN_NOT_REGISTERED', `插件未注册: ${name}`);
|
|
201
197
|
}
|
|
202
198
|
const { onProgress, ...invokeOptions } = options;
|
|
199
|
+
const childState = { stack: [...state.stack, name] };
|
|
203
200
|
return loadPlugin(registration.entry, {
|
|
204
201
|
options: invokeOptions,
|
|
205
|
-
...(context.buc ? { buc: context.buc } : {}),
|
|
206
202
|
progress: onProgress ?? (() => undefined),
|
|
207
203
|
async invoke(action, payload, overrides) {
|
|
208
|
-
const
|
|
204
|
+
const nestedOptions = {
|
|
209
205
|
...invokeOptions,
|
|
210
206
|
...overrides,
|
|
211
|
-
}
|
|
207
|
+
};
|
|
208
|
+
if (COMMAND_MAP.has(action) || action === 'connector_list') {
|
|
209
|
+
const result = await context.invoke(action, payload, nestedOptions);
|
|
210
|
+
if (result?.action?.success === false) {
|
|
211
|
+
throw new Error(result.action.error || `${action} failed`);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
const pluginInvocation = isPluginInvocation(payload);
|
|
216
|
+
if (findPluginRegistration(action) || findOfficialPlugin(action) || pluginInvocation) {
|
|
217
|
+
if (!pluginInvocation) {
|
|
218
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `调用插件 ${action} 需要 {"command":"...","input":{...}}`);
|
|
219
|
+
}
|
|
220
|
+
return invokeRegisteredPluginWithState(action, payload, context, {
|
|
221
|
+
...nestedOptions,
|
|
222
|
+
...(onProgress ? { onProgress } : {}),
|
|
223
|
+
}, childState);
|
|
224
|
+
}
|
|
225
|
+
const result = await context.invoke(action, payload, nestedOptions);
|
|
212
226
|
if (result?.action?.success === false) {
|
|
213
227
|
throw new Error(result.action.error || `${action} failed`);
|
|
214
228
|
}
|
|
@@ -216,7 +230,13 @@ export async function loadRegisteredPlugin(name, context, options = {}) {
|
|
|
216
230
|
},
|
|
217
231
|
}, name);
|
|
218
232
|
}
|
|
219
|
-
export
|
|
220
|
-
|
|
233
|
+
export function loadRegisteredPlugin(name, context, options = {}) {
|
|
234
|
+
return loadRegisteredPluginWithState(name, context, options);
|
|
235
|
+
}
|
|
236
|
+
async function invokeRegisteredPluginWithState(name, invocation, context, options = {}, state = { stack: [] }) {
|
|
237
|
+
const plugin = await loadRegisteredPluginWithState(name, context, options, state);
|
|
221
238
|
return plugin.run([], invocation);
|
|
222
239
|
}
|
|
240
|
+
export function invokeRegisteredPlugin(name, invocation, context, options = {}) {
|
|
241
|
+
return invokeRegisteredPluginWithState(name, invocation, context, options);
|
|
242
|
+
}
|
package/dist/socket.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* 通过 Unix Domain Socket (macOS/Linux) 或 Named Pipe (Windows) 与 native host 进程通信。
|
|
5
5
|
* 每次调用新建连接,收到响应后关闭,简单可靠。
|
|
6
6
|
*
|
|
7
|
-
* 当 Socket
|
|
7
|
+
* 当 Socket 不存在,或 legacy Socket 是无人监听的残留文件时,
|
|
8
|
+
* 自动尝试拉起 native host,稍后重试连接。
|
|
8
9
|
*/
|
|
9
10
|
export declare const CLIENT_VERSION: string;
|
|
10
11
|
/**
|
|
@@ -34,7 +35,7 @@ export declare function decorateBrowserListResult(result: Record<string, any>):
|
|
|
34
35
|
*
|
|
35
36
|
* 目标解析优先级:显式 options.browser → setDefaultBrowser → 环境变量
|
|
36
37
|
* MEARL_BROWSER → 自动(注册表中最近聚焦的浏览器)。注册表为空时回退到
|
|
37
|
-
* legacy well-known socket,并在 socket
|
|
38
|
+
* legacy well-known socket,并在 socket 不存在或已残留时自动拉起 native host。
|
|
38
39
|
*/
|
|
39
40
|
export declare function invoke(action: string, data?: Record<string, any>, options?: InvokeOptions): Promise<any>;
|
|
40
41
|
export {};
|