@mearl/client 2.17.1 → 2.19.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 +86 -5
- package/dist/autoUpdate.d.ts +33 -0
- package/dist/autoUpdate.js +359 -0
- package/dist/autoUpdateWorker.d.ts +1 -0
- package/dist/autoUpdateWorker.js +5 -0
- package/dist/bin.js +3 -0
- package/dist/check.d.ts +11 -0
- package/dist/check.js +106 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -1
- package/dist/generated-browser-action-protocol.d.ts +4 -2
- package/dist/generated-browser-action-protocol.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +11 -3
- package/dist/plugin.js +44 -7
- package/dist/pluginPageBundle.d.ts +8 -0
- package/dist/pluginPageBundle.js +680 -0
- package/dist/pluginRegistry.d.ts +39 -0
- package/dist/pluginRegistry.js +261 -1
- package/dist/pythonPlugin.d.ts +1 -0
- package/dist/pythonPlugin.js +6 -0
- package/dist/socket.js +8 -13
- package/dist/unifiedConfig.d.ts +2 -0
- package/dist/unifiedConfig.js +23 -0
- package/package.json +3 -3
package/dist/pluginRegistry.d.ts
CHANGED
|
@@ -2,20 +2,59 @@ export declare class PluginError extends Error {
|
|
|
2
2
|
readonly code: string;
|
|
3
3
|
constructor(code: string, message: string);
|
|
4
4
|
}
|
|
5
|
+
export interface PluginToolboxSandboxPage {
|
|
6
|
+
type: 'sandbox';
|
|
7
|
+
runtime?: 'preact';
|
|
8
|
+
entry: string;
|
|
9
|
+
commands: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface PluginToolboxLocale {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PluginToolboxAgentConfig {
|
|
16
|
+
skill: string;
|
|
17
|
+
layout?: 'drawer' | 'workspace';
|
|
18
|
+
}
|
|
19
|
+
export interface PluginToolboxContribution {
|
|
20
|
+
version: 1;
|
|
21
|
+
title: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
locales?: Record<string, PluginToolboxLocale>;
|
|
25
|
+
agent?: PluginToolboxAgentConfig;
|
|
26
|
+
page: PluginToolboxSandboxPage;
|
|
27
|
+
order?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface PluginPageBundleRegistration {
|
|
30
|
+
hash: string;
|
|
31
|
+
size: number;
|
|
32
|
+
chunks: number;
|
|
33
|
+
}
|
|
5
34
|
export interface PluginRegistration {
|
|
6
35
|
entry: string;
|
|
7
36
|
description?: string;
|
|
8
37
|
managedBy?: string;
|
|
38
|
+
toolbox?: PluginToolboxContribution;
|
|
39
|
+
pageBundle?: PluginPageBundleRegistration;
|
|
9
40
|
}
|
|
10
41
|
export interface PluginRegistrationOptions {
|
|
11
42
|
managedBy?: string;
|
|
43
|
+
toolbox?: PluginToolboxContribution;
|
|
12
44
|
}
|
|
13
45
|
export declare function pluginDirectory(): string;
|
|
14
46
|
export declare function assertPluginName(name: string): void;
|
|
47
|
+
export declare function normalizePluginToolboxContribution(value: unknown): PluginToolboxContribution | undefined;
|
|
15
48
|
export declare function findPluginDirectoryEntry(directory: string): string | undefined;
|
|
16
49
|
export declare function resolvePluginEntry(target: string): string;
|
|
17
50
|
export declare function registerPlugin(name: string, target: string, description?: string, options?: PluginRegistrationOptions): string;
|
|
18
51
|
export declare function unregisterPlugin(name: string): boolean;
|
|
19
52
|
export declare function findPluginRegistration(name: string): PluginRegistration | undefined;
|
|
53
|
+
export declare function readPluginPageBundleChunk(name: string, hash: string, index: number): {
|
|
54
|
+
data: string;
|
|
55
|
+
hash: string;
|
|
56
|
+
index: number;
|
|
57
|
+
total: number;
|
|
58
|
+
};
|
|
20
59
|
export declare function findPluginEntry(name: string): string | undefined;
|
|
21
60
|
export declare function listPluginNames(): string[];
|
package/dist/pluginRegistry.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { COMMAND_MAP } from './commands.js';
|
|
6
|
+
import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, } from './pluginPageBundle.js';
|
|
6
7
|
const RESERVED_NAMES = new Set([
|
|
7
8
|
'check',
|
|
8
9
|
'help',
|
|
@@ -21,11 +22,196 @@ export class PluginError extends Error {
|
|
|
21
22
|
export function pluginDirectory() {
|
|
22
23
|
return resolve(process.env.MEARL_PLUGIN_DIR || join(homedir(), '.mearl', 'plugins'));
|
|
23
24
|
}
|
|
25
|
+
function pluginPageDirectory() {
|
|
26
|
+
return join(pluginDirectory(), '.pages');
|
|
27
|
+
}
|
|
28
|
+
function pluginPageBundlePath(name, hash) {
|
|
29
|
+
return join(pluginPageDirectory(), `${name}-${hash}.html`);
|
|
30
|
+
}
|
|
31
|
+
function removePluginPageBundles(name, exceptHash) {
|
|
32
|
+
let files;
|
|
33
|
+
try {
|
|
34
|
+
files = readdirSync(pluginPageDirectory());
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
const keep = exceptHash ? `${name}-${exceptHash}.html` : undefined;
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
if (file === keep || !file.startsWith(`${name}-`) || !file.endsWith('.html'))
|
|
44
|
+
continue;
|
|
45
|
+
rmSync(join(pluginPageDirectory(), file), { force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function normalizePageBundle(value) {
|
|
49
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
50
|
+
throw new Error('invalid page bundle');
|
|
51
|
+
}
|
|
52
|
+
const candidate = value;
|
|
53
|
+
if (typeof candidate.hash !== 'string' ||
|
|
54
|
+
!/^[a-f0-9]{64}$/.test(candidate.hash) ||
|
|
55
|
+
!Number.isInteger(candidate.size) ||
|
|
56
|
+
(candidate.size ?? 0) <= 0 ||
|
|
57
|
+
!Number.isInteger(candidate.chunks) ||
|
|
58
|
+
candidate.chunks !== Math.ceil((candidate.size ?? 0) / PLUGIN_PAGE_CHUNK_BYTES)) {
|
|
59
|
+
throw new Error('invalid page bundle');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
hash: candidate.hash,
|
|
63
|
+
size: candidate.size,
|
|
64
|
+
chunks: candidate.chunks,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function persistPluginPageBundle(name, bundle) {
|
|
68
|
+
const directory = pluginPageDirectory();
|
|
69
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
70
|
+
const target = pluginPageBundlePath(name, bundle.hash);
|
|
71
|
+
try {
|
|
72
|
+
if (statSync(target).isFile())
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// 页面内容哈希变化时创建新的不可变产物。
|
|
77
|
+
}
|
|
78
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(temporary, bundle.content, { mode: 0o600, flag: 'wx' });
|
|
81
|
+
renameSync(temporary, target);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rmSync(temporary, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
24
87
|
export function assertPluginName(name) {
|
|
25
88
|
if (!/^[a-z][a-z0-9-]*$/.test(name) || RESERVED_NAMES.has(name) || COMMAND_MAP.has(name)) {
|
|
26
89
|
throw new PluginError('PLUGIN_INVALID_NAME', `不可用的插件名称: ${name}`);
|
|
27
90
|
}
|
|
28
91
|
}
|
|
92
|
+
export function normalizePluginToolboxContribution(value) {
|
|
93
|
+
if (value === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
96
|
+
throw new Error('toolbox 必须是对象');
|
|
97
|
+
}
|
|
98
|
+
const candidate = value;
|
|
99
|
+
const title = typeof candidate.title === 'string' ? candidate.title.trim() : '';
|
|
100
|
+
const description = typeof candidate.description === 'string' ? candidate.description.trim() : undefined;
|
|
101
|
+
const icon = typeof candidate.icon === 'string' ? candidate.icon.trim() : undefined;
|
|
102
|
+
if (candidate.version !== 1)
|
|
103
|
+
throw new Error('toolbox.version 必须为 1');
|
|
104
|
+
if (!title || title.length > 80)
|
|
105
|
+
throw new Error('toolbox.title 必须是 1-80 个字符');
|
|
106
|
+
if (description && description.length > 240) {
|
|
107
|
+
throw new Error('toolbox.description 不能超过 240 个字符');
|
|
108
|
+
}
|
|
109
|
+
if (icon && (!/^[a-z][a-z0-9-]*$/.test(icon) || icon.length > 40)) {
|
|
110
|
+
throw new Error('toolbox.icon 必须是短横线命名的图标名称');
|
|
111
|
+
}
|
|
112
|
+
let locales;
|
|
113
|
+
if (candidate.locales !== undefined) {
|
|
114
|
+
if (!candidate.locales ||
|
|
115
|
+
typeof candidate.locales !== 'object' ||
|
|
116
|
+
Array.isArray(candidate.locales)) {
|
|
117
|
+
throw new Error('toolbox.locales 必须是对象');
|
|
118
|
+
}
|
|
119
|
+
const entries = Object.entries(candidate.locales);
|
|
120
|
+
if (entries.length === 0 || entries.length > 20) {
|
|
121
|
+
throw new Error('toolbox.locales 必须包含 1-20 个语言');
|
|
122
|
+
}
|
|
123
|
+
locales = Object.fromEntries(entries.map(([locale, localized]) => {
|
|
124
|
+
if (!/^[a-z]{2,3}(?:[_-][A-Za-z0-9]{2,8})*$/.test(locale)) {
|
|
125
|
+
throw new Error(`toolbox.locales 包含无效语言: ${locale}`);
|
|
126
|
+
}
|
|
127
|
+
if (!localized || typeof localized !== 'object' || Array.isArray(localized)) {
|
|
128
|
+
throw new Error(`toolbox.locales.${locale} 必须是对象`);
|
|
129
|
+
}
|
|
130
|
+
const localizedTitle = typeof localized.title === 'string' ? localized.title.trim() : undefined;
|
|
131
|
+
const localizedDescription = typeof localized.description === 'string' ? localized.description.trim() : undefined;
|
|
132
|
+
if (localizedTitle !== undefined && (!localizedTitle || localizedTitle.length > 80)) {
|
|
133
|
+
throw new Error(`toolbox.locales.${locale}.title 必须是 1-80 个字符`);
|
|
134
|
+
}
|
|
135
|
+
if (localizedDescription !== undefined && localizedDescription.length > 240) {
|
|
136
|
+
throw new Error(`toolbox.locales.${locale}.description 不能超过 240 个字符`);
|
|
137
|
+
}
|
|
138
|
+
if (!localizedTitle && !localizedDescription) {
|
|
139
|
+
throw new Error(`toolbox.locales.${locale} 至少声明 title 或 description`);
|
|
140
|
+
}
|
|
141
|
+
return [
|
|
142
|
+
locale,
|
|
143
|
+
{
|
|
144
|
+
...(localizedTitle ? { title: localizedTitle } : {}),
|
|
145
|
+
...(localizedDescription ? { description: localizedDescription } : {}),
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
if (!candidate.page || typeof candidate.page !== 'object' || Array.isArray(candidate.page)) {
|
|
151
|
+
throw new Error('toolbox.page 必须是对象');
|
|
152
|
+
}
|
|
153
|
+
const pageCandidate = candidate.page;
|
|
154
|
+
if (pageCandidate.type !== 'sandbox') {
|
|
155
|
+
throw new Error('toolbox.page.type 必须为 sandbox');
|
|
156
|
+
}
|
|
157
|
+
if (pageCandidate.runtime !== undefined && pageCandidate.runtime !== 'preact') {
|
|
158
|
+
throw new Error('toolbox.page.runtime 仅支持 preact');
|
|
159
|
+
}
|
|
160
|
+
const pageEntry = typeof pageCandidate.entry === 'string' ? pageCandidate.entry.trim() : '';
|
|
161
|
+
if (!pageEntry ||
|
|
162
|
+
pageEntry.length > 240 ||
|
|
163
|
+
isAbsolute(pageEntry) ||
|
|
164
|
+
pageEntry.includes('\\') ||
|
|
165
|
+
pageEntry.split('/').some(segment => !segment || segment === '.' || segment === '..')) {
|
|
166
|
+
throw new Error('toolbox.page.entry 必须是插件目录内的相对 HTML 路径');
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(pageCandidate.commands)) {
|
|
169
|
+
throw new Error('toolbox.page.commands 必须是命令名称数组');
|
|
170
|
+
}
|
|
171
|
+
const commands = pageCandidate.commands.map(command => typeof command === 'string' ? command.trim() : '');
|
|
172
|
+
if (commands.length === 0 ||
|
|
173
|
+
commands.length > 64 ||
|
|
174
|
+
new Set(commands).size !== commands.length ||
|
|
175
|
+
commands.some(command => !/^[a-z][a-z0-9-]*$/.test(command))) {
|
|
176
|
+
throw new Error('toolbox.page.commands 必须包含 1-64 个不重复的插件命令名称');
|
|
177
|
+
}
|
|
178
|
+
const page = {
|
|
179
|
+
type: 'sandbox',
|
|
180
|
+
...(pageCandidate.runtime ? { runtime: pageCandidate.runtime } : {}),
|
|
181
|
+
entry: pageEntry,
|
|
182
|
+
commands,
|
|
183
|
+
};
|
|
184
|
+
let agent;
|
|
185
|
+
if (candidate.agent !== undefined) {
|
|
186
|
+
if (!candidate.agent || typeof candidate.agent !== 'object' || Array.isArray(candidate.agent)) {
|
|
187
|
+
throw new Error('toolbox.agent 必须是对象');
|
|
188
|
+
}
|
|
189
|
+
const agentCandidate = candidate.agent;
|
|
190
|
+
const skill = typeof agentCandidate.skill === 'string' ? agentCandidate.skill.trim() : '';
|
|
191
|
+
if (!skill || skill.length > 128 || !/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/.test(skill)) {
|
|
192
|
+
throw new Error('toolbox.agent.skill 必须是有效的 Skill 名称');
|
|
193
|
+
}
|
|
194
|
+
const layout = agentCandidate.layout;
|
|
195
|
+
if (layout !== undefined && layout !== 'drawer' && layout !== 'workspace') {
|
|
196
|
+
throw new Error('toolbox.agent.layout 必须是 drawer 或 workspace');
|
|
197
|
+
}
|
|
198
|
+
agent = { skill, ...(layout ? { layout } : {}) };
|
|
199
|
+
}
|
|
200
|
+
if (candidate.order !== undefined &&
|
|
201
|
+
(!Number.isInteger(candidate.order) || candidate.order < 0 || candidate.order > 10_000)) {
|
|
202
|
+
throw new Error('toolbox.order 必须是 0-10000 的整数');
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
version: 1,
|
|
206
|
+
title,
|
|
207
|
+
...(description ? { description } : {}),
|
|
208
|
+
...(icon ? { icon } : {}),
|
|
209
|
+
...(locales ? { locales } : {}),
|
|
210
|
+
...(agent ? { agent } : {}),
|
|
211
|
+
page,
|
|
212
|
+
...(candidate.order !== undefined ? { order: candidate.order } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
29
215
|
export function findPluginDirectoryEntry(directory) {
|
|
30
216
|
for (const filename of ['main.mjs', 'main.py']) {
|
|
31
217
|
const entry = join(directory, 'scripts', filename);
|
|
@@ -63,10 +249,31 @@ export function registerPlugin(name, target, description, options = {}) {
|
|
|
63
249
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
64
250
|
const summary = description?.trim();
|
|
65
251
|
const managedBy = options.managedBy?.trim();
|
|
252
|
+
let toolbox;
|
|
253
|
+
try {
|
|
254
|
+
toolbox = normalizePluginToolboxContribution(options.toolbox);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
throw new PluginError('PLUGIN_INVALID_TOOLBOX', error instanceof Error ? error.message : String(error));
|
|
258
|
+
}
|
|
259
|
+
let builtPage;
|
|
260
|
+
if (toolbox) {
|
|
261
|
+
try {
|
|
262
|
+
builtPage = buildPluginPageBundle(entry, toolbox.page.entry);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
throw new PluginError('PLUGIN_INVALID_TOOLBOX_PAGE', error instanceof Error ? error.message : String(error));
|
|
266
|
+
}
|
|
267
|
+
persistPluginPageBundle(name, builtPage);
|
|
268
|
+
}
|
|
66
269
|
const record = {
|
|
67
270
|
entry,
|
|
68
271
|
...(summary ? { description: summary } : {}),
|
|
69
272
|
...(managedBy ? { managedBy } : {}),
|
|
273
|
+
...(toolbox ? { toolbox } : {}),
|
|
274
|
+
...(builtPage
|
|
275
|
+
? { pageBundle: { hash: builtPage.hash, size: builtPage.size, chunks: builtPage.chunks } }
|
|
276
|
+
: {}),
|
|
70
277
|
};
|
|
71
278
|
try {
|
|
72
279
|
writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
|
|
@@ -75,6 +282,7 @@ export function registerPlugin(name, target, description, options = {}) {
|
|
|
75
282
|
finally {
|
|
76
283
|
rmSync(temporary, { force: true });
|
|
77
284
|
}
|
|
285
|
+
removePluginPageBundles(name, builtPage?.hash);
|
|
78
286
|
return entry;
|
|
79
287
|
}
|
|
80
288
|
export function unregisterPlugin(name) {
|
|
@@ -82,6 +290,7 @@ export function unregisterPlugin(name) {
|
|
|
82
290
|
const file = join(pluginDirectory(), `${name}.json`);
|
|
83
291
|
try {
|
|
84
292
|
rmSync(file);
|
|
293
|
+
removePluginPageBundles(name);
|
|
85
294
|
return true;
|
|
86
295
|
}
|
|
87
296
|
catch (error) {
|
|
@@ -113,16 +322,67 @@ export function findPluginRegistration(name) {
|
|
|
113
322
|
const managedBy = typeof record.managedBy === 'string' && record.managedBy.trim()
|
|
114
323
|
? record.managedBy.trim()
|
|
115
324
|
: undefined;
|
|
325
|
+
const toolbox = normalizePluginToolboxContribution(record.toolbox);
|
|
326
|
+
const pageBundle = toolbox ? normalizePageBundle(record.pageBundle) : undefined;
|
|
116
327
|
return {
|
|
117
328
|
entry: record.entry,
|
|
118
329
|
...(description ? { description } : {}),
|
|
119
330
|
...(managedBy ? { managedBy } : {}),
|
|
331
|
+
...(toolbox ? { toolbox } : {}),
|
|
332
|
+
...(pageBundle ? { pageBundle } : {}),
|
|
120
333
|
};
|
|
121
334
|
}
|
|
122
335
|
catch {
|
|
123
336
|
throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file}`);
|
|
124
337
|
}
|
|
125
338
|
}
|
|
339
|
+
export function readPluginPageBundleChunk(name, hash, index) {
|
|
340
|
+
assertPluginName(name);
|
|
341
|
+
const registration = findPluginRegistration(name);
|
|
342
|
+
const bundle = registration?.pageBundle;
|
|
343
|
+
if (!bundle || bundle.hash !== hash) {
|
|
344
|
+
throw new PluginError('PLUGIN_PAGE_CHANGED', `插件页面已更新: ${name}`);
|
|
345
|
+
}
|
|
346
|
+
if (!Number.isInteger(index) || index < 0 || index >= bundle.chunks) {
|
|
347
|
+
throw new PluginError('PLUGIN_PAGE_CHUNK_INVALID', `插件页面分块无效: ${index}`);
|
|
348
|
+
}
|
|
349
|
+
const path = pluginPageBundlePath(name, hash);
|
|
350
|
+
let size;
|
|
351
|
+
try {
|
|
352
|
+
size = statSync(path).size;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物不存在: ${name}`);
|
|
356
|
+
}
|
|
357
|
+
if (size !== bundle.size) {
|
|
358
|
+
throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物损坏: ${name}`);
|
|
359
|
+
}
|
|
360
|
+
const start = index * PLUGIN_PAGE_CHUNK_BYTES;
|
|
361
|
+
const content = Buffer.allocUnsafe(Math.min(PLUGIN_PAGE_CHUNK_BYTES, bundle.size - start));
|
|
362
|
+
let descriptor;
|
|
363
|
+
try {
|
|
364
|
+
descriptor = openSync(path, 'r');
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物无法读取: ${name}`);
|
|
368
|
+
}
|
|
369
|
+
let bytesRead;
|
|
370
|
+
try {
|
|
371
|
+
bytesRead = readSync(descriptor, content, 0, content.byteLength, start);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
closeSync(descriptor);
|
|
375
|
+
}
|
|
376
|
+
if (bytesRead !== content.byteLength) {
|
|
377
|
+
throw new PluginError('PLUGIN_PAGE_INVALID', `插件页面产物损坏: ${name}`);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
data: content.toString('base64'),
|
|
381
|
+
hash,
|
|
382
|
+
index,
|
|
383
|
+
total: bundle.chunks,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
126
386
|
export function findPluginEntry(name) {
|
|
127
387
|
return findPluginRegistration(name)?.entry;
|
|
128
388
|
}
|
package/dist/pythonPlugin.d.ts
CHANGED
package/dist/pythonPlugin.js
CHANGED
|
@@ -143,10 +143,13 @@ try:
|
|
|
143
143
|
|
|
144
144
|
plugin_name = getattr(module, "name", None)
|
|
145
145
|
plugin_description = getattr(module, "description", None)
|
|
146
|
+
plugin_toolbox = getattr(module, "toolbox", None)
|
|
146
147
|
if plugin_name is not None and not isinstance(plugin_name, str):
|
|
147
148
|
raise TypeError("入口的 name 必须为字符串")
|
|
148
149
|
if plugin_description is not None and not isinstance(plugin_description, str):
|
|
149
150
|
raise TypeError("入口的 description 必须为字符串")
|
|
151
|
+
if plugin_toolbox is not None and not isinstance(plugin_toolbox, dict):
|
|
152
|
+
raise TypeError("入口的 toolbox 必须为对象")
|
|
150
153
|
register = getattr(module, "register", None)
|
|
151
154
|
if not callable(register):
|
|
152
155
|
raise TypeError("入口必须导出 register(app) 函数")
|
|
@@ -168,6 +171,8 @@ try:
|
|
|
168
171
|
descriptor["name"] = plugin_name
|
|
169
172
|
if plugin_description is not None:
|
|
170
173
|
descriptor["description"] = plugin_description
|
|
174
|
+
if plugin_toolbox is not None:
|
|
175
|
+
descriptor["toolbox"] = plugin_toolbox
|
|
171
176
|
_send(descriptor)
|
|
172
177
|
|
|
173
178
|
if mode == "run":
|
|
@@ -233,6 +238,7 @@ function parseDescriptor(message) {
|
|
|
233
238
|
commands,
|
|
234
239
|
...(typeof message.name === 'string' ? { name: message.name } : {}),
|
|
235
240
|
...(typeof message.description === 'string' ? { description: message.description } : {}),
|
|
241
|
+
...(message.toolbox !== undefined ? { toolbox: message.toolbox } : {}),
|
|
236
242
|
};
|
|
237
243
|
}
|
|
238
244
|
function bridgedError(message) {
|
package/dist/socket.js
CHANGED
|
@@ -13,7 +13,8 @@ import os from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
import { StringDecoder } from 'node:string_decoder';
|
|
16
|
-
import {
|
|
16
|
+
import { listInstalledProviders } from '@mearl/provider/host';
|
|
17
|
+
import { readLiveProvider } from '@mearl/provider/runtime';
|
|
17
18
|
import { resolveFileRefs } from './fileRefs.js';
|
|
18
19
|
import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
|
|
19
20
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
@@ -95,12 +96,8 @@ function browserListFromProviderRecords(browsers) {
|
|
|
95
96
|
browsers: providerBrowsers,
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
? result.warnings.filter((warning) => typeof warning === 'string')
|
|
101
|
-
: [];
|
|
102
|
-
const merged = [...new Set([...resultWarnings, ...warnings])];
|
|
103
|
-
return { ...result, ...(merged.length > 0 ? { warnings: merged } : {}) };
|
|
99
|
+
function areInstalledProvidersRunning() {
|
|
100
|
+
return listInstalledProviders().every(provider => readLiveProvider(provider.providerId));
|
|
104
101
|
}
|
|
105
102
|
function isConnectedBrowser(browser) {
|
|
106
103
|
return browser.status === undefined || browser.status === 'connected';
|
|
@@ -243,11 +240,7 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
243
240
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
244
241
|
const resolvedData = resolveFileRefs(data);
|
|
245
242
|
if (action === 'browser_list') {
|
|
246
|
-
|
|
247
|
-
const providerStartup = hasNativeAggregator
|
|
248
|
-
? Promise.resolve([])
|
|
249
|
-
: ensureInstalledProvidersRunning();
|
|
250
|
-
return providerStartup.then(providerWarnings => dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(mergeWarnings(result, providerWarnings))));
|
|
243
|
+
return dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(result));
|
|
251
244
|
}
|
|
252
245
|
const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
253
246
|
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
|
|
@@ -325,7 +318,9 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
325
318
|
throw error;
|
|
326
319
|
}
|
|
327
320
|
}
|
|
328
|
-
if (action === 'browser_list' &&
|
|
321
|
+
if (action === 'browser_list' &&
|
|
322
|
+
browsers.some(browser => browser.transport === 'provider') &&
|
|
323
|
+
areInstalledProvidersRunning()) {
|
|
329
324
|
return browserListFromProviderRecords(browsers);
|
|
330
325
|
}
|
|
331
326
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
|
package/dist/unifiedConfig.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface CloudServerConfig {
|
|
2
2
|
server: string;
|
|
3
3
|
}
|
|
4
|
+
/** Number of local calendar days between CLI update checks; null disables them. */
|
|
5
|
+
export declare function loadCliAutoUpdateIntervalDays(configPath?: string): number | null;
|
|
4
6
|
export declare function getCloudServerConfigPath(): string;
|
|
5
7
|
export declare function loadCloudServerConfig(): CloudServerConfig | null;
|
|
6
8
|
export declare function loadLocalConnectorId(): string | null;
|
package/dist/unifiedConfig.js
CHANGED
|
@@ -2,6 +2,29 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
const MEARL_CONFIG_DIR = join(homedir(), '.mearl');
|
|
5
|
+
/** Number of local calendar days between CLI update checks; null disables them. */
|
|
6
|
+
export function loadCliAutoUpdateIntervalDays(configPath = join(MEARL_CONFIG_DIR, 'config.json')) {
|
|
7
|
+
try {
|
|
8
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
9
|
+
if (config.cliAutoUpdate === false)
|
|
10
|
+
return null;
|
|
11
|
+
if (config.cliAutoUpdate === undefined)
|
|
12
|
+
return 1;
|
|
13
|
+
if (typeof config.cliAutoUpdate === 'object' &&
|
|
14
|
+
config.cliAutoUpdate !== null &&
|
|
15
|
+
!Array.isArray(config.cliAutoUpdate)) {
|
|
16
|
+
const { intervalDays } = config.cliAutoUpdate;
|
|
17
|
+
if (typeof intervalDays === 'number' &&
|
|
18
|
+
Number.isSafeInteger(intervalDays) &&
|
|
19
|
+
intervalDays > 0)
|
|
20
|
+
return intervalDays;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// A missing or invalid config uses the daily default.
|
|
25
|
+
}
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
5
28
|
export function getCloudServerConfigPath() {
|
|
6
29
|
return join(MEARL_CONFIG_DIR, 'cloud-server.json');
|
|
7
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.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/
|
|
63
|
-
"@mearl/
|
|
62
|
+
"@mearl/provider": "2.19.0",
|
|
63
|
+
"@mearl/cloud-types": "2.19.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.9.1",
|