@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.
@@ -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
+ }