@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.
- package/README.md +153 -3
- package/dist/actionTimeouts.js +1 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/browserRegistry.d.ts +1 -1
- package/dist/check.d.ts +1 -1
- package/dist/cli.js +14 -8
- package/dist/commands.d.ts +23 -14
- package/dist/commands.js +24 -4
- package/dist/{generated-app-profile-definition.d.ts → generated/app-profile-definition.d.ts} +2 -2
- package/dist/{generated-app-profile-runtime-types.d.ts → generated/app-profile-runtime-types.d.ts} +2 -2
- package/dist/{generated-browser-action-protocol.d.ts → generated/browser-action-protocol.d.ts} +11 -1
- package/dist/{generated-browser-action-protocol.js → 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 +15 -4
- package/dist/index.js +7 -1
- package/dist/pageEvalArguments.d.ts +1 -1
- package/dist/pageEvalArguments.js +1 -1
- 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 +1 -65
- package/dist/plugin.js +1 -391
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/plugins/clients/pluginHostConnection.d.ts +35 -0
- package/dist/plugins/clients/pluginHostConnection.js +256 -0
- package/dist/plugins/clients/pluginModels.d.ts +17 -0
- package/dist/plugins/clients/pluginModels.js +47 -0
- package/dist/plugins/clients/pluginPages.d.ts +21 -0
- package/dist/plugins/clients/pluginPages.js +46 -0
- package/dist/plugins/clients/pluginState.d.ts +26 -0
- package/dist/plugins/clients/pluginState.js +28 -0
- package/dist/plugins/clients/pluginTasks.d.ts +51 -0
- package/dist/plugins/clients/pluginTasks.js +181 -0
- package/dist/{officialPluginInstaller.js → plugins/officialPluginInstaller.js} +1 -3
- package/dist/{pluginPageBundle.js → plugins/pluginPageBundle.js} +74 -17
- package/dist/{pluginRegistry.d.ts → plugins/pluginRegistry.d.ts} +3 -4
- package/dist/{pluginRegistry.js → plugins/pluginRegistry.js} +16 -15
- package/dist/plugins/pluginTaskProtocol.d.ts +47 -0
- package/dist/plugins/pluginTaskProtocol.js +1 -0
- package/dist/plugins/pluginTaskWire.d.ts +4 -0
- package/dist/plugins/pluginTaskWire.js +42 -0
- package/dist/plugins/runtime/pluginCommandWorker.d.ts +2 -0
- package/dist/plugins/runtime/pluginCommandWorker.js +174 -0
- package/dist/plugins/runtime/pluginDiagnostics.d.ts +26 -0
- package/dist/plugins/runtime/pluginDiagnostics.js +309 -0
- package/dist/plugins/runtime/pluginExecution.d.ts +21 -0
- package/dist/plugins/runtime/pluginExecution.js +43 -0
- package/dist/plugins/runtime/pluginExecutor.d.ts +29 -0
- package/dist/plugins/runtime/pluginExecutor.js +223 -0
- package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +55 -0
- package/dist/plugins/runtime/pluginExecutorProtocol.js +47 -0
- package/dist/plugins/runtime/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/plugins/runtime/pluginExecutorSupervisor.js +63 -0
- package/dist/plugins/runtime/pluginHost.d.ts +20 -0
- package/dist/plugins/runtime/pluginHost.js +53 -0
- package/dist/plugins/runtime/pluginHostFiles.d.ts +1 -0
- package/dist/plugins/runtime/pluginHostFiles.js +21 -0
- package/dist/plugins/runtime/pluginHostPaths.d.ts +11 -0
- package/dist/plugins/runtime/pluginHostPaths.js +22 -0
- package/dist/plugins/runtime/pluginHostServer.d.ts +7 -0
- package/dist/plugins/runtime/pluginHostServer.js +252 -0
- package/dist/plugins/runtime/pluginModelCatalog.d.ts +14 -0
- package/dist/plugins/runtime/pluginModelCatalog.js +51 -0
- package/dist/plugins/runtime/pluginModelSessions.d.ts +16 -0
- package/dist/plugins/runtime/pluginModelSessions.js +234 -0
- package/dist/plugins/runtime/pluginPageManager.d.ts +57 -0
- package/dist/plugins/runtime/pluginPageManager.js +877 -0
- package/dist/plugins/runtime/pluginProcessTree.d.ts +2 -0
- package/dist/plugins/runtime/pluginProcessTree.js +31 -0
- package/dist/plugins/runtime/pluginRevision.d.ts +14 -0
- package/dist/plugins/runtime/pluginRevision.js +227 -0
- package/dist/plugins/runtime/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/plugins/runtime/pluginRuntimeCleanup.js +55 -0
- package/dist/plugins/runtime/pluginStateManager.d.ts +24 -0
- package/dist/plugins/runtime/pluginStateManager.js +132 -0
- package/dist/plugins/runtime/pluginStateStore.d.ts +57 -0
- package/dist/plugins/runtime/pluginStateStore.js +396 -0
- package/dist/plugins/runtime/pluginTaskManager.d.ts +92 -0
- package/dist/plugins/runtime/pluginTaskManager.js +1080 -0
- package/dist/plugins/runtime/pluginTaskStore.d.ts +53 -0
- package/dist/plugins/runtime/pluginTaskStore.js +319 -0
- package/dist/plugins/runtime/pythonPlugin.d.ts +31 -0
- package/dist/{pythonPlugin.js → plugins/runtime/pythonPlugin.js} +236 -31
- package/dist/plugins/sdk/plugin.d.ts +118 -0
- package/dist/plugins/sdk/plugin.js +672 -0
- package/dist/{pluginAppProfiles.d.ts → plugins/sdk/pluginAppProfiles.d.ts} +1 -1
- package/dist/{pluginAppProfiles.js → plugins/sdk/pluginAppProfiles.js} +2 -2
- package/dist/plugins/sdk/pluginModelDefinition.d.ts +6 -0
- package/dist/plugins/sdk/pluginModelDefinition.js +75 -0
- package/dist/plugins/sdk/pluginPageDefinition.d.ts +3 -0
- package/dist/plugins/sdk/pluginPageDefinition.js +62 -0
- package/dist/plugins/sdk/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/plugins/sdk/pluginRecoveryDefinition.js +56 -0
- package/dist/plugins/sdk/pluginStateDefinition.d.ts +4 -0
- package/dist/plugins/sdk/pluginStateDefinition.js +68 -0
- package/dist/runtimePaths.d.ts +4 -0
- package/dist/runtimePaths.js +8 -0
- package/dist/socket.d.ts +2 -2
- package/dist/socket.js +10 -6
- package/dist/unified.d.ts +3 -2
- package/dist/unified.js +83 -2
- package/package.json +7 -3
- package/dist/pythonPlugin.d.ts +0 -20
- /package/dist/{generated-app-profile-definition.js → generated/app-profile-definition.js} +0 -0
- /package/dist/{generated-app-profile-runtime-types.js → generated/app-profile-runtime-types.js} +0 -0
- /package/dist/{officialPluginCatalog.d.ts → plugins/officialPluginCatalog.d.ts} +0 -0
- /package/dist/{officialPluginCatalog.js → plugins/officialPluginCatalog.js} +0 -0
- /package/dist/{officialPluginInstaller.d.ts → plugins/officialPluginInstaller.d.ts} +0 -0
- /package/dist/{pluginPageBundle.d.ts → plugins/pluginPageBundle.d.ts} +0 -0
- /package/dist/{pluginSource.d.ts → plugins/pluginSource.d.ts} +0 -0
- /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 '
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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';
|