@mearl/client 2.20.5 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -2
- package/dist/cli.js +10 -5
- package/dist/commands.d.ts +14 -5
- package/dist/commands.js +21 -2
- package/dist/generated-app-profile-definition.js +20 -10
- package/dist/generated-app-profile-runtime-types.d.ts +4 -0
- package/dist/generated-browser-action-protocol.d.ts +11 -1
- package/dist/generated-browser-action-protocol.js +3 -0
- package/dist/generated-plugin-diagnostics.d.ts +110 -0
- package/dist/generated-plugin-diagnostics.js +1 -0
- package/dist/generated-plugin-execution.d.ts +27 -0
- package/dist/generated-plugin-execution.js +1 -0
- package/dist/generated-plugin-models.d.ts +64 -0
- package/dist/generated-plugin-models.js +1 -0
- package/dist/generated-plugin-pages.d.ts +76 -0
- package/dist/generated-plugin-pages.js +59 -0
- package/dist/generated-plugin-recovery.d.ts +32 -0
- package/dist/generated-plugin-recovery.js +1 -0
- package/dist/generated-plugin-state.d.ts +50 -0
- package/dist/generated-plugin-state.js +1 -0
- package/dist/generated-plugin-tasks.d.ts +74 -0
- package/dist/generated-plugin-tasks.js +7 -0
- package/dist/generated-toolbox-icon.d.ts +10 -0
- package/dist/generated-toolbox-icon.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/plugin-task-host.d.ts +1 -0
- package/dist/plugin-task-host.js +14 -0
- package/dist/plugin-worker.d.ts +1 -0
- package/dist/plugin-worker.js +17 -0
- package/dist/plugin.d.ts +57 -4
- package/dist/plugin.js +296 -15
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginCommandWorker.d.ts +2 -0
- package/dist/pluginCommandWorker.js +174 -0
- package/dist/pluginDiagnostics.d.ts +26 -0
- package/dist/pluginDiagnostics.js +309 -0
- package/dist/pluginExecution.d.ts +21 -0
- package/dist/pluginExecution.js +43 -0
- package/dist/pluginExecutor.d.ts +29 -0
- package/dist/pluginExecutor.js +223 -0
- package/dist/pluginExecutorProtocol.d.ts +55 -0
- package/dist/pluginExecutorProtocol.js +47 -0
- package/dist/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/pluginExecutorSupervisor.js +63 -0
- package/dist/pluginHost.d.ts +20 -0
- package/dist/pluginHost.js +53 -0
- package/dist/pluginHostConnection.d.ts +35 -0
- package/dist/pluginHostConnection.js +257 -0
- package/dist/pluginHostFiles.d.ts +1 -0
- package/dist/pluginHostFiles.js +21 -0
- package/dist/pluginHostPaths.d.ts +11 -0
- package/dist/pluginHostPaths.js +22 -0
- package/dist/pluginHostServer.d.ts +7 -0
- package/dist/pluginHostServer.js +252 -0
- package/dist/pluginModelCatalog.d.ts +14 -0
- package/dist/pluginModelCatalog.js +51 -0
- package/dist/pluginModelDefinition.d.ts +6 -0
- package/dist/pluginModelDefinition.js +75 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/pluginModelSessions.d.ts +16 -0
- package/dist/pluginModelSessions.js +234 -0
- package/dist/pluginModels.d.ts +17 -0
- package/dist/pluginModels.js +47 -0
- package/dist/pluginPageBundle.js +74 -17
- package/dist/pluginPageDefinition.d.ts +3 -0
- package/dist/pluginPageDefinition.js +62 -0
- package/dist/pluginPageManager.d.ts +57 -0
- package/dist/pluginPageManager.js +877 -0
- package/dist/pluginPages.d.ts +21 -0
- package/dist/pluginPages.js +46 -0
- package/dist/pluginProcessTree.d.ts +2 -0
- package/dist/pluginProcessTree.js +31 -0
- package/dist/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/pluginRecoveryDefinition.js +56 -0
- package/dist/pluginRegistry.d.ts +2 -3
- package/dist/pluginRegistry.js +14 -13
- package/dist/pluginRevision.d.ts +14 -0
- package/dist/pluginRevision.js +227 -0
- package/dist/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/pluginRuntimeCleanup.js +55 -0
- package/dist/pluginState.d.ts +26 -0
- package/dist/pluginState.js +28 -0
- package/dist/pluginStateDefinition.d.ts +4 -0
- package/dist/pluginStateDefinition.js +68 -0
- package/dist/pluginStateManager.d.ts +24 -0
- package/dist/pluginStateManager.js +132 -0
- package/dist/pluginStateStore.d.ts +57 -0
- package/dist/pluginStateStore.js +396 -0
- package/dist/pluginTaskManager.d.ts +92 -0
- package/dist/pluginTaskManager.js +1080 -0
- package/dist/pluginTaskProtocol.d.ts +47 -0
- package/dist/pluginTaskProtocol.js +1 -0
- package/dist/pluginTaskStore.d.ts +53 -0
- package/dist/pluginTaskStore.js +319 -0
- package/dist/pluginTaskWire.d.ts +4 -0
- package/dist/pluginTaskWire.js +42 -0
- package/dist/pluginTasks.d.ts +51 -0
- package/dist/pluginTasks.js +181 -0
- package/dist/pythonPlugin.d.ts +13 -2
- package/dist/pythonPlugin.js +235 -30
- package/dist/socket.js +5 -2
- package/dist/unified.d.ts +1 -0
- package/dist/unified.js +81 -0
- package/package.json +7 -3
package/dist/pythonPlugin.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { pluginRecoverySignature } from './pluginRecoveryDefinition.js';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import { createInterface } from 'node:readline';
|
|
3
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
|
}
|
package/dist/socket.js
CHANGED
|
@@ -581,6 +581,7 @@ async function invokeOnce(action, data, timeoutSec, transport, socketPath, contr
|
|
|
581
581
|
socketPath = host.socketPath;
|
|
582
582
|
}
|
|
583
583
|
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
584
|
+
const deadline = Date.now() + Math.ceil(timeoutSec * 1000);
|
|
584
585
|
return new Promise((resolve, reject) => {
|
|
585
586
|
if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
|
|
586
587
|
const ownerUid = fs.statSync(socketPath).uid;
|
|
@@ -622,8 +623,10 @@ async function invokeOnce(action, data, timeoutSec, transport, socketPath, contr
|
|
|
622
623
|
message.controlSessionId = data[SESSION_OPTION];
|
|
623
624
|
if (transport)
|
|
624
625
|
message.transport = transport;
|
|
625
|
-
if (providerTarget)
|
|
626
|
+
if (providerTarget) {
|
|
626
627
|
message.providerTarget = providerTarget;
|
|
628
|
+
message.deadline = deadline;
|
|
629
|
+
}
|
|
627
630
|
socket.write(JSON.stringify(message) + '\n');
|
|
628
631
|
});
|
|
629
632
|
socket.on('data', chunk => {
|
|
@@ -681,7 +684,7 @@ function formatConnectError(error, socketPath) {
|
|
|
681
684
|
'3. 确认 Chrome 浏览器正在运行,如仍无法连接可在 chrome://extensions 重新加载插件',
|
|
682
685
|
'',
|
|
683
686
|
'【方式二:CDP 远程调试模式(Chrome 145+,无需插件)】',
|
|
684
|
-
' 访问 chrome://inspect/#remote-debugging,勾选 "
|
|
687
|
+
' 访问 chrome://inspect/#remote-debugging,勾选 "Allow remote debugging for this browser instance"',
|
|
685
688
|
' Chrome 会弹出授权提示,点击允许,native host 自动读取 DevToolsActivePort 文件连接',
|
|
686
689
|
' 确认有普通网页已打开(非 chrome:// 页面)',
|
|
687
690
|
].join('\n');
|
package/dist/unified.d.ts
CHANGED
package/dist/unified.js
CHANGED
|
@@ -127,9 +127,69 @@ export class UnifiedClient {
|
|
|
127
127
|
const { invokeRegisteredPlugin } = await import('./plugin.js');
|
|
128
128
|
return invokeRegisteredPlugin(name, invocation, {
|
|
129
129
|
invoke: (action, payload, invokeOptions) => this.invoke(action, payload, invokeOptions),
|
|
130
|
+
runModel: async (plugin, request, execution) => {
|
|
131
|
+
const { PluginModelClient } = await import('./pluginModels.js');
|
|
132
|
+
const models = new PluginModelClient({ source: options.source ?? 'sdk' });
|
|
133
|
+
try {
|
|
134
|
+
return await models.run(plugin, request, {
|
|
135
|
+
signal: execution.signal,
|
|
136
|
+
onProgress: progress => {
|
|
137
|
+
if (progress !== undefined)
|
|
138
|
+
execution.onProgress?.(progress);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
models.disconnect();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
130
146
|
}, options);
|
|
131
147
|
}
|
|
132
148
|
async invoke(action, data = {}, options = {}) {
|
|
149
|
+
if (action === 'plugin_state') {
|
|
150
|
+
const { PluginStateClient } = await import('./pluginState.js');
|
|
151
|
+
const state = new PluginStateClient();
|
|
152
|
+
try {
|
|
153
|
+
return await state.call(String(data.plugin ?? ''), data.collection, data.operation, data, options);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
state.disconnect();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (action === 'plugin_model') {
|
|
160
|
+
const { PluginModelClient } = await import('./pluginModels.js');
|
|
161
|
+
const models = new PluginModelClient();
|
|
162
|
+
try {
|
|
163
|
+
if (data.operation === 'capabilities')
|
|
164
|
+
return await models.capabilities();
|
|
165
|
+
if (data.operation === 'list')
|
|
166
|
+
return await models.list(data.provider);
|
|
167
|
+
if (data.operation === 'start')
|
|
168
|
+
return await models.start(String(data.plugin ?? ''), data.request, data.taskOptions ?? {});
|
|
169
|
+
if (data.operation === 'run')
|
|
170
|
+
return await models.run(String(data.plugin ?? ''), data.request, data.taskOptions ?? {});
|
|
171
|
+
throw new Error('Invalid model operation');
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
models.disconnect();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (action === 'plugin_page') {
|
|
178
|
+
const { PluginHostConnection } = await import('./pluginHostConnection.js');
|
|
179
|
+
const { PluginPageClient } = await import('./pluginPages.js');
|
|
180
|
+
const connection = new PluginHostConnection();
|
|
181
|
+
const pages = new PluginPageClient({}, connection);
|
|
182
|
+
try {
|
|
183
|
+
const pinned = ['attach', 'activation'].includes(data.operation)
|
|
184
|
+
? await this.pinBrowserTarget(options)
|
|
185
|
+
: options;
|
|
186
|
+
return await pages.call(String(data.plugin ?? ''), data.operation, data, pinned);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
pages.disconnect();
|
|
190
|
+
connection.disconnect();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
133
193
|
if (options.session && options.session !== 'new') {
|
|
134
194
|
const session = await this.resolveControlSession(options.session, options);
|
|
135
195
|
const selected = decodeBrowserTarget(options.browser);
|
|
@@ -209,6 +269,27 @@ export class UnifiedClient {
|
|
|
209
269
|
saveArtifact(artifact, outputPath, options = {}) {
|
|
210
270
|
return exportArtifact(this, artifact, outputPath, options);
|
|
211
271
|
}
|
|
272
|
+
async pinBrowserTarget(options = {}) {
|
|
273
|
+
const selector = options.browser ?? this.defaultBrowser;
|
|
274
|
+
if (decodeBrowserTarget(selector))
|
|
275
|
+
return { ...options, browser: selector };
|
|
276
|
+
const listing = (await this.invoke('browser_list', {}, {
|
|
277
|
+
...options,
|
|
278
|
+
...(options.connector || !this.defaultConnector
|
|
279
|
+
? {}
|
|
280
|
+
: { connector: this.defaultConnector }),
|
|
281
|
+
}));
|
|
282
|
+
const connected = listing.browsers.filter(browser => browser.status === 'connected');
|
|
283
|
+
const matches = selector
|
|
284
|
+
? findBrowsers(connected, selector)
|
|
285
|
+
: connected.filter(browser => browser.browserId === listing.defaultBrowserId);
|
|
286
|
+
const target = matches.length ? matches : selector ? [] : connected;
|
|
287
|
+
if (target.length !== 1)
|
|
288
|
+
throw new Error('Plugin pages require an explicit, connected and unambiguous browser target');
|
|
289
|
+
const pinned = { ...options, browser: target[0].browserId };
|
|
290
|
+
delete pinned.connector;
|
|
291
|
+
return pinned;
|
|
292
|
+
}
|
|
212
293
|
async pinPageEvalTarget(options, data) {
|
|
213
294
|
const target = decodeBrowserTarget(options.browser);
|
|
214
295
|
if (target && !options.connector)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -60,11 +60,15 @@
|
|
|
60
60
|
"registry": "https://registry.npmjs.org"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"acorn": "^8.16.0",
|
|
64
|
+
"ajv": "^8.18.0",
|
|
63
65
|
"commander": "^15.0.0",
|
|
64
66
|
"fastest-levenshtein": "^1.0.16",
|
|
65
67
|
"ws": "^8.18.0",
|
|
66
|
-
"@mearl/
|
|
67
|
-
"@mearl/provider": "2.
|
|
68
|
+
"@mearl/daemon-core": "2.21.0",
|
|
69
|
+
"@mearl/provider": "2.21.0",
|
|
70
|
+
"@mearl/agent-runtime": "2.21.0",
|
|
71
|
+
"@mearl/cloud-types": "2.21.0"
|
|
68
72
|
},
|
|
69
73
|
"devDependencies": {
|
|
70
74
|
"@types/node": "^24.9.1",
|