@actiondock/core 2.0.12 → 2.2.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 +61 -30
- package/dist/app/app.d.ts +73 -0
- package/dist/app/app.js +700 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/types.d.ts +283 -0
- package/dist/app/types.js +1 -0
- package/dist/catalog/action-index.d.ts +1 -1
- package/dist/catalog/action-index.js +93 -0
- package/dist/catalog/action-resolver.d.ts +2 -2
- package/dist/catalog/action-resolver.js +95 -0
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +5 -0
- package/dist/catalog/location-registry.d.ts +5 -5
- package/dist/catalog/location-registry.js +138 -0
- package/dist/catalog/package-catalog.d.ts +2 -2
- package/dist/catalog/package-catalog.js +76 -0
- package/dist/catalog/types.d.ts +2 -2
- package/dist/catalog/types.js +1 -0
- package/dist/doctor/doctor.d.ts +2 -1
- package/dist/doctor/doctor.js +613 -0
- package/dist/doctor/index.d.ts +2 -2
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/types.js +1 -0
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +123 -0
- package/dist/execution/index.d.ts +2 -2
- package/dist/execution/index.js +2 -0
- package/dist/execution/service.d.ts +46 -41
- package/dist/execution/service.js +633 -0
- package/dist/execution/types.d.ts +59 -2
- package/dist/execution/types.js +1 -0
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +1 -0
- package/dist/export/templates.d.ts +45 -7
- package/dist/export/templates.js +621 -0
- package/dist/filter/index.d.ts +1 -1
- package/dist/filter/index.js +1 -0
- package/dist/filter/intent.js +123 -0
- package/dist/host/host.d.ts +55 -0
- package/dist/host/host.js +740 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.js +2 -0
- package/dist/host/types.d.ts +81 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +21 -16
- package/dist/index.js +20 -8458
- package/dist/ipc/diagnostic.d.ts +65 -0
- package/dist/ipc/diagnostic.js +129 -0
- package/dist/ipc/host.d.ts +7 -0
- package/dist/ipc/host.js +93 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +4 -0
- package/dist/ipc/target.d.ts +56 -0
- package/dist/ipc/target.js +297 -0
- package/dist/ipc/types.d.ts +42 -0
- package/dist/ipc/types.js +1 -0
- package/dist/platform/default.d.ts +32 -0
- package/dist/platform/default.js +40 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/node-fs.d.ts +37 -0
- package/dist/platform/node-fs.js +81 -0
- package/dist/platform/types.d.ts +104 -0
- package/dist/platform/types.js +1 -0
- package/dist/profile/client.d.ts +58 -12
- package/dist/profile/client.js +467 -0
- package/dist/profile/index.d.ts +3 -3
- package/dist/profile/index.js +3 -0
- package/dist/profile/manager.d.ts +2 -2
- package/dist/profile/manager.js +294 -0
- package/dist/profile/types.d.ts +1 -1
- package/dist/profile/types.js +1 -0
- package/dist/project/closure.js +68 -0
- package/dist/project/digest.d.ts +29 -0
- package/dist/project/digest.js +324 -0
- package/dist/project/index.d.ts +10 -5
- package/dist/project/index.js +10 -0
- package/dist/project/init.d.ts +3 -4
- package/dist/project/init.js +175 -0
- package/dist/project/loader.d.ts +21 -27
- package/dist/project/loader.js +484 -0
- package/dist/project/lockfile.d.ts +55 -0
- package/dist/project/lockfile.js +175 -0
- package/dist/project/manifest.d.ts +6 -28
- package/dist/project/manifest.js +220 -0
- package/dist/project/resolver.d.ts +116 -0
- package/dist/project/resolver.js +376 -0
- package/dist/project/transactions.d.ts +103 -0
- package/dist/project/transactions.js +1203 -0
- package/dist/project/types-generator.d.ts +33 -0
- package/dist/project/types-generator.js +168 -0
- package/dist/project/types.d.ts +58 -82
- package/dist/project/types.js +1 -0
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +5 -0
- package/dist/registry/lock.d.ts +27 -0
- package/dist/registry/lock.js +93 -0
- package/dist/registry/registry.d.ts +32 -11
- package/dist/registry/registry.js +611 -0
- package/dist/registry/resolve.d.ts +62 -0
- package/dist/registry/resolve.js +195 -0
- package/dist/registry/scan.d.ts +38 -0
- package/dist/registry/scan.js +135 -0
- package/dist/registry/types.d.ts +1 -1
- package/dist/registry/types.js +1 -0
- package/dist/runtime/action-collection.d.ts +27 -0
- package/dist/runtime/action-collection.js +57 -0
- package/dist/runtime/clock.d.ts +0 -2
- package/dist/runtime/clock.js +17 -0
- package/dist/runtime/context.d.ts +16 -5
- package/dist/runtime/context.js +242 -0
- package/dist/runtime/env.d.ts +3 -1
- package/dist/runtime/env.js +159 -0
- package/dist/runtime/events.d.ts +46 -14
- package/dist/runtime/events.js +604 -0
- package/dist/runtime/execution-manager.d.ts +1 -1
- package/dist/runtime/execution-manager.js +66 -0
- package/dist/runtime/index.d.ts +10 -9
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/module-loader.d.ts +0 -8
- package/dist/runtime/module-loader.js +16 -0
- package/dist/runtime/process.d.ts +2 -4
- package/dist/runtime/process.js +146 -0
- package/dist/runtime/runner.d.ts +117 -10
- package/dist/runtime/runner.js +984 -0
- package/dist/runtime/standalone.d.ts +68 -19
- package/dist/runtime/standalone.js +536 -0
- package/dist/schema/validator.js +116 -0
- package/dist/server/body.js +93 -0
- package/dist/server/index.d.ts +5 -6
- package/dist/server/index.js +5 -0
- package/dist/server/routes/actions.d.ts +2 -2
- package/dist/server/routes/actions.js +298 -0
- package/dist/server/routes/common.d.ts +28 -17
- package/dist/server/routes/common.js +104 -0
- package/dist/server/routes/config.d.ts +2 -1
- package/dist/server/routes/config.js +173 -0
- package/dist/server/routes/doctor.d.ts +2 -2
- package/dist/server/routes/doctor.js +63 -0
- package/dist/server/routes/health.d.ts +2 -2
- package/dist/server/routes/health.js +35 -0
- package/dist/server/routes/index.d.ts +9 -9
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/info.d.ts +4 -2
- package/dist/server/routes/info.js +158 -0
- package/dist/server/routes/playbooks.d.ts +2 -2
- package/dist/server/routes/playbooks.js +134 -0
- package/dist/server/routes/runs.d.ts +2 -2
- package/dist/server/routes/runs.js +343 -0
- package/dist/server/routes/state.d.ts +2 -1
- package/dist/server/routes/state.js +169 -0
- package/dist/server/security.d.ts +11 -4
- package/dist/server/security.js +101 -0
- package/dist/server/server.d.ts +6 -6
- package/dist/server/server.js +355 -0
- package/dist/server/types.d.ts +27 -8
- package/dist/server/types.js +1 -0
- package/dist/storage/data-dir-lock.d.ts +119 -0
- package/dist/storage/data-dir-lock.js +1016 -0
- package/dist/storage/driver.d.ts +4 -10
- package/dist/storage/driver.js +59 -0
- package/dist/storage/index.d.ts +30 -10
- package/dist/storage/index.js +98 -0
- package/dist/storage/lazy.d.ts +8 -0
- package/dist/storage/lazy.js +44 -0
- package/dist/storage/mask.d.ts +12 -5
- package/dist/storage/mask.js +48 -0
- package/dist/storage/params.d.ts +13 -0
- package/dist/storage/params.js +32 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +689 -0
- package/dist/storage/types.d.ts +57 -6
- package/dist/storage/types.js +14 -0
- package/dist/target/index.d.ts +4 -0
- package/dist/target/index.js +4 -0
- package/dist/target/local.d.ts +52 -0
- package/dist/target/local.js +343 -0
- package/dist/target/remote.d.ts +76 -0
- package/dist/target/remote.js +839 -0
- package/dist/target/target.d.ts +6 -0
- package/dist/target/target.js +35 -0
- package/dist/target/types.d.ts +246 -0
- package/dist/target/types.js +34 -0
- package/dist/utils/index.d.ts +22 -2
- package/dist/utils/index.js +180 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +4 -0
- package/package.json +6 -7
- package/dist/build/index.d.ts +0 -1
- package/dist/build/templates.d.ts +0 -19
- package/dist/server/runtime-registry.d.ts +0 -78
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { ActionResolver } from "../catalog/action-resolver.js";
|
|
5
|
+
import { loadActions, loadProjectConfig } from "../project/loader.js";
|
|
6
|
+
import { resolveActionProject, resolvePackageRoot } from "../registry/registry.js";
|
|
7
|
+
import { ACTION_CALL_CYCLE, ACTION_CYCLE_DETECTED, ACTION_FAILED, ACTION_MAX_DEPTH_EXCEEDED, ACTION_NOT_FOUND, ACTION_SUBRUN_LIMIT, ACTION_TIMEOUT, describeActionLoadFailure, INPUT_NOT_JSON, MAX_SUBRUNS_REACHED, OUTPUT_NOT_JSON, RUN_PERSISTENCE_FAILED, RUN_REPOSITORY_UNAVAILABLE, UNDECLARED_ACTION_DEPENDENCY, PACKAGE_NOT_FOUND, INPUT_VALIDATION_FAILED, OUTPUT_VALIDATION_FAILED, ACTION_CANCELLED, } from "../errors.js";
|
|
8
|
+
import { validateSchema } from "../schema/validator.js";
|
|
9
|
+
import { createActionContext, StderrLogger } from "./context.js";
|
|
10
|
+
// 错误码常量已收敛至 src/errors.ts 单一事实源,此处保留 re-export 以维持既有导入路径兼容。
|
|
11
|
+
export { RUN_REPOSITORY_UNAVAILABLE, RUN_PERSISTENCE_FAILED, INPUT_NOT_JSON, OUTPUT_NOT_JSON, ACTION_SUBRUN_LIMIT, MAX_SUBRUNS_REACHED, ACTION_CALL_CYCLE, ACTION_CYCLE_DETECTED, ACTION_MAX_DEPTH_EXCEEDED, } from "../errors.js";
|
|
12
|
+
/**
|
|
13
|
+
* 校验值是否为合法的 JSON 兼容结构,严禁 NaN、Infinity、循环引用及不可序列化类型。
|
|
14
|
+
*/
|
|
15
|
+
export function validateJsonValue(val, seen = new WeakSet()) {
|
|
16
|
+
if (val === null || typeof val === "boolean" || typeof val === "string") {
|
|
17
|
+
return { valid: true };
|
|
18
|
+
}
|
|
19
|
+
if (typeof val === "number") {
|
|
20
|
+
if (!Number.isFinite(val) || Number.isNaN(val)) {
|
|
21
|
+
return { valid: false, reason: `Number is non-finite or NaN (${val})` };
|
|
22
|
+
}
|
|
23
|
+
return { valid: true };
|
|
24
|
+
}
|
|
25
|
+
if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol" || typeof val === "bigint") {
|
|
26
|
+
return { valid: false, reason: `Unsupported JSON type '${typeof val}'` };
|
|
27
|
+
}
|
|
28
|
+
if (typeof val === "object") {
|
|
29
|
+
if (seen.has(val)) {
|
|
30
|
+
return { valid: false, reason: "Circular reference detected in object structure" };
|
|
31
|
+
}
|
|
32
|
+
seen.add(val);
|
|
33
|
+
if (Array.isArray(val)) {
|
|
34
|
+
for (const item of val) {
|
|
35
|
+
const res = validateJsonValue(item, seen);
|
|
36
|
+
if (!res.valid)
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
return { valid: true };
|
|
40
|
+
}
|
|
41
|
+
for (const v of Object.values(val)) {
|
|
42
|
+
if (v !== undefined) {
|
|
43
|
+
const res = validateJsonValue(v, seen);
|
|
44
|
+
if (!res.valid)
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { valid: true };
|
|
49
|
+
}
|
|
50
|
+
return { valid: true };
|
|
51
|
+
}
|
|
52
|
+
const anonymousRunnerActionIds = new WeakMap();
|
|
53
|
+
let anonymousRunnerActionCounter = 0;
|
|
54
|
+
/**
|
|
55
|
+
* ActionDock 核心执行引擎(ActionRunner)。
|
|
56
|
+
*
|
|
57
|
+
* 职责:
|
|
58
|
+
* - 负责 Action 执行的全生命周期管理(校验、隔离、跟踪、落库)。
|
|
59
|
+
* - 入参 (inputSchema) 与出参 (outputSchema) 的 JSON Schema 严格校验。
|
|
60
|
+
* - 嵌套 Action 相互调用的环路检测(Cycle Detection)。
|
|
61
|
+
* - 超时 (Timeout) 与中断信号 (AbortSignal) 竞态控制。
|
|
62
|
+
* - 自动记录并持久化 RunRecord 运行记录至 SQLite 存储。
|
|
63
|
+
*/
|
|
64
|
+
export class ActionRunner {
|
|
65
|
+
packageId;
|
|
66
|
+
storage;
|
|
67
|
+
globalStorage;
|
|
68
|
+
projectRoot;
|
|
69
|
+
projectConfig;
|
|
70
|
+
configOverrides;
|
|
71
|
+
actions;
|
|
72
|
+
clock;
|
|
73
|
+
process;
|
|
74
|
+
platform;
|
|
75
|
+
maxCallDepth;
|
|
76
|
+
maxSubRuns;
|
|
77
|
+
activeSubRuns = 0;
|
|
78
|
+
packageRunners = new Map();
|
|
79
|
+
actionResolver;
|
|
80
|
+
getStorageForPackage;
|
|
81
|
+
packageContextResolver;
|
|
82
|
+
customHome;
|
|
83
|
+
hostSessionId;
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.packageId = options.packageId;
|
|
86
|
+
this.hostSessionId = options.hostSessionId;
|
|
87
|
+
this.projectRoot = options.projectRoot;
|
|
88
|
+
this.projectConfig = options.projectConfig;
|
|
89
|
+
this.configOverrides = options.configOverrides || {};
|
|
90
|
+
this.actions = options.actions || new Map();
|
|
91
|
+
this.customHome = options.customHome;
|
|
92
|
+
this.platform = options.platform;
|
|
93
|
+
if (options.platform) {
|
|
94
|
+
this.clock = options.platform.clock;
|
|
95
|
+
this.process = options.platform.process;
|
|
96
|
+
this.storage = options.storage ?? options.platform.storage.createStorage(this.packageId, {
|
|
97
|
+
projectRoot: this.projectRoot,
|
|
98
|
+
customHome: this.customHome,
|
|
99
|
+
});
|
|
100
|
+
this.globalStorage = options.globalStorage ?? options.platform.storage.createGlobalStorage({
|
|
101
|
+
customHome: this.customHome,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (!options.storage) {
|
|
106
|
+
throw new Error("ActionRunner requires either 'storage' or 'platform' option");
|
|
107
|
+
}
|
|
108
|
+
this.storage = options.storage;
|
|
109
|
+
this.globalStorage = options.globalStorage;
|
|
110
|
+
this.process = options.process;
|
|
111
|
+
this.clock = options.clock;
|
|
112
|
+
}
|
|
113
|
+
this.maxCallDepth = options.maxCallDepth ?? 16;
|
|
114
|
+
this.maxSubRuns = options.maxSubRuns ?? 64;
|
|
115
|
+
this.actionResolver = options.actionResolver;
|
|
116
|
+
this.getStorageForPackage = options.getStorageForPackage;
|
|
117
|
+
this.packageContextResolver = options.packageContextResolver;
|
|
118
|
+
}
|
|
119
|
+
getStorage() {
|
|
120
|
+
return this.storage;
|
|
121
|
+
}
|
|
122
|
+
getPackageRunners() {
|
|
123
|
+
return this.packageRunners;
|
|
124
|
+
}
|
|
125
|
+
registerAction(idOrAction, actionDef) {
|
|
126
|
+
if (typeof idOrAction === "string") {
|
|
127
|
+
if (actionDef) {
|
|
128
|
+
this.actions.set(idOrAction, actionDef);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const actObj = idOrAction;
|
|
133
|
+
const id = actObj.id || "anonymous-action";
|
|
134
|
+
const act = actObj.action || (actObj.run ? actObj : undefined);
|
|
135
|
+
if (act) {
|
|
136
|
+
this.actions.set(id, act);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 根据 ID 检索注册的 Action。
|
|
142
|
+
*/
|
|
143
|
+
getAction(id) {
|
|
144
|
+
return this.actions.get(id);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 动态解析 Action(支持本地注册表、自定义解析器委托与已链接包目录索引检索)。
|
|
148
|
+
*
|
|
149
|
+
* @param actionOrRef Action 定义对象、引用或标识符
|
|
150
|
+
* @returns 解析判别联合结果(found | not_found | load_failed)
|
|
151
|
+
*/
|
|
152
|
+
async resolveAction(actionOrRef) {
|
|
153
|
+
if (typeof actionOrRef === "object" &&
|
|
154
|
+
"run" in actionOrRef &&
|
|
155
|
+
typeof actionOrRef.run === "function") {
|
|
156
|
+
return { status: "found", action: actionOrRef };
|
|
157
|
+
}
|
|
158
|
+
const ref = actionOrRef;
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = ActionResolver.parseRef(ref);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return { status: "not_found", reason: err.message };
|
|
165
|
+
}
|
|
166
|
+
const targetActionId = parsed.actionId;
|
|
167
|
+
const targetPackageId = parsed.packageId;
|
|
168
|
+
// 本地 actions 映射表优先检索
|
|
169
|
+
if (targetPackageId && targetPackageId !== this.packageId) {
|
|
170
|
+
if (this.actions.has(`${targetPackageId}/${targetActionId}`)) {
|
|
171
|
+
return { status: "found", action: this.actions.get(`${targetPackageId}/${targetActionId}`) };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
if (this.actions.has(targetActionId)) {
|
|
176
|
+
return { status: "found", action: this.actions.get(targetActionId) };
|
|
177
|
+
}
|
|
178
|
+
if (this.packageId && this.actions.has(`${this.packageId}/${targetActionId}`)) {
|
|
179
|
+
return { status: "found", action: this.actions.get(`${this.packageId}/${targetActionId}`) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 外部注入的自定义 actionResolver 调度
|
|
183
|
+
if (this.actionResolver) {
|
|
184
|
+
try {
|
|
185
|
+
const customResolved = await this.actionResolver(ref, this.packageId);
|
|
186
|
+
if (customResolved) {
|
|
187
|
+
if (targetPackageId && targetPackageId !== this.packageId) {
|
|
188
|
+
this.actions.set(`${targetPackageId}/${targetActionId}`, customResolved);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.actions.set(targetActionId, customResolved);
|
|
192
|
+
if (this.packageId) {
|
|
193
|
+
this.actions.set(`${this.packageId}/${targetActionId}`, customResolved);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { status: "found", action: customResolved };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
return { status: "not_found", reason: err.message };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 基于全局链接注册表与目录索引的动态寻址与按需加载
|
|
204
|
+
const identifier = targetPackageId
|
|
205
|
+
? `${targetPackageId}/${targetActionId}`
|
|
206
|
+
: targetActionId;
|
|
207
|
+
let resolved;
|
|
208
|
+
try {
|
|
209
|
+
resolved = await resolveActionProject(identifier, this.projectRoot, this.customHome);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
return { status: "not_found", reason: err.message };
|
|
213
|
+
}
|
|
214
|
+
if (!resolved || !existsSync(resolved.projectRoot)) {
|
|
215
|
+
return {
|
|
216
|
+
status: "not_found",
|
|
217
|
+
reason: `Project root not found for action '${identifier}'`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
let config;
|
|
221
|
+
try {
|
|
222
|
+
config = loadProjectConfig(resolved.projectRoot);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
return {
|
|
226
|
+
status: "load_failed",
|
|
227
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
228
|
+
packageId: resolved.packageId,
|
|
229
|
+
projectRoot: resolved.projectRoot,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
let actionsMap;
|
|
233
|
+
try {
|
|
234
|
+
actionsMap = await loadActions(resolved.projectRoot, config.actionsDir, {
|
|
235
|
+
autoInstall: false,
|
|
236
|
+
loader: this.platform?.modules,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
return {
|
|
241
|
+
status: "load_failed",
|
|
242
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
243
|
+
packageId: resolved.packageId,
|
|
244
|
+
projectRoot: resolved.projectRoot,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const matched = actionsMap.get(resolved.actionId);
|
|
248
|
+
if (matched) {
|
|
249
|
+
this.actions.set(`${resolved.packageId}/${resolved.actionId}`, matched);
|
|
250
|
+
// 仅当目标包就是当前项目时才注册短标识符,避免跨包动态载入污染全局短标识符
|
|
251
|
+
if (!targetPackageId || resolved.packageId === this.packageId) {
|
|
252
|
+
this.actions.set(resolved.actionId, matched);
|
|
253
|
+
}
|
|
254
|
+
return { status: "found", action: matched };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
status: "not_found",
|
|
258
|
+
reason: `Action '${resolved.actionId}' not found in package '${resolved.packageId}' (${resolved.projectRoot})`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 获取当前 Runner 已注册的所有 Action 列表。
|
|
263
|
+
*/
|
|
264
|
+
listActions() {
|
|
265
|
+
return Array.from(this.actions.values());
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 注入或更新跨包运行上下文解析委托。
|
|
269
|
+
*/
|
|
270
|
+
setPackageContextResolver(resolver) {
|
|
271
|
+
this.packageContextResolver = resolver;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 跨包运行时解析与获取(确保跨包执行具备独立的配置、存储、状态与 Action 注册表)。
|
|
275
|
+
*/
|
|
276
|
+
async resolveTargetPackageRunner(targetPackageId) {
|
|
277
|
+
if (this.packageRunners.has(targetPackageId)) {
|
|
278
|
+
return this.packageRunners.get(targetPackageId);
|
|
279
|
+
}
|
|
280
|
+
if (this.packageContextResolver) {
|
|
281
|
+
const resolved = await this.packageContextResolver(targetPackageId);
|
|
282
|
+
if (resolved) {
|
|
283
|
+
const runner = new ActionRunner({
|
|
284
|
+
packageId: targetPackageId,
|
|
285
|
+
storage: resolved.storage,
|
|
286
|
+
globalStorage: this.globalStorage,
|
|
287
|
+
projectRoot: resolved.projectRoot,
|
|
288
|
+
projectConfig: resolved.projectConfig,
|
|
289
|
+
actions: resolved.actions,
|
|
290
|
+
process: this.process,
|
|
291
|
+
clock: this.clock,
|
|
292
|
+
platform: this.platform,
|
|
293
|
+
maxCallDepth: this.maxCallDepth,
|
|
294
|
+
maxSubRuns: this.maxSubRuns,
|
|
295
|
+
actionResolver: this.actionResolver,
|
|
296
|
+
getStorageForPackage: this.getStorageForPackage,
|
|
297
|
+
packageContextResolver: this.packageContextResolver,
|
|
298
|
+
});
|
|
299
|
+
this.packageRunners.set(targetPackageId, runner);
|
|
300
|
+
return runner;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const root = resolvePackageRoot(targetPackageId, this.projectRoot, this.customHome);
|
|
304
|
+
if (root && existsSync(root)) {
|
|
305
|
+
const config = loadProjectConfig(root);
|
|
306
|
+
let storage;
|
|
307
|
+
if (this.getStorageForPackage) {
|
|
308
|
+
storage = this.getStorageForPackage(targetPackageId, root);
|
|
309
|
+
}
|
|
310
|
+
else if (this.platform) {
|
|
311
|
+
storage = this.platform.storage.createStorage(targetPackageId, {
|
|
312
|
+
projectRoot: root,
|
|
313
|
+
customHome: this.customHome,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const sqlitePath = config.storage?.sqlitePath || ".actiondock/storage.db";
|
|
318
|
+
const dbPath = join(root, sqlitePath);
|
|
319
|
+
const { SqliteRuntimeStorage } = await import("../storage/sqlite.js");
|
|
320
|
+
storage = new SqliteRuntimeStorage({ dbPath, packageId: targetPackageId, clock: this.clock });
|
|
321
|
+
}
|
|
322
|
+
const actionsMap = await loadActions(root, config.actionsDir, {
|
|
323
|
+
autoInstall: false,
|
|
324
|
+
loader: this.platform?.modules,
|
|
325
|
+
});
|
|
326
|
+
const runner = new ActionRunner({
|
|
327
|
+
packageId: targetPackageId,
|
|
328
|
+
storage,
|
|
329
|
+
globalStorage: this.globalStorage,
|
|
330
|
+
projectRoot: root,
|
|
331
|
+
projectConfig: config,
|
|
332
|
+
actions: actionsMap,
|
|
333
|
+
process: this.process,
|
|
334
|
+
clock: this.clock,
|
|
335
|
+
platform: this.platform,
|
|
336
|
+
maxCallDepth: this.maxCallDepth,
|
|
337
|
+
maxSubRuns: this.maxSubRuns,
|
|
338
|
+
actionResolver: this.actionResolver,
|
|
339
|
+
getStorageForPackage: this.getStorageForPackage,
|
|
340
|
+
packageContextResolver: this.packageContextResolver,
|
|
341
|
+
customHome: this.customHome,
|
|
342
|
+
});
|
|
343
|
+
this.packageRunners.set(targetPackageId, runner);
|
|
344
|
+
return runner;
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 异步启动 Action 的执行并立即返回 ExecutionHandle 句柄。
|
|
350
|
+
*
|
|
351
|
+
* @param actionOrId Action 定义对象、引用或标识符
|
|
352
|
+
* @param input 传递给 Action 的输入数据
|
|
353
|
+
* @param options 执行控制选项(超时、取消信号、父运行 ID 等)
|
|
354
|
+
* @returns 包含 runId、result Promise 和 cancel 方法的执行句柄
|
|
355
|
+
*/
|
|
356
|
+
start(actionOrId, input = {}, options = {}) {
|
|
357
|
+
const runCtx = this.prepareRunContext(actionOrId, input, options);
|
|
358
|
+
const { runId, targetActionId, targetPackageId } = runCtx;
|
|
359
|
+
// 输入参数 JSON 格式与合法性防御校验(拦截 NaN/Infinity/循环引用等非 JSON 类型)
|
|
360
|
+
const inputCheck = validateJsonValue(input);
|
|
361
|
+
if (!inputCheck.valid) {
|
|
362
|
+
const error = {
|
|
363
|
+
code: INPUT_NOT_JSON,
|
|
364
|
+
message: `Input validation failed for action '${targetActionId}': ${inputCheck.reason}`,
|
|
365
|
+
};
|
|
366
|
+
// 安全记录 failed 状态(不可序列化的非法 input 严禁直接写入持久化存储)
|
|
367
|
+
this.tryPersistInitialRun(runCtx, "failed", error);
|
|
368
|
+
return {
|
|
369
|
+
runId,
|
|
370
|
+
result: Promise.resolve({ ok: false, runId, error }),
|
|
371
|
+
cancel: () => false,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
// 始终优先将执行尝试持久化到存储中(确保任意异常与终态都可追溯)
|
|
375
|
+
this.persistInitialRun(runCtx, "running");
|
|
376
|
+
const finalizer = this.createRunFinalizer(runCtx);
|
|
377
|
+
// 调用嵌套深度限制检测 (Max Call Depth Check)
|
|
378
|
+
const depthError = this.checkCallDepth(runCtx.callStack, targetActionId, runCtx.options);
|
|
379
|
+
if (depthError) {
|
|
380
|
+
finalizer.finalize("failed", undefined, depthError);
|
|
381
|
+
return {
|
|
382
|
+
runId,
|
|
383
|
+
result: Promise.resolve({ ok: false, runId, error: depthError }),
|
|
384
|
+
cancel: () => false,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// 环路死锁检测 (Cycle Detection)
|
|
388
|
+
const cycle = this.computeCallKey(runCtx.callStack, targetActionId, targetPackageId);
|
|
389
|
+
if (cycle.error) {
|
|
390
|
+
finalizer.finalize("failed", undefined, cycle.error);
|
|
391
|
+
return {
|
|
392
|
+
runId,
|
|
393
|
+
result: Promise.resolve({ ok: false, runId, error: cycle.error }),
|
|
394
|
+
cancel: () => false,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
runCtx.callStack.push(cycle.callKey);
|
|
398
|
+
// 输入参数 JSON Schema 校验(若 action 已就绪)
|
|
399
|
+
const schemaError = this.checkActionInputSchema(runCtx.action, targetActionId, input);
|
|
400
|
+
if (schemaError) {
|
|
401
|
+
finalizer.finalize("failed", undefined, schemaError);
|
|
402
|
+
return {
|
|
403
|
+
runId,
|
|
404
|
+
result: Promise.resolve({ ok: false, runId, error: schemaError }),
|
|
405
|
+
cancel: () => false,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
// 初始化 AbortController 与超时定时器
|
|
409
|
+
const controller = new AbortController();
|
|
410
|
+
let onAbort;
|
|
411
|
+
if (options.signal) {
|
|
412
|
+
if (options.signal.aborted) {
|
|
413
|
+
controller.abort(options.signal.reason);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
onAbort = () => controller.abort(options.signal?.reason);
|
|
417
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
418
|
+
finalizer.signal = options.signal;
|
|
419
|
+
finalizer.onAbort = onAbort;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (typeof options.timeoutMs === "number" && options.timeoutMs > 0) {
|
|
423
|
+
finalizer.startTimeout(controller, options.timeoutMs);
|
|
424
|
+
}
|
|
425
|
+
// 构建 ActionContext 运行时上下文
|
|
426
|
+
const ctx = this.buildActionContext({
|
|
427
|
+
runCtx,
|
|
428
|
+
controller,
|
|
429
|
+
rootRunId: this.computeRootRunId(runCtx),
|
|
430
|
+
});
|
|
431
|
+
// 执行 Action 业务逻辑并与取消/超时信号进行竞态
|
|
432
|
+
const executionPromise = this.raceExecutionAndAbort({
|
|
433
|
+
runCtx,
|
|
434
|
+
actionOrId,
|
|
435
|
+
input,
|
|
436
|
+
options,
|
|
437
|
+
controller,
|
|
438
|
+
finalizer,
|
|
439
|
+
ctx,
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
runId,
|
|
443
|
+
result: executionPromise,
|
|
444
|
+
cancel: (reason) => {
|
|
445
|
+
if (finalizer.finalized || controller.signal.aborted) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
if (options.signal && onAbort) {
|
|
449
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
450
|
+
finalizer.onAbort = undefined;
|
|
451
|
+
}
|
|
452
|
+
controller.abort(new Error(reason || "Action execution was cancelled"));
|
|
453
|
+
return true;
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 计算根运行标识(优先显式 rootRunId,其次 parentRunId,最后自身 runId)。
|
|
459
|
+
*/
|
|
460
|
+
computeRootRunId(runCtx) {
|
|
461
|
+
return runCtx.options.rootRunId || runCtx.options.parentRunId || runCtx.runId;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* 预解析执行目标并装配运行期共享上下文对象(runId、目标标识、时钟源与调用栈快照)。
|
|
465
|
+
*/
|
|
466
|
+
prepareRunContext(actionOrId, input, options) {
|
|
467
|
+
const runId = options.runId || randomUUID();
|
|
468
|
+
const effectiveClock = options.platform?.clock ?? this.clock;
|
|
469
|
+
const effectiveProcess = options.process || options.platform?.process || this.process;
|
|
470
|
+
const startedAt = effectiveClock?.now().toISOString() ||
|
|
471
|
+
(typeof this.storage.clock?.now === "function"
|
|
472
|
+
? this.storage.clock.now().toISOString()
|
|
473
|
+
: new Date().toISOString());
|
|
474
|
+
const callStack = [...(options.callStack || [])];
|
|
475
|
+
let action;
|
|
476
|
+
let targetActionId;
|
|
477
|
+
let targetPackageId = this.packageId;
|
|
478
|
+
if (typeof actionOrId === "object" &&
|
|
479
|
+
"run" in actionOrId &&
|
|
480
|
+
typeof actionOrId.run === "function") {
|
|
481
|
+
action = actionOrId;
|
|
482
|
+
const actObj = action;
|
|
483
|
+
if (actObj.id) {
|
|
484
|
+
targetActionId = actObj.id;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
targetActionId = this.resolveAnonymousActionId(action);
|
|
488
|
+
}
|
|
489
|
+
this.actions.set(targetActionId, action);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const parsed = ActionResolver.parseRef(actionOrId);
|
|
493
|
+
targetActionId = parsed.actionId;
|
|
494
|
+
if (parsed.packageId) {
|
|
495
|
+
targetPackageId = parsed.packageId;
|
|
496
|
+
}
|
|
497
|
+
if (parsed.packageId && parsed.packageId !== this.packageId) {
|
|
498
|
+
action = this.actions.get(`${parsed.packageId}/${targetActionId}`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
action =
|
|
502
|
+
this.actions.get(targetActionId) ||
|
|
503
|
+
(this.packageId ? this.actions.get(`${this.packageId}/${targetActionId}`) : undefined);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
runId,
|
|
508
|
+
options,
|
|
509
|
+
input,
|
|
510
|
+
action,
|
|
511
|
+
actionOrId,
|
|
512
|
+
targetActionId,
|
|
513
|
+
targetPackageId,
|
|
514
|
+
effectiveProcess,
|
|
515
|
+
startedAt,
|
|
516
|
+
callStack,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 为匿名传入的 Action 定义对象解析或分配稳定标识。
|
|
521
|
+
*/
|
|
522
|
+
resolveAnonymousActionId(action) {
|
|
523
|
+
const actObj = action;
|
|
524
|
+
let foundId;
|
|
525
|
+
for (const [id, a] of this.actions) {
|
|
526
|
+
if (a === action) {
|
|
527
|
+
foundId = id;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (foundId) {
|
|
532
|
+
return foundId;
|
|
533
|
+
}
|
|
534
|
+
let anonId = anonymousRunnerActionIds.get(action);
|
|
535
|
+
if (!anonId) {
|
|
536
|
+
anonymousRunnerActionCounter++;
|
|
537
|
+
anonId = `anonymous-action-${anonymousRunnerActionCounter}`;
|
|
538
|
+
anonymousRunnerActionIds.set(action, anonId);
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
actObj.id = anonId;
|
|
542
|
+
}
|
|
543
|
+
catch { }
|
|
544
|
+
return anonId;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* 尝试将 failed 终态初始记录写入存储(非法输入场景,存储异常静默忽略)。
|
|
548
|
+
*/
|
|
549
|
+
tryPersistInitialRun(runCtx, status, error) {
|
|
550
|
+
const initialRun = this.buildInitialRunRecord(runCtx, status, error);
|
|
551
|
+
try {
|
|
552
|
+
this.storage.createRun(initialRun);
|
|
553
|
+
}
|
|
554
|
+
catch { }
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* 构建 running 初始记录并强制写入存储(存储不可用时抛出 RUN_REPOSITORY_UNAVAILABLE)。
|
|
558
|
+
*/
|
|
559
|
+
persistInitialRun(runCtx, status) {
|
|
560
|
+
const initialRun = this.buildInitialRunRecord(runCtx, status);
|
|
561
|
+
try {
|
|
562
|
+
this.storage.createRun(initialRun);
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
const error = new Error(`RUN_REPOSITORY_UNAVAILABLE: Failed to initialize run record in repository: ${err?.message || String(err)}`);
|
|
566
|
+
error.code = RUN_REPOSITORY_UNAVAILABLE;
|
|
567
|
+
error.details = { originalError: err?.message };
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 依据运行期上下文构造 RunRecord 初始记录。
|
|
573
|
+
*/
|
|
574
|
+
buildInitialRunRecord(runCtx, status, error) {
|
|
575
|
+
const { runId, options, targetPackageId, targetActionId, startedAt } = runCtx;
|
|
576
|
+
const record = {
|
|
577
|
+
id: runId,
|
|
578
|
+
rootRunId: this.computeRootRunId(runCtx),
|
|
579
|
+
parentRunId: options.parentRunId,
|
|
580
|
+
packageId: targetPackageId,
|
|
581
|
+
packageInstanceId: options.packageInstanceId || targetPackageId,
|
|
582
|
+
actionId: targetActionId,
|
|
583
|
+
generationId: options.generationId || "1",
|
|
584
|
+
ownerId: options.ownerId || "local",
|
|
585
|
+
hostSessionId: options.hostSessionId || this.hostSessionId,
|
|
586
|
+
status,
|
|
587
|
+
error,
|
|
588
|
+
startedAt,
|
|
589
|
+
};
|
|
590
|
+
if (status === "running") {
|
|
591
|
+
record.input = runCtx.input;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
record.finishedAt = startedAt;
|
|
595
|
+
}
|
|
596
|
+
return record;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* 创建运行终态收敛器:封装终态去重、超时定时器清理与落库异常捕获。
|
|
600
|
+
*/
|
|
601
|
+
createRunFinalizer(runCtx) {
|
|
602
|
+
const finalizer = {
|
|
603
|
+
finalized: false,
|
|
604
|
+
isTimeout: false,
|
|
605
|
+
persistError: undefined,
|
|
606
|
+
timeoutTimer: undefined,
|
|
607
|
+
controller: undefined,
|
|
608
|
+
signal: undefined,
|
|
609
|
+
onAbort: undefined,
|
|
610
|
+
startTimeout: (controller, timeoutMs) => {
|
|
611
|
+
finalizer.controller = controller;
|
|
612
|
+
finalizer.timeoutTimer = setTimeout(() => {
|
|
613
|
+
finalizer.isTimeout = true;
|
|
614
|
+
finalizer.controller?.abort(new Error(`Action exceeded timeout of ${timeoutMs}ms`));
|
|
615
|
+
}, timeoutMs);
|
|
616
|
+
},
|
|
617
|
+
finalize: (status, output, error) => {
|
|
618
|
+
if (finalizer.timeoutTimer) {
|
|
619
|
+
clearTimeout(finalizer.timeoutTimer);
|
|
620
|
+
finalizer.timeoutTimer = undefined;
|
|
621
|
+
}
|
|
622
|
+
if (finalizer.signal && finalizer.onAbort) {
|
|
623
|
+
finalizer.signal.removeEventListener("abort", finalizer.onAbort);
|
|
624
|
+
finalizer.onAbort = undefined;
|
|
625
|
+
}
|
|
626
|
+
if (finalizer.finalized)
|
|
627
|
+
return;
|
|
628
|
+
finalizer.finalized = true;
|
|
629
|
+
try {
|
|
630
|
+
this.storage.updateRun(runCtx.runId, status, output, error);
|
|
631
|
+
}
|
|
632
|
+
catch (persistErr) {
|
|
633
|
+
finalizer.persistError = {
|
|
634
|
+
code: RUN_PERSISTENCE_FAILED,
|
|
635
|
+
message: `RUN_PERSISTENCE_FAILED: Failed to persist run state: ${persistErr?.message || String(persistErr)}`,
|
|
636
|
+
details: { originalError: persistErr?.message },
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
return finalizer;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* 调用嵌套深度限制检测(超限时返回 ACTION_CALL_CYCLE 错误)。
|
|
645
|
+
*/
|
|
646
|
+
checkCallDepth(callStack, targetActionId, options) {
|
|
647
|
+
// 执行级 options.maxCallDepth 可覆盖构造级默认值,保留原覆盖契约
|
|
648
|
+
const maxDepth = options.maxCallDepth ?? this.maxCallDepth;
|
|
649
|
+
if (callStack.length >= maxDepth) {
|
|
650
|
+
return {
|
|
651
|
+
code: ACTION_CALL_CYCLE,
|
|
652
|
+
message: `Maximum call depth of ${maxDepth} exceeded: ${callStack.join(" -> ")} -> ${targetActionId}`,
|
|
653
|
+
details: { alias: ACTION_MAX_DEPTH_EXCEEDED, reason: "depth_exceeded", maxDepth, callStack: [...callStack] },
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* 计算调用键并执行环路死锁检测(命中时返回携带错误的调用键结果)。
|
|
660
|
+
*/
|
|
661
|
+
computeCallKey(callStack, targetActionId, targetPackageId) {
|
|
662
|
+
const isExternal = Boolean(targetPackageId && targetPackageId !== this.packageId);
|
|
663
|
+
const callKey = isExternal
|
|
664
|
+
? `${targetPackageId}/${targetActionId}`
|
|
665
|
+
: targetActionId;
|
|
666
|
+
const hasCycle = isExternal
|
|
667
|
+
? callStack.includes(callKey)
|
|
668
|
+
: (callStack.includes(callKey) || (this.packageId ? callStack.includes(`${this.packageId}/${targetActionId}`) : false));
|
|
669
|
+
if (hasCycle) {
|
|
670
|
+
return {
|
|
671
|
+
callKey,
|
|
672
|
+
error: {
|
|
673
|
+
code: ACTION_CALL_CYCLE,
|
|
674
|
+
message: `Cycle detected in action invocation: ${callStack.join(" -> ")} -> ${callKey}`,
|
|
675
|
+
details: { alias: ACTION_CYCLE_DETECTED, reason: "cycle_detected", callStack: [...callStack], target: callKey },
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return { callKey };
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* 输入参数 JSON Schema 校验(若 action 已就绪,返回校验错误或 undefined)。
|
|
683
|
+
*/
|
|
684
|
+
checkActionInputSchema(action, targetActionId, input) {
|
|
685
|
+
const targetInputSchema = action?.inputSchema ?? this.projectConfig?.actions?.[targetActionId]?.inputSchema;
|
|
686
|
+
if (targetInputSchema) {
|
|
687
|
+
const val = validateSchema(targetInputSchema, input);
|
|
688
|
+
if (!val.valid) {
|
|
689
|
+
return {
|
|
690
|
+
code: INPUT_VALIDATION_FAILED,
|
|
691
|
+
message: `Input schema validation failed for action '${targetActionId}'`,
|
|
692
|
+
details: val.errors,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* 构建 ActionContext 运行时上下文(内嵌子 Action 调用委托提为私有方法)。
|
|
700
|
+
*/
|
|
701
|
+
buildActionContext(args) {
|
|
702
|
+
const { runCtx, controller, rootRunId } = args;
|
|
703
|
+
const { options, targetActionId, effectiveProcess } = runCtx;
|
|
704
|
+
return createActionContext({
|
|
705
|
+
actionId: targetActionId,
|
|
706
|
+
storage: this.storage,
|
|
707
|
+
globalStorage: this.globalStorage,
|
|
708
|
+
overrides: { ...this.configOverrides, ...(options.configOverrides || {}) },
|
|
709
|
+
projectConfig: this.projectConfig,
|
|
710
|
+
runId: runCtx.runId,
|
|
711
|
+
rootRunId,
|
|
712
|
+
parentRunId: options.parentRunId,
|
|
713
|
+
signal: controller.signal,
|
|
714
|
+
process: effectiveProcess,
|
|
715
|
+
progress: options.progress,
|
|
716
|
+
logger: options.logger || new StderrLogger(targetActionId),
|
|
717
|
+
onActionInvoke: (childAction, childInput, parentRunId) => this.invokeChildAction({
|
|
718
|
+
runCtx,
|
|
719
|
+
controller,
|
|
720
|
+
rootRunId,
|
|
721
|
+
childAction,
|
|
722
|
+
childInput,
|
|
723
|
+
parentRunId,
|
|
724
|
+
}),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* 子 Action 调用委托:并发上限守卫、uses 跨包声明校验与子执行派发。
|
|
729
|
+
*/
|
|
730
|
+
async invokeChildAction(args) {
|
|
731
|
+
const { runCtx, controller, rootRunId, childAction, childInput, parentRunId } = args;
|
|
732
|
+
const { options, targetActionId, effectiveProcess, callStack } = runCtx;
|
|
733
|
+
if (this.activeSubRuns >= this.maxSubRuns) {
|
|
734
|
+
const err = new Error(`Maximum concurrent sub-runs (${this.maxSubRuns}) reached`);
|
|
735
|
+
err.code = ACTION_SUBRUN_LIMIT;
|
|
736
|
+
err.details = { alias: MAX_SUBRUNS_REACHED, limit: this.maxSubRuns };
|
|
737
|
+
throw err;
|
|
738
|
+
}
|
|
739
|
+
let childPackageId = this.packageId;
|
|
740
|
+
const parsed = ActionResolver.parseRef(childAction);
|
|
741
|
+
if (parsed.packageId) {
|
|
742
|
+
childPackageId = parsed.packageId;
|
|
743
|
+
}
|
|
744
|
+
const childActionId = parsed.actionId;
|
|
745
|
+
this.activeSubRuns++;
|
|
746
|
+
try {
|
|
747
|
+
const runnerToUse = await this.resolveChildRunner(childPackageId, childActionId);
|
|
748
|
+
if (runnerToUse !== this) {
|
|
749
|
+
this.assertDeclaredUses(runCtx.action, targetActionId, childPackageId, childActionId);
|
|
750
|
+
}
|
|
751
|
+
const childResult = await runnerToUse.execute(childAction, childInput, {
|
|
752
|
+
rootRunId,
|
|
753
|
+
parentRunId,
|
|
754
|
+
callStack,
|
|
755
|
+
signal: controller.signal,
|
|
756
|
+
process: effectiveProcess,
|
|
757
|
+
platform: options.platform || this.platform,
|
|
758
|
+
progress: options.progress,
|
|
759
|
+
logger: options.logger,
|
|
760
|
+
configOverrides: options.configOverrides,
|
|
761
|
+
maxCallDepth: options.maxCallDepth ?? this.maxCallDepth,
|
|
762
|
+
});
|
|
763
|
+
if (!childResult.ok) {
|
|
764
|
+
const err = new Error(childResult.error.message);
|
|
765
|
+
err.code = childResult.error.code;
|
|
766
|
+
err.details = childResult.error.details;
|
|
767
|
+
throw err;
|
|
768
|
+
}
|
|
769
|
+
return childResult.data;
|
|
770
|
+
}
|
|
771
|
+
finally {
|
|
772
|
+
this.activeSubRuns--;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* 校验跨包子调用是否已在 uses 声明中授权(未声明时抛出 UNDECLARED_ACTION_DEPENDENCY)。
|
|
777
|
+
*/
|
|
778
|
+
assertDeclaredUses(action, targetActionId, childPackageId, childActionId) {
|
|
779
|
+
if (!childPackageId || childPackageId === this.packageId) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const actionConfig = this.projectConfig?.actions?.[targetActionId];
|
|
783
|
+
const rawUses = actionConfig?.uses ?? action?.uses;
|
|
784
|
+
const declaredUses = Array.isArray(rawUses) ? rawUses : [];
|
|
785
|
+
const targetRef = `${childPackageId}/${childActionId}`;
|
|
786
|
+
const isAllowed = declaredUses.some((u) => u === targetRef || u === `${childPackageId}/*` || u === childPackageId);
|
|
787
|
+
if (!isAllowed) {
|
|
788
|
+
const err = new Error(`Undeclared cross-package dependency: Action '${this.packageId}/${targetActionId}' does not declare '${targetRef}' in 'uses'`);
|
|
789
|
+
err.code = UNDECLARED_ACTION_DEPENDENCY;
|
|
790
|
+
err.details = {
|
|
791
|
+
caller: `${this.packageId}/${targetActionId}`,
|
|
792
|
+
target: targetRef,
|
|
793
|
+
declaredUses: rawUses,
|
|
794
|
+
};
|
|
795
|
+
throw err;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* 解析子调用应使用的 Runner(本包或已解析的目标包 Runner)。
|
|
800
|
+
*/
|
|
801
|
+
async resolveChildRunner(childPackageId, childActionId) {
|
|
802
|
+
if (childPackageId && childPackageId !== this.packageId) {
|
|
803
|
+
const targetRunner = await this.resolveTargetPackageRunner(childPackageId);
|
|
804
|
+
if (targetRunner) {
|
|
805
|
+
return targetRunner;
|
|
806
|
+
}
|
|
807
|
+
if (this.actions.has(`${childPackageId}/${childActionId}`)) {
|
|
808
|
+
return this;
|
|
809
|
+
}
|
|
810
|
+
if (!this.actionResolver) {
|
|
811
|
+
const err = new Error(`Package '${childPackageId}' could not be resolved`);
|
|
812
|
+
err.code = PACKAGE_NOT_FOUND;
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return this;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* 执行 Action 业务逻辑并与取消/超时信号进行竞态,返回执行结果信封 Promise。
|
|
820
|
+
*/
|
|
821
|
+
raceExecutionAndAbort(args) {
|
|
822
|
+
const { runCtx, actionOrId, input, options, controller, finalizer, ctx } = args;
|
|
823
|
+
const { runId, targetActionId } = runCtx;
|
|
824
|
+
const abortPromise = new Promise((_, reject) => {
|
|
825
|
+
if (controller.signal.aborted) {
|
|
826
|
+
reject(controller.signal.reason || new Error("Action execution was cancelled"));
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
controller.signal.addEventListener("abort", () => reject(controller.signal.reason || new Error("Action execution was cancelled")), { once: true });
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
return (async () => {
|
|
833
|
+
try {
|
|
834
|
+
let currentAction = runCtx.action;
|
|
835
|
+
if (!currentAction) {
|
|
836
|
+
const resolution = await this.resolveAction(actionOrId);
|
|
837
|
+
if (resolution.status === "found") {
|
|
838
|
+
currentAction = resolution.action;
|
|
839
|
+
runCtx.action = currentAction;
|
|
840
|
+
}
|
|
841
|
+
else if (resolution.status === "load_failed") {
|
|
842
|
+
const cause = resolution.error;
|
|
843
|
+
const error = describeActionLoadFailure(cause, {
|
|
844
|
+
actionId: targetActionId,
|
|
845
|
+
packageId: resolution.packageId,
|
|
846
|
+
projectRoot: resolution.projectRoot,
|
|
847
|
+
});
|
|
848
|
+
finalizer.finalize("failed", undefined, error);
|
|
849
|
+
return { ok: false, runId, error };
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
const error = {
|
|
853
|
+
code: ACTION_NOT_FOUND,
|
|
854
|
+
message: `Action '${targetActionId}' not found in registry or linked packages`,
|
|
855
|
+
details: resolution.reason ? { reason: resolution.reason } : undefined,
|
|
856
|
+
};
|
|
857
|
+
finalizer.finalize("failed", undefined, error);
|
|
858
|
+
return { ok: false, runId, error };
|
|
859
|
+
}
|
|
860
|
+
const inputCheck = validateJsonValue(input);
|
|
861
|
+
if (!inputCheck.valid) {
|
|
862
|
+
const error = {
|
|
863
|
+
code: INPUT_NOT_JSON,
|
|
864
|
+
message: `Input validation failed for action '${targetActionId}': ${inputCheck.reason}`,
|
|
865
|
+
};
|
|
866
|
+
finalizer.finalize("failed", undefined, error);
|
|
867
|
+
return { ok: false, runId, error };
|
|
868
|
+
}
|
|
869
|
+
if (currentAction.inputSchema) {
|
|
870
|
+
const val = validateSchema(currentAction.inputSchema, input);
|
|
871
|
+
if (!val.valid) {
|
|
872
|
+
const error = {
|
|
873
|
+
code: INPUT_VALIDATION_FAILED,
|
|
874
|
+
message: `Input schema validation failed for action '${targetActionId}'`,
|
|
875
|
+
details: val.errors,
|
|
876
|
+
};
|
|
877
|
+
finalizer.finalize("failed", undefined, error);
|
|
878
|
+
return { ok: false, runId, error };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const rawOutput = await Promise.race([
|
|
883
|
+
Promise.resolve().then(() => currentAction.run(input, ctx)),
|
|
884
|
+
abortPromise,
|
|
885
|
+
]);
|
|
886
|
+
// 输出结果非 JSON 格式与合法性校验(拦截 NaN/Infinity/循环引用等)
|
|
887
|
+
const outputCheck = validateJsonValue(rawOutput);
|
|
888
|
+
if (!outputCheck.valid) {
|
|
889
|
+
const error = {
|
|
890
|
+
code: OUTPUT_NOT_JSON,
|
|
891
|
+
message: `Output validation failed for action '${targetActionId}': ${outputCheck.reason}`,
|
|
892
|
+
};
|
|
893
|
+
finalizer.finalize("failed", undefined, error);
|
|
894
|
+
return { ok: false, runId, error };
|
|
895
|
+
}
|
|
896
|
+
// 输出结果 Schema 校验
|
|
897
|
+
const outputSchemaError = this.checkActionOutputSchema(currentAction, targetActionId, rawOutput);
|
|
898
|
+
if (outputSchemaError) {
|
|
899
|
+
finalizer.finalize("failed", undefined, outputSchemaError);
|
|
900
|
+
return { ok: false, runId, error: outputSchemaError };
|
|
901
|
+
}
|
|
902
|
+
finalizer.finalize("success", rawOutput);
|
|
903
|
+
if (finalizer.persistError) {
|
|
904
|
+
return { ok: false, runId, error: finalizer.persistError };
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
ok: true,
|
|
908
|
+
runId,
|
|
909
|
+
data: rawOutput,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
return this.classifyExecutionError(err, runCtx, controller, finalizer);
|
|
914
|
+
}
|
|
915
|
+
})();
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* 输出结果 Schema 校验(返回校验错误或 undefined)。
|
|
919
|
+
*/
|
|
920
|
+
checkActionOutputSchema(action, targetActionId, rawOutput) {
|
|
921
|
+
const targetOutputSchema = action?.outputSchema ?? this.projectConfig?.actions?.[targetActionId]?.outputSchema;
|
|
922
|
+
if (targetOutputSchema) {
|
|
923
|
+
const outVal = validateSchema(targetOutputSchema, rawOutput);
|
|
924
|
+
if (!outVal.valid) {
|
|
925
|
+
return {
|
|
926
|
+
code: OUTPUT_VALIDATION_FAILED,
|
|
927
|
+
message: `Output schema validation failed for action '${targetActionId}'`,
|
|
928
|
+
details: outVal.errors,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* 异常分类收敛:超时、取消与业务失败分别映射为对应终态与错误码。
|
|
936
|
+
*/
|
|
937
|
+
classifyExecutionError(err, runCtx, controller, finalizer) {
|
|
938
|
+
const { runId, options } = runCtx;
|
|
939
|
+
if (finalizer.isTimeout) {
|
|
940
|
+
const error = {
|
|
941
|
+
code: ACTION_TIMEOUT,
|
|
942
|
+
message: `Action exceeded timeout of ${options.timeoutMs}ms`,
|
|
943
|
+
};
|
|
944
|
+
finalizer.finalize("timed_out", undefined, error);
|
|
945
|
+
return { ok: false, runId, error: finalizer.persistError || error };
|
|
946
|
+
}
|
|
947
|
+
if (controller.signal.aborted) {
|
|
948
|
+
const reason = controller.signal.reason;
|
|
949
|
+
const reasonMsg = reason instanceof Error
|
|
950
|
+
? reason.message
|
|
951
|
+
: typeof reason === "string"
|
|
952
|
+
? reason
|
|
953
|
+
: undefined;
|
|
954
|
+
const error = {
|
|
955
|
+
code: ACTION_CANCELLED,
|
|
956
|
+
message: "Action execution was cancelled",
|
|
957
|
+
details: reasonMsg ? { reason: reasonMsg } : undefined,
|
|
958
|
+
};
|
|
959
|
+
finalizer.finalize("cancelled", undefined, error);
|
|
960
|
+
return { ok: false, runId, error: finalizer.persistError || error };
|
|
961
|
+
}
|
|
962
|
+
const error = {
|
|
963
|
+
code: err?.code || ACTION_FAILED,
|
|
964
|
+
message: err?.message || String(err),
|
|
965
|
+
details: err?.details,
|
|
966
|
+
};
|
|
967
|
+
finalizer.finalize("failed", undefined, error);
|
|
968
|
+
return {
|
|
969
|
+
ok: false,
|
|
970
|
+
runId,
|
|
971
|
+
error: finalizer.persistError || error,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* 同步等待方式执行指定 Action,直接返回 ExecutionResult 信封结果。
|
|
976
|
+
*
|
|
977
|
+
* @param actionOrId Action 定义对象、引用或标识符
|
|
978
|
+
* @param input 输入参数
|
|
979
|
+
* @param options 执行控制选项
|
|
980
|
+
*/
|
|
981
|
+
async execute(actionOrId, input = {}, options = {}) {
|
|
982
|
+
return this.start(actionOrId, input, options).result;
|
|
983
|
+
}
|
|
984
|
+
}
|