@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.
Files changed (107) hide show
  1. package/README.md +122 -2
  2. package/dist/cli.js +10 -5
  3. package/dist/commands.d.ts +14 -5
  4. package/dist/commands.js +21 -2
  5. package/dist/generated-app-profile-definition.js +20 -10
  6. package/dist/generated-app-profile-runtime-types.d.ts +4 -0
  7. package/dist/generated-browser-action-protocol.d.ts +11 -1
  8. package/dist/generated-browser-action-protocol.js +3 -0
  9. package/dist/generated-plugin-diagnostics.d.ts +110 -0
  10. package/dist/generated-plugin-diagnostics.js +1 -0
  11. package/dist/generated-plugin-execution.d.ts +27 -0
  12. package/dist/generated-plugin-execution.js +1 -0
  13. package/dist/generated-plugin-models.d.ts +64 -0
  14. package/dist/generated-plugin-models.js +1 -0
  15. package/dist/generated-plugin-pages.d.ts +76 -0
  16. package/dist/generated-plugin-pages.js +59 -0
  17. package/dist/generated-plugin-recovery.d.ts +32 -0
  18. package/dist/generated-plugin-recovery.js +1 -0
  19. package/dist/generated-plugin-state.d.ts +50 -0
  20. package/dist/generated-plugin-state.js +1 -0
  21. package/dist/generated-plugin-tasks.d.ts +74 -0
  22. package/dist/generated-plugin-tasks.js +7 -0
  23. package/dist/generated-toolbox-icon.d.ts +10 -0
  24. package/dist/generated-toolbox-icon.js +37 -0
  25. package/dist/index.d.ts +11 -0
  26. package/dist/index.js +6 -0
  27. package/dist/plugin-task-host.d.ts +1 -0
  28. package/dist/plugin-task-host.js +14 -0
  29. package/dist/plugin-worker.d.ts +1 -0
  30. package/dist/plugin-worker.js +17 -0
  31. package/dist/plugin.d.ts +57 -4
  32. package/dist/plugin.js +296 -15
  33. package/dist/pluginCliOutput.d.ts +2 -0
  34. package/dist/pluginCliOutput.js +7 -0
  35. package/dist/pluginCommandWorker.d.ts +2 -0
  36. package/dist/pluginCommandWorker.js +174 -0
  37. package/dist/pluginDiagnostics.d.ts +26 -0
  38. package/dist/pluginDiagnostics.js +309 -0
  39. package/dist/pluginExecution.d.ts +21 -0
  40. package/dist/pluginExecution.js +43 -0
  41. package/dist/pluginExecutor.d.ts +29 -0
  42. package/dist/pluginExecutor.js +223 -0
  43. package/dist/pluginExecutorProtocol.d.ts +55 -0
  44. package/dist/pluginExecutorProtocol.js +47 -0
  45. package/dist/pluginExecutorSupervisor.d.ts +4 -0
  46. package/dist/pluginExecutorSupervisor.js +63 -0
  47. package/dist/pluginHost.d.ts +20 -0
  48. package/dist/pluginHost.js +53 -0
  49. package/dist/pluginHostConnection.d.ts +35 -0
  50. package/dist/pluginHostConnection.js +257 -0
  51. package/dist/pluginHostFiles.d.ts +1 -0
  52. package/dist/pluginHostFiles.js +21 -0
  53. package/dist/pluginHostPaths.d.ts +11 -0
  54. package/dist/pluginHostPaths.js +22 -0
  55. package/dist/pluginHostServer.d.ts +7 -0
  56. package/dist/pluginHostServer.js +252 -0
  57. package/dist/pluginModelCatalog.d.ts +14 -0
  58. package/dist/pluginModelCatalog.js +51 -0
  59. package/dist/pluginModelDefinition.d.ts +6 -0
  60. package/dist/pluginModelDefinition.js +75 -0
  61. package/dist/pluginModelSessionWorker.d.ts +23 -0
  62. package/dist/pluginModelSessionWorker.js +60 -0
  63. package/dist/pluginModelSessions.d.ts +16 -0
  64. package/dist/pluginModelSessions.js +234 -0
  65. package/dist/pluginModels.d.ts +17 -0
  66. package/dist/pluginModels.js +47 -0
  67. package/dist/pluginPageBundle.js +74 -17
  68. package/dist/pluginPageDefinition.d.ts +3 -0
  69. package/dist/pluginPageDefinition.js +62 -0
  70. package/dist/pluginPageManager.d.ts +57 -0
  71. package/dist/pluginPageManager.js +877 -0
  72. package/dist/pluginPages.d.ts +21 -0
  73. package/dist/pluginPages.js +46 -0
  74. package/dist/pluginProcessTree.d.ts +2 -0
  75. package/dist/pluginProcessTree.js +31 -0
  76. package/dist/pluginRecoveryDefinition.d.ts +3 -0
  77. package/dist/pluginRecoveryDefinition.js +56 -0
  78. package/dist/pluginRegistry.d.ts +2 -3
  79. package/dist/pluginRegistry.js +14 -13
  80. package/dist/pluginRevision.d.ts +14 -0
  81. package/dist/pluginRevision.js +227 -0
  82. package/dist/pluginRuntimeCleanup.d.ts +2 -0
  83. package/dist/pluginRuntimeCleanup.js +55 -0
  84. package/dist/pluginState.d.ts +26 -0
  85. package/dist/pluginState.js +28 -0
  86. package/dist/pluginStateDefinition.d.ts +4 -0
  87. package/dist/pluginStateDefinition.js +68 -0
  88. package/dist/pluginStateManager.d.ts +24 -0
  89. package/dist/pluginStateManager.js +132 -0
  90. package/dist/pluginStateStore.d.ts +57 -0
  91. package/dist/pluginStateStore.js +396 -0
  92. package/dist/pluginTaskManager.d.ts +92 -0
  93. package/dist/pluginTaskManager.js +1080 -0
  94. package/dist/pluginTaskProtocol.d.ts +47 -0
  95. package/dist/pluginTaskProtocol.js +1 -0
  96. package/dist/pluginTaskStore.d.ts +53 -0
  97. package/dist/pluginTaskStore.js +319 -0
  98. package/dist/pluginTaskWire.d.ts +4 -0
  99. package/dist/pluginTaskWire.js +42 -0
  100. package/dist/pluginTasks.d.ts +51 -0
  101. package/dist/pluginTasks.js +181 -0
  102. package/dist/pythonPlugin.d.ts +13 -2
  103. package/dist/pythonPlugin.js +235 -30
  104. package/dist/socket.js +5 -2
  105. package/dist/unified.d.ts +1 -0
  106. package/dist/unified.js +81 -0
  107. 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
- export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, } from './pluginRegistry.js';
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, { command, signature, handler, examples: [...examples], toolboxOnly });
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
- const register = command.toolboxOnly ? app.toolboxCommand : app.command;
190
- register(command.signature, input => runPythonPluginCommand(entry, commandName, input, app), command.description);
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 { command, handler } = find(envelope?.command ?? positionals[0] ?? '', toolboxPage);
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 = findPluginRegistration(name);
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
- return invokeRegisteredPluginWithState(action, payload, context, {
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 context.invoke(action, payload, nestedOptions);
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,2 @@
1
+ /** CLI host commands share the SDK error code without exporting arbitrary error metadata. */
2
+ export declare function writePluginCliError(error: unknown): void;
@@ -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,2 @@
1
+ import type { PluginWorkerRequest } from './pluginExecutorProtocol.js';
2
+ export declare function handlePluginWorkerMessage(message: PluginWorkerRequest): void;
@@ -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
+ }