@actiondock/core 2.0.10 → 2.0.11
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/package.json +2 -2
- package/src/export/templates.ts +127 -0
- package/src/project/init.ts +2 -2
- package/src/registry/registry.ts +145 -2
- package/src/runtime/context.ts +6 -2
- package/src/runtime/runner.ts +171 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "ActionDock Core Engine - Project loader, runtime execution, SQLite storage, standalone builder, and skill exporter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test": "bun test"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@actiondock/sdk": "^2.0.
|
|
30
|
+
"@actiondock/sdk": "^2.0.11",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"ajv-formats": "^3.0.1",
|
|
33
33
|
"yaml": "^2.7.0"
|
package/src/export/templates.ts
CHANGED
|
@@ -144,6 +144,14 @@ ad link "<skill_root>"
|
|
|
144
144
|
|
|
145
145
|
> \`ad link\` 天然具备幂等性,同一 Package 多次执行会直接更新路径,可安全重复调用。
|
|
146
146
|
|
|
147
|
+
### 动作参数契约按需调阅
|
|
148
|
+
|
|
149
|
+
在调用未知参数的 Action 前,可在终端执行命令按需查阅该 Action 的输入输出模式与详细说明:
|
|
150
|
+
|
|
151
|
+
\`\`\`bash
|
|
152
|
+
ad action show ${pkgId}/${firstAction}
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
147
155
|
### 执行 Action
|
|
148
156
|
|
|
149
157
|
为避免多技能之间的 Action ID 命名冲突,建议统一使用带有 Package 前缀的完全限定 ID。
|
|
@@ -376,6 +384,125 @@ export function generateSkillMd(
|
|
|
376
384
|
return generateStandaloneSkillMd(config, actions, playbooks, optionsOrBinaryPath.binaryRelPath || "./bin/action-bin");
|
|
377
385
|
}
|
|
378
386
|
|
|
387
|
+
export interface CompositeSkillPackageInfo {
|
|
388
|
+
config: ProjectConfig;
|
|
389
|
+
actions: Array<{ id: string; description?: string }>;
|
|
390
|
+
playbooks: Array<{ id: string; name?: string; description?: string; filePath: string }>;
|
|
391
|
+
packageDir: string;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 生成多包聚合的复合模式 SKILL.md 文档。
|
|
396
|
+
*/
|
|
397
|
+
export function generateCompositeSkillMd(
|
|
398
|
+
bundleName: string,
|
|
399
|
+
description: string,
|
|
400
|
+
packages: CompositeSkillPackageInfo[]
|
|
401
|
+
): string {
|
|
402
|
+
const cleanName = bundleName.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
|
|
403
|
+
const samplePkg = packages.find((p) => p.actions && p.actions.length > 0);
|
|
404
|
+
const sampleActionId = samplePkg
|
|
405
|
+
? `${samplePkg.config.id}/${samplePkg.actions[0].id}`
|
|
406
|
+
: "sample.greet";
|
|
407
|
+
|
|
408
|
+
const actionSections = packages
|
|
409
|
+
.map((pkg) => {
|
|
410
|
+
const actList = pkg.actions
|
|
411
|
+
.map((a) => {
|
|
412
|
+
const desc = a.description ? `: ${a.description}` : "";
|
|
413
|
+
return `- \`${pkg.config.id}/${a.id}\`${desc}`;
|
|
414
|
+
})
|
|
415
|
+
.join("\n");
|
|
416
|
+
return `### ${pkg.config.name} (${pkg.config.id})\n${actList || "- 无可用 Action"}`;
|
|
417
|
+
})
|
|
418
|
+
.join("\n\n");
|
|
419
|
+
|
|
420
|
+
const playbookEntries: string[] = [];
|
|
421
|
+
for (const pkg of packages) {
|
|
422
|
+
for (const pb of pkg.playbooks) {
|
|
423
|
+
const relPath = `packages/${pkg.packageDir}/playbooks/${basename(pb.filePath)}`;
|
|
424
|
+
playbookEntries.push(`- [${pb.name || pb.id}](${relPath}): ${pb.description || "标准操作规程"}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const playbookSection =
|
|
429
|
+
playbookEntries.length > 0
|
|
430
|
+
? `## 推荐操作规程\n\n涉及多步骤或业务流程时,优先遵循以下原位规程:\n\n${playbookEntries.join("\n")}\n\n---\n`
|
|
431
|
+
: "";
|
|
432
|
+
|
|
433
|
+
return `---
|
|
434
|
+
name: ${cleanName}
|
|
435
|
+
description: ${description}
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
# ${bundleName} 复合技能套件
|
|
439
|
+
|
|
440
|
+
${description}
|
|
441
|
+
|
|
442
|
+
## ActionDock 运行时初始化
|
|
443
|
+
|
|
444
|
+
本技能为 **ActionDock 复合工作区技能包**,聚合了多个功能包。智能体在初次调用或初始化时,在当前技能根目录执行注册命令:
|
|
445
|
+
|
|
446
|
+
\`\`\`bash
|
|
447
|
+
ad link "<skill_root>"
|
|
448
|
+
\`\`\`
|
|
449
|
+
|
|
450
|
+
> \`ad link\` 会自动识别并注册工作区下的所有子包,使其中的 Action 随时可以通过完全限定标识调用。
|
|
451
|
+
|
|
452
|
+
## 动作参数契约按需调阅
|
|
453
|
+
|
|
454
|
+
为节省上下文开销,各 Action 的详细参数结构不静态内嵌在说明书中。在调用未知参数的 Action 前,可在终端执行命令查阅输入输出约束:
|
|
455
|
+
|
|
456
|
+
\`\`\`bash
|
|
457
|
+
ad action show ${sampleActionId}
|
|
458
|
+
\`\`\`
|
|
459
|
+
|
|
460
|
+
## 可用 Action 工具清单
|
|
461
|
+
|
|
462
|
+
${actionSections}
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
${playbookSection}
|
|
467
|
+
## 标准调用命令
|
|
468
|
+
|
|
469
|
+
推荐使用参数文件传递内容,杜绝终端引号转义问题:
|
|
470
|
+
|
|
471
|
+
\`\`\`bash
|
|
472
|
+
cat << 'EOF' > /tmp/input.json
|
|
473
|
+
{
|
|
474
|
+
"param": "value"
|
|
475
|
+
}
|
|
476
|
+
EOF
|
|
477
|
+
ad run ${sampleActionId} --input-file /tmp/input.json
|
|
478
|
+
\`\`\`
|
|
479
|
+
|
|
480
|
+
### 结构化响应解析
|
|
481
|
+
|
|
482
|
+
所有 Action 执行结果均在 \`stdout\` 输出标准格式的 JSON 信封:
|
|
483
|
+
|
|
484
|
+
\`\`\`json
|
|
485
|
+
// 执行成功响应 (ok 为 true)
|
|
486
|
+
{
|
|
487
|
+
"ok": true,
|
|
488
|
+
"runId": "01J...",
|
|
489
|
+
"data": { ... }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 执行失败响应 (ok 为 false)
|
|
493
|
+
{
|
|
494
|
+
"ok": false,
|
|
495
|
+
"runId": "01J...",
|
|
496
|
+
"error": {
|
|
497
|
+
"code": "ACTION_EXECUTION_FAILED",
|
|
498
|
+
"message": "错误详细描述信息"
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
\`\`\`
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
379
506
|
export interface GenerateSkillJsonOptions {
|
|
380
507
|
mode?: "source" | "standalone";
|
|
381
508
|
executable?: string;
|
package/src/project/init.ts
CHANGED
|
@@ -108,10 +108,10 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
108
108
|
node: ">=22.12.0",
|
|
109
109
|
},
|
|
110
110
|
dependencies: {
|
|
111
|
-
"@actiondock/sdk": "^2.0.
|
|
111
|
+
"@actiondock/sdk": "^2.0.11",
|
|
112
112
|
},
|
|
113
113
|
devDependencies: {
|
|
114
|
-
"@actiondock/testing": "^2.0.
|
|
114
|
+
"@actiondock/testing": "^2.0.11",
|
|
115
115
|
"@types/node": "^22.12.0",
|
|
116
116
|
"tsx": "^4.19.0",
|
|
117
117
|
"typescript": "^5.7.0",
|
package/src/registry/registry.ts
CHANGED
|
@@ -348,6 +348,149 @@ async function projectHasAction(
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
function projectHasActionSync(
|
|
352
|
+
projectRoot: string,
|
|
353
|
+
actionsDir: string | undefined,
|
|
354
|
+
actionId: string
|
|
355
|
+
): boolean {
|
|
356
|
+
const manifest = loadManifest(projectRoot);
|
|
357
|
+
if (manifest?.actions && actionId in manifest.actions) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
const dir = join(projectRoot, actionsDir || "actions");
|
|
361
|
+
if (!existsSync(dir)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (actionId.includes("..") || actionId.startsWith("/") || actionId.startsWith("\\")) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (existsSync(join(dir, `${actionId}.ts`)) || existsSync(join(dir, `${actionId}.js`))) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
const relFile = actionId.replace(/\./g, "/");
|
|
371
|
+
if (existsSync(join(dir, `${relFile}.ts`)) || existsSync(join(dir, `${relFile}.js`))) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function resolveActionProjectSync(
|
|
378
|
+
actionIdentifier: string,
|
|
379
|
+
cwd: string = process.cwd(),
|
|
380
|
+
customHome?: string
|
|
381
|
+
): ResolvedActionProject {
|
|
382
|
+
// 1. Check current directory / parent project
|
|
383
|
+
const currentRoot = findProjectRoot(cwd);
|
|
384
|
+
if (currentRoot) {
|
|
385
|
+
try {
|
|
386
|
+
const config = loadProjectConfig(currentRoot);
|
|
387
|
+
if (projectHasActionSync(currentRoot, config.actionsDir, actionIdentifier)) {
|
|
388
|
+
return {
|
|
389
|
+
projectRoot: currentRoot,
|
|
390
|
+
packageId: config.id,
|
|
391
|
+
actionId: actionIdentifier,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Ignore and proceed to registry lookup
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 2. Check if scoped format: <package-id>/<action-id> or <package-id>:<action-id>
|
|
400
|
+
let targetPackage: string | undefined;
|
|
401
|
+
let pureActionId = actionIdentifier;
|
|
402
|
+
|
|
403
|
+
if (actionIdentifier.includes("/")) {
|
|
404
|
+
const slashIdx = actionIdentifier.lastIndexOf("/");
|
|
405
|
+
targetPackage = actionIdentifier.slice(0, slashIdx);
|
|
406
|
+
pureActionId = actionIdentifier.slice(slashIdx + 1);
|
|
407
|
+
} else if (actionIdentifier.includes(":")) {
|
|
408
|
+
const colonIdx = actionIdentifier.lastIndexOf(":");
|
|
409
|
+
targetPackage = actionIdentifier.slice(0, colonIdx);
|
|
410
|
+
pureActionId = actionIdentifier.slice(colonIdx + 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const linkedList = listLinkedPackages(customHome);
|
|
414
|
+
|
|
415
|
+
if (targetPackage) {
|
|
416
|
+
let targetRoot: string | undefined;
|
|
417
|
+
let targetPkgId = targetPackage;
|
|
418
|
+
|
|
419
|
+
const pkg = linkedList.find(
|
|
420
|
+
(p) => p.id === targetPackage || getPackageSlug(p.id) === targetPackage
|
|
421
|
+
);
|
|
422
|
+
if (pkg && existsSync(pkg.path)) {
|
|
423
|
+
targetRoot = pkg.path;
|
|
424
|
+
targetPkgId = pkg.id;
|
|
425
|
+
} else if (currentRoot) {
|
|
426
|
+
try {
|
|
427
|
+
const config = loadProjectConfig(currentRoot);
|
|
428
|
+
if (config.id === targetPackage || getPackageSlug(config.id) === targetPackage) {
|
|
429
|
+
targetRoot = currentRoot;
|
|
430
|
+
targetPkgId = config.id;
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Ignore
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!targetRoot || !existsSync(targetRoot)) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Linked package '${targetPackage}' not found or path no longer exists (${pkg?.path || "unregistered"}). Run 'ad link' in the package directory.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const config = loadProjectConfig(targetRoot);
|
|
444
|
+
if (!projectHasActionSync(targetRoot, config.actionsDir, pureActionId)) {
|
|
445
|
+
throw new Error(`Action '${pureActionId}' not found in package '${targetPkgId}' (${targetRoot})`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
projectRoot: targetRoot,
|
|
450
|
+
packageId: targetPkgId,
|
|
451
|
+
actionId: pureActionId,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 3. Search across all linked packages
|
|
456
|
+
const matches: Array<{ entry: LinkedPackageEntry; actionId: string }> = [];
|
|
457
|
+
|
|
458
|
+
for (const pkg of linkedList) {
|
|
459
|
+
if (!existsSync(pkg.path)) continue;
|
|
460
|
+
try {
|
|
461
|
+
const config = loadProjectConfig(pkg.path);
|
|
462
|
+
if (projectHasActionSync(pkg.path, config.actionsDir, actionIdentifier)) {
|
|
463
|
+
matches.push({ entry: pkg, actionId: actionIdentifier });
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Ignore invalid linked package
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (matches.length === 1) {
|
|
471
|
+
return {
|
|
472
|
+
projectRoot: matches[0].entry.path,
|
|
473
|
+
packageId: matches[0].entry.id,
|
|
474
|
+
actionId: matches[0].actionId,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (matches.length > 1) {
|
|
479
|
+
const pkgList = matches.map((m) => `'${m.entry.id}'`).join(", ");
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Action '${actionIdentifier}' is provided by multiple linked packages: ${pkgList}. Please specify using '<package-id>/${actionIdentifier}'.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (currentRoot) {
|
|
486
|
+
throw new Error(`Action '${actionIdentifier}' not found in current project or any linked packages`);
|
|
487
|
+
} else {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Action '${actionIdentifier}' not found. You are not in an ActionDock project, and no linked package provides '${actionIdentifier}'. Use 'ad link' to register your package.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
351
494
|
export async function resolveActionProject(
|
|
352
495
|
actionIdentifier: string,
|
|
353
496
|
cwd: string = process.cwd(),
|
|
@@ -375,11 +518,11 @@ export async function resolveActionProject(
|
|
|
375
518
|
let pureActionId = actionIdentifier;
|
|
376
519
|
|
|
377
520
|
if (actionIdentifier.includes("/")) {
|
|
378
|
-
const slashIdx = actionIdentifier.
|
|
521
|
+
const slashIdx = actionIdentifier.lastIndexOf("/");
|
|
379
522
|
targetPackage = actionIdentifier.slice(0, slashIdx);
|
|
380
523
|
pureActionId = actionIdentifier.slice(slashIdx + 1);
|
|
381
524
|
} else if (actionIdentifier.includes(":")) {
|
|
382
|
-
const colonIdx = actionIdentifier.
|
|
525
|
+
const colonIdx = actionIdentifier.lastIndexOf(":");
|
|
383
526
|
targetPackage = actionIdentifier.slice(0, colonIdx);
|
|
384
527
|
pureActionId = actionIdentifier.slice(colonIdx + 1);
|
|
385
528
|
}
|
package/src/runtime/context.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ActionContext,
|
|
4
4
|
ActionDefinition,
|
|
5
5
|
ActionInvoker,
|
|
6
|
+
ActionRef,
|
|
6
7
|
Config,
|
|
7
8
|
Logger,
|
|
8
9
|
ProcessAPI,
|
|
@@ -196,7 +197,7 @@ export interface ContextOptions {
|
|
|
196
197
|
progress?: ProgressReporter;
|
|
197
198
|
logger?: Logger;
|
|
198
199
|
onActionInvoke?: (
|
|
199
|
-
action: ActionDefinition,
|
|
200
|
+
action: ActionDefinition | ActionRef | string,
|
|
200
201
|
input: unknown,
|
|
201
202
|
parentRunId?: string
|
|
202
203
|
) => Promise<unknown>;
|
|
@@ -221,7 +222,10 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
221
222
|
const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
|
|
222
223
|
|
|
223
224
|
const invoker: ActionInvoker = {
|
|
224
|
-
async invoke<I, O>(
|
|
225
|
+
async invoke<I, O>(
|
|
226
|
+
action: ActionDefinition<I, O> | ActionRef | string,
|
|
227
|
+
input?: I
|
|
228
|
+
): Promise<O> {
|
|
225
229
|
if (options.onActionInvoke) {
|
|
226
230
|
return (await options.onActionInvoke(
|
|
227
231
|
action as any,
|
package/src/runtime/runner.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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,
|
|
7
9
|
Logger,
|
|
@@ -10,7 +12,10 @@ import type {
|
|
|
10
12
|
RuntimeError,
|
|
11
13
|
RunRecord,
|
|
12
14
|
} from "@actiondock/sdk";
|
|
15
|
+
import { ActionResolver } from "../catalog/action-resolver";
|
|
16
|
+
import { loadActions, loadProjectConfig } from "../project/loader";
|
|
13
17
|
import type { ProjectConfig } from "../project/types";
|
|
18
|
+
import { resolveActionProject } from "../registry/registry";
|
|
14
19
|
import { validateSchema } from "../schema/validator";
|
|
15
20
|
import type { RuntimeStorage, TerminalRunStatus } from "../storage/types";
|
|
16
21
|
import { createActionContext, StderrLogger } from "./context";
|
|
@@ -31,6 +36,11 @@ 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
|
/**
|
|
@@ -95,6 +105,10 @@ export class ActionRunner {
|
|
|
95
105
|
private projectConfig?: ProjectConfig;
|
|
96
106
|
private configOverrides: Record<string, unknown>;
|
|
97
107
|
private actions: Map<string, ActionDefinition>;
|
|
108
|
+
private actionResolver?: (
|
|
109
|
+
ref: ActionRef | string,
|
|
110
|
+
currentPackageId?: string
|
|
111
|
+
) => ActionDefinition | undefined | Promise<ActionDefinition | undefined>;
|
|
98
112
|
|
|
99
113
|
constructor(options: RunnerOptions) {
|
|
100
114
|
this.packageId = options.packageId;
|
|
@@ -102,6 +116,7 @@ export class ActionRunner {
|
|
|
102
116
|
this.projectConfig = options.projectConfig;
|
|
103
117
|
this.configOverrides = options.configOverrides || {};
|
|
104
118
|
this.actions = options.actions || new Map();
|
|
119
|
+
this.actionResolver = options.actionResolver;
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
/**
|
|
@@ -118,6 +133,86 @@ export class ActionRunner {
|
|
|
118
133
|
return this.actions.get(id);
|
|
119
134
|
}
|
|
120
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
|
+
|
|
121
216
|
/**
|
|
122
217
|
* 获取当前 Runner 已注册的所有 Action 列表。
|
|
123
218
|
*/
|
|
@@ -128,13 +223,13 @@ export class ActionRunner {
|
|
|
128
223
|
/**
|
|
129
224
|
* 异步启动 Action 的执行并立即返回 ExecutionHandle 句柄。
|
|
130
225
|
*
|
|
131
|
-
* @param actionOrId Action
|
|
226
|
+
* @param actionOrId Action 定义对象、引用或标识符
|
|
132
227
|
* @param input 传递给 Action 的输入数据
|
|
133
228
|
* @param options 执行控制选项(超时、取消信号、父运行 ID 等)
|
|
134
229
|
* @returns 包含 runId、result Promise 和 cancel 方法的执行句柄
|
|
135
230
|
*/
|
|
136
231
|
start(
|
|
137
|
-
actionOrId: ActionDefinition | string,
|
|
232
|
+
actionOrId: ActionDefinition | ActionRef | string,
|
|
138
233
|
input: unknown = {},
|
|
139
234
|
options: ExecutionStartOptions = {}
|
|
140
235
|
): ExecutionHandle {
|
|
@@ -142,30 +237,47 @@ export class ActionRunner {
|
|
|
142
237
|
const startedAt = new Date().toISOString();
|
|
143
238
|
const callStack = [...(options.callStack || [])];
|
|
144
239
|
|
|
145
|
-
let action: ActionDefinition;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
cancel: () => false,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
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;
|
|
160
251
|
} else {
|
|
161
|
-
|
|
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
|
+
}
|
|
162
265
|
}
|
|
163
266
|
|
|
164
267
|
// 1. 环路死锁检测 (Cycle Detection)
|
|
165
|
-
|
|
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) {
|
|
166
278
|
const error: RuntimeError = {
|
|
167
279
|
code: "ACTION_CYCLE_DETECTED",
|
|
168
|
-
message: `Cycle detected in action invocation: ${callStack.join(" -> ")} -> ${
|
|
280
|
+
message: `Cycle detected in action invocation: ${callStack.join(" -> ")} -> ${callKey}`,
|
|
169
281
|
};
|
|
170
282
|
return {
|
|
171
283
|
runId,
|
|
@@ -173,10 +285,10 @@ export class ActionRunner {
|
|
|
173
285
|
cancel: () => false,
|
|
174
286
|
};
|
|
175
287
|
}
|
|
176
|
-
callStack.push(
|
|
288
|
+
callStack.push(callKey);
|
|
177
289
|
|
|
178
|
-
// 2. 输入参数 JSON Schema
|
|
179
|
-
if (action
|
|
290
|
+
// 2. 输入参数 JSON Schema 校验(若 action 已就绪)
|
|
291
|
+
if (action?.inputSchema) {
|
|
180
292
|
const val = validateSchema(action.inputSchema, input);
|
|
181
293
|
if (!val.valid) {
|
|
182
294
|
const error: RuntimeError = {
|
|
@@ -197,9 +309,9 @@ export class ActionRunner {
|
|
|
197
309
|
id: runId,
|
|
198
310
|
rootRunId: options.rootRunId || options.parentRunId || runId,
|
|
199
311
|
parentRunId: options.parentRunId,
|
|
200
|
-
packageId:
|
|
201
|
-
packageInstanceId: options.packageInstanceId ||
|
|
202
|
-
actionId:
|
|
312
|
+
packageId: targetPackageId,
|
|
313
|
+
packageInstanceId: options.packageInstanceId || targetPackageId,
|
|
314
|
+
actionId: targetActionId,
|
|
203
315
|
generationId: options.generationId || "1",
|
|
204
316
|
ownerId: options.ownerId || "local",
|
|
205
317
|
status: "running",
|
|
@@ -257,7 +369,7 @@ export class ActionRunner {
|
|
|
257
369
|
signal: controller.signal,
|
|
258
370
|
process: options.process,
|
|
259
371
|
progress: options.progress,
|
|
260
|
-
logger: options.logger || new StderrLogger(action
|
|
372
|
+
logger: options.logger || new StderrLogger(action?.id || targetActionId),
|
|
261
373
|
onActionInvoke: async (childAction, childInput, parentRunId) => {
|
|
262
374
|
const childResult = await this.execute(childAction, childInput, {
|
|
263
375
|
rootRunId: initialRun.rootRunId,
|
|
@@ -293,18 +405,44 @@ export class ActionRunner {
|
|
|
293
405
|
|
|
294
406
|
const executionPromise = (async (): Promise<ExecutionResult> => {
|
|
295
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
|
+
|
|
296
434
|
const rawOutput = await Promise.race([
|
|
297
|
-
Promise.resolve().then(() =>
|
|
435
|
+
Promise.resolve().then(() => currentAction!.run(input, ctx)),
|
|
298
436
|
abortPromise,
|
|
299
437
|
]);
|
|
300
438
|
|
|
301
439
|
// 输出结果 Schema 校验
|
|
302
|
-
if (
|
|
303
|
-
const outVal = validateSchema(
|
|
440
|
+
if (currentAction.outputSchema) {
|
|
441
|
+
const outVal = validateSchema(currentAction.outputSchema, rawOutput);
|
|
304
442
|
if (!outVal.valid) {
|
|
305
443
|
const error: RuntimeError = {
|
|
306
444
|
code: "OUTPUT_VALIDATION_FAILED",
|
|
307
|
-
message: `Output schema validation failed for action '${
|
|
445
|
+
message: `Output schema validation failed for action '${currentAction.id}'`,
|
|
308
446
|
details: outVal.errors,
|
|
309
447
|
};
|
|
310
448
|
finalizeRun("failed", undefined, error);
|
|
@@ -375,12 +513,12 @@ export class ActionRunner {
|
|
|
375
513
|
/**
|
|
376
514
|
* 同步等待方式执行指定 Action,直接返回 ExecutionResult 信封结果。
|
|
377
515
|
*
|
|
378
|
-
* @param actionOrId Action
|
|
516
|
+
* @param actionOrId Action 定义对象、引用或标识符
|
|
379
517
|
* @param input 输入参数
|
|
380
518
|
* @param options 执行控制选项
|
|
381
519
|
*/
|
|
382
520
|
async execute(
|
|
383
|
-
actionOrId: ActionDefinition | string,
|
|
521
|
+
actionOrId: ActionDefinition | ActionRef | string,
|
|
384
522
|
input: unknown = {},
|
|
385
523
|
options: ExecutionStartOptions = {}
|
|
386
524
|
): Promise<ExecutionResult> {
|