@mearl/client 2.20.6 → 2.22.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 (130) hide show
  1. package/README.md +153 -3
  2. package/dist/actionTimeouts.js +1 -1
  3. package/dist/artifacts.d.ts +1 -1
  4. package/dist/browserRegistry.d.ts +1 -1
  5. package/dist/check.d.ts +1 -1
  6. package/dist/cli.js +14 -8
  7. package/dist/commands.d.ts +23 -14
  8. package/dist/commands.js +24 -4
  9. package/dist/{generated-app-profile-definition.d.ts → generated/app-profile-definition.d.ts} +2 -2
  10. package/dist/{generated-app-profile-runtime-types.d.ts → generated/app-profile-runtime-types.d.ts} +2 -2
  11. package/dist/{generated-browser-action-protocol.d.ts → generated/browser-action-protocol.d.ts} +11 -1
  12. package/dist/{generated-browser-action-protocol.js → generated/browser-action-protocol.js} +3 -0
  13. package/dist/generated/plugin-diagnostics.d.ts +110 -0
  14. package/dist/generated/plugin-diagnostics.js +1 -0
  15. package/dist/generated/plugin-execution.d.ts +27 -0
  16. package/dist/generated/plugin-execution.js +1 -0
  17. package/dist/generated/plugin-models.d.ts +64 -0
  18. package/dist/generated/plugin-models.js +1 -0
  19. package/dist/generated/plugin-pages.d.ts +76 -0
  20. package/dist/generated/plugin-pages.js +59 -0
  21. package/dist/generated/plugin-recovery.d.ts +32 -0
  22. package/dist/generated/plugin-recovery.js +1 -0
  23. package/dist/generated/plugin-state.d.ts +50 -0
  24. package/dist/generated/plugin-state.js +1 -0
  25. package/dist/generated/plugin-tasks.d.ts +74 -0
  26. package/dist/generated/plugin-tasks.js +7 -0
  27. package/dist/generated/toolbox-icon.d.ts +10 -0
  28. package/dist/generated/toolbox-icon.js +37 -0
  29. package/dist/index.d.ts +15 -4
  30. package/dist/index.js +7 -1
  31. package/dist/pageEvalArguments.d.ts +1 -1
  32. package/dist/pageEvalArguments.js +1 -1
  33. package/dist/plugin-task-host.d.ts +1 -0
  34. package/dist/plugin-task-host.js +14 -0
  35. package/dist/plugin-worker.d.ts +1 -0
  36. package/dist/plugin-worker.js +17 -0
  37. package/dist/plugin.d.ts +1 -65
  38. package/dist/plugin.js +1 -391
  39. package/dist/pluginCliOutput.d.ts +2 -0
  40. package/dist/pluginCliOutput.js +7 -0
  41. package/dist/pluginModelSessionWorker.d.ts +23 -0
  42. package/dist/pluginModelSessionWorker.js +60 -0
  43. package/dist/plugins/clients/pluginHostConnection.d.ts +35 -0
  44. package/dist/plugins/clients/pluginHostConnection.js +256 -0
  45. package/dist/plugins/clients/pluginModels.d.ts +17 -0
  46. package/dist/plugins/clients/pluginModels.js +47 -0
  47. package/dist/plugins/clients/pluginPages.d.ts +21 -0
  48. package/dist/plugins/clients/pluginPages.js +46 -0
  49. package/dist/plugins/clients/pluginState.d.ts +26 -0
  50. package/dist/plugins/clients/pluginState.js +28 -0
  51. package/dist/plugins/clients/pluginTasks.d.ts +51 -0
  52. package/dist/plugins/clients/pluginTasks.js +181 -0
  53. package/dist/{officialPluginInstaller.js → plugins/officialPluginInstaller.js} +1 -3
  54. package/dist/{pluginPageBundle.js → plugins/pluginPageBundle.js} +74 -17
  55. package/dist/{pluginRegistry.d.ts → plugins/pluginRegistry.d.ts} +3 -4
  56. package/dist/{pluginRegistry.js → plugins/pluginRegistry.js} +16 -15
  57. package/dist/plugins/pluginTaskProtocol.d.ts +47 -0
  58. package/dist/plugins/pluginTaskProtocol.js +1 -0
  59. package/dist/plugins/pluginTaskWire.d.ts +4 -0
  60. package/dist/plugins/pluginTaskWire.js +42 -0
  61. package/dist/plugins/runtime/pluginCommandWorker.d.ts +2 -0
  62. package/dist/plugins/runtime/pluginCommandWorker.js +174 -0
  63. package/dist/plugins/runtime/pluginDiagnostics.d.ts +26 -0
  64. package/dist/plugins/runtime/pluginDiagnostics.js +309 -0
  65. package/dist/plugins/runtime/pluginExecution.d.ts +21 -0
  66. package/dist/plugins/runtime/pluginExecution.js +43 -0
  67. package/dist/plugins/runtime/pluginExecutor.d.ts +29 -0
  68. package/dist/plugins/runtime/pluginExecutor.js +223 -0
  69. package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +55 -0
  70. package/dist/plugins/runtime/pluginExecutorProtocol.js +47 -0
  71. package/dist/plugins/runtime/pluginExecutorSupervisor.d.ts +4 -0
  72. package/dist/plugins/runtime/pluginExecutorSupervisor.js +63 -0
  73. package/dist/plugins/runtime/pluginHost.d.ts +20 -0
  74. package/dist/plugins/runtime/pluginHost.js +53 -0
  75. package/dist/plugins/runtime/pluginHostFiles.d.ts +1 -0
  76. package/dist/plugins/runtime/pluginHostFiles.js +21 -0
  77. package/dist/plugins/runtime/pluginHostPaths.d.ts +11 -0
  78. package/dist/plugins/runtime/pluginHostPaths.js +22 -0
  79. package/dist/plugins/runtime/pluginHostServer.d.ts +7 -0
  80. package/dist/plugins/runtime/pluginHostServer.js +252 -0
  81. package/dist/plugins/runtime/pluginModelCatalog.d.ts +14 -0
  82. package/dist/plugins/runtime/pluginModelCatalog.js +51 -0
  83. package/dist/plugins/runtime/pluginModelSessions.d.ts +16 -0
  84. package/dist/plugins/runtime/pluginModelSessions.js +234 -0
  85. package/dist/plugins/runtime/pluginPageManager.d.ts +57 -0
  86. package/dist/plugins/runtime/pluginPageManager.js +877 -0
  87. package/dist/plugins/runtime/pluginProcessTree.d.ts +2 -0
  88. package/dist/plugins/runtime/pluginProcessTree.js +31 -0
  89. package/dist/plugins/runtime/pluginRevision.d.ts +14 -0
  90. package/dist/plugins/runtime/pluginRevision.js +227 -0
  91. package/dist/plugins/runtime/pluginRuntimeCleanup.d.ts +2 -0
  92. package/dist/plugins/runtime/pluginRuntimeCleanup.js +55 -0
  93. package/dist/plugins/runtime/pluginStateManager.d.ts +24 -0
  94. package/dist/plugins/runtime/pluginStateManager.js +132 -0
  95. package/dist/plugins/runtime/pluginStateStore.d.ts +57 -0
  96. package/dist/plugins/runtime/pluginStateStore.js +396 -0
  97. package/dist/plugins/runtime/pluginTaskManager.d.ts +92 -0
  98. package/dist/plugins/runtime/pluginTaskManager.js +1080 -0
  99. package/dist/plugins/runtime/pluginTaskStore.d.ts +53 -0
  100. package/dist/plugins/runtime/pluginTaskStore.js +319 -0
  101. package/dist/plugins/runtime/pythonPlugin.d.ts +31 -0
  102. package/dist/{pythonPlugin.js → plugins/runtime/pythonPlugin.js} +236 -31
  103. package/dist/plugins/sdk/plugin.d.ts +118 -0
  104. package/dist/plugins/sdk/plugin.js +672 -0
  105. package/dist/{pluginAppProfiles.d.ts → plugins/sdk/pluginAppProfiles.d.ts} +1 -1
  106. package/dist/{pluginAppProfiles.js → plugins/sdk/pluginAppProfiles.js} +2 -2
  107. package/dist/plugins/sdk/pluginModelDefinition.d.ts +6 -0
  108. package/dist/plugins/sdk/pluginModelDefinition.js +75 -0
  109. package/dist/plugins/sdk/pluginPageDefinition.d.ts +3 -0
  110. package/dist/plugins/sdk/pluginPageDefinition.js +62 -0
  111. package/dist/plugins/sdk/pluginRecoveryDefinition.d.ts +3 -0
  112. package/dist/plugins/sdk/pluginRecoveryDefinition.js +56 -0
  113. package/dist/plugins/sdk/pluginStateDefinition.d.ts +4 -0
  114. package/dist/plugins/sdk/pluginStateDefinition.js +68 -0
  115. package/dist/runtimePaths.d.ts +4 -0
  116. package/dist/runtimePaths.js +8 -0
  117. package/dist/socket.d.ts +2 -2
  118. package/dist/socket.js +10 -6
  119. package/dist/unified.d.ts +3 -2
  120. package/dist/unified.js +83 -2
  121. package/package.json +7 -3
  122. package/dist/pythonPlugin.d.ts +0 -20
  123. /package/dist/{generated-app-profile-definition.js → generated/app-profile-definition.js} +0 -0
  124. /package/dist/{generated-app-profile-runtime-types.js → generated/app-profile-runtime-types.js} +0 -0
  125. /package/dist/{officialPluginCatalog.d.ts → plugins/officialPluginCatalog.d.ts} +0 -0
  126. /package/dist/{officialPluginCatalog.js → plugins/officialPluginCatalog.js} +0 -0
  127. /package/dist/{officialPluginInstaller.d.ts → plugins/officialPluginInstaller.d.ts} +0 -0
  128. /package/dist/{pluginPageBundle.d.ts → plugins/pluginPageBundle.d.ts} +0 -0
  129. /package/dist/{pluginSource.d.ts → plugins/pluginSource.d.ts} +0 -0
  130. /package/dist/{pluginSource.js → plugins/pluginSource.js} +0 -0
@@ -1,6 +1,8 @@
1
+ import { pluginRecoverySignature } from '../sdk/pluginRecoveryDefinition.js';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { createInterface } from 'node:readline';
3
- import { PluginError } from './pluginRegistry.js';
4
+ import { PluginError } from '../pluginRegistry.js';
5
+ import { pluginExecutionError, serializePluginExecutionError } from './pluginExecutorProtocol.js';
4
6
  const STDERR_LIMIT = 64 * 1024;
5
7
  // Bootstrap 随 Node client 分发,Python 插件无需额外安装宿主 SDK。
6
8
  const PYTHON_BOOTSTRAP = String.raw `
@@ -9,6 +11,7 @@ import importlib.util
9
11
  import inspect
10
12
  import json
11
13
  import os
14
+ import signal
12
15
  import sys
13
16
  from types import MappingProxyType
14
17
 
@@ -20,6 +23,9 @@ def _send(message):
20
23
  encoded = json.dumps(message, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
21
24
  _protocol_output.write(encoded + "\n")
22
25
  _protocol_output.flush()
26
+ if message.get("type") in ("result", "error"):
27
+ if _receive().get("type") != "terminal_ack":
28
+ raise RuntimeError("Mearl 宿主未确认执行终态")
23
29
 
24
30
 
25
31
  def _receive():
@@ -38,10 +44,54 @@ def _resolve(value):
38
44
  return value
39
45
 
40
46
 
47
+ class _Signal:
48
+ def __init__(self):
49
+ self.aborted = False
50
+
51
+ def throw_if_aborted(self):
52
+ if self.aborted:
53
+ raise RuntimeError("插件命令已取消")
54
+
55
+
56
+ class _CommandContext:
57
+ def __init__(self, app, identity):
58
+ self.identity = MappingProxyType({**identity, "host": MappingProxyType(identity["host"])})
59
+ self.signal = _Signal()
60
+ self.invoke = app.invoke
61
+ self.progress = app.progress
62
+ self.state = lambda collection, operation, payload=None: app.state(collection, operation, {**({"instanceId": identity["page"]["instanceId"]} if identity.get("page") else {}), **(payload or {})})
63
+ self.model = app.model
64
+ self.checkpoint = app.checkpoint
65
+ self.recovery = identity.get("recovery")
66
+ self.page = _BoundPage(app, identity["page"]["instanceId"]) if identity.get("page") else None
67
+ app.identity = identity
68
+
69
+ def cancel(self, *_):
70
+ self.signal.aborted = True
71
+ self.signal.throw_if_aborted()
72
+
73
+
74
+ class _BoundPage:
75
+ def __init__(self, app, instance_id):
76
+ self._app = app
77
+ self._instance_id = instance_id
78
+
79
+ def send(self, message, options=None):
80
+ return self._app.page("send", {"instanceId": self._instance_id, "message": message, **({"timeoutSec": options["timeoutSec"]} if options and "timeoutSec" in options else {})})
81
+
82
+ def get(self):
83
+ return self._app.page("get", {"instanceId": self._instance_id})
84
+
85
+
41
86
  class _InvocationError(RuntimeError):
42
- def __init__(self, message, code=None):
87
+ def __init__(self, message, code=None, details=None, metadata=None):
43
88
  super().__init__(message)
44
89
  self.code = code
90
+ self.details = details
91
+ self.metadata = metadata
92
+ for key, value in (metadata or {}).items():
93
+ if not key.startswith("_") and key not in ("code", "details", "metadata"):
94
+ setattr(self, key, value)
45
95
 
46
96
 
47
97
  class _App:
@@ -51,6 +101,8 @@ class _App:
51
101
  self.registering = True
52
102
  self.executing = False
53
103
  self.request_id = 0
104
+ self.responses = {}
105
+ self.observers = {}
54
106
 
55
107
  def command(self, signature, handler=None, description=""):
56
108
  return self._register_command(signature, handler, description, False)
@@ -58,7 +110,16 @@ class _App:
58
110
  def toolboxCommand(self, signature, handler=None, description=""):
59
111
  return self._register_command(signature, handler, description, True)
60
112
 
61
- def _register_command(self, signature, handler, description, toolbox_only):
113
+ def runtimeCommand(self, signature, handler=None, description=""):
114
+ return self._register_command(signature, handler, description, False, runtime_only=True)
115
+
116
+ def recoveryCommand(self, command, handler=None):
117
+ return self._register_command(command, handler, "", False, True)
118
+
119
+ def checkpointMigration(self, command, handler=None):
120
+ return self._register_command(command, handler, "", False, False, True)
121
+
122
+ def _register_command(self, signature, handler, description, toolbox_only, recovery_only=False, migration_only=False, runtime_only=False):
62
123
  if not self.registering:
63
124
  raise RuntimeError("命令只能在入口注册阶段声明")
64
125
  if not isinstance(signature, str):
@@ -74,6 +135,9 @@ class _App:
74
135
  "description": description,
75
136
  "handler": candidate,
76
137
  "toolboxOnly": toolbox_only,
138
+ "runtimeOnly": runtime_only,
139
+ "recoveryOnly": recovery_only,
140
+ "migrationOnly": migration_only,
77
141
  })
78
142
  return candidate
79
143
 
@@ -82,7 +146,7 @@ class _App:
82
146
  add(handler)
83
147
  return None
84
148
 
85
- def invoke(self, action, payload=None, options=None):
149
+ def invoke(self, action, payload=None, options=None, on_progress=None):
86
150
  if not self.executing:
87
151
  raise RuntimeError("app.invoke 只能在命令执行期间调用")
88
152
  if not isinstance(action, str) or not action:
@@ -95,21 +159,53 @@ class _App:
95
159
  raise TypeError("app.invoke payload 和 options 必须为字典")
96
160
  self.request_id += 1
97
161
  request_id = self.request_id
162
+ if on_progress is not None:
163
+ if not callable(on_progress):
164
+ raise TypeError("on_progress 必须为可调用对象")
165
+ self.observers[request_id] = on_progress
98
166
  _send({
99
167
  "type": "invoke",
100
168
  "id": request_id,
101
169
  "action": action,
102
170
  "payload": payload,
103
171
  "options": options,
172
+ "stream": on_progress is not None,
104
173
  })
105
- response = _receive()
106
- if response.get("type") != "response" or response.get("id") != request_id:
107
- raise RuntimeError("Mearl 宿主返回了无效的 invoke 响应")
174
+ try:
175
+ while request_id not in self.responses:
176
+ message = _receive()
177
+ if message.get("type") == "invoke-progress":
178
+ observer = self.observers.get(message.get("id"))
179
+ if observer:
180
+ _resolve(observer(message.get("progress")))
181
+ elif message.get("type") == "response" and isinstance(message.get("id"), int):
182
+ self.responses[message["id"]] = message
183
+ else:
184
+ raise RuntimeError("Mearl 宿主返回了无效的 invoke 响应")
185
+ response = self.responses.pop(request_id)
186
+ finally:
187
+ self.observers.pop(request_id, None)
108
188
  error = response.get("error")
109
189
  if isinstance(error, dict):
110
- raise _InvocationError(str(error.get("message", "调用失败")), error.get("code"))
190
+ raise _InvocationError(str(error.get("message", "调用失败")), error.get("code"), error.get("details"), error.get("metadata"))
111
191
  return response.get("result")
112
192
 
193
+ def state(self, collection, operation, payload=None):
194
+ return self.invoke("plugin_state", {**(payload or {}), "plugin": self.identity["plugin"], "collection": collection, "operation": operation})
195
+
196
+ def checkpoint(self, data, expectedSequence):
197
+ return self.invoke("plugin_task_checkpoint", {"plugin": self.identity["plugin"], "data":data,"expectedSequence":expectedSequence})
198
+
199
+ def model(self, request, on_progress=None):
200
+ return self.invoke("plugin_model", {"plugin": self.identity["plugin"], "operation": "run", "request": request}, on_progress=on_progress)
201
+
202
+ def page(self, operation, payload=None):
203
+ if payload is None:
204
+ payload = {}
205
+ if not isinstance(operation, str) or not operation or not isinstance(payload, dict):
206
+ raise TypeError("app.page 需要非空 operation 和字典 payload")
207
+ return self.invoke("plugin_page", {**payload, "plugin": self.identity["plugin"], "operation": operation})
208
+
113
209
  def progress(self, message):
114
210
  if not self.executing:
115
211
  raise RuntimeError("app.progress 只能在命令执行期间调用")
@@ -172,7 +268,7 @@ try:
172
268
  descriptor = {
173
269
  "type": "ready",
174
270
  "commands": [
175
- {"signature": item["signature"], "description": item["description"], "toolboxOnly": item["toolboxOnly"]}
271
+ {"signature": item["signature"], "description": item["description"], "toolboxOnly": item["toolboxOnly"], "runtimeOnly": item["runtimeOnly"], "recoveryOnly": item["recoveryOnly"], "migrationOnly": item["migrationOnly"]}
176
272
  for item in app.commands
177
273
  ],
178
274
  }
@@ -182,28 +278,42 @@ try:
182
278
  descriptor["description"] = plugin_description
183
279
  if plugin_toolbox is not None:
184
280
  descriptor["toolbox"] = plugin_toolbox
281
+ if getattr(module, "pageRuntime", None) is not None:
282
+ descriptor["pageRuntime"] = module.pageRuntime
283
+ if getattr(module, "stateStorage", None) is not None:
284
+ descriptor["stateStorage"] = module.stateStorage
285
+ if getattr(module, "taskRecovery", None) is not None:
286
+ descriptor["taskRecovery"] = module.taskRecovery
185
287
  if plugin_app_profiles is not None:
186
288
  descriptor["appProfiles"] = plugin_app_profiles
187
289
  _send(descriptor)
188
290
 
291
+ acknowledgement = _receive()
292
+ if acknowledgement.get("type") != "ready_ack":
293
+ raise RuntimeError("Mearl 宿主未确认插件注册信息")
189
294
  if mode == "run":
190
- acknowledgement = _receive()
191
- if acknowledgement.get("type") != "ready_ack":
192
- raise RuntimeError("Mearl 宿主未确认插件注册信息")
193
295
  command_name = initial.get("command")
194
296
  command_input = initial.get("input")
195
297
  if not isinstance(command_name, str) or not isinstance(command_input, dict):
196
298
  raise RuntimeError("Mearl 宿主发送了无效的命令")
197
299
  selected = next(
198
- (item for item in app.commands if item["signature"].strip().split()[0] == command_name),
300
+ (item for item in app.commands if item["signature"].strip().split()[0] == command_name and item["runtimeOnly"] == (initial.get("runtime") is True) and item["recoveryOnly"] == (initial.get("recovery") is True) and item["migrationOnly"] == (initial.get("recovery") == "migration")),
199
301
  None,
200
302
  )
201
303
  if selected is None:
202
304
  raise RuntimeError("未知子命令: " + command_name)
203
305
  phase = "run"
306
+ command_context = _CommandContext(app, initial["identity"])
307
+ signal.signal(signal.SIGTERM, command_context.cancel)
204
308
  app.executing = True
205
309
  try:
206
- result = _resolve(selected["handler"](command_input))
310
+ handler = selected["handler"]
311
+ try:
312
+ inspect.signature(handler).bind(command_input, command_context)
313
+ takes_context = True
314
+ except TypeError:
315
+ takes_context = False
316
+ result = _resolve(handler(command_input, command_context) if takes_context else handler(command_input))
207
317
  finally:
208
318
  app.executing = False
209
319
  _send({"type": "result", "result": result})
@@ -212,6 +322,10 @@ except BaseException as error:
212
322
  code = getattr(error, "code", None)
213
323
  if isinstance(code, str):
214
324
  response["code"] = code
325
+ for field in ("details", "metadata"):
326
+ value = getattr(error, field, None)
327
+ if isinstance(value, dict):
328
+ response[field] = value
215
329
  try:
216
330
  _send(response)
217
331
  except BaseException as serialization_error:
@@ -235,13 +349,17 @@ function parseDescriptor(message) {
235
349
  if (!isObject(value) ||
236
350
  typeof value.signature !== 'string' ||
237
351
  typeof value.description !== 'string' ||
238
- typeof value.toolboxOnly !== 'boolean') {
352
+ typeof value.toolboxOnly !== 'boolean' ||
353
+ typeof value.runtimeOnly !== 'boolean') {
239
354
  throw new Error('Python 插件返回了无效的命令描述');
240
355
  }
241
356
  return {
242
357
  signature: value.signature,
243
358
  description: value.description,
244
359
  toolboxOnly: value.toolboxOnly,
360
+ runtimeOnly: value.runtimeOnly,
361
+ recoveryOnly: value.recoveryOnly === true,
362
+ migrationOnly: value.migrationOnly === true,
245
363
  };
246
364
  });
247
365
  if (message.name !== undefined && typeof message.name !== 'string') {
@@ -255,29 +373,40 @@ function parseDescriptor(message) {
255
373
  ...(typeof message.name === 'string' ? { name: message.name } : {}),
256
374
  ...(typeof message.description === 'string' ? { description: message.description } : {}),
257
375
  ...(message.toolbox !== undefined ? { toolbox: message.toolbox } : {}),
376
+ ...(message.pageRuntime !== undefined ? { pageRuntime: message.pageRuntime } : {}),
377
+ ...(message.stateStorage !== undefined ? { stateStorage: message.stateStorage } : {}),
378
+ ...(message.taskRecovery !== undefined ? { taskRecovery: message.taskRecovery } : {}),
258
379
  ...(message.appProfiles !== undefined ? { appProfiles: message.appProfiles } : {}),
259
380
  };
260
381
  }
261
382
  function bridgedError(message) {
262
383
  const detail = typeof message.message === 'string' ? message.message : 'Python 插件执行失败';
384
+ const restored = pluginExecutionError({
385
+ message: detail,
386
+ ...(typeof message.code === 'string' ? { code: message.code } : {}),
387
+ ...(isObject(message.details) ? { details: message.details } : {}),
388
+ ...(isObject(message.metadata) ? { metadata: message.metadata } : {}),
389
+ });
263
390
  if (typeof message.code === 'string' && /^PLUGIN_[A-Z0-9_]+$/.test(message.code)) {
264
- return new PluginError(message.code, detail);
391
+ return Object.assign(new PluginError(message.code, detail), restored);
265
392
  }
266
- return new Error(detail);
393
+ return restored;
267
394
  }
268
395
  function errorResponse(error) {
396
+ const serialized = serializePluginExecutionError(error);
269
397
  if (error instanceof PluginError) {
270
398
  const prefix = `${error.code}: `;
271
399
  return {
400
+ ...serialized,
272
401
  code: error.code,
273
402
  message: error.message.startsWith(prefix)
274
403
  ? error.message.slice(prefix.length)
275
404
  : error.message,
276
405
  };
277
406
  }
278
- return { message: error instanceof Error ? error.message : String(error) };
407
+ return serialized;
279
408
  }
280
- async function runPythonWorker(entry, initial, app) {
409
+ async function runPythonWorker(entry, initial, app, signal, expectedDescriptor) {
281
410
  return new Promise((resolve, reject) => {
282
411
  const executable = pythonExecutable();
283
412
  const child = spawn(executable, ['-u', '-c', PYTHON_BOOTSTRAP, entry], {
@@ -293,7 +422,7 @@ async function runPythonWorker(entry, initial, app) {
293
422
  if (protocolError)
294
423
  return;
295
424
  protocolError = error instanceof Error ? error : new Error(String(error));
296
- child.kill();
425
+ cancel();
297
426
  };
298
427
  const write = (message) => {
299
428
  try {
@@ -308,6 +437,27 @@ async function runPythonWorker(entry, initial, app) {
308
437
  stderr = `${stderr}${chunk}`.slice(-STDERR_LIMIT);
309
438
  });
310
439
  child.stdin.on('error', () => undefined);
440
+ let killTimer;
441
+ let terminalStopRequested = false;
442
+ const armKill = () => {
443
+ if (killTimer)
444
+ return;
445
+ killTimer = setTimeout(() => {
446
+ terminalStopRequested =
447
+ !protocolError &&
448
+ !signal?.aborted &&
449
+ Boolean(outcome || (initial.mode === 'describe' && descriptor));
450
+ child.kill('SIGKILL');
451
+ }, 1000);
452
+ killTimer.unref();
453
+ };
454
+ const cancel = () => {
455
+ child.kill('SIGTERM');
456
+ armKill();
457
+ };
458
+ signal?.addEventListener('abort', cancel, { once: true });
459
+ if (signal?.aborted)
460
+ cancel();
311
461
  child.once('error', error => {
312
462
  if (error.code === 'ENOENT') {
313
463
  reject(new Error(`未找到 ${executable},Python 插件需要 Python 3.9 或更高版本` +
@@ -318,6 +468,8 @@ async function runPythonWorker(entry, initial, app) {
318
468
  }
319
469
  });
320
470
  lines.on('line', line => {
471
+ if (protocolError)
472
+ return;
321
473
  let message;
322
474
  try {
323
475
  const value = JSON.parse(line);
@@ -335,8 +487,14 @@ async function runPythonWorker(entry, initial, app) {
335
487
  if (descriptor)
336
488
  throw new Error('Python 插件重复发送 ready');
337
489
  descriptor = parseDescriptor(message);
338
- if (initial.mode === 'run')
339
- write({ type: 'ready_ack' });
490
+ if (initial.mode === 'run') {
491
+ if (expectedDescriptor &&
492
+ pluginRecoverySignature(descriptor) !== pluginRecoverySignature(expectedDescriptor))
493
+ throw new PluginError('PLUGIN_DECLARATION_CHANGED', 'Python execution declaration differs from its loaded metadata');
494
+ }
495
+ write({ type: 'ready_ack' });
496
+ if (initial.mode === 'describe')
497
+ armKill();
340
498
  }
341
499
  catch (error) {
342
500
  failProtocol(error);
@@ -365,8 +523,24 @@ async function runPythonWorker(entry, initial, app) {
365
523
  failProtocol(new Error('Python 插件发送了无效的 invoke 消息'));
366
524
  return;
367
525
  }
368
- void app
369
- .invoke(message.action, message.payload, message.options)
526
+ const { action, payload, options } = message;
527
+ void Promise.resolve()
528
+ .then(() => action === 'plugin_task_checkpoint' && app.checkpoint
529
+ ? app.checkpoint(payload.data, payload.expectedSequence)
530
+ : action === 'plugin_model' && payload.operation === 'run' && app.model
531
+ ? app.model(payload.request, message.stream
532
+ ? {
533
+ onProgress: progress => {
534
+ if (!signal?.aborted && !outcome)
535
+ write({ type: 'invoke-progress', id: message.id, progress });
536
+ },
537
+ }
538
+ : undefined)
539
+ : action === 'plugin_state' && app.state
540
+ ? app.state(payload.collection, payload.operation, payload)
541
+ : action === 'plugin_page' && app.page
542
+ ? app.page(payload.operation, payload)
543
+ : app.invoke(action, payload, options))
370
544
  .then(result => {
371
545
  write({
372
546
  type: 'response',
@@ -380,17 +554,37 @@ async function runPythonWorker(entry, initial, app) {
380
554
  return;
381
555
  }
382
556
  if (message.type === 'result') {
383
- outcome = { hasResult: true, result: message.result };
557
+ if (!outcome)
558
+ outcome = { hasResult: true, result: message.result };
559
+ write({ type: 'terminal_ack' });
560
+ armKill();
384
561
  return;
385
562
  }
386
563
  if (message.type === 'error') {
387
- outcome = { error: bridgedError(message) };
564
+ if (!outcome)
565
+ outcome = { error: bridgedError(message) };
566
+ write({ type: 'terminal_ack' });
567
+ armKill();
388
568
  return;
389
569
  }
390
570
  failProtocol(new Error(`Python 插件发送了未知消息类型: ${message.type}`));
391
571
  });
392
- child.once('close', code => {
572
+ let finished = false;
573
+ const finish = (code) => {
574
+ if (finished)
575
+ return;
576
+ finished = true;
577
+ signal?.removeEventListener('abort', cancel);
578
+ if (killTimer)
579
+ clearTimeout(killTimer);
393
580
  lines.close();
581
+ child.stdin.destroy();
582
+ child.stdout.destroy();
583
+ child.stderr.destroy();
584
+ if (signal?.aborted) {
585
+ reject(signal.reason ?? new Error('Plugin command cancelled'));
586
+ return;
587
+ }
394
588
  if (protocolError) {
395
589
  reject(protocolError);
396
590
  return;
@@ -399,7 +593,7 @@ async function runPythonWorker(entry, initial, app) {
399
593
  reject(outcome.error);
400
594
  return;
401
595
  }
402
- if (code !== 0) {
596
+ if (code !== 0 && !terminalStopRequested) {
403
597
  reject(new Error(stderr.trim() || `Python 插件进程异常退出${code === null ? '' : ` (exit ${code})`}`));
404
598
  return;
405
599
  }
@@ -412,7 +606,10 @@ async function runPythonWorker(entry, initial, app) {
412
606
  return;
413
607
  }
414
608
  resolve({ descriptor, ...(outcome?.hasResult ? { result: outcome.result } : {}) });
415
- });
609
+ };
610
+ // Acknowledged frames precede process exit; inherited pipes cannot delay completion.
611
+ child.once('exit', finish);
612
+ child.once('close', finish);
416
613
  write(initial);
417
614
  });
418
615
  }
@@ -420,7 +617,15 @@ export async function describePythonPlugin(entry, options) {
420
617
  const { descriptor } = await runPythonWorker(entry, { mode: 'describe', options });
421
618
  return descriptor;
422
619
  }
423
- export async function runPythonPluginCommand(entry, command, input, app) {
424
- const { result } = await runPythonWorker(entry, { mode: 'run', command, input, options: app.options }, app);
620
+ export async function runPythonPluginCommand(entry, command, input, app, context, recovery = false, expectedDescriptor, runtimeOnly = false) {
621
+ const { result } = await runPythonWorker(entry, {
622
+ mode: 'run',
623
+ command,
624
+ input,
625
+ options: app.options,
626
+ identity: context.identity,
627
+ recovery,
628
+ runtime: runtimeOnly,
629
+ }, app, context.signal, expectedDescriptor);
425
630
  return result;
426
631
  }
@@ -0,0 +1,118 @@
1
+ import type { PluginTaskRecoveryDeclaration, PluginTaskCheckpoint, PluginCheckpointMigrationInput } from '../../generated/plugin-recovery.js';
2
+ import type { PluginStateDeclaration } from '../../generated/plugin-state.js';
3
+ import type { PluginModelRequest, PluginModelResult } from '../../generated/plugin-models.js';
4
+ import type { PluginPageRuntime } from '../../generated/plugin-pages.js';
5
+ import type { AppProfileContribution } from '../../generated/app-profile-definition.js';
6
+ export type { AppProfileContribution, AppProfileDefinition, } from '../../generated/app-profile-definition.js';
7
+ export { invokePageEvalWithArgs } from '../../pageEvalArguments.js';
8
+ import { type OfficialPluginInstaller } from '../officialPluginInstaller.js';
9
+ import { type PluginToolboxContribution, type PluginRegistration } from '../pluginRegistry.js';
10
+ import { type PluginCommandContext, type PluginInvocationIdentity, type PluginInvocationSource } from '../runtime/pluginExecution.js';
11
+ import type { UnifiedInvokeOptions } from '../../unified.js';
12
+ export type { PluginCommandContext, PluginExecutionHost, PluginInvocationIdentity, PluginInvocationSource, } from '../runtime/pluginExecution.js';
13
+ export { executeRegisteredPlugin, type PluginExecutorOptions, type PluginExecutorDependencies, } from '../runtime/pluginExecutor.js';
14
+ export { PluginTaskClient } from '../clients/pluginTasks.js';
15
+ export { cleanupPluginRuntime } from '../runtime/pluginRuntimeCleanup.js';
16
+ export { PluginPageClient } from '../clients/pluginPages.js';
17
+ export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, pluginDirectory, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, type PluginRegistration, type PluginRegistrationOptions, type PluginToolboxAgentConfig, type PluginPageBundleRegistration, type PluginToolboxContribution, type PluginToolboxLocale, type PluginToolboxSandboxPage, } from '../pluginRegistry.js';
18
+ export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, type OfficialPluginDefinition, type OfficialPluginName, } from '../officialPluginCatalog.js';
19
+ export type PluginOptions = UnifiedInvokeOptions;
20
+ export interface StructuredPluginProgress {
21
+ message: string;
22
+ data?: Record<string, unknown>;
23
+ }
24
+ export type PluginProgress = string | StructuredPluginProgress;
25
+ export interface PluginInvocation {
26
+ command: string;
27
+ input: Record<string, any>;
28
+ }
29
+ export interface PluginInvokeOptions extends UnifiedInvokeOptions {
30
+ onProgress?: (progress: PluginProgress) => void;
31
+ signal?: AbortSignal;
32
+ /** Audit label supplied by the entry point, never an authorization decision. */
33
+ source?: PluginInvocationSource;
34
+ }
35
+ export interface PluginExecutionContext {
36
+ runModel?(plugin: string, request: PluginModelRequest, options: Pick<PluginInvokeOptions, 'signal' | 'onProgress'>): Promise<PluginModelResult>;
37
+ ensurePlugin?: OfficialPluginInstaller;
38
+ source?: PluginInvocationSource;
39
+ parentInvocation?: PluginInvocationIdentity;
40
+ invocation?: PluginInvocationIdentity;
41
+ resolveRegistration?: (name: string) => PluginRegistration | undefined;
42
+ invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions, onProgress?: PluginInvokeOptions['onProgress']): Promise<any>;
43
+ }
44
+ export interface PluginApp {
45
+ readonly options: Readonly<PluginOptions>;
46
+ /** Examples omit the `mearl <plugin>` prefix, which help adds when displayed. */
47
+ command(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
48
+ /** Registers a page-only command, also declared in toolbox.page.commands. */
49
+ state(collection: string, operation: string, input?: Record<string, unknown>): Promise<any>;
50
+ model(request: PluginModelRequest, options?: Pick<PluginInvokeOptions, 'onProgress'>): Promise<PluginModelResult>;
51
+ runtimeCommand(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
52
+ recoveryCommand(command: string, handler: PluginHandler): void;
53
+ checkpointMigration(command: string, handler: (input: PluginCheckpointMigrationInput, context: PluginCommandContext) => unknown | Promise<unknown>): void;
54
+ checkpoint(data: unknown, expectedSequence: number): Promise<PluginTaskCheckpoint>;
55
+ page(operation: string, input?: Record<string, unknown>): Promise<any>;
56
+ toolboxCommand(signature: string, handler: PluginHandler, description?: string, examples?: string[]): void;
57
+ invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
58
+ progress(progress: PluginProgress): void;
59
+ }
60
+ export type PluginHandler = (input: Record<string, any>, context: PluginCommandContext) => unknown | Promise<unknown>;
61
+ /** 工具列表仅检查入口;页面资源只在打开对应工具时检查。 */
62
+ export declare function refreshRegisteredPlugin(name: string, includePage?: boolean): Promise<PluginRegistration | undefined>;
63
+ /** 注册和执行复用同一宿主能力边界;仅 handler 执行期间开放浏览器调用。 */
64
+ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'options' | 'progress'> & {
65
+ invoke: PluginExecutionContext['invoke'];
66
+ source?: PluginInvocationSource;
67
+ parentInvocation?: PluginInvocationIdentity;
68
+ invocation?: PluginInvocationIdentity;
69
+ signal?: AbortSignal;
70
+ runModel?: PluginExecutionContext['runModel'];
71
+ onInvocation?: (identity: PluginInvocationIdentity | undefined) => void;
72
+ }, pluginName?: string): Promise<{
73
+ name: string | undefined;
74
+ description: string | undefined;
75
+ toolbox: PluginToolboxContribution | undefined;
76
+ pageRuntime: PluginPageRuntime | undefined;
77
+ stateStorage: PluginStateDeclaration | undefined;
78
+ taskRecovery: PluginTaskRecoveryDeclaration | undefined;
79
+ appProfiles: AppProfileContribution | undefined;
80
+ profileSources: Record<string, string> | undefined;
81
+ version: string | undefined;
82
+ commandHelp(namespace: string): string;
83
+ help(namespace: string, name?: string): string;
84
+ run(positionals: string[], payload?: unknown, namedInput?: Record<string, unknown>): Promise<unknown>;
85
+ runRuntime(invocation: PluginInvocation): Promise<unknown>;
86
+ runRecovery(invocation: PluginInvocation): Promise<unknown>;
87
+ runMigration(invocation: PluginInvocation): Promise<unknown>;
88
+ runToolbox(invocation: PluginInvocation): Promise<unknown>;
89
+ }>;
90
+ export declare function loadRegisteredPlugin(name: string, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<{
91
+ name: string | undefined;
92
+ description: string | undefined;
93
+ toolbox: PluginToolboxContribution | undefined;
94
+ pageRuntime: PluginPageRuntime | undefined;
95
+ stateStorage: PluginStateDeclaration | undefined;
96
+ taskRecovery: PluginTaskRecoveryDeclaration | undefined;
97
+ appProfiles: AppProfileContribution | undefined;
98
+ profileSources: Record<string, string> | undefined;
99
+ version: string | undefined;
100
+ commandHelp(namespace: string): string;
101
+ help(namespace: string, name?: string): string;
102
+ run(positionals: string[], payload?: unknown, namedInput?: Record<string, unknown>): Promise<unknown>;
103
+ runRuntime(invocation: PluginInvocation): Promise<unknown>;
104
+ runRecovery(invocation: PluginInvocation): Promise<unknown>;
105
+ runMigration(invocation: PluginInvocation): Promise<unknown>;
106
+ runToolbox(invocation: PluginInvocation): Promise<unknown>;
107
+ }>;
108
+ export declare function invokeRegisteredPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
109
+ /** Entry point for the native toolbox service; nested plugin calls retain public-command access. */
110
+ export declare function invokeRegisteredToolboxPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
111
+ export declare function invokeRegisteredRuntimePlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
112
+ export declare function invokeRegisteredRecoveryPlugin(name: string, invocation: PluginInvocation, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<unknown>;
113
+ export { PluginStateClient } from '../clients/pluginState.js';
114
+ export { PluginModelClient } from '../clients/pluginModels.js';
115
+ export type * from '../../generated/plugin-state.js';
116
+ export type * from '../../generated/plugin-models.js';
117
+ export type * from '../../generated/plugin-recovery.js';
118
+ export type * from '../../generated/plugin-diagnostics.js';