@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.
- package/package.json +2 -2
- package/src/build/index.ts +0 -1
- package/src/build/templates.ts +1 -1
- package/src/catalog/types.ts +1 -9
- package/src/doctor/doctor.ts +99 -38
- package/src/execution/service.ts +47 -7
- package/src/export/index.ts +0 -1
- package/src/export/templates.ts +348 -59
- package/src/index.ts +1 -0
- package/src/project/init.ts +3 -2
- package/src/project/loader.ts +46 -7
- package/src/project/manifest.ts +205 -2
- package/src/project/types.ts +49 -0
- package/src/registry/registry.ts +271 -34
- package/src/runtime/context.ts +8 -3
- package/src/runtime/index.ts +1 -0
- package/src/runtime/module-loader.ts +67 -0
- package/src/runtime/process.ts +12 -11
- package/src/runtime/runner.ts +193 -68
- package/src/server/index.ts +1 -0
- package/src/server/routes/actions.ts +262 -0
- package/src/server/routes/common.ts +126 -0
- package/src/server/routes/config.ts +147 -0
- package/src/server/routes/doctor.ts +29 -0
- package/src/server/routes/health.ts +40 -0
- package/src/server/routes/index.ts +9 -0
- package/src/server/routes/info.ts +221 -0
- package/src/server/routes/playbooks.ts +109 -0
- package/src/server/routes/runs.ts +301 -0
- package/src/server/routes/state.ts +133 -0
- package/src/server/server.ts +82 -1323
- package/src/storage/index.ts +20 -4
- package/src/storage/sqlite.ts +60 -31
- package/src/storage/types.ts +8 -0
- package/src/utils/index.ts +2 -0
- package/src/build/builder.ts +0 -218
- package/src/export/skill.ts +0 -350
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { filterWithFallbackInfo } from "../../filter";
|
|
3
|
+
import { loadActions, loadPlaybooks, loadProjectConfig } from "../../project/loader";
|
|
4
|
+
import { getRegistryStatus, listLinkedPackages } from "../../registry/registry";
|
|
5
|
+
import { type RouteContext, jsonResponse } from "./common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 处理系统探索与自省接口(GET /api/v1/info)。
|
|
9
|
+
*/
|
|
10
|
+
export async function handleInfoRoute(ctx: RouteContext): Promise<Response | null> {
|
|
11
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome, options } = ctx;
|
|
12
|
+
|
|
13
|
+
if (pathname !== "/api/v1/info" || req.method !== "GET") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const isTree = url.searchParams.get("tree") === "true";
|
|
19
|
+
if (isTree) {
|
|
20
|
+
const status = getRegistryStatus(customHome);
|
|
21
|
+
return jsonResponse(
|
|
22
|
+
{ ok: true, type: "tree", ...status },
|
|
23
|
+
200,
|
|
24
|
+
corsHeaders
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
29
|
+
const targetPkg = url.searchParams.get("package") || undefined;
|
|
30
|
+
|
|
31
|
+
// 收集所有候选包全量元数据
|
|
32
|
+
const aggregatedPackages: any[] = [];
|
|
33
|
+
|
|
34
|
+
if (projectRoot) {
|
|
35
|
+
try {
|
|
36
|
+
const config = loadProjectConfig(projectRoot);
|
|
37
|
+
const actions = await loadActions(projectRoot, config.actionsDir);
|
|
38
|
+
const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
|
|
39
|
+
aggregatedPackages.push({
|
|
40
|
+
id: config.id,
|
|
41
|
+
name: config.name,
|
|
42
|
+
version: config.version,
|
|
43
|
+
description: config.description || "",
|
|
44
|
+
path: projectRoot,
|
|
45
|
+
actionsCount: actions.size,
|
|
46
|
+
playbooksCount: playbooks.size,
|
|
47
|
+
actions: Array.from(actions.entries()).map(([id, a]) => ({
|
|
48
|
+
id,
|
|
49
|
+
description: a.description || "",
|
|
50
|
+
inputSchema: a.inputSchema || null,
|
|
51
|
+
outputSchema: a.outputSchema || null,
|
|
52
|
+
})),
|
|
53
|
+
playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
|
|
54
|
+
id,
|
|
55
|
+
description: p.description || "",
|
|
56
|
+
actions: p.actions || [],
|
|
57
|
+
})),
|
|
58
|
+
configDeclared: config.config || {},
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
// 忽略解析异常
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const linked = listLinkedPackages(customHome);
|
|
65
|
+
for (const pkg of linked) {
|
|
66
|
+
if (!existsSync(pkg.path)) continue;
|
|
67
|
+
try {
|
|
68
|
+
const config = loadProjectConfig(pkg.path);
|
|
69
|
+
const actions = await loadActions(pkg.path, config.actionsDir);
|
|
70
|
+
const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
|
|
71
|
+
aggregatedPackages.push({
|
|
72
|
+
id: config.id,
|
|
73
|
+
name: config.name,
|
|
74
|
+
version: config.version,
|
|
75
|
+
description: config.description || "",
|
|
76
|
+
path: pkg.path,
|
|
77
|
+
actionsCount: actions.size,
|
|
78
|
+
playbooksCount: playbooks.size,
|
|
79
|
+
actions: Array.from(actions.entries()).map(([id, a]) => ({
|
|
80
|
+
id,
|
|
81
|
+
description: a.description || "",
|
|
82
|
+
inputSchema: a.inputSchema || null,
|
|
83
|
+
outputSchema: a.outputSchema || null,
|
|
84
|
+
})),
|
|
85
|
+
playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
|
|
86
|
+
id,
|
|
87
|
+
description: p.description || "",
|
|
88
|
+
actions: p.actions || [],
|
|
89
|
+
})),
|
|
90
|
+
configDeclared: config.config || {},
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
// 忽略故障包
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 显式指定 package 详情下钻
|
|
99
|
+
if (targetPkg) {
|
|
100
|
+
const matched = aggregatedPackages.find(
|
|
101
|
+
(p) => p.id === targetPkg || p.path === targetPkg
|
|
102
|
+
);
|
|
103
|
+
if (!matched) {
|
|
104
|
+
return jsonResponse(
|
|
105
|
+
{
|
|
106
|
+
ok: false,
|
|
107
|
+
error: {
|
|
108
|
+
code: "PACKAGE_NOT_FOUND",
|
|
109
|
+
message: `Package '${targetPkg}' not found on remote server`,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
404,
|
|
113
|
+
corsHeaders
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return jsonResponse(
|
|
117
|
+
{
|
|
118
|
+
ok: true,
|
|
119
|
+
type: "package_detail",
|
|
120
|
+
...matched,
|
|
121
|
+
...(options.exposeDebugInfo ? { projectRoot: matched.path } : {}),
|
|
122
|
+
},
|
|
123
|
+
200,
|
|
124
|
+
corsHeaders
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 意图过滤与决议
|
|
129
|
+
if (intent) {
|
|
130
|
+
const filterRes = filterWithFallbackInfo(
|
|
131
|
+
aggregatedPackages,
|
|
132
|
+
intent,
|
|
133
|
+
[
|
|
134
|
+
(p) => p.id,
|
|
135
|
+
(p) => p.name,
|
|
136
|
+
(p) => p.description,
|
|
137
|
+
(p) => p.actions.map((a: any) => a.id),
|
|
138
|
+
(p) => p.actions.map((a: any) => a.description),
|
|
139
|
+
(p) => p.playbooks.map((pb: any) => pb.id),
|
|
140
|
+
(p) => p.playbooks.map((pb: any) => pb.description),
|
|
141
|
+
],
|
|
142
|
+
true
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// 唯一命中智能展开详情
|
|
146
|
+
if (filterRes.matchedCount === 1) {
|
|
147
|
+
const single = filterRes.items[0];
|
|
148
|
+
return jsonResponse(
|
|
149
|
+
{
|
|
150
|
+
ok: true,
|
|
151
|
+
type: "package_detail",
|
|
152
|
+
isSingleMatch: true,
|
|
153
|
+
...single,
|
|
154
|
+
...(options.exposeDebugInfo ? { projectRoot: single.path } : {}),
|
|
155
|
+
},
|
|
156
|
+
200,
|
|
157
|
+
corsHeaders
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return jsonResponse(
|
|
162
|
+
{
|
|
163
|
+
ok: true,
|
|
164
|
+
type: "package_list",
|
|
165
|
+
isFallback: filterRes.isFallback,
|
|
166
|
+
packages: filterRes.items,
|
|
167
|
+
},
|
|
168
|
+
200,
|
|
169
|
+
corsHeaders
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 单包模式直接展开详情
|
|
174
|
+
if (projectRoot && aggregatedPackages.length === 1) {
|
|
175
|
+
const p = aggregatedPackages[0];
|
|
176
|
+
return jsonResponse(
|
|
177
|
+
{
|
|
178
|
+
ok: true,
|
|
179
|
+
type: "package_detail",
|
|
180
|
+
id: p.id,
|
|
181
|
+
name: p.name,
|
|
182
|
+
version: p.version,
|
|
183
|
+
description: p.description,
|
|
184
|
+
actionsCount: p.actionsCount,
|
|
185
|
+
playbooksCount: p.playbooksCount,
|
|
186
|
+
actions: p.actions.map((a: any) => a.id),
|
|
187
|
+
actionsDetail: p.actions,
|
|
188
|
+
playbooks: p.playbooks.map((pb: any) => pb.id),
|
|
189
|
+
playbooksDetail: p.playbooks,
|
|
190
|
+
configDeclared: p.configDeclared,
|
|
191
|
+
linkedPackages: listLinkedPackages(customHome),
|
|
192
|
+
...(options.exposeDebugInfo ? { projectRoot } : {}),
|
|
193
|
+
},
|
|
194
|
+
200,
|
|
195
|
+
corsHeaders
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 全局注册表概览
|
|
200
|
+
return jsonResponse(
|
|
201
|
+
{
|
|
202
|
+
ok: true,
|
|
203
|
+
type: "package_list",
|
|
204
|
+
version: "2.0.0",
|
|
205
|
+
packages: aggregatedPackages,
|
|
206
|
+
linkedPackages: listLinkedPackages(customHome),
|
|
207
|
+
},
|
|
208
|
+
200,
|
|
209
|
+
corsHeaders
|
|
210
|
+
);
|
|
211
|
+
} catch (err: any) {
|
|
212
|
+
return jsonResponse(
|
|
213
|
+
{
|
|
214
|
+
ok: false,
|
|
215
|
+
error: { code: "INFO_ERROR", message: err.message },
|
|
216
|
+
},
|
|
217
|
+
500,
|
|
218
|
+
corsHeaders
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { filterByIntent } from "../../filter";
|
|
3
|
+
import { loadPlaybooks, loadProjectConfig } from "../../project/loader";
|
|
4
|
+
import { listLinkedPackages, resolvePlaybookProject } from "../../registry/registry";
|
|
5
|
+
import { type RouteContext, jsonResponse } from "./common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 处理 Playbook 列表与 SOP 详情接口。
|
|
9
|
+
*/
|
|
10
|
+
export async function handlePlaybooksRoutes(ctx: RouteContext): Promise<Response | null> {
|
|
11
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome } = ctx;
|
|
12
|
+
|
|
13
|
+
// 1. Playbooks List: GET /api/v1/playbooks
|
|
14
|
+
if (pathname === "/api/v1/playbooks" && req.method === "GET") {
|
|
15
|
+
try {
|
|
16
|
+
const pbList: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
description: string;
|
|
19
|
+
actions: string[];
|
|
20
|
+
packageId: string;
|
|
21
|
+
filePath: string;
|
|
22
|
+
}> = [];
|
|
23
|
+
|
|
24
|
+
const targetPkg = url.searchParams.get("package");
|
|
25
|
+
const intent = url.searchParams.get("intent");
|
|
26
|
+
|
|
27
|
+
const roots: Array<{ root: string; packageId: string }> = [];
|
|
28
|
+
if (projectRoot) {
|
|
29
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
30
|
+
roots.push({ root: projectRoot, packageId: cfg.id });
|
|
31
|
+
}
|
|
32
|
+
const linked = listLinkedPackages(customHome);
|
|
33
|
+
for (const pkg of linked) {
|
|
34
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
35
|
+
if (!existsSync(pkg.path)) continue;
|
|
36
|
+
roots.push({ root: pkg.path, packageId: pkg.id });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const item of roots) {
|
|
40
|
+
if (targetPkg && item.packageId !== targetPkg && item.root !== targetPkg) continue;
|
|
41
|
+
try {
|
|
42
|
+
const cfg = loadProjectConfig(item.root);
|
|
43
|
+
const pbs = loadPlaybooks(item.root, cfg.playbooksDir);
|
|
44
|
+
for (const [id, pb] of pbs.entries()) {
|
|
45
|
+
pbList.push({
|
|
46
|
+
id,
|
|
47
|
+
description: pb.description || "",
|
|
48
|
+
actions: pb.actions || [],
|
|
49
|
+
packageId: item.packageId,
|
|
50
|
+
filePath: pb.filePath,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const filtered = intent
|
|
57
|
+
? filterByIntent(
|
|
58
|
+
pbList,
|
|
59
|
+
intent,
|
|
60
|
+
[(p) => p.id, (p) => p.description, (p) => p.packageId, (p) => p.actions],
|
|
61
|
+
false
|
|
62
|
+
)
|
|
63
|
+
: pbList;
|
|
64
|
+
|
|
65
|
+
return jsonResponse(filtered, 200, corsHeaders);
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
return jsonResponse(
|
|
68
|
+
{ ok: false, error: { code: "PLAYBOOKS_LIST_ERROR", message: err.message } },
|
|
69
|
+
500,
|
|
70
|
+
corsHeaders
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Playbook Show: GET /api/v1/playbooks/:id
|
|
76
|
+
const pbShowMatch = pathname.match(/^\/api\/v1\/playbooks\/([^/]+)$/);
|
|
77
|
+
if (pbShowMatch && req.method === "GET") {
|
|
78
|
+
const pbId = decodeURIComponent(pbShowMatch[1]);
|
|
79
|
+
try {
|
|
80
|
+
const resolved = resolvePlaybookProject(
|
|
81
|
+
pbId,
|
|
82
|
+
projectRoot || process.cwd(),
|
|
83
|
+
customHome
|
|
84
|
+
);
|
|
85
|
+
const pb = resolved.playbook;
|
|
86
|
+
return jsonResponse(
|
|
87
|
+
{
|
|
88
|
+
ok: true,
|
|
89
|
+
id: pb.id,
|
|
90
|
+
packageId: resolved.packageId,
|
|
91
|
+
description: pb.description || "",
|
|
92
|
+
actions: pb.actions || [],
|
|
93
|
+
filePath: pb.filePath,
|
|
94
|
+
content: pb.content,
|
|
95
|
+
},
|
|
96
|
+
200,
|
|
97
|
+
corsHeaders
|
|
98
|
+
);
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
return jsonResponse(
|
|
101
|
+
{ ok: false, error: { code: "PLAYBOOK_NOT_FOUND", message: err.message } },
|
|
102
|
+
404,
|
|
103
|
+
corsHeaders
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { filterByIntent } from "../../filter";
|
|
3
|
+
import { loadProjectConfig } from "../../project/loader";
|
|
4
|
+
import { listLinkedPackages } from "../../registry/registry";
|
|
5
|
+
import type { RuntimeStorage } from "../../storage/types";
|
|
6
|
+
import { readJsonBody } from "../body";
|
|
7
|
+
import { type RouteContext, findRunAcrossStorages, jsonResponse } from "./common";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 处理历史运行记录检索、清理、详情及 SSE 流式日志接口。
|
|
11
|
+
*/
|
|
12
|
+
export async function handleRunsRoutes(ctx: RouteContext): Promise<Response | null> {
|
|
13
|
+
const { req, url, pathname, corsHeaders, projectRoot, customHome, runtimeRegistry, options } = ctx;
|
|
14
|
+
|
|
15
|
+
// 1. Runs List: GET /api/v1/runs
|
|
16
|
+
if (pathname === "/api/v1/runs" && req.method === "GET") {
|
|
17
|
+
try {
|
|
18
|
+
const status = url.searchParams.get("status") || undefined;
|
|
19
|
+
const actionId = url.searchParams.get("actionId") || undefined;
|
|
20
|
+
const packageId = url.searchParams.get("packageId") || undefined;
|
|
21
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
22
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
23
|
+
|
|
24
|
+
const allRuns: any[] = [];
|
|
25
|
+
const seenRunIds = new Set<string>();
|
|
26
|
+
|
|
27
|
+
const candidateStorages: Array<{ packageId: string; storage: RuntimeStorage }> = [];
|
|
28
|
+
if (projectRoot) {
|
|
29
|
+
try {
|
|
30
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
31
|
+
candidateStorages.push({
|
|
32
|
+
packageId: cfg.id,
|
|
33
|
+
storage: runtimeRegistry.getStorage(cfg.id, projectRoot),
|
|
34
|
+
});
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
const linked = listLinkedPackages(customHome);
|
|
38
|
+
for (const pkg of linked) {
|
|
39
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
40
|
+
if (!existsSync(pkg.path)) continue;
|
|
41
|
+
candidateStorages.push({
|
|
42
|
+
packageId: pkg.id,
|
|
43
|
+
storage: runtimeRegistry.getStorage(pkg.id, pkg.path),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const item of candidateStorages) {
|
|
48
|
+
if (packageId && item.packageId !== packageId) continue;
|
|
49
|
+
try {
|
|
50
|
+
const records = item.storage.listRuns({ actionId, limit });
|
|
51
|
+
for (const r of records) {
|
|
52
|
+
if (!seenRunIds.has(r.id)) {
|
|
53
|
+
seenRunIds.add(r.id);
|
|
54
|
+
if (status && r.status !== status) continue;
|
|
55
|
+
allRuns.push(r);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
62
|
+
|
|
63
|
+
const filtered = intent
|
|
64
|
+
? filterByIntent(
|
|
65
|
+
allRuns,
|
|
66
|
+
intent,
|
|
67
|
+
[(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.packageId],
|
|
68
|
+
false
|
|
69
|
+
)
|
|
70
|
+
: allRuns;
|
|
71
|
+
|
|
72
|
+
const sliced = filtered.slice(0, limit);
|
|
73
|
+
return jsonResponse(
|
|
74
|
+
{ ok: true, total: filtered.length, items: sliced },
|
|
75
|
+
200,
|
|
76
|
+
corsHeaders
|
|
77
|
+
);
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
return jsonResponse(
|
|
80
|
+
{ ok: false, error: { code: "RUNS_LIST_ERROR", message: err.message } },
|
|
81
|
+
500,
|
|
82
|
+
corsHeaders
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Runs Clear: POST /api/v1/runs/clear or DELETE /api/v1/runs
|
|
88
|
+
if (
|
|
89
|
+
(pathname === "/api/v1/runs/clear" && req.method === "POST") ||
|
|
90
|
+
(pathname === "/api/v1/runs" && req.method === "DELETE")
|
|
91
|
+
) {
|
|
92
|
+
try {
|
|
93
|
+
let body: any = {};
|
|
94
|
+
if (req.method === "POST" || req.headers.get("content-type")?.includes("json")) {
|
|
95
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
|
|
96
|
+
}
|
|
97
|
+
const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
|
|
98
|
+
const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
|
|
99
|
+
const status = url.searchParams.get("status") || body.status || undefined;
|
|
100
|
+
|
|
101
|
+
let clearedCount = 0;
|
|
102
|
+
const candidateStorages: RuntimeStorage[] = [];
|
|
103
|
+
if (projectRoot) {
|
|
104
|
+
try {
|
|
105
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
106
|
+
candidateStorages.push(runtimeRegistry.getStorage(cfg.id, projectRoot));
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
const linked = listLinkedPackages(customHome);
|
|
110
|
+
for (const pkg of linked) {
|
|
111
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
112
|
+
if (!existsSync(pkg.path)) continue;
|
|
113
|
+
if (packageId && pkg.id !== packageId) continue;
|
|
114
|
+
candidateStorages.push(runtimeRegistry.getStorage(pkg.id, pkg.path));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const storage of candidateStorages) {
|
|
118
|
+
clearedCount += storage.clearRuns({ actionId, status });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
return jsonResponse(
|
|
124
|
+
{ ok: false, error: { code: "RUNS_CLEAR_ERROR", message: err.message } },
|
|
125
|
+
500,
|
|
126
|
+
corsHeaders
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Run Stream (SSE): GET /api/v1/runs/:runId/stream
|
|
132
|
+
const runStreamMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/stream$/);
|
|
133
|
+
if (runStreamMatch && req.method === "GET") {
|
|
134
|
+
const runId = decodeURIComponent(runStreamMatch[1]);
|
|
135
|
+
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
136
|
+
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
137
|
+
|
|
138
|
+
if (!found && !activeHandle) {
|
|
139
|
+
return jsonResponse(
|
|
140
|
+
{ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run '${runId}' not found` } },
|
|
141
|
+
404,
|
|
142
|
+
corsHeaders
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const stream = new ReadableStream({
|
|
147
|
+
start(controller) {
|
|
148
|
+
const encoder = new TextEncoder();
|
|
149
|
+
const sendEvent = (event: string, data: any) => {
|
|
150
|
+
try {
|
|
151
|
+
controller.enqueue(
|
|
152
|
+
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
153
|
+
);
|
|
154
|
+
} catch {}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (activeHandle) {
|
|
158
|
+
sendEvent("status", { runId, status: "running" });
|
|
159
|
+
const unsubscribe = runtimeRegistry.subscribe(runId, (evt) => {
|
|
160
|
+
sendEvent(evt.type, evt.data);
|
|
161
|
+
if (evt.type === "finish") {
|
|
162
|
+
try { controller.close(); } catch {}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
activeHandle.result.then(
|
|
167
|
+
(res) => {
|
|
168
|
+
sendEvent("finish", res);
|
|
169
|
+
try { controller.close(); } catch {}
|
|
170
|
+
},
|
|
171
|
+
(err) => {
|
|
172
|
+
sendEvent("finish", {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: { message: err?.message || String(err) },
|
|
175
|
+
});
|
|
176
|
+
try { controller.close(); } catch {}
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
req.signal.addEventListener("abort", () => {
|
|
181
|
+
unsubscribe();
|
|
182
|
+
try { controller.close(); } catch {}
|
|
183
|
+
});
|
|
184
|
+
} else if (found) {
|
|
185
|
+
sendEvent("finish", found.run);
|
|
186
|
+
controller.close();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return new Response(stream, {
|
|
192
|
+
status: 200,
|
|
193
|
+
headers: {
|
|
194
|
+
"Content-Type": "text/event-stream",
|
|
195
|
+
"Cache-Control": "no-cache",
|
|
196
|
+
"Connection": "keep-alive",
|
|
197
|
+
...corsHeaders,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 4. Run Show: GET /api/v1/runs/:runId
|
|
203
|
+
const runShowMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
|
|
204
|
+
if (runShowMatch && req.method === "GET") {
|
|
205
|
+
const runId = decodeURIComponent(runShowMatch[1]);
|
|
206
|
+
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
207
|
+
|
|
208
|
+
if (!found) {
|
|
209
|
+
return jsonResponse(
|
|
210
|
+
{
|
|
211
|
+
ok: false,
|
|
212
|
+
error: {
|
|
213
|
+
code: "RUN_NOT_FOUND",
|
|
214
|
+
message: `Run '${runId}' not found`,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
404,
|
|
218
|
+
corsHeaders
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return jsonResponse(found.run, 200, corsHeaders);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 5. Run Cancel: POST /api/v1/runs/:runId/cancel
|
|
226
|
+
const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
|
|
227
|
+
if (runCancelMatch && req.method === "POST") {
|
|
228
|
+
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
229
|
+
let body: any = {};
|
|
230
|
+
try {
|
|
231
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
232
|
+
} catch {
|
|
233
|
+
// Body is optional
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const reason = body?.reason || "Cancelled by client request";
|
|
237
|
+
|
|
238
|
+
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
239
|
+
if (activeHandle) {
|
|
240
|
+
const cancelled = runtimeRegistry.executionManager.cancel(runId, reason);
|
|
241
|
+
if (cancelled) {
|
|
242
|
+
return jsonResponse(
|
|
243
|
+
{
|
|
244
|
+
ok: true,
|
|
245
|
+
runId,
|
|
246
|
+
status: "cancelled",
|
|
247
|
+
},
|
|
248
|
+
200,
|
|
249
|
+
corsHeaders
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
255
|
+
if (!found) {
|
|
256
|
+
return jsonResponse(
|
|
257
|
+
{
|
|
258
|
+
ok: false,
|
|
259
|
+
error: {
|
|
260
|
+
code: "RUN_NOT_FOUND",
|
|
261
|
+
message: `Run '${runId}' not found`,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
404,
|
|
265
|
+
corsHeaders
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { storage, run } = found;
|
|
270
|
+
if (run.status === "success" || run.status === "failed" || run.status === "cancelled") {
|
|
271
|
+
return jsonResponse(
|
|
272
|
+
{
|
|
273
|
+
ok: false,
|
|
274
|
+
error: {
|
|
275
|
+
code: "RUN_ALREADY_FINISHED",
|
|
276
|
+
message: `Run '${runId}' has already finished with status '${run.status}'`,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
409,
|
|
280
|
+
corsHeaders
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
storage.updateRun(runId, "cancelled", undefined, {
|
|
285
|
+
code: "ACTION_CANCELLED",
|
|
286
|
+
message: reason,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return jsonResponse(
|
|
290
|
+
{
|
|
291
|
+
ok: true,
|
|
292
|
+
runId,
|
|
293
|
+
status: "cancelled",
|
|
294
|
+
},
|
|
295
|
+
200,
|
|
296
|
+
corsHeaders
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return null;
|
|
301
|
+
}
|