@actiondock/core 2.19.0 → 2.20.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/dist/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +2 -0
- package/dist/execution/service.js +8 -5
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +9 -1
- package/dist/host/host.js +183 -38
- package/dist/host/types.d.ts +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +2 -0
- package/dist/invocation/types.js +1 -0
- package/dist/package/index.d.ts +1 -1
- package/dist/package/runtime.js +31 -12
- package/dist/platform/types.d.ts +48 -1
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/index.d.ts +5 -2
- package/dist/project/index.js +5 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +4 -0
- package/dist/project/manifest.js +10 -0
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +6 -3
- package/dist/runtime/context.js +60 -13
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +65 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +1 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +20 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { ActionDockError, ACTION_CANCELLED, EXECUTION_FAILED, INVALID_ARGUMENT, NETWORK_ERROR, SERVICE_CLOSED, SERVICE_RESULT_UNKNOWN, TIMEOUT, } from "../errors.js";
|
|
4
|
+
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
5
|
+
import { RemoteActionDockService } from "../service/remote.js";
|
|
6
|
+
/**
|
|
7
|
+
* 清理、反查与发现默认有界等待时限(毫秒)。
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_CLEANUP_TIMEOUT_MS = 5000;
|
|
10
|
+
const DEFAULT_LOOKUP_TIMEOUT_MS = 3000;
|
|
11
|
+
const DEFAULT_DISCOVERY_TIMEOUT_MS = 10000;
|
|
12
|
+
/**
|
|
13
|
+
* 过滤 URL 中携带的用户、密码凭据与敏感查询参数,防止泄漏至日志或异常元数据。
|
|
14
|
+
*/
|
|
15
|
+
function sanitizeServerUrl(urlStr) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(urlStr);
|
|
18
|
+
parsed.username = "";
|
|
19
|
+
parsed.password = "";
|
|
20
|
+
if (parsed.searchParams.has("token")) {
|
|
21
|
+
parsed.searchParams.set("token", "[REDACTED]");
|
|
22
|
+
}
|
|
23
|
+
return parsed.origin;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "[invalid-url]";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 脱敏字符串中的敏感 Token 与 URL 凭据。
|
|
31
|
+
*/
|
|
32
|
+
function sanitizeString(str, token) {
|
|
33
|
+
let result = str;
|
|
34
|
+
if (token && token.length > 0) {
|
|
35
|
+
result = result.replaceAll(token, "[REDACTED]");
|
|
36
|
+
}
|
|
37
|
+
// 脱敏 URL 用户密码凭据 (http://user:pass@host)
|
|
38
|
+
result = result.replace(/(https?:\/\/)([^/@\s]+)@/g, "$1[REDACTED]@");
|
|
39
|
+
// 脱敏查询参数中的 token / key / secret / password
|
|
40
|
+
result = result.replace(/([?&](?:token|key|secret|password)=)[^&\s]+/gi, "$1[REDACTED]");
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 格式化脱敏错误信息。
|
|
45
|
+
*/
|
|
46
|
+
function sanitizeErrorMessage(msg, token) {
|
|
47
|
+
if (typeof msg !== "string") {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
return sanitizeString(msg, token);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 深度脱敏错误元数据与原始对象,防止 Token 与 URL 凭据泄漏。
|
|
54
|
+
*/
|
|
55
|
+
function sanitizeDetails(val, token) {
|
|
56
|
+
if (val === null || val === undefined) {
|
|
57
|
+
return val;
|
|
58
|
+
}
|
|
59
|
+
if (typeof val === "string") {
|
|
60
|
+
return sanitizeString(val, token);
|
|
61
|
+
}
|
|
62
|
+
if (val instanceof Error) {
|
|
63
|
+
return sanitizeString(val.message, token);
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(val)) {
|
|
66
|
+
return val.map((item) => sanitizeDetails(item, token));
|
|
67
|
+
}
|
|
68
|
+
if (typeof val === "object") {
|
|
69
|
+
const sanitizedObj = {};
|
|
70
|
+
for (const [k, v] of Object.entries(val)) {
|
|
71
|
+
if (k.toLowerCase().includes("token") ||
|
|
72
|
+
k.toLowerCase().includes("secret") ||
|
|
73
|
+
k.toLowerCase().includes("password")) {
|
|
74
|
+
sanitizedObj[k] = "[REDACTED]";
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
sanitizedObj[k] = sanitizeDetails(v, token);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sanitizedObj;
|
|
81
|
+
}
|
|
82
|
+
return val;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 构造统一且剔除敏感凭据的错误详情信封。
|
|
86
|
+
*/
|
|
87
|
+
function buildErrorDetails(action, metadata, requestId, resultUnknown, remoteRunId, extra) {
|
|
88
|
+
return {
|
|
89
|
+
action,
|
|
90
|
+
binding: metadata.binding,
|
|
91
|
+
profile: metadata.profile,
|
|
92
|
+
requestId,
|
|
93
|
+
remoteRunId,
|
|
94
|
+
resultUnknown,
|
|
95
|
+
...extra,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 针对指定唯一 requestId 执行有界反查。
|
|
100
|
+
*/
|
|
101
|
+
async function queryRunByRequestId(service, requestId, timeoutMs) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
const runs = await service.runs.list({ requestIds: [requestId], limit: 1 }, { signal: controller.signal });
|
|
106
|
+
return { record: runs && runs.length > 0 ? runs[0] : undefined };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return { error: err };
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 针对远端任务执行有界取消调用,避免无限阻塞与定时器泄漏。
|
|
117
|
+
*/
|
|
118
|
+
async function cancelRunWithBoundedTimeout(service, runId, timeoutMs) {
|
|
119
|
+
let timer;
|
|
120
|
+
try {
|
|
121
|
+
const res = await Promise.race([
|
|
122
|
+
service.runs.cancel(runId, "Parent invocation cancelled"),
|
|
123
|
+
new Promise((_, reject) => {
|
|
124
|
+
timer = setTimeout(() => reject(new Error(`Cancellation timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
if (res.outcome === "not_found" || res.outcome === "not_owner") {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
outcome: res.outcome,
|
|
131
|
+
resultUnknown: true,
|
|
132
|
+
error: new Error(`Cancellation failed with outcome: ${res.outcome}`),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (res.outcome === "already_terminal") {
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
outcome: "already_terminal",
|
|
139
|
+
resultUnknown: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// res.outcome === "requested"
|
|
143
|
+
// requested 仅表示已请求,不可宣称终态已停止,resultUnknown 要诚实
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
outcome: "requested",
|
|
147
|
+
resultUnknown: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
resultUnknown: true,
|
|
154
|
+
error: err,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if (timer !== undefined) {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** 等待必须响应调用方取消,不能依赖服务实现主动结束结果 Promise。 */
|
|
164
|
+
function waitWithSignal(operation, signal) {
|
|
165
|
+
if (signal.aborted)
|
|
166
|
+
return Promise.reject(signal.reason ?? new Error("Invocation cancelled"));
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const onAbort = () => reject(signal.reason ?? new Error("Invocation cancelled"));
|
|
169
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
170
|
+
operation.then((value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error); });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 安全的远端嵌套调用适配执行器。
|
|
175
|
+
*
|
|
176
|
+
* 遵循原则:
|
|
177
|
+
* - 目标 target 由 Host 预先根据 profile 解析并冻结传入,不自作路由决断。
|
|
178
|
+
* - 连接缓存按 target 对象身份隔离复用,并在 close() 时全量释放。
|
|
179
|
+
* - 严格防范凭证与本地私有上下文泄漏,仅透传必要控制字段。
|
|
180
|
+
* - 取消与清理全链路有界且透传远端 runs.cancel,严禁静默吞没未确认终态。
|
|
181
|
+
*/
|
|
182
|
+
export class RemoteInvocationExecutor {
|
|
183
|
+
logger;
|
|
184
|
+
services = new Map();
|
|
185
|
+
isClosed = false;
|
|
186
|
+
closeController = new AbortController();
|
|
187
|
+
pending = new Set();
|
|
188
|
+
constructor(options) {
|
|
189
|
+
this.logger = options?.logger;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 触发执行远端 Action 动作。
|
|
193
|
+
*/
|
|
194
|
+
invoke(target, ref, input, context, metadata, expectedContract) {
|
|
195
|
+
const operation = this.execute(target, ref, input, {
|
|
196
|
+
...context,
|
|
197
|
+
signal: AbortSignal.any([context.signal, this.closeController.signal]),
|
|
198
|
+
}, metadata, expectedContract);
|
|
199
|
+
this.pending.add(operation);
|
|
200
|
+
const settled = () => { this.pending.delete(operation); };
|
|
201
|
+
operation.then(settled, settled);
|
|
202
|
+
return operation;
|
|
203
|
+
}
|
|
204
|
+
async execute(target, ref, input, context, metadata, expectedContract) {
|
|
205
|
+
const childRequestId = randomUUID();
|
|
206
|
+
let parsedRef;
|
|
207
|
+
try {
|
|
208
|
+
parsedRef = parseActionRef(ref);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
parsedRef = typeof ref === "object" && ref !== null ? ref : { actionId: String(ref) };
|
|
212
|
+
}
|
|
213
|
+
const actionStr = parsedRef.packageId
|
|
214
|
+
? `${parsedRef.packageId}/${parsedRef.actionId}`
|
|
215
|
+
: parsedRef.actionId;
|
|
216
|
+
const logger = context.logger ?? this.logger;
|
|
217
|
+
const sanitizedTarget = sanitizeServerUrl(target.serverUrl);
|
|
218
|
+
const makeDetails = (resultUnknown, remoteRunId, extra) => buildErrorDetails(actionStr, metadata, childRequestId, resultUnknown, remoteRunId, extra);
|
|
219
|
+
// 1. 检查执行器生命周期状态
|
|
220
|
+
if (this.isClosed) {
|
|
221
|
+
const err = new ActionDockError(SERVICE_CLOSED, "RemoteInvocationExecutor is closed", makeDetails(false, undefined));
|
|
222
|
+
logger?.error?.(`[RemoteInvocation] Executor closed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
// 2. 检查前置取消信号,阻止任何网络通信
|
|
226
|
+
if (context.signal.aborted) {
|
|
227
|
+
const err = new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled before start", makeDetails(false, undefined));
|
|
228
|
+
logger?.warn?.(`[RemoteInvocation] Pre-cancelled: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
// 3. 复用或建立 target 对应的远端服务连接
|
|
232
|
+
let service = this.services.get(target);
|
|
233
|
+
if (!service) {
|
|
234
|
+
if (this.isClosed) {
|
|
235
|
+
throw new ActionDockError(SERVICE_CLOSED, "RemoteInvocationExecutor is closed", makeDetails(false, undefined));
|
|
236
|
+
}
|
|
237
|
+
service = new RemoteActionDockService(target);
|
|
238
|
+
this.services.set(target, service);
|
|
239
|
+
}
|
|
240
|
+
// 4. 远端契约发现与校验(使用有界 / AbortSignal race 确保响应父级取消,避免无限等待或取消后仍继续执行)
|
|
241
|
+
let remoteSpec;
|
|
242
|
+
const discoveryTimeoutMs = Math.min(DEFAULT_DISCOVERY_TIMEOUT_MS, Math.max(1000, context.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS));
|
|
243
|
+
try {
|
|
244
|
+
if (context.signal.aborted) {
|
|
245
|
+
throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled before discovery", makeDetails(false, undefined));
|
|
246
|
+
}
|
|
247
|
+
let abortListener;
|
|
248
|
+
let discoveryTimer;
|
|
249
|
+
try {
|
|
250
|
+
remoteSpec = await Promise.race([
|
|
251
|
+
service.discovery.describeAction(ref),
|
|
252
|
+
new Promise((_, reject) => {
|
|
253
|
+
abortListener = () => {
|
|
254
|
+
reject(new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled during discovery", makeDetails(false, undefined)));
|
|
255
|
+
};
|
|
256
|
+
context.signal.addEventListener("abort", abortListener, { once: true });
|
|
257
|
+
discoveryTimer = setTimeout(() => {
|
|
258
|
+
reject(new ActionDockError(NETWORK_ERROR, `Discovery timed out after ${discoveryTimeoutMs}ms for action '${actionStr}'`, makeDetails(false, undefined)));
|
|
259
|
+
}, discoveryTimeoutMs);
|
|
260
|
+
}),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
if (discoveryTimer !== undefined) {
|
|
265
|
+
clearTimeout(discoveryTimer);
|
|
266
|
+
}
|
|
267
|
+
if (abortListener !== undefined) {
|
|
268
|
+
context.signal.removeEventListener("abort", abortListener);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (context.signal.aborted) {
|
|
274
|
+
throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled during discovery", makeDetails(false, undefined));
|
|
275
|
+
}
|
|
276
|
+
const code = err?.code || NETWORK_ERROR;
|
|
277
|
+
const sanitizedMsg = sanitizeErrorMessage(err?.message, target.token);
|
|
278
|
+
const wrappedErr = new ActionDockError(code, `Failed to describe remote action '${actionStr}': ${sanitizedMsg}`, makeDetails(false, undefined, {
|
|
279
|
+
originalError: sanitizeDetails(err?.details ?? err?.message, target.token),
|
|
280
|
+
}));
|
|
281
|
+
logger?.error?.(`[RemoteInvocation] Discovery failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, error=${sanitizedMsg}`);
|
|
282
|
+
throw wrappedErr;
|
|
283
|
+
}
|
|
284
|
+
if (expectedContract) {
|
|
285
|
+
const expectedInput = expectedContract.inputSchema ?? {};
|
|
286
|
+
const actualInput = remoteSpec.inputSchema ?? {};
|
|
287
|
+
const expectedOutput = expectedContract.outputSchema ?? {};
|
|
288
|
+
const actualOutput = remoteSpec.outputSchema ?? {};
|
|
289
|
+
if (!isDeepStrictEqual(expectedInput, actualInput) || !isDeepStrictEqual(expectedOutput, actualOutput)) {
|
|
290
|
+
const err = new ActionDockError(INVALID_ARGUMENT, `Remote action contract schema mismatch for action '${actionStr}'`, makeDetails(false, undefined, {
|
|
291
|
+
expectedInput,
|
|
292
|
+
actualInput,
|
|
293
|
+
expectedOutput,
|
|
294
|
+
actualOutput,
|
|
295
|
+
}));
|
|
296
|
+
logger?.error?.(`[RemoteInvocation] Contract schema mismatch: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// 5. 校验通过,准备发起远端异步执行
|
|
301
|
+
if (context.signal.aborted) {
|
|
302
|
+
throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled before start", makeDetails(false, undefined));
|
|
303
|
+
}
|
|
304
|
+
const cleanupTimeoutMs = Math.min(DEFAULT_CLEANUP_TIMEOUT_MS, Math.max(1000, context.timeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS));
|
|
305
|
+
const lookupTimeoutMs = Math.min(DEFAULT_LOOKUP_TIMEOUT_MS, Math.max(1000, context.timeoutMs ?? DEFAULT_LOOKUP_TIMEOUT_MS));
|
|
306
|
+
const startOptions = {
|
|
307
|
+
timeoutMs: context.timeoutMs,
|
|
308
|
+
requestId: childRequestId,
|
|
309
|
+
signal: context.signal,
|
|
310
|
+
};
|
|
311
|
+
let ticket;
|
|
312
|
+
let startError;
|
|
313
|
+
try {
|
|
314
|
+
ticket = await service.execution.start(ref, input, startOptions);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
startError = err;
|
|
318
|
+
}
|
|
319
|
+
// 检查 ticket 是否代表确定性拒绝(如鉴权失败、入参校验失败等):
|
|
320
|
+
// execution.start 可能返回 { status: 'failed', runId: '', result: Promise.resolve({ ok: false, error: ... }) }
|
|
321
|
+
// 必须先读取 failed ticket 保留确定拒绝 code,不能误判为 SERVICE_RESULT_UNKNOWN。
|
|
322
|
+
if (ticket && (ticket.status === "failed" || !ticket.runId)) {
|
|
323
|
+
if (ticket.result) {
|
|
324
|
+
try {
|
|
325
|
+
const res = await ticket.result;
|
|
326
|
+
if (res && !res.ok && res.error) {
|
|
327
|
+
const errCode = res.error.code;
|
|
328
|
+
const isTransportUnknown = errCode === NETWORK_ERROR ||
|
|
329
|
+
errCode === "ACTION_TIMEOUT" ||
|
|
330
|
+
errCode === "TIMEOUT" ||
|
|
331
|
+
errCode === ACTION_CANCELLED;
|
|
332
|
+
if (!isTransportUnknown) {
|
|
333
|
+
const sanitizedErrMsg = sanitizeErrorMessage(res.error.message, target.token) || "Remote execution rejected";
|
|
334
|
+
logger?.warn?.(`[RemoteInvocation] Remote start rejected deterministically: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, code=${errCode}, error=${sanitizedErrMsg}`);
|
|
335
|
+
throw new ActionDockError(errCode || EXECUTION_FAILED, sanitizedErrMsg, makeDetails(false, undefined, {
|
|
336
|
+
remoteError: sanitizeDetails(res.error.details ?? res.error, target.token),
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (readErr) {
|
|
342
|
+
if (readErr instanceof ActionDockError) {
|
|
343
|
+
throw readErr;
|
|
344
|
+
}
|
|
345
|
+
startError = readErr;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!ticket.runId) {
|
|
349
|
+
ticket = undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// 6. 接受前中断处理(远端可能已受理执行,进行有界反查与补发取消)
|
|
353
|
+
if (!ticket || !ticket.runId) {
|
|
354
|
+
const lookupRes = await queryRunByRequestId(service, childRequestId, lookupTimeoutMs);
|
|
355
|
+
const foundRun = lookupRes.record;
|
|
356
|
+
if (foundRun) {
|
|
357
|
+
// 使用 RunRecord 的唯一标识 id(不可使用 runId)
|
|
358
|
+
const cancelRes = await cancelRunWithBoundedTimeout(service, foundRun.id, cleanupTimeoutMs);
|
|
359
|
+
if (!cancelRes.ok) {
|
|
360
|
+
const sanitizedCancelMsg = sanitizeErrorMessage(cancelRes.error?.message, target.token);
|
|
361
|
+
logger?.error?.(`[RemoteInvocation] Cancel cleanup failed for accepted run '${foundRun.id}': ${sanitizedCancelMsg}`);
|
|
362
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Interrupted before acceptance, and cancel cleanup failed for run '${foundRun.id}': ${sanitizedCancelMsg}`, makeDetails(true, foundRun.id, {
|
|
363
|
+
cancelOutcome: cancelRes.outcome,
|
|
364
|
+
originalError: sanitizedCancelMsg,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
if (context.signal.aborted) {
|
|
368
|
+
throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled", makeDetails(cancelRes.resultUnknown, foundRun.id, {
|
|
369
|
+
cancelOutcome: cancelRes.outcome,
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
const sanitizedStartMsg = sanitizeErrorMessage(startError?.message, target.token);
|
|
373
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Action execution was interrupted before acceptance and cancelled on remote: ${sanitizedStartMsg}`, makeDetails(cancelRes.resultUnknown, foundRun.id, {
|
|
374
|
+
cancelOutcome: cancelRes.outcome,
|
|
375
|
+
originalError: sanitizedStartMsg,
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
// 反查若发生错误,必须提供可观测错误元信息,不可宣称无记录
|
|
379
|
+
if (lookupRes.error) {
|
|
380
|
+
const sanitizedLookupMsg = sanitizeErrorMessage(lookupRes.error?.message, target.token);
|
|
381
|
+
const sanitizedStartMsg = sanitizeErrorMessage(startError?.message, target.token);
|
|
382
|
+
logger?.warn?.(`[RemoteInvocation] Interrupted before acceptance and run lookup failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, lookupError=${sanitizedLookupMsg}`);
|
|
383
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Action execution interrupted before acceptance; query run by requestId failed: ${sanitizedLookupMsg}`, makeDetails(true, undefined, {
|
|
384
|
+
lookupError: sanitizedLookupMsg,
|
|
385
|
+
originalError: sanitizedStartMsg,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
// 反查成功但无记录:未能确认终态应使用 SERVICE_RESULT_UNKNOWN
|
|
389
|
+
const sanitizedStartMsg = sanitizeErrorMessage(startError?.message, target.token);
|
|
390
|
+
logger?.warn?.(`[RemoteInvocation] Interrupted before acceptance with unknown remote status: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}`);
|
|
391
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Action execution interrupted before acceptance; remote execution status is unknown: ${sanitizedStartMsg}`, makeDetails(true, undefined, {
|
|
392
|
+
lookupRecordFound: false,
|
|
393
|
+
originalError: sanitizedStartMsg,
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
// 7. 远端已成功接受调用并返回 runId
|
|
397
|
+
const remoteRunId = ticket.runId;
|
|
398
|
+
logger?.info(`[RemoteInvocation] Accepted: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
|
|
399
|
+
// 显式检查 ticket.result 是否存在,缺失应抛出 EXECUTION_FAILED 且不可触发 TypeError
|
|
400
|
+
if (!ticket.result) {
|
|
401
|
+
const err = new ActionDockError(EXECUTION_FAILED, `Execution ticket missing result Promise for remote run '${remoteRunId}'`, makeDetails(true, remoteRunId));
|
|
402
|
+
logger?.error?.(`[RemoteInvocation] Missing ticket.result: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
// 防止未处理的 Promise Rejection
|
|
406
|
+
ticket.result.catch(() => { });
|
|
407
|
+
let cancelPromise;
|
|
408
|
+
const abortHandler = () => {
|
|
409
|
+
cancelPromise = cancelRunWithBoundedTimeout(service, remoteRunId, cleanupTimeoutMs);
|
|
410
|
+
};
|
|
411
|
+
if (context.signal.aborted) {
|
|
412
|
+
abortHandler();
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
context.signal.addEventListener("abort", abortHandler, { once: true });
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
let execResult;
|
|
419
|
+
try {
|
|
420
|
+
execResult = await waitWithSignal(ticket.result, context.signal);
|
|
421
|
+
}
|
|
422
|
+
catch (waitErr) {
|
|
423
|
+
if (context.signal.aborted) {
|
|
424
|
+
const cancelRes = await (cancelPromise ?? cancelRunWithBoundedTimeout(service, remoteRunId, cleanupTimeoutMs));
|
|
425
|
+
if (!cancelRes.ok) {
|
|
426
|
+
const sanitizedCancelMsg = sanitizeErrorMessage(cancelRes.error?.message, target.token);
|
|
427
|
+
logger?.error?.(`[RemoteInvocation] Cancellation cleanup failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, error=${sanitizedCancelMsg}`);
|
|
428
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Cancellation cleanup failed: ${sanitizedCancelMsg}`, makeDetails(true, remoteRunId, { cancelOutcome: cancelRes.outcome }));
|
|
429
|
+
}
|
|
430
|
+
throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled", makeDetails(cancelRes.resultUnknown, remoteRunId, { cancelOutcome: cancelRes.outcome }));
|
|
431
|
+
}
|
|
432
|
+
const sanitizedWaitMsg = sanitizeErrorMessage(waitErr?.message, target.token);
|
|
433
|
+
logger?.error?.(`[RemoteInvocation] Network failure while waiting: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, error=${sanitizedWaitMsg}`);
|
|
434
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Communication lost while waiting for remote run '${remoteRunId}': ${sanitizedWaitMsg}`, makeDetails(true, remoteRunId, { originalError: sanitizedWaitMsg }));
|
|
435
|
+
}
|
|
436
|
+
// 若在此期间已被信号取消,必须等待远端 runs.cancel 发送完成
|
|
437
|
+
if (context.signal.aborted) {
|
|
438
|
+
const cancelRes = await (cancelPromise ?? cancelRunWithBoundedTimeout(service, remoteRunId, cleanupTimeoutMs));
|
|
439
|
+
if (!cancelRes.ok) {
|
|
440
|
+
const sanitizedCancelMsg = sanitizeErrorMessage(cancelRes.error?.message, target.token);
|
|
441
|
+
logger?.error?.(`[RemoteInvocation] Cancellation cleanup failed: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, error=${sanitizedCancelMsg}`);
|
|
442
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Cancellation cleanup failed: ${sanitizedCancelMsg}`, makeDetails(true, remoteRunId, { cancelOutcome: cancelRes.outcome }));
|
|
443
|
+
}
|
|
444
|
+
logger?.info?.(`[RemoteInvocation] Cancellation requested: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
|
|
445
|
+
throw new ActionDockError(ACTION_CANCELLED, "Action execution was cancelled", makeDetails(cancelRes.resultUnknown, remoteRunId, { cancelOutcome: cancelRes.outcome }));
|
|
446
|
+
}
|
|
447
|
+
if (execResult.ok) {
|
|
448
|
+
logger?.info?.(`[RemoteInvocation] Succeeded: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, target=${sanitizedTarget}`);
|
|
449
|
+
return execResult.data;
|
|
450
|
+
}
|
|
451
|
+
// 执行终态失败分支
|
|
452
|
+
const errCode = execResult.error?.code;
|
|
453
|
+
const rawErrMsg = execResult.error?.message || "Remote action execution failed";
|
|
454
|
+
const sanitizedErrMsg = sanitizeErrorMessage(rawErrMsg, target.token);
|
|
455
|
+
if (errCode === NETWORK_ERROR || errCode === TIMEOUT || errCode === SERVICE_RESULT_UNKNOWN) {
|
|
456
|
+
logger?.error?.(`[RemoteInvocation] Network error during execution wait: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}`);
|
|
457
|
+
throw new ActionDockError(SERVICE_RESULT_UNKNOWN, `Network error while waiting for remote run '${remoteRunId}': ${sanitizedErrMsg}`, makeDetails(true, remoteRunId, {
|
|
458
|
+
remoteError: sanitizeDetails(execResult.error, target.token),
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
if (errCode === SERVICE_CLOSED) {
|
|
462
|
+
throw new ActionDockError(SERVICE_CLOSED, `Remote service closed: ${sanitizedErrMsg}`, makeDetails(true, remoteRunId));
|
|
463
|
+
}
|
|
464
|
+
// 确定业务失败:保留原错误码
|
|
465
|
+
logger?.warn?.(`[RemoteInvocation] Business error: action=${actionStr}, binding=${metadata.binding}, profile=${metadata.profile}, requestId=${childRequestId}, remoteRunId=${remoteRunId}, code=${errCode}`);
|
|
466
|
+
throw new ActionDockError(errCode || "ACTION_FAILED", sanitizedErrMsg, makeDetails(false, remoteRunId, {
|
|
467
|
+
remoteError: sanitizeDetails(execResult.error?.details ?? execResult.error, target.token),
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
context.signal.removeEventListener("abort", abortHandler);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 关闭所有持有的远端服务连接并释放关联资源,对外拒绝后续调用。
|
|
476
|
+
*/
|
|
477
|
+
async close() {
|
|
478
|
+
if (this.isClosed) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.isClosed = true;
|
|
482
|
+
this.closeController.abort(new ActionDockError(SERVICE_CLOSED, "Remote invocation executor is closing"));
|
|
483
|
+
// 先让在途调用完成有界反查与协作取消,再关闭其依赖的服务连接。
|
|
484
|
+
await Promise.allSettled(Array.from(this.pending));
|
|
485
|
+
const servicesToClose = Array.from(this.services.values());
|
|
486
|
+
this.services.clear();
|
|
487
|
+
await Promise.all(servicesToClose.map((s) => s.close()));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
@@ -49,6 +49,8 @@ export interface InvocationContext {
|
|
|
49
49
|
readonly caller?: InvocationCaller;
|
|
50
50
|
/** 调用栈数组快照(用于环路检测与调用深度限制) */
|
|
51
51
|
readonly callStack: readonly string[];
|
|
52
|
+
/** 已解析目标的调用栈作用域,仅用于宿主内部执行,不跨网络透传。 */
|
|
53
|
+
readonly targetScope?: string;
|
|
52
54
|
/** 目标包物理与快照身份标识 */
|
|
53
55
|
readonly package: PackageIdentity;
|
|
54
56
|
/** 取消信号 */
|
package/dist/invocation/types.js
CHANGED
|
@@ -31,6 +31,7 @@ export function createInvocationContext(options) {
|
|
|
31
31
|
parentRunId: options.parentRunId,
|
|
32
32
|
caller: options.caller,
|
|
33
33
|
callStack: options.callStack ? [...options.callStack] : [],
|
|
34
|
+
targetScope: options.targetScope,
|
|
34
35
|
package: pkg,
|
|
35
36
|
signal: options.signal ?? new AbortController().signal,
|
|
36
37
|
timeoutMs: options.timeoutMs,
|
package/dist/package/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { createPackageRuntime, DefaultPackageRuntime, } from "./runtime.js";
|
|
|
10
10
|
export { createNonClosingStorageView } from "./storage-view.js";
|
|
11
11
|
export { findExecutable } from "../utils/index.js";
|
|
12
12
|
export type { PackageRuntime, PackageRuntimeOptions, PackageInfo, ActionSpec, ActionSummary, PlaybookSpec, PlaybookSummary, } from "./types.js";
|
|
13
|
-
export type { RuntimePlatform, StorageFactory, StorageFactoryOptions, GlobalStorageFactoryOptions, } from "../platform/types.js";
|
|
13
|
+
export type { RuntimePlatform, StorageFactory, StorageFactoryOptions, GlobalStorageFactoryOptions, InvocationTargetResolution, InvocationCallerInfo, InvocationTargetResolver, } from "../platform/types.js";
|
|
14
14
|
export { type Clock, SystemClock, } from "../storage/clock.js";
|
|
15
15
|
export type { ModuleLoader } from "../platform/module-loader.js";
|
|
16
16
|
export { InMemoryEventSink, type EventSink, } from "../runtime/events.js";
|
package/dist/package/runtime.js
CHANGED
|
@@ -5,7 +5,8 @@ import { createNodePlatform } from "../platform/index.js";
|
|
|
5
5
|
import { findProjectRoot, loadProjectConfig } from "../project/loader.js";
|
|
6
6
|
import { RuntimeConfig } from "../runtime/context.js";
|
|
7
7
|
import { normalizeActionCollection } from "../runtime/action-collection.js";
|
|
8
|
-
import {
|
|
8
|
+
import { redactConfigValue, sanitizeConfigDefinitions } from "../storage/mask.js";
|
|
9
|
+
import { assertConcreteConfigKey, assertValidConfigDeclarations, isConfigTemplate, resolveConfigDeclaration, validateConfigValue, } from "../project/config-declarations.js";
|
|
9
10
|
import { decodeStateKey, SqliteRuntimeStorage } from "../storage/sqlite.js";
|
|
10
11
|
import { createPackageIdentity } from "../runtime/identity.js";
|
|
11
12
|
import { ACTION_NOT_FOUND, ActionDockError, CAPABILITY_UNAVAILABLE, INVALID_ARGUMENT, INVOCATION_UNSUPPORTED, NOT_FOUND, SERVICE_CLOSED, } from "../errors.js";
|
|
@@ -127,6 +128,9 @@ export class DefaultPackageRuntime {
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
this.packageRoot = packageRoot;
|
|
131
|
+
if (projectConfig.config !== undefined) {
|
|
132
|
+
assertValidConfigDeclarations(projectConfig.config);
|
|
133
|
+
}
|
|
130
134
|
this.projectConfig = projectConfig;
|
|
131
135
|
// 兼容未在公开选项契约中显式声明的包实例标识与代系标识内部参数透传
|
|
132
136
|
this.identity = options.identity || createPackageIdentity({
|
|
@@ -679,9 +683,18 @@ export class DefaultPackageRuntime {
|
|
|
679
683
|
}
|
|
680
684
|
async listConfig() {
|
|
681
685
|
this.assertOpen();
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
const
|
|
686
|
+
const packageStorage = await this.prepareStorageAsync();
|
|
687
|
+
const globalStorage = await this.ensureGlobalStorage();
|
|
688
|
+
const declaredKeys = Object.keys(this.projectConfig.config || {}).filter((k) => !isConfigTemplate(k));
|
|
689
|
+
const packageStoredKeys = Object.keys(packageStorage.listConfig());
|
|
690
|
+
const globalStoredKeys = Object.keys(globalStorage.listConfig());
|
|
691
|
+
const overrideKeys = Object.keys(this.options.configOverrides || {});
|
|
692
|
+
const allKeys = Array.from(new Set([
|
|
693
|
+
...declaredKeys,
|
|
694
|
+
...packageStoredKeys,
|
|
695
|
+
...globalStoredKeys,
|
|
696
|
+
...overrideKeys,
|
|
697
|
+
])).filter((k) => !isConfigTemplate(k));
|
|
685
698
|
const views = [];
|
|
686
699
|
for (const key of allKeys) {
|
|
687
700
|
const item = await this.getConfig(key);
|
|
@@ -693,27 +706,33 @@ export class DefaultPackageRuntime {
|
|
|
693
706
|
}
|
|
694
707
|
async getConfig(key) {
|
|
695
708
|
this.assertOpen();
|
|
696
|
-
|
|
697
|
-
const
|
|
709
|
+
assertConcreteConfigKey(key);
|
|
710
|
+
const decl = resolveConfigDeclaration(this.projectConfig.config, key);
|
|
698
711
|
// 委托 RuntimeConfig 五层优先级链单一事实源,避免重复实现解析链
|
|
699
712
|
const resolved = this.ensureRuntimeConfig().describe(key);
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
const configured = resolved.source !== "default";
|
|
713
|
+
// configured 显式判定:仅真实显式来源(override/package/global/env)为 true,
|
|
714
|
+
// default 与整链未命中(source null)均为 false,不得依赖与 "default" 的不等比较
|
|
715
|
+
const configured = resolved.source !== null && resolved.source !== "default";
|
|
716
|
+
const redacted = redactConfigValue(resolved.value, decl?.item);
|
|
703
717
|
return {
|
|
704
718
|
key,
|
|
705
719
|
configured,
|
|
706
|
-
secret:
|
|
707
|
-
source,
|
|
708
|
-
value:
|
|
720
|
+
secret: redacted.secret,
|
|
721
|
+
source: resolved.source,
|
|
722
|
+
value: redacted.value,
|
|
723
|
+
redactedPaths: redacted.redactedPaths,
|
|
709
724
|
};
|
|
710
725
|
}
|
|
711
726
|
async setConfig(key, value) {
|
|
712
727
|
this.assertOpen();
|
|
728
|
+
assertConcreteConfigKey(key);
|
|
729
|
+
const decl = resolveConfigDeclaration(this.projectConfig.config, key);
|
|
730
|
+
validateConfigValue(key, value, decl?.item);
|
|
713
731
|
await (await this.prepareStorageAsync()).setConfig(key, value);
|
|
714
732
|
}
|
|
715
733
|
async deleteConfig(key) {
|
|
716
734
|
this.assertOpen();
|
|
735
|
+
assertConcreteConfigKey(key);
|
|
717
736
|
return await (await this.prepareStorageAsync()).deleteConfig(key);
|
|
718
737
|
}
|
|
719
738
|
/**
|
package/dist/platform/types.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ProcessAPI } from "@actiondock/sdk";
|
|
1
|
+
import type { ActionInvokeOptions, ProcessAPI } from "@actiondock/sdk";
|
|
2
2
|
import type { Clock } from "../storage/clock.js";
|
|
3
3
|
import type { ModuleLoader } from "./module-loader.js";
|
|
4
4
|
import type { RunsRetentionPolicy, RuntimeStorage } from "../storage/types.js";
|
|
5
5
|
import type { EventSink } from "../runtime/events.js";
|
|
6
|
+
import type { PackageRuntime } from "../package/types.js";
|
|
6
7
|
/**
|
|
7
8
|
* 跨运行时持久化存储工厂配置选项。
|
|
8
9
|
*/
|
|
@@ -42,6 +43,48 @@ export interface StorageFactory {
|
|
|
42
43
|
*/
|
|
43
44
|
createGlobalStorage(options?: GlobalStorageFactoryOptions): RuntimeStorage;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* 动作调用目标解析结果。
|
|
48
|
+
*/
|
|
49
|
+
export interface InvocationTargetResolution {
|
|
50
|
+
/** 目标 PackageRuntime 实例 */
|
|
51
|
+
readonly runtime: PackageRuntime;
|
|
52
|
+
/** 可选调用作用域,用于区分调用目标与环路死锁检测身份 */
|
|
53
|
+
readonly scope?: string;
|
|
54
|
+
/** 是否强制要求调用方显式声明 uses 依赖 */
|
|
55
|
+
readonly requiresExplicitUses?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 动作调用发起方元数据信息。
|
|
59
|
+
*/
|
|
60
|
+
export interface InvocationCallerInfo {
|
|
61
|
+
/** 调用发起方包标识 */
|
|
62
|
+
readonly packageId: string;
|
|
63
|
+
/** 调用发起方 Action 动作标识 */
|
|
64
|
+
readonly actionId?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 运行时平台调用目标解析器端口契约。
|
|
68
|
+
* 允许底层运行时平台在 Host 选择执行目标阶段介入目标解析,提供自定义 PackageRuntime。
|
|
69
|
+
*/
|
|
70
|
+
export interface InvocationTargetResolver {
|
|
71
|
+
/**
|
|
72
|
+
* 解析指定 Action 引用与调用选项对应的目标 PackageRuntime。
|
|
73
|
+
*
|
|
74
|
+
* @param action 规范化 Action 引用元数据
|
|
75
|
+
* @param options 动作调用选项(包含命名 binding 等)
|
|
76
|
+
* @param caller 发起调用的调用方元数据
|
|
77
|
+
* @returns 目标解析结果;返回的 Runtime 由 Host 接管生命周期;未命中返回 undefined 交由默认主链处理
|
|
78
|
+
*/
|
|
79
|
+
resolve(action: {
|
|
80
|
+
packageId: string;
|
|
81
|
+
actionId: string;
|
|
82
|
+
}, options: ActionInvokeOptions | undefined, caller: InvocationCallerInfo): InvocationTargetResolution | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* 清理端口缓存和附属资源,不重复关闭已移交 Host 的 Runtime。
|
|
85
|
+
*/
|
|
86
|
+
close?(): Promise<void>;
|
|
87
|
+
}
|
|
45
88
|
/**
|
|
46
89
|
* 运行时底层环境核心标准契约。
|
|
47
90
|
* 提供统一的运行时契约,屏蔽宿主平台与测试沙箱环境的实现差异。
|
|
@@ -61,4 +104,8 @@ export interface RuntimePlatform {
|
|
|
61
104
|
* 可选执行事件接收器,宿主可注入,缺省由上层默认构造。
|
|
62
105
|
*/
|
|
63
106
|
readonly eventSink?: EventSink;
|
|
107
|
+
/**
|
|
108
|
+
* 可选动作调用目标解析器端口,用于宿主环境自定义动作调用路由(例如测试具名 Mock 模拟目标)。
|
|
109
|
+
*/
|
|
110
|
+
readonly invocationTargets?: InvocationTargetResolver;
|
|
64
111
|
}
|