@actiondock/core 2.0.9 → 2.0.11-beta.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.
@@ -6,4 +6,5 @@ export * from "./env";
6
6
  export * from "./clock";
7
7
  export * from "./process";
8
8
  export * from "./events";
9
+ export * from "./module-loader";
9
10
 
@@ -0,0 +1,67 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ /**
4
+ * 统一源码模块加载器接口。
5
+ * 解耦 Action 与各类扩展模块的具体加载机制(如 ECMAScript 原生 import、tsx 动态转译加载等)。
6
+ */
7
+ export interface ModuleLoader {
8
+ /**
9
+ * 解析模块标识符为绝对路径或完整 URL。
10
+ *
11
+ * @param specifier 模块规范说明符或物理路径
12
+ * @param parentPath 发起解析的父级文件或目录路径
13
+ */
14
+ resolve?(specifier: string, parentPath?: string): string;
15
+
16
+ /**
17
+ * 动态加载模块并返回命名空间全量导出对象。
18
+ *
19
+ * @param specifier 模块规范说明符或物理路径
20
+ * @param parentPath 发起加载的父级文件或目录路径
21
+ */
22
+ load<T = any>(specifier: string, parentPath?: string): Promise<T>;
23
+
24
+ /**
25
+ * 加载模块并解包其默认导出(default 或 action 属性)。
26
+ *
27
+ * @param specifier 模块规范说明符或物理路径
28
+ * @param parentPath 发起加载的父级文件或目录路径
29
+ */
30
+ loadDefault?<T = any>(specifier: string, parentPath?: string): Promise<T>;
31
+ }
32
+
33
+ /**
34
+ * 基于标准 ECMAScript 动态 import 的默认模块加载器。
35
+ */
36
+ export class DefaultModuleLoader implements ModuleLoader {
37
+ async load<T = any>(specifier: string, _parentPath?: string): Promise<T> {
38
+ const importSpecifier = specifier.startsWith("file://")
39
+ ? specifier
40
+ : pathToFileURL(specifier).href;
41
+ return (await import(importSpecifier)) as T;
42
+ }
43
+
44
+ async loadDefault<T = any>(specifier: string, parentPath?: string): Promise<T> {
45
+ const mod = await this.load<any>(specifier, parentPath);
46
+ return (mod?.default !== undefined ? mod.default : mod?.action !== undefined ? mod.action : mod) as T;
47
+ }
48
+ }
49
+
50
+ let globalModuleLoader: ModuleLoader | undefined;
51
+
52
+ /**
53
+ * 注册全局模块加载器实现。
54
+ */
55
+ export function setModuleLoader(loader: ModuleLoader): void {
56
+ globalModuleLoader = loader;
57
+ }
58
+
59
+ /**
60
+ * 获取当前全局模块加载器,若未显式注册则回退使用 DefaultModuleLoader。
61
+ */
62
+ export function getModuleLoader(): ModuleLoader {
63
+ if (!globalModuleLoader) {
64
+ globalModuleLoader = new DefaultModuleLoader();
65
+ }
66
+ return globalModuleLoader;
67
+ }
@@ -200,23 +200,24 @@ export class DefaultProcessExecutor implements ProcessExecutor {
200
200
  }
201
201
 
202
202
  try {
203
- const checkRes = await this.exec(options.command, ["--version"], {
204
- timeoutMs: 1000,
205
- });
206
- const isReady = await options.probe(checkRes);
203
+ const res: DetachedProcessResult = {
204
+ ok: true,
205
+ pid: child.pid,
206
+ ready: true,
207
+ durationMs: Date.now() - startTime,
208
+ };
209
+ const isReady = await options.probe(res as any);
207
210
  if (isReady) {
208
- return {
209
- ok: true,
210
- pid: child.pid,
211
- ready: true,
212
- durationMs: Date.now() - startTime,
213
- };
211
+ return res;
214
212
  }
215
213
  } catch {
216
214
  // 探测失败继续轮询
217
215
  }
218
216
 
219
- await new Promise((r) => setTimeout(r, probeInterval));
217
+ const remaining = deadline - Date.now();
218
+ if (remaining <= 0) break;
219
+ const sleepTime = Math.min(probeInterval, remaining);
220
+ await new Promise((r) => setTimeout(r, sleepTime));
220
221
  }
221
222
 
222
223
  return {
@@ -1,19 +1,24 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import type {
3
4
  ActionContext,
4
5
  ActionDefinition,
6
+ ActionRef,
5
7
  ExecutionResult,
6
8
  JsonValue,
9
+ Logger,
7
10
  ProcessAPI,
8
11
  ProgressReporter,
9
12
  RuntimeError,
10
13
  RunRecord,
11
14
  } from "@actiondock/sdk";
15
+ import { ActionResolver } from "../catalog/action-resolver";
16
+ import { loadActions, loadProjectConfig } from "../project/loader";
12
17
  import type { ProjectConfig } from "../project/types";
18
+ import { resolveActionProject } from "../registry/registry";
13
19
  import { validateSchema } from "../schema/validator";
14
20
  import type { RuntimeStorage, TerminalRunStatus } from "../storage/types";
15
- import { RuntimeConfig, RuntimeStateStore, StderrLogger } from "./context";
16
- import { getProcessExecutor } from "./process";
21
+ import { createActionContext, StderrLogger } from "./context";
17
22
 
18
23
  /**
19
24
  * ActionRunner 初始化配置选项。
@@ -31,12 +36,19 @@ export interface RunnerOptions {
31
36
  actions?: Map<string, ActionDefinition>;
32
37
  /** 外部注入的进程执行器 */
33
38
  process?: ProcessAPI;
39
+ /** 动态解析跨包或未注册 Action 的委托函数 */
40
+ actionResolver?: (
41
+ ref: ActionRef | string,
42
+ currentPackageId?: string
43
+ ) => ActionDefinition | undefined | Promise<ActionDefinition | undefined>;
34
44
  }
35
45
 
36
46
  /**
37
47
  * 启动 Action 执行时的可选控制参数。
38
48
  */
39
49
  export interface ExecutionStartOptions {
50
+ /** 显式指定的运行 ID */
51
+ runId?: string;
40
52
  /** 根运行 ID */
41
53
  rootRunId?: string;
42
54
  /** 父级运行 ID(嵌套调用场景下建立调用链树) */
@@ -57,6 +69,8 @@ export interface ExecutionStartOptions {
57
69
  process?: ProcessAPI;
58
70
  /** 外部注入的进度报告器 */
59
71
  progress?: ProgressReporter;
72
+ /** 外部注入的日志记录器 */
73
+ logger?: Logger;
60
74
  }
61
75
 
62
76
  /**
@@ -91,6 +105,10 @@ export class ActionRunner {
91
105
  private projectConfig?: ProjectConfig;
92
106
  private configOverrides: Record<string, unknown>;
93
107
  private actions: Map<string, ActionDefinition>;
108
+ private actionResolver?: (
109
+ ref: ActionRef | string,
110
+ currentPackageId?: string
111
+ ) => ActionDefinition | undefined | Promise<ActionDefinition | undefined>;
94
112
 
95
113
  constructor(options: RunnerOptions) {
96
114
  this.packageId = options.packageId;
@@ -98,6 +116,7 @@ export class ActionRunner {
98
116
  this.projectConfig = options.projectConfig;
99
117
  this.configOverrides = options.configOverrides || {};
100
118
  this.actions = options.actions || new Map();
119
+ this.actionResolver = options.actionResolver;
101
120
  }
102
121
 
103
122
  /**
@@ -114,6 +133,86 @@ export class ActionRunner {
114
133
  return this.actions.get(id);
115
134
  }
116
135
 
136
+ /**
137
+ * 动态解析 Action(支持本地注册表、自定义解析器委托与已链接包目录索引检索)。
138
+ *
139
+ * @param actionOrRef Action 定义对象、引用或标识符
140
+ * @returns 解析出的 ActionDefinition,若未找到则返回 undefined
141
+ */
142
+ public async resolveAction(
143
+ actionOrRef: ActionDefinition | ActionRef | string
144
+ ): Promise<ActionDefinition | undefined> {
145
+ if (
146
+ typeof actionOrRef === "object" &&
147
+ "run" in actionOrRef &&
148
+ typeof (actionOrRef as any).run === "function"
149
+ ) {
150
+ return actionOrRef as ActionDefinition;
151
+ }
152
+
153
+ const ref = actionOrRef as ActionRef | string;
154
+ const parsed = ActionResolver.parseRef(ref);
155
+ const targetActionId = parsed.actionId;
156
+ const targetPackageId = parsed.packageId;
157
+
158
+ // 1. 本地 actions 映射表优先检索
159
+ if (targetPackageId && targetPackageId !== this.packageId) {
160
+ if (this.actions.has(`${targetPackageId}/${targetActionId}`)) {
161
+ return this.actions.get(`${targetPackageId}/${targetActionId}`);
162
+ }
163
+ } else {
164
+ if (this.actions.has(targetActionId)) {
165
+ return this.actions.get(targetActionId);
166
+ }
167
+ if (this.packageId && this.actions.has(`${this.packageId}/${targetActionId}`)) {
168
+ return this.actions.get(`${this.packageId}/${targetActionId}`);
169
+ }
170
+ }
171
+
172
+ // 2. 外部注入的自定义 actionResolver 调度
173
+ if (this.actionResolver) {
174
+ const customResolved = await this.actionResolver(ref, this.packageId);
175
+ if (customResolved) {
176
+ if (targetPackageId && targetPackageId !== this.packageId) {
177
+ this.actions.set(`${targetPackageId}/${targetActionId}`, customResolved);
178
+ } else {
179
+ this.actions.set(targetActionId, customResolved);
180
+ if (this.packageId) {
181
+ this.actions.set(`${this.packageId}/${targetActionId}`, customResolved);
182
+ }
183
+ }
184
+ return customResolved;
185
+ }
186
+ }
187
+
188
+ // 3. 基于全局链接注册表与目录索引的动态寻址与按需加载
189
+ try {
190
+ const identifier = targetPackageId
191
+ ? `${targetPackageId}/${targetActionId}`
192
+ : targetActionId;
193
+ const resolved = await resolveActionProject(identifier);
194
+ if (resolved && existsSync(resolved.projectRoot)) {
195
+ const config = loadProjectConfig(resolved.projectRoot);
196
+ const actionsMap = await loadActions(resolved.projectRoot, config.actionsDir, {
197
+ autoInstall: false,
198
+ });
199
+ const matched = actionsMap.get(resolved.actionId);
200
+ if (matched) {
201
+ this.actions.set(`${resolved.packageId}/${resolved.actionId}`, matched);
202
+ // 仅当目标包就是当前项目时才注册短标识符,避免跨包动态载入污染全局短标识符
203
+ if (!targetPackageId || resolved.packageId === this.packageId) {
204
+ this.actions.set(resolved.actionId, matched);
205
+ }
206
+ return matched;
207
+ }
208
+ }
209
+ } catch {
210
+ // 忽略寻址异常并返回 undefined
211
+ }
212
+
213
+ return undefined;
214
+ }
215
+
117
216
  /**
118
217
  * 获取当前 Runner 已注册的所有 Action 列表。
119
218
  */
@@ -124,44 +223,61 @@ export class ActionRunner {
124
223
  /**
125
224
  * 异步启动 Action 的执行并立即返回 ExecutionHandle 句柄。
126
225
  *
127
- * @param actionOrId Action 定义对象或已注册的 Action ID
226
+ * @param actionOrId Action 定义对象、引用或标识符
128
227
  * @param input 传递给 Action 的输入数据
129
228
  * @param options 执行控制选项(超时、取消信号、父运行 ID 等)
130
229
  * @returns 包含 runId、result Promise 和 cancel 方法的执行句柄
131
230
  */
132
231
  start(
133
- actionOrId: ActionDefinition | string,
232
+ actionOrId: ActionDefinition | ActionRef | string,
134
233
  input: unknown = {},
135
234
  options: ExecutionStartOptions = {}
136
235
  ): ExecutionHandle {
137
- const runId = randomUUID();
236
+ const runId = options.runId || randomUUID();
138
237
  const startedAt = new Date().toISOString();
139
238
  const callStack = [...(options.callStack || [])];
140
239
 
141
- let action: ActionDefinition;
142
- if (typeof actionOrId === "string") {
143
- const found = this.actions.get(actionOrId);
144
- if (!found) {
145
- const error: RuntimeError = {
146
- code: "ACTION_NOT_FOUND",
147
- message: `Action '${actionOrId}' not found in registry`,
148
- };
149
- return {
150
- runId,
151
- result: Promise.resolve({ ok: false, runId, error }),
152
- cancel: () => false,
153
- };
154
- }
155
- action = found;
240
+ let action: ActionDefinition | undefined;
241
+ let targetActionId: string;
242
+ let targetPackageId: string = this.packageId;
243
+
244
+ if (
245
+ typeof actionOrId === "object" &&
246
+ "run" in actionOrId &&
247
+ typeof (actionOrId as any).run === "function"
248
+ ) {
249
+ action = actionOrId as ActionDefinition;
250
+ targetActionId = action.id;
156
251
  } else {
157
- action = actionOrId;
252
+ const parsed = ActionResolver.parseRef(actionOrId as ActionRef | string);
253
+ targetActionId = parsed.actionId;
254
+ if (parsed.packageId) {
255
+ targetPackageId = parsed.packageId;
256
+ }
257
+
258
+ if (parsed.packageId && parsed.packageId !== this.packageId) {
259
+ action = this.actions.get(`${parsed.packageId}/${targetActionId}`);
260
+ } else {
261
+ action =
262
+ this.actions.get(targetActionId) ||
263
+ (this.packageId ? this.actions.get(`${this.packageId}/${targetActionId}`) : undefined);
264
+ }
158
265
  }
159
266
 
160
267
  // 1. 环路死锁检测 (Cycle Detection)
161
- if (callStack.includes(action.id)) {
268
+ const isExternal = Boolean(targetPackageId && targetPackageId !== this.packageId);
269
+ const callKey = isExternal
270
+ ? `${targetPackageId}/${targetActionId}`
271
+ : targetActionId;
272
+
273
+ const hasCycle = isExternal
274
+ ? callStack.includes(callKey)
275
+ : (callStack.includes(callKey) || (this.packageId ? callStack.includes(`${this.packageId}/${targetActionId}`) : false));
276
+
277
+ if (hasCycle) {
162
278
  const error: RuntimeError = {
163
279
  code: "ACTION_CYCLE_DETECTED",
164
- message: `Cycle detected in action invocation: ${callStack.join(" -> ")} -> ${action.id}`,
280
+ message: `Cycle detected in action invocation: ${callStack.join(" -> ")} -> ${callKey}`,
165
281
  };
166
282
  return {
167
283
  runId,
@@ -169,10 +285,10 @@ export class ActionRunner {
169
285
  cancel: () => false,
170
286
  };
171
287
  }
172
- callStack.push(action.id);
288
+ callStack.push(callKey);
173
289
 
174
- // 2. 输入参数 JSON Schema 校验
175
- if (action.inputSchema) {
290
+ // 2. 输入参数 JSON Schema 校验(若 action 已就绪)
291
+ if (action?.inputSchema) {
176
292
  const val = validateSchema(action.inputSchema, input);
177
293
  if (!val.valid) {
178
294
  const error: RuntimeError = {
@@ -193,9 +309,9 @@ export class ActionRunner {
193
309
  id: runId,
194
310
  rootRunId: options.rootRunId || options.parentRunId || runId,
195
311
  parentRunId: options.parentRunId,
196
- packageId: this.packageId,
197
- packageInstanceId: options.packageInstanceId || this.packageId,
198
- actionId: action.id,
312
+ packageId: targetPackageId,
313
+ packageInstanceId: options.packageInstanceId || targetPackageId,
314
+ actionId: targetActionId,
199
315
  generationId: options.generationId || "1",
200
316
  ownerId: options.ownerId || "local",
201
317
  status: "running",
@@ -243,26 +359,26 @@ export class ActionRunner {
243
359
  };
244
360
 
245
361
  // 5. 构建 ActionContext 运行时上下文
246
- const config = new RuntimeConfig(
247
- this.storage,
248
- this.configOverrides,
249
- this.projectConfig
250
- );
251
- const state = new RuntimeStateStore(this.storage);
252
- const log = new StderrLogger(action.id);
253
-
254
- const invoker = {
255
- invoke: async <I, O>(
256
- childAction: ActionDefinition<I, O>,
257
- childInput: I
258
- ): Promise<O> => {
362
+ const ctx = createActionContext({
363
+ storage: this.storage,
364
+ overrides: this.configOverrides,
365
+ projectConfig: this.projectConfig,
366
+ runId,
367
+ rootRunId: initialRun.rootRunId,
368
+ parentRunId: options.parentRunId,
369
+ signal: controller.signal,
370
+ process: options.process,
371
+ progress: options.progress,
372
+ logger: options.logger || new StderrLogger(action?.id || targetActionId),
373
+ onActionInvoke: async (childAction, childInput, parentRunId) => {
259
374
  const childResult = await this.execute(childAction, childInput, {
260
375
  rootRunId: initialRun.rootRunId,
261
- parentRunId: runId,
376
+ parentRunId,
262
377
  callStack,
263
378
  signal: controller.signal,
264
379
  process: options.process,
265
380
  progress: options.progress,
381
+ logger: options.logger,
266
382
  });
267
383
  if (!childResult.ok) {
268
384
  const err = new Error(childResult.error.message);
@@ -270,26 +386,9 @@ export class ActionRunner {
270
386
  (err as any).details = childResult.error.details;
271
387
  throw err;
272
388
  }
273
- return childResult.data as O;
389
+ return childResult.data;
274
390
  },
275
- };
276
-
277
- const ctx: ActionContext = {
278
- config,
279
- state,
280
- actions: invoker,
281
- process: options.process || getProcessExecutor(),
282
- log,
283
- progress: options.progress || {
284
- report() {},
285
- },
286
- signal: controller.signal,
287
- run: {
288
- id: runId,
289
- rootId: initialRun.rootRunId,
290
- parentId: options.parentRunId,
291
- },
292
- };
391
+ });
293
392
 
294
393
  // 6. 执行 Action 业务逻辑并与取消/超时信号进行竞态
295
394
  const abortPromise = new Promise<never>((_, reject) => {
@@ -306,18 +405,44 @@ export class ActionRunner {
306
405
 
307
406
  const executionPromise = (async (): Promise<ExecutionResult> => {
308
407
  try {
408
+ let currentAction = action;
409
+ if (!currentAction) {
410
+ currentAction = await this.resolveAction(actionOrId);
411
+ if (!currentAction) {
412
+ const error: RuntimeError = {
413
+ code: "ACTION_NOT_FOUND",
414
+ message: `Action '${targetActionId}' not found in registry or linked packages`,
415
+ };
416
+ finalizeRun("failed", undefined, error);
417
+ return { ok: false, runId, error };
418
+ }
419
+
420
+ if (currentAction.inputSchema) {
421
+ const val = validateSchema(currentAction.inputSchema, input);
422
+ if (!val.valid) {
423
+ const error: RuntimeError = {
424
+ code: "INPUT_VALIDATION_FAILED",
425
+ message: `Input schema validation failed for action '${currentAction.id}'`,
426
+ details: val.errors,
427
+ };
428
+ finalizeRun("failed", undefined, error);
429
+ return { ok: false, runId, error };
430
+ }
431
+ }
432
+ }
433
+
309
434
  const rawOutput = await Promise.race([
310
- Promise.resolve().then(() => action.run(input, ctx)),
435
+ Promise.resolve().then(() => currentAction!.run(input, ctx)),
311
436
  abortPromise,
312
437
  ]);
313
438
 
314
439
  // 输出结果 Schema 校验
315
- if (action.outputSchema) {
316
- const outVal = validateSchema(action.outputSchema, rawOutput);
440
+ if (currentAction.outputSchema) {
441
+ const outVal = validateSchema(currentAction.outputSchema, rawOutput);
317
442
  if (!outVal.valid) {
318
443
  const error: RuntimeError = {
319
444
  code: "OUTPUT_VALIDATION_FAILED",
320
- message: `Output schema validation failed for action '${action.id}'`,
445
+ message: `Output schema validation failed for action '${currentAction.id}'`,
321
446
  details: outVal.errors,
322
447
  };
323
448
  finalizeRun("failed", undefined, error);
@@ -388,12 +513,12 @@ export class ActionRunner {
388
513
  /**
389
514
  * 同步等待方式执行指定 Action,直接返回 ExecutionResult 信封结果。
390
515
  *
391
- * @param actionOrId Action 定义对象或 ID
516
+ * @param actionOrId Action 定义对象、引用或标识符
392
517
  * @param input 输入参数
393
518
  * @param options 执行控制选项
394
519
  */
395
520
  async execute(
396
- actionOrId: ActionDefinition | string,
521
+ actionOrId: ActionDefinition | ActionRef | string,
397
522
  input: unknown = {},
398
523
  options: ExecutionStartOptions = {}
399
524
  ): Promise<ExecutionResult> {
@@ -2,5 +2,6 @@ export * from "./types";
2
2
  export * from "./security";
3
3
  export * from "./body";
4
4
  export * from "./runtime-registry";
5
+ export * from "./routes";
5
6
  export * from "./server";
6
7