@actiondock/core 2.0.1 → 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/profile/client.ts +252 -2
- 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/runtime-registry.ts +35 -0
- package/src/server/server.ts +994 -74
- package/src/server/types.ts +15 -0
- package/src/storage/driver.ts +126 -0
- package/src/storage/index.ts +4 -3
- package/src/storage/sqlite.ts +365 -258
- package/src/storage/types.ts +42 -22
package/src/server/server.ts
CHANGED
|
@@ -1,19 +1,124 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { runDoctorChecks } from "../doctor/doctor";
|
|
5
|
+
import { filterByIntent, filterWithFallbackInfo } from "../filter";
|
|
5
6
|
import {
|
|
6
7
|
findProjectRoot,
|
|
7
8
|
loadActions,
|
|
8
9
|
loadPlaybooks,
|
|
9
10
|
loadProjectConfig,
|
|
10
11
|
} from "../project/loader";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getRegistryStatus,
|
|
14
|
+
listLinkedPackages,
|
|
15
|
+
resolveActionProject,
|
|
16
|
+
resolvePackageRoot,
|
|
17
|
+
resolvePlaybookProject,
|
|
18
|
+
} from "../registry/registry";
|
|
12
19
|
import { ActionRunner } from "../runtime/runner";
|
|
20
|
+
import type { RuntimeStorage } from "../storage/types";
|
|
21
|
+
import { createServer as createNodeHttpServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
22
|
+
import { Readable } from "node:stream";
|
|
23
|
+
import { pipeline } from "node:stream/promises";
|
|
13
24
|
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "./body";
|
|
14
25
|
import { ServerRuntimeRegistry } from "./runtime-registry";
|
|
15
26
|
import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security";
|
|
16
|
-
import type { ActionDockServerInstance, ServerOptions } from "./types";
|
|
27
|
+
import type { ActionDockServerInstance, CoreHttpServerFactory, CoreHttpServerInstance, ServerOptions } from "./types";
|
|
28
|
+
|
|
29
|
+
let customHttpServerFactory: CoreHttpServerFactory | undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 注册自定义 HTTP 服务端工厂(用于 Node.js / Bun 运行时环境适配)。
|
|
33
|
+
*/
|
|
34
|
+
export function setHttpServerFactory(factory: CoreHttpServerFactory): void {
|
|
35
|
+
customHttpServerFactory = factory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
|
|
40
|
+
*/
|
|
41
|
+
export function launchHttpServer(
|
|
42
|
+
port: number,
|
|
43
|
+
host: string,
|
|
44
|
+
fetchHandler: (req: Request) => Promise<Response>
|
|
45
|
+
): CoreHttpServerInstance {
|
|
46
|
+
if (customHttpServerFactory) {
|
|
47
|
+
return customHttpServerFactory({ port, host, fetch: fetchHandler }) as CoreHttpServerInstance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 若处于原生 Bun 运行时
|
|
51
|
+
if (typeof (globalThis as any).Bun !== "undefined" && typeof (globalThis as any).Bun.serve === "function") {
|
|
52
|
+
const bunServer = (globalThis as any).Bun.serve({
|
|
53
|
+
port,
|
|
54
|
+
hostname: host,
|
|
55
|
+
fetch: fetchHandler,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
port: bunServer.port,
|
|
59
|
+
stop: (closeActive?: boolean) => bunServer.stop(closeActive),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Node.js 原生 node:http 兜底实现
|
|
64
|
+
const srv = createNodeHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
65
|
+
try {
|
|
66
|
+
const protocol = (req.socket as any)?.encrypted ? "https" : "http";
|
|
67
|
+
const hostHeader = req.headers.host || "127.0.0.1";
|
|
68
|
+
const url = new URL(req.url || "/", `${protocol}://${hostHeader}`).href;
|
|
69
|
+
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
72
|
+
if (v === undefined) continue;
|
|
73
|
+
if (Array.isArray(v)) {
|
|
74
|
+
for (const item of v) headers.append(k, item);
|
|
75
|
+
} else {
|
|
76
|
+
headers.set(k, v);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const method = (req.method || "GET").toUpperCase();
|
|
81
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
82
|
+
const init: RequestInit = { method, headers };
|
|
83
|
+
if (hasBody) {
|
|
84
|
+
(init as any).body = Readable.toWeb(req);
|
|
85
|
+
(init as any).duplex = "half";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const webReq = new Request(url, init);
|
|
89
|
+
const webRes = await fetchHandler(webReq);
|
|
90
|
+
|
|
91
|
+
res.statusCode = webRes.status;
|
|
92
|
+
if (webRes.statusText) res.statusMessage = webRes.statusText;
|
|
93
|
+
webRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
94
|
+
|
|
95
|
+
if (!webRes.body) {
|
|
96
|
+
res.end();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await pipeline(Readable.fromWeb(webRes.body as any), res);
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
if (!res.headersSent) {
|
|
102
|
+
res.statusCode = 500;
|
|
103
|
+
res.end(JSON.stringify({ error: err?.message || String(err) }));
|
|
104
|
+
} else {
|
|
105
|
+
res.destroy(err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
srv.listen(port, host);
|
|
111
|
+
const addr = srv.address();
|
|
112
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
port: actualPort,
|
|
116
|
+
stop: () => {
|
|
117
|
+
srv.close();
|
|
118
|
+
(srv as any).closeAllConnections?.();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
17
122
|
|
|
18
123
|
/**
|
|
19
124
|
* 辅助函数:快速构造带 CORS 头的 JSON HTTP 响应。
|
|
@@ -70,20 +175,83 @@ function findRunAcrossStorages(
|
|
|
70
175
|
return null;
|
|
71
176
|
}
|
|
72
177
|
|
|
178
|
+
/**
|
|
179
|
+
* 辅助函数:依据 package 参数或项目上下文解析目标 Storage 实例与根目录。
|
|
180
|
+
*/
|
|
181
|
+
function resolveStorageForPackage(
|
|
182
|
+
packageIdOrPath: string | undefined,
|
|
183
|
+
runtimeRegistry: ServerRuntimeRegistry,
|
|
184
|
+
projectRoot?: string | null,
|
|
185
|
+
customHome?: string
|
|
186
|
+
): { packageId: string; storage: RuntimeStorage; projectRoot?: string } {
|
|
187
|
+
if (packageIdOrPath) {
|
|
188
|
+
const root = resolvePackageRoot(packageIdOrPath, customHome);
|
|
189
|
+
if (root) {
|
|
190
|
+
const config = loadProjectConfig(root);
|
|
191
|
+
return {
|
|
192
|
+
packageId: config.id,
|
|
193
|
+
storage: runtimeRegistry.getStorage(config.id, root),
|
|
194
|
+
projectRoot: root,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
packageId: packageIdOrPath,
|
|
199
|
+
storage: runtimeRegistry.getStorage(packageIdOrPath),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (projectRoot) {
|
|
204
|
+
const config = loadProjectConfig(projectRoot);
|
|
205
|
+
return {
|
|
206
|
+
packageId: config.id,
|
|
207
|
+
storage: runtimeRegistry.getStorage(config.id, projectRoot),
|
|
208
|
+
projectRoot,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const linked = listLinkedPackages(customHome);
|
|
213
|
+
if (linked.length > 0) {
|
|
214
|
+
const first = linked[0];
|
|
215
|
+
return {
|
|
216
|
+
packageId: first.id,
|
|
217
|
+
storage: runtimeRegistry.getStorage(first.id, first.path),
|
|
218
|
+
projectRoot: first.path,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
packageId: "default",
|
|
224
|
+
storage: runtimeRegistry.getStorage("default"),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
73
228
|
/**
|
|
74
229
|
* 启动 ActionDock 2.0 原生轻量级 HTTP Runner 服务端。
|
|
75
230
|
*
|
|
76
|
-
*
|
|
77
|
-
* -
|
|
78
|
-
* -
|
|
79
|
-
* -
|
|
80
|
-
* -
|
|
81
|
-
* -
|
|
82
|
-
* -
|
|
83
|
-
* -
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
231
|
+
* 全面暴露 RESTful 调度与运维接口:
|
|
232
|
+
* - GET /api/v1/health : 健康检查与就绪状态
|
|
233
|
+
* - GET /api/v1/info : 统一自省探索(支持 ?intent= 智能决议、?package= 详情下钻、?tree=true 工作区拓扑)
|
|
234
|
+
* - GET /api/v1/doctor : 深度环境与依赖诊断
|
|
235
|
+
* - GET /api/v1/actions : 列出可用 Actions(支持 ?intent= 与 ?package=)
|
|
236
|
+
* - GET /api/v1/actions/:id : 查看单个 Action 的 Schema 详情
|
|
237
|
+
* - POST /api/v1/actions/:id/run : 同步或异步执行 Action
|
|
238
|
+
* - GET /api/v1/playbooks : 列出可用 Playbook 工作流
|
|
239
|
+
* - GET /api/v1/playbooks/:id : 查看 Playbook 步骤流程与 SOP 规程正文
|
|
240
|
+
* - GET /api/v1/runs : 多维检索历史运行记录
|
|
241
|
+
* - POST /api/v1/runs/clear : 批量清理历史运行记录
|
|
242
|
+
* - GET /api/v1/runs/:runId : 查询历史或异步任务运行状态
|
|
243
|
+
* - POST /api/v1/runs/:runId/cancel : 中断在途任务
|
|
244
|
+
* - GET /api/v1/runs/:runId/stream : 基于 SSE 实时长任务日志与进度流推送
|
|
245
|
+
* - GET /api/v1/state : 检索状态键名列表
|
|
246
|
+
* - GET /api/v1/state/:key : 获取指定状态值
|
|
247
|
+
* - PUT /api/v1/state/:key : 写入持久化状态(支持 TTL)
|
|
248
|
+
* - DELETE /api/v1/state/:key : 删除指定状态
|
|
249
|
+
* - POST /api/v1/state/clear : 清空状态数据
|
|
250
|
+
* - GET /api/v1/config : 获取配置项规格与当前值(敏感凭证脱敏)
|
|
251
|
+
* - PUT /api/v1/config : 持久化更新配置
|
|
252
|
+
* - DELETE /api/v1/config/:key : 删除配置项
|
|
253
|
+
* - GET /api/v1/config/env : 检查环境变量满足率
|
|
254
|
+
* - ALL /mcp : 一体化 MCP Streamable HTTP 协议端点(可选)
|
|
87
255
|
*/
|
|
88
256
|
export function startActionDockServer(
|
|
89
257
|
options: ServerOptions = {}
|
|
@@ -105,11 +273,8 @@ export function startActionDockServer(
|
|
|
105
273
|
|
|
106
274
|
const runtimeRegistry = new ServerRuntimeRegistry();
|
|
107
275
|
|
|
108
|
-
const server =
|
|
109
|
-
|
|
110
|
-
hostname: host,
|
|
111
|
-
async fetch(req) {
|
|
112
|
-
const origin = req.headers.get("origin");
|
|
276
|
+
const server = launchHttpServer(port, host, async (req) => {
|
|
277
|
+
const origin = req.headers.get("origin");
|
|
113
278
|
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
114
279
|
|
|
115
280
|
if (req.method === "OPTIONS") {
|
|
@@ -122,7 +287,7 @@ export function startActionDockServer(
|
|
|
122
287
|
const url = new URL(req.url);
|
|
123
288
|
const pathname = url.pathname;
|
|
124
289
|
|
|
125
|
-
//
|
|
290
|
+
// 0. Health Check (supports /api/v1/health and /health)
|
|
126
291
|
if (pathname === "/api/v1/health" || pathname === "/health") {
|
|
127
292
|
if (!verifyBearerToken(req, token)) {
|
|
128
293
|
return jsonResponse(
|
|
@@ -149,7 +314,30 @@ export function startActionDockServer(
|
|
|
149
314
|
return jsonResponse(healthData, 200, corsHeaders);
|
|
150
315
|
}
|
|
151
316
|
|
|
152
|
-
//
|
|
317
|
+
// 1. MCP Unified Gateway: /mcp
|
|
318
|
+
if (
|
|
319
|
+
options.enableMcp !== false &&
|
|
320
|
+
options.mcpHandler &&
|
|
321
|
+
(pathname === "/mcp" || pathname.startsWith("/mcp/"))
|
|
322
|
+
) {
|
|
323
|
+
if (!verifyBearerToken(req, token)) {
|
|
324
|
+
return jsonResponse(
|
|
325
|
+
{
|
|
326
|
+
ok: false,
|
|
327
|
+
error: {
|
|
328
|
+
code: "UNAUTHORIZED",
|
|
329
|
+
message: "Invalid or missing Bearer token",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
401,
|
|
333
|
+
corsHeaders
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const mcpRes = await options.mcpHandler(req);
|
|
337
|
+
if (mcpRes) return mcpRes;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 鉴权检查(后续所有 /api/v1/* 均要求鉴权)
|
|
153
341
|
if (!verifyBearerToken(req, token)) {
|
|
154
342
|
return jsonResponse(
|
|
155
343
|
{
|
|
@@ -164,40 +352,202 @@ export function startActionDockServer(
|
|
|
164
352
|
);
|
|
165
353
|
}
|
|
166
354
|
|
|
167
|
-
// 2. Info
|
|
355
|
+
// 2. Info 自省: GET /api/v1/info
|
|
168
356
|
if (pathname === "/api/v1/info" && req.method === "GET") {
|
|
169
357
|
try {
|
|
358
|
+
const isTree = url.searchParams.get("tree") === "true";
|
|
359
|
+
if (isTree) {
|
|
360
|
+
const status = getRegistryStatus(customHome);
|
|
361
|
+
return jsonResponse(
|
|
362
|
+
{ ok: true, type: "tree", ...status },
|
|
363
|
+
200,
|
|
364
|
+
corsHeaders
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
369
|
+
const targetPkg = url.searchParams.get("package") || undefined;
|
|
370
|
+
|
|
371
|
+
// 收集所有候选包全量元数据
|
|
372
|
+
const aggregatedPackages: any[] = [];
|
|
373
|
+
|
|
170
374
|
if (projectRoot) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
375
|
+
try {
|
|
376
|
+
const config = loadProjectConfig(projectRoot);
|
|
377
|
+
const actions = await loadActions(projectRoot, config.actionsDir);
|
|
378
|
+
const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
|
|
379
|
+
aggregatedPackages.push({
|
|
380
|
+
id: config.id,
|
|
381
|
+
name: config.name,
|
|
382
|
+
version: config.version,
|
|
383
|
+
description: config.description || "",
|
|
384
|
+
path: projectRoot,
|
|
385
|
+
actionsCount: actions.size,
|
|
386
|
+
playbooksCount: playbooks.size,
|
|
387
|
+
actions: Array.from(actions.entries()).map(([id, a]) => ({
|
|
388
|
+
id,
|
|
389
|
+
description: a.description || "",
|
|
390
|
+
inputSchema: a.inputSchema || null,
|
|
391
|
+
outputSchema: a.outputSchema || null,
|
|
392
|
+
})),
|
|
393
|
+
playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
|
|
394
|
+
id,
|
|
395
|
+
description: p.description || "",
|
|
396
|
+
actions: p.actions || [],
|
|
397
|
+
})),
|
|
398
|
+
configDeclared: config.config || {},
|
|
399
|
+
});
|
|
400
|
+
} catch {
|
|
401
|
+
// 忽略解析异常
|
|
187
402
|
}
|
|
188
|
-
return jsonResponse(infoData, 200, corsHeaders);
|
|
189
403
|
} else {
|
|
190
404
|
const linked = listLinkedPackages(customHome);
|
|
405
|
+
for (const pkg of linked) {
|
|
406
|
+
if (!existsSync(pkg.path)) continue;
|
|
407
|
+
try {
|
|
408
|
+
const config = loadProjectConfig(pkg.path);
|
|
409
|
+
const actions = await loadActions(pkg.path, config.actionsDir);
|
|
410
|
+
const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
|
|
411
|
+
aggregatedPackages.push({
|
|
412
|
+
id: config.id,
|
|
413
|
+
name: config.name,
|
|
414
|
+
version: config.version,
|
|
415
|
+
description: config.description || "",
|
|
416
|
+
path: pkg.path,
|
|
417
|
+
actionsCount: actions.size,
|
|
418
|
+
playbooksCount: playbooks.size,
|
|
419
|
+
actions: Array.from(actions.entries()).map(([id, a]) => ({
|
|
420
|
+
id,
|
|
421
|
+
description: a.description || "",
|
|
422
|
+
inputSchema: a.inputSchema || null,
|
|
423
|
+
outputSchema: a.outputSchema || null,
|
|
424
|
+
})),
|
|
425
|
+
playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
|
|
426
|
+
id,
|
|
427
|
+
description: p.description || "",
|
|
428
|
+
actions: p.actions || [],
|
|
429
|
+
})),
|
|
430
|
+
configDeclared: config.config || {},
|
|
431
|
+
});
|
|
432
|
+
} catch {
|
|
433
|
+
// 忽略故障包
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 显式指定 package 详情下钻
|
|
439
|
+
if (targetPkg) {
|
|
440
|
+
const matched = aggregatedPackages.find(
|
|
441
|
+
(p) => p.id === targetPkg || p.path === targetPkg
|
|
442
|
+
);
|
|
443
|
+
if (!matched) {
|
|
444
|
+
return jsonResponse(
|
|
445
|
+
{
|
|
446
|
+
ok: false,
|
|
447
|
+
error: {
|
|
448
|
+
code: "PACKAGE_NOT_FOUND",
|
|
449
|
+
message: `Package '${targetPkg}' not found on remote server`,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
404,
|
|
453
|
+
corsHeaders
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return jsonResponse(
|
|
457
|
+
{
|
|
458
|
+
ok: true,
|
|
459
|
+
type: "package_detail",
|
|
460
|
+
...matched,
|
|
461
|
+
...(options.exposeDebugInfo ? { projectRoot: matched.path } : {}),
|
|
462
|
+
},
|
|
463
|
+
200,
|
|
464
|
+
corsHeaders
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 意图过滤与决议
|
|
469
|
+
if (intent) {
|
|
470
|
+
const filterRes = filterWithFallbackInfo(
|
|
471
|
+
aggregatedPackages,
|
|
472
|
+
intent,
|
|
473
|
+
[
|
|
474
|
+
(p) => p.id,
|
|
475
|
+
(p) => p.name,
|
|
476
|
+
(p) => p.description,
|
|
477
|
+
(p) => p.actions.map((a: any) => a.id),
|
|
478
|
+
(p) => p.actions.map((a: any) => a.description),
|
|
479
|
+
(p) => p.playbooks.map((pb: any) => pb.id),
|
|
480
|
+
(p) => p.playbooks.map((pb: any) => pb.description),
|
|
481
|
+
],
|
|
482
|
+
true
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// 唯一命中智能展开详情
|
|
486
|
+
if (filterRes.matchedCount === 1) {
|
|
487
|
+
const single = filterRes.items[0];
|
|
488
|
+
return jsonResponse(
|
|
489
|
+
{
|
|
490
|
+
ok: true,
|
|
491
|
+
type: "package_detail",
|
|
492
|
+
isSingleMatch: true,
|
|
493
|
+
...single,
|
|
494
|
+
...(options.exposeDebugInfo ? { projectRoot: single.path } : {}),
|
|
495
|
+
},
|
|
496
|
+
200,
|
|
497
|
+
corsHeaders
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
191
501
|
return jsonResponse(
|
|
192
502
|
{
|
|
193
503
|
ok: true,
|
|
194
|
-
|
|
195
|
-
|
|
504
|
+
type: "package_list",
|
|
505
|
+
isFallback: filterRes.isFallback,
|
|
506
|
+
packages: filterRes.items,
|
|
196
507
|
},
|
|
197
508
|
200,
|
|
198
509
|
corsHeaders
|
|
199
510
|
);
|
|
200
511
|
}
|
|
512
|
+
|
|
513
|
+
// 单包模式直接展开详情
|
|
514
|
+
if (projectRoot && aggregatedPackages.length === 1) {
|
|
515
|
+
const p = aggregatedPackages[0];
|
|
516
|
+
return jsonResponse(
|
|
517
|
+
{
|
|
518
|
+
ok: true,
|
|
519
|
+
type: "package_detail",
|
|
520
|
+
id: p.id,
|
|
521
|
+
name: p.name,
|
|
522
|
+
version: p.version,
|
|
523
|
+
description: p.description,
|
|
524
|
+
actionsCount: p.actionsCount,
|
|
525
|
+
playbooksCount: p.playbooksCount,
|
|
526
|
+
actions: p.actions.map((a: any) => a.id),
|
|
527
|
+
actionsDetail: p.actions,
|
|
528
|
+
playbooks: p.playbooks.map((pb: any) => pb.id),
|
|
529
|
+
playbooksDetail: p.playbooks,
|
|
530
|
+
configDeclared: p.configDeclared,
|
|
531
|
+
linkedPackages: listLinkedPackages(customHome),
|
|
532
|
+
...(options.exposeDebugInfo ? { projectRoot } : {}),
|
|
533
|
+
},
|
|
534
|
+
200,
|
|
535
|
+
corsHeaders
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 全局注册表概览
|
|
540
|
+
return jsonResponse(
|
|
541
|
+
{
|
|
542
|
+
ok: true,
|
|
543
|
+
type: "package_list",
|
|
544
|
+
version: "2.0.0",
|
|
545
|
+
packages: aggregatedPackages,
|
|
546
|
+
linkedPackages: listLinkedPackages(customHome),
|
|
547
|
+
},
|
|
548
|
+
200,
|
|
549
|
+
corsHeaders
|
|
550
|
+
);
|
|
201
551
|
} catch (err: any) {
|
|
202
552
|
return jsonResponse(
|
|
203
553
|
{
|
|
@@ -210,7 +560,26 @@ export function startActionDockServer(
|
|
|
210
560
|
}
|
|
211
561
|
}
|
|
212
562
|
|
|
213
|
-
// 3.
|
|
563
|
+
// 3. Doctor 深度体检: GET /api/v1/doctor
|
|
564
|
+
if (pathname === "/api/v1/doctor" && req.method === "GET") {
|
|
565
|
+
try {
|
|
566
|
+
const targetPkg = url.searchParams.get("package") || undefined;
|
|
567
|
+
const report = await runDoctorChecks({
|
|
568
|
+
cwd: projectRoot || process.cwd(),
|
|
569
|
+
packageIdOrPath: targetPkg,
|
|
570
|
+
customHome,
|
|
571
|
+
});
|
|
572
|
+
return jsonResponse({ ok: true, report }, 200, corsHeaders);
|
|
573
|
+
} catch (err: any) {
|
|
574
|
+
return jsonResponse(
|
|
575
|
+
{ ok: false, error: { code: "DOCTOR_ERROR", message: err.message } },
|
|
576
|
+
500,
|
|
577
|
+
corsHeaders
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 4. Actions List: GET /api/v1/actions
|
|
214
583
|
if (pathname === "/api/v1/actions" && req.method === "GET") {
|
|
215
584
|
try {
|
|
216
585
|
const actionList: Array<{
|
|
@@ -251,15 +620,21 @@ export function startActionDockServer(
|
|
|
251
620
|
}
|
|
252
621
|
|
|
253
622
|
const intent = url.searchParams.get("intent");
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
259
|
-
false
|
|
260
|
-
)
|
|
623
|
+
const targetPkg = url.searchParams.get("package");
|
|
624
|
+
|
|
625
|
+
let filtered = targetPkg
|
|
626
|
+
? actionList.filter((a) => a.packageId === targetPkg)
|
|
261
627
|
: actionList;
|
|
262
628
|
|
|
629
|
+
if (intent) {
|
|
630
|
+
filtered = filterByIntent(
|
|
631
|
+
filtered,
|
|
632
|
+
intent,
|
|
633
|
+
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
634
|
+
false
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
263
638
|
return jsonResponse(filtered, 200, corsHeaders);
|
|
264
639
|
} catch (err: any) {
|
|
265
640
|
return jsonResponse(
|
|
@@ -273,7 +648,7 @@ export function startActionDockServer(
|
|
|
273
648
|
}
|
|
274
649
|
}
|
|
275
650
|
|
|
276
|
-
//
|
|
651
|
+
// 5. Action Show: GET /api/v1/actions/:id
|
|
277
652
|
const actionShowMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)$/);
|
|
278
653
|
if (actionShowMatch && req.method === "GET") {
|
|
279
654
|
const actionId = decodeURIComponent(actionShowMatch[1]);
|
|
@@ -323,7 +698,7 @@ export function startActionDockServer(
|
|
|
323
698
|
}
|
|
324
699
|
}
|
|
325
700
|
|
|
326
|
-
//
|
|
701
|
+
// 6. Action Run: POST /api/v1/actions/:id/run
|
|
327
702
|
const actionRunMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)\/run$/);
|
|
328
703
|
if (actionRunMatch && req.method === "POST") {
|
|
329
704
|
const actionId = decodeURIComponent(actionRunMatch[1]);
|
|
@@ -336,10 +711,7 @@ export function startActionDockServer(
|
|
|
336
711
|
{
|
|
337
712
|
ok: false,
|
|
338
713
|
runId: randomUUID(),
|
|
339
|
-
error: {
|
|
340
|
-
code: "REQUEST_TOO_LARGE",
|
|
341
|
-
message: err.message,
|
|
342
|
-
},
|
|
714
|
+
error: { code: "REQUEST_TOO_LARGE", message: err.message },
|
|
343
715
|
},
|
|
344
716
|
413,
|
|
345
717
|
corsHeaders
|
|
@@ -350,10 +722,7 @@ export function startActionDockServer(
|
|
|
350
722
|
{
|
|
351
723
|
ok: false,
|
|
352
724
|
runId: randomUUID(),
|
|
353
|
-
error: {
|
|
354
|
-
code: "INVALID_JSON",
|
|
355
|
-
message: err.message,
|
|
356
|
-
},
|
|
725
|
+
error: { code: "INVALID_JSON", message: err.message },
|
|
357
726
|
},
|
|
358
727
|
400,
|
|
359
728
|
corsHeaders
|
|
@@ -363,10 +732,7 @@ export function startActionDockServer(
|
|
|
363
732
|
{
|
|
364
733
|
ok: false,
|
|
365
734
|
runId: randomUUID(),
|
|
366
|
-
error: {
|
|
367
|
-
code: "INVALID_JSON",
|
|
368
|
-
message: `Failed to parse request body: ${err.message}`,
|
|
369
|
-
},
|
|
735
|
+
error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
|
|
370
736
|
},
|
|
371
737
|
400,
|
|
372
738
|
corsHeaders
|
|
@@ -391,30 +757,49 @@ export function startActionDockServer(
|
|
|
391
757
|
actions,
|
|
392
758
|
});
|
|
393
759
|
|
|
394
|
-
const
|
|
760
|
+
const isAsync = body?.execution?.mode === "async" || body?.async === true;
|
|
395
761
|
const timeoutMs =
|
|
396
762
|
typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
|
|
397
763
|
? body.execution.timeoutMs
|
|
398
764
|
: undefined;
|
|
399
765
|
|
|
400
|
-
if (
|
|
766
|
+
if (isAsync) {
|
|
401
767
|
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
402
768
|
timeoutMs,
|
|
403
769
|
});
|
|
404
770
|
runtimeRegistry.executionManager.register(handle);
|
|
405
771
|
|
|
772
|
+
// 监听结算以广播 SSE 完成事件
|
|
773
|
+
handle.result
|
|
774
|
+
.then((res) => {
|
|
775
|
+
runtimeRegistry.emit(handle.runId, { type: "finish", data: res });
|
|
776
|
+
})
|
|
777
|
+
.catch((err) => {
|
|
778
|
+
runtimeRegistry.emit(handle.runId, {
|
|
779
|
+
type: "finish",
|
|
780
|
+
data: {
|
|
781
|
+
ok: false,
|
|
782
|
+
error: {
|
|
783
|
+
code: "ACTION_EXECUTION_ERROR",
|
|
784
|
+
message: err?.message || String(err),
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
406
790
|
return jsonResponse(
|
|
407
791
|
{
|
|
408
792
|
ok: true,
|
|
409
793
|
runId: handle.runId,
|
|
410
794
|
status: "running",
|
|
795
|
+
streamUrl: `/api/v1/runs/${handle.runId}/stream`,
|
|
411
796
|
},
|
|
412
797
|
202,
|
|
413
798
|
corsHeaders
|
|
414
799
|
);
|
|
415
800
|
}
|
|
416
801
|
|
|
417
|
-
//
|
|
802
|
+
// 同步执行模式
|
|
418
803
|
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
419
804
|
signal: req.signal,
|
|
420
805
|
timeoutMs,
|
|
@@ -439,7 +824,289 @@ export function startActionDockServer(
|
|
|
439
824
|
}
|
|
440
825
|
}
|
|
441
826
|
|
|
442
|
-
//
|
|
827
|
+
// 7. Playbooks List: GET /api/v1/playbooks
|
|
828
|
+
if (pathname === "/api/v1/playbooks" && req.method === "GET") {
|
|
829
|
+
try {
|
|
830
|
+
const pbList: Array<{
|
|
831
|
+
id: string;
|
|
832
|
+
description: string;
|
|
833
|
+
actions: string[];
|
|
834
|
+
packageId: string;
|
|
835
|
+
filePath: string;
|
|
836
|
+
}> = [];
|
|
837
|
+
|
|
838
|
+
const targetPkg = url.searchParams.get("package");
|
|
839
|
+
const intent = url.searchParams.get("intent");
|
|
840
|
+
|
|
841
|
+
const roots: Array<{ root: string; packageId: string }> = [];
|
|
842
|
+
if (projectRoot) {
|
|
843
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
844
|
+
roots.push({ root: projectRoot, packageId: cfg.id });
|
|
845
|
+
}
|
|
846
|
+
const linked = listLinkedPackages(customHome);
|
|
847
|
+
for (const pkg of linked) {
|
|
848
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
849
|
+
if (!existsSync(pkg.path)) continue;
|
|
850
|
+
roots.push({ root: pkg.path, packageId: pkg.id });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
for (const item of roots) {
|
|
854
|
+
if (targetPkg && item.packageId !== targetPkg && item.root !== targetPkg) continue;
|
|
855
|
+
try {
|
|
856
|
+
const cfg = loadProjectConfig(item.root);
|
|
857
|
+
const pbs = loadPlaybooks(item.root, cfg.playbooksDir);
|
|
858
|
+
for (const [id, pb] of pbs.entries()) {
|
|
859
|
+
pbList.push({
|
|
860
|
+
id,
|
|
861
|
+
description: pb.description || "",
|
|
862
|
+
actions: pb.actions || [],
|
|
863
|
+
packageId: item.packageId,
|
|
864
|
+
filePath: pb.filePath,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
} catch {}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const filtered = intent
|
|
871
|
+
? filterByIntent(
|
|
872
|
+
pbList,
|
|
873
|
+
intent,
|
|
874
|
+
[(p) => p.id, (p) => p.description, (p) => p.packageId, (p) => p.actions],
|
|
875
|
+
false
|
|
876
|
+
)
|
|
877
|
+
: pbList;
|
|
878
|
+
|
|
879
|
+
return jsonResponse(filtered, 200, corsHeaders);
|
|
880
|
+
} catch (err: any) {
|
|
881
|
+
return jsonResponse(
|
|
882
|
+
{ ok: false, error: { code: "PLAYBOOKS_LIST_ERROR", message: err.message } },
|
|
883
|
+
500,
|
|
884
|
+
corsHeaders
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// 8. Playbook Show: GET /api/v1/playbooks/:id
|
|
890
|
+
const pbShowMatch = pathname.match(/^\/api\/v1\/playbooks\/([^/]+)$/);
|
|
891
|
+
if (pbShowMatch && req.method === "GET") {
|
|
892
|
+
const pbId = decodeURIComponent(pbShowMatch[1]);
|
|
893
|
+
try {
|
|
894
|
+
const resolved = resolvePlaybookProject(
|
|
895
|
+
pbId,
|
|
896
|
+
projectRoot || process.cwd(),
|
|
897
|
+
customHome
|
|
898
|
+
);
|
|
899
|
+
const pb = resolved.playbook;
|
|
900
|
+
return jsonResponse(
|
|
901
|
+
{
|
|
902
|
+
ok: true,
|
|
903
|
+
id: pb.id,
|
|
904
|
+
packageId: resolved.packageId,
|
|
905
|
+
description: pb.description || "",
|
|
906
|
+
actions: pb.actions || [],
|
|
907
|
+
filePath: pb.filePath,
|
|
908
|
+
content: pb.content,
|
|
909
|
+
},
|
|
910
|
+
200,
|
|
911
|
+
corsHeaders
|
|
912
|
+
);
|
|
913
|
+
} catch (err: any) {
|
|
914
|
+
return jsonResponse(
|
|
915
|
+
{ ok: false, error: { code: "PLAYBOOK_NOT_FOUND", message: err.message } },
|
|
916
|
+
404,
|
|
917
|
+
corsHeaders
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// 9. Runs List: GET /api/v1/runs
|
|
923
|
+
if (pathname === "/api/v1/runs" && req.method === "GET") {
|
|
924
|
+
try {
|
|
925
|
+
const status = url.searchParams.get("status") || undefined;
|
|
926
|
+
const actionId = url.searchParams.get("actionId") || undefined;
|
|
927
|
+
const packageId = url.searchParams.get("packageId") || undefined;
|
|
928
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
929
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
930
|
+
|
|
931
|
+
const allRuns: any[] = [];
|
|
932
|
+
const seenRunIds = new Set<string>();
|
|
933
|
+
|
|
934
|
+
const candidateStorages: Array<{ packageId: string; storage: RuntimeStorage }> = [];
|
|
935
|
+
if (projectRoot) {
|
|
936
|
+
try {
|
|
937
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
938
|
+
candidateStorages.push({
|
|
939
|
+
packageId: cfg.id,
|
|
940
|
+
storage: runtimeRegistry.getStorage(cfg.id, projectRoot),
|
|
941
|
+
});
|
|
942
|
+
} catch {}
|
|
943
|
+
}
|
|
944
|
+
const linked = listLinkedPackages(customHome);
|
|
945
|
+
for (const pkg of linked) {
|
|
946
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
947
|
+
if (!existsSync(pkg.path)) continue;
|
|
948
|
+
candidateStorages.push({
|
|
949
|
+
packageId: pkg.id,
|
|
950
|
+
storage: runtimeRegistry.getStorage(pkg.id, pkg.path),
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
for (const item of candidateStorages) {
|
|
955
|
+
if (packageId && item.packageId !== packageId) continue;
|
|
956
|
+
try {
|
|
957
|
+
const records = item.storage.listRuns({ actionId, limit });
|
|
958
|
+
for (const r of records) {
|
|
959
|
+
if (!seenRunIds.has(r.id)) {
|
|
960
|
+
seenRunIds.add(r.id);
|
|
961
|
+
if (status && r.status !== status) continue;
|
|
962
|
+
allRuns.push(r);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
} catch {}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
969
|
+
|
|
970
|
+
const filtered = intent
|
|
971
|
+
? filterByIntent(
|
|
972
|
+
allRuns,
|
|
973
|
+
intent,
|
|
974
|
+
[(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.packageId],
|
|
975
|
+
false
|
|
976
|
+
)
|
|
977
|
+
: allRuns;
|
|
978
|
+
|
|
979
|
+
const sliced = filtered.slice(0, limit);
|
|
980
|
+
return jsonResponse(
|
|
981
|
+
{ ok: true, total: filtered.length, items: sliced },
|
|
982
|
+
200,
|
|
983
|
+
corsHeaders
|
|
984
|
+
);
|
|
985
|
+
} catch (err: any) {
|
|
986
|
+
return jsonResponse(
|
|
987
|
+
{ ok: false, error: { code: "RUNS_LIST_ERROR", message: err.message } },
|
|
988
|
+
500,
|
|
989
|
+
corsHeaders
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// 10. Runs Clear: POST /api/v1/runs/clear or DELETE /api/v1/runs
|
|
995
|
+
if (
|
|
996
|
+
(pathname === "/api/v1/runs/clear" && req.method === "POST") ||
|
|
997
|
+
(pathname === "/api/v1/runs" && req.method === "DELETE")
|
|
998
|
+
) {
|
|
999
|
+
try {
|
|
1000
|
+
let body: any = {};
|
|
1001
|
+
if (req.method === "POST" || req.headers.get("content-type")?.includes("json")) {
|
|
1002
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
|
|
1003
|
+
}
|
|
1004
|
+
const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
|
|
1005
|
+
const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
|
|
1006
|
+
const status = url.searchParams.get("status") || body.status || undefined;
|
|
1007
|
+
|
|
1008
|
+
let clearedCount = 0;
|
|
1009
|
+
const candidateStorages: RuntimeStorage[] = [];
|
|
1010
|
+
if (projectRoot) {
|
|
1011
|
+
try {
|
|
1012
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
1013
|
+
candidateStorages.push(runtimeRegistry.getStorage(cfg.id, projectRoot));
|
|
1014
|
+
} catch {}
|
|
1015
|
+
}
|
|
1016
|
+
const linked = listLinkedPackages(customHome);
|
|
1017
|
+
for (const pkg of linked) {
|
|
1018
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
1019
|
+
if (!existsSync(pkg.path)) continue;
|
|
1020
|
+
if (packageId && pkg.id !== packageId) continue;
|
|
1021
|
+
candidateStorages.push(runtimeRegistry.getStorage(pkg.id, pkg.path));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for (const storage of candidateStorages) {
|
|
1025
|
+
clearedCount += storage.clearRuns({ actionId, status });
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
|
|
1029
|
+
} catch (err: any) {
|
|
1030
|
+
return jsonResponse(
|
|
1031
|
+
{ ok: false, error: { code: "RUNS_CLEAR_ERROR", message: err.message } },
|
|
1032
|
+
500,
|
|
1033
|
+
corsHeaders
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// 11. Run Stream (SSE): GET /api/v1/runs/:runId/stream
|
|
1039
|
+
const runStreamMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/stream$/);
|
|
1040
|
+
if (runStreamMatch && req.method === "GET") {
|
|
1041
|
+
const runId = decodeURIComponent(runStreamMatch[1]);
|
|
1042
|
+
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
1043
|
+
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
1044
|
+
|
|
1045
|
+
if (!found && !activeHandle) {
|
|
1046
|
+
return jsonResponse(
|
|
1047
|
+
{ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run '${runId}' not found` } },
|
|
1048
|
+
404,
|
|
1049
|
+
corsHeaders
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const stream = new ReadableStream({
|
|
1054
|
+
start(controller) {
|
|
1055
|
+
const encoder = new TextEncoder();
|
|
1056
|
+
const sendEvent = (event: string, data: any) => {
|
|
1057
|
+
try {
|
|
1058
|
+
controller.enqueue(
|
|
1059
|
+
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
1060
|
+
);
|
|
1061
|
+
} catch {}
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
if (activeHandle) {
|
|
1065
|
+
sendEvent("status", { runId, status: "running" });
|
|
1066
|
+
const unsubscribe = runtimeRegistry.subscribe(runId, (evt) => {
|
|
1067
|
+
sendEvent(evt.type, evt.data);
|
|
1068
|
+
if (evt.type === "finish") {
|
|
1069
|
+
try { controller.close(); } catch {}
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
activeHandle.result.then(
|
|
1074
|
+
(res) => {
|
|
1075
|
+
sendEvent("finish", res);
|
|
1076
|
+
try { controller.close(); } catch {}
|
|
1077
|
+
},
|
|
1078
|
+
(err) => {
|
|
1079
|
+
sendEvent("finish", {
|
|
1080
|
+
ok: false,
|
|
1081
|
+
error: { message: err?.message || String(err) },
|
|
1082
|
+
});
|
|
1083
|
+
try { controller.close(); } catch {}
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
req.signal.addEventListener("abort", () => {
|
|
1088
|
+
unsubscribe();
|
|
1089
|
+
try { controller.close(); } catch {}
|
|
1090
|
+
});
|
|
1091
|
+
} else if (found) {
|
|
1092
|
+
sendEvent("finish", found.run);
|
|
1093
|
+
controller.close();
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
return new Response(stream, {
|
|
1099
|
+
status: 200,
|
|
1100
|
+
headers: {
|
|
1101
|
+
"Content-Type": "text/event-stream",
|
|
1102
|
+
"Cache-Control": "no-cache",
|
|
1103
|
+
"Connection": "keep-alive",
|
|
1104
|
+
...corsHeaders,
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// 12. Run Show: GET /api/v1/runs/:runId
|
|
443
1110
|
const runShowMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
|
|
444
1111
|
if (runShowMatch && req.method === "GET") {
|
|
445
1112
|
const runId = decodeURIComponent(runShowMatch[1]);
|
|
@@ -462,7 +1129,7 @@ export function startActionDockServer(
|
|
|
462
1129
|
return jsonResponse(found.run, 200, corsHeaders);
|
|
463
1130
|
}
|
|
464
1131
|
|
|
465
|
-
//
|
|
1132
|
+
// 13. Run Cancel: POST /api/v1/runs/:runId/cancel
|
|
466
1133
|
const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
|
|
467
1134
|
if (runCancelMatch && req.method === "POST") {
|
|
468
1135
|
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
@@ -475,7 +1142,6 @@ export function startActionDockServer(
|
|
|
475
1142
|
|
|
476
1143
|
const reason = body?.reason || "Cancelled by client request";
|
|
477
1144
|
|
|
478
|
-
// 1. Try cancelling in-memory active handle
|
|
479
1145
|
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
480
1146
|
if (activeHandle) {
|
|
481
1147
|
const cancelled = runtimeRegistry.executionManager.cancel(runId, reason);
|
|
@@ -492,9 +1158,7 @@ export function startActionDockServer(
|
|
|
492
1158
|
}
|
|
493
1159
|
}
|
|
494
1160
|
|
|
495
|
-
// 2. Check storage for run status
|
|
496
1161
|
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
497
|
-
|
|
498
1162
|
if (!found) {
|
|
499
1163
|
return jsonResponse(
|
|
500
1164
|
{
|
|
@@ -524,7 +1188,6 @@ export function startActionDockServer(
|
|
|
524
1188
|
);
|
|
525
1189
|
}
|
|
526
1190
|
|
|
527
|
-
// Running in storage but no longer in memory
|
|
528
1191
|
storage.updateRun(runId, "cancelled", undefined, {
|
|
529
1192
|
code: "ACTION_CANCELLED",
|
|
530
1193
|
message: reason,
|
|
@@ -541,6 +1204,263 @@ export function startActionDockServer(
|
|
|
541
1204
|
);
|
|
542
1205
|
}
|
|
543
1206
|
|
|
1207
|
+
// 14. State List: GET /api/v1/state
|
|
1208
|
+
if (pathname === "/api/v1/state" && req.method === "GET") {
|
|
1209
|
+
try {
|
|
1210
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1211
|
+
const nsParam = url.searchParams.get("namespace") ?? undefined;
|
|
1212
|
+
const prefix = url.searchParams.get("prefix") || "";
|
|
1213
|
+
|
|
1214
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1215
|
+
pkgParam,
|
|
1216
|
+
runtimeRegistry,
|
|
1217
|
+
projectRoot,
|
|
1218
|
+
customHome
|
|
1219
|
+
);
|
|
1220
|
+
const keys = await storage.listStateKeys(nsParam !== undefined ? nsParam : null, prefix);
|
|
1221
|
+
return jsonResponse({ ok: true, packageId, keys }, 200, corsHeaders);
|
|
1222
|
+
} catch (err: any) {
|
|
1223
|
+
return jsonResponse(
|
|
1224
|
+
{ ok: false, error: { code: "STATE_LIST_ERROR", message: err.message } },
|
|
1225
|
+
500,
|
|
1226
|
+
corsHeaders
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 15. State Clear: POST /api/v1/state/clear
|
|
1232
|
+
if (pathname === "/api/v1/state/clear" && req.method === "POST") {
|
|
1233
|
+
try {
|
|
1234
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
|
|
1235
|
+
const pkgParam = url.searchParams.get("package") || body.package || undefined;
|
|
1236
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1237
|
+
pkgParam,
|
|
1238
|
+
runtimeRegistry,
|
|
1239
|
+
projectRoot,
|
|
1240
|
+
customHome
|
|
1241
|
+
);
|
|
1242
|
+
const clearedCount = await storage.clearState({
|
|
1243
|
+
namespace: body.namespace ?? (url.searchParams.get("namespace") || undefined),
|
|
1244
|
+
all: Boolean(body.all ?? url.searchParams.get("all") === "true"),
|
|
1245
|
+
prefix: body.prefix ?? (url.searchParams.get("prefix") || undefined),
|
|
1246
|
+
});
|
|
1247
|
+
return jsonResponse({ ok: true, packageId, clearedCount }, 200, corsHeaders);
|
|
1248
|
+
} catch (err: any) {
|
|
1249
|
+
return jsonResponse(
|
|
1250
|
+
{ ok: false, error: { code: "STATE_CLEAR_ERROR", message: err.message } },
|
|
1251
|
+
500,
|
|
1252
|
+
corsHeaders
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// 16. State Key CRUD: GET / PUT / POST / DELETE /api/v1/state/:key
|
|
1258
|
+
const stateKeyMatch = pathname.match(/^\/api\/v1\/state\/([^/]+)$/);
|
|
1259
|
+
if (stateKeyMatch) {
|
|
1260
|
+
const key = decodeURIComponent(stateKeyMatch[1]);
|
|
1261
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1262
|
+
const nsParam = url.searchParams.get("namespace") || undefined;
|
|
1263
|
+
|
|
1264
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1265
|
+
pkgParam,
|
|
1266
|
+
runtimeRegistry,
|
|
1267
|
+
projectRoot,
|
|
1268
|
+
customHome
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
if (req.method === "GET") {
|
|
1272
|
+
const entry = await storage.findState(key, nsParam);
|
|
1273
|
+
if (!entry || entry.value === undefined) {
|
|
1274
|
+
return jsonResponse(
|
|
1275
|
+
{ ok: false, error: { code: "STATE_KEY_NOT_FOUND", message: `State key '${key}' not found` } },
|
|
1276
|
+
404,
|
|
1277
|
+
corsHeaders
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
return jsonResponse(
|
|
1281
|
+
{
|
|
1282
|
+
ok: true,
|
|
1283
|
+
packageId,
|
|
1284
|
+
key: entry.key,
|
|
1285
|
+
namespace: entry.namespace,
|
|
1286
|
+
value: entry.value,
|
|
1287
|
+
expiresAt: entry.expiresAt,
|
|
1288
|
+
},
|
|
1289
|
+
200,
|
|
1290
|
+
corsHeaders
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
1295
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
1296
|
+
const val = body.value !== undefined ? body.value : body;
|
|
1297
|
+
const ttl = typeof body.ttl === "number" ? body.ttl : undefined;
|
|
1298
|
+
const namespace = body.namespace || nsParam || "";
|
|
1299
|
+
|
|
1300
|
+
let actualKey = key;
|
|
1301
|
+
let ns = namespace;
|
|
1302
|
+
if (!ns && key.includes(":")) {
|
|
1303
|
+
const idx = key.indexOf(":");
|
|
1304
|
+
ns = key.slice(0, idx);
|
|
1305
|
+
actualKey = key.slice(idx + 1);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
await storage.setState(ns, actualKey, val, ttl);
|
|
1309
|
+
return jsonResponse(
|
|
1310
|
+
{ ok: true, packageId, key: actualKey, namespace: ns, message: "updated" },
|
|
1311
|
+
200,
|
|
1312
|
+
corsHeaders
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (req.method === "DELETE") {
|
|
1317
|
+
const deleted = await storage.deleteStateSmart(key, nsParam);
|
|
1318
|
+
if (!deleted) {
|
|
1319
|
+
return jsonResponse(
|
|
1320
|
+
{ ok: false, error: { code: "STATE_KEY_NOT_FOUND", message: `State key '${key}' not found` } },
|
|
1321
|
+
404,
|
|
1322
|
+
corsHeaders
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
return jsonResponse({ ok: true, packageId, key, deleted: true }, 200, corsHeaders);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// 17. Config Env Check: GET /api/v1/config/env
|
|
1330
|
+
if (pathname === "/api/v1/config/env" && req.method === "GET") {
|
|
1331
|
+
try {
|
|
1332
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1333
|
+
const { packageId, projectRoot: root } = resolveStorageForPackage(
|
|
1334
|
+
pkgParam,
|
|
1335
|
+
runtimeRegistry,
|
|
1336
|
+
projectRoot,
|
|
1337
|
+
customHome
|
|
1338
|
+
);
|
|
1339
|
+
if (!root) {
|
|
1340
|
+
return jsonResponse({ ok: true, packageId, envChecks: [] }, 200, corsHeaders);
|
|
1341
|
+
}
|
|
1342
|
+
const cfg = loadProjectConfig(root);
|
|
1343
|
+
const declared = cfg.config || {};
|
|
1344
|
+
const envChecks: any[] = [];
|
|
1345
|
+
for (const [k, def] of Object.entries(declared)) {
|
|
1346
|
+
const envKeys = [
|
|
1347
|
+
k,
|
|
1348
|
+
`ACTIONDOCK_${k}`,
|
|
1349
|
+
`${packageId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${k}`,
|
|
1350
|
+
];
|
|
1351
|
+
const foundEnv = envKeys.find((ek) => process.env[ek] !== undefined);
|
|
1352
|
+
envChecks.push({
|
|
1353
|
+
key: k,
|
|
1354
|
+
required: def.default === undefined,
|
|
1355
|
+
satisfied: Boolean(foundEnv || def.default !== undefined),
|
|
1356
|
+
matchedEnv: foundEnv || null,
|
|
1357
|
+
hasDefault: def.default !== undefined,
|
|
1358
|
+
secret: Boolean(def.secret),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
return jsonResponse({ ok: true, packageId, envChecks }, 200, corsHeaders);
|
|
1362
|
+
} catch (err: any) {
|
|
1363
|
+
return jsonResponse(
|
|
1364
|
+
{ ok: false, error: { code: "CONFIG_ENV_ERROR", message: err.message } },
|
|
1365
|
+
500,
|
|
1366
|
+
corsHeaders
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// 18. Config Query: GET /api/v1/config
|
|
1372
|
+
if (pathname === "/api/v1/config" && req.method === "GET") {
|
|
1373
|
+
try {
|
|
1374
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1375
|
+
const { packageId, storage, projectRoot: root } = resolveStorageForPackage(
|
|
1376
|
+
pkgParam,
|
|
1377
|
+
runtimeRegistry,
|
|
1378
|
+
projectRoot,
|
|
1379
|
+
customHome
|
|
1380
|
+
);
|
|
1381
|
+
const stored = storage.listConfig();
|
|
1382
|
+
let declared: Record<string, any> = {};
|
|
1383
|
+
if (root) {
|
|
1384
|
+
try {
|
|
1385
|
+
const cfg = loadProjectConfig(root);
|
|
1386
|
+
declared = cfg.config || {};
|
|
1387
|
+
} catch {}
|
|
1388
|
+
}
|
|
1389
|
+
const maskedValues: Record<string, any> = {};
|
|
1390
|
+
for (const [k, v] of Object.entries(stored)) {
|
|
1391
|
+
if (declared[k]?.secret) {
|
|
1392
|
+
maskedValues[k] = "******";
|
|
1393
|
+
} else {
|
|
1394
|
+
maskedValues[k] = v;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return jsonResponse(
|
|
1398
|
+
{ ok: true, packageId, declared, values: maskedValues },
|
|
1399
|
+
200,
|
|
1400
|
+
corsHeaders
|
|
1401
|
+
);
|
|
1402
|
+
} catch (err: any) {
|
|
1403
|
+
return jsonResponse(
|
|
1404
|
+
{ ok: false, error: { code: "CONFIG_LIST_ERROR", message: err.message } },
|
|
1405
|
+
500,
|
|
1406
|
+
corsHeaders
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// 19. Config Update: PUT / POST /api/v1/config
|
|
1412
|
+
if (pathname === "/api/v1/config" && (req.method === "PUT" || req.method === "POST")) {
|
|
1413
|
+
try {
|
|
1414
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
1415
|
+
const pkgParam = url.searchParams.get("package") || body.package || undefined;
|
|
1416
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1417
|
+
pkgParam,
|
|
1418
|
+
runtimeRegistry,
|
|
1419
|
+
projectRoot,
|
|
1420
|
+
customHome
|
|
1421
|
+
);
|
|
1422
|
+
const key = body.key;
|
|
1423
|
+
if (!key) {
|
|
1424
|
+
return jsonResponse(
|
|
1425
|
+
{ ok: false, error: { code: "INVALID_ARGUMENT", message: "Config 'key' is required" } },
|
|
1426
|
+
400,
|
|
1427
|
+
corsHeaders
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
storage.setConfig(key, body.value);
|
|
1431
|
+
return jsonResponse({ ok: true, packageId, key, message: "updated" }, 200, corsHeaders);
|
|
1432
|
+
} catch (err: any) {
|
|
1433
|
+
return jsonResponse(
|
|
1434
|
+
{ ok: false, error: { code: "CONFIG_SET_ERROR", message: err.message } },
|
|
1435
|
+
500,
|
|
1436
|
+
corsHeaders
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// 20. Config Delete: DELETE /api/v1/config/:key
|
|
1442
|
+
const configKeyMatch = pathname.match(/^\/api\/v1\/config\/([^/]+)$/);
|
|
1443
|
+
if (configKeyMatch && req.method === "DELETE") {
|
|
1444
|
+
try {
|
|
1445
|
+
const key = decodeURIComponent(configKeyMatch[1]);
|
|
1446
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1447
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1448
|
+
pkgParam,
|
|
1449
|
+
runtimeRegistry,
|
|
1450
|
+
projectRoot,
|
|
1451
|
+
customHome
|
|
1452
|
+
);
|
|
1453
|
+
const deleted = storage.deleteConfig(key);
|
|
1454
|
+
return jsonResponse({ ok: true, packageId, key, deleted }, 200, corsHeaders);
|
|
1455
|
+
} catch (err: any) {
|
|
1456
|
+
return jsonResponse(
|
|
1457
|
+
{ ok: false, error: { code: "CONFIG_DELETE_ERROR", message: err.message } },
|
|
1458
|
+
500,
|
|
1459
|
+
corsHeaders
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
544
1464
|
// 404 Not Found
|
|
545
1465
|
return jsonResponse(
|
|
546
1466
|
{
|
|
@@ -553,8 +1473,8 @@ export function startActionDockServer(
|
|
|
553
1473
|
404,
|
|
554
1474
|
corsHeaders
|
|
555
1475
|
);
|
|
556
|
-
}
|
|
557
|
-
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
558
1478
|
|
|
559
1479
|
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
560
1480
|
const url = `http://${actualHost}:${server.port}`;
|