@actiondock/core 2.0.9 → 2.0.11-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/build/index.ts +0 -1
- package/src/build/templates.ts +1 -1
- package/src/catalog/types.ts +1 -9
- package/src/doctor/doctor.ts +99 -38
- package/src/execution/service.ts +47 -7
- package/src/export/index.ts +0 -1
- package/src/export/templates.ts +348 -59
- package/src/index.ts +1 -0
- package/src/project/init.ts +3 -2
- package/src/project/loader.ts +46 -7
- package/src/project/manifest.ts +205 -2
- package/src/project/types.ts +49 -0
- package/src/registry/registry.ts +271 -34
- package/src/runtime/context.ts +8 -3
- package/src/runtime/index.ts +1 -0
- package/src/runtime/module-loader.ts +67 -0
- package/src/runtime/process.ts +12 -11
- package/src/runtime/runner.ts +193 -68
- package/src/server/index.ts +1 -0
- package/src/server/routes/actions.ts +262 -0
- package/src/server/routes/common.ts +126 -0
- package/src/server/routes/config.ts +147 -0
- package/src/server/routes/doctor.ts +29 -0
- package/src/server/routes/health.ts +40 -0
- package/src/server/routes/index.ts +9 -0
- package/src/server/routes/info.ts +221 -0
- package/src/server/routes/playbooks.ts +109 -0
- package/src/server/routes/runs.ts +301 -0
- package/src/server/routes/state.ts +133 -0
- package/src/server/server.ts +82 -1323
- package/src/storage/index.ts +20 -4
- package/src/storage/sqlite.ts +60 -31
- package/src/storage/types.ts +8 -0
- package/src/utils/index.ts +2 -0
- package/src/build/builder.ts +0 -218
- package/src/export/skill.ts +0 -350
package/src/server/server.ts
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { runDoctorChecks } from "../doctor/doctor";
|
|
5
|
-
import { filterByIntent, filterWithFallbackInfo } from "../filter";
|
|
6
|
-
import {
|
|
7
|
-
findProjectRoot,
|
|
8
|
-
loadActions,
|
|
9
|
-
loadPlaybooks,
|
|
10
|
-
loadProjectConfig,
|
|
11
|
-
} from "../project/loader";
|
|
12
|
-
import {
|
|
13
|
-
getRegistryStatus,
|
|
14
|
-
listLinkedPackages,
|
|
15
|
-
resolveActionProject,
|
|
16
|
-
resolvePackageRoot,
|
|
17
|
-
resolvePlaybookProject,
|
|
18
|
-
} from "../registry/registry";
|
|
19
|
-
import { ActionRunner } from "../runtime/runner";
|
|
20
|
-
import type { RuntimeStorage } from "../storage/types";
|
|
21
1
|
import { createServer as createNodeHttpServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import { resolve } from "node:path";
|
|
22
3
|
import { Readable } from "node:stream";
|
|
23
4
|
import { pipeline } from "node:stream/promises";
|
|
24
|
-
import {
|
|
5
|
+
import { findProjectRoot } from "../project/loader";
|
|
6
|
+
import {
|
|
7
|
+
handleActionsRoutes,
|
|
8
|
+
handleConfigRoutes,
|
|
9
|
+
handleDoctorRoute,
|
|
10
|
+
handleHealthRoute,
|
|
11
|
+
handleInfoRoute,
|
|
12
|
+
handlePlaybooksRoutes,
|
|
13
|
+
handleRunsRoutes,
|
|
14
|
+
handleStateRoutes,
|
|
15
|
+
jsonResponse,
|
|
16
|
+
type RouteContext,
|
|
17
|
+
} from "./routes";
|
|
25
18
|
import { ServerRuntimeRegistry } from "./runtime-registry";
|
|
26
19
|
import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security";
|
|
27
20
|
import type { ActionDockServerInstance, CoreHttpServerFactory, CoreHttpServerInstance, ServerOptions } from "./types";
|
|
@@ -121,137 +114,9 @@ export function launchHttpServer(
|
|
|
121
114
|
}
|
|
122
115
|
|
|
123
116
|
/**
|
|
124
|
-
*
|
|
125
|
-
*/
|
|
126
|
-
function jsonResponse(
|
|
127
|
-
data: unknown,
|
|
128
|
-
status = 200,
|
|
129
|
-
corsHeaders: Record<string, string> = {}
|
|
130
|
-
): Response {
|
|
131
|
-
return new Response(JSON.stringify(data, null, 2), {
|
|
132
|
-
status,
|
|
133
|
-
headers: {
|
|
134
|
-
"Content-Type": "application/json",
|
|
135
|
-
...corsHeaders,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* 跨内存活跃连接与所有已知持久化存储全局检索指定 runId 的运行记录。
|
|
142
|
-
*/
|
|
143
|
-
function findRunAcrossStorages(
|
|
144
|
-
runId: string,
|
|
145
|
-
runtimeRegistry: ServerRuntimeRegistry,
|
|
146
|
-
projectRoot?: string | null,
|
|
147
|
-
customHome?: string
|
|
148
|
-
) {
|
|
149
|
-
const inMemory = runtimeRegistry.findRun(runId);
|
|
150
|
-
if (inMemory) return inMemory;
|
|
151
|
-
|
|
152
|
-
if (projectRoot) {
|
|
153
|
-
try {
|
|
154
|
-
const config = loadProjectConfig(projectRoot);
|
|
155
|
-
const storage = runtimeRegistry.getStorage(config.id, projectRoot);
|
|
156
|
-
const run = storage.getRun(runId);
|
|
157
|
-
if (run) return { storage, run };
|
|
158
|
-
} catch {
|
|
159
|
-
// 忽略读取错误
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const linked = listLinkedPackages(customHome);
|
|
165
|
-
for (const pkg of linked) {
|
|
166
|
-
if (!existsSync(pkg.path)) continue;
|
|
167
|
-
const storage = runtimeRegistry.getStorage(pkg.id, pkg.path);
|
|
168
|
-
const run = storage.getRun(runId);
|
|
169
|
-
if (run) return { storage, run };
|
|
170
|
-
}
|
|
171
|
-
} catch {
|
|
172
|
-
// 忽略读取错误
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
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
|
-
|
|
228
|
-
/**
|
|
229
|
-
* 启动 ActionDock 2.0 原生轻量级 HTTP Runner 服务端。
|
|
117
|
+
* 启动 ActionDock 2.0 原生轻量级 HTTP 服务装配中枢。
|
|
230
118
|
*
|
|
231
|
-
*
|
|
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 协议端点(可选)
|
|
119
|
+
* 仅负责中间件流转、认证拦截、路由分发与服务生命周期管理。
|
|
255
120
|
*/
|
|
256
121
|
export function startActionDockServer(
|
|
257
122
|
options: ServerOptions = {}
|
|
@@ -275,69 +140,41 @@ export function startActionDockServer(
|
|
|
275
140
|
|
|
276
141
|
const server = launchHttpServer(port, host, async (req) => {
|
|
277
142
|
const origin = req.headers.get("origin");
|
|
278
|
-
|
|
143
|
+
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
279
144
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
145
|
+
if (req.method === "OPTIONS") {
|
|
146
|
+
return new Response(null, {
|
|
147
|
+
status: 204,
|
|
148
|
+
headers: corsHeaders,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
286
151
|
|
|
287
|
-
|
|
288
|
-
|
|
152
|
+
const url = new URL(req.url);
|
|
153
|
+
const pathname = url.pathname;
|
|
289
154
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
},
|
|
301
|
-
401,
|
|
302
|
-
corsHeaders
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
const healthData: Record<string, unknown> = {
|
|
306
|
-
status: "ok",
|
|
307
|
-
version: "2.0.0",
|
|
308
|
-
timestamp: new Date().toISOString(),
|
|
309
|
-
uptime: process.uptime(),
|
|
310
|
-
};
|
|
311
|
-
if (options.exposeDebugInfo && projectRoot) {
|
|
312
|
-
healthData.projectRoot = projectRoot;
|
|
313
|
-
}
|
|
314
|
-
return jsonResponse(healthData, 200, corsHeaders);
|
|
315
|
-
}
|
|
155
|
+
const ctx: RouteContext = {
|
|
156
|
+
req,
|
|
157
|
+
url,
|
|
158
|
+
pathname,
|
|
159
|
+
corsHeaders,
|
|
160
|
+
projectRoot,
|
|
161
|
+
customHome,
|
|
162
|
+
runtimeRegistry,
|
|
163
|
+
options,
|
|
164
|
+
};
|
|
316
165
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
}
|
|
166
|
+
// 1. 健康检查路由(内部处理独立鉴权逻辑)
|
|
167
|
+
const healthResponse = await handleHealthRoute(ctx);
|
|
168
|
+
if (healthResponse) {
|
|
169
|
+
return healthResponse;
|
|
170
|
+
}
|
|
339
171
|
|
|
340
|
-
|
|
172
|
+
// 2. MCP 统一网关端点
|
|
173
|
+
if (
|
|
174
|
+
options.enableMcp !== false &&
|
|
175
|
+
options.mcpHandler &&
|
|
176
|
+
(pathname === "/mcp" || pathname.startsWith("/mcp/"))
|
|
177
|
+
) {
|
|
341
178
|
if (!verifyBearerToken(req, token)) {
|
|
342
179
|
return jsonResponse(
|
|
343
180
|
{
|
|
@@ -351,1130 +188,52 @@ export function startActionDockServer(
|
|
|
351
188
|
corsHeaders
|
|
352
189
|
);
|
|
353
190
|
}
|
|
191
|
+
const mcpRes = await options.mcpHandler(req);
|
|
192
|
+
if (mcpRes) return mcpRes;
|
|
193
|
+
}
|
|
354
194
|
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
374
|
-
if (projectRoot) {
|
|
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
|
-
// 忽略解析异常
|
|
402
|
-
}
|
|
403
|
-
} else {
|
|
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
|
-
|
|
501
|
-
return jsonResponse(
|
|
502
|
-
{
|
|
503
|
-
ok: true,
|
|
504
|
-
type: "package_list",
|
|
505
|
-
isFallback: filterRes.isFallback,
|
|
506
|
-
packages: filterRes.items,
|
|
507
|
-
},
|
|
508
|
-
200,
|
|
509
|
-
corsHeaders
|
|
510
|
-
);
|
|
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
|
-
);
|
|
551
|
-
} catch (err: any) {
|
|
552
|
-
return jsonResponse(
|
|
553
|
-
{
|
|
554
|
-
ok: false,
|
|
555
|
-
error: { code: "INFO_ERROR", message: err.message },
|
|
556
|
-
},
|
|
557
|
-
500,
|
|
558
|
-
corsHeaders
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
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
|
|
583
|
-
if (pathname === "/api/v1/actions" && req.method === "GET") {
|
|
584
|
-
try {
|
|
585
|
-
const actionList: Array<{
|
|
586
|
-
id: string;
|
|
587
|
-
description: string;
|
|
588
|
-
packageId?: string;
|
|
589
|
-
}> = [];
|
|
590
|
-
|
|
591
|
-
if (projectRoot) {
|
|
592
|
-
const config = loadProjectConfig(projectRoot);
|
|
593
|
-
const actions = await loadActions(projectRoot, config.actionsDir);
|
|
594
|
-
for (const [id, a] of actions.entries()) {
|
|
595
|
-
actionList.push({
|
|
596
|
-
id,
|
|
597
|
-
description: a.description || "",
|
|
598
|
-
packageId: config.id,
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const linked = listLinkedPackages(customHome);
|
|
604
|
-
for (const pkg of linked) {
|
|
605
|
-
if (projectRoot && pkg.path === projectRoot) continue;
|
|
606
|
-
if (!existsSync(pkg.path)) continue;
|
|
607
|
-
try {
|
|
608
|
-
const config = loadProjectConfig(pkg.path);
|
|
609
|
-
const actions = await loadActions(pkg.path, config.actionsDir);
|
|
610
|
-
for (const [id, a] of actions.entries()) {
|
|
611
|
-
actionList.push({
|
|
612
|
-
id,
|
|
613
|
-
description: a.description || "",
|
|
614
|
-
packageId: pkg.id,
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
} catch {
|
|
618
|
-
// Ignore broken package
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const intent = url.searchParams.get("intent");
|
|
623
|
-
const targetPkg = url.searchParams.get("package");
|
|
624
|
-
|
|
625
|
-
let filtered = targetPkg
|
|
626
|
-
? actionList.filter((a) => a.packageId === targetPkg)
|
|
627
|
-
: actionList;
|
|
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
|
-
|
|
638
|
-
return jsonResponse(filtered, 200, corsHeaders);
|
|
639
|
-
} catch (err: any) {
|
|
640
|
-
return jsonResponse(
|
|
641
|
-
{
|
|
642
|
-
ok: false,
|
|
643
|
-
error: { code: "ACTIONS_LIST_ERROR", message: err.message },
|
|
644
|
-
},
|
|
645
|
-
500,
|
|
646
|
-
corsHeaders
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// 5. Action Show: GET /api/v1/actions/:id
|
|
652
|
-
const actionShowMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)$/);
|
|
653
|
-
if (actionShowMatch && req.method === "GET") {
|
|
654
|
-
const actionId = decodeURIComponent(actionShowMatch[1]);
|
|
655
|
-
try {
|
|
656
|
-
const resolved = await resolveActionProject(
|
|
657
|
-
actionId,
|
|
658
|
-
projectRoot || process.cwd(),
|
|
659
|
-
customHome
|
|
660
|
-
);
|
|
661
|
-
const config = loadProjectConfig(resolved.projectRoot);
|
|
662
|
-
const actions = await loadActions(resolved.projectRoot, config.actionsDir);
|
|
663
|
-
const action = actions.get(resolved.actionId);
|
|
664
|
-
if (!action) {
|
|
665
|
-
return jsonResponse(
|
|
666
|
-
{
|
|
667
|
-
ok: false,
|
|
668
|
-
error: {
|
|
669
|
-
code: "ACTION_NOT_FOUND",
|
|
670
|
-
message: `Action '${resolved.actionId}' not found in package '${resolved.packageId}'`,
|
|
671
|
-
},
|
|
672
|
-
},
|
|
673
|
-
404,
|
|
674
|
-
corsHeaders
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
return jsonResponse(
|
|
679
|
-
{
|
|
680
|
-
id: action.id,
|
|
681
|
-
packageId: resolved.packageId,
|
|
682
|
-
description: action.description || "",
|
|
683
|
-
inputSchema: action.inputSchema || null,
|
|
684
|
-
outputSchema: action.outputSchema || null,
|
|
685
|
-
},
|
|
686
|
-
200,
|
|
687
|
-
corsHeaders
|
|
688
|
-
);
|
|
689
|
-
} catch (err: any) {
|
|
690
|
-
return jsonResponse(
|
|
691
|
-
{
|
|
692
|
-
ok: false,
|
|
693
|
-
error: { code: "ACTION_NOT_FOUND", message: err.message },
|
|
694
|
-
},
|
|
695
|
-
404,
|
|
696
|
-
corsHeaders
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// 6. Action Run: POST /api/v1/actions/:id/run
|
|
702
|
-
const actionRunMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)\/run$/);
|
|
703
|
-
if (actionRunMatch && req.method === "POST") {
|
|
704
|
-
const actionId = decodeURIComponent(actionRunMatch[1]);
|
|
705
|
-
let body: any = {};
|
|
706
|
-
try {
|
|
707
|
-
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
708
|
-
} catch (err: any) {
|
|
709
|
-
if (err instanceof RequestTooLargeError) {
|
|
710
|
-
return jsonResponse(
|
|
711
|
-
{
|
|
712
|
-
ok: false,
|
|
713
|
-
runId: randomUUID(),
|
|
714
|
-
error: { code: "REQUEST_TOO_LARGE", message: err.message },
|
|
715
|
-
},
|
|
716
|
-
413,
|
|
717
|
-
corsHeaders
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
if (err instanceof InvalidJsonError) {
|
|
721
|
-
return jsonResponse(
|
|
722
|
-
{
|
|
723
|
-
ok: false,
|
|
724
|
-
runId: randomUUID(),
|
|
725
|
-
error: { code: "INVALID_JSON", message: err.message },
|
|
726
|
-
},
|
|
727
|
-
400,
|
|
728
|
-
corsHeaders
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
return jsonResponse(
|
|
732
|
-
{
|
|
733
|
-
ok: false,
|
|
734
|
-
runId: randomUUID(),
|
|
735
|
-
error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
|
|
736
|
-
},
|
|
737
|
-
400,
|
|
738
|
-
corsHeaders
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
const resolved = await resolveActionProject(
|
|
744
|
-
actionId,
|
|
745
|
-
projectRoot || process.cwd(),
|
|
746
|
-
customHome
|
|
747
|
-
);
|
|
748
|
-
const config = loadProjectConfig(resolved.projectRoot);
|
|
749
|
-
const actions = await loadActions(resolved.projectRoot, config.actionsDir);
|
|
750
|
-
const storage = runtimeRegistry.getStorage(config.id, resolved.projectRoot);
|
|
751
|
-
|
|
752
|
-
const runner = new ActionRunner({
|
|
753
|
-
packageId: config.id,
|
|
754
|
-
storage,
|
|
755
|
-
projectConfig: config,
|
|
756
|
-
configOverrides: body?.config || {},
|
|
757
|
-
actions,
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
const isAsync = body?.execution?.mode === "async" || body?.async === true;
|
|
761
|
-
const timeoutMs =
|
|
762
|
-
typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
|
|
763
|
-
? body.execution.timeoutMs
|
|
764
|
-
: undefined;
|
|
765
|
-
|
|
766
|
-
if (isAsync) {
|
|
767
|
-
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
768
|
-
timeoutMs,
|
|
769
|
-
});
|
|
770
|
-
runtimeRegistry.executionManager.register(handle);
|
|
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
|
-
|
|
790
|
-
return jsonResponse(
|
|
791
|
-
{
|
|
792
|
-
ok: true,
|
|
793
|
-
runId: handle.runId,
|
|
794
|
-
status: "running",
|
|
795
|
-
streamUrl: `/api/v1/runs/${handle.runId}/stream`,
|
|
796
|
-
},
|
|
797
|
-
202,
|
|
798
|
-
corsHeaders
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// 同步执行模式
|
|
803
|
-
const handle = runner.start(resolved.actionId, body?.input || {}, {
|
|
804
|
-
signal: req.signal,
|
|
805
|
-
timeoutMs,
|
|
806
|
-
});
|
|
807
|
-
runtimeRegistry.executionManager.register(handle);
|
|
808
|
-
const result = await handle.result;
|
|
809
|
-
|
|
810
|
-
return jsonResponse(result, 200, corsHeaders);
|
|
811
|
-
} catch (err: any) {
|
|
812
|
-
return jsonResponse(
|
|
813
|
-
{
|
|
814
|
-
ok: false,
|
|
815
|
-
runId: randomUUID(),
|
|
816
|
-
error: {
|
|
817
|
-
code: "ACTION_EXECUTION_ERROR",
|
|
818
|
-
message: err.message || String(err),
|
|
819
|
-
},
|
|
820
|
-
},
|
|
821
|
-
500,
|
|
822
|
-
corsHeaders
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
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
|
|
1110
|
-
const runShowMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
|
|
1111
|
-
if (runShowMatch && req.method === "GET") {
|
|
1112
|
-
const runId = decodeURIComponent(runShowMatch[1]);
|
|
1113
|
-
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
1114
|
-
|
|
1115
|
-
if (!found) {
|
|
1116
|
-
return jsonResponse(
|
|
1117
|
-
{
|
|
1118
|
-
ok: false,
|
|
1119
|
-
error: {
|
|
1120
|
-
code: "RUN_NOT_FOUND",
|
|
1121
|
-
message: `Run '${runId}' not found`,
|
|
1122
|
-
},
|
|
1123
|
-
},
|
|
1124
|
-
404,
|
|
1125
|
-
corsHeaders
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
return jsonResponse(found.run, 200, corsHeaders);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// 13. Run Cancel: POST /api/v1/runs/:runId/cancel
|
|
1133
|
-
const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
|
|
1134
|
-
if (runCancelMatch && req.method === "POST") {
|
|
1135
|
-
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
1136
|
-
let body: any = {};
|
|
1137
|
-
try {
|
|
1138
|
-
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
|
|
1139
|
-
} catch {
|
|
1140
|
-
// Body is optional
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
const reason = body?.reason || "Cancelled by client request";
|
|
1144
|
-
|
|
1145
|
-
const activeHandle = runtimeRegistry.executionManager.get(runId);
|
|
1146
|
-
if (activeHandle) {
|
|
1147
|
-
const cancelled = runtimeRegistry.executionManager.cancel(runId, reason);
|
|
1148
|
-
if (cancelled) {
|
|
1149
|
-
return jsonResponse(
|
|
1150
|
-
{
|
|
1151
|
-
ok: true,
|
|
1152
|
-
runId,
|
|
1153
|
-
status: "cancelled",
|
|
1154
|
-
},
|
|
1155
|
-
200,
|
|
1156
|
-
corsHeaders
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
|
|
1162
|
-
if (!found) {
|
|
1163
|
-
return jsonResponse(
|
|
1164
|
-
{
|
|
1165
|
-
ok: false,
|
|
1166
|
-
error: {
|
|
1167
|
-
code: "RUN_NOT_FOUND",
|
|
1168
|
-
message: `Run '${runId}' not found`,
|
|
1169
|
-
},
|
|
1170
|
-
},
|
|
1171
|
-
404,
|
|
1172
|
-
corsHeaders
|
|
1173
|
-
);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const { storage, run } = found;
|
|
1177
|
-
if (run.status === "success" || run.status === "failed" || run.status === "cancelled") {
|
|
1178
|
-
return jsonResponse(
|
|
1179
|
-
{
|
|
1180
|
-
ok: false,
|
|
1181
|
-
error: {
|
|
1182
|
-
code: "RUN_ALREADY_FINISHED",
|
|
1183
|
-
message: `Run '${runId}' has already finished with status '${run.status}'`,
|
|
1184
|
-
},
|
|
1185
|
-
},
|
|
1186
|
-
409,
|
|
1187
|
-
corsHeaders
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
storage.updateRun(runId, "cancelled", undefined, {
|
|
1192
|
-
code: "ACTION_CANCELLED",
|
|
1193
|
-
message: reason,
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
return jsonResponse(
|
|
1197
|
-
{
|
|
1198
|
-
ok: true,
|
|
1199
|
-
runId,
|
|
1200
|
-
status: "cancelled",
|
|
1201
|
-
},
|
|
1202
|
-
200,
|
|
1203
|
-
corsHeaders
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
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
|
-
|
|
1464
|
-
// 404 Not Found
|
|
195
|
+
// 3. 全局 API 认证鉴权拦截
|
|
196
|
+
if (!verifyBearerToken(req, token)) {
|
|
1465
197
|
return jsonResponse(
|
|
1466
198
|
{
|
|
1467
199
|
ok: false,
|
|
1468
200
|
error: {
|
|
1469
|
-
code: "
|
|
1470
|
-
message:
|
|
201
|
+
code: "UNAUTHORIZED",
|
|
202
|
+
message: "Invalid or missing Bearer token",
|
|
1471
203
|
},
|
|
1472
204
|
},
|
|
1473
|
-
|
|
205
|
+
401,
|
|
1474
206
|
corsHeaders
|
|
1475
207
|
);
|
|
1476
208
|
}
|
|
1477
|
-
|
|
209
|
+
|
|
210
|
+
// 4. 业务领域路由分发
|
|
211
|
+
const routeResponse =
|
|
212
|
+
(await handleInfoRoute(ctx)) ||
|
|
213
|
+
(await handleDoctorRoute(ctx)) ||
|
|
214
|
+
(await handleActionsRoutes(ctx)) ||
|
|
215
|
+
(await handlePlaybooksRoutes(ctx)) ||
|
|
216
|
+
(await handleRunsRoutes(ctx)) ||
|
|
217
|
+
(await handleStateRoutes(ctx)) ||
|
|
218
|
+
(await handleConfigRoutes(ctx));
|
|
219
|
+
|
|
220
|
+
if (routeResponse) {
|
|
221
|
+
return routeResponse;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 5. 404 路由兜底
|
|
225
|
+
return jsonResponse(
|
|
226
|
+
{
|
|
227
|
+
ok: false,
|
|
228
|
+
error: {
|
|
229
|
+
code: "NOT_FOUND",
|
|
230
|
+
message: `Route not found: ${req.method} ${pathname}`,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
404,
|
|
234
|
+
corsHeaders
|
|
235
|
+
);
|
|
236
|
+
});
|
|
1478
237
|
|
|
1479
238
|
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
1480
239
|
const url = `http://${actualHost}:${server.port}`;
|