@mearl/client 2.17.0 → 2.18.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 +75 -11
- package/dist/cli.js +9 -19
- package/dist/commands.d.ts +70 -43
- package/dist/commands.js +110 -67
- package/dist/generated-browser-action-protocol.d.ts +50 -32
- package/dist/generated-browser-action-protocol.js +40 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/plugin.d.ts +11 -3
- package/dist/plugin.js +44 -7
- package/dist/pluginPageBundle.d.ts +8 -0
- package/dist/pluginPageBundle.js +420 -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 +15 -14
- package/dist/unified.js +2 -0
- package/package.json +3 -3
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';
|
|
@@ -117,6 +114,9 @@ function supportsAction(browser, action, data) {
|
|
|
117
114
|
const required = data.deleteProfile === true ? 'deleteData' : 'close';
|
|
118
115
|
return browser.operations?.includes(required) === true;
|
|
119
116
|
}
|
|
117
|
+
if (action === 'browser_release') {
|
|
118
|
+
return browser.transport !== 'provider' || browser.operations?.includes('detach') === true;
|
|
119
|
+
}
|
|
120
120
|
if (!isConnectedBrowser(browser))
|
|
121
121
|
return false;
|
|
122
122
|
return (browser.transport !== 'provider' ||
|
|
@@ -240,11 +240,7 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
240
240
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
241
241
|
const resolvedData = resolveFileRefs(data);
|
|
242
242
|
if (action === 'browser_list') {
|
|
243
|
-
|
|
244
|
-
const providerStartup = hasNativeAggregator
|
|
245
|
-
? Promise.resolve([])
|
|
246
|
-
: ensureInstalledProvidersRunning();
|
|
247
|
-
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));
|
|
248
244
|
}
|
|
249
245
|
const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
250
246
|
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
|
|
@@ -282,7 +278,10 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
282
278
|
if (transport === 'cdp' && match.transport === 'provider') {
|
|
283
279
|
throw new Error(`Browser ${describeBrowser(match)} does not support the CDP transport`);
|
|
284
280
|
}
|
|
285
|
-
if (match.status === 'pairing' &&
|
|
281
|
+
if (match.status === 'pairing' &&
|
|
282
|
+
action !== 'get_versions' &&
|
|
283
|
+
action !== 'browser_close' &&
|
|
284
|
+
action !== 'browser_release') {
|
|
286
285
|
throw new Error(`Browser ${describeBrowser(match)} is waiting for device pairing`);
|
|
287
286
|
}
|
|
288
287
|
if (!supportsAction(match, action, data)) {
|
|
@@ -319,7 +318,9 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
319
318
|
throw error;
|
|
320
319
|
}
|
|
321
320
|
}
|
|
322
|
-
if (action === 'browser_list' &&
|
|
321
|
+
if (action === 'browser_list' &&
|
|
322
|
+
browsers.some(browser => browser.transport === 'provider') &&
|
|
323
|
+
areInstalledProvidersRunning()) {
|
|
323
324
|
return browserListFromProviderRecords(browsers);
|
|
324
325
|
}
|
|
325
326
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
|
package/dist/unified.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { DEFAULT_CONNECT_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_REQUEST_TIMEOUT, MAX_BUFFER_SIZE, isHeartbeatMessage, resolveCloudConnectorSelector, } from '@mearl/cloud-types';
|
|
3
3
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
4
|
+
import { validateRequiredActionTabId, } from './generated-browser-action-protocol.js';
|
|
4
5
|
import { invoke as invokeLocalBrowser } from './socket.js';
|
|
5
6
|
import { loadCloudServerConfig, loadLocalConnectorId } from './unifiedConfig.js';
|
|
6
7
|
const RESPONSE_GRACE_MS = 1_000;
|
|
@@ -126,6 +127,7 @@ export class UnifiedClient {
|
|
|
126
127
|
}, options);
|
|
127
128
|
}
|
|
128
129
|
async invoke(action, data = {}, options = {}) {
|
|
130
|
+
validateRequiredActionTabId(action, data);
|
|
129
131
|
const timeoutSec = options.timeoutSec ?? resolveActionTimeoutSec(action, data, this.options.requestTimeoutSec);
|
|
130
132
|
if (action === 'connector_list')
|
|
131
133
|
return this.listConnectors(timeoutSec);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.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/cloud-types": "2.18.0",
|
|
63
|
+
"@mearl/provider": "2.18.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.9.1",
|