@mearl/client 2.14.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 +39 -21
- package/dist/actionTimeouts.js +2 -4
- package/dist/check.d.ts +0 -2
- package/dist/check.js +0 -2
- package/dist/cli.js +113 -20
- package/dist/commands.d.ts +20 -120
- package/dist/commands.js +19 -123
- package/dist/generated-browser-action-protocol.d.ts +20 -65
- package/dist/generated-browser-action-protocol.js +0 -9
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -10
- 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 +22 -1
- package/dist/plugin.js +72 -3
- package/dist/pluginRegistry.d.ts +5 -1
- package/dist/pluginRegistry.js +15 -3
- package/dist/socket.d.ts +3 -2
- package/dist/socket.js +79 -36
- package/dist/unified.d.ts +3 -0
- package/dist/unified.js +24 -2
- package/package.json +3 -3
|
@@ -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,6 +1,19 @@
|
|
|
1
|
+
import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
|
|
1
2
|
import type { UnifiedInvokeOptions } from './unified.js';
|
|
2
|
-
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, } from './pluginRegistry.js';
|
|
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';
|
|
3
5
|
export type PluginOptions = UnifiedInvokeOptions;
|
|
6
|
+
export interface PluginInvocation {
|
|
7
|
+
command: string;
|
|
8
|
+
input: Record<string, any>;
|
|
9
|
+
}
|
|
10
|
+
export interface PluginInvokeOptions extends UnifiedInvokeOptions {
|
|
11
|
+
onProgress?: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface PluginExecutionContext {
|
|
14
|
+
ensurePlugin?: OfficialPluginInstaller;
|
|
15
|
+
invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
16
|
+
}
|
|
4
17
|
export interface PluginApp {
|
|
5
18
|
readonly options: Readonly<PluginOptions>;
|
|
6
19
|
command(signature: string, handler: PluginHandler, description?: string): void;
|
|
@@ -16,3 +29,11 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invo
|
|
|
16
29
|
help(namespace: string, name?: string): string;
|
|
17
30
|
run(positionals: string[], payload?: unknown): Promise<unknown>;
|
|
18
31
|
}>;
|
|
32
|
+
export declare function loadRegisteredPlugin(name: string, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<{
|
|
33
|
+
name: string | undefined;
|
|
34
|
+
description: string | undefined;
|
|
35
|
+
version: string | undefined;
|
|
36
|
+
help(namespace: string, name?: string): string;
|
|
37
|
+
run(positionals: string[], payload?: unknown): Promise<unknown>;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function invokeRegisteredPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
|
package/dist/plugin.js
CHANGED
|
@@ -2,8 +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';
|
|
8
|
+
import { findPluginRegistration, PluginError } from './pluginRegistry.js';
|
|
6
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';
|
|
7
11
|
function readPluginVersion(entry) {
|
|
8
12
|
try {
|
|
9
13
|
const content = readFileSync(resolve(dirname(entry), '..', 'SKILL.md'), 'utf8');
|
|
@@ -18,6 +22,9 @@ function readPluginVersion(entry) {
|
|
|
18
22
|
function isObject(value) {
|
|
19
23
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
24
|
}
|
|
25
|
+
function isPluginInvocation(value) {
|
|
26
|
+
return isObject(value) && typeof value.command === 'string' && isObject(value.input);
|
|
27
|
+
}
|
|
21
28
|
function missingPluginEntryMessage(entry, pluginName) {
|
|
22
29
|
const name = pluginName ?? '<name>';
|
|
23
30
|
return [
|
|
@@ -31,9 +38,12 @@ function missingPluginEntryMessage(entry, pluginName) {
|
|
|
31
38
|
}
|
|
32
39
|
/** 注册和执行共享一个 app;仅 handler 执行期间开放浏览器调用。 */
|
|
33
40
|
export async function loadPlugin(entry, context, pluginName) {
|
|
41
|
+
let entryVersion;
|
|
34
42
|
try {
|
|
35
|
-
|
|
43
|
+
const stat = statSync(entry);
|
|
44
|
+
if (!stat.isFile())
|
|
36
45
|
throw new Error('not a file');
|
|
46
|
+
entryVersion = `${stat.mtimeMs}-${stat.size}`;
|
|
37
47
|
}
|
|
38
48
|
catch {
|
|
39
49
|
throw new PluginError('PLUGIN_ENTRY_MISSING', missingPluginEntryMessage(entry, pluginName));
|
|
@@ -78,7 +88,9 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
78
88
|
},
|
|
79
89
|
});
|
|
80
90
|
try {
|
|
81
|
-
const
|
|
91
|
+
const moduleUrl = pathToFileURL(entry);
|
|
92
|
+
moduleUrl.searchParams.set('mearl-version', entryVersion);
|
|
93
|
+
const module = await import(moduleUrl.href);
|
|
82
94
|
if (module.name !== undefined && typeof module.name !== 'string')
|
|
83
95
|
throw new Error('入口的 name 导出必须为字符串');
|
|
84
96
|
name = module.name;
|
|
@@ -171,3 +183,60 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
171
183
|
},
|
|
172
184
|
};
|
|
173
185
|
}
|
|
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
|
+
}
|
|
195
|
+
if (!registration) {
|
|
196
|
+
throw new PluginError('PLUGIN_NOT_REGISTERED', `插件未注册: ${name}`);
|
|
197
|
+
}
|
|
198
|
+
const { onProgress, ...invokeOptions } = options;
|
|
199
|
+
const childState = { stack: [...state.stack, name] };
|
|
200
|
+
return loadPlugin(registration.entry, {
|
|
201
|
+
options: invokeOptions,
|
|
202
|
+
progress: onProgress ?? (() => undefined),
|
|
203
|
+
async invoke(action, payload, overrides) {
|
|
204
|
+
const nestedOptions = {
|
|
205
|
+
...invokeOptions,
|
|
206
|
+
...overrides,
|
|
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);
|
|
226
|
+
if (result?.action?.success === false) {
|
|
227
|
+
throw new Error(result.action.error || `${action} failed`);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
},
|
|
231
|
+
}, name);
|
|
232
|
+
}
|
|
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);
|
|
238
|
+
return plugin.run([], invocation);
|
|
239
|
+
}
|
|
240
|
+
export function invokeRegisteredPlugin(name, invocation, context, options = {}) {
|
|
241
|
+
return invokeRegisteredPluginWithState(name, invocation, context, options);
|
|
242
|
+
}
|
package/dist/pluginRegistry.d.ts
CHANGED
|
@@ -5,11 +5,15 @@ export declare class PluginError extends Error {
|
|
|
5
5
|
export interface PluginRegistration {
|
|
6
6
|
entry: string;
|
|
7
7
|
description?: string;
|
|
8
|
+
managedBy?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PluginRegistrationOptions {
|
|
11
|
+
managedBy?: string;
|
|
8
12
|
}
|
|
9
13
|
export declare function pluginDirectory(): string;
|
|
10
14
|
export declare function assertPluginName(name: string): void;
|
|
11
15
|
export declare function resolvePluginEntry(target: string): string;
|
|
12
|
-
export declare function registerPlugin(name: string, target: string, description?: string): string;
|
|
16
|
+
export declare function registerPlugin(name: string, target: string, description?: string, options?: PluginRegistrationOptions): string;
|
|
13
17
|
export declare function unregisterPlugin(name: string): boolean;
|
|
14
18
|
export declare function findPluginRegistration(name: string): PluginRegistration | undefined;
|
|
15
19
|
export declare function findPluginEntry(name: string): string | undefined;
|
package/dist/pluginRegistry.js
CHANGED
|
@@ -40,7 +40,7 @@ export function resolvePluginEntry(target) {
|
|
|
40
40
|
throw new PluginError('PLUGIN_ENTRY_MISSING', `入口不存在: ${absolute}(目录默认使用 scripts/main.mjs)`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
export function registerPlugin(name, target, description) {
|
|
43
|
+
export function registerPlugin(name, target, description, options = {}) {
|
|
44
44
|
assertPluginName(name);
|
|
45
45
|
const entry = resolvePluginEntry(target);
|
|
46
46
|
const directory = pluginDirectory();
|
|
@@ -49,7 +49,12 @@ export function registerPlugin(name, target, description) {
|
|
|
49
49
|
// 每个插件独立保存,多个名称并发注册不会覆盖彼此;同名注册原子替换绑定。
|
|
50
50
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
51
51
|
const summary = description?.trim();
|
|
52
|
-
const
|
|
52
|
+
const managedBy = options.managedBy?.trim();
|
|
53
|
+
const record = {
|
|
54
|
+
entry,
|
|
55
|
+
...(summary ? { description: summary } : {}),
|
|
56
|
+
...(managedBy ? { managedBy } : {}),
|
|
57
|
+
};
|
|
53
58
|
try {
|
|
54
59
|
writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
|
|
55
60
|
renameSync(temporary, file);
|
|
@@ -92,7 +97,14 @@ export function findPluginRegistration(name) {
|
|
|
92
97
|
const description = typeof record.description === 'string' && record.description.trim()
|
|
93
98
|
? record.description.trim()
|
|
94
99
|
: undefined;
|
|
95
|
-
|
|
100
|
+
const managedBy = typeof record.managedBy === 'string' && record.managedBy.trim()
|
|
101
|
+
? record.managedBy.trim()
|
|
102
|
+
: undefined;
|
|
103
|
+
return {
|
|
104
|
+
entry: record.entry,
|
|
105
|
+
...(description ? { description } : {}),
|
|
106
|
+
...(managedBy ? { managedBy } : {}),
|
|
107
|
+
};
|
|
96
108
|
}
|
|
97
109
|
catch {
|
|
98
110
|
throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file}`);
|
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 {};
|