@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
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
export const PLUGIN_PAGE_CHUNK_BYTES = 384 * 1024;
|
|
5
|
+
const MAX_PAGE_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
const MAX_PAGE_BUNDLE_BYTES = 24 * 1024 * 1024;
|
|
7
|
+
const MIME_TYPES = {
|
|
8
|
+
'.avif': 'image/avif',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.ico': 'image/x-icon',
|
|
11
|
+
'.jpeg': 'image/jpeg',
|
|
12
|
+
'.jpg': 'image/jpeg',
|
|
13
|
+
'.mp3': 'audio/mpeg',
|
|
14
|
+
'.mp4': 'video/mp4',
|
|
15
|
+
'.ogg': 'audio/ogg',
|
|
16
|
+
'.otf': 'font/otf',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.svg': 'image/svg+xml',
|
|
19
|
+
'.ttf': 'font/ttf',
|
|
20
|
+
'.wav': 'audio/wav',
|
|
21
|
+
'.webm': 'video/webm',
|
|
22
|
+
'.webp': 'image/webp',
|
|
23
|
+
'.woff': 'font/woff',
|
|
24
|
+
'.woff2': 'font/woff2',
|
|
25
|
+
};
|
|
26
|
+
const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
|
|
27
|
+
let context = null;
|
|
28
|
+
let port = null;
|
|
29
|
+
let requestSequence = 0;
|
|
30
|
+
const pending = new Map();
|
|
31
|
+
let resolveReady;
|
|
32
|
+
const ready = new Promise(resolve => { resolveReady = resolve; });
|
|
33
|
+
|
|
34
|
+
const requestHost = async (type, payload = {}) => {
|
|
35
|
+
await ready;
|
|
36
|
+
const requestId = String(++requestSequence);
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
pending.set(requestId, { resolve, reject });
|
|
39
|
+
port.postMessage({ type, requestId, ...payload });
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const invoke = async (command, input = {}) => {
|
|
44
|
+
if (typeof command !== 'string' || !/^[a-z][a-z0-9-]*$/.test(command)) {
|
|
45
|
+
throw new Error('Invalid plugin command');
|
|
46
|
+
}
|
|
47
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
48
|
+
throw new Error('Plugin command input must be an object');
|
|
49
|
+
}
|
|
50
|
+
return requestHost('invoke', { command, input });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const start = async (command, input = {}) => {
|
|
54
|
+
if (typeof command !== 'string' || !/^[a-z][a-z0-9-]*$/.test(command)) {
|
|
55
|
+
throw new Error('Invalid plugin command');
|
|
56
|
+
}
|
|
57
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
58
|
+
throw new Error('Plugin command input must be an object');
|
|
59
|
+
}
|
|
60
|
+
return requestHost('invoke-start', { command, input });
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const operation = async operationId => {
|
|
64
|
+
if (typeof operationId !== 'string' || !/^[0-9a-f-]{36}$/i.test(operationId)) {
|
|
65
|
+
throw new Error('Invalid plugin operation id');
|
|
66
|
+
}
|
|
67
|
+
return requestHost('invoke-operation', { operationId });
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const runningOperations = async () => requestHost('invoke-operations');
|
|
71
|
+
|
|
72
|
+
const selectDirectory = async () => {
|
|
73
|
+
const path = await requestHost('select-directory');
|
|
74
|
+
if (path !== null && typeof path !== 'string') {
|
|
75
|
+
throw new Error('Directory selection returned an invalid path');
|
|
76
|
+
}
|
|
77
|
+
return path;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const copyText = async text => {
|
|
81
|
+
if (typeof text !== 'string' || text.length > 1000000) {
|
|
82
|
+
throw new Error('Clipboard text must be a string no longer than 1000000 characters');
|
|
83
|
+
}
|
|
84
|
+
await requestHost('clipboard-write', { text });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const openExternalLink = async url => {
|
|
88
|
+
if (typeof url !== 'string' || !url || url.length > 8192) {
|
|
89
|
+
throw new Error('External link must be a non-empty URL no longer than 8192 characters');
|
|
90
|
+
}
|
|
91
|
+
await requestHost('open-external-link', { url });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleExternalLinkClick = event => {
|
|
95
|
+
if (event.defaultPrevented || event.button !== 0) return;
|
|
96
|
+
const link = event.target?.closest?.('a[href]');
|
|
97
|
+
if (!link || String(link.target).toLowerCase() !== '_blank' || link.hasAttribute?.('download')) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let url;
|
|
101
|
+
try {
|
|
102
|
+
url = new URL(link.href);
|
|
103
|
+
} catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
void openExternalLink(url.href).catch(error => {
|
|
109
|
+
console.error('Failed to open external link', error);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const toast = async (message, options = {}) => {
|
|
114
|
+
if (typeof message !== 'string' || !message.trim() || message.length > 4000) {
|
|
115
|
+
throw new Error('Toast message must be a non-empty string no longer than 4000 characters');
|
|
116
|
+
}
|
|
117
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
118
|
+
throw new Error('Toast options must be an object');
|
|
119
|
+
}
|
|
120
|
+
const type = options.type ?? 'info';
|
|
121
|
+
const duration = options.duration ?? 3000;
|
|
122
|
+
if (!['success', 'error', 'info'].includes(type)) throw new Error('Invalid toast type');
|
|
123
|
+
if (!Number.isInteger(duration) || duration < 1000 || duration > 30000) {
|
|
124
|
+
throw new Error('Toast duration must be between 1000 and 30000 milliseconds');
|
|
125
|
+
}
|
|
126
|
+
await ready;
|
|
127
|
+
port.postMessage({ type: 'toast', message: message.trim(), toastType: type, duration });
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const agentPanelLayout = options => {
|
|
131
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
132
|
+
throw new Error('Agent panel options must be an object');
|
|
133
|
+
}
|
|
134
|
+
const layout = options.layout;
|
|
135
|
+
if (layout !== undefined && layout !== 'drawer' && layout !== 'workspace' && layout !== 'inline') {
|
|
136
|
+
throw new Error('Agent panel layout must be drawer, workspace, or inline');
|
|
137
|
+
}
|
|
138
|
+
return layout;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const agentPanelKey = (options, required = false) => {
|
|
142
|
+
const key = typeof options.key === 'string' ? options.key.trim() : '';
|
|
143
|
+
if ((required || options.key !== undefined) && (!key || key.length > 128)) {
|
|
144
|
+
throw new Error('Agent panel key must be a non-empty string no longer than 128 characters');
|
|
145
|
+
}
|
|
146
|
+
return key || null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const agentPanelCwd = options => {
|
|
150
|
+
const cwd = options.cwd;
|
|
151
|
+
if (
|
|
152
|
+
cwd !== undefined &&
|
|
153
|
+
(typeof cwd !== 'string' ||
|
|
154
|
+
!cwd.trim() ||
|
|
155
|
+
cwd.length > 4096 ||
|
|
156
|
+
!/^(?:\/|[a-zA-Z]:[\\/])/.test(cwd.trim()))
|
|
157
|
+
) {
|
|
158
|
+
throw new Error('Agent panel cwd must be an absolute path no longer than 4096 characters');
|
|
159
|
+
}
|
|
160
|
+
return cwd === undefined ? null : cwd.trim();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const agentPanelContext = options => {
|
|
164
|
+
const context = options.context;
|
|
165
|
+
if (
|
|
166
|
+
context !== undefined &&
|
|
167
|
+
(typeof context !== 'string' || !context.trim() || context.length > 65536)
|
|
168
|
+
) {
|
|
169
|
+
throw new Error('Agent panel context must be a non-empty string no longer than 65536 characters');
|
|
170
|
+
}
|
|
171
|
+
return context === undefined ? null : context.trim();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const inlineAgentPanels = new Map();
|
|
175
|
+
|
|
176
|
+
const stopInlineAgentPanelTracking = key => {
|
|
177
|
+
if (key) {
|
|
178
|
+
inlineAgentPanels.get(key)?.cleanup();
|
|
179
|
+
inlineAgentPanels.delete(key);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
for (const tracked of inlineAgentPanels.values()) tracked.cleanup();
|
|
183
|
+
inlineAgentPanels.clear();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const inlineAgentPanelAnchor = (options, key) => {
|
|
187
|
+
const anchor = options.anchor;
|
|
188
|
+
if (!(anchor instanceof Element)) {
|
|
189
|
+
throw new Error('Inline Agent panel anchor must be an element');
|
|
190
|
+
}
|
|
191
|
+
const rect = anchor.getBoundingClientRect();
|
|
192
|
+
if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) {
|
|
193
|
+
throw new Error('Inline Agent panel anchor returned invalid bounds');
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
element: anchor,
|
|
197
|
+
key,
|
|
198
|
+
bounds: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const trackInlineAgentPanel = presentation => {
|
|
203
|
+
stopInlineAgentPanelTracking(presentation.key);
|
|
204
|
+
let frame = null;
|
|
205
|
+
const update = () => {
|
|
206
|
+
frame = null;
|
|
207
|
+
if (!presentation.element.isConnected || !port) return;
|
|
208
|
+
const rect = presentation.element.getBoundingClientRect();
|
|
209
|
+
if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) return;
|
|
210
|
+
const bounds = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
|
|
211
|
+
if (
|
|
212
|
+
bounds.top === presentation.bounds.top &&
|
|
213
|
+
bounds.left === presentation.bounds.left &&
|
|
214
|
+
bounds.width === presentation.bounds.width &&
|
|
215
|
+
bounds.height === presentation.bounds.height
|
|
216
|
+
) return;
|
|
217
|
+
presentation.bounds = bounds;
|
|
218
|
+
port.postMessage({
|
|
219
|
+
type: 'agent-panel-position',
|
|
220
|
+
key: presentation.key,
|
|
221
|
+
bounds,
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const schedule = () => {
|
|
225
|
+
if (frame !== null) return;
|
|
226
|
+
frame = requestAnimationFrame(update);
|
|
227
|
+
};
|
|
228
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(schedule) : null;
|
|
229
|
+
observer?.observe(presentation.element);
|
|
230
|
+
observer?.observe(document.documentElement);
|
|
231
|
+
const mutationObserver = typeof MutationObserver === 'function'
|
|
232
|
+
? new MutationObserver(schedule)
|
|
233
|
+
: null;
|
|
234
|
+
mutationObserver?.observe(document.documentElement, {
|
|
235
|
+
attributes: true,
|
|
236
|
+
childList: true,
|
|
237
|
+
characterData: true,
|
|
238
|
+
subtree: true,
|
|
239
|
+
});
|
|
240
|
+
window.addEventListener('resize', schedule);
|
|
241
|
+
document.addEventListener('scroll', schedule, true);
|
|
242
|
+
const cleanup = () => {
|
|
243
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
244
|
+
observer?.disconnect();
|
|
245
|
+
mutationObserver?.disconnect();
|
|
246
|
+
window.removeEventListener('resize', schedule);
|
|
247
|
+
document.removeEventListener('scroll', schedule, true);
|
|
248
|
+
};
|
|
249
|
+
inlineAgentPanels.set(presentation.key, { presentation, cleanup });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const scrollInlineAgentPage = (presentation, deltaX, deltaY) => {
|
|
253
|
+
const scrollAxis = (axis, delta) => {
|
|
254
|
+
if (!delta) return;
|
|
255
|
+
let element = presentation.element;
|
|
256
|
+
while (element) {
|
|
257
|
+
const style = window.getComputedStyle(element);
|
|
258
|
+
const overflow = axis === 'x' ? style.overflowX : style.overflowY;
|
|
259
|
+
const scrollSize = axis === 'x' ? element.scrollWidth : element.scrollHeight;
|
|
260
|
+
const clientSize = axis === 'x' ? element.clientWidth : element.clientHeight;
|
|
261
|
+
if (/^(auto|scroll|overlay)$/.test(overflow) && scrollSize > clientSize) {
|
|
262
|
+
const scrollPosition = axis === 'x' ? element.scrollLeft : element.scrollTop;
|
|
263
|
+
const maximum = scrollSize - clientSize;
|
|
264
|
+
if ((delta < 0 && scrollPosition > 0) || (delta > 0 && scrollPosition < maximum - 1)) {
|
|
265
|
+
if (axis === 'x') element.scrollLeft += delta;
|
|
266
|
+
else element.scrollTop += delta;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
element = element.parentElement;
|
|
271
|
+
}
|
|
272
|
+
window.scrollBy(axis === 'x' ? delta : 0, axis === 'y' ? delta : 0);
|
|
273
|
+
};
|
|
274
|
+
scrollAxis('x', deltaX);
|
|
275
|
+
scrollAxis('y', deltaY);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const agentPanelPresentation = (options, layout, key) => {
|
|
279
|
+
if (layout !== 'inline') return null;
|
|
280
|
+
return inlineAgentPanelAnchor(options, key);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const setAgentPanelVisible = async (visible, options = {}) => {
|
|
284
|
+
if (typeof visible !== 'boolean') throw new Error('Agent panel visibility must be a boolean');
|
|
285
|
+
const layout = agentPanelLayout(options);
|
|
286
|
+
const key = agentPanelKey(options, visible && layout === 'inline');
|
|
287
|
+
const cwd = visible ? agentPanelCwd(options) : null;
|
|
288
|
+
const context = visible ? agentPanelContext(options) : null;
|
|
289
|
+
const presentation = visible ? agentPanelPresentation(options, layout, key) : null;
|
|
290
|
+
const request = requestHost('agent-panel', {
|
|
291
|
+
visible,
|
|
292
|
+
...(layout ? { layout } : {}),
|
|
293
|
+
...(key ? { key } : {}),
|
|
294
|
+
...(cwd ? { cwd } : {}),
|
|
295
|
+
...(context ? { context } : {}),
|
|
296
|
+
...(presentation ? { bounds: presentation.bounds } : {}),
|
|
297
|
+
});
|
|
298
|
+
if (presentation) {
|
|
299
|
+
await ready;
|
|
300
|
+
trackInlineAgentPanel(presentation);
|
|
301
|
+
} else if (!visible && key) {
|
|
302
|
+
stopInlineAgentPanelTracking(key);
|
|
303
|
+
} else {
|
|
304
|
+
stopInlineAgentPanelTracking();
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
await request;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (presentation) stopInlineAgentPanelTracking(presentation.key);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const agentGuideText = (value, label, maximumLength) => {
|
|
315
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
316
|
+
if (!normalized || normalized.length > maximumLength) {
|
|
317
|
+
throw new Error(label + ' must be a non-empty string no longer than ' + maximumLength + ' characters');
|
|
318
|
+
}
|
|
319
|
+
return normalized;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const setAgentGuides = async guides => {
|
|
323
|
+
if (!Array.isArray(guides) || guides.length === 0 || guides.length > 6) {
|
|
324
|
+
throw new Error('Agent guides must contain between 1 and 6 items');
|
|
325
|
+
}
|
|
326
|
+
const normalized = guides.map((guide, index) => {
|
|
327
|
+
if (!guide || typeof guide !== 'object' || Array.isArray(guide)) {
|
|
328
|
+
throw new Error('Agent guide ' + index + ' must be an object');
|
|
329
|
+
}
|
|
330
|
+
const icon = guide.icon === undefined ? undefined : agentGuideText(guide.icon, 'Agent guide icon', 40);
|
|
331
|
+
if (icon && !/^[a-z][a-z0-9-]*$/.test(icon)) {
|
|
332
|
+
throw new Error('Agent guide icon must use kebab-case');
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
title: agentGuideText(guide.title, 'Agent guide title', 80),
|
|
336
|
+
description: agentGuideText(guide.description, 'Agent guide description', 160),
|
|
337
|
+
prompt: agentGuideText(guide.prompt, 'Agent guide prompt', 4000),
|
|
338
|
+
...(icon ? { icon } : {}),
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
await requestHost('agent-guides', { guides: normalized });
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const runAgentTask = async (prompt, options = {}) => {
|
|
345
|
+
if (typeof prompt !== 'string' || !prompt.trim() || prompt.length > 16000) {
|
|
346
|
+
throw new Error('Agent task prompt must be a non-empty string no longer than 16000 characters');
|
|
347
|
+
}
|
|
348
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
349
|
+
throw new Error('Agent task options must be an object');
|
|
350
|
+
}
|
|
351
|
+
const cwd = options.cwd;
|
|
352
|
+
const ephemeral = options.ephemeral;
|
|
353
|
+
const layout = agentPanelLayout(options);
|
|
354
|
+
const key = agentPanelKey(options, layout === 'inline');
|
|
355
|
+
const presentation = agentPanelPresentation(options, layout, key);
|
|
356
|
+
if (ephemeral !== undefined && ephemeral !== false) {
|
|
357
|
+
throw new Error('Temporary Agent conversations are not supported');
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
cwd !== undefined &&
|
|
361
|
+
(typeof cwd !== 'string' ||
|
|
362
|
+
!cwd.trim() ||
|
|
363
|
+
cwd.length > 4096 ||
|
|
364
|
+
!/^(?:\/|[a-zA-Z]:[\\/])/.test(cwd.trim()))
|
|
365
|
+
) {
|
|
366
|
+
throw new Error('Agent task cwd must be an absolute path no longer than 4096 characters');
|
|
367
|
+
}
|
|
368
|
+
const request = requestHost('agent-task', {
|
|
369
|
+
prompt: prompt.trim(),
|
|
370
|
+
...(cwd !== undefined ? { cwd: cwd.trim() } : {}),
|
|
371
|
+
...(layout ? { layout } : {}),
|
|
372
|
+
...(key ? { key } : {}),
|
|
373
|
+
...(presentation ? { bounds: presentation.bounds } : {}),
|
|
374
|
+
});
|
|
375
|
+
if (presentation) {
|
|
376
|
+
await ready;
|
|
377
|
+
trackInlineAgentPanel(presentation);
|
|
378
|
+
} else {
|
|
379
|
+
stopInlineAgentPanelTracking();
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await request;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (presentation) stopInlineAgentPanelTracking(presentation.key);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const setQueryParam = async (name, value) => {
|
|
390
|
+
if (typeof name !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
|
|
391
|
+
throw new Error('Query parameter name is invalid');
|
|
392
|
+
}
|
|
393
|
+
if (value !== null && (typeof value !== 'string' || value.length > 512)) {
|
|
394
|
+
throw new Error('Query parameter value must be null or a string no longer than 512 characters');
|
|
395
|
+
}
|
|
396
|
+
const query = await requestHost('query-param', { name, value });
|
|
397
|
+
applyContext({ query });
|
|
398
|
+
return query;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const api = { copyText, invoke, operation, ready: () => ready, runAgentTask, runningOperations, selectDirectory, setAgentGuides, setAgentPanelVisible, setQueryParam, start, toast };
|
|
402
|
+
const ui = globalThis.__mearlToolboxUi;
|
|
403
|
+
if (ui !== undefined) {
|
|
404
|
+
if (!ui || typeof ui !== 'object' || ui.runtime !== 'preact') {
|
|
405
|
+
throw new Error('Invalid Mearl toolbox UI runtime');
|
|
406
|
+
}
|
|
407
|
+
Object.defineProperty(api, 'ui', { enumerable: true, value: ui });
|
|
408
|
+
delete globalThis.__mearlToolboxUi;
|
|
409
|
+
}
|
|
410
|
+
Object.defineProperty(api, 'context', { enumerable: true, get: () => context });
|
|
411
|
+
Object.defineProperty(window, 'mearl', { configurable: false, value: Object.freeze(api) });
|
|
412
|
+
document.addEventListener('click', handleExternalLinkClick);
|
|
413
|
+
|
|
414
|
+
const applyContext = nextContext => {
|
|
415
|
+
const theme =
|
|
416
|
+
nextContext?.theme === 'dark' ||
|
|
417
|
+
(nextContext?.theme !== 'light' && context?.theme === 'dark')
|
|
418
|
+
? 'dark'
|
|
419
|
+
: 'light';
|
|
420
|
+
const fontSize = String(nextContext?.fontSize || context?.fontSize || '');
|
|
421
|
+
document.documentElement.dataset.mearlTheme = theme;
|
|
422
|
+
document.documentElement.style.colorScheme = theme;
|
|
423
|
+
if (/^\d+(?:\.\d+)?px$/.test(fontSize)) document.documentElement.style.fontSize = fontSize;
|
|
424
|
+
context = Object.freeze({ ...(context || {}), ...(nextContext || {}), theme });
|
|
425
|
+
return context;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const connect = event => {
|
|
429
|
+
const message = event.data;
|
|
430
|
+
const nextPort = event.ports && event.ports[0];
|
|
431
|
+
if (
|
|
432
|
+
(event.source && event.source !== window.parent) ||
|
|
433
|
+
message?.type !== 'mearl-plugin-connect' ||
|
|
434
|
+
!nextPort
|
|
435
|
+
) return;
|
|
436
|
+
window.removeEventListener('message', connect);
|
|
437
|
+
applyContext(message.context);
|
|
438
|
+
port = nextPort;
|
|
439
|
+
port.onmessage = responseEvent => {
|
|
440
|
+
const response = responseEvent.data;
|
|
441
|
+
if (response?.type === 'context') {
|
|
442
|
+
applyContext(response.context);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (
|
|
446
|
+
response?.type === 'agent-panel-wheel' &&
|
|
447
|
+
typeof response.key === 'string' &&
|
|
448
|
+
inlineAgentPanels.has(response.key) &&
|
|
449
|
+
typeof response.deltaX === 'number' &&
|
|
450
|
+
Number.isFinite(response.deltaX) &&
|
|
451
|
+
typeof response.deltaY === 'number' &&
|
|
452
|
+
Number.isFinite(response.deltaY)
|
|
453
|
+
) {
|
|
454
|
+
scrollInlineAgentPage(
|
|
455
|
+
inlineAgentPanels.get(response.key).presentation,
|
|
456
|
+
response.deltaX,
|
|
457
|
+
response.deltaY,
|
|
458
|
+
);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (response?.type !== 'result' || typeof response.requestId !== 'string') return;
|
|
462
|
+
const request = pending.get(response.requestId);
|
|
463
|
+
if (!request) return;
|
|
464
|
+
pending.delete(response.requestId);
|
|
465
|
+
if (response.ok) request.resolve(response.result);
|
|
466
|
+
else request.reject(new Error(response.error || 'Plugin command failed'));
|
|
467
|
+
};
|
|
468
|
+
port.start();
|
|
469
|
+
resolveReady(context);
|
|
470
|
+
port.postMessage({ type: 'ready' });
|
|
471
|
+
};
|
|
472
|
+
window.addEventListener('message', connect);
|
|
473
|
+
window.parent.postMessage({ type: 'mearl-plugin-ready' }, '*');
|
|
474
|
+
})();`;
|
|
475
|
+
const PAGE_SECURITY_META = "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; media-src data: blob:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">";
|
|
476
|
+
const PAGE_THEME_STYLE = `<style data-mearl-theme>
|
|
477
|
+
:root {
|
|
478
|
+
color-scheme: light dark;
|
|
479
|
+
--mearl-toolbar: #f3f3f3;
|
|
480
|
+
--mearl-toolbar-border: #d0d0d0;
|
|
481
|
+
--mearl-text-primary: #2c2c2c;
|
|
482
|
+
--mearl-text-secondary: #6e6e6e;
|
|
483
|
+
--mearl-bg-primary: #ffffff;
|
|
484
|
+
--mearl-bg-secondary: #f8f9fa;
|
|
485
|
+
--mearl-bg-list: #fafbfc;
|
|
486
|
+
--mearl-bg-hover: #f0f1f3;
|
|
487
|
+
--mearl-bg-selected: #f5f5f5;
|
|
488
|
+
--mearl-border: #e1e4e8;
|
|
489
|
+
--mearl-accent: #2a57c9;
|
|
490
|
+
--mearl-accent-hover: #1d4ed8;
|
|
491
|
+
--mearl-action-primary: oklch(54.6% 0.245 262.881);
|
|
492
|
+
--mearl-action-primary-hover: oklch(48.8% 0.243 264.376);
|
|
493
|
+
--mearl-action-primary-text: #ffffff;
|
|
494
|
+
--mearl-danger: #dc2626;
|
|
495
|
+
--mearl-danger-bg: rgb(220 38 38 / 8%);
|
|
496
|
+
--mearl-success: #16a34a;
|
|
497
|
+
--mearl-scrollbar-track: #f6f8fa;
|
|
498
|
+
--mearl-scrollbar-thumb: #d1d5db;
|
|
499
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
500
|
+
background: var(--mearl-bg-primary);
|
|
501
|
+
}
|
|
502
|
+
:root[data-mearl-theme="dark"] {
|
|
503
|
+
color-scheme: dark;
|
|
504
|
+
--mearl-toolbar: #1f1f1f;
|
|
505
|
+
--mearl-toolbar-border: #343434;
|
|
506
|
+
--mearl-text-primary: #e4e4e7;
|
|
507
|
+
--mearl-text-secondary: #a1a1aa;
|
|
508
|
+
--mearl-bg-primary: #0f0f0f;
|
|
509
|
+
--mearl-bg-secondary: #1a1a1a;
|
|
510
|
+
--mearl-bg-list: #0f0f0f;
|
|
511
|
+
--mearl-bg-hover: #262626;
|
|
512
|
+
--mearl-bg-selected: #1f1f1f;
|
|
513
|
+
--mearl-border: #30363d;
|
|
514
|
+
--mearl-accent: #58a6ff;
|
|
515
|
+
--mearl-accent-hover: #79b8ff;
|
|
516
|
+
--mearl-danger: #f87171;
|
|
517
|
+
--mearl-danger-bg: rgb(248 113 113 / 10%);
|
|
518
|
+
--mearl-success: #4ade80;
|
|
519
|
+
--mearl-scrollbar-track: #1a1a1a;
|
|
520
|
+
--mearl-scrollbar-thumb: #3b3b3b;
|
|
521
|
+
}
|
|
522
|
+
@media (prefers-color-scheme: dark) {
|
|
523
|
+
:root:not([data-mearl-theme="light"]) {
|
|
524
|
+
color-scheme: dark;
|
|
525
|
+
--mearl-toolbar: #1f1f1f;
|
|
526
|
+
--mearl-toolbar-border: #343434;
|
|
527
|
+
--mearl-text-primary: #e4e4e7;
|
|
528
|
+
--mearl-text-secondary: #a1a1aa;
|
|
529
|
+
--mearl-bg-primary: #0f0f0f;
|
|
530
|
+
--mearl-bg-secondary: #1a1a1a;
|
|
531
|
+
--mearl-bg-list: #0f0f0f;
|
|
532
|
+
--mearl-bg-hover: #262626;
|
|
533
|
+
--mearl-bg-selected: #1f1f1f;
|
|
534
|
+
--mearl-border: #30363d;
|
|
535
|
+
--mearl-accent: #58a6ff;
|
|
536
|
+
--mearl-accent-hover: #79b8ff;
|
|
537
|
+
--mearl-danger: #f87171;
|
|
538
|
+
--mearl-danger-bg: rgb(248 113 113 / 10%);
|
|
539
|
+
--mearl-success: #4ade80;
|
|
540
|
+
--mearl-scrollbar-track: #1a1a1a;
|
|
541
|
+
--mearl-scrollbar-thumb: #3b3b3b;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
body {
|
|
545
|
+
margin: 0;
|
|
546
|
+
background: var(--mearl-bg-primary);
|
|
547
|
+
color: var(--mearl-text-primary);
|
|
548
|
+
}
|
|
549
|
+
button, input, select, textarea { font: inherit; }
|
|
550
|
+
</style>`;
|
|
551
|
+
function pluginRoot(entry) {
|
|
552
|
+
const entryDirectory = dirname(entry);
|
|
553
|
+
return basename(entryDirectory) === 'scripts' ? dirname(entryDirectory) : entryDirectory;
|
|
554
|
+
}
|
|
555
|
+
function attributeValue(tag, name) {
|
|
556
|
+
const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i'));
|
|
557
|
+
return match?.[1] ?? match?.[2] ?? match?.[3];
|
|
558
|
+
}
|
|
559
|
+
function removeAttribute(tag, name) {
|
|
560
|
+
return tag.replace(new RegExp(`\\s+${name}\\s*=\\s*(?:"[^"]*"|'[^']*'|[^\\s>]+)`, 'i'), '');
|
|
561
|
+
}
|
|
562
|
+
function isEmbeddedReference(reference) {
|
|
563
|
+
return /^(?:data:|blob:|#)/i.test(reference.trim());
|
|
564
|
+
}
|
|
565
|
+
function isRemoteReference(reference) {
|
|
566
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(reference.trim());
|
|
567
|
+
}
|
|
568
|
+
function contentType(path) {
|
|
569
|
+
return MIME_TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream';
|
|
570
|
+
}
|
|
571
|
+
function escapeStyleText(value) {
|
|
572
|
+
return value.replace(/<\/style/gi, '\\3C /style');
|
|
573
|
+
}
|
|
574
|
+
function escapeScriptText(value) {
|
|
575
|
+
return value.replace(/<\/script/gi, '<\\/script');
|
|
576
|
+
}
|
|
577
|
+
export function buildPluginPageBundle(entry, pageEntry) {
|
|
578
|
+
const root = realpathSync(pluginRoot(entry));
|
|
579
|
+
let sourceBytes = 0;
|
|
580
|
+
const countedFiles = new Set();
|
|
581
|
+
const resolveLocalFile = (reference, fromFile) => {
|
|
582
|
+
const cleanReference = reference.trim().split(/[?#]/, 1)[0];
|
|
583
|
+
if (!cleanReference || isEmbeddedReference(cleanReference))
|
|
584
|
+
return '';
|
|
585
|
+
if (isRemoteReference(cleanReference) || isAbsolute(cleanReference)) {
|
|
586
|
+
throw new Error(`toolbox.page 不允许远程或绝对资源: ${reference}`);
|
|
587
|
+
}
|
|
588
|
+
const resolved = realpathSync(resolve(dirname(fromFile), cleanReference));
|
|
589
|
+
const relativePath = relative(root, resolved);
|
|
590
|
+
if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
591
|
+
throw new Error(`toolbox.page 资源超出插件目录: ${reference}`);
|
|
592
|
+
}
|
|
593
|
+
return resolved;
|
|
594
|
+
};
|
|
595
|
+
const readAsset = (path) => {
|
|
596
|
+
const content = readFileSync(path);
|
|
597
|
+
if (!countedFiles.has(path)) {
|
|
598
|
+
countedFiles.add(path);
|
|
599
|
+
sourceBytes += content.byteLength;
|
|
600
|
+
if (sourceBytes > MAX_PAGE_SOURCE_BYTES) {
|
|
601
|
+
throw new Error('toolbox.page 源文件总大小不能超过 16 MiB');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return content;
|
|
605
|
+
};
|
|
606
|
+
const dataUrl = (reference, fromFile) => {
|
|
607
|
+
if (isEmbeddedReference(reference))
|
|
608
|
+
return reference;
|
|
609
|
+
const path = resolveLocalFile(reference, fromFile);
|
|
610
|
+
return `data:${contentType(path)};base64,${readAsset(path).toString('base64')}`;
|
|
611
|
+
};
|
|
612
|
+
const inlineCss = (css, cssPath) => css.replace(/url\(\s*(['"]?)([^'"()]+)\1\s*\)/gi, (_match, _quote, reference) => {
|
|
613
|
+
const value = reference.trim();
|
|
614
|
+
if (isEmbeddedReference(value))
|
|
615
|
+
return `url("${value}")`;
|
|
616
|
+
return `url("${dataUrl(value, cssPath)}")`;
|
|
617
|
+
});
|
|
618
|
+
const absoluteEntry = resolveLocalFile(pageEntry, join(root, '__toolbox_entry__'));
|
|
619
|
+
if (extname(absoluteEntry).toLowerCase() !== '.html') {
|
|
620
|
+
throw new Error('toolbox.page.entry 必须指向 HTML 文件');
|
|
621
|
+
}
|
|
622
|
+
let html = readAsset(absoluteEntry)
|
|
623
|
+
.toString('utf8')
|
|
624
|
+
.replace(/^\uFEFF/, '');
|
|
625
|
+
if (!/<html\b|<!doctype\b/i.test(html)) {
|
|
626
|
+
throw new Error('toolbox.page.entry 必须是完整 HTML 文档');
|
|
627
|
+
}
|
|
628
|
+
if (/<base\b/i.test(html))
|
|
629
|
+
throw new Error('toolbox.page 不允许声明 base 标签');
|
|
630
|
+
if (/<meta\b[^>]*http-equiv\s*=\s*["']?content-security-policy/i.test(html)) {
|
|
631
|
+
throw new Error('toolbox.page 的安全策略由 Mearl 管理,请移除 CSP meta 标签');
|
|
632
|
+
}
|
|
633
|
+
html = html.replace(/<link\b[^>]*>/gi, tag => {
|
|
634
|
+
const rel = attributeValue(tag, 'rel')?.toLowerCase().split(/\s+/) ?? [];
|
|
635
|
+
const href = attributeValue(tag, 'href');
|
|
636
|
+
if (rel.includes('stylesheet')) {
|
|
637
|
+
if (!href)
|
|
638
|
+
throw new Error('toolbox.page stylesheet 缺少 href');
|
|
639
|
+
const cssPath = resolveLocalFile(href, absoluteEntry);
|
|
640
|
+
const css = inlineCss(readAsset(cssPath).toString('utf8'), cssPath);
|
|
641
|
+
return `<style data-mearl-source="${href.replaceAll('"', '"')}">${escapeStyleText(css)}</style>`;
|
|
642
|
+
}
|
|
643
|
+
if (href && rel.some(value => ['icon', 'apple-touch-icon'].includes(value))) {
|
|
644
|
+
return tag.replace(href, dataUrl(href, absoluteEntry));
|
|
645
|
+
}
|
|
646
|
+
if (href && isRemoteReference(href)) {
|
|
647
|
+
throw new Error(`toolbox.page 不允许远程 link 资源: ${href}`);
|
|
648
|
+
}
|
|
649
|
+
return tag;
|
|
650
|
+
});
|
|
651
|
+
html = html.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, tag => {
|
|
652
|
+
const src = attributeValue(tag, 'src');
|
|
653
|
+
if (!src)
|
|
654
|
+
return tag;
|
|
655
|
+
const scriptPath = resolveLocalFile(src, absoluteEntry);
|
|
656
|
+
const script = readAsset(scriptPath).toString('utf8');
|
|
657
|
+
if (attributeValue(tag, 'type')?.toLowerCase() === 'module' &&
|
|
658
|
+
/(?:^|[;\n]\s*)(?:import\s|export\s)|\bimport\s*\(/m.test(script)) {
|
|
659
|
+
throw new Error('toolbox.page 的模块脚本必须先打包为无 import 的单文件');
|
|
660
|
+
}
|
|
661
|
+
const openingTag = removeAttribute(tag.slice(0, tag.indexOf('>') + 1), 'src');
|
|
662
|
+
return `${openingTag}${escapeScriptText(script)}</script>`;
|
|
663
|
+
});
|
|
664
|
+
html = html.replace(/(<(?:img|source|video|audio|track|input)\b[^>]*?\s(?:src|poster)\s*=\s*)(["'])([^"']+)\2/gi, (_match, prefix, quote, reference) => `${prefix}${quote}${dataUrl(reference, absoluteEntry)}${quote}`);
|
|
665
|
+
const injection = `<meta name="color-scheme" content="light dark">${PAGE_SECURITY_META}${PAGE_THEME_STYLE}<script data-mearl-bridge>${escapeScriptText(PAGE_BRIDGE_SCRIPT)}</script>`;
|
|
666
|
+
html = /<head\b[^>]*>/i.test(html)
|
|
667
|
+
? html.replace(/<head\b[^>]*>/i, tag => `${tag}${injection}`)
|
|
668
|
+
: html.replace(/<html\b[^>]*>/i, tag => `${tag}<head>${injection}</head>`);
|
|
669
|
+
const content = html;
|
|
670
|
+
const buffer = Buffer.from(content, 'utf8');
|
|
671
|
+
if (buffer.byteLength > MAX_PAGE_BUNDLE_BYTES) {
|
|
672
|
+
throw new Error('toolbox.page 打包后不能超过 24 MiB');
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
content,
|
|
676
|
+
hash: createHash('sha256').update(buffer).digest('hex'),
|
|
677
|
+
size: buffer.byteLength,
|
|
678
|
+
chunks: Math.ceil(buffer.byteLength / PLUGIN_PAGE_CHUNK_BYTES),
|
|
679
|
+
};
|
|
680
|
+
}
|