@mearl/client 2.15.0 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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)` 报告可选进度。CLI 默认只输出最终结果,传入
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 都只解析本机 registry 中已注册的名称,不接受任意入口路径。
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
- 插件通过 `app.buc.request({ app, auth, url, ... })` 调用需要 BUC 身份的内部 HTTP 接口。`auth` 支持
182
- `id-token`(默认)、`ticket-header` 和兼容旧服务的 `ticket-query`。Mearl 按请求创建一次性凭据并注入
183
- `Authorization` + `DPoP`、`SSO-TICKET` Header 或 `SSO_TICKET` Query,只允许访问 HTTPS
184
- `alibaba-inc.com` 域名;凭据不会返回给插件,也不会复用到下一次请求,调用方也不能自行传入这些认证字段。
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 SUGGESTABLE_COMMANDS = [...COMMANDS, ...BUILTIN_COMMANDS];
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,3 @@
1
+ export type OfficialPluginInstallProgress = (message: string) => void;
2
+ export type OfficialPluginInstaller = (name: string, progress?: OfficialPluginInstallProgress) => Promise<void>;
3
+ export declare const ensureOfficialPluginRegistered: OfficialPluginInstaller;
@@ -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 BucApi } from './buc.js';
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
- buc?: BucApi;
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 { buc } from './buc.js';
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
- export async function loadRegisteredPlugin(name, context, options = {}) {
198
- const registration = findPluginRegistration(name);
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 result = await context.invoke(action, payload, {
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 async function invokeRegisteredPlugin(name, invocation, context, options = {}) {
220
- const plugin = await loadRegisteredPlugin(name, context, options);
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 文件不存在(ENOENT)时,自动尝试拉起 native host,稍后重试连接。
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 不存在(ENOENT)时自动拉起 native host。
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 {};