@actiondock/core 2.10.0 → 2.11.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 +1 -2
- package/dist/catalog/action-catalog.js +3 -33
- package/dist/catalog/discovery.d.ts +0 -3
- package/dist/catalog/discovery.js +4 -4
- package/dist/catalog/graph.d.ts +0 -4
- package/dist/catalog/graph.js +8 -14
- package/dist/{profile/client-actions.d.ts → client/actions.d.ts} +1 -1
- package/dist/{profile/client-actions.js → client/actions.js} +2 -8
- package/dist/{profile/client-config.d.ts → client/config.d.ts} +1 -1
- package/dist/{profile/client-config.js → client/config.js} +1 -1
- package/dist/client/dispatcher.d.ts +4 -0
- package/dist/client/dispatcher.js +21 -0
- package/dist/client/health.d.ts +38 -0
- package/dist/{profile/client-health.js → client/health.js} +9 -6
- package/dist/client/index.d.ts +16 -0
- package/dist/client/index.js +20 -0
- package/dist/{profile/client-playbooks.d.ts → client/playbooks.d.ts} +1 -1
- package/dist/{profile/client-playbooks.js → client/playbooks.js} +1 -1
- package/dist/{profile/client-runs.d.ts → client/runs.d.ts} +4 -2
- package/dist/{profile/client-runs.js → client/runs.js} +7 -1
- package/dist/{profile/client-state.d.ts → client/state.d.ts} +1 -1
- package/dist/{profile/client-state.js → client/state.js} +1 -1
- package/dist/{profile/client-transport.d.ts → client/transport.d.ts} +29 -10
- package/dist/{profile/client-transport.js → client/transport.js} +125 -35
- package/dist/doctor/checks/index.d.ts +1 -1
- package/dist/doctor/checks/index.js +1 -1
- package/dist/doctor/checks/project.js +20 -88
- package/dist/doctor/checks/runtime.d.ts +0 -4
- package/dist/doctor/checks/runtime.js +0 -28
- package/dist/doctor/doctor.js +1 -2
- package/dist/errors.d.ts +53 -3
- package/dist/errors.js +53 -3
- package/dist/execution/service.d.ts +55 -37
- package/dist/execution/service.js +601 -212
- package/dist/execution/types.d.ts +24 -5
- package/dist/host/host.d.ts +42 -22
- package/dist/host/host.js +562 -149
- package/dist/host/routing.js +6 -6
- package/dist/host/types.d.ts +25 -13
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/input/advice.d.ts +0 -44
- package/dist/input/advice.js +7 -141
- package/dist/input/flat-materializer.js +1 -1
- package/dist/input/flat-parser.js +1 -1
- package/dist/input/index.d.ts +0 -1
- package/dist/input/index.js +0 -1
- package/dist/input/input-resolver.js +1 -1
- package/dist/input/validation-mapper.d.ts +1 -1
- package/dist/ipc/diagnostic.d.ts +8 -21
- package/dist/ipc/diagnostic.js +24 -57
- package/dist/ipc/host.js +21 -7
- package/dist/ipc/service.d.ts +14 -0
- package/dist/ipc/service.js +132 -26
- package/dist/ipc/types.d.ts +0 -4
- package/dist/package/index.d.ts +4 -5
- package/dist/package/index.js +1 -2
- package/dist/package/runtime.d.ts +94 -23
- package/dist/package/runtime.js +628 -274
- package/dist/package/static-index.d.ts +3 -6
- package/dist/package/static-index.js +11 -40
- package/dist/package/types.d.ts +19 -4
- package/dist/platform/index.d.ts +2 -1
- package/dist/platform/index.js +2 -1
- package/dist/platform/loader-hook.d.ts +32 -0
- package/dist/platform/loader-hook.js +90 -0
- package/dist/{node → platform}/module-loader.d.ts +13 -0
- package/dist/{node → platform}/module-loader.js +51 -4
- package/dist/platform/node.d.ts +19 -8
- package/dist/platform/node.js +50 -20
- package/dist/platform/types.d.ts +7 -47
- package/dist/process/context-process.d.ts +3 -18
- package/dist/process/context-process.js +3 -44
- package/dist/process/control.d.ts +21 -0
- package/dist/process/control.js +34 -0
- package/dist/process/converters.d.ts +23 -0
- package/dist/process/converters.js +92 -0
- package/dist/process/cursor.js +2 -22
- package/dist/process/{diagnostics-sink.d.ts → diagnostics.d.ts} +0 -7
- package/dist/process/{diagnostics-sink.js → diagnostics.js} +6 -8
- package/dist/process/index.d.ts +1 -3
- package/dist/process/index.js +1 -3
- package/dist/process/input-dispatcher.d.ts +4 -13
- package/dist/process/input-dispatcher.js +48 -69
- package/dist/process/managed-record.d.ts +3 -31
- package/dist/process/metadata-store.d.ts +0 -41
- package/dist/process/metadata-store.js +2 -341
- package/dist/process/process-driver.js +2 -2
- package/dist/process/process-manager.d.ts +8 -50
- package/dist/process/process-manager.js +28 -417
- package/dist/process/quotas.d.ts +14 -0
- package/dist/process/quotas.js +29 -0
- package/dist/process/run-executor.js +0 -1
- package/dist/process/terminal-output-cache.d.ts +20 -34
- package/dist/process/terminal-output-cache.js +43 -64
- package/dist/profile/index.d.ts +1 -2
- package/dist/profile/index.js +1 -2
- package/dist/profile/manager.d.ts +2 -4
- package/dist/profile/manager.js +10 -40
- package/dist/profile/types.d.ts +1 -17
- package/dist/project/digest.d.ts +5 -1
- package/dist/project/digest.js +81 -219
- package/dist/project/index.d.ts +4 -5
- package/dist/project/index.js +4 -5
- package/dist/project/init.js +2 -2
- package/dist/project/loader.d.ts +3 -3
- package/dist/project/loader.js +72 -50
- package/dist/project/lockfile.js +6 -5
- package/dist/project/manifest.js +5 -4
- package/dist/project/transactions.d.ts +4 -42
- package/dist/project/transactions.js +83 -138
- package/dist/project/types-generator.js +2 -1
- package/dist/project/types.d.ts +2 -0
- package/dist/registry/index.d.ts +0 -1
- package/dist/registry/index.js +0 -1
- package/dist/registry/lock.d.ts +2 -11
- package/dist/registry/lock.js +14 -183
- package/dist/registry/registry.d.ts +1 -6
- package/dist/registry/registry.js +5 -17
- package/dist/runtime/action-registry.d.ts +1 -5
- package/dist/runtime/action-registry.js +0 -12
- package/dist/runtime/context.js +2 -2
- package/dist/runtime/events.d.ts +11 -50
- package/dist/runtime/events.js +88 -368
- package/dist/runtime/index.d.ts +0 -4
- package/dist/runtime/index.js +0 -4
- package/dist/runtime/run-persistence.d.ts +2 -0
- package/dist/runtime/run-persistence.js +2 -1
- package/dist/runtime/standalone-commands.d.ts +18 -0
- package/dist/runtime/standalone-commands.js +466 -0
- package/dist/runtime/standalone-help.d.ts +20 -0
- package/dist/runtime/standalone-help.js +72 -0
- package/dist/runtime/standalone.d.ts +1 -44
- package/dist/runtime/standalone.js +32 -573
- package/dist/schema/validator.d.ts +2 -2
- package/dist/schema/validator.js +2 -9
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +0 -2
- package/dist/server/routes/actions.js +1 -1
- package/dist/server/routes/info.js +1 -1
- package/dist/server/routes/playbooks.js +1 -1
- package/dist/server/routes/runs.js +123 -16
- package/dist/server/routes/state.js +43 -18
- package/dist/server/security.d.ts +1 -1
- package/dist/server/security.js +2 -2
- package/dist/server/server.js +3 -7
- package/dist/service/factory.js +4 -3
- package/dist/service/index.d.ts +1 -1
- package/dist/service/index.js +1 -1
- package/dist/service/remote-errors.d.ts +1 -1
- package/dist/service/remote-errors.js +6 -6
- package/dist/service/remote-polling.d.ts +3 -0
- package/dist/service/remote-polling.js +10 -5
- package/dist/service/remote.d.ts +5 -1
- package/dist/service/remote.js +108 -17
- package/dist/service/sse-stream.js +2 -2
- package/dist/service/types.d.ts +22 -19
- package/dist/service/types.js +1 -24
- package/dist/storage/clock.d.ts +1 -1
- package/dist/storage/clock.js +1 -1
- package/dist/storage/data-dir-lock.d.ts +10 -22
- package/dist/storage/data-dir-lock.js +28 -56
- package/dist/storage/file-lock.d.ts +64 -0
- package/dist/storage/file-lock.js +447 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +5 -4
- package/dist/storage/run-liveness.d.ts +48 -0
- package/dist/storage/run-liveness.js +44 -0
- package/dist/storage/sqlite-config.d.ts +16 -0
- package/dist/storage/sqlite-config.js +59 -0
- package/dist/storage/sqlite-driver.d.ts +3 -1
- package/dist/storage/sqlite-driver.js +14 -4
- package/dist/storage/sqlite-events.d.ts +13 -0
- package/dist/storage/sqlite-events.js +48 -0
- package/dist/storage/sqlite-runs.d.ts +107 -0
- package/dist/storage/sqlite-runs.js +622 -0
- package/dist/storage/sqlite-schema.d.ts +5 -0
- package/dist/storage/sqlite-schema.js +143 -0
- package/dist/storage/sqlite-state.d.ts +33 -0
- package/dist/storage/sqlite-state.js +270 -0
- package/dist/storage/sqlite.d.ts +30 -57
- package/dist/storage/sqlite.js +84 -741
- package/dist/storage/types.d.ts +35 -2
- package/dist/utils/index.d.ts +18 -14
- package/dist/utils/index.js +37 -29
- package/dist/utils/semver.d.ts +18 -0
- package/dist/utils/semver.js +19 -0
- package/dist/utils/warning.d.ts +1 -0
- package/dist/utils/warning.js +52 -0
- package/dist/{json/value-validator.d.ts → value-validator.d.ts} +9 -0
- package/dist/value-validator.js +330 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -12
- package/dist/execution/index.d.ts +0 -2
- package/dist/execution/index.js +0 -2
- package/dist/filter/index.d.ts +0 -1
- package/dist/filter/index.js +0 -1
- package/dist/graph/index.d.ts +0 -4
- package/dist/graph/index.js +0 -4
- package/dist/input/metadata.d.ts +0 -99
- package/dist/input/metadata.js +0 -99
- package/dist/invocation/index.d.ts +0 -2
- package/dist/invocation/index.js +0 -2
- package/dist/ipc/index.d.ts +0 -4
- package/dist/ipc/index.js +0 -4
- package/dist/json/index.d.ts +0 -1
- package/dist/json/index.js +0 -1
- package/dist/json/value-validator.js +0 -330
- package/dist/node/index.d.ts +0 -1
- package/dist/node/index.js +0 -1
- package/dist/platform/node-fs.d.ts +0 -37
- package/dist/platform/node-fs.js +0 -81
- package/dist/process/control-arbiter.d.ts +0 -108
- package/dist/process/control-arbiter.js +0 -250
- package/dist/process/process-executor.d.ts +0 -47
- package/dist/process/process-executor.js +0 -296
- package/dist/profile/client-health.d.ts +0 -14
- package/dist/profile/client-query.d.ts +0 -30
- package/dist/profile/client-query.js +0 -69
- package/dist/profile/client.d.ts +0 -26
- package/dist/profile/client.js +0 -31
- package/dist/project/closure.d.ts +0 -25
- package/dist/project/closure.js +0 -74
- package/dist/registry/store.d.ts +0 -42
- package/dist/registry/store.js +0 -43
- package/dist/runtime/clock.d.ts +0 -7
- package/dist/runtime/clock.js +0 -7
- package/dist/runtime/module-loader.d.ts +0 -2
- package/dist/runtime/module-loader.js +0 -1
- package/dist/runtime/process.d.ts +0 -8
- package/dist/runtime/process.js +0 -1
- package/dist/runtime/runner.d.ts +0 -234
- package/dist/runtime/runner.js +0 -609
- package/dist/schema/index.d.ts +0 -1
- package/dist/schema/index.js +0 -1
- package/dist/server/dispatcher.d.ts +0 -25
- package/dist/server/dispatcher.js +0 -54
- package/dist/service/local.d.ts +0 -27
- package/dist/service/local.js +0 -141
- package/dist/storage/driver.d.ts +0 -10
- package/dist/storage/driver.js +0 -10
- package/dist/storage/lazy.d.ts +0 -8
- package/dist/storage/lazy.js +0 -45
- package/dist/storage/lock-core.d.ts +0 -158
- package/dist/storage/lock-core.js +0 -872
- package/dist/utils/net.d.ts +0 -10
- package/dist/utils/net.js +0 -16
- package/dist/utils/process.d.ts +0 -10
- package/dist/utils/process.js +0 -21
- /package/dist/{filter → utils}/intent.d.ts +0 -0
- /package/dist/{filter → utils}/intent.js +0 -0
|
@@ -2,50 +2,88 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
3
3
|
import { computeDigest } from "../project/digest.js";
|
|
4
4
|
import { loadActions, loadProjectConfig } from "../project/loader.js";
|
|
5
|
+
import { SystemClock } from "../storage/clock.js";
|
|
5
6
|
import { InMemoryEventSink } from "../runtime/events.js";
|
|
6
|
-
import {
|
|
7
|
-
import { ActionDockError, ACTION_NOT_FOUND, EXECUTION_FAILED, IDEMPOTENCY_CONFLICT, RUN_REPOSITORY_UNAVAILABLE, UNHANDLED_EXECUTION_ERROR, describeActionLoadFailure, } from "../errors.js";
|
|
7
|
+
import { ActionDockError, ACTION_CANCELLED, ACTION_FAILED, ACTION_NOT_FOUND, ACTION_TIMEOUT, EXECUTION_CONCURRENCY_LIMIT, EXECUTION_FAILED, IDEMPOTENCY_CONFLICT, INPUT_NOT_JSON, INPUT_VALIDATION_FAILED, INVALID_ARGUMENT, INVOCATION_UNSUPPORTED, OUTPUT_NOT_JSON, OUTPUT_VALIDATION_FAILED, RUN_REPOSITORY_UNAVAILABLE, SERVICE_CLOSED, UNHANDLED_EXECUTION_ERROR, describeActionLoadFailure, } from "../errors.js";
|
|
8
8
|
import { resultStatusToRunStatus } from "../storage/types.js";
|
|
9
|
+
import { createDefaultProcessOwner, } from "../invocation/types.js";
|
|
10
|
+
import { ActionRegistry, isActionDefinitionObject, resolveAnonymousActionId, } from "../runtime/action-registry.js";
|
|
11
|
+
import { buildInitialRunRecord, createRunFinalizer, createRunOrThrow, tryCreateRun, } from "../runtime/run-persistence.js";
|
|
12
|
+
import { createActionContext } from "../runtime/context.js";
|
|
13
|
+
import { validateActionInputValue, validateJsonValue } from "../value-validator.js";
|
|
14
|
+
import { validateSchemaOnly } from "../schema/validator.js";
|
|
9
15
|
/**
|
|
10
|
-
*
|
|
16
|
+
* 在途运行心跳刷新间隔(毫秒)。
|
|
17
|
+
*
|
|
18
|
+
* 必须显著小于存活判定的宽限期(见 run-liveness 的 RUN_LIVENESS_GRACE_MS),
|
|
19
|
+
* 保证心跳正常的宿主绝不被误判过期;间隔内崩溃由进程探测兜底,
|
|
20
|
+
* 心跳只服务无进程标识的遗留记录收敛判定。
|
|
21
|
+
*/
|
|
22
|
+
export const RUN_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
23
|
+
/**
|
|
24
|
+
* 将输入值校验结果映射为标准运行时错误(单一事实源)。
|
|
25
|
+
*/
|
|
26
|
+
function buildInputValidationError(targetActionId, check) {
|
|
27
|
+
if (check.kind === "json-value") {
|
|
28
|
+
return {
|
|
29
|
+
code: INPUT_NOT_JSON,
|
|
30
|
+
message: `Input validation failed for action '${targetActionId}': ${check.reason}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
code: INPUT_VALIDATION_FAILED,
|
|
35
|
+
message: `Input validation failed for action '${targetActionId}': ${check.reason}`,
|
|
36
|
+
details: [check.reason],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 统一执行引擎(融合原协调服务与运行器)。
|
|
41
|
+
* 消除双层 AbortController 与重复参数透传,收敛唯一生命周期与存储落库。
|
|
11
42
|
*/
|
|
12
43
|
export class DefaultExecutionService {
|
|
13
44
|
identity;
|
|
14
45
|
packageId;
|
|
46
|
+
packageInstanceId;
|
|
47
|
+
generationId;
|
|
48
|
+
hostSessionId;
|
|
49
|
+
/** 宿主进程标识:落库到运行记录供跨进程存活判定(并发打开同一数据目录时不误收割) */
|
|
50
|
+
hostPid;
|
|
51
|
+
eventSink;
|
|
15
52
|
storage;
|
|
53
|
+
globalStorage;
|
|
16
54
|
projectConfig;
|
|
17
55
|
projectRoot;
|
|
56
|
+
configOverrides;
|
|
18
57
|
moduleLoader;
|
|
19
|
-
eventSink;
|
|
20
|
-
packageInstanceId;
|
|
21
|
-
generationId;
|
|
22
58
|
maxActiveRuns;
|
|
23
59
|
ownerId;
|
|
24
|
-
|
|
25
|
-
_runner;
|
|
60
|
+
actionResolver;
|
|
26
61
|
logger;
|
|
27
62
|
clock;
|
|
28
63
|
process;
|
|
29
|
-
actionResolver;
|
|
30
|
-
activeRuns = new Map();
|
|
31
|
-
globalStorage;
|
|
32
64
|
actionInvoker;
|
|
65
|
+
registry;
|
|
66
|
+
activeRuns = new Map();
|
|
67
|
+
/** 在途运行心跳定时器:周期性刷新心跳时间戳,支撑无进程标识判定路径的兜底收敛 */
|
|
68
|
+
heartbeatTimer;
|
|
33
69
|
isClosing = false;
|
|
34
70
|
reservedSlots = 0;
|
|
35
71
|
constructor(options) {
|
|
36
72
|
if (!options.identity) {
|
|
37
|
-
throw new
|
|
73
|
+
throw new ActionDockError(INVALID_ARGUMENT, "DefaultExecutionService requires 'identity' PackageIdentity option");
|
|
38
74
|
}
|
|
39
75
|
if (!options.storage) {
|
|
40
|
-
throw new
|
|
76
|
+
throw new ActionDockError(INVALID_ARGUMENT, "DefaultExecutionService requires 'storage' option");
|
|
41
77
|
}
|
|
42
78
|
this.identity = options.identity;
|
|
43
79
|
this.packageId = this.identity.id;
|
|
44
80
|
this.packageInstanceId = this.identity.instanceId;
|
|
45
81
|
this.generationId = this.identity.generation;
|
|
46
82
|
this.hostSessionId = options.hostSessionId;
|
|
83
|
+
this.hostPid = process.pid;
|
|
47
84
|
this.projectConfig = options.projectConfig;
|
|
48
85
|
this.projectRoot = options.projectRoot;
|
|
86
|
+
this.configOverrides = options.configOverrides || {};
|
|
49
87
|
this.moduleLoader = options.moduleLoader;
|
|
50
88
|
this.eventSink = options.eventSink || new InMemoryEventSink();
|
|
51
89
|
this.maxActiveRuns = options.maxActiveRuns || 32;
|
|
@@ -55,65 +93,58 @@ export class DefaultExecutionService {
|
|
|
55
93
|
this.actionInvoker = options.actionInvoker;
|
|
56
94
|
this.storage = options.storage;
|
|
57
95
|
this.globalStorage = options.globalStorage;
|
|
58
|
-
this.clock = options.clock;
|
|
96
|
+
this.clock = options.clock ?? new SystemClock();
|
|
59
97
|
this.process = options.process;
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
customHome: options.customHome,
|
|
73
|
-
actionInvoker: this.actionInvoker,
|
|
74
|
-
});
|
|
98
|
+
this.registry = new ActionRegistry(options.actions);
|
|
99
|
+
}
|
|
100
|
+
/** 获取运行底层存储实例 */
|
|
101
|
+
getStorage() {
|
|
102
|
+
return this.storage;
|
|
103
|
+
}
|
|
104
|
+
/** 兼容性引用,执行引擎即执行器自身 */
|
|
105
|
+
get _runner() {
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
get activeRunsCount() {
|
|
109
|
+
return this.activeRuns.size + this.reservedSlots;
|
|
75
110
|
}
|
|
76
111
|
setActionInvoker(invoker) {
|
|
77
112
|
this.actionInvoker = invoker;
|
|
78
|
-
this._runner.setActionInvoker(invoker);
|
|
79
113
|
}
|
|
80
114
|
registerAction(idOrAction, actionDef) {
|
|
81
115
|
if (typeof idOrAction === "string") {
|
|
82
|
-
this.
|
|
116
|
+
this.registry.registerAction(idOrAction, actionDef);
|
|
83
117
|
}
|
|
84
118
|
else {
|
|
85
|
-
this.
|
|
119
|
+
this.registry.registerAction(idOrAction);
|
|
86
120
|
}
|
|
87
121
|
}
|
|
88
122
|
getAction(id) {
|
|
89
|
-
return this.
|
|
123
|
+
return this.registry.getAction(id);
|
|
90
124
|
}
|
|
91
125
|
listActions() {
|
|
92
|
-
return this.
|
|
126
|
+
return this.registry.listActions();
|
|
93
127
|
}
|
|
94
128
|
getActiveHandle(runId) {
|
|
95
|
-
return this.activeRuns.get(runId)?.
|
|
129
|
+
return this.activeRuns.get(runId)?.ticket;
|
|
96
130
|
}
|
|
97
|
-
async execute(ref, input,
|
|
98
|
-
const ticket = await this.start(ref, input,
|
|
131
|
+
async execute(ref, input = {}, contextOrOptions) {
|
|
132
|
+
const ticket = await this.start(ref, input, contextOrOptions);
|
|
99
133
|
if (!ticket.result) {
|
|
100
|
-
throw new
|
|
134
|
+
throw new ActionDockError(EXECUTION_FAILED, `Execution ticket for run '${ticket.runId}' has no result Promise`);
|
|
101
135
|
}
|
|
102
136
|
return ticket.result;
|
|
103
137
|
}
|
|
104
|
-
async run(ref, input,
|
|
105
|
-
return this.execute(ref, input,
|
|
106
|
-
}
|
|
107
|
-
get activeRunsCount() {
|
|
108
|
-
return this.activeRuns.size + this.reservedSlots;
|
|
138
|
+
async run(ref, input = {}, contextOrOptions) {
|
|
139
|
+
return this.execute(ref, input, contextOrOptions);
|
|
109
140
|
}
|
|
110
|
-
async start(ref, input,
|
|
141
|
+
async start(ref, input = {}, contextOrOptions) {
|
|
111
142
|
if (this.isClosing) {
|
|
112
|
-
throw new
|
|
143
|
+
throw new ActionDockError(SERVICE_CLOSED, "ExecutionService is closing: new tasks rejected");
|
|
113
144
|
}
|
|
114
145
|
const currentTotal = this.activeRuns.size + this.reservedSlots;
|
|
115
146
|
if (currentTotal >= this.maxActiveRuns) {
|
|
116
|
-
throw new
|
|
147
|
+
throw new ActionDockError(EXECUTION_CONCURRENCY_LIMIT, `Concurrency limit reached: ${currentTotal}/${this.maxActiveRuns} active runs`);
|
|
117
148
|
}
|
|
118
149
|
this.reservedSlots++;
|
|
119
150
|
let hasReservedSlot = true;
|
|
@@ -125,16 +156,27 @@ export class DefaultExecutionService {
|
|
|
125
156
|
};
|
|
126
157
|
try {
|
|
127
158
|
let parsedRef;
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
let targetActionDef;
|
|
160
|
+
if (isActionDefinitionObject(ref)) {
|
|
161
|
+
targetActionDef = ref;
|
|
162
|
+
const actObj = ref;
|
|
163
|
+
const targetActionId = actObj.id || resolveAnonymousActionId(this.registry.map, ref);
|
|
164
|
+
this.registry.registerAction(targetActionId, ref);
|
|
165
|
+
parsedRef = { packageId: this.packageId, actionId: targetActionId };
|
|
130
166
|
}
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
else {
|
|
168
|
+
try {
|
|
169
|
+
parsedRef = parseActionRef(ref);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
parsedRef = typeof ref === "object" ? ref : { actionId: ref };
|
|
173
|
+
}
|
|
133
174
|
}
|
|
134
175
|
const targetActionId = parsedRef.actionId;
|
|
135
176
|
const targetPackageId = parsedRef.packageId || this.packageId;
|
|
136
177
|
const actionRef = `${targetPackageId}/${targetActionId}`;
|
|
137
178
|
const effectiveClock = this.clock;
|
|
179
|
+
const context = this.normalizeContext(contextOrOptions);
|
|
138
180
|
// requestId 幂等检查与去重处理
|
|
139
181
|
const gateResult = await this.checkIdempotencyGate(input, context, actionRef, effectiveClock);
|
|
140
182
|
if (gateResult.ticket) {
|
|
@@ -142,7 +184,9 @@ export class DefaultExecutionService {
|
|
|
142
184
|
return gateResult.ticket;
|
|
143
185
|
}
|
|
144
186
|
const designatedRunId = gateResult.designatedRunId;
|
|
145
|
-
const target =
|
|
187
|
+
const target = targetActionDef
|
|
188
|
+
? { action: targetActionDef }
|
|
189
|
+
: await this.resolveExecutionTarget(parsedRef, targetPackageId, targetActionId);
|
|
146
190
|
if (!target.action) {
|
|
147
191
|
releaseSlot();
|
|
148
192
|
return this.failTicketForMissingAction({
|
|
@@ -155,77 +199,358 @@ export class DefaultExecutionService {
|
|
|
155
199
|
effectiveClock,
|
|
156
200
|
});
|
|
157
201
|
}
|
|
202
|
+
const currentAction = target.action;
|
|
158
203
|
if (this.isClosing) {
|
|
159
|
-
throw new
|
|
204
|
+
throw new ActionDockError(SERVICE_CLOSED, "ExecutionService is closing: new tasks rejected");
|
|
205
|
+
}
|
|
206
|
+
const runId = designatedRunId || context.runId || crypto.randomUUID();
|
|
207
|
+
const rootRunId = context.rootRunId || context.parentRunId || runId;
|
|
208
|
+
const bridge = this.createEventBridge({ runId, context, effectiveClock });
|
|
209
|
+
// 输入参数 JSON 格式与合法性校验
|
|
210
|
+
const inputCheck = validateActionInputValue(input);
|
|
211
|
+
if (!inputCheck.valid) {
|
|
212
|
+
releaseSlot();
|
|
213
|
+
const error = buildInputValidationError(targetActionId, inputCheck);
|
|
214
|
+
tryCreateRun(this.storage, buildInitialRunRecord(this.buildPersistenceInput({
|
|
215
|
+
runId,
|
|
216
|
+
rootRunId,
|
|
217
|
+
context,
|
|
218
|
+
targetPackageId,
|
|
219
|
+
targetActionId,
|
|
220
|
+
input,
|
|
221
|
+
effectiveClock,
|
|
222
|
+
}), "failed", error));
|
|
223
|
+
bridge.emitEvent({ type: "status", status: "failed" });
|
|
224
|
+
bridge.emitEvent({ type: "finish", result: { ok: false, runId, error } });
|
|
225
|
+
return {
|
|
226
|
+
runId,
|
|
227
|
+
status: "failed",
|
|
228
|
+
result: Promise.resolve({ ok: false, runId, error }),
|
|
229
|
+
cancel: () => false,
|
|
230
|
+
};
|
|
160
231
|
}
|
|
232
|
+
// 输入 Schema 校验
|
|
233
|
+
const schemaError = this.checkActionInputSchema(currentAction, targetActionId, input);
|
|
234
|
+
if (schemaError) {
|
|
235
|
+
releaseSlot();
|
|
236
|
+
tryCreateRun(this.storage, buildInitialRunRecord(this.buildPersistenceInput({
|
|
237
|
+
runId,
|
|
238
|
+
rootRunId,
|
|
239
|
+
context,
|
|
240
|
+
targetPackageId,
|
|
241
|
+
targetActionId,
|
|
242
|
+
input,
|
|
243
|
+
effectiveClock,
|
|
244
|
+
}), "failed", schemaError));
|
|
245
|
+
bridge.emitEvent({ type: "status", status: "failed" });
|
|
246
|
+
bridge.emitEvent({ type: "finish", result: { ok: false, runId, error: schemaError } });
|
|
247
|
+
return {
|
|
248
|
+
runId,
|
|
249
|
+
status: "failed",
|
|
250
|
+
result: Promise.resolve({ ok: false, runId, error: schemaError }),
|
|
251
|
+
cancel: () => false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// 持久化 running 初始记录
|
|
255
|
+
createRunOrThrow(this.storage, buildInitialRunRecord(this.buildPersistenceInput({
|
|
256
|
+
runId,
|
|
257
|
+
rootRunId,
|
|
258
|
+
context,
|
|
259
|
+
targetPackageId,
|
|
260
|
+
targetActionId,
|
|
261
|
+
input,
|
|
262
|
+
effectiveClock,
|
|
263
|
+
}), "running"));
|
|
264
|
+
const finalizer = createRunFinalizer(this.storage, runId);
|
|
265
|
+
// 调用栈快照跟踪
|
|
266
|
+
const callStack = context.callStack ? [...context.callStack] : [];
|
|
267
|
+
const callKey = targetPackageId ? `${targetPackageId}/${targetActionId}` : targetActionId;
|
|
268
|
+
const lastStackItem = callStack[callStack.length - 1];
|
|
269
|
+
const isAlreadyAtTop = lastStackItem === callKey ||
|
|
270
|
+
(targetActionId && lastStackItem === targetActionId) ||
|
|
271
|
+
(targetPackageId && lastStackItem === `${targetPackageId}/${targetActionId}`);
|
|
272
|
+
if (!isAlreadyAtTop) {
|
|
273
|
+
callStack.push(callKey);
|
|
274
|
+
}
|
|
275
|
+
// 单一 AbortController 控制器
|
|
161
276
|
const controller = new AbortController();
|
|
162
277
|
let onAbort;
|
|
163
|
-
if (context.signal
|
|
278
|
+
if (context.signal) {
|
|
164
279
|
if (context.signal.aborted) {
|
|
165
280
|
controller.abort(context.signal.reason);
|
|
166
281
|
}
|
|
167
282
|
else {
|
|
168
283
|
onAbort = () => controller.abort(context.signal?.reason);
|
|
169
284
|
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
285
|
+
finalizer.signal = context.signal;
|
|
286
|
+
finalizer.onAbort = onAbort;
|
|
170
287
|
}
|
|
171
288
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
289
|
+
if (typeof context.timeoutMs === "number" && context.timeoutMs > 0) {
|
|
290
|
+
finalizer.startTimeout(controller, context.timeoutMs);
|
|
291
|
+
}
|
|
292
|
+
const effectiveOwner = context.owner ||
|
|
293
|
+
createDefaultProcessOwner({
|
|
294
|
+
tenantId: context.tenantId,
|
|
295
|
+
principalId: context.principalId,
|
|
296
|
+
packageInstanceId: this.packageInstanceId,
|
|
297
|
+
generationId: this.generationId,
|
|
298
|
+
});
|
|
299
|
+
const effectiveProcess = context.process || this.process;
|
|
300
|
+
const actionCtx = createActionContext({
|
|
301
|
+
actionId: targetActionId,
|
|
302
|
+
storage: this.storage,
|
|
303
|
+
globalStorage: this.globalStorage,
|
|
304
|
+
overrides: { ...this.configOverrides, ...(context.config || {}) },
|
|
305
|
+
projectConfig: this.projectConfig,
|
|
176
306
|
runId,
|
|
177
|
-
rootRunId
|
|
307
|
+
rootRunId,
|
|
178
308
|
parentRunId: context.parentRunId,
|
|
179
|
-
callStack: context.callStack ? [...context.callStack] : undefined,
|
|
180
|
-
hostSessionId: context.hostSessionId || this.hostSessionId,
|
|
181
|
-
maxCallDepth: context.maxCallDepth,
|
|
182
|
-
configOverrides: context.config,
|
|
183
309
|
signal: controller.signal,
|
|
184
|
-
|
|
310
|
+
process: effectiveProcess,
|
|
311
|
+
owner: effectiveOwner,
|
|
185
312
|
progress: bridge.progressReporter,
|
|
186
313
|
logger: bridge.executionLogger,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
314
|
+
onActionInvoke: (childAction, childInput, parentRunId) => this.invokeChildAction({
|
|
315
|
+
controller,
|
|
316
|
+
rootRunId,
|
|
317
|
+
childAction,
|
|
318
|
+
childInput,
|
|
319
|
+
parentRunId: parentRunId || runId,
|
|
320
|
+
currentActionId: targetActionId,
|
|
321
|
+
currentAction,
|
|
322
|
+
callStack,
|
|
323
|
+
context,
|
|
324
|
+
effectiveProcess,
|
|
325
|
+
effectiveOwner,
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
const executionPromise = this.raceExecution({
|
|
329
|
+
currentAction,
|
|
330
|
+
targetActionId,
|
|
331
|
+
input,
|
|
332
|
+
actionCtx,
|
|
333
|
+
controller,
|
|
334
|
+
finalizer,
|
|
335
|
+
runId,
|
|
336
|
+
context,
|
|
201
337
|
});
|
|
338
|
+
const cancelFn = (reason) => {
|
|
339
|
+
if (finalizer.finalized || controller.signal.aborted) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (context.signal && onAbort) {
|
|
343
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
344
|
+
finalizer.onAbort = undefined;
|
|
345
|
+
}
|
|
346
|
+
controller.abort(new Error(reason || "Action execution was cancelled"));
|
|
347
|
+
return true;
|
|
348
|
+
};
|
|
349
|
+
const ticket = {
|
|
350
|
+
runId,
|
|
351
|
+
status: "running",
|
|
352
|
+
result: executionPromise,
|
|
353
|
+
cancel: cancelFn,
|
|
354
|
+
};
|
|
202
355
|
const activeItem = {
|
|
203
|
-
runId
|
|
204
|
-
handle,
|
|
356
|
+
runId,
|
|
205
357
|
controller,
|
|
206
358
|
status: "running",
|
|
207
|
-
startedAt: (effectiveClock?.now() ??
|
|
359
|
+
startedAt: (effectiveClock?.now() ?? this.clock.now()).toISOString(),
|
|
208
360
|
signal: context.signal,
|
|
209
361
|
onAbort,
|
|
362
|
+
ticket,
|
|
210
363
|
};
|
|
211
|
-
this.activeRuns.set(
|
|
364
|
+
this.activeRuns.set(runId, activeItem);
|
|
212
365
|
releaseSlot();
|
|
366
|
+
this.ensureHeartbeatTimer();
|
|
213
367
|
bridge.emitEvent({ type: "status", status: "running" });
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
status
|
|
218
|
-
|
|
219
|
-
|
|
368
|
+
executionPromise
|
|
369
|
+
.then((result) => {
|
|
370
|
+
const finalStatus = resultStatusToRunStatus(result.ok, result.ok ? undefined : result.error?.code);
|
|
371
|
+
activeItem.status = finalStatus;
|
|
372
|
+
bridge.emitEvent({ type: "status", status: finalStatus });
|
|
373
|
+
bridge.emitEvent({ type: "finish", result });
|
|
374
|
+
})
|
|
375
|
+
.catch((err) => {
|
|
376
|
+
activeItem.status = "failed";
|
|
377
|
+
bridge.emitEvent({ type: "status", status: "failed" });
|
|
378
|
+
bridge.emitEvent({
|
|
379
|
+
type: "finish",
|
|
380
|
+
result: {
|
|
381
|
+
ok: false,
|
|
382
|
+
runId,
|
|
383
|
+
error: {
|
|
384
|
+
code: UNHANDLED_EXECUTION_ERROR,
|
|
385
|
+
message: err?.message || String(err),
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
})
|
|
390
|
+
.finally(async () => {
|
|
391
|
+
if (activeItem.signal && activeItem.onAbort) {
|
|
392
|
+
activeItem.signal.removeEventListener("abort", activeItem.onAbort);
|
|
393
|
+
activeItem.onAbort = undefined;
|
|
394
|
+
}
|
|
395
|
+
this.activeRuns.delete(runId);
|
|
396
|
+
if (actionCtx &&
|
|
397
|
+
actionCtx.process?.runScoped === true &&
|
|
398
|
+
typeof actionCtx.process.dispose === "function") {
|
|
399
|
+
try {
|
|
400
|
+
await actionCtx.process.dispose();
|
|
401
|
+
}
|
|
402
|
+
catch { }
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return ticket;
|
|
220
406
|
}
|
|
221
407
|
catch (err) {
|
|
222
408
|
releaseSlot();
|
|
223
409
|
throw err;
|
|
224
410
|
}
|
|
225
411
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
412
|
+
normalizeContext(contextOrOptions) {
|
|
413
|
+
if (contextOrOptions && contextOrOptions.package) {
|
|
414
|
+
return contextOrOptions;
|
|
415
|
+
}
|
|
416
|
+
const opts = (contextOrOptions || {});
|
|
417
|
+
return {
|
|
418
|
+
runId: opts.runId,
|
|
419
|
+
rootRunId: opts.rootRunId,
|
|
420
|
+
parentRunId: opts.parentRunId,
|
|
421
|
+
callStack: opts.callStack ? [...opts.callStack] : [],
|
|
422
|
+
package: opts.package || this.identity,
|
|
423
|
+
signal: opts.signal,
|
|
424
|
+
timeoutMs: opts.timeoutMs,
|
|
425
|
+
maxCallDepth: opts.maxCallDepth,
|
|
426
|
+
config: opts.config || opts.configOverrides,
|
|
427
|
+
tenantId: opts.tenantId || opts.owner?.tenantId,
|
|
428
|
+
principalId: opts.principalId || opts.ownerId || opts.owner?.principalId,
|
|
429
|
+
hostSessionId: opts.hostSessionId || this.hostSessionId,
|
|
430
|
+
logger: opts.logger,
|
|
431
|
+
progress: opts.progress,
|
|
432
|
+
process: opts.process || this.process,
|
|
433
|
+
owner: opts.owner,
|
|
434
|
+
requestId: opts.requestId,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async raceExecution(args) {
|
|
438
|
+
const { currentAction, targetActionId, input, actionCtx, controller, finalizer, runId, context } = args;
|
|
439
|
+
const abortPromise = new Promise((_, reject) => {
|
|
440
|
+
const rejectWithReason = () => reject(controller.signal.reason || new Error("Action execution was cancelled"));
|
|
441
|
+
if (controller.signal.aborted) {
|
|
442
|
+
rejectWithReason();
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
controller.signal.addEventListener("abort", rejectWithReason, { once: true });
|
|
446
|
+
finalizer.raceListenerRemovers.push(() => {
|
|
447
|
+
controller.signal.removeEventListener("abort", rejectWithReason);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
try {
|
|
452
|
+
const rawOutput = await Promise.race([
|
|
453
|
+
Promise.resolve().then(() => currentAction.run(input, actionCtx)),
|
|
454
|
+
abortPromise,
|
|
455
|
+
]);
|
|
456
|
+
const outputCheck = validateJsonValue(rawOutput);
|
|
457
|
+
if (!outputCheck.valid) {
|
|
458
|
+
const error = {
|
|
459
|
+
code: OUTPUT_NOT_JSON,
|
|
460
|
+
message: `Output validation failed for action '${targetActionId}': ${outputCheck.reason}`,
|
|
461
|
+
};
|
|
462
|
+
finalizer.finalize("failed", undefined, error);
|
|
463
|
+
return { ok: false, runId, error };
|
|
464
|
+
}
|
|
465
|
+
const outputSchemaError = this.checkActionOutputSchema(currentAction, targetActionId, rawOutput);
|
|
466
|
+
if (outputSchemaError) {
|
|
467
|
+
finalizer.finalize("failed", undefined, outputSchemaError);
|
|
468
|
+
return { ok: false, runId, error: outputSchemaError };
|
|
469
|
+
}
|
|
470
|
+
finalizer.finalize("success", rawOutput);
|
|
471
|
+
if (finalizer.persistError) {
|
|
472
|
+
return { ok: false, runId, error: finalizer.persistError };
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
runId,
|
|
477
|
+
data: rawOutput,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
return this.classifyExecutionError(err, runId, context.timeoutMs, controller, finalizer);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
classifyExecutionError(err, runId, timeoutMs, controller, finalizer) {
|
|
485
|
+
if (finalizer.isTimeout) {
|
|
486
|
+
const error = {
|
|
487
|
+
code: ACTION_TIMEOUT,
|
|
488
|
+
message: `Action exceeded timeout of ${timeoutMs}ms`,
|
|
489
|
+
};
|
|
490
|
+
finalizer.finalize("timed_out", undefined, error);
|
|
491
|
+
return { ok: false, runId, error: finalizer.persistError || error };
|
|
492
|
+
}
|
|
493
|
+
if (controller.signal.aborted) {
|
|
494
|
+
const reason = controller.signal.reason;
|
|
495
|
+
const reasonMsg = reason instanceof Error
|
|
496
|
+
? reason.message
|
|
497
|
+
: typeof reason === "string"
|
|
498
|
+
? reason
|
|
499
|
+
: undefined;
|
|
500
|
+
const error = {
|
|
501
|
+
code: ACTION_CANCELLED,
|
|
502
|
+
message: "Action execution was cancelled",
|
|
503
|
+
details: reasonMsg ? { reason: reasonMsg } : undefined,
|
|
504
|
+
};
|
|
505
|
+
finalizer.finalize("cancelled", undefined, error);
|
|
506
|
+
return { ok: false, runId, error: finalizer.persistError || error };
|
|
507
|
+
}
|
|
508
|
+
const error = {
|
|
509
|
+
code: err?.code || ACTION_FAILED,
|
|
510
|
+
message: err?.message || String(err),
|
|
511
|
+
details: err?.details,
|
|
512
|
+
};
|
|
513
|
+
finalizer.finalize("failed", undefined, error);
|
|
514
|
+
return {
|
|
515
|
+
ok: false,
|
|
516
|
+
runId,
|
|
517
|
+
error: finalizer.persistError || error,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async invokeChildAction(args) {
|
|
521
|
+
const { controller, rootRunId, childAction, childInput, parentRunId, currentActionId, currentAction, callStack, context, effectiveProcess, effectiveOwner, } = args;
|
|
522
|
+
const invoker = this.actionInvoker;
|
|
523
|
+
if (!invoker) {
|
|
524
|
+
throw new ActionDockError(INVOCATION_UNSUPPORTED, "Nested action invocation requires a Host ActionInvoker");
|
|
525
|
+
}
|
|
526
|
+
const childRunId = crypto.randomUUID();
|
|
527
|
+
const invocationContext = {
|
|
528
|
+
runId: childRunId,
|
|
529
|
+
rootRunId,
|
|
530
|
+
parentRunId,
|
|
531
|
+
caller: {
|
|
532
|
+
packageId: this.packageId,
|
|
533
|
+
actionId: currentActionId,
|
|
534
|
+
runId: parentRunId,
|
|
535
|
+
declaredUses: currentAction?.uses ||
|
|
536
|
+
this.projectConfig?.actions?.[currentActionId]?.uses,
|
|
537
|
+
},
|
|
538
|
+
callStack: [...callStack],
|
|
539
|
+
package: this.identity,
|
|
540
|
+
signal: controller.signal,
|
|
541
|
+
timeoutMs: context.timeoutMs,
|
|
542
|
+
maxCallDepth: context.maxCallDepth,
|
|
543
|
+
config: context.config,
|
|
544
|
+
tenantId: effectiveOwner.tenantId,
|
|
545
|
+
principalId: effectiveOwner.principalId,
|
|
546
|
+
hostSessionId: context.hostSessionId || this.hostSessionId,
|
|
547
|
+
logger: context.logger,
|
|
548
|
+
progress: context.progress,
|
|
549
|
+
process: effectiveProcess,
|
|
550
|
+
owner: effectiveOwner,
|
|
551
|
+
};
|
|
552
|
+
return await invoker(childAction, childInput, invocationContext);
|
|
553
|
+
}
|
|
229
554
|
async checkIdempotencyGate(input, context, actionRef, effectiveClock) {
|
|
230
555
|
if (!context.requestId) {
|
|
231
556
|
return {};
|
|
@@ -246,7 +571,7 @@ export class DefaultExecutionService {
|
|
|
246
571
|
requestId: context.requestId,
|
|
247
572
|
inputDigest,
|
|
248
573
|
runId: provisionalRunId,
|
|
249
|
-
createdAt: (effectiveClock?.now() ??
|
|
574
|
+
createdAt: (effectiveClock?.now() ?? this.clock.now()).toISOString(),
|
|
250
575
|
});
|
|
251
576
|
if (idemp.outcome === "conflict") {
|
|
252
577
|
throw new ActionDockError(IDEMPOTENCY_CONFLICT, `Idempotency conflict for requestId '${context.requestId}': input parameters digest mismatch`, {
|
|
@@ -261,11 +586,7 @@ export class DefaultExecutionService {
|
|
|
261
586
|
const active = this.activeRuns.get(existingRunId);
|
|
262
587
|
if (active) {
|
|
263
588
|
return {
|
|
264
|
-
ticket:
|
|
265
|
-
runId: existingRunId,
|
|
266
|
-
status: active.status,
|
|
267
|
-
result: active.handle.result,
|
|
268
|
-
},
|
|
589
|
+
ticket: active.ticket,
|
|
269
590
|
};
|
|
270
591
|
}
|
|
271
592
|
const record = await this.get(existingRunId);
|
|
@@ -285,91 +606,90 @@ export class DefaultExecutionService {
|
|
|
285
606
|
runId: existingRunId,
|
|
286
607
|
status: record.status,
|
|
287
608
|
result: Promise.resolve(execRes),
|
|
609
|
+
cancel: () => false,
|
|
288
610
|
},
|
|
289
611
|
};
|
|
290
612
|
}
|
|
291
613
|
}
|
|
292
614
|
return { designatedRunId: provisionalRunId };
|
|
293
615
|
}
|
|
294
|
-
/**
|
|
295
|
-
* 解析执行目标:确定目标 Action 定义。
|
|
296
|
-
* 铁律 4:Runner 不找 Package,ExecutionService 仅执行本 Package 的 Action。
|
|
297
|
-
*/
|
|
298
616
|
async resolveExecutionTarget(parsedRef, targetPackageId, targetActionId) {
|
|
299
|
-
const runnerToUse = this._runner;
|
|
300
|
-
let resolveError;
|
|
301
617
|
if (targetPackageId && targetPackageId !== this.packageId) {
|
|
302
|
-
resolveError = {
|
|
618
|
+
const resolveError = {
|
|
303
619
|
code: ACTION_NOT_FOUND,
|
|
304
620
|
message: `Package '${this.packageId}' cannot execute action for external package '${targetPackageId}'`,
|
|
621
|
+
details: {
|
|
622
|
+
reason: `Cross-package action '${targetPackageId}/${targetActionId}' cannot be resolved by ActionRunner of package '${this.packageId}'`,
|
|
623
|
+
},
|
|
305
624
|
};
|
|
306
|
-
return {
|
|
625
|
+
return { action: undefined, resolveError };
|
|
307
626
|
}
|
|
308
|
-
let action =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
let config;
|
|
321
|
-
try {
|
|
322
|
-
config = this.projectConfig || loadProjectConfig(this.projectRoot);
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
resolveError = describeActionLoadFailure(err, {
|
|
326
|
-
actionId: targetActionId,
|
|
327
|
-
packageId: this.packageId,
|
|
328
|
-
projectRoot: this.projectRoot,
|
|
329
|
-
});
|
|
330
|
-
return { runner: runnerToUse, action: undefined, resolveError };
|
|
331
|
-
}
|
|
332
|
-
let actionsMap;
|
|
333
|
-
try {
|
|
334
|
-
actionsMap = await loadActions(this.projectRoot, config.actionsDir, {
|
|
335
|
-
loader: this.moduleLoader,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
catch (err) {
|
|
339
|
-
resolveError = describeActionLoadFailure(err, {
|
|
340
|
-
actionId: targetActionId,
|
|
341
|
-
packageId: this.packageId,
|
|
342
|
-
projectRoot: this.projectRoot,
|
|
343
|
-
});
|
|
344
|
-
return { runner: runnerToUse, action: undefined, resolveError };
|
|
345
|
-
}
|
|
346
|
-
const matched = actionsMap.get(targetActionId);
|
|
347
|
-
if (matched) {
|
|
348
|
-
action = matched;
|
|
349
|
-
runnerToUse.registerAction(targetActionId, matched);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
resolveError = {
|
|
353
|
-
code: ACTION_NOT_FOUND,
|
|
354
|
-
message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
|
|
355
|
-
details: resolution.reason ? { reason: resolution.reason } : undefined,
|
|
356
|
-
};
|
|
627
|
+
let action = this.registry.getAction(targetActionId) ||
|
|
628
|
+
this.registry.getAction(`${targetPackageId}/${targetActionId}`);
|
|
629
|
+
if (action) {
|
|
630
|
+
return { action };
|
|
631
|
+
}
|
|
632
|
+
if (this.actionResolver) {
|
|
633
|
+
try {
|
|
634
|
+
const customResolved = await this.actionResolver(targetActionId);
|
|
635
|
+
if (customResolved) {
|
|
636
|
+
this.registry.registerAction(targetActionId, customResolved);
|
|
637
|
+
if (this.packageId) {
|
|
638
|
+
this.registry.registerAction(`${this.packageId}/${targetActionId}`, customResolved);
|
|
357
639
|
}
|
|
640
|
+
return { action: customResolved };
|
|
358
641
|
}
|
|
359
|
-
|
|
360
|
-
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
return {
|
|
645
|
+
action: undefined,
|
|
646
|
+
resolveError: {
|
|
361
647
|
code: ACTION_NOT_FOUND,
|
|
362
648
|
message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
|
|
363
|
-
details:
|
|
364
|
-
}
|
|
365
|
-
}
|
|
649
|
+
details: { reason: err.message },
|
|
650
|
+
},
|
|
651
|
+
};
|
|
366
652
|
}
|
|
367
653
|
}
|
|
368
|
-
|
|
654
|
+
if (this.projectRoot && existsSync(this.projectRoot)) {
|
|
655
|
+
let config;
|
|
656
|
+
try {
|
|
657
|
+
config = this.projectConfig || loadProjectConfig(this.projectRoot);
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
const resolveError = describeActionLoadFailure(err, {
|
|
661
|
+
actionId: targetActionId,
|
|
662
|
+
packageId: this.packageId,
|
|
663
|
+
projectRoot: this.projectRoot,
|
|
664
|
+
});
|
|
665
|
+
return { action: undefined, resolveError };
|
|
666
|
+
}
|
|
667
|
+
let actionsMap;
|
|
668
|
+
try {
|
|
669
|
+
actionsMap = await loadActions(this.projectRoot, config.actionsDir, {
|
|
670
|
+
loader: this.moduleLoader,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
const resolveError = describeActionLoadFailure(err, {
|
|
675
|
+
actionId: targetActionId,
|
|
676
|
+
packageId: this.packageId,
|
|
677
|
+
projectRoot: this.projectRoot,
|
|
678
|
+
});
|
|
679
|
+
return { action: undefined, resolveError };
|
|
680
|
+
}
|
|
681
|
+
const matched = actionsMap.get(targetActionId);
|
|
682
|
+
if (matched) {
|
|
683
|
+
this.registry.registerAction(targetActionId, matched);
|
|
684
|
+
return { action: matched };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const resolveError = {
|
|
688
|
+
code: ACTION_NOT_FOUND,
|
|
689
|
+
message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
|
|
690
|
+
};
|
|
691
|
+
return { action: undefined, resolveError };
|
|
369
692
|
}
|
|
370
|
-
/**
|
|
371
|
-
* 解析指定 Action 定义(供 PackageRuntime 或内部使用)。
|
|
372
|
-
*/
|
|
373
693
|
async resolveAction(ref) {
|
|
374
694
|
let parsedRef;
|
|
375
695
|
try {
|
|
@@ -383,13 +703,10 @@ export class DefaultExecutionService {
|
|
|
383
703
|
const target = await this.resolveExecutionTarget(parsedRef, targetPackageId, targetActionId);
|
|
384
704
|
return target.action;
|
|
385
705
|
}
|
|
386
|
-
/**
|
|
387
|
-
* 目标 Action 缺失时的失败票据:落库 failed 记录并补发 status 与 finish 事件。
|
|
388
|
-
*/
|
|
389
706
|
failTicketForMissingAction(args) {
|
|
390
707
|
const { target, input, context, targetPackageId, targetActionId, designatedRunId, effectiveClock } = args;
|
|
391
708
|
const runId = designatedRunId || context.runId || crypto.randomUUID();
|
|
392
|
-
const now = (effectiveClock?.now() ??
|
|
709
|
+
const now = (effectiveClock?.now() ?? this.clock.now()).toISOString();
|
|
393
710
|
const error = target.resolveError || {
|
|
394
711
|
code: ACTION_NOT_FOUND,
|
|
395
712
|
message: `Action '${targetActionId}' not found in package '${targetPackageId}'`,
|
|
@@ -400,19 +717,20 @@ export class DefaultExecutionService {
|
|
|
400
717
|
rootRunId,
|
|
401
718
|
parentRunId: context.parentRunId,
|
|
402
719
|
packageId: targetPackageId,
|
|
403
|
-
packageInstanceId: context.package
|
|
720
|
+
packageInstanceId: context.package?.instanceId || this.packageInstanceId,
|
|
404
721
|
actionId: targetActionId,
|
|
405
|
-
generationId: context.package
|
|
722
|
+
generationId: context.package?.generation || this.generationId,
|
|
406
723
|
ownerId: this.ownerId,
|
|
407
724
|
hostSessionId: context.hostSessionId || this.hostSessionId,
|
|
725
|
+
hostPid: this.hostPid,
|
|
408
726
|
status: "failed",
|
|
409
|
-
input,
|
|
727
|
+
input: input,
|
|
410
728
|
error,
|
|
411
729
|
startedAt: now,
|
|
412
730
|
finishedAt: now,
|
|
413
731
|
};
|
|
414
732
|
try {
|
|
415
|
-
|
|
733
|
+
this.storage.createRun(initialRun);
|
|
416
734
|
}
|
|
417
735
|
catch (err) {
|
|
418
736
|
throw new ActionDockError(RUN_REPOSITORY_UNAVAILABLE, `RUN_REPOSITORY_UNAVAILABLE: Failed to initialize run record in repository: ${err?.message || String(err)}`, { originalError: err?.message });
|
|
@@ -446,11 +764,66 @@ export class DefaultExecutionService {
|
|
|
446
764
|
runId,
|
|
447
765
|
error,
|
|
448
766
|
}),
|
|
767
|
+
cancel: () => false,
|
|
449
768
|
};
|
|
450
769
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
770
|
+
buildPersistenceInput(args) {
|
|
771
|
+
const { runId, rootRunId, context, targetPackageId, targetActionId, input, effectiveClock } = args;
|
|
772
|
+
const startedAt = effectiveClock?.now().toISOString() ||
|
|
773
|
+
(typeof this.storage.clock?.now === "function"
|
|
774
|
+
? this.storage.clock.now().toISOString()
|
|
775
|
+
: this.clock.now().toISOString());
|
|
776
|
+
return {
|
|
777
|
+
runId,
|
|
778
|
+
rootRunId,
|
|
779
|
+
parentRunId: context.parentRunId,
|
|
780
|
+
ownerId: context.principalId || context.tenantId || this.ownerId,
|
|
781
|
+
hostSessionId: context.hostSessionId || this.hostSessionId,
|
|
782
|
+
hostPid: this.hostPid,
|
|
783
|
+
packageInstanceId: context.package?.instanceId || this.packageInstanceId,
|
|
784
|
+
generationId: context.package?.generation || this.generationId,
|
|
785
|
+
targetPackageId,
|
|
786
|
+
targetActionId,
|
|
787
|
+
startedAt,
|
|
788
|
+
input,
|
|
789
|
+
runnerPackageId: this.packageId,
|
|
790
|
+
runnerPackageInstanceId: this.packageInstanceId,
|
|
791
|
+
runnerGenerationId: this.generationId,
|
|
792
|
+
runnerHostSessionId: this.hostSessionId,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
checkActionInputSchema(action, targetActionId, input) {
|
|
796
|
+
const targetInputSchema = action?.inputSchema !== undefined
|
|
797
|
+
? action.inputSchema
|
|
798
|
+
: this.projectConfig?.actions?.[targetActionId]?.inputSchema;
|
|
799
|
+
if (targetInputSchema !== undefined) {
|
|
800
|
+
const val = validateSchemaOnly(targetInputSchema, input);
|
|
801
|
+
if (!val.valid) {
|
|
802
|
+
return {
|
|
803
|
+
code: INPUT_VALIDATION_FAILED,
|
|
804
|
+
message: `Input schema validation failed for action '${targetActionId}'`,
|
|
805
|
+
details: val.errors,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
checkActionOutputSchema(action, targetActionId, rawOutput) {
|
|
812
|
+
const targetOutputSchema = action?.outputSchema !== undefined
|
|
813
|
+
? action.outputSchema
|
|
814
|
+
: this.projectConfig?.actions?.[targetActionId]?.outputSchema;
|
|
815
|
+
if (targetOutputSchema !== undefined) {
|
|
816
|
+
const outVal = validateSchemaOnly(targetOutputSchema, rawOutput);
|
|
817
|
+
if (!outVal.valid) {
|
|
818
|
+
return {
|
|
819
|
+
code: OUTPUT_VALIDATION_FAILED,
|
|
820
|
+
message: `Output schema validation failed for action '${targetActionId}'`,
|
|
821
|
+
details: outVal.errors,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
454
827
|
createEventBridge(args) {
|
|
455
828
|
const { runId, context, effectiveClock } = args;
|
|
456
829
|
let sequence = 0;
|
|
@@ -460,7 +833,7 @@ export class DefaultExecutionService {
|
|
|
460
833
|
runId,
|
|
461
834
|
rootRunId: context.rootRunId || context.parentRunId || runId,
|
|
462
835
|
sequence: sequence++,
|
|
463
|
-
timestamp: (effectiveClock?.now() ??
|
|
836
|
+
timestamp: (effectiveClock?.now() ?? this.clock.now()).toISOString(),
|
|
464
837
|
};
|
|
465
838
|
this.eventSink.emit(evt);
|
|
466
839
|
};
|
|
@@ -493,40 +866,6 @@ export class DefaultExecutionService {
|
|
|
493
866
|
};
|
|
494
867
|
return { emitEvent, progressReporter, executionLogger };
|
|
495
868
|
}
|
|
496
|
-
/**
|
|
497
|
-
* 监听执行句柄终态:映射终态状态、补发 status 与 finish 事件并注销活跃运行。
|
|
498
|
-
*/
|
|
499
|
-
watchHandleCompletion(handle, activeItem, bridge) {
|
|
500
|
-
handle.result
|
|
501
|
-
.then((result) => {
|
|
502
|
-
const finalStatus = resultStatusToRunStatus(result.ok, result.ok ? undefined : result.error?.code);
|
|
503
|
-
activeItem.status = finalStatus;
|
|
504
|
-
bridge.emitEvent({ type: "status", status: finalStatus });
|
|
505
|
-
bridge.emitEvent({ type: "finish", result });
|
|
506
|
-
})
|
|
507
|
-
.catch((err) => {
|
|
508
|
-
activeItem.status = "failed";
|
|
509
|
-
bridge.emitEvent({ type: "status", status: "failed" });
|
|
510
|
-
bridge.emitEvent({
|
|
511
|
-
type: "finish",
|
|
512
|
-
result: {
|
|
513
|
-
ok: false,
|
|
514
|
-
runId: handle.runId,
|
|
515
|
-
error: {
|
|
516
|
-
code: UNHANDLED_EXECUTION_ERROR,
|
|
517
|
-
message: err?.message || String(err),
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
});
|
|
521
|
-
})
|
|
522
|
-
.finally(() => {
|
|
523
|
-
if (activeItem.signal && activeItem.onAbort) {
|
|
524
|
-
activeItem.signal.removeEventListener("abort", activeItem.onAbort);
|
|
525
|
-
activeItem.onAbort = undefined;
|
|
526
|
-
}
|
|
527
|
-
this.activeRuns.delete(handle.runId);
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
869
|
async get(runId) {
|
|
531
870
|
return this.storage.getRun(runId) ?? undefined;
|
|
532
871
|
}
|
|
@@ -544,22 +883,69 @@ export class DefaultExecutionService {
|
|
|
544
883
|
active.onAbort = undefined;
|
|
545
884
|
}
|
|
546
885
|
active.controller.abort(new Error(reason || "Execution cancelled"));
|
|
547
|
-
active.handle.cancel(reason);
|
|
548
886
|
return { outcome: "requested", runId };
|
|
549
887
|
}
|
|
550
888
|
events(runId, options = {}) {
|
|
551
889
|
return this.eventSink.subscribe(runId, options);
|
|
552
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* 确保在途运行心跳定时器存在。
|
|
893
|
+
*
|
|
894
|
+
* 存活判定的第一依据是进程探测(运行记录已落库宿主进程标识),
|
|
895
|
+
* 心跳是对遗留记录(无进程标识)与其他协作场景的兜底信号:
|
|
896
|
+
* 持有进程周期性刷新在途记录的心跳时间戳,收割方仅对心跳过期的
|
|
897
|
+
* 遗留记录做宽限期判定,正常运行中的记录绝不会被误判过期。
|
|
898
|
+
* 定时器不阻塞进程退出(unref),无在途运行时不产生任何调度。
|
|
899
|
+
*/
|
|
900
|
+
ensureHeartbeatTimer() {
|
|
901
|
+
if (this.heartbeatTimer)
|
|
902
|
+
return;
|
|
903
|
+
const touch = () => {
|
|
904
|
+
if (this.isClosing || this.activeRuns.size === 0)
|
|
905
|
+
return;
|
|
906
|
+
if (typeof this.storage.touchRunHeartbeat !== "function")
|
|
907
|
+
return;
|
|
908
|
+
try {
|
|
909
|
+
this.storage.touchRunHeartbeat(Array.from(this.activeRuns.keys()));
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
// 心跳失败不影响执行主链路,存活判定仍可依赖进程探测,但必须通过 logger.warn 记录告警
|
|
913
|
+
const errDetail = err instanceof Error ? `${err.message}${err.stack ? `\n${err.stack}` : ""}` : String(err);
|
|
914
|
+
const warnMsg = `[ExecutionService] Failed to touch run heartbeat: ${errDetail}`;
|
|
915
|
+
if (this.logger) {
|
|
916
|
+
this.logger.warn(warnMsg, {
|
|
917
|
+
error: err instanceof Error ? err.message : String(err),
|
|
918
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
console.warn(warnMsg);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
this.heartbeatTimer = setInterval(touch, RUN_HEARTBEAT_INTERVAL_MS);
|
|
927
|
+
if (this.heartbeatTimer.unref) {
|
|
928
|
+
this.heartbeatTimer.unref();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
553
931
|
async close(options = {}) {
|
|
554
932
|
this.isClosing = true;
|
|
933
|
+
if (this.heartbeatTimer) {
|
|
934
|
+
clearInterval(this.heartbeatTimer);
|
|
935
|
+
this.heartbeatTimer = undefined;
|
|
936
|
+
}
|
|
555
937
|
this.reservedSlots = 0;
|
|
556
938
|
const graceMs = options.graceMs ?? 5000;
|
|
557
939
|
for (const [_, active] of this.activeRuns) {
|
|
558
940
|
active.controller.abort(new Error("Service shutting down"));
|
|
559
|
-
active.handle.cancel("Service shutting down");
|
|
560
941
|
}
|
|
561
942
|
if (this.activeRuns.size > 0) {
|
|
562
|
-
const waitPromise = Promise.all(Array.from(this.activeRuns.values()).map((a) => a.
|
|
943
|
+
const waitPromise = Promise.all(Array.from(this.activeRuns.values()).map((a) => a.ticket.result?.catch(() => { })));
|
|
944
|
+
// 此处优雅关闭竞速保留原生 setTimeout 与 clearTimeout 的取舍说明:
|
|
945
|
+
// Clock 接口仅提供 sleep 异步等待能力,未提供注销或取消计划中休眠的句柄;
|
|
946
|
+
// 若直接使用 clock.sleep 与 waitPromise 竞速,在任务提早完成时未触发的休眠 Promise 将在后台悬挂直至超时;
|
|
947
|
+
// 保留原生定时器并在 finally 中即时清理,可确保优雅关闭在任何分支下均不产生定时器泄漏;
|
|
948
|
+
// 且此处仅为服务终结阶段的兜底防护,对确定性测试执行链路无负面干扰。
|
|
563
949
|
let timeoutTimer;
|
|
564
950
|
const timeoutPromise = new Promise((resolve) => {
|
|
565
951
|
timeoutTimer = setTimeout(resolve, graceMs);
|
|
@@ -574,7 +960,10 @@ export class DefaultExecutionService {
|
|
|
574
960
|
}
|
|
575
961
|
}
|
|
576
962
|
this.activeRuns.clear();
|
|
577
|
-
|
|
963
|
+
}
|
|
964
|
+
async dispose() {
|
|
965
|
+
await this.close();
|
|
578
966
|
}
|
|
579
967
|
}
|
|
580
968
|
export { DefaultExecutionService as ExecutionService };
|
|
969
|
+
export { DefaultExecutionService as ActionRunner };
|