@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.
@@ -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 { InvalidJsonError, readJsonBody, RequestTooLargeError } from "./body";
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
- * 辅助函数:快速构造带 CORS 头的 JSON HTTP 响应。
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
- * 全面暴露 RESTful 调度与运维接口:
232
- * - GET /api/v1/health : 健康检查与就绪状态
233
- * - GET /api/v1/info : 统一自省探索(支持 ?intent= 智能决议、?package= 详情下钻、?tree=true 工作区拓扑)
234
- * - GET /api/v1/doctor : 深度环境与依赖诊断
235
- * - GET /api/v1/actions : 列出可用 Actions(支持 ?intent= 与 ?package=)
236
- * - GET /api/v1/actions/:id : 查看单个 Action 的 Schema 详情
237
- * - POST /api/v1/actions/:id/run : 同步或异步执行 Action
238
- * - GET /api/v1/playbooks : 列出可用 Playbook 工作流
239
- * - GET /api/v1/playbooks/:id : 查看 Playbook 步骤流程与 SOP 规程正文
240
- * - GET /api/v1/runs : 多维检索历史运行记录
241
- * - POST /api/v1/runs/clear : 批量清理历史运行记录
242
- * - GET /api/v1/runs/:runId : 查询历史或异步任务运行状态
243
- * - POST /api/v1/runs/:runId/cancel : 中断在途任务
244
- * - GET /api/v1/runs/:runId/stream : 基于 SSE 实时长任务日志与进度流推送
245
- * - GET /api/v1/state : 检索状态键名列表
246
- * - GET /api/v1/state/:key : 获取指定状态值
247
- * - PUT /api/v1/state/:key : 写入持久化状态(支持 TTL)
248
- * - DELETE /api/v1/state/:key : 删除指定状态
249
- * - POST /api/v1/state/clear : 清空状态数据
250
- * - GET /api/v1/config : 获取配置项规格与当前值(敏感凭证脱敏)
251
- * - PUT /api/v1/config : 持久化更新配置
252
- * - DELETE /api/v1/config/:key : 删除配置项
253
- * - GET /api/v1/config/env : 检查环境变量满足率
254
- * - ALL /mcp : 一体化 MCP Streamable HTTP 协议端点(可选)
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
- const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
143
+ const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
279
144
 
280
- if (req.method === "OPTIONS") {
281
- return new Response(null, {
282
- status: 204,
283
- headers: corsHeaders,
284
- });
285
- }
145
+ if (req.method === "OPTIONS") {
146
+ return new Response(null, {
147
+ status: 204,
148
+ headers: corsHeaders,
149
+ });
150
+ }
286
151
 
287
- const url = new URL(req.url);
288
- const pathname = url.pathname;
152
+ const url = new URL(req.url);
153
+ const pathname = url.pathname;
289
154
 
290
- // 0. Health Check (supports /api/v1/health and /health)
291
- if (pathname === "/api/v1/health" || pathname === "/health") {
292
- if (!verifyBearerToken(req, token)) {
293
- return jsonResponse(
294
- {
295
- ok: false,
296
- error: {
297
- code: "UNAUTHORIZED",
298
- message: "Invalid or missing Bearer token",
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
- // 1. MCP Unified Gateway: /mcp
318
- if (
319
- options.enableMcp !== false &&
320
- options.mcpHandler &&
321
- (pathname === "/mcp" || pathname.startsWith("/mcp/"))
322
- ) {
323
- if (!verifyBearerToken(req, token)) {
324
- return jsonResponse(
325
- {
326
- ok: false,
327
- error: {
328
- code: "UNAUTHORIZED",
329
- message: "Invalid or missing Bearer token",
330
- },
331
- },
332
- 401,
333
- corsHeaders
334
- );
335
- }
336
- const mcpRes = await options.mcpHandler(req);
337
- if (mcpRes) return mcpRes;
338
- }
166
+ // 1. 健康检查路由(内部处理独立鉴权逻辑)
167
+ const healthResponse = await handleHealthRoute(ctx);
168
+ if (healthResponse) {
169
+ return healthResponse;
170
+ }
339
171
 
340
- // 鉴权检查(后续所有 /api/v1/* 均要求鉴权)
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
- // 2. Info 自省: GET /api/v1/info
356
- if (pathname === "/api/v1/info" && req.method === "GET") {
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: "NOT_FOUND",
1470
- message: `Route not found: ${req.method} ${pathname}`,
201
+ code: "UNAUTHORIZED",
202
+ message: "Invalid or missing Bearer token",
1471
203
  },
1472
204
  },
1473
- 404,
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}`;