@actiondock/core 2.0.1 → 2.0.2
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 +1 -1
- package/src/profile/client.ts +252 -2
- package/src/server/runtime-registry.ts +35 -0
- package/src/server/server.ts +892 -66
- package/src/server/types.ts +4 -0
- package/src/storage/sqlite.ts +43 -16
- package/src/storage/types.ts +2 -0
package/src/server/server.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
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";
|
|
13
21
|
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "./body";
|
|
14
22
|
import { ServerRuntimeRegistry } from "./runtime-registry";
|
|
15
23
|
import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security";
|
|
@@ -70,20 +78,83 @@ function findRunAcrossStorages(
|
|
|
70
78
|
return null;
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
/**
|
|
82
|
+
* 辅助函数:依据 package 参数或项目上下文解析目标 Storage 实例与根目录。
|
|
83
|
+
*/
|
|
84
|
+
function resolveStorageForPackage(
|
|
85
|
+
packageIdOrPath: string | undefined,
|
|
86
|
+
runtimeRegistry: ServerRuntimeRegistry,
|
|
87
|
+
projectRoot?: string | null,
|
|
88
|
+
customHome?: string
|
|
89
|
+
): { packageId: string; storage: RuntimeStorage; projectRoot?: string } {
|
|
90
|
+
if (packageIdOrPath) {
|
|
91
|
+
const root = resolvePackageRoot(packageIdOrPath, customHome);
|
|
92
|
+
if (root) {
|
|
93
|
+
const config = loadProjectConfig(root);
|
|
94
|
+
return {
|
|
95
|
+
packageId: config.id,
|
|
96
|
+
storage: runtimeRegistry.getStorage(config.id, root),
|
|
97
|
+
projectRoot: root,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
packageId: packageIdOrPath,
|
|
102
|
+
storage: runtimeRegistry.getStorage(packageIdOrPath),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (projectRoot) {
|
|
107
|
+
const config = loadProjectConfig(projectRoot);
|
|
108
|
+
return {
|
|
109
|
+
packageId: config.id,
|
|
110
|
+
storage: runtimeRegistry.getStorage(config.id, projectRoot),
|
|
111
|
+
projectRoot,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const linked = listLinkedPackages(customHome);
|
|
116
|
+
if (linked.length > 0) {
|
|
117
|
+
const first = linked[0];
|
|
118
|
+
return {
|
|
119
|
+
packageId: first.id,
|
|
120
|
+
storage: runtimeRegistry.getStorage(first.id, first.path),
|
|
121
|
+
projectRoot: first.path,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
packageId: "default",
|
|
127
|
+
storage: runtimeRegistry.getStorage("default"),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
73
131
|
/**
|
|
74
132
|
* 启动 ActionDock 2.0 原生轻量级 HTTP Runner 服务端。
|
|
75
133
|
*
|
|
76
|
-
*
|
|
77
|
-
* -
|
|
78
|
-
* -
|
|
79
|
-
* -
|
|
80
|
-
* -
|
|
81
|
-
* -
|
|
82
|
-
* -
|
|
83
|
-
* -
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
134
|
+
* 全面暴露 RESTful 调度与运维接口:
|
|
135
|
+
* - GET /api/v1/health : 健康检查与就绪状态
|
|
136
|
+
* - GET /api/v1/info : 统一自省探索(支持 ?intent= 智能决议、?package= 详情下钻、?tree=true 工作区拓扑)
|
|
137
|
+
* - GET /api/v1/doctor : 深度环境与依赖诊断
|
|
138
|
+
* - GET /api/v1/actions : 列出可用 Actions(支持 ?intent= 与 ?package=)
|
|
139
|
+
* - GET /api/v1/actions/:id : 查看单个 Action 的 Schema 详情
|
|
140
|
+
* - POST /api/v1/actions/:id/run : 同步或异步执行 Action
|
|
141
|
+
* - GET /api/v1/playbooks : 列出可用 Playbook 工作流
|
|
142
|
+
* - GET /api/v1/playbooks/:id : 查看 Playbook 步骤流程与 SOP 规程正文
|
|
143
|
+
* - GET /api/v1/runs : 多维检索历史运行记录
|
|
144
|
+
* - POST /api/v1/runs/clear : 批量清理历史运行记录
|
|
145
|
+
* - GET /api/v1/runs/:runId : 查询历史或异步任务运行状态
|
|
146
|
+
* - POST /api/v1/runs/:runId/cancel : 中断在途任务
|
|
147
|
+
* - GET /api/v1/runs/:runId/stream : 基于 SSE 实时长任务日志与进度流推送
|
|
148
|
+
* - GET /api/v1/state : 检索状态键名列表
|
|
149
|
+
* - GET /api/v1/state/:key : 获取指定状态值
|
|
150
|
+
* - PUT /api/v1/state/:key : 写入持久化状态(支持 TTL)
|
|
151
|
+
* - DELETE /api/v1/state/:key : 删除指定状态
|
|
152
|
+
* - POST /api/v1/state/clear : 清空状态数据
|
|
153
|
+
* - GET /api/v1/config : 获取配置项规格与当前值(敏感凭证脱敏)
|
|
154
|
+
* - PUT /api/v1/config : 持久化更新配置
|
|
155
|
+
* - DELETE /api/v1/config/:key : 删除配置项
|
|
156
|
+
* - GET /api/v1/config/env : 检查环境变量满足率
|
|
157
|
+
* - ALL /mcp : 一体化 MCP Streamable HTTP 协议端点(可选)
|
|
87
158
|
*/
|
|
88
159
|
export function startActionDockServer(
|
|
89
160
|
options: ServerOptions = {}
|
|
@@ -122,7 +193,7 @@ export function startActionDockServer(
|
|
|
122
193
|
const url = new URL(req.url);
|
|
123
194
|
const pathname = url.pathname;
|
|
124
195
|
|
|
125
|
-
//
|
|
196
|
+
// 0. Health Check (supports /api/v1/health and /health)
|
|
126
197
|
if (pathname === "/api/v1/health" || pathname === "/health") {
|
|
127
198
|
if (!verifyBearerToken(req, token)) {
|
|
128
199
|
return jsonResponse(
|
|
@@ -149,7 +220,30 @@ export function startActionDockServer(
|
|
|
149
220
|
return jsonResponse(healthData, 200, corsHeaders);
|
|
150
221
|
}
|
|
151
222
|
|
|
152
|
-
//
|
|
223
|
+
// 1. MCP Unified Gateway: /mcp
|
|
224
|
+
if (
|
|
225
|
+
options.enableMcp !== false &&
|
|
226
|
+
options.mcpHandler &&
|
|
227
|
+
(pathname === "/mcp" || pathname.startsWith("/mcp/"))
|
|
228
|
+
) {
|
|
229
|
+
if (!verifyBearerToken(req, token)) {
|
|
230
|
+
return jsonResponse(
|
|
231
|
+
{
|
|
232
|
+
ok: false,
|
|
233
|
+
error: {
|
|
234
|
+
code: "UNAUTHORIZED",
|
|
235
|
+
message: "Invalid or missing Bearer token",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
401,
|
|
239
|
+
corsHeaders
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const mcpRes = await options.mcpHandler(req);
|
|
243
|
+
if (mcpRes) return mcpRes;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 鉴权检查(后续所有 /api/v1/* 均要求鉴权)
|
|
153
247
|
if (!verifyBearerToken(req, token)) {
|
|
154
248
|
return jsonResponse(
|
|
155
249
|
{
|
|
@@ -164,40 +258,202 @@ export function startActionDockServer(
|
|
|
164
258
|
);
|
|
165
259
|
}
|
|
166
260
|
|
|
167
|
-
// 2. Info
|
|
261
|
+
// 2. Info 自省: GET /api/v1/info
|
|
168
262
|
if (pathname === "/api/v1/info" && req.method === "GET") {
|
|
169
263
|
try {
|
|
264
|
+
const isTree = url.searchParams.get("tree") === "true";
|
|
265
|
+
if (isTree) {
|
|
266
|
+
const status = getRegistryStatus(customHome);
|
|
267
|
+
return jsonResponse(
|
|
268
|
+
{ ok: true, type: "tree", ...status },
|
|
269
|
+
200,
|
|
270
|
+
corsHeaders
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
275
|
+
const targetPkg = url.searchParams.get("package") || undefined;
|
|
276
|
+
|
|
277
|
+
// 收集所有候选包全量元数据
|
|
278
|
+
const aggregatedPackages: any[] = [];
|
|
279
|
+
|
|
170
280
|
if (projectRoot) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
281
|
+
try {
|
|
282
|
+
const config = loadProjectConfig(projectRoot);
|
|
283
|
+
const actions = await loadActions(projectRoot, config.actionsDir);
|
|
284
|
+
const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
|
|
285
|
+
aggregatedPackages.push({
|
|
286
|
+
id: config.id,
|
|
287
|
+
name: config.name,
|
|
288
|
+
version: config.version,
|
|
289
|
+
description: config.description || "",
|
|
290
|
+
path: projectRoot,
|
|
291
|
+
actionsCount: actions.size,
|
|
292
|
+
playbooksCount: playbooks.size,
|
|
293
|
+
actions: Array.from(actions.entries()).map(([id, a]) => ({
|
|
294
|
+
id,
|
|
295
|
+
description: a.description || "",
|
|
296
|
+
inputSchema: a.inputSchema || null,
|
|
297
|
+
outputSchema: a.outputSchema || null,
|
|
298
|
+
})),
|
|
299
|
+
playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
|
|
300
|
+
id,
|
|
301
|
+
description: p.description || "",
|
|
302
|
+
actions: p.actions || [],
|
|
303
|
+
})),
|
|
304
|
+
configDeclared: config.config || {},
|
|
305
|
+
});
|
|
306
|
+
} catch {
|
|
307
|
+
// 忽略解析异常
|
|
187
308
|
}
|
|
188
|
-
return jsonResponse(infoData, 200, corsHeaders);
|
|
189
309
|
} else {
|
|
190
310
|
const linked = listLinkedPackages(customHome);
|
|
311
|
+
for (const pkg of linked) {
|
|
312
|
+
if (!existsSync(pkg.path)) continue;
|
|
313
|
+
try {
|
|
314
|
+
const config = loadProjectConfig(pkg.path);
|
|
315
|
+
const actions = await loadActions(pkg.path, config.actionsDir);
|
|
316
|
+
const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
|
|
317
|
+
aggregatedPackages.push({
|
|
318
|
+
id: config.id,
|
|
319
|
+
name: config.name,
|
|
320
|
+
version: config.version,
|
|
321
|
+
description: config.description || "",
|
|
322
|
+
path: pkg.path,
|
|
323
|
+
actionsCount: actions.size,
|
|
324
|
+
playbooksCount: playbooks.size,
|
|
325
|
+
actions: Array.from(actions.entries()).map(([id, a]) => ({
|
|
326
|
+
id,
|
|
327
|
+
description: a.description || "",
|
|
328
|
+
inputSchema: a.inputSchema || null,
|
|
329
|
+
outputSchema: a.outputSchema || null,
|
|
330
|
+
})),
|
|
331
|
+
playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
|
|
332
|
+
id,
|
|
333
|
+
description: p.description || "",
|
|
334
|
+
actions: p.actions || [],
|
|
335
|
+
})),
|
|
336
|
+
configDeclared: config.config || {},
|
|
337
|
+
});
|
|
338
|
+
} catch {
|
|
339
|
+
// 忽略故障包
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 显式指定 package 详情下钻
|
|
345
|
+
if (targetPkg) {
|
|
346
|
+
const matched = aggregatedPackages.find(
|
|
347
|
+
(p) => p.id === targetPkg || p.path === targetPkg
|
|
348
|
+
);
|
|
349
|
+
if (!matched) {
|
|
350
|
+
return jsonResponse(
|
|
351
|
+
{
|
|
352
|
+
ok: false,
|
|
353
|
+
error: {
|
|
354
|
+
code: "PACKAGE_NOT_FOUND",
|
|
355
|
+
message: `Package '${targetPkg}' not found on remote server`,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
404,
|
|
359
|
+
corsHeaders
|
|
360
|
+
);
|
|
361
|
+
}
|
|
191
362
|
return jsonResponse(
|
|
192
363
|
{
|
|
193
364
|
ok: true,
|
|
194
|
-
|
|
195
|
-
|
|
365
|
+
type: "package_detail",
|
|
366
|
+
...matched,
|
|
367
|
+
...(options.exposeDebugInfo ? { projectRoot: matched.path } : {}),
|
|
196
368
|
},
|
|
197
369
|
200,
|
|
198
370
|
corsHeaders
|
|
199
371
|
);
|
|
200
372
|
}
|
|
373
|
+
|
|
374
|
+
// 意图过滤与决议
|
|
375
|
+
if (intent) {
|
|
376
|
+
const filterRes = filterWithFallbackInfo(
|
|
377
|
+
aggregatedPackages,
|
|
378
|
+
intent,
|
|
379
|
+
[
|
|
380
|
+
(p) => p.id,
|
|
381
|
+
(p) => p.name,
|
|
382
|
+
(p) => p.description,
|
|
383
|
+
(p) => p.actions.map((a: any) => a.id),
|
|
384
|
+
(p) => p.actions.map((a: any) => a.description),
|
|
385
|
+
(p) => p.playbooks.map((pb: any) => pb.id),
|
|
386
|
+
(p) => p.playbooks.map((pb: any) => pb.description),
|
|
387
|
+
],
|
|
388
|
+
true
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// 唯一命中智能展开详情
|
|
392
|
+
if (filterRes.matchedCount === 1) {
|
|
393
|
+
const single = filterRes.items[0];
|
|
394
|
+
return jsonResponse(
|
|
395
|
+
{
|
|
396
|
+
ok: true,
|
|
397
|
+
type: "package_detail",
|
|
398
|
+
isSingleMatch: true,
|
|
399
|
+
...single,
|
|
400
|
+
...(options.exposeDebugInfo ? { projectRoot: single.path } : {}),
|
|
401
|
+
},
|
|
402
|
+
200,
|
|
403
|
+
corsHeaders
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return jsonResponse(
|
|
408
|
+
{
|
|
409
|
+
ok: true,
|
|
410
|
+
type: "package_list",
|
|
411
|
+
isFallback: filterRes.isFallback,
|
|
412
|
+
packages: filterRes.items,
|
|
413
|
+
},
|
|
414
|
+
200,
|
|
415
|
+
corsHeaders
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 单包模式直接展开详情
|
|
420
|
+
if (projectRoot && aggregatedPackages.length === 1) {
|
|
421
|
+
const p = aggregatedPackages[0];
|
|
422
|
+
return jsonResponse(
|
|
423
|
+
{
|
|
424
|
+
ok: true,
|
|
425
|
+
type: "package_detail",
|
|
426
|
+
id: p.id,
|
|
427
|
+
name: p.name,
|
|
428
|
+
version: p.version,
|
|
429
|
+
description: p.description,
|
|
430
|
+
actionsCount: p.actionsCount,
|
|
431
|
+
playbooksCount: p.playbooksCount,
|
|
432
|
+
actions: p.actions.map((a: any) => a.id),
|
|
433
|
+
actionsDetail: p.actions,
|
|
434
|
+
playbooks: p.playbooks.map((pb: any) => pb.id),
|
|
435
|
+
playbooksDetail: p.playbooks,
|
|
436
|
+
configDeclared: p.configDeclared,
|
|
437
|
+
linkedPackages: listLinkedPackages(customHome),
|
|
438
|
+
...(options.exposeDebugInfo ? { projectRoot } : {}),
|
|
439
|
+
},
|
|
440
|
+
200,
|
|
441
|
+
corsHeaders
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 全局注册表概览
|
|
446
|
+
return jsonResponse(
|
|
447
|
+
{
|
|
448
|
+
ok: true,
|
|
449
|
+
type: "package_list",
|
|
450
|
+
version: "2.0.0",
|
|
451
|
+
packages: aggregatedPackages,
|
|
452
|
+
linkedPackages: listLinkedPackages(customHome),
|
|
453
|
+
},
|
|
454
|
+
200,
|
|
455
|
+
corsHeaders
|
|
456
|
+
);
|
|
201
457
|
} catch (err: any) {
|
|
202
458
|
return jsonResponse(
|
|
203
459
|
{
|
|
@@ -210,7 +466,26 @@ export function startActionDockServer(
|
|
|
210
466
|
}
|
|
211
467
|
}
|
|
212
468
|
|
|
213
|
-
// 3.
|
|
469
|
+
// 3. Doctor 深度体检: GET /api/v1/doctor
|
|
470
|
+
if (pathname === "/api/v1/doctor" && req.method === "GET") {
|
|
471
|
+
try {
|
|
472
|
+
const targetPkg = url.searchParams.get("package") || undefined;
|
|
473
|
+
const report = await runDoctorChecks({
|
|
474
|
+
cwd: projectRoot || process.cwd(),
|
|
475
|
+
packageIdOrPath: targetPkg,
|
|
476
|
+
customHome,
|
|
477
|
+
});
|
|
478
|
+
return jsonResponse({ ok: true, report }, 200, corsHeaders);
|
|
479
|
+
} catch (err: any) {
|
|
480
|
+
return jsonResponse(
|
|
481
|
+
{ ok: false, error: { code: "DOCTOR_ERROR", message: err.message } },
|
|
482
|
+
500,
|
|
483
|
+
corsHeaders
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 4. Actions List: GET /api/v1/actions
|
|
214
489
|
if (pathname === "/api/v1/actions" && req.method === "GET") {
|
|
215
490
|
try {
|
|
216
491
|
const actionList: Array<{
|
|
@@ -251,15 +526,21 @@ export function startActionDockServer(
|
|
|
251
526
|
}
|
|
252
527
|
|
|
253
528
|
const intent = url.searchParams.get("intent");
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
259
|
-
false
|
|
260
|
-
)
|
|
529
|
+
const targetPkg = url.searchParams.get("package");
|
|
530
|
+
|
|
531
|
+
let filtered = targetPkg
|
|
532
|
+
? actionList.filter((a) => a.packageId === targetPkg)
|
|
261
533
|
: actionList;
|
|
262
534
|
|
|
535
|
+
if (intent) {
|
|
536
|
+
filtered = filterByIntent(
|
|
537
|
+
filtered,
|
|
538
|
+
intent,
|
|
539
|
+
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
540
|
+
false
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
263
544
|
return jsonResponse(filtered, 200, corsHeaders);
|
|
264
545
|
} catch (err: any) {
|
|
265
546
|
return jsonResponse(
|
|
@@ -273,7 +554,7 @@ export function startActionDockServer(
|
|
|
273
554
|
}
|
|
274
555
|
}
|
|
275
556
|
|
|
276
|
-
//
|
|
557
|
+
// 5. Action Show: GET /api/v1/actions/:id
|
|
277
558
|
const actionShowMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)$/);
|
|
278
559
|
if (actionShowMatch && req.method === "GET") {
|
|
279
560
|
const actionId = decodeURIComponent(actionShowMatch[1]);
|
|
@@ -323,7 +604,7 @@ export function startActionDockServer(
|
|
|
323
604
|
}
|
|
324
605
|
}
|
|
325
606
|
|
|
326
|
-
//
|
|
607
|
+
// 6. Action Run: POST /api/v1/actions/:id/run
|
|
327
608
|
const actionRunMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)\/run$/);
|
|
328
609
|
if (actionRunMatch && req.method === "POST") {
|
|
329
610
|
const actionId = decodeURIComponent(actionRunMatch[1]);
|
|
@@ -336,10 +617,7 @@ export function startActionDockServer(
|
|
|
336
617
|
{
|
|
337
618
|
ok: false,
|
|
338
619
|
runId: randomUUID(),
|
|
339
|
-
error: {
|
|
340
|
-
code: "REQUEST_TOO_LARGE",
|
|
341
|
-
message: err.message,
|
|
342
|
-
},
|
|
620
|
+
error: { code: "REQUEST_TOO_LARGE", message: err.message },
|
|
343
621
|
},
|
|
344
622
|
413,
|
|
345
623
|
corsHeaders
|
|
@@ -350,10 +628,7 @@ export function startActionDockServer(
|
|
|
350
628
|
{
|
|
351
629
|
ok: false,
|
|
352
630
|
runId: randomUUID(),
|
|
353
|
-
error: {
|
|
354
|
-
code: "INVALID_JSON",
|
|
355
|
-
message: err.message,
|
|
356
|
-
},
|
|
631
|
+
error: { code: "INVALID_JSON", message: err.message },
|
|
357
632
|
},
|
|
358
633
|
400,
|
|
359
634
|
corsHeaders
|
|
@@ -363,10 +638,7 @@ export function startActionDockServer(
|
|
|
363
638
|
{
|
|
364
639
|
ok: false,
|
|
365
640
|
runId: randomUUID(),
|
|
366
|
-
error: {
|
|
367
|
-
code: "INVALID_JSON",
|
|
368
|
-
message: `Failed to parse request body: ${err.message}`,
|
|
369
|
-
},
|
|
641
|
+
error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
|
|
370
642
|
},
|
|
371
643
|
400,
|
|
372
644
|
corsHeaders
|
|
@@ -391,30 +663,49 @@ export function startActionDockServer(
|
|
|
391
663
|
actions,
|
|
392
664
|
});
|
|
393
665
|
|
|
394
|
-
const
|
|
666
|
+
const isAsync = body?.execution?.mode === "async" || body?.async === true;
|
|
395
667
|
const timeoutMs =
|
|
396
668
|
typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
|
|
397
669
|
? body.execution.timeoutMs
|
|
398
670
|
: undefined;
|
|
399
671
|
|
|
400
|
-
if (
|
|
672
|
+
if (isAsync) {
|
|
401
673
|
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
402
674
|
timeoutMs,
|
|
403
675
|
});
|
|
404
676
|
runtimeRegistry.executionManager.register(handle);
|
|
405
677
|
|
|
678
|
+
// 监听结算以广播 SSE 完成事件
|
|
679
|
+
handle.result
|
|
680
|
+
.then((res) => {
|
|
681
|
+
runtimeRegistry.emit(handle.runId, { type: "finish", data: res });
|
|
682
|
+
})
|
|
683
|
+
.catch((err) => {
|
|
684
|
+
runtimeRegistry.emit(handle.runId, {
|
|
685
|
+
type: "finish",
|
|
686
|
+
data: {
|
|
687
|
+
ok: false,
|
|
688
|
+
error: {
|
|
689
|
+
code: "ACTION_EXECUTION_ERROR",
|
|
690
|
+
message: err?.message || String(err),
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
406
696
|
return jsonResponse(
|
|
407
697
|
{
|
|
408
698
|
ok: true,
|
|
409
699
|
runId: handle.runId,
|
|
410
700
|
status: "running",
|
|
701
|
+
streamUrl: `/api/v1/runs/${handle.runId}/stream`,
|
|
411
702
|
},
|
|
412
703
|
202,
|
|
413
704
|
corsHeaders
|
|
414
705
|
);
|
|
415
706
|
}
|
|
416
707
|
|
|
417
|
-
//
|
|
708
|
+
// 同步执行模式
|
|
418
709
|
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
419
710
|
signal: req.signal,
|
|
420
711
|
timeoutMs,
|
|
@@ -439,7 +730,289 @@ export function startActionDockServer(
|
|
|
439
730
|
}
|
|
440
731
|
}
|
|
441
732
|
|
|
442
|
-
//
|
|
733
|
+
// 7. Playbooks List: GET /api/v1/playbooks
|
|
734
|
+
if (pathname === "/api/v1/playbooks" && req.method === "GET") {
|
|
735
|
+
try {
|
|
736
|
+
const pbList: Array<{
|
|
737
|
+
id: string;
|
|
738
|
+
description: string;
|
|
739
|
+
actions: string[];
|
|
740
|
+
packageId: string;
|
|
741
|
+
filePath: string;
|
|
742
|
+
}> = [];
|
|
743
|
+
|
|
744
|
+
const targetPkg = url.searchParams.get("package");
|
|
745
|
+
const intent = url.searchParams.get("intent");
|
|
746
|
+
|
|
747
|
+
const roots: Array<{ root: string; packageId: string }> = [];
|
|
748
|
+
if (projectRoot) {
|
|
749
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
750
|
+
roots.push({ root: projectRoot, packageId: cfg.id });
|
|
751
|
+
}
|
|
752
|
+
const linked = listLinkedPackages(customHome);
|
|
753
|
+
for (const pkg of linked) {
|
|
754
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
755
|
+
if (!existsSync(pkg.path)) continue;
|
|
756
|
+
roots.push({ root: pkg.path, packageId: pkg.id });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
for (const item of roots) {
|
|
760
|
+
if (targetPkg && item.packageId !== targetPkg && item.root !== targetPkg) continue;
|
|
761
|
+
try {
|
|
762
|
+
const cfg = loadProjectConfig(item.root);
|
|
763
|
+
const pbs = loadPlaybooks(item.root, cfg.playbooksDir);
|
|
764
|
+
for (const [id, pb] of pbs.entries()) {
|
|
765
|
+
pbList.push({
|
|
766
|
+
id,
|
|
767
|
+
description: pb.description || "",
|
|
768
|
+
actions: pb.actions || [],
|
|
769
|
+
packageId: item.packageId,
|
|
770
|
+
filePath: pb.filePath,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
} catch {}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const filtered = intent
|
|
777
|
+
? filterByIntent(
|
|
778
|
+
pbList,
|
|
779
|
+
intent,
|
|
780
|
+
[(p) => p.id, (p) => p.description, (p) => p.packageId, (p) => p.actions],
|
|
781
|
+
false
|
|
782
|
+
)
|
|
783
|
+
: pbList;
|
|
784
|
+
|
|
785
|
+
return jsonResponse(filtered, 200, corsHeaders);
|
|
786
|
+
} catch (err: any) {
|
|
787
|
+
return jsonResponse(
|
|
788
|
+
{ ok: false, error: { code: "PLAYBOOKS_LIST_ERROR", message: err.message } },
|
|
789
|
+
500,
|
|
790
|
+
corsHeaders
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 8. Playbook Show: GET /api/v1/playbooks/:id
|
|
796
|
+
const pbShowMatch = pathname.match(/^\/api\/v1\/playbooks\/([^/]+)$/);
|
|
797
|
+
if (pbShowMatch && req.method === "GET") {
|
|
798
|
+
const pbId = decodeURIComponent(pbShowMatch[1]);
|
|
799
|
+
try {
|
|
800
|
+
const resolved = resolvePlaybookProject(
|
|
801
|
+
pbId,
|
|
802
|
+
projectRoot || process.cwd(),
|
|
803
|
+
customHome
|
|
804
|
+
);
|
|
805
|
+
const pb = resolved.playbook;
|
|
806
|
+
return jsonResponse(
|
|
807
|
+
{
|
|
808
|
+
ok: true,
|
|
809
|
+
id: pb.id,
|
|
810
|
+
packageId: resolved.packageId,
|
|
811
|
+
description: pb.description || "",
|
|
812
|
+
actions: pb.actions || [],
|
|
813
|
+
filePath: pb.filePath,
|
|
814
|
+
content: pb.content,
|
|
815
|
+
},
|
|
816
|
+
200,
|
|
817
|
+
corsHeaders
|
|
818
|
+
);
|
|
819
|
+
} catch (err: any) {
|
|
820
|
+
return jsonResponse(
|
|
821
|
+
{ ok: false, error: { code: "PLAYBOOK_NOT_FOUND", message: err.message } },
|
|
822
|
+
404,
|
|
823
|
+
corsHeaders
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 9. Runs List: GET /api/v1/runs
|
|
829
|
+
if (pathname === "/api/v1/runs" && req.method === "GET") {
|
|
830
|
+
try {
|
|
831
|
+
const status = url.searchParams.get("status") || undefined;
|
|
832
|
+
const actionId = url.searchParams.get("actionId") || undefined;
|
|
833
|
+
const packageId = url.searchParams.get("packageId") || undefined;
|
|
834
|
+
const intent = url.searchParams.get("intent") || undefined;
|
|
835
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
836
|
+
|
|
837
|
+
const allRuns: any[] = [];
|
|
838
|
+
const seenRunIds = new Set<string>();
|
|
839
|
+
|
|
840
|
+
const candidateStorages: Array<{ packageId: string; storage: RuntimeStorage }> = [];
|
|
841
|
+
if (projectRoot) {
|
|
842
|
+
try {
|
|
843
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
844
|
+
candidateStorages.push({
|
|
845
|
+
packageId: cfg.id,
|
|
846
|
+
storage: runtimeRegistry.getStorage(cfg.id, projectRoot),
|
|
847
|
+
});
|
|
848
|
+
} catch {}
|
|
849
|
+
}
|
|
850
|
+
const linked = listLinkedPackages(customHome);
|
|
851
|
+
for (const pkg of linked) {
|
|
852
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
853
|
+
if (!existsSync(pkg.path)) continue;
|
|
854
|
+
candidateStorages.push({
|
|
855
|
+
packageId: pkg.id,
|
|
856
|
+
storage: runtimeRegistry.getStorage(pkg.id, pkg.path),
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
for (const item of candidateStorages) {
|
|
861
|
+
if (packageId && item.packageId !== packageId) continue;
|
|
862
|
+
try {
|
|
863
|
+
const records = item.storage.listRuns({ actionId, limit });
|
|
864
|
+
for (const r of records) {
|
|
865
|
+
if (!seenRunIds.has(r.id)) {
|
|
866
|
+
seenRunIds.add(r.id);
|
|
867
|
+
if (status && r.status !== status) continue;
|
|
868
|
+
allRuns.push(r);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
} catch {}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
875
|
+
|
|
876
|
+
const filtered = intent
|
|
877
|
+
? filterByIntent(
|
|
878
|
+
allRuns,
|
|
879
|
+
intent,
|
|
880
|
+
[(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.packageId],
|
|
881
|
+
false
|
|
882
|
+
)
|
|
883
|
+
: allRuns;
|
|
884
|
+
|
|
885
|
+
const sliced = filtered.slice(0, limit);
|
|
886
|
+
return jsonResponse(
|
|
887
|
+
{ ok: true, total: filtered.length, items: sliced },
|
|
888
|
+
200,
|
|
889
|
+
corsHeaders
|
|
890
|
+
);
|
|
891
|
+
} catch (err: any) {
|
|
892
|
+
return jsonResponse(
|
|
893
|
+
{ ok: false, error: { code: "RUNS_LIST_ERROR", message: err.message } },
|
|
894
|
+
500,
|
|
895
|
+
corsHeaders
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 10. Runs Clear: POST /api/v1/runs/clear or DELETE /api/v1/runs
|
|
901
|
+
if (
|
|
902
|
+
(pathname === "/api/v1/runs/clear" && req.method === "POST") ||
|
|
903
|
+
(pathname === "/api/v1/runs" && req.method === "DELETE")
|
|
904
|
+
) {
|
|
905
|
+
try {
|
|
906
|
+
let body: any = {};
|
|
907
|
+
if (req.method === "POST" || req.headers.get("content-type")?.includes("json")) {
|
|
908
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
|
|
909
|
+
}
|
|
910
|
+
const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
|
|
911
|
+
const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
|
|
912
|
+
const status = url.searchParams.get("status") || body.status || undefined;
|
|
913
|
+
|
|
914
|
+
let clearedCount = 0;
|
|
915
|
+
const candidateStorages: RuntimeStorage[] = [];
|
|
916
|
+
if (projectRoot) {
|
|
917
|
+
try {
|
|
918
|
+
const cfg = loadProjectConfig(projectRoot);
|
|
919
|
+
candidateStorages.push(runtimeRegistry.getStorage(cfg.id, projectRoot));
|
|
920
|
+
} catch {}
|
|
921
|
+
}
|
|
922
|
+
const linked = listLinkedPackages(customHome);
|
|
923
|
+
for (const pkg of linked) {
|
|
924
|
+
if (projectRoot && pkg.path === projectRoot) continue;
|
|
925
|
+
if (!existsSync(pkg.path)) continue;
|
|
926
|
+
if (packageId && pkg.id !== packageId) continue;
|
|
927
|
+
candidateStorages.push(runtimeRegistry.getStorage(pkg.id, pkg.path));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
for (const storage of candidateStorages) {
|
|
931
|
+
clearedCount += storage.clearRuns({ actionId, status });
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
|
|
935
|
+
} catch (err: any) {
|
|
936
|
+
return jsonResponse(
|
|
937
|
+
{ ok: false, error: { code: "RUNS_CLEAR_ERROR", message: err.message } },
|
|
938
|
+
500,
|
|
939
|
+
corsHeaders
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// 11. Run Stream (SSE): GET /api/v1/runs/:runId/stream
|
|
945
|
+
const runStreamMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/stream$/);
|
|
946
|
+
if (runStreamMatch && req.method === "GET") {
|
|
947
|
+
const runId = decodeURIComponent(runStreamMatch[1]);
|
|
948
|
+
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
949
|
+
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
950
|
+
|
|
951
|
+
if (!found && !activeHandle) {
|
|
952
|
+
return jsonResponse(
|
|
953
|
+
{ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run '${runId}' not found` } },
|
|
954
|
+
404,
|
|
955
|
+
corsHeaders
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const stream = new ReadableStream({
|
|
960
|
+
start(controller) {
|
|
961
|
+
const encoder = new TextEncoder();
|
|
962
|
+
const sendEvent = (event: string, data: any) => {
|
|
963
|
+
try {
|
|
964
|
+
controller.enqueue(
|
|
965
|
+
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
966
|
+
);
|
|
967
|
+
} catch {}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
if (activeHandle) {
|
|
971
|
+
sendEvent("status", { runId, status: "running" });
|
|
972
|
+
const unsubscribe = runtimeRegistry.subscribe(runId, (evt) => {
|
|
973
|
+
sendEvent(evt.type, evt.data);
|
|
974
|
+
if (evt.type === "finish") {
|
|
975
|
+
try { controller.close(); } catch {}
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
activeHandle.result.then(
|
|
980
|
+
(res) => {
|
|
981
|
+
sendEvent("finish", res);
|
|
982
|
+
try { controller.close(); } catch {}
|
|
983
|
+
},
|
|
984
|
+
(err) => {
|
|
985
|
+
sendEvent("finish", {
|
|
986
|
+
ok: false,
|
|
987
|
+
error: { message: err?.message || String(err) },
|
|
988
|
+
});
|
|
989
|
+
try { controller.close(); } catch {}
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
req.signal.addEventListener("abort", () => {
|
|
994
|
+
unsubscribe();
|
|
995
|
+
try { controller.close(); } catch {}
|
|
996
|
+
});
|
|
997
|
+
} else if (found) {
|
|
998
|
+
sendEvent("finish", found.run);
|
|
999
|
+
controller.close();
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
return new Response(stream, {
|
|
1005
|
+
status: 200,
|
|
1006
|
+
headers: {
|
|
1007
|
+
"Content-Type": "text/event-stream",
|
|
1008
|
+
"Cache-Control": "no-cache",
|
|
1009
|
+
"Connection": "keep-alive",
|
|
1010
|
+
...corsHeaders,
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// 12. Run Show: GET /api/v1/runs/:runId
|
|
443
1016
|
const runShowMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
|
|
444
1017
|
if (runShowMatch && req.method === "GET") {
|
|
445
1018
|
const runId = decodeURIComponent(runShowMatch[1]);
|
|
@@ -462,7 +1035,7 @@ export function startActionDockServer(
|
|
|
462
1035
|
return jsonResponse(found.run, 200, corsHeaders);
|
|
463
1036
|
}
|
|
464
1037
|
|
|
465
|
-
//
|
|
1038
|
+
// 13. Run Cancel: POST /api/v1/runs/:runId/cancel
|
|
466
1039
|
const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
|
|
467
1040
|
if (runCancelMatch && req.method === "POST") {
|
|
468
1041
|
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
@@ -475,7 +1048,6 @@ export function startActionDockServer(
|
|
|
475
1048
|
|
|
476
1049
|
const reason = body?.reason || "Cancelled by client request";
|
|
477
1050
|
|
|
478
|
-
// 1. Try cancelling in-memory active handle
|
|
479
1051
|
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
480
1052
|
if (activeHandle) {
|
|
481
1053
|
const cancelled = runtimeRegistry.executionManager.cancel(runId, reason);
|
|
@@ -492,9 +1064,7 @@ export function startActionDockServer(
|
|
|
492
1064
|
}
|
|
493
1065
|
}
|
|
494
1066
|
|
|
495
|
-
// 2. Check storage for run status
|
|
496
1067
|
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
497
|
-
|
|
498
1068
|
if (!found) {
|
|
499
1069
|
return jsonResponse(
|
|
500
1070
|
{
|
|
@@ -524,7 +1094,6 @@ export function startActionDockServer(
|
|
|
524
1094
|
);
|
|
525
1095
|
}
|
|
526
1096
|
|
|
527
|
-
// Running in storage but no longer in memory
|
|
528
1097
|
storage.updateRun(runId, "cancelled", undefined, {
|
|
529
1098
|
code: "ACTION_CANCELLED",
|
|
530
1099
|
message: reason,
|
|
@@ -541,6 +1110,263 @@ export function startActionDockServer(
|
|
|
541
1110
|
);
|
|
542
1111
|
}
|
|
543
1112
|
|
|
1113
|
+
// 14. State List: GET /api/v1/state
|
|
1114
|
+
if (pathname === "/api/v1/state" && req.method === "GET") {
|
|
1115
|
+
try {
|
|
1116
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1117
|
+
const nsParam = url.searchParams.get("namespace") ?? undefined;
|
|
1118
|
+
const prefix = url.searchParams.get("prefix") || "";
|
|
1119
|
+
|
|
1120
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1121
|
+
pkgParam,
|
|
1122
|
+
runtimeRegistry,
|
|
1123
|
+
projectRoot,
|
|
1124
|
+
customHome
|
|
1125
|
+
);
|
|
1126
|
+
const keys = await storage.listStateKeys(nsParam !== undefined ? nsParam : null, prefix);
|
|
1127
|
+
return jsonResponse({ ok: true, packageId, keys }, 200, corsHeaders);
|
|
1128
|
+
} catch (err: any) {
|
|
1129
|
+
return jsonResponse(
|
|
1130
|
+
{ ok: false, error: { code: "STATE_LIST_ERROR", message: err.message } },
|
|
1131
|
+
500,
|
|
1132
|
+
corsHeaders
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// 15. State Clear: POST /api/v1/state/clear
|
|
1138
|
+
if (pathname === "/api/v1/state/clear" && req.method === "POST") {
|
|
1139
|
+
try {
|
|
1140
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
|
|
1141
|
+
const pkgParam = url.searchParams.get("package") || body.package || undefined;
|
|
1142
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1143
|
+
pkgParam,
|
|
1144
|
+
runtimeRegistry,
|
|
1145
|
+
projectRoot,
|
|
1146
|
+
customHome
|
|
1147
|
+
);
|
|
1148
|
+
const clearedCount = await storage.clearState({
|
|
1149
|
+
namespace: body.namespace ?? (url.searchParams.get("namespace") || undefined),
|
|
1150
|
+
all: Boolean(body.all ?? url.searchParams.get("all") === "true"),
|
|
1151
|
+
prefix: body.prefix ?? (url.searchParams.get("prefix") || undefined),
|
|
1152
|
+
});
|
|
1153
|
+
return jsonResponse({ ok: true, packageId, clearedCount }, 200, corsHeaders);
|
|
1154
|
+
} catch (err: any) {
|
|
1155
|
+
return jsonResponse(
|
|
1156
|
+
{ ok: false, error: { code: "STATE_CLEAR_ERROR", message: err.message } },
|
|
1157
|
+
500,
|
|
1158
|
+
corsHeaders
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// 16. State Key CRUD: GET / PUT / POST / DELETE /api/v1/state/:key
|
|
1164
|
+
const stateKeyMatch = pathname.match(/^\/api\/v1\/state\/([^/]+)$/);
|
|
1165
|
+
if (stateKeyMatch) {
|
|
1166
|
+
const key = decodeURIComponent(stateKeyMatch[1]);
|
|
1167
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1168
|
+
const nsParam = url.searchParams.get("namespace") || undefined;
|
|
1169
|
+
|
|
1170
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1171
|
+
pkgParam,
|
|
1172
|
+
runtimeRegistry,
|
|
1173
|
+
projectRoot,
|
|
1174
|
+
customHome
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
if (req.method === "GET") {
|
|
1178
|
+
const entry = await storage.findState(key, nsParam);
|
|
1179
|
+
if (!entry || entry.value === undefined) {
|
|
1180
|
+
return jsonResponse(
|
|
1181
|
+
{ ok: false, error: { code: "STATE_KEY_NOT_FOUND", message: `State key '${key}' not found` } },
|
|
1182
|
+
404,
|
|
1183
|
+
corsHeaders
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
return jsonResponse(
|
|
1187
|
+
{
|
|
1188
|
+
ok: true,
|
|
1189
|
+
packageId,
|
|
1190
|
+
key: entry.key,
|
|
1191
|
+
namespace: entry.namespace,
|
|
1192
|
+
value: entry.value,
|
|
1193
|
+
expiresAt: entry.expiresAt,
|
|
1194
|
+
},
|
|
1195
|
+
200,
|
|
1196
|
+
corsHeaders
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
1201
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
1202
|
+
const val = body.value !== undefined ? body.value : body;
|
|
1203
|
+
const ttl = typeof body.ttl === "number" ? body.ttl : undefined;
|
|
1204
|
+
const namespace = body.namespace || nsParam || "";
|
|
1205
|
+
|
|
1206
|
+
let actualKey = key;
|
|
1207
|
+
let ns = namespace;
|
|
1208
|
+
if (!ns && key.includes(":")) {
|
|
1209
|
+
const idx = key.indexOf(":");
|
|
1210
|
+
ns = key.slice(0, idx);
|
|
1211
|
+
actualKey = key.slice(idx + 1);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
await storage.setState(ns, actualKey, val, ttl);
|
|
1215
|
+
return jsonResponse(
|
|
1216
|
+
{ ok: true, packageId, key: actualKey, namespace: ns, message: "updated" },
|
|
1217
|
+
200,
|
|
1218
|
+
corsHeaders
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (req.method === "DELETE") {
|
|
1223
|
+
const deleted = await storage.deleteStateSmart(key, nsParam);
|
|
1224
|
+
if (!deleted) {
|
|
1225
|
+
return jsonResponse(
|
|
1226
|
+
{ ok: false, error: { code: "STATE_KEY_NOT_FOUND", message: `State key '${key}' not found` } },
|
|
1227
|
+
404,
|
|
1228
|
+
corsHeaders
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
return jsonResponse({ ok: true, packageId, key, deleted: true }, 200, corsHeaders);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// 17. Config Env Check: GET /api/v1/config/env
|
|
1236
|
+
if (pathname === "/api/v1/config/env" && req.method === "GET") {
|
|
1237
|
+
try {
|
|
1238
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1239
|
+
const { packageId, projectRoot: root } = resolveStorageForPackage(
|
|
1240
|
+
pkgParam,
|
|
1241
|
+
runtimeRegistry,
|
|
1242
|
+
projectRoot,
|
|
1243
|
+
customHome
|
|
1244
|
+
);
|
|
1245
|
+
if (!root) {
|
|
1246
|
+
return jsonResponse({ ok: true, packageId, envChecks: [] }, 200, corsHeaders);
|
|
1247
|
+
}
|
|
1248
|
+
const cfg = loadProjectConfig(root);
|
|
1249
|
+
const declared = cfg.config || {};
|
|
1250
|
+
const envChecks: any[] = [];
|
|
1251
|
+
for (const [k, def] of Object.entries(declared)) {
|
|
1252
|
+
const envKeys = [
|
|
1253
|
+
k,
|
|
1254
|
+
`ACTIONDOCK_${k}`,
|
|
1255
|
+
`${packageId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${k}`,
|
|
1256
|
+
];
|
|
1257
|
+
const foundEnv = envKeys.find((ek) => process.env[ek] !== undefined);
|
|
1258
|
+
envChecks.push({
|
|
1259
|
+
key: k,
|
|
1260
|
+
required: def.default === undefined,
|
|
1261
|
+
satisfied: Boolean(foundEnv || def.default !== undefined),
|
|
1262
|
+
matchedEnv: foundEnv || null,
|
|
1263
|
+
hasDefault: def.default !== undefined,
|
|
1264
|
+
secret: Boolean(def.secret),
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
return jsonResponse({ ok: true, packageId, envChecks }, 200, corsHeaders);
|
|
1268
|
+
} catch (err: any) {
|
|
1269
|
+
return jsonResponse(
|
|
1270
|
+
{ ok: false, error: { code: "CONFIG_ENV_ERROR", message: err.message } },
|
|
1271
|
+
500,
|
|
1272
|
+
corsHeaders
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// 18. Config Query: GET /api/v1/config
|
|
1278
|
+
if (pathname === "/api/v1/config" && req.method === "GET") {
|
|
1279
|
+
try {
|
|
1280
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1281
|
+
const { packageId, storage, projectRoot: root } = resolveStorageForPackage(
|
|
1282
|
+
pkgParam,
|
|
1283
|
+
runtimeRegistry,
|
|
1284
|
+
projectRoot,
|
|
1285
|
+
customHome
|
|
1286
|
+
);
|
|
1287
|
+
const stored = storage.listConfig();
|
|
1288
|
+
let declared: Record<string, any> = {};
|
|
1289
|
+
if (root) {
|
|
1290
|
+
try {
|
|
1291
|
+
const cfg = loadProjectConfig(root);
|
|
1292
|
+
declared = cfg.config || {};
|
|
1293
|
+
} catch {}
|
|
1294
|
+
}
|
|
1295
|
+
const maskedValues: Record<string, any> = {};
|
|
1296
|
+
for (const [k, v] of Object.entries(stored)) {
|
|
1297
|
+
if (declared[k]?.secret) {
|
|
1298
|
+
maskedValues[k] = "******";
|
|
1299
|
+
} else {
|
|
1300
|
+
maskedValues[k] = v;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return jsonResponse(
|
|
1304
|
+
{ ok: true, packageId, declared, values: maskedValues },
|
|
1305
|
+
200,
|
|
1306
|
+
corsHeaders
|
|
1307
|
+
);
|
|
1308
|
+
} catch (err: any) {
|
|
1309
|
+
return jsonResponse(
|
|
1310
|
+
{ ok: false, error: { code: "CONFIG_LIST_ERROR", message: err.message } },
|
|
1311
|
+
500,
|
|
1312
|
+
corsHeaders
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// 19. Config Update: PUT / POST /api/v1/config
|
|
1318
|
+
if (pathname === "/api/v1/config" && (req.method === "PUT" || req.method === "POST")) {
|
|
1319
|
+
try {
|
|
1320
|
+
const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
1321
|
+
const pkgParam = url.searchParams.get("package") || body.package || undefined;
|
|
1322
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1323
|
+
pkgParam,
|
|
1324
|
+
runtimeRegistry,
|
|
1325
|
+
projectRoot,
|
|
1326
|
+
customHome
|
|
1327
|
+
);
|
|
1328
|
+
const key = body.key;
|
|
1329
|
+
if (!key) {
|
|
1330
|
+
return jsonResponse(
|
|
1331
|
+
{ ok: false, error: { code: "INVALID_ARGUMENT", message: "Config 'key' is required" } },
|
|
1332
|
+
400,
|
|
1333
|
+
corsHeaders
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
storage.setConfig(key, body.value);
|
|
1337
|
+
return jsonResponse({ ok: true, packageId, key, message: "updated" }, 200, corsHeaders);
|
|
1338
|
+
} catch (err: any) {
|
|
1339
|
+
return jsonResponse(
|
|
1340
|
+
{ ok: false, error: { code: "CONFIG_SET_ERROR", message: err.message } },
|
|
1341
|
+
500,
|
|
1342
|
+
corsHeaders
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// 20. Config Delete: DELETE /api/v1/config/:key
|
|
1348
|
+
const configKeyMatch = pathname.match(/^\/api\/v1\/config\/([^/]+)$/);
|
|
1349
|
+
if (configKeyMatch && req.method === "DELETE") {
|
|
1350
|
+
try {
|
|
1351
|
+
const key = decodeURIComponent(configKeyMatch[1]);
|
|
1352
|
+
const pkgParam = url.searchParams.get("package") || undefined;
|
|
1353
|
+
const { packageId, storage } = resolveStorageForPackage(
|
|
1354
|
+
pkgParam,
|
|
1355
|
+
runtimeRegistry,
|
|
1356
|
+
projectRoot,
|
|
1357
|
+
customHome
|
|
1358
|
+
);
|
|
1359
|
+
const deleted = storage.deleteConfig(key);
|
|
1360
|
+
return jsonResponse({ ok: true, packageId, key, deleted }, 200, corsHeaders);
|
|
1361
|
+
} catch (err: any) {
|
|
1362
|
+
return jsonResponse(
|
|
1363
|
+
{ ok: false, error: { code: "CONFIG_DELETE_ERROR", message: err.message } },
|
|
1364
|
+
500,
|
|
1365
|
+
corsHeaders
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
544
1370
|
// 404 Not Found
|
|
545
1371
|
return jsonResponse(
|
|
546
1372
|
{
|