@mearl/client 2.20.5 → 2.21.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 +122 -2
- package/dist/cli.js +10 -5
- package/dist/commands.d.ts +14 -5
- package/dist/commands.js +21 -2
- package/dist/generated-app-profile-definition.js +20 -10
- package/dist/generated-app-profile-runtime-types.d.ts +4 -0
- package/dist/generated-browser-action-protocol.d.ts +11 -1
- package/dist/generated-browser-action-protocol.js +3 -0
- package/dist/generated-plugin-diagnostics.d.ts +110 -0
- package/dist/generated-plugin-diagnostics.js +1 -0
- package/dist/generated-plugin-execution.d.ts +27 -0
- package/dist/generated-plugin-execution.js +1 -0
- package/dist/generated-plugin-models.d.ts +64 -0
- package/dist/generated-plugin-models.js +1 -0
- package/dist/generated-plugin-pages.d.ts +76 -0
- package/dist/generated-plugin-pages.js +59 -0
- package/dist/generated-plugin-recovery.d.ts +32 -0
- package/dist/generated-plugin-recovery.js +1 -0
- package/dist/generated-plugin-state.d.ts +50 -0
- package/dist/generated-plugin-state.js +1 -0
- package/dist/generated-plugin-tasks.d.ts +74 -0
- package/dist/generated-plugin-tasks.js +7 -0
- package/dist/generated-toolbox-icon.d.ts +10 -0
- package/dist/generated-toolbox-icon.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/plugin-task-host.d.ts +1 -0
- package/dist/plugin-task-host.js +14 -0
- package/dist/plugin-worker.d.ts +1 -0
- package/dist/plugin-worker.js +17 -0
- package/dist/plugin.d.ts +57 -4
- package/dist/plugin.js +296 -15
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginCommandWorker.d.ts +2 -0
- package/dist/pluginCommandWorker.js +174 -0
- package/dist/pluginDiagnostics.d.ts +26 -0
- package/dist/pluginDiagnostics.js +309 -0
- package/dist/pluginExecution.d.ts +21 -0
- package/dist/pluginExecution.js +43 -0
- package/dist/pluginExecutor.d.ts +29 -0
- package/dist/pluginExecutor.js +223 -0
- package/dist/pluginExecutorProtocol.d.ts +55 -0
- package/dist/pluginExecutorProtocol.js +47 -0
- package/dist/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/pluginExecutorSupervisor.js +63 -0
- package/dist/pluginHost.d.ts +20 -0
- package/dist/pluginHost.js +53 -0
- package/dist/pluginHostConnection.d.ts +35 -0
- package/dist/pluginHostConnection.js +257 -0
- package/dist/pluginHostFiles.d.ts +1 -0
- package/dist/pluginHostFiles.js +21 -0
- package/dist/pluginHostPaths.d.ts +11 -0
- package/dist/pluginHostPaths.js +22 -0
- package/dist/pluginHostServer.d.ts +7 -0
- package/dist/pluginHostServer.js +252 -0
- package/dist/pluginModelCatalog.d.ts +14 -0
- package/dist/pluginModelCatalog.js +51 -0
- package/dist/pluginModelDefinition.d.ts +6 -0
- package/dist/pluginModelDefinition.js +75 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/pluginModelSessions.d.ts +16 -0
- package/dist/pluginModelSessions.js +234 -0
- package/dist/pluginModels.d.ts +17 -0
- package/dist/pluginModels.js +47 -0
- package/dist/pluginPageBundle.js +74 -17
- package/dist/pluginPageDefinition.d.ts +3 -0
- package/dist/pluginPageDefinition.js +62 -0
- package/dist/pluginPageManager.d.ts +57 -0
- package/dist/pluginPageManager.js +877 -0
- package/dist/pluginPages.d.ts +21 -0
- package/dist/pluginPages.js +46 -0
- package/dist/pluginProcessTree.d.ts +2 -0
- package/dist/pluginProcessTree.js +31 -0
- package/dist/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/pluginRecoveryDefinition.js +56 -0
- package/dist/pluginRegistry.d.ts +2 -3
- package/dist/pluginRegistry.js +14 -13
- package/dist/pluginRevision.d.ts +14 -0
- package/dist/pluginRevision.js +227 -0
- package/dist/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/pluginRuntimeCleanup.js +55 -0
- package/dist/pluginState.d.ts +26 -0
- package/dist/pluginState.js +28 -0
- package/dist/pluginStateDefinition.d.ts +4 -0
- package/dist/pluginStateDefinition.js +68 -0
- package/dist/pluginStateManager.d.ts +24 -0
- package/dist/pluginStateManager.js +132 -0
- package/dist/pluginStateStore.d.ts +57 -0
- package/dist/pluginStateStore.js +396 -0
- package/dist/pluginTaskManager.d.ts +92 -0
- package/dist/pluginTaskManager.js +1080 -0
- package/dist/pluginTaskProtocol.d.ts +47 -0
- package/dist/pluginTaskProtocol.js +1 -0
- package/dist/pluginTaskStore.d.ts +53 -0
- package/dist/pluginTaskStore.js +319 -0
- package/dist/pluginTaskWire.d.ts +4 -0
- package/dist/pluginTaskWire.js +42 -0
- package/dist/pluginTasks.d.ts +51 -0
- package/dist/pluginTasks.js +181 -0
- package/dist/pythonPlugin.d.ts +13 -2
- package/dist/pythonPlugin.js +235 -30
- package/dist/socket.js +5 -2
- package/dist/unified.d.ts +1 -0
- package/dist/unified.js +81 -0
- package/package.json +7 -3
package/dist/plugin.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizePluginState, pluginStateSignature } from './pluginStateDefinition.js';
|
|
2
|
+
import { normalizePluginTaskRecovery, pluginRecoverySignature, } from './pluginRecoveryDefinition.js';
|
|
3
|
+
import { normalizePluginPageRuntime } from './pluginPageDefinition.js';
|
|
1
4
|
import { loadPluginAppProfiles } from './pluginAppProfiles.js';
|
|
2
5
|
export { invokePageEvalWithArgs } from './pageEvalArguments.js';
|
|
3
6
|
import { readFileSync } from 'node:fs';
|
|
@@ -10,7 +13,12 @@ import { ensureOfficialPluginRegistered, } from './officialPluginInstaller.js';
|
|
|
10
13
|
import { findPluginRegistration, pluginRegistrationNeedsRefresh, registerPlugin, pluginDirectory, normalizePluginToolboxContribution, PluginError, } from './pluginRegistry.js';
|
|
11
14
|
import { pluginFileVersion } from './pluginSource.js';
|
|
12
15
|
import { describePythonPlugin, runPythonPluginCommand } from './pythonPlugin.js';
|
|
13
|
-
|
|
16
|
+
import { createPluginInvocationIdentity, } from './pluginExecution.js';
|
|
17
|
+
export { executeRegisteredPlugin, } from './pluginExecutor.js';
|
|
18
|
+
export { PluginTaskClient } from './pluginTasks.js';
|
|
19
|
+
export { cleanupPluginRuntime } from './pluginRuntimeCleanup.js';
|
|
20
|
+
export { PluginPageClient } from './pluginPages.js';
|
|
21
|
+
export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, pluginDirectory, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, } from './pluginRegistry.js';
|
|
14
22
|
export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, } from './officialPluginCatalog.js';
|
|
15
23
|
function readPluginVersion(entry) {
|
|
16
24
|
try {
|
|
@@ -107,6 +115,7 @@ export async function refreshRegisteredPlugin(name, includePage = false) {
|
|
|
107
115
|
throw new PluginError('PLUGIN_SOURCE_CHANGED', `插件更新进行中,请重新打开工具: ${name}`);
|
|
108
116
|
}
|
|
109
117
|
registerPlugin(name, registration.entry, loaded.description, {
|
|
118
|
+
hasPublicCommands: 'commandHelp' in loaded ? Boolean(loaded.commandHelp(name)) : loaded.hasPublicCommands,
|
|
110
119
|
toolbox: loaded.toolbox,
|
|
111
120
|
appProfiles: loaded.appProfiles,
|
|
112
121
|
profileSources: 'profileSources' in loaded ? loaded.profileSources : loaded.source?.profiles,
|
|
@@ -132,14 +141,21 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
132
141
|
throw new PluginError('PLUGIN_ENTRY_MISSING', missingPluginEntryMessage(entry, pluginName));
|
|
133
142
|
}
|
|
134
143
|
const commands = new Map();
|
|
144
|
+
const recoveries = new Map();
|
|
145
|
+
const migrations = new Map();
|
|
135
146
|
let executing = false;
|
|
147
|
+
let checkpointDeclaration;
|
|
148
|
+
let invocationGeneration = 0;
|
|
136
149
|
let registering = true;
|
|
137
150
|
let name;
|
|
138
151
|
let description;
|
|
139
152
|
let toolbox;
|
|
153
|
+
let pageRuntime;
|
|
154
|
+
let stateStorage;
|
|
155
|
+
let taskRecovery;
|
|
140
156
|
let appProfiles;
|
|
141
157
|
let profileSources;
|
|
142
|
-
const registerCommand = (signature, handler, description = '', examples = [], toolboxOnly = false) => {
|
|
158
|
+
const registerCommand = (signature, handler, description = '', examples = [], toolboxOnly = false, runtimeOnly = false) => {
|
|
143
159
|
if (!registering)
|
|
144
160
|
throw new Error('命令只能在入口注册阶段声明');
|
|
145
161
|
const [name, ...args] = signature.trim().split(/\s+/);
|
|
@@ -160,15 +176,99 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
160
176
|
const names = command.registeredArguments.map(argument => argument.name());
|
|
161
177
|
if (new Set(names).size !== names.length)
|
|
162
178
|
throw new Error(`参数名称重复: ${signature}`);
|
|
163
|
-
commands.set(name, {
|
|
179
|
+
commands.set(name, {
|
|
180
|
+
command,
|
|
181
|
+
signature,
|
|
182
|
+
handler,
|
|
183
|
+
examples: [...examples],
|
|
184
|
+
toolboxOnly,
|
|
185
|
+
runtimeOnly,
|
|
186
|
+
});
|
|
164
187
|
};
|
|
165
188
|
const app = Object.freeze({
|
|
166
189
|
options: Object.freeze({ ...context.options }),
|
|
167
190
|
command: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples),
|
|
191
|
+
runtimeCommand: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples, false, true),
|
|
192
|
+
recoveryCommand(command, handler) {
|
|
193
|
+
if (!registering ||
|
|
194
|
+
!/^[a-z][a-z0-9-]*$/.test(command) ||
|
|
195
|
+
typeof handler !== 'function' ||
|
|
196
|
+
recoveries.has(command))
|
|
197
|
+
throw new Error('Invalid or duplicate recovery command');
|
|
198
|
+
recoveries.set(command, handler);
|
|
199
|
+
},
|
|
200
|
+
checkpointMigration(command, handler) {
|
|
201
|
+
if (!registering ||
|
|
202
|
+
!/^[a-z][a-z0-9-]*$/.test(command) ||
|
|
203
|
+
typeof handler !== 'function' ||
|
|
204
|
+
migrations.has(command))
|
|
205
|
+
throw new Error('Invalid or duplicate checkpoint migration');
|
|
206
|
+
migrations.set(command, (input, commandContext) => handler(input, commandContext));
|
|
207
|
+
},
|
|
208
|
+
checkpoint(data, expectedSequence) {
|
|
209
|
+
if (!executing || !context.invocation?.taskId)
|
|
210
|
+
throw new PluginError('PLUGIN_TASK_REQUIRED', 'Checkpoints require a managed task');
|
|
211
|
+
context.signal?.throwIfAborted();
|
|
212
|
+
return context.invoke('plugin_task_checkpoint', {
|
|
213
|
+
plugin: pluginName ?? name,
|
|
214
|
+
data,
|
|
215
|
+
expectedSequence,
|
|
216
|
+
expectedDeclaration: checkpointDeclaration,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
state(collection, operation, input = {}) {
|
|
220
|
+
if (!executing)
|
|
221
|
+
throw new Error('app.state is only available during command execution');
|
|
222
|
+
context.signal?.throwIfAborted();
|
|
223
|
+
if (!stateStorage || !Object.hasOwn(stateStorage.collections, collection))
|
|
224
|
+
throw new PluginError('PLUGIN_STATE_NOT_DECLARED', 'Plugin state collection is not declared');
|
|
225
|
+
return context.invoke('plugin_state', {
|
|
226
|
+
...input,
|
|
227
|
+
plugin: pluginName ?? name,
|
|
228
|
+
collection,
|
|
229
|
+
operation,
|
|
230
|
+
expectedDeclaration: pluginStateSignature(stateStorage.collections[collection]),
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
model(request, options = {}) {
|
|
234
|
+
if (!executing)
|
|
235
|
+
throw new Error('app.model is only available during command execution');
|
|
236
|
+
context.signal?.throwIfAborted();
|
|
237
|
+
const generation = invocationGeneration;
|
|
238
|
+
const observing = () => executing && generation === invocationGeneration && !context.signal?.aborted;
|
|
239
|
+
if (context.runModel)
|
|
240
|
+
return context.runModel(pluginName ?? name, request, {
|
|
241
|
+
signal: context.signal,
|
|
242
|
+
onProgress: progress => {
|
|
243
|
+
if (!observing())
|
|
244
|
+
return;
|
|
245
|
+
context.progress(progress);
|
|
246
|
+
options.onProgress?.(progress);
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
const payload = {
|
|
250
|
+
plugin: pluginName ?? name,
|
|
251
|
+
operation: 'run',
|
|
252
|
+
request,
|
|
253
|
+
};
|
|
254
|
+
return (options.onProgress
|
|
255
|
+
? context.invoke('plugin_model', payload, {}, progress => {
|
|
256
|
+
if (observing())
|
|
257
|
+
options.onProgress?.(progress);
|
|
258
|
+
})
|
|
259
|
+
: context.invoke('plugin_model', payload));
|
|
260
|
+
},
|
|
261
|
+
page(operation, input = {}) {
|
|
262
|
+
if (!executing)
|
|
263
|
+
throw new Error('app.page is only available during command execution');
|
|
264
|
+
context.signal?.throwIfAborted();
|
|
265
|
+
return context.invoke('plugin_page', { ...input, plugin: pluginName ?? name, operation });
|
|
266
|
+
},
|
|
168
267
|
toolboxCommand: (signature, handler, description = '', examples = []) => registerCommand(signature, handler, description, examples, true),
|
|
169
268
|
invoke(action, payload = {}, options = {}) {
|
|
170
269
|
if (!executing)
|
|
171
270
|
throw new Error('app.invoke 只能在命令执行期间调用');
|
|
271
|
+
context.signal?.throwIfAborted();
|
|
172
272
|
return context.invoke(action, payload, options);
|
|
173
273
|
},
|
|
174
274
|
progress(progress) {
|
|
@@ -183,11 +283,26 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
183
283
|
name = descriptor.name;
|
|
184
284
|
description = descriptor.description;
|
|
185
285
|
toolbox = normalizePluginToolboxContribution(descriptor.toolbox);
|
|
286
|
+
pageRuntime = normalizePluginPageRuntime(descriptor.pageRuntime);
|
|
287
|
+
stateStorage = normalizePluginState(descriptor.stateStorage);
|
|
288
|
+
taskRecovery = normalizePluginTaskRecovery(descriptor.taskRecovery);
|
|
186
289
|
({ appProfiles, profileSources } = loadPluginAppProfiles(entry, descriptor.appProfiles));
|
|
187
290
|
for (const command of descriptor.commands) {
|
|
188
291
|
const commandName = command.signature.trim().split(/\s+/)[0];
|
|
189
|
-
|
|
190
|
-
|
|
292
|
+
if (command.migrationOnly) {
|
|
293
|
+
app.checkpointMigration(commandName, (input, commandContext) => runPythonPluginCommand(entry, commandName, input, app, commandContext, 'migration', descriptor));
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (command.recoveryOnly) {
|
|
297
|
+
app.recoveryCommand(commandName, (input, commandContext) => runPythonPluginCommand(entry, commandName, input, app, commandContext, true, descriptor));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const register = command.runtimeOnly
|
|
301
|
+
? app.runtimeCommand
|
|
302
|
+
: command.toolboxOnly
|
|
303
|
+
? app.toolboxCommand
|
|
304
|
+
: app.command;
|
|
305
|
+
register(command.signature, (input, commandContext) => runPythonPluginCommand(entry, commandName, input, app, commandContext, false, descriptor, command.runtimeOnly), command.description);
|
|
191
306
|
}
|
|
192
307
|
}
|
|
193
308
|
else {
|
|
@@ -201,6 +316,9 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
201
316
|
throw new Error('入口的 description 导出必须为字符串');
|
|
202
317
|
description = module.description;
|
|
203
318
|
toolbox = normalizePluginToolboxContribution(module.toolbox);
|
|
319
|
+
pageRuntime = normalizePluginPageRuntime(module.pageRuntime);
|
|
320
|
+
stateStorage = normalizePluginState(module.stateStorage);
|
|
321
|
+
taskRecovery = normalizePluginTaskRecovery(module.taskRecovery);
|
|
204
322
|
({ appProfiles, profileSources } = loadPluginAppProfiles(entry, module.appProfiles));
|
|
205
323
|
if (module.default !== undefined && typeof module.default !== 'function')
|
|
206
324
|
throw new Error('入口必须默认导出 register(app) 函数');
|
|
@@ -211,6 +329,33 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
211
329
|
}
|
|
212
330
|
if (commands.size === 0 && !appProfiles)
|
|
213
331
|
throw new Error('入口未注册任何命令');
|
|
332
|
+
for (const command of Object.keys(taskRecovery?.commands ?? {})) {
|
|
333
|
+
if (!commands.has(command) ||
|
|
334
|
+
commands.get(command)?.runtimeOnly ||
|
|
335
|
+
commands.get(command)?.toolboxOnly ||
|
|
336
|
+
!recoveries.has(command))
|
|
337
|
+
throw new Error(`Recovery requires a command and recoveryCommand: ${command}`);
|
|
338
|
+
}
|
|
339
|
+
for (const command of recoveries.keys())
|
|
340
|
+
if (!Object.hasOwn(taskRecovery?.commands ?? {}, command))
|
|
341
|
+
throw new Error(`Undeclared recovery command: ${command}`);
|
|
342
|
+
for (const [command, definition] of Object.entries(taskRecovery?.commands ?? {})) {
|
|
343
|
+
if (Boolean(definition.migrateFrom?.length) !== migrations.has(command))
|
|
344
|
+
throw new Error(`Checkpoint migrations require a declaration and checkpointMigration: ${command}`);
|
|
345
|
+
}
|
|
346
|
+
for (const command of migrations.keys())
|
|
347
|
+
if (!Object.hasOwn(taskRecovery?.commands ?? {}, command))
|
|
348
|
+
throw new Error(`Undeclared checkpoint migration: ${command}`);
|
|
349
|
+
const runtimeCommands = new Set(Object.values(pageRuntime?.events ?? {}).map(event => event.command));
|
|
350
|
+
if (pageRuntime?.initialize)
|
|
351
|
+
runtimeCommands.add(pageRuntime.initialize);
|
|
352
|
+
for (const [name, command] of commands) {
|
|
353
|
+
if (command.runtimeOnly && !runtimeCommands.has(name))
|
|
354
|
+
throw new Error(`runtimeCommand is not declared in pageRuntime.events: ${name}`);
|
|
355
|
+
}
|
|
356
|
+
for (const name of runtimeCommands)
|
|
357
|
+
if (!commands.has(name) || commands.get(name)?.toolboxOnly)
|
|
358
|
+
throw new Error(`Invalid page runtime command: ${name}`);
|
|
214
359
|
const toolboxCommands = toolbox?.page.commands ?? [];
|
|
215
360
|
for (const [name, command] of commands) {
|
|
216
361
|
if (command.toolboxOnly && !toolboxCommands.includes(name)) {
|
|
@@ -232,7 +377,16 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
232
377
|
const command = commands.get(name);
|
|
233
378
|
if (!command)
|
|
234
379
|
throw new PluginError('PLUGIN_COMMAND_NOT_FOUND', `未知子命令: ${name}`);
|
|
235
|
-
if (toolboxPage) {
|
|
380
|
+
if (toolboxPage === 'runtime') {
|
|
381
|
+
if (!(pageRuntime?.initialize === name ||
|
|
382
|
+
Object.values(pageRuntime?.events ?? {}).some(event => event.command === name)) ||
|
|
383
|
+
command.toolboxOnly)
|
|
384
|
+
throw new PluginError('PLUGIN_COMMAND_NOT_ALLOWED', 'Page runtime command is not declared');
|
|
385
|
+
}
|
|
386
|
+
else if (command.runtimeOnly) {
|
|
387
|
+
throw new PluginError('PLUGIN_COMMAND_RUNTIME_ONLY', `命令仅供页面运行时调用: ${name}`);
|
|
388
|
+
}
|
|
389
|
+
else if (toolboxPage) {
|
|
236
390
|
if (!toolbox?.page.commands.includes(name)) {
|
|
237
391
|
throw new PluginError('PLUGIN_COMMAND_NOT_ALLOWED', `工具页面不允许调用命令: ${name}`);
|
|
238
392
|
}
|
|
@@ -242,7 +396,7 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
242
396
|
}
|
|
243
397
|
return command;
|
|
244
398
|
};
|
|
245
|
-
const publicCommands = () => [...commands.values()].filter(command => !command.toolboxOnly);
|
|
399
|
+
const publicCommands = () => [...commands.values()].filter(command => !command.toolboxOnly && !command.runtimeOnly);
|
|
246
400
|
const run = async (positionals, payload, namedInput = {}, toolboxPage = false) => {
|
|
247
401
|
if (payload !== undefined && positionals.length > 0) {
|
|
248
402
|
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数调用与 --payload 调用不能混用');
|
|
@@ -255,7 +409,21 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
255
409
|
throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
|
|
256
410
|
}
|
|
257
411
|
const envelope = payload;
|
|
258
|
-
const
|
|
412
|
+
const commandName = envelope?.command ?? positionals[0] ?? '';
|
|
413
|
+
const hidden = toolboxPage === 'recovery' || toolboxPage === 'migration';
|
|
414
|
+
const selected = find(commandName, hidden ? false : toolboxPage);
|
|
415
|
+
const command = selected.command;
|
|
416
|
+
const handler = toolboxPage === 'recovery'
|
|
417
|
+
? recoveries.get(commandName)
|
|
418
|
+
: toolboxPage === 'migration'
|
|
419
|
+
? migrations.get(commandName)
|
|
420
|
+
: selected.handler;
|
|
421
|
+
if (!handler || (toolboxPage === 'recovery' && !context.invocation?.recovery))
|
|
422
|
+
throw new PluginError('PLUGIN_TASK_RECOVERY_UNAVAILABLE', 'Recovery handler requires a managed recovery attempt');
|
|
423
|
+
if (toolboxPage === 'recovery' &&
|
|
424
|
+
context.invocation?.recovery?.declarationHash !==
|
|
425
|
+
pluginRecoverySignature(taskRecovery.commands[commandName]))
|
|
426
|
+
throw new PluginError('PLUGIN_RECOVERY_DECLARATION_CHANGED', 'Recovery declaration differs from the admitted format');
|
|
259
427
|
const positionalInput = envelope
|
|
260
428
|
? { ...envelope.input }
|
|
261
429
|
: Object.fromEntries(command.registeredArguments
|
|
@@ -275,7 +443,7 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
275
443
|
positionals.length - 1 > command.registeredArguments.length) {
|
|
276
444
|
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
|
|
277
445
|
}
|
|
278
|
-
for (const argument of command.registeredArguments) {
|
|
446
|
+
for (const argument of hidden ? [] : command.registeredArguments) {
|
|
279
447
|
const value = Object.hasOwn(input, argument.name()) ? input[argument.name()] : undefined;
|
|
280
448
|
if (argument.required &&
|
|
281
449
|
(value === undefined ||
|
|
@@ -288,18 +456,86 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
288
456
|
throw new PluginError('PLUGIN_INVALID_INPUT', `${argument.name()} 必须为数组`);
|
|
289
457
|
}
|
|
290
458
|
}
|
|
459
|
+
if (executing)
|
|
460
|
+
throw new PluginError('PLUGIN_BUSY', '同一命令实例不能并发执行');
|
|
461
|
+
const signal = context.signal ?? new AbortController().signal;
|
|
462
|
+
signal.throwIfAborted();
|
|
463
|
+
const identity = context.invocation ??
|
|
464
|
+
createPluginInvocationIdentity(pluginName ?? name ?? 'unregistered', context.source ?? 'sdk', context.parentInvocation);
|
|
465
|
+
const generation = ++invocationGeneration;
|
|
466
|
+
const assertActive = () => {
|
|
467
|
+
if (!executing || generation !== invocationGeneration)
|
|
468
|
+
throw new PluginError('PLUGIN_INVOCATION_ENDED', '命令调用已结束');
|
|
469
|
+
signal.throwIfAborted();
|
|
470
|
+
};
|
|
471
|
+
const commandContext = Object.freeze({
|
|
472
|
+
identity,
|
|
473
|
+
signal,
|
|
474
|
+
...(identity.recovery ? { recovery: structuredClone(identity.recovery) } : {}),
|
|
475
|
+
checkpoint: (data, expectedSequence) => {
|
|
476
|
+
assertActive();
|
|
477
|
+
return app.checkpoint(data, expectedSequence);
|
|
478
|
+
},
|
|
479
|
+
...(identity.page
|
|
480
|
+
? {
|
|
481
|
+
page: Object.freeze({
|
|
482
|
+
send: (message, options = {}) => {
|
|
483
|
+
assertActive();
|
|
484
|
+
return app.page('send', {
|
|
485
|
+
instanceId: identity.page.instanceId,
|
|
486
|
+
message,
|
|
487
|
+
...(options.timeoutSec !== undefined ? { timeoutSec: options.timeoutSec } : {}),
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
get: () => {
|
|
491
|
+
assertActive();
|
|
492
|
+
return app.page('get', { instanceId: identity.page.instanceId });
|
|
493
|
+
},
|
|
494
|
+
}),
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
|
+
invoke: (action, payload, options) => {
|
|
498
|
+
assertActive();
|
|
499
|
+
return app.invoke(action, payload, options);
|
|
500
|
+
},
|
|
501
|
+
state: (collection, operation, input) => {
|
|
502
|
+
assertActive();
|
|
503
|
+
return app.state(collection, operation, {
|
|
504
|
+
...(identity.page ? { instanceId: identity.page.instanceId } : {}),
|
|
505
|
+
...input,
|
|
506
|
+
});
|
|
507
|
+
},
|
|
508
|
+
model: (request, options) => {
|
|
509
|
+
assertActive();
|
|
510
|
+
return app.model(request, options);
|
|
511
|
+
},
|
|
512
|
+
progress: progress => {
|
|
513
|
+
assertActive();
|
|
514
|
+
app.progress(progress);
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
checkpointDeclaration =
|
|
518
|
+
taskRecovery && Object.hasOwn(taskRecovery.commands, commandName)
|
|
519
|
+
? pluginRecoverySignature(taskRecovery.commands[commandName])
|
|
520
|
+
: undefined;
|
|
291
521
|
executing = true;
|
|
522
|
+
context.onInvocation?.(identity);
|
|
292
523
|
try {
|
|
293
|
-
return await handler(input);
|
|
524
|
+
return await handler(input, commandContext);
|
|
294
525
|
}
|
|
295
526
|
finally {
|
|
296
527
|
executing = false;
|
|
528
|
+
checkpointDeclaration = undefined;
|
|
529
|
+
context.onInvocation?.(undefined);
|
|
297
530
|
}
|
|
298
531
|
};
|
|
299
532
|
return {
|
|
300
533
|
name,
|
|
301
534
|
description,
|
|
302
535
|
toolbox,
|
|
536
|
+
pageRuntime,
|
|
537
|
+
stateStorage,
|
|
538
|
+
taskRecovery,
|
|
303
539
|
appProfiles,
|
|
304
540
|
profileSources,
|
|
305
541
|
version: readPluginVersion(entry),
|
|
@@ -310,6 +546,12 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
310
546
|
},
|
|
311
547
|
help(namespace, name) {
|
|
312
548
|
const selected = name ? [find(name)] : publicCommands();
|
|
549
|
+
if (!selected.length) {
|
|
550
|
+
const location = toolbox
|
|
551
|
+
? `请在 Mearl ${toolbox.displayMode === 'popup' ? 'Popup' : '工具箱'} 中打开「${toolbox.title}」使用。`
|
|
552
|
+
: '此插件未提供公开 CLI 命令。';
|
|
553
|
+
return `${namespace} 未提供 CLI 命令。\n${location}`;
|
|
554
|
+
}
|
|
313
555
|
const examples = selected.flatMap(({ examples }) => examples);
|
|
314
556
|
return [
|
|
315
557
|
'用法:',
|
|
@@ -322,6 +564,15 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
322
564
|
run(positionals, payload, namedInput = {}) {
|
|
323
565
|
return run(positionals, payload, namedInput);
|
|
324
566
|
},
|
|
567
|
+
runRuntime(invocation) {
|
|
568
|
+
return run([], invocation, {}, 'runtime');
|
|
569
|
+
},
|
|
570
|
+
runRecovery(invocation) {
|
|
571
|
+
return run([], invocation, {}, 'recovery');
|
|
572
|
+
},
|
|
573
|
+
runMigration(invocation) {
|
|
574
|
+
return run([], invocation, {}, 'migration');
|
|
575
|
+
},
|
|
325
576
|
runToolbox(invocation) {
|
|
326
577
|
return run([], invocation, {}, true);
|
|
327
578
|
},
|
|
@@ -331,7 +582,9 @@ async function loadRegisteredPluginWithState(name, context, options = {}, state
|
|
|
331
582
|
if (state.stack.includes(name)) {
|
|
332
583
|
throw new PluginError('PLUGIN_INVOCATION_CYCLE', `插件调用形成循环: ${[...state.stack, name].join(' -> ')}`);
|
|
333
584
|
}
|
|
334
|
-
let registration =
|
|
585
|
+
let registration = context.resolveRegistration
|
|
586
|
+
? context.resolveRegistration(name)
|
|
587
|
+
: findPluginRegistration(name);
|
|
335
588
|
if (!registration && findOfficialPlugin(name)) {
|
|
336
589
|
await (context.ensurePlugin ?? ensureOfficialPluginRegistered)(name, options.onProgress ? message => options.onProgress?.(message) : undefined);
|
|
337
590
|
registration = findPluginRegistration(name);
|
|
@@ -339,12 +592,21 @@ async function loadRegisteredPluginWithState(name, context, options = {}, state
|
|
|
339
592
|
if (!registration) {
|
|
340
593
|
throw new PluginError('PLUGIN_NOT_REGISTERED', `插件未注册: ${name}`);
|
|
341
594
|
}
|
|
342
|
-
const { onProgress, ...invokeOptions } = options;
|
|
595
|
+
const { onProgress, signal, source, ...invokeOptions } = options;
|
|
343
596
|
const childState = { stack: [...state.stack, name] };
|
|
597
|
+
let activeInvocation;
|
|
344
598
|
return loadPlugin(registration.entry, {
|
|
345
599
|
options: invokeOptions,
|
|
600
|
+
source: source ?? context.source,
|
|
601
|
+
parentInvocation: context.parentInvocation,
|
|
602
|
+
invocation: context.invocation,
|
|
603
|
+
signal,
|
|
604
|
+
runModel: context.runModel,
|
|
605
|
+
onInvocation: identity => {
|
|
606
|
+
activeInvocation = identity;
|
|
607
|
+
},
|
|
346
608
|
progress: onProgress ?? (() => undefined),
|
|
347
|
-
async invoke(action, payload, overrides) {
|
|
609
|
+
async invoke(action, payload, overrides, progress) {
|
|
348
610
|
const nestedOptions = {
|
|
349
611
|
...invokeOptions,
|
|
350
612
|
...overrides,
|
|
@@ -361,12 +623,21 @@ async function loadRegisteredPluginWithState(name, context, options = {}, state
|
|
|
361
623
|
if (!pluginInvocation) {
|
|
362
624
|
throw new PluginError('PLUGIN_INVALID_INPUT', `调用插件 ${action} 需要 {"command":"...","input":{...}}`);
|
|
363
625
|
}
|
|
364
|
-
|
|
626
|
+
if (activeInvocation?.taskId) {
|
|
627
|
+
if (!findPluginRegistration(action) && findOfficialPlugin(action))
|
|
628
|
+
await (context.ensurePlugin ?? ensureOfficialPluginRegistered)(action);
|
|
629
|
+
return context.invoke('plugin_invoke_child', { plugin: action, invocation: payload }, nestedOptions);
|
|
630
|
+
}
|
|
631
|
+
return invokeRegisteredPluginWithState(action, payload, { ...context, invocation: undefined, parentInvocation: activeInvocation }, {
|
|
365
632
|
...nestedOptions,
|
|
366
633
|
...(onProgress ? { onProgress } : {}),
|
|
634
|
+
...(signal ? { signal } : {}),
|
|
635
|
+
...(source ? { source } : {}),
|
|
367
636
|
}, childState);
|
|
368
637
|
}
|
|
369
|
-
const result = await
|
|
638
|
+
const result = await (progress
|
|
639
|
+
? context.invoke(action, payload, nestedOptions, progress)
|
|
640
|
+
: context.invoke(action, payload, nestedOptions));
|
|
370
641
|
if (result?.action?.success === false) {
|
|
371
642
|
throw new Error(result.action.error || `${action} failed`);
|
|
372
643
|
}
|
|
@@ -389,3 +660,13 @@ export async function invokeRegisteredToolboxPlugin(name, invocation, context, o
|
|
|
389
660
|
const plugin = await loadRegisteredPluginWithState(name, context, options);
|
|
390
661
|
return plugin.runToolbox(invocation);
|
|
391
662
|
}
|
|
663
|
+
export async function invokeRegisteredRuntimePlugin(name, invocation, context, options = {}) {
|
|
664
|
+
const plugin = await loadRegisteredPluginWithState(name, context, options);
|
|
665
|
+
return plugin.runRuntime(invocation);
|
|
666
|
+
}
|
|
667
|
+
export async function invokeRegisteredRecoveryPlugin(name, invocation, context, options = {}) {
|
|
668
|
+
const plugin = await loadRegisteredPluginWithState(name, context, options);
|
|
669
|
+
return plugin.runRecovery(invocation);
|
|
670
|
+
}
|
|
671
|
+
export { PluginStateClient } from './pluginState.js';
|
|
672
|
+
export { PluginModelClient } from './pluginModels.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** CLI host commands share the SDK error code without exporting arbitrary error metadata. */
|
|
2
|
+
export function writePluginCliError(error) {
|
|
3
|
+
const code = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
4
|
+
? error.code
|
|
5
|
+
: 'PLUGIN_HOST_ERROR';
|
|
6
|
+
process.stderr.write(`${JSON.stringify({ error: { code, message: error instanceof Error ? error.message : String(error) } })}\n`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { pluginRecoverySignature } from './pluginRecoveryDefinition.js';
|
|
2
|
+
import { loadPlugin, findPluginRegistration, invokeRegisteredPlugin, invokeRegisteredToolboxPlugin, invokeRegisteredRuntimePlugin, invokeRegisteredRecoveryPlugin, } from './plugin.js';
|
|
3
|
+
import { pluginFileVersion } from './pluginSource.js';
|
|
4
|
+
import { MAX_PLUGIN_RESULT_BYTES, pluginExecutionError, serializePluginExecutionError, } from './pluginExecutorProtocol.js';
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const pending = new Map();
|
|
7
|
+
let sequence = 0;
|
|
8
|
+
let started = false;
|
|
9
|
+
let completed = false;
|
|
10
|
+
function send(event) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
if (!process.connected || !process.send) {
|
|
13
|
+
reject(new Error('Plugin executor disconnected'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
process.send(event, error => (error ? reject(error) : resolve()));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async function run(message) {
|
|
20
|
+
try {
|
|
21
|
+
const registration = message.registration ?? findPluginRegistration(message.plugin);
|
|
22
|
+
if (registration?.entry !== message.entry ||
|
|
23
|
+
pluginFileVersion(message.entry) !== message.entryVersion)
|
|
24
|
+
throw Object.assign(new Error('Plugin source changed before execution'), {
|
|
25
|
+
code: 'PLUGIN_SOURCE_CHANGED',
|
|
26
|
+
});
|
|
27
|
+
const invokePlugin = message.recovery
|
|
28
|
+
? invokeRegisteredRecoveryPlugin
|
|
29
|
+
: message.access === 'runtime'
|
|
30
|
+
? invokeRegisteredRuntimePlugin
|
|
31
|
+
: message.access === 'toolbox'
|
|
32
|
+
? invokeRegisteredToolboxPlugin
|
|
33
|
+
: invokeRegisteredPlugin;
|
|
34
|
+
const declaration = message.describe
|
|
35
|
+
? await loadPlugin(message.entry, {
|
|
36
|
+
options: {},
|
|
37
|
+
progress: () => { },
|
|
38
|
+
invoke: async () => {
|
|
39
|
+
throw new Error('Host actions are unavailable during runtime declaration');
|
|
40
|
+
},
|
|
41
|
+
}, message.plugin)
|
|
42
|
+
: undefined;
|
|
43
|
+
const migration = message.migration
|
|
44
|
+
? await loadPlugin(message.entry, {
|
|
45
|
+
options: {},
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
progress: () => { },
|
|
48
|
+
invoke: async () => {
|
|
49
|
+
throw Object.assign(new Error('Checkpoint migration cannot invoke host actions or plugins'), { code: 'PLUGIN_CHECKPOINT_MIGRATION_HOST_CALL' });
|
|
50
|
+
},
|
|
51
|
+
}, message.plugin)
|
|
52
|
+
: undefined;
|
|
53
|
+
if (migration) {
|
|
54
|
+
const definitions = migration.taskRecovery?.commands;
|
|
55
|
+
const definition = definitions && Object.hasOwn(definitions, message.invocation.command)
|
|
56
|
+
? definitions[message.invocation.command]
|
|
57
|
+
: undefined;
|
|
58
|
+
if (!definition ||
|
|
59
|
+
!message.expectedRecovery ||
|
|
60
|
+
pluginRecoverySignature(definition) !== message.expectedRecovery)
|
|
61
|
+
throw Object.assign(new Error('Migration declaration differs from the admitted target format'), { code: 'PLUGIN_RECOVERY_DECLARATION_CHANGED' });
|
|
62
|
+
}
|
|
63
|
+
const value = migration
|
|
64
|
+
? await migration.runMigration(message.invocation)
|
|
65
|
+
: declaration
|
|
66
|
+
? {
|
|
67
|
+
pageRuntime: declaration.pageRuntime,
|
|
68
|
+
stateStorage: declaration.stateStorage,
|
|
69
|
+
taskRecovery: declaration.taskRecovery,
|
|
70
|
+
}
|
|
71
|
+
: message.model
|
|
72
|
+
? await (await import('@mearl/agent-runtime/codex')).runCodexModel(message.model, {
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
onDiagnostic: metadata => {
|
|
76
|
+
void send({ type: 'diagnostic', metadata }).catch(() => process.exit(1));
|
|
77
|
+
},
|
|
78
|
+
onDelta: (delta, index, metadata) => {
|
|
79
|
+
void send({
|
|
80
|
+
type: 'progress',
|
|
81
|
+
progress: {
|
|
82
|
+
message: 'model.delta',
|
|
83
|
+
data: { delta, index, ...metadata, partial: true },
|
|
84
|
+
},
|
|
85
|
+
}).catch(() => process.exit(1));
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
: await invokePlugin(message.plugin, message.invocation, {
|
|
89
|
+
invocation: Object.freeze({
|
|
90
|
+
...message.identity,
|
|
91
|
+
host: Object.freeze(message.identity.host),
|
|
92
|
+
}),
|
|
93
|
+
...(message.registration
|
|
94
|
+
? {
|
|
95
|
+
resolveRegistration: (name) => name === message.plugin
|
|
96
|
+
? message.registration
|
|
97
|
+
: findPluginRegistration(name),
|
|
98
|
+
}
|
|
99
|
+
: {}),
|
|
100
|
+
invoke: (action, payload = {}, options = {}, onProgress) => {
|
|
101
|
+
controller.signal.throwIfAborted();
|
|
102
|
+
const id = ++sequence;
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
pending.set(id, { resolve, reject, onProgress });
|
|
105
|
+
void send({
|
|
106
|
+
type: 'invoke',
|
|
107
|
+
id,
|
|
108
|
+
action,
|
|
109
|
+
payload,
|
|
110
|
+
options,
|
|
111
|
+
...(onProgress ? { stream: true } : {}),
|
|
112
|
+
}).catch(error => {
|
|
113
|
+
pending.delete(id);
|
|
114
|
+
reject(error);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
}, {
|
|
119
|
+
...message.options,
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
source: message.identity.source,
|
|
122
|
+
onProgress: progress => {
|
|
123
|
+
void send({ type: 'progress', progress }).catch(() => process.exit(1));
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
// Business results use a separate lossless channel; diagnostics never supply the result.
|
|
127
|
+
const json = JSON.stringify({ value });
|
|
128
|
+
if (Buffer.byteLength(json) > (message.migration ? 64 * 1024 : MAX_PLUGIN_RESULT_BYTES))
|
|
129
|
+
throw Object.assign(new Error('Plugin result exceeds the execution result limit'), {
|
|
130
|
+
code: 'PLUGIN_RESULT_TOO_LARGE',
|
|
131
|
+
});
|
|
132
|
+
await send({ type: 'result', json });
|
|
133
|
+
completed = true;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
await send({ type: 'error', error: serializePluginExecutionError(error) }).catch(() => { });
|
|
137
|
+
completed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function handlePluginWorkerMessage(message) {
|
|
141
|
+
if (message.type === 'start') {
|
|
142
|
+
if (started)
|
|
143
|
+
return;
|
|
144
|
+
started = true;
|
|
145
|
+
void run(message);
|
|
146
|
+
}
|
|
147
|
+
else if (message.type === 'cancel') {
|
|
148
|
+
if (completed)
|
|
149
|
+
return;
|
|
150
|
+
const error = pluginExecutionError(message.error);
|
|
151
|
+
controller.abort(error);
|
|
152
|
+
for (const request of pending.values())
|
|
153
|
+
request.reject(error);
|
|
154
|
+
pending.clear();
|
|
155
|
+
}
|
|
156
|
+
else if (message.type === 'invoke-progress') {
|
|
157
|
+
const request = pending.get(message.id);
|
|
158
|
+
try {
|
|
159
|
+
request?.onProgress?.(message.progress);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
pending.delete(message.id);
|
|
163
|
+
request?.reject(error instanceof Error ? error : new Error(String(error)));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (message.type === 'response') {
|
|
167
|
+
const request = pending.get(message.id);
|
|
168
|
+
pending.delete(message.id);
|
|
169
|
+
if (message.error)
|
|
170
|
+
request?.reject(pluginExecutionError(message.error));
|
|
171
|
+
else
|
|
172
|
+
request?.resolve(message.result);
|
|
173
|
+
}
|
|
174
|
+
}
|