@actiondock/core 2.0.2 → 2.0.3
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 +59 -27
- package/package.json +2 -2
- package/src/build/builder.ts +20 -7
- package/src/catalog/action-index.ts +92 -0
- package/src/catalog/action-resolver.ts +120 -0
- package/src/catalog/index.ts +5 -0
- package/src/catalog/location-registry.ts +134 -0
- package/src/catalog/package-catalog.ts +87 -0
- package/src/catalog/types.ts +83 -0
- package/src/doctor/doctor.ts +59 -33
- package/src/execution/index.ts +2 -0
- package/src/execution/service.ts +282 -0
- package/src/execution/types.ts +73 -0
- package/src/export/skill.ts +7 -6
- package/src/export/templates.ts +1 -1
- package/src/index.ts +2 -0
- package/src/project/index.ts +1 -0
- package/src/project/init.ts +80 -33
- package/src/project/loader.ts +11 -12
- package/src/project/manifest.ts +96 -0
- package/src/project/types.ts +30 -0
- package/src/runtime/clock.ts +41 -0
- package/src/runtime/context.ts +24 -2
- package/src/runtime/events.ts +142 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/process.ts +244 -0
- package/src/runtime/runner.ts +37 -3
- package/src/server/server.ts +102 -8
- package/src/server/types.ts +11 -0
- package/src/storage/driver.ts +126 -0
- package/src/storage/index.ts +4 -3
- package/src/storage/sqlite.ts +331 -251
- package/src/storage/types.ts +41 -23
package/src/project/init.ts
CHANGED
|
@@ -16,13 +16,14 @@ export interface InitOptions {
|
|
|
16
16
|
/**
|
|
17
17
|
* 在目标目录初始化一个完整的 ActionDock 2.0 Action Package 脚手架。
|
|
18
18
|
* 生成内容包括:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* - actiondock.json(项目元数据与配置声明)
|
|
20
|
+
* - actiondock.manifest.json(声明式元数据清单事实源)
|
|
21
|
+
* - package.json(Node.js 标准脚本与依赖声明)
|
|
22
|
+
* - tsconfig.json(现代 NodeNext 模块规范)
|
|
23
|
+
* - .gitignore(排除持久化 db、node_modules、dist)
|
|
24
|
+
* - actions/greet.ts(标准示例 Action,演示 config、state、log 使用)
|
|
25
|
+
* - playbooks/greet-user.md(标准 SOP Playbook 演示)
|
|
26
|
+
* - tests/greet.test.ts(基于 node:test 与 @actiondock/testing 的测试用例)
|
|
26
27
|
*
|
|
27
28
|
* @param targetDir 目标项目目录
|
|
28
29
|
* @param options 初始化选项
|
|
@@ -58,20 +59,61 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
58
59
|
JSON.stringify(actiondockJson, null, 2) + "\n"
|
|
59
60
|
);
|
|
60
61
|
|
|
61
|
-
// 2.
|
|
62
|
+
// 2. actiondock.manifest.json
|
|
63
|
+
const manifestJson = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
actions: {
|
|
66
|
+
"sample.greet": {
|
|
67
|
+
entry: "actions/greet.ts",
|
|
68
|
+
description: "Greeting action demonstrating basic input, config, and state usage",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
name: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Name of the person to greet",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["name"],
|
|
78
|
+
},
|
|
79
|
+
outputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
message: { type: "string" },
|
|
83
|
+
timesGreeted: { type: "number" },
|
|
84
|
+
},
|
|
85
|
+
required: ["message", "timesGreeted"],
|
|
86
|
+
},
|
|
87
|
+
uses: [],
|
|
88
|
+
tags: ["sample"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
assets: [],
|
|
92
|
+
};
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(root, "actiondock.manifest.json"),
|
|
95
|
+
JSON.stringify(manifestJson, null, 2) + "\n"
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// 3. package.json
|
|
62
99
|
const packageJson = {
|
|
63
100
|
name: id,
|
|
64
101
|
version: "0.1.0",
|
|
65
102
|
description,
|
|
66
103
|
type: "module",
|
|
67
104
|
scripts: {
|
|
68
|
-
test: "
|
|
105
|
+
test: "node --import tsx --test tests/*.test.ts",
|
|
106
|
+
},
|
|
107
|
+
engines: {
|
|
108
|
+
node: ">=22.12.0",
|
|
69
109
|
},
|
|
70
110
|
dependencies: {
|
|
71
|
-
"@actiondock/sdk": "^2.0.
|
|
111
|
+
"@actiondock/sdk": "^2.0.3",
|
|
72
112
|
},
|
|
73
113
|
devDependencies: {
|
|
74
|
-
"@
|
|
114
|
+
"@actiondock/testing": "^2.0.3",
|
|
115
|
+
"@types/node": "^22.12.0",
|
|
116
|
+
"tsx": "^4.19.0",
|
|
75
117
|
"typescript": "^5.7.0",
|
|
76
118
|
},
|
|
77
119
|
};
|
|
@@ -80,15 +122,15 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
80
122
|
JSON.stringify(packageJson, null, 2) + "\n"
|
|
81
123
|
);
|
|
82
124
|
|
|
83
|
-
//
|
|
125
|
+
// 4. tsconfig.json
|
|
84
126
|
const tsconfigJson = {
|
|
85
127
|
compilerOptions: {
|
|
86
|
-
target: "
|
|
87
|
-
module: "
|
|
88
|
-
moduleResolution: "
|
|
128
|
+
target: "ES2022",
|
|
129
|
+
module: "NodeNext",
|
|
130
|
+
moduleResolution: "NodeNext",
|
|
89
131
|
strict: true,
|
|
90
132
|
skipLibCheck: true,
|
|
91
|
-
types: ["
|
|
133
|
+
types: ["node"],
|
|
92
134
|
},
|
|
93
135
|
};
|
|
94
136
|
writeFileSync(
|
|
@@ -96,16 +138,17 @@ export function initProject(targetDir: string, options: InitOptions = {}): void
|
|
|
96
138
|
JSON.stringify(tsconfigJson, null, 2) + "\n"
|
|
97
139
|
);
|
|
98
140
|
|
|
99
|
-
//
|
|
141
|
+
// 5. .gitignore
|
|
100
142
|
const gitignore = `.actiondock/
|
|
101
143
|
node_modules/
|
|
102
144
|
dist/
|
|
103
|
-
|
|
104
|
-
*.
|
|
145
|
+
build/
|
|
146
|
+
*.log
|
|
147
|
+
.env
|
|
105
148
|
`;
|
|
106
149
|
writeFileSync(join(root, ".gitignore"), gitignore);
|
|
107
150
|
|
|
108
|
-
//
|
|
151
|
+
// 6. actions/
|
|
109
152
|
const actionsDir = join(root, "actions");
|
|
110
153
|
mkdirSync(actionsDir, { recursive: true });
|
|
111
154
|
|
|
@@ -113,12 +156,15 @@ bun.lock
|
|
|
113
156
|
|
|
114
157
|
export default defineAction({
|
|
115
158
|
id: "sample.greet",
|
|
116
|
-
description: "
|
|
159
|
+
description: "Greeting action demonstrating basic input, config, and state usage",
|
|
117
160
|
|
|
118
161
|
inputSchema: {
|
|
119
162
|
type: "object",
|
|
120
163
|
properties: {
|
|
121
|
-
name: {
|
|
164
|
+
name: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Name of the person to greet",
|
|
167
|
+
},
|
|
122
168
|
},
|
|
123
169
|
required: ["name"],
|
|
124
170
|
},
|
|
@@ -127,9 +173,9 @@ export default defineAction({
|
|
|
127
173
|
type: "object",
|
|
128
174
|
properties: {
|
|
129
175
|
message: { type: "string" },
|
|
130
|
-
|
|
176
|
+
timesGreeted: { type: "number" },
|
|
131
177
|
},
|
|
132
|
-
required: ["message", "
|
|
178
|
+
required: ["message", "timesGreeted"],
|
|
133
179
|
},
|
|
134
180
|
|
|
135
181
|
async run(input: { name: string }, ctx) {
|
|
@@ -141,14 +187,14 @@ export default defineAction({
|
|
|
141
187
|
|
|
142
188
|
return {
|
|
143
189
|
message: \`\${greeting}, \${input.name}!\`,
|
|
144
|
-
|
|
190
|
+
timesGreeted: count,
|
|
145
191
|
};
|
|
146
192
|
},
|
|
147
193
|
});
|
|
148
194
|
`;
|
|
149
195
|
writeFileSync(join(actionsDir, "greet.ts"), sampleAction);
|
|
150
196
|
|
|
151
|
-
//
|
|
197
|
+
// 7. playbooks/
|
|
152
198
|
const playbooksDir = join(root, "playbooks");
|
|
153
199
|
mkdirSync(playbooksDir, { recursive: true });
|
|
154
200
|
|
|
@@ -166,12 +212,13 @@ actions:
|
|
|
166
212
|
`;
|
|
167
213
|
writeFileSync(join(playbooksDir, "greet-user.md"), samplePlaybook);
|
|
168
214
|
|
|
169
|
-
//
|
|
215
|
+
// 8. tests/
|
|
170
216
|
const testsDir = join(root, "tests");
|
|
171
217
|
mkdirSync(testsDir, { recursive: true });
|
|
172
218
|
|
|
173
|
-
const sampleTest = `import { describe,
|
|
174
|
-
import
|
|
219
|
+
const sampleTest = `import { describe, it } from "node:test";
|
|
220
|
+
import assert from "node:assert/strict";
|
|
221
|
+
import { createTestRuntime } from "@actiondock/testing";
|
|
175
222
|
import greetAction from "../actions/greet";
|
|
176
223
|
|
|
177
224
|
describe("greet action", () => {
|
|
@@ -181,12 +228,12 @@ describe("greet action", () => {
|
|
|
181
228
|
});
|
|
182
229
|
|
|
183
230
|
const res1 = await runtime.run(greetAction, { name: "Alice" });
|
|
184
|
-
|
|
185
|
-
|
|
231
|
+
assert.equal(res1.message, "Hi, Alice!");
|
|
232
|
+
assert.equal(await runtime.state.get("greet_count"), 1);
|
|
186
233
|
|
|
187
234
|
const res2 = await runtime.run(greetAction, { name: "Bob" });
|
|
188
|
-
|
|
189
|
-
|
|
235
|
+
assert.equal(res2.message, "Hi, Bob!");
|
|
236
|
+
assert.equal(await runtime.state.get("greet_count"), 2);
|
|
190
237
|
});
|
|
191
238
|
});
|
|
192
239
|
`;
|
package/src/project/loader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
4
|
import YAML from "yaml";
|
|
@@ -68,29 +69,28 @@ export function loadProjectConfig(projectRoot: string): ProjectConfig {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
* 探测宿主系统中可用的包管理工具(优先级:
|
|
72
|
+
* 探测宿主系统中可用的包管理工具(优先级:pnpm > npm > yarn > bun)。
|
|
72
73
|
*/
|
|
73
74
|
function getInstallCommand(): string[] {
|
|
74
75
|
const candidates: [string, string][] = [
|
|
75
|
-
["bun", "install"],
|
|
76
76
|
["pnpm", "install"],
|
|
77
|
-
["yarn", "install"],
|
|
78
77
|
["npm", "install"],
|
|
78
|
+
["yarn", "install"],
|
|
79
|
+
["bun", "install"],
|
|
79
80
|
];
|
|
80
81
|
for (const [pm, action] of candidates) {
|
|
81
82
|
try {
|
|
82
|
-
const check =
|
|
83
|
-
|
|
84
|
-
stderr: "pipe",
|
|
83
|
+
const check = spawnSync(pm, ["--version"], {
|
|
84
|
+
stdio: "pipe",
|
|
85
85
|
});
|
|
86
|
-
if (check.
|
|
86
|
+
if (check.status === 0) {
|
|
87
87
|
return [pm, action];
|
|
88
88
|
}
|
|
89
89
|
} catch {
|
|
90
90
|
// 继续探测下一个候选包管理器
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
return ["
|
|
93
|
+
return ["npm", "install"];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
@@ -131,13 +131,12 @@ export function ensureProjectDependencies(projectRoot: string, force = false): b
|
|
|
131
131
|
`[actiondock] Installing dependencies using ${installCmd[0]} for '${pkg.name || basename(projectRoot)}'...\n`
|
|
132
132
|
);
|
|
133
133
|
|
|
134
|
-
const proc =
|
|
134
|
+
const proc = spawnSync(installCmd[0], installCmd.slice(1), {
|
|
135
135
|
cwd: projectRoot,
|
|
136
|
-
|
|
137
|
-
stderr: "pipe",
|
|
136
|
+
stdio: "pipe",
|
|
138
137
|
});
|
|
139
138
|
|
|
140
|
-
if (proc.
|
|
139
|
+
if (proc.status !== 0) {
|
|
141
140
|
const errText = proc.stderr?.toString() || `Unknown error during ${installCmd[0]} install`;
|
|
142
141
|
process.stderr.write(`[actiondock] Warning: Dependency installation failed: ${errText}\n`);
|
|
143
142
|
return false;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ActionDockManifest, ActionManifestEntry } from "./types";
|
|
4
|
+
|
|
5
|
+
export const MANIFEST_FILE_NAME = "actiondock.manifest.json";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 读取并解析项目的声明式清单文件。
|
|
9
|
+
* 若文件不存在则返回 null。
|
|
10
|
+
*/
|
|
11
|
+
export function loadManifest(projectRoot: string): ActionDockManifest | null {
|
|
12
|
+
const filePath = join(projectRoot, MANIFEST_FILE_NAME);
|
|
13
|
+
if (!existsSync(filePath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
18
|
+
const parsed = JSON.parse(raw) as ActionDockManifest;
|
|
19
|
+
if (!parsed || typeof parsed !== "object" || parsed.schemaVersion !== 1) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 保存声明式清单文件至项目根目录。
|
|
30
|
+
*/
|
|
31
|
+
export function saveManifest(projectRoot: string, manifest: ActionDockManifest): void {
|
|
32
|
+
const filePath = join(projectRoot, MANIFEST_FILE_NAME);
|
|
33
|
+
writeFileSync(filePath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 校验清单数据结构的合法性。
|
|
38
|
+
*/
|
|
39
|
+
export function validateManifest(manifest: unknown): { valid: boolean; errors?: string[] } {
|
|
40
|
+
if (!manifest || typeof manifest !== "object") {
|
|
41
|
+
return { valid: false, errors: ["Manifest must be an object"] };
|
|
42
|
+
}
|
|
43
|
+
const m = manifest as ActionDockManifest;
|
|
44
|
+
const errors: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (m.schemaVersion !== 1) {
|
|
47
|
+
errors.push("Manifest 'schemaVersion' must be 1");
|
|
48
|
+
}
|
|
49
|
+
if (!m.actions || typeof m.actions !== "object") {
|
|
50
|
+
errors.push("Manifest 'actions' must be an object");
|
|
51
|
+
} else {
|
|
52
|
+
for (const [actionId, item] of Object.entries(m.actions)) {
|
|
53
|
+
if (!item || typeof item !== "object") {
|
|
54
|
+
errors.push(`Action entry '${actionId}' must be an object`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!item.entry || typeof item.entry !== "string") {
|
|
58
|
+
errors.push(`Action '${actionId}' must specify string 'entry'`);
|
|
59
|
+
}
|
|
60
|
+
if (item.uses && !Array.isArray(item.uses)) {
|
|
61
|
+
errors.push(`Action '${actionId}' property 'uses' must be an array`);
|
|
62
|
+
}
|
|
63
|
+
if (item.tags && !Array.isArray(item.tags)) {
|
|
64
|
+
errors.push(`Action '${actionId}' property 'tags' must be an array`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
valid: errors.length === 0,
|
|
71
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 为单个 Action 构建清单项。
|
|
77
|
+
*/
|
|
78
|
+
export function createManifestEntry(options: {
|
|
79
|
+
entry: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
inputSchema?: Record<string, unknown> | boolean;
|
|
82
|
+
outputSchema?: Record<string, unknown> | boolean;
|
|
83
|
+
uses?: string[];
|
|
84
|
+
tags?: string[];
|
|
85
|
+
annotations?: Record<string, unknown>;
|
|
86
|
+
}): ActionManifestEntry {
|
|
87
|
+
return {
|
|
88
|
+
entry: options.entry,
|
|
89
|
+
description: options.description,
|
|
90
|
+
inputSchema: options.inputSchema,
|
|
91
|
+
outputSchema: options.outputSchema,
|
|
92
|
+
uses: options.uses || [],
|
|
93
|
+
tags: options.tags || [],
|
|
94
|
+
annotations: options.annotations,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/project/types.ts
CHANGED
|
@@ -60,3 +60,33 @@ export interface PlaybookDefinition extends PlaybookFrontmatter {
|
|
|
60
60
|
/** Playbook 源文件的绝对物理路径 */
|
|
61
61
|
filePath: string;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 单个 Action 在清单中的声明项。
|
|
66
|
+
*/
|
|
67
|
+
export interface ActionManifestEntry {
|
|
68
|
+
/** Action 入口文件相对路径(如 "actions/greet.ts") */
|
|
69
|
+
entry: string;
|
|
70
|
+
/** Action 功能描述 */
|
|
71
|
+
description?: string;
|
|
72
|
+
/** 输入参数模式规范 */
|
|
73
|
+
inputSchema?: Record<string, unknown> | boolean;
|
|
74
|
+
/** 输出结果模式规范 */
|
|
75
|
+
outputSchema?: Record<string, unknown> | boolean;
|
|
76
|
+
/** 静态依赖的 Action 列表 */
|
|
77
|
+
uses?: string[];
|
|
78
|
+
/** 标签列表 */
|
|
79
|
+
tags?: string[];
|
|
80
|
+
/** 协议注解元数据 */
|
|
81
|
+
annotations?: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ActionDock 声明式清单(actiondock.manifest.json)规范。
|
|
86
|
+
* 作为元数据的单一事实源,实现无副作用的模块发现与构建规划。
|
|
87
|
+
*/
|
|
88
|
+
export interface ActionDockManifest {
|
|
89
|
+
schemaVersion: number;
|
|
90
|
+
actions: Record<string, ActionManifestEntry>;
|
|
91
|
+
assets?: string[];
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一时间与时钟接口。
|
|
3
|
+
*/
|
|
4
|
+
export interface Clock {
|
|
5
|
+
/** 获取当前系统墙上时间 */
|
|
6
|
+
now(): Date;
|
|
7
|
+
/** 获取单调递增时间戳(单位:毫秒) */
|
|
8
|
+
monotonic(): number;
|
|
9
|
+
/** 异步休眠指定毫秒 */
|
|
10
|
+
sleep(ms: number): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 生产环境系统时钟实现。
|
|
15
|
+
*/
|
|
16
|
+
export class SystemClock implements Clock {
|
|
17
|
+
now(): Date {
|
|
18
|
+
return new Date();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
monotonic(): number {
|
|
22
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
23
|
+
return performance.now();
|
|
24
|
+
}
|
|
25
|
+
return Date.now();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sleep(ms: number): Promise<void> {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let defaultClock: Clock = new SystemClock();
|
|
34
|
+
|
|
35
|
+
export function getSystemClock(): Clock {
|
|
36
|
+
return defaultClock;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setSystemClock(clock: Clock): void {
|
|
40
|
+
defaultClock = clock;
|
|
41
|
+
}
|
package/src/runtime/context.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type {
|
|
2
3
|
ActionContext,
|
|
3
4
|
ActionDefinition,
|
|
4
5
|
ActionInvoker,
|
|
5
6
|
Config,
|
|
6
7
|
Logger,
|
|
8
|
+
ProcessAPI,
|
|
9
|
+
ProgressReporter,
|
|
7
10
|
StateStore,
|
|
8
11
|
} from "@actiondock/sdk";
|
|
9
12
|
import type { ProjectConfig } from "../project/types";
|
|
10
13
|
import { createGlobalStorage } from "../storage";
|
|
11
14
|
import type { RuntimeStorage } from "../storage/types";
|
|
12
15
|
import { resolveEnvValue } from "./env";
|
|
16
|
+
import { getProcessExecutor } from "./process";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* 生产级配置解析器实现。
|
|
@@ -184,8 +188,12 @@ export interface ContextOptions {
|
|
|
184
188
|
overrides?: Record<string, unknown>;
|
|
185
189
|
projectConfig?: ProjectConfig;
|
|
186
190
|
parentRunId?: string;
|
|
191
|
+
runId?: string;
|
|
192
|
+
rootRunId?: string;
|
|
187
193
|
callStack?: string[];
|
|
188
194
|
signal?: AbortSignal;
|
|
195
|
+
process?: ProcessAPI;
|
|
196
|
+
progress?: ProgressReporter;
|
|
189
197
|
onActionInvoke?: (
|
|
190
198
|
action: ActionDefinition,
|
|
191
199
|
input: unknown,
|
|
@@ -196,7 +204,7 @@ export interface ContextOptions {
|
|
|
196
204
|
/**
|
|
197
205
|
* 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
|
|
198
206
|
*
|
|
199
|
-
* @param options
|
|
207
|
+
* @param options 上下文构建参数
|
|
200
208
|
* @returns 组装完毕的 ActionContext 实例
|
|
201
209
|
*/
|
|
202
210
|
export function createActionContext(options: ContextOptions): ActionContext {
|
|
@@ -208,6 +216,8 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
208
216
|
const state = new RuntimeStateStore(options.storage);
|
|
209
217
|
const log = new StderrLogger();
|
|
210
218
|
const signal = options.signal ?? new AbortController().signal;
|
|
219
|
+
const currentRunId = options.runId || randomUUID();
|
|
220
|
+
const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
|
|
211
221
|
|
|
212
222
|
const invoker: ActionInvoker = {
|
|
213
223
|
async invoke<I, O>(action: ActionDefinition<I, O>, input: I): Promise<O> {
|
|
@@ -215,18 +225,30 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
215
225
|
return (await options.onActionInvoke(
|
|
216
226
|
action as any,
|
|
217
227
|
input,
|
|
218
|
-
|
|
228
|
+
currentRunId
|
|
219
229
|
)) as O;
|
|
220
230
|
}
|
|
221
231
|
throw new Error("ActionInvoker not configured with an invocation delegate");
|
|
222
232
|
},
|
|
223
233
|
};
|
|
224
234
|
|
|
235
|
+
const processApi = options.process || getProcessExecutor();
|
|
236
|
+
const progressApi: ProgressReporter = options.progress || {
|
|
237
|
+
report() {},
|
|
238
|
+
};
|
|
239
|
+
|
|
225
240
|
return {
|
|
226
241
|
config,
|
|
227
242
|
state,
|
|
228
243
|
actions: invoker,
|
|
244
|
+
process: processApi,
|
|
229
245
|
log,
|
|
246
|
+
progress: progressApi,
|
|
230
247
|
signal,
|
|
248
|
+
run: {
|
|
249
|
+
id: currentRunId,
|
|
250
|
+
rootId: currentRootRunId,
|
|
251
|
+
parentId: options.parentRunId,
|
|
252
|
+
},
|
|
231
253
|
};
|
|
232
254
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ExecutionEvent } from "@actiondock/sdk";
|
|
2
|
+
|
|
3
|
+
export interface EventSink {
|
|
4
|
+
emit(event: ExecutionEvent): void;
|
|
5
|
+
subscribe(
|
|
6
|
+
runId: string,
|
|
7
|
+
options?: { after?: number; signal?: AbortSignal }
|
|
8
|
+
): AsyncIterable<ExecutionEvent>;
|
|
9
|
+
clear(runId: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 进程内有界事件缓冲区实现。
|
|
14
|
+
* 遵循设计文档:每个运行最多保留 1024 条或 1MB 事件,支持单调序号。
|
|
15
|
+
*/
|
|
16
|
+
export class InMemoryEventSink implements EventSink {
|
|
17
|
+
private eventsByRun = new Map<string, ExecutionEvent[]>();
|
|
18
|
+
private listeners = new Map<string, Set<(event: ExecutionEvent) => void>>();
|
|
19
|
+
private maxEventsPerRun = 1024;
|
|
20
|
+
|
|
21
|
+
emit(event: ExecutionEvent): void {
|
|
22
|
+
let list = this.eventsByRun.get(event.runId);
|
|
23
|
+
if (!list) {
|
|
24
|
+
list = [];
|
|
25
|
+
this.eventsByRun.set(event.runId, list);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (list.length >= this.maxEventsPerRun) {
|
|
29
|
+
// 淘汰旧日志和进度事件,保留状态和结果
|
|
30
|
+
const nonEssentialIndex = list.findIndex((e) => e.type === "log" || e.type === "progress");
|
|
31
|
+
if (nonEssentialIndex >= 0) {
|
|
32
|
+
list.splice(nonEssentialIndex, 1);
|
|
33
|
+
} else {
|
|
34
|
+
list.shift();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
list.push(event);
|
|
38
|
+
|
|
39
|
+
const subs = this.listeners.get(event.runId);
|
|
40
|
+
if (subs) {
|
|
41
|
+
for (const listener of subs) {
|
|
42
|
+
try {
|
|
43
|
+
listener(event);
|
|
44
|
+
} catch {
|
|
45
|
+
// 忽略单个监听器内部异常
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async *subscribe(
|
|
52
|
+
runId: string,
|
|
53
|
+
options: { after?: number; signal?: AbortSignal } = {}
|
|
54
|
+
): AsyncIterable<ExecutionEvent> {
|
|
55
|
+
const after = options.after ?? -1;
|
|
56
|
+
const history = this.eventsByRun.get(runId) || [];
|
|
57
|
+
|
|
58
|
+
for (const evt of history) {
|
|
59
|
+
if (evt.sequence > after) {
|
|
60
|
+
yield evt;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lastEvt = history[history.length - 1];
|
|
65
|
+
if (lastEvt && (lastEvt.type === "finish" || lastEvt.type === "status" && lastEvt.status !== "running")) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const queue: ExecutionEvent[] = [];
|
|
70
|
+
let notify: (() => void) | null = null;
|
|
71
|
+
let done = false;
|
|
72
|
+
|
|
73
|
+
const listener = (evt: ExecutionEvent) => {
|
|
74
|
+
if (evt.sequence > after) {
|
|
75
|
+
queue.push(evt);
|
|
76
|
+
if (notify) {
|
|
77
|
+
notify();
|
|
78
|
+
notify = null;
|
|
79
|
+
}
|
|
80
|
+
if (evt.type === "finish") {
|
|
81
|
+
done = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let subs = this.listeners.get(runId);
|
|
87
|
+
if (!subs) {
|
|
88
|
+
subs = new Set();
|
|
89
|
+
this.listeners.set(runId, subs);
|
|
90
|
+
}
|
|
91
|
+
subs.add(listener);
|
|
92
|
+
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
subs?.delete(listener);
|
|
95
|
+
if (subs && subs.size === 0) {
|
|
96
|
+
this.listeners.delete(runId);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (options.signal) {
|
|
101
|
+
options.signal.addEventListener("abort", () => {
|
|
102
|
+
done = true;
|
|
103
|
+
if (notify) {
|
|
104
|
+
notify();
|
|
105
|
+
notify = null;
|
|
106
|
+
}
|
|
107
|
+
}, { once: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
while (!done && !options.signal?.aborted) {
|
|
112
|
+
if (queue.length > 0) {
|
|
113
|
+
yield queue.shift()!;
|
|
114
|
+
} else {
|
|
115
|
+
await new Promise<void>((resolve) => {
|
|
116
|
+
notify = resolve;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
while (queue.length > 0) {
|
|
121
|
+
yield queue.shift()!;
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
cleanup();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
clear(runId: string): void {
|
|
129
|
+
this.eventsByRun.delete(runId);
|
|
130
|
+
this.listeners.delete(runId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let defaultEventSink: EventSink = new InMemoryEventSink();
|
|
135
|
+
|
|
136
|
+
export function getDefaultEventSink(): EventSink {
|
|
137
|
+
return defaultEventSink;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function setDefaultEventSink(sink: EventSink): void {
|
|
141
|
+
defaultEventSink = sink;
|
|
142
|
+
}
|