@actiondock/core 2.6.0 → 2.8.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.
Files changed (268) hide show
  1. package/README.md +61 -68
  2. package/dist/catalog/action-catalog.d.ts +56 -0
  3. package/dist/catalog/action-catalog.js +202 -0
  4. package/dist/catalog/discovery.d.ts +60 -0
  5. package/dist/catalog/discovery.js +119 -0
  6. package/dist/catalog/graph.d.ts +190 -0
  7. package/dist/catalog/graph.js +589 -0
  8. package/dist/catalog/index.d.ts +5 -5
  9. package/dist/catalog/index.js +5 -5
  10. package/dist/catalog/resolve-action.d.ts +43 -0
  11. package/dist/catalog/resolve-action.js +119 -0
  12. package/dist/catalog/resolve-playbook.d.ts +26 -0
  13. package/dist/catalog/resolve-playbook.js +114 -0
  14. package/dist/doctor/checks/project.js +27 -8
  15. package/dist/doctor/checks/registry.js +2 -2
  16. package/dist/doctor/checks/runtime.js +34 -1
  17. package/dist/errors.d.ts +122 -10
  18. package/dist/errors.js +123 -10
  19. package/dist/execution/service.d.ts +18 -16
  20. package/dist/execution/service.js +183 -252
  21. package/dist/execution/types.d.ts +25 -69
  22. package/dist/execution/types.js +2 -1
  23. package/dist/filter/intent.d.ts +8 -9
  24. package/dist/filter/intent.js +49 -10
  25. package/dist/graph/index.d.ts +4 -0
  26. package/dist/graph/index.js +4 -0
  27. package/dist/host/host.d.ts +74 -32
  28. package/dist/host/host.js +585 -336
  29. package/dist/host/routing.d.ts +15 -48
  30. package/dist/host/routing.js +90 -153
  31. package/dist/host/types.d.ts +50 -17
  32. package/dist/index.d.ts +21 -22
  33. package/dist/index.js +19 -21
  34. package/dist/input/advice.d.ts +104 -0
  35. package/dist/input/advice.js +716 -0
  36. package/dist/input/describe.d.ts +106 -0
  37. package/dist/input/describe.js +176 -0
  38. package/dist/input/file-input.d.ts +54 -0
  39. package/dist/input/file-input.js +124 -0
  40. package/dist/input/flat-decode.d.ts +11 -0
  41. package/dist/input/flat-decode.js +13 -0
  42. package/dist/input/flat-errors.d.ts +71 -0
  43. package/dist/input/flat-errors.js +87 -0
  44. package/dist/input/flat-materializer.d.ts +29 -0
  45. package/dist/input/flat-materializer.js +177 -0
  46. package/dist/input/flat-parser.d.ts +50 -0
  47. package/dist/input/flat-parser.js +169 -0
  48. package/dist/input/flat-predicates.d.ts +34 -0
  49. package/dist/input/flat-predicates.js +50 -0
  50. package/dist/input/index.d.ts +14 -0
  51. package/dist/input/index.js +14 -0
  52. package/dist/input/input-resolver.d.ts +63 -0
  53. package/dist/input/input-resolver.js +254 -0
  54. package/dist/input/json-depth-scanner.d.ts +17 -0
  55. package/dist/input/json-depth-scanner.js +49 -0
  56. package/dist/input/metadata.d.ts +99 -0
  57. package/dist/input/metadata.js +99 -0
  58. package/dist/input/stdin-input.d.ts +26 -0
  59. package/dist/input/stdin-input.js +125 -0
  60. package/dist/input/utf8.d.ts +19 -0
  61. package/dist/input/utf8.js +36 -0
  62. package/dist/input/validation-mapper.d.ts +27 -0
  63. package/dist/input/validation-mapper.js +144 -0
  64. package/dist/invocation/index.d.ts +2 -0
  65. package/dist/invocation/index.js +2 -0
  66. package/dist/invocation/policy.d.ts +157 -0
  67. package/dist/invocation/policy.js +231 -0
  68. package/dist/invocation/types.d.ts +172 -0
  69. package/dist/invocation/types.js +97 -0
  70. package/dist/ipc/diagnostic.d.ts +0 -12
  71. package/dist/ipc/diagnostic.js +0 -21
  72. package/dist/ipc/host.d.ts +4 -4
  73. package/dist/ipc/host.js +44 -47
  74. package/dist/ipc/index.d.ts +1 -1
  75. package/dist/ipc/index.js +1 -1
  76. package/dist/ipc/service.d.ts +40 -0
  77. package/dist/ipc/service.js +349 -0
  78. package/dist/ipc/types.d.ts +21 -15
  79. package/dist/json/index.d.ts +1 -0
  80. package/dist/json/index.js +1 -0
  81. package/dist/json/value-validator.d.ts +88 -0
  82. package/dist/json/value-validator.js +330 -0
  83. package/dist/node/index.d.ts +1 -0
  84. package/dist/node/index.js +1 -0
  85. package/dist/node/module-loader.d.ts +63 -0
  86. package/dist/node/module-loader.js +112 -0
  87. package/dist/package/index.d.ts +23 -0
  88. package/dist/package/index.js +18 -0
  89. package/dist/{app/app.d.ts → package/runtime.d.ts} +41 -20
  90. package/dist/{app/app.js → package/runtime.js} +140 -95
  91. package/dist/{app → package}/static-index.js +58 -41
  92. package/dist/package/storage-view.d.ts +13 -0
  93. package/dist/package/storage-view.js +22 -0
  94. package/dist/{app → package}/types.d.ts +76 -57
  95. package/dist/package/types.js +22 -0
  96. package/dist/platform/index.d.ts +1 -1
  97. package/dist/platform/index.js +1 -1
  98. package/dist/platform/node.d.ts +48 -0
  99. package/dist/platform/node.js +96 -0
  100. package/dist/platform/types.d.ts +0 -9
  101. package/dist/process/control-arbiter.d.ts +2 -1
  102. package/dist/process/control-arbiter.js +1 -2
  103. package/dist/process/driver.d.ts +9 -8
  104. package/dist/process/driver.js +11 -22
  105. package/dist/process/index.d.ts +2 -0
  106. package/dist/process/index.js +2 -0
  107. package/dist/process/managed-record.d.ts +2 -2
  108. package/dist/process/process-driver.d.ts +109 -0
  109. package/dist/process/process-driver.js +650 -0
  110. package/dist/process/process-executor.d.ts +47 -0
  111. package/dist/process/process-executor.js +296 -0
  112. package/dist/process/process-manager.js +48 -26
  113. package/dist/process/run-executor.js +12 -12
  114. package/dist/profile/client-actions.js +2 -2
  115. package/dist/profile/client-health.js +3 -2
  116. package/dist/profile/client-query.js +5 -6
  117. package/dist/profile/client-transport.d.ts +19 -17
  118. package/dist/profile/client-transport.js +48 -80
  119. package/dist/profile/client.d.ts +2 -2
  120. package/dist/profile/client.js +1 -1
  121. package/dist/profile/index.d.ts +1 -0
  122. package/dist/profile/index.js +1 -0
  123. package/dist/profile/manager.d.ts +5 -0
  124. package/dist/profile/manager.js +41 -6
  125. package/dist/project/closure.js +2 -2
  126. package/dist/project/index.d.ts +8 -1
  127. package/dist/project/index.js +10 -1
  128. package/dist/project/init.js +2 -2
  129. package/dist/project/loader.d.ts +1 -1
  130. package/dist/project/loader.js +26 -29
  131. package/dist/project/transactions.js +5 -10
  132. package/dist/registry/index.d.ts +3 -1
  133. package/dist/registry/index.js +2 -1
  134. package/dist/registry/lock.d.ts +28 -7
  135. package/dist/registry/lock.js +136 -16
  136. package/dist/registry/registry.d.ts +1 -5
  137. package/dist/registry/registry.js +1 -53
  138. package/dist/registry/scan.d.ts +0 -15
  139. package/dist/registry/scan.js +0 -59
  140. package/dist/registry/store.d.ts +42 -0
  141. package/dist/registry/store.js +43 -0
  142. package/dist/registry/types.d.ts +0 -24
  143. package/dist/runtime/action-collection.d.ts +1 -1
  144. package/dist/runtime/action-registry.d.ts +0 -4
  145. package/dist/runtime/action-registry.js +0 -7
  146. package/dist/runtime/context.js +5 -11
  147. package/dist/runtime/events.js +3 -6
  148. package/dist/runtime/identity.d.ts +22 -0
  149. package/dist/runtime/identity.js +10 -0
  150. package/dist/runtime/index.d.ts +1 -2
  151. package/dist/runtime/index.js +1 -2
  152. package/dist/runtime/module-loader.d.ts +2 -34
  153. package/dist/runtime/module-loader.js +1 -16
  154. package/dist/runtime/process.d.ts +6 -24
  155. package/dist/runtime/process.js +1 -118
  156. package/dist/runtime/run-persistence.js +2 -5
  157. package/dist/runtime/runner.d.ts +33 -103
  158. package/dist/runtime/runner.js +142 -404
  159. package/dist/runtime/standalone.d.ts +37 -9
  160. package/dist/runtime/standalone.js +195 -136
  161. package/dist/schema/index.d.ts +1 -0
  162. package/dist/schema/index.js +1 -0
  163. package/dist/schema/validator.d.ts +10 -0
  164. package/dist/schema/validator.js +48 -54
  165. package/dist/server/body.d.ts +16 -8
  166. package/dist/server/body.js +33 -16
  167. package/dist/server/dispatcher.d.ts +6 -10
  168. package/dist/server/dispatcher.js +24 -32
  169. package/dist/server/http-server.d.ts +87 -0
  170. package/dist/server/http-server.js +304 -0
  171. package/dist/server/index.d.ts +11 -0
  172. package/dist/server/index.js +10 -0
  173. package/dist/server/mcp-endpoint.d.ts +61 -0
  174. package/dist/server/mcp-endpoint.js +118 -0
  175. package/dist/server/routes/actions.js +34 -28
  176. package/dist/server/routes/common.d.ts +10 -14
  177. package/dist/server/routes/common.js +32 -56
  178. package/dist/server/routes/config.js +54 -64
  179. package/dist/server/routes/doctor.d.ts +1 -1
  180. package/dist/server/routes/doctor.js +10 -18
  181. package/dist/server/routes/health.d.ts +1 -1
  182. package/dist/server/routes/health.js +2 -3
  183. package/dist/server/routes/info.d.ts +2 -2
  184. package/dist/server/routes/info.js +12 -13
  185. package/dist/server/routes/playbooks.js +16 -29
  186. package/dist/server/routes/runs.js +95 -117
  187. package/dist/server/routes/state.js +63 -62
  188. package/dist/server/server.d.ts +2 -2
  189. package/dist/server/server.js +43 -227
  190. package/dist/server/types.d.ts +8 -19
  191. package/dist/service/factory.d.ts +12 -0
  192. package/dist/service/factory.js +85 -0
  193. package/dist/{target → service}/index.d.ts +1 -1
  194. package/dist/{target → service}/index.js +1 -1
  195. package/dist/service/local.d.ts +27 -0
  196. package/dist/service/local.js +138 -0
  197. package/dist/{target → service}/remote-errors.d.ts +3 -6
  198. package/dist/service/remote-errors.js +38 -0
  199. package/dist/{target → service}/remote-polling.d.ts +2 -2
  200. package/dist/{target → service}/remote-polling.js +8 -8
  201. package/dist/service/remote.d.ts +34 -0
  202. package/dist/service/remote.js +634 -0
  203. package/dist/service/sse-stream.d.ts +24 -0
  204. package/dist/service/sse-stream.js +139 -0
  205. package/dist/service/types.d.ts +272 -0
  206. package/dist/service/types.js +32 -0
  207. package/dist/storage/data-dir-lock.js +10 -5
  208. package/dist/storage/driver.d.ts +2 -1
  209. package/dist/storage/driver.js +3 -52
  210. package/dist/storage/index.d.ts +1 -0
  211. package/dist/storage/index.js +1 -0
  212. package/dist/storage/lock-core.d.ts +2 -6
  213. package/dist/storage/lock-core.js +4 -17
  214. package/dist/storage/params.d.ts +2 -8
  215. package/dist/storage/params.js +2 -8
  216. package/dist/storage/sqlite-driver.d.ts +40 -0
  217. package/dist/storage/sqlite-driver.js +116 -0
  218. package/dist/storage/sqlite.js +21 -6
  219. package/dist/storage/types.d.ts +4 -4
  220. package/dist/storage/types.js +2 -2
  221. package/dist/utils/index.d.ts +53 -0
  222. package/dist/utils/index.js +99 -4
  223. package/dist/utils/process.d.ts +10 -0
  224. package/dist/utils/process.js +21 -0
  225. package/dist/version.d.ts +1 -1
  226. package/dist/version.js +1 -1
  227. package/package.json +41 -3
  228. package/dist/app/index.d.ts +0 -2
  229. package/dist/app/index.js +0 -2
  230. package/dist/app/types.js +0 -1
  231. package/dist/catalog/action-index.d.ts +0 -9
  232. package/dist/catalog/action-index.js +0 -95
  233. package/dist/catalog/action-resolver.d.ts +0 -20
  234. package/dist/catalog/action-resolver.js +0 -95
  235. package/dist/catalog/location-registry.d.ts +0 -15
  236. package/dist/catalog/location-registry.js +0 -138
  237. package/dist/catalog/package-catalog.d.ts +0 -7
  238. package/dist/catalog/package-catalog.js +0 -77
  239. package/dist/catalog/types.d.ts +0 -68
  240. package/dist/catalog/types.js +0 -1
  241. package/dist/export/index.d.ts +0 -1
  242. package/dist/export/index.js +0 -1
  243. package/dist/export/templates.d.ts +0 -71
  244. package/dist/export/templates.js +0 -623
  245. package/dist/ipc/target.d.ts +0 -74
  246. package/dist/ipc/target.js +0 -365
  247. package/dist/platform/default.d.ts +0 -38
  248. package/dist/platform/default.js +0 -78
  249. package/dist/project/resolver.d.ts +0 -116
  250. package/dist/project/resolver.js +0 -376
  251. package/dist/registry/resolve.d.ts +0 -62
  252. package/dist/registry/resolve.js +0 -195
  253. package/dist/runtime/execution-manager.d.ts +0 -48
  254. package/dist/runtime/execution-manager.js +0 -66
  255. package/dist/runtime/package-runner-factory.d.ts +0 -82
  256. package/dist/runtime/package-runner-factory.js +0 -186
  257. package/dist/target/local.d.ts +0 -53
  258. package/dist/target/local.js +0 -349
  259. package/dist/target/remote-errors.js +0 -51
  260. package/dist/target/remote.d.ts +0 -86
  261. package/dist/target/remote.js +0 -818
  262. package/dist/target/target.d.ts +0 -6
  263. package/dist/target/target.js +0 -37
  264. package/dist/target/types.d.ts +0 -261
  265. package/dist/target/types.js +0 -35
  266. /package/dist/{app → package}/static-index.d.ts +0 -0
  267. /package/dist/{target → service}/sse-parser.d.ts +0 -0
  268. /package/dist/{target → service}/sse-parser.js +0 -0
@@ -1,5 +1,4 @@
1
- import { filterByIntent } from "../../filter/index.js";
2
- import { EXECUTION_FAILED } from "../../errors.js";
1
+ import { CAPABILITY_UNAVAILABLE, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED, EXECUTION_FAILED, PACKAGE_FORBIDDEN, RUN_ALREADY_FINISHED, RUN_INTERRUPTED, RUN_NOT_FOUND, RUNS_CLEAR_ERROR, RUNS_LIST_ERROR, } from "../../errors.js";
3
2
  import { isTerminalRunStatus } from "../../storage/types.js";
4
3
  import { readJsonBody } from "../body.js";
5
4
  import { getSubPath, jsonResponse } from "./common.js";
@@ -7,7 +6,7 @@ import { getSubPath, jsonResponse } from "./common.js";
7
6
  * 处理历史运行记录查询、清理、详情及 SSE 流式日志接口。
8
7
  */
9
8
  export async function handleRunsRoutes(ctx) {
10
- const { req, url, pathname, corsHeaders, options, target, host } = ctx;
9
+ const { req, url, pathname, corsHeaders, options, service } = ctx;
11
10
  const subpath = getSubPath(pathname);
12
11
  // 1. Runs List: GET /api/v2/runs, GET /runs
13
12
  if (subpath === "/runs" && req.method === "GET") {
@@ -16,7 +15,11 @@ export async function handleRunsRoutes(ctx) {
16
15
  const actionId = url.searchParams.get("actionId") || undefined;
17
16
  const packageId = url.searchParams.get("packageId") || undefined;
18
17
  const intent = url.searchParams.get("intent") || undefined;
19
- const limit = parseInt(url.searchParams.get("limit") || "50", 10);
18
+ // limit 边界防护:非法或超界值回退默认 50,并夹紧到 1 至 500 区间
19
+ const parsedLimit = parseInt(url.searchParams.get("limit") || "50", 10);
20
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0
21
+ ? Math.min(parsedLimit, 500)
22
+ : 50;
20
23
  if (packageId &&
21
24
  options.packageAllowlist &&
22
25
  options.packageAllowlist.length > 0 &&
@@ -24,61 +27,26 @@ export async function handleRunsRoutes(ctx) {
24
27
  return jsonResponse({
25
28
  ok: false,
26
29
  error: {
27
- code: "PACKAGE_FORBIDDEN",
30
+ code: PACKAGE_FORBIDDEN,
28
31
  message: `Package '${packageId}' is not in the allowed package list`,
29
32
  },
30
33
  }, 403, corsHeaders);
31
34
  }
32
- const allRuns = [];
33
- const seenRunIds = new Set();
34
- const apps = host
35
- ? host.listApps()
36
- : (() => {
37
- const inner = target?.unwrap?.();
38
- return inner && "listApps" in inner ? inner.listApps() : [inner].filter(Boolean);
39
- })();
40
- for (const app of apps) {
41
- if (!app)
42
- continue;
43
- if (packageId && app.packageId !== packageId)
44
- continue;
45
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
46
- if (!options.packageAllowlist.includes(app.packageId)) {
47
- continue;
48
- }
49
- }
50
- if (app.storage && typeof app.storage.listRuns === "function") {
51
- try {
52
- // 状态过滤下推到存储层 SQL,避免先分页后过滤导致某包前 N 条非目标状态时贡献 0 条
53
- const records = app.storage.listRuns({ actionId, status, limit });
54
- for (const r of records) {
55
- if (!seenRunIds.has(r.id)) {
56
- seenRunIds.add(r.id);
57
- if (options.packageAllowlist &&
58
- options.packageAllowlist.length > 0 &&
59
- r.packageId &&
60
- !options.packageAllowlist.includes(r.packageId)) {
61
- continue;
62
- }
63
- allRuns.push(r);
64
- }
65
- }
66
- }
67
- catch (err) {
68
- // 单包查询失败不阻断整体列表响应,但必须可观测
69
- console.warn(`[actiondock] listRuns failed for package '${app.packageId}': ${err instanceof Error ? err.message : String(err)}`);
70
- }
71
- }
35
+ let allRuns = await service.runs.list({
36
+ actionId,
37
+ status,
38
+ packageId,
39
+ intent,
40
+ limit,
41
+ });
42
+ if (options.packageAllowlist && options.packageAllowlist.length > 0) {
43
+ allRuns = allRuns.filter((r) => !r.packageId || options.packageAllowlist.includes(r.packageId));
72
44
  }
73
- allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
74
- const filtered = intent
75
- ? filterByIntent(allRuns, intent, [(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.packageId], false)
76
- : allRuns;
77
- const sliced = filtered.slice(0, limit);
78
- return jsonResponse({ ok: true, total: filtered.length, items: sliced }, 200, corsHeaders);
45
+ const sliced = allRuns.slice(0, limit);
46
+ return jsonResponse({ ok: true, total: allRuns.length, items: sliced }, 200, corsHeaders);
79
47
  }
80
48
  catch (err) {
81
- return jsonResponse({ ok: false, error: { code: "RUNS_LIST_ERROR", message: err.message } }, 500, corsHeaders);
49
+ return jsonResponse({ ok: false, error: { code: RUNS_LIST_ERROR, message: err.message } }, 500, corsHeaders);
82
50
  }
83
51
  }
84
52
  // 2. Runs Clear: POST /api/v2/runs/clear, DELETE /api/v2/runs, etc.
@@ -99,45 +67,28 @@ export async function handleRunsRoutes(ctx) {
99
67
  return jsonResponse({
100
68
  ok: false,
101
69
  error: {
102
- code: "PACKAGE_FORBIDDEN",
70
+ code: PACKAGE_FORBIDDEN,
103
71
  message: `Package '${packageId}' is not in the allowed package list`,
104
72
  },
105
73
  }, 403, corsHeaders);
106
74
  }
107
75
  let clearedCount = 0;
108
- const apps = host
109
- ? host.listApps()
110
- : (() => {
111
- const inner = target?.unwrap?.();
112
- return inner && "listApps" in inner ? inner.listApps() : [inner].filter(Boolean);
113
- })();
114
- for (const app of apps) {
115
- if (!app)
116
- continue;
117
- if (packageId && app.packageId !== packageId)
118
- continue;
119
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
120
- if (!options.packageAllowlist.includes(app.packageId)) {
121
- continue;
122
- }
123
- }
124
- if (app.storage && typeof app.storage.clearRuns === "function") {
125
- clearedCount += app.storage.clearRuns({ actionId, status });
126
- }
76
+ if (service.runs.clear) {
77
+ clearedCount = await service.runs.clear({ packageId, actionId, status });
127
78
  }
128
79
  return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
129
80
  }
130
81
  catch (err) {
131
- return jsonResponse({ ok: false, error: { code: "RUNS_CLEAR_ERROR", message: err.message } }, 500, corsHeaders);
82
+ return jsonResponse({ ok: false, error: { code: RUNS_CLEAR_ERROR, message: err.message } }, 500, corsHeaders);
132
83
  }
133
84
  }
134
85
  // 3. Run Events SSE: GET /api/v2/runs/:runId/events (and stream alias)
135
86
  const runEventsMatch = subpath.match(/^\/runs\/([^/]+)\/(events|stream)$/);
136
87
  if (runEventsMatch && req.method === "GET") {
137
88
  const runId = decodeURIComponent(runEventsMatch[1]);
138
- const run = await target.getRun(runId);
89
+ const run = await service.runs.get(runId);
139
90
  if (!run) {
140
- return jsonResponse({ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run '${runId}' not found` } }, 404, corsHeaders);
91
+ return jsonResponse({ ok: false, error: { code: RUN_NOT_FOUND, message: `Run '${runId}' not found` } }, 404, corsHeaders);
141
92
  }
142
93
  if (options.packageAllowlist &&
143
94
  options.packageAllowlist.length > 0 &&
@@ -145,7 +96,7 @@ export async function handleRunsRoutes(ctx) {
145
96
  return jsonResponse({
146
97
  ok: false,
147
98
  error: {
148
- code: "PACKAGE_FORBIDDEN",
99
+ code: PACKAGE_FORBIDDEN,
149
100
  message: `Package '${run.packageId}' is not in the allowed package list`,
150
101
  },
151
102
  }, 403, corsHeaders);
@@ -165,44 +116,62 @@ export async function handleRunsRoutes(ctx) {
165
116
  afterCursor = trimmed;
166
117
  }
167
118
  }
168
- const eventStream = target.events(runId, { after: afterCursor, signal: req.signal });
119
+ if (!service.events) {
120
+ return jsonResponse({
121
+ ok: false,
122
+ error: {
123
+ code: CAPABILITY_UNAVAILABLE,
124
+ message: "EventsPort is not supported by this ActionDock service",
125
+ },
126
+ }, 501);
127
+ }
128
+ const eventStream = service.events.events(runId, { after: afterCursor, signal: req.signal });
169
129
  const iterator = eventStream[Symbol.asyncIterator]();
170
- // 检查游标是否在建流前已过期:拉取首个事件,若抛出 EVENT_CURSOR_EXPIRED 直接返回 410
130
+ const encoder = new TextEncoder();
131
+ const encodeEvent = (evt) => {
132
+ const eventType = evt.type || "message";
133
+ const idField = evt.eventId ? `id: ${evt.eventId}\n` : `id: ${evt.sequence}\n`;
134
+ return encoder.encode(`${idField}event: ${eventType}\ndata: ${JSON.stringify(evt)}\n\n`);
135
+ };
136
+ // 游标过期检测:仅等待首事件一小段时间(过期错误通常在拉取瞬间抛出);
137
+ // 超时后立即开流,保证响应头及时下发,不再等首事件到达
138
+ const CURSOR_PROBE_MS = 250;
171
139
  let firstResult;
140
+ let cursorExpired = null;
141
+ const firstPull = iterator.next();
172
142
  try {
173
- firstResult = await iterator.next();
143
+ firstResult = await Promise.race([
144
+ firstPull,
145
+ new Promise((resolve) => setTimeout(() => resolve(undefined), CURSOR_PROBE_MS)),
146
+ ]);
174
147
  }
175
148
  catch (err) {
176
- if (err?.code === "EVENT_CURSOR_EXPIRED") {
177
- return jsonResponse({
178
- ok: false,
179
- error: {
180
- code: "EVENT_CURSOR_EXPIRED",
181
- message: err.message || "Event cursor has expired",
182
- details: err.details,
183
- },
184
- }, 410, corsHeaders);
185
- }
186
- throw err;
149
+ cursorExpired = err;
150
+ }
151
+ if (cursorExpired?.code === EVENT_CURSOR_EXPIRED) {
152
+ return jsonResponse({
153
+ ok: false,
154
+ error: {
155
+ code: EVENT_CURSOR_EXPIRED,
156
+ message: cursorExpired.message || "Event cursor has expired",
157
+ details: cursorExpired.details,
158
+ },
159
+ }, 410, corsHeaders);
187
160
  }
188
161
  const stream = new ReadableStream({
189
162
  async start(controller) {
190
- const encoder = new TextEncoder();
191
163
  let sawFinish = false;
192
- let eventsCount = 0;
193
164
  try {
194
- // 先消费首个事件结果
195
- if (!firstResult.done) {
196
- eventsCount++;
197
- const evt = firstResult.value;
165
+ // 先消费首个事件结果(探测期内未完成则继续等待)
166
+ const first = firstResult ?? (await firstPull);
167
+ if (!first.done) {
168
+ const evt = first.value;
198
169
  if (evt.type === "finish") {
199
170
  sawFinish = true;
200
171
  }
201
- const eventType = evt.type || "message";
202
- const idField = evt.eventId ? `id: ${evt.eventId}\n` : `id: ${evt.sequence}\n`;
203
- controller.enqueue(encoder.encode(`${idField}event: ${eventType}\ndata: ${JSON.stringify(evt)}\n\n`));
172
+ controller.enqueue(encodeEvent(evt));
204
173
  // 若首条事件为背压终止事件,立即关闭通道
205
- if (evt.type === "error" && evt.error?.code === "EVENT_BACKPRESSURE_LIMIT") {
174
+ if (evt.type === "error" && evt.error?.code === EVENT_BACKPRESSURE_LIMIT) {
206
175
  controller.close();
207
176
  return;
208
177
  }
@@ -212,16 +181,13 @@ export async function handleRunsRoutes(ctx) {
212
181
  const nextResult = await iterator.next();
213
182
  if (nextResult.done)
214
183
  break;
215
- eventsCount++;
216
184
  const evt = nextResult.value;
217
185
  if (evt.type === "finish") {
218
186
  sawFinish = true;
219
187
  }
220
- const eventType = evt.type || "message";
221
- const idField = evt.eventId ? `id: ${evt.eventId}\n` : `id: ${evt.sequence}\n`;
222
- controller.enqueue(encoder.encode(`${idField}event: ${eventType}\ndata: ${JSON.stringify(evt)}\n\n`));
188
+ controller.enqueue(encodeEvent(evt));
223
189
  // 若收到背压截断终止事件,正常关闭流通道
224
- if (evt.type === "error" && evt.error?.code === "EVENT_BACKPRESSURE_LIMIT") {
190
+ if (evt.type === "error" && evt.error?.code === EVENT_BACKPRESSURE_LIMIT) {
225
191
  break;
226
192
  }
227
193
  }
@@ -236,7 +202,7 @@ export async function handleRunsRoutes(ctx) {
236
202
  ok: false,
237
203
  runId,
238
204
  error: run.error || {
239
- code: run.status === "interrupted" ? "RUN_INTERRUPTED" : EXECUTION_FAILED,
205
+ code: run.status === "interrupted" ? RUN_INTERRUPTED : EXECUTION_FAILED,
240
206
  message: `Run finished with status ${run.status}`,
241
207
  },
242
208
  },
@@ -244,8 +210,11 @@ export async function handleRunsRoutes(ctx) {
244
210
  controller.enqueue(encoder.encode(`event: finish\ndata: ${JSON.stringify(finishEvt)}\n\n`));
245
211
  }
246
212
  }
247
- catch {
248
- // 忽略中断
213
+ catch (err) {
214
+ // 流中断不静默:保留诊断线索(客户端断连属正常路径,仅记录非预期异常)
215
+ if (!(err instanceof Error) || err.name !== "AbortError") {
216
+ console.warn(`[RunsRoute] SSE stream for run '${runId}' interrupted: ${err instanceof Error ? err.message : String(err)}`);
217
+ }
249
218
  }
250
219
  finally {
251
220
  try {
@@ -254,7 +223,16 @@ export async function handleRunsRoutes(ctx) {
254
223
  catch { }
255
224
  }
256
225
  },
257
- cancel() { },
226
+ cancel() {
227
+ // 客户端断开时释放底层事件迭代器,避免资源悬挂
228
+ try {
229
+ const ret = iterator.return?.();
230
+ if (ret && typeof ret.catch === "function") {
231
+ ret.catch(() => { });
232
+ }
233
+ }
234
+ catch { }
235
+ },
258
236
  });
259
237
  return new Response(stream, {
260
238
  status: 200,
@@ -270,12 +248,12 @@ export async function handleRunsRoutes(ctx) {
270
248
  const runShowMatch = subpath.match(/^\/runs\/([^/]+)$/);
271
249
  if (runShowMatch && req.method === "GET") {
272
250
  const runId = decodeURIComponent(runShowMatch[1]);
273
- const run = await target.getRun(runId);
251
+ const run = await service.runs.get(runId);
274
252
  if (!run) {
275
253
  return jsonResponse({
276
254
  ok: false,
277
255
  error: {
278
- code: "RUN_NOT_FOUND",
256
+ code: RUN_NOT_FOUND,
279
257
  message: `Run '${runId}' not found`,
280
258
  },
281
259
  }, 404, corsHeaders);
@@ -286,7 +264,7 @@ export async function handleRunsRoutes(ctx) {
286
264
  return jsonResponse({
287
265
  ok: false,
288
266
  error: {
289
- code: "PACKAGE_FORBIDDEN",
267
+ code: PACKAGE_FORBIDDEN,
290
268
  message: `Package '${run.packageId || "unknown"}' is not in the allowed package list`,
291
269
  },
292
270
  }, 403, corsHeaders);
@@ -298,12 +276,12 @@ export async function handleRunsRoutes(ctx) {
298
276
  if (runCancelMatch && req.method === "POST") {
299
277
  const runId = decodeURIComponent(runCancelMatch[1]);
300
278
  if (options.packageAllowlist && options.packageAllowlist.length > 0) {
301
- const run = await target.getRun(runId);
279
+ const run = await service.runs.get(runId);
302
280
  if (run && (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
303
281
  return jsonResponse({
304
282
  ok: false,
305
283
  error: {
306
- code: "PACKAGE_FORBIDDEN",
284
+ code: PACKAGE_FORBIDDEN,
307
285
  message: `Package '${run.packageId}' is not in the allowed package list`,
308
286
  },
309
287
  }, 403, corsHeaders);
@@ -315,12 +293,12 @@ export async function handleRunsRoutes(ctx) {
315
293
  }
316
294
  catch { }
317
295
  const reason = body?.reason || "Cancelled by client request";
318
- const cancelResult = await target.cancelRun(runId, reason);
296
+ const cancelResult = await service.runs.cancel(runId, reason);
319
297
  if (cancelResult.outcome === "not_found") {
320
298
  return jsonResponse({
321
299
  ok: false,
322
300
  error: {
323
- code: "RUN_NOT_FOUND",
301
+ code: RUN_NOT_FOUND,
324
302
  message: `Run '${runId}' not found`,
325
303
  },
326
304
  }, 404, corsHeaders);
@@ -329,7 +307,7 @@ export async function handleRunsRoutes(ctx) {
329
307
  return jsonResponse({
330
308
  ok: false,
331
309
  error: {
332
- code: "RUN_ALREADY_FINISHED",
310
+ code: RUN_ALREADY_FINISHED,
333
311
  message: `Run '${runId}' has already finished with status '${cancelResult.status}'`,
334
312
  status: cancelResult.status,
335
313
  },
@@ -1,13 +1,22 @@
1
1
  import { decodeStateKey } from "../../storage/index.js";
2
- import { CAPABILITY_UNAVAILABLE, STATE_KEY_NOT_FOUND } from "../../errors.js";
2
+ import { AMBIGUOUS_STATE_KEY, CAPABILITY_UNAVAILABLE, INVALID_ARGUMENT, INVALID_PACKAGE_ID, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND, PATH_TRAVERSAL, STATE_CLEAR_ERROR, STATE_KEY_ERROR, STATE_KEY_NOT_FOUND, STATE_LIST_ERROR, } from "../../errors.js";
3
3
  import { readJsonBody } from "../body.js";
4
- import { assertPackageAllowed, getSubPath, jsonResponse, resolveAppForPackage } from "./common.js";
4
+ import { getSubPath, jsonResponse, resolveTargetPackageId } from "./common.js";
5
+ function isClientStateError(err) {
6
+ return (err?.code === PACKAGE_NOT_FOUND ||
7
+ err?.code === INVALID_PACKAGE_ID ||
8
+ err?.code === PATH_TRAVERSAL ||
9
+ err?.code === AMBIGUOUS_STATE_KEY ||
10
+ err?.code === INVALID_ARGUMENT ||
11
+ err?.status === 400 ||
12
+ err?.status === 404);
13
+ }
5
14
  /**
6
15
  * 处理状态键名列表、读取、写入、删除及清空接口。
7
16
  * 在未显式开启管理功能 (options.enableManagement !== true) 时返回 403 拒绝。
8
17
  */
9
18
  export async function handleStateRoutes(ctx) {
10
- const { req, url, pathname, corsHeaders, options, host, target } = ctx;
19
+ const { req, url, pathname, corsHeaders, options, service } = ctx;
11
20
  const subpath = getSubPath(pathname);
12
21
  const isStateRoute = subpath === "/state" ||
13
22
  subpath.startsWith("/state/") ||
@@ -16,7 +25,7 @@ export async function handleStateRoutes(ctx) {
16
25
  return null;
17
26
  }
18
27
  // 校验管理能力开关
19
- if (options.enableManagement !== true) {
28
+ if (options.enableManagement !== true || !service.management?.state) {
20
29
  return jsonResponse({
21
30
  ok: false,
22
31
  error: {
@@ -32,31 +41,26 @@ export async function handleStateRoutes(ctx) {
32
41
  const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || "";
33
42
  const nsParam = url.searchParams.get("namespace") ?? undefined;
34
43
  const prefix = url.searchParams.get("prefix") || "";
35
- if (pkgParam) {
36
- assertPackageAllowed(pkgParam, options);
37
- }
38
- const app = resolveAppForPackage(pkgParam, host, target, options);
39
- assertPackageAllowed(app.packageId, options);
40
- const effectiveNs = actionParam
41
- ? (nsParam !== undefined ? `${actionParam}:${nsParam}` : actionParam)
42
- : (nsParam !== undefined ? nsParam : null);
43
- const keys = await app.storage.listStateKeys(effectiveNs, prefix);
44
- return jsonResponse({ ok: true, packageId: app.packageId, keys }, 200, corsHeaders);
44
+ const pkgs = await service.discovery.listPackages();
45
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
46
+ const keys = await service.management.state.list(targetPackageId, actionParam, {
47
+ namespace: nsParam,
48
+ prefix,
49
+ });
50
+ return jsonResponse({ ok: true, packageId: targetPackageId, keys }, 200, corsHeaders);
45
51
  }
46
52
  catch (err) {
47
- if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
53
+ if (err.code === PACKAGE_NOT_ALLOWED || err.status === 403) {
48
54
  return jsonResponse({
49
55
  ok: false,
50
56
  error: {
51
- code: "PACKAGE_NOT_ALLOWED",
57
+ code: PACKAGE_NOT_ALLOWED,
52
58
  message: err.message || "Package is not in the allowed package list",
53
59
  },
54
60
  }, 403, corsHeaders);
55
61
  }
56
- const isClient = err.message?.includes("Unknown or unregistered package") ||
57
- err.message?.includes("Invalid packageId") ||
58
- err.message?.includes("escapes boundary");
59
- return jsonResponse({ ok: false, error: { code: isClient ? "INVALID_ARGUMENT" : "STATE_LIST_ERROR", message: err.message } }, isClient ? 400 : 500, corsHeaders);
62
+ const isClient = isClientStateError(err);
63
+ return jsonResponse({ ok: false, error: { code: isClient ? INVALID_ARGUMENT : STATE_LIST_ERROR, message: err.message } }, isClient ? 400 : 500, corsHeaders);
60
64
  }
61
65
  }
62
66
  // 2. State Clear: POST /state/clear
@@ -66,33 +70,27 @@ export async function handleStateRoutes(ctx) {
66
70
  const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || body.package || undefined;
67
71
  const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || body.action || body.actionId || "";
68
72
  const baseNs = body.namespace ?? (url.searchParams.get("namespace") || undefined);
69
- const effectiveNs = actionParam ? (baseNs ? `${actionParam}:${baseNs}` : actionParam) : baseNs;
70
- if (pkgParam) {
71
- assertPackageAllowed(pkgParam, options);
72
- }
73
- const app = resolveAppForPackage(pkgParam, host, target, options);
74
- assertPackageAllowed(app.packageId, options);
75
- const clearedCount = await app.storage.clearState({
76
- namespace: effectiveNs,
73
+ const pkgs = await service.discovery.listPackages();
74
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
75
+ const clearedCount = await service.management.state.clear(targetPackageId, actionParam, {
76
+ namespace: baseNs,
77
77
  all: Boolean(body.all ?? url.searchParams.get("all") === "true"),
78
78
  prefix: body.prefix ?? (url.searchParams.get("prefix") || undefined),
79
79
  });
80
- return jsonResponse({ ok: true, packageId: app.packageId, clearedCount }, 200, corsHeaders);
80
+ return jsonResponse({ ok: true, packageId: targetPackageId, clearedCount }, 200, corsHeaders);
81
81
  }
82
82
  catch (err) {
83
- if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
83
+ if (err.code === PACKAGE_NOT_ALLOWED || err.status === 403) {
84
84
  return jsonResponse({
85
85
  ok: false,
86
86
  error: {
87
- code: "PACKAGE_NOT_ALLOWED",
87
+ code: PACKAGE_NOT_ALLOWED,
88
88
  message: err.message || "Package is not in the allowed package list",
89
89
  },
90
90
  }, 403, corsHeaders);
91
91
  }
92
- const isClient = err.message?.includes("Unknown or unregistered package") ||
93
- err.message?.includes("Invalid packageId") ||
94
- err.message?.includes("escapes boundary");
95
- return jsonResponse({ ok: false, error: { code: isClient ? "INVALID_ARGUMENT" : "STATE_CLEAR_ERROR", message: err.message } }, isClient ? 400 : 500, corsHeaders);
92
+ const isClient = isClientStateError(err);
93
+ return jsonResponse({ ok: false, error: { code: isClient ? INVALID_ARGUMENT : STATE_CLEAR_ERROR, message: err.message } }, isClient ? 400 : 500, corsHeaders);
96
94
  }
97
95
  }
98
96
  // 3. State Key CRUD: GET / PUT / POST / DELETE /state/:key
@@ -103,24 +101,26 @@ export async function handleStateRoutes(ctx) {
103
101
  const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
104
102
  const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || "";
105
103
  const nsParam = url.searchParams.get("namespace") || undefined;
106
- const effectiveNs = actionParam ? (nsParam ? `${actionParam}:${nsParam}` : actionParam) : nsParam;
107
- if (pkgParam) {
108
- assertPackageAllowed(pkgParam, options);
109
- }
110
- const app = resolveAppForPackage(pkgParam, host, target, options);
111
- assertPackageAllowed(app.packageId, options);
104
+ const pkgs = await service.discovery.listPackages();
105
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
112
106
  if (req.method === "GET") {
113
- const entry = await app.storage.findState(key, effectiveNs);
114
- if (!entry || entry.value === undefined) {
107
+ const entry = await service.management.state.get(targetPackageId, actionParam, key, {
108
+ namespace: nsParam,
109
+ detail: true,
110
+ });
111
+ if (!entry || (typeof entry === "object" && entry.value === undefined)) {
115
112
  return jsonResponse({ ok: false, error: { code: STATE_KEY_NOT_FOUND, message: `State key '${key}' not found` } }, 404, corsHeaders);
116
113
  }
114
+ const data = typeof entry === "object" && "value" in entry
115
+ ? entry
116
+ : { key, namespace: nsParam || "", value: entry, expiresAt: undefined };
117
117
  return jsonResponse({
118
118
  ok: true,
119
- packageId: app.packageId,
120
- key: entry.key,
121
- namespace: entry.namespace,
122
- value: entry.value,
123
- expiresAt: entry.expiresAt,
119
+ packageId: targetPackageId,
120
+ key: data.key || key,
121
+ namespace: data.namespace ?? nsParam ?? "",
122
+ value: data.value,
123
+ expiresAt: data.expiresAt,
124
124
  }, 200, corsHeaders);
125
125
  }
126
126
  if (req.method === "PUT" || req.method === "POST") {
@@ -129,40 +129,41 @@ export async function handleStateRoutes(ctx) {
129
129
  const ttl = typeof body.ttl === "number" ? body.ttl : undefined;
130
130
  const bodyAction = body.action || body.actionId || actionParam;
131
131
  const explicitNs = body.namespace || nsParam;
132
- const combinedNs = bodyAction ? (explicitNs ? `${bodyAction}:${explicitNs}` : bodyAction) : explicitNs;
133
132
  let actualKey = key;
134
- let ns = combinedNs || "";
135
- if (!combinedNs) {
133
+ let ns = explicitNs;
134
+ if (!bodyAction && !explicitNs) {
136
135
  const decoded = decodeStateKey(key);
137
136
  ns = decoded.namespace;
138
137
  actualKey = decoded.key;
139
138
  }
140
- await app.storage.setState(ns, actualKey, val, ttl);
141
- return jsonResponse({ ok: true, packageId: app.packageId, key: actualKey, namespace: ns, message: "updated" }, 200, corsHeaders);
139
+ await service.management.state.set(targetPackageId, bodyAction, actualKey, val, {
140
+ namespace: ns,
141
+ ttl,
142
+ });
143
+ return jsonResponse({ ok: true, packageId: targetPackageId, key: actualKey, namespace: ns || bodyAction || "", message: "updated" }, 200, corsHeaders);
142
144
  }
143
145
  if (req.method === "DELETE") {
144
- const deleted = await app.storage.deleteStateSmart(key, effectiveNs);
146
+ const deleted = await service.management.state.delete(targetPackageId, actionParam, key, {
147
+ namespace: nsParam,
148
+ });
145
149
  if (!deleted) {
146
150
  return jsonResponse({ ok: false, error: { code: STATE_KEY_NOT_FOUND, message: `State key '${key}' not found` } }, 404, corsHeaders);
147
151
  }
148
- return jsonResponse({ ok: true, packageId: app.packageId, key, deleted: true }, 200, corsHeaders);
152
+ return jsonResponse({ ok: true, packageId: targetPackageId, key, deleted: true }, 200, corsHeaders);
149
153
  }
150
154
  }
151
155
  catch (err) {
152
- if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
156
+ if (err.code === PACKAGE_NOT_ALLOWED || err.status === 403) {
153
157
  return jsonResponse({
154
158
  ok: false,
155
159
  error: {
156
- code: "PACKAGE_NOT_ALLOWED",
160
+ code: PACKAGE_NOT_ALLOWED,
157
161
  message: err.message || "Package is not in the allowed package list",
158
162
  },
159
163
  }, 403, corsHeaders);
160
164
  }
161
- const isClient = err.message?.includes("Unknown or unregistered package") ||
162
- err.message?.includes("Invalid packageId") ||
163
- err.message?.includes("escapes boundary") ||
164
- err.message?.includes("Ambiguous state key");
165
- return jsonResponse({ ok: false, error: { code: isClient ? "INVALID_ARGUMENT" : "STATE_KEY_ERROR", message: err.message } }, isClient ? 400 : 500, corsHeaders);
165
+ const isClient = isClientStateError(err);
166
+ return jsonResponse({ ok: false, error: { code: isClient ? INVALID_ARGUMENT : STATE_KEY_ERROR, message: err.message } }, isClient ? 400 : 500, corsHeaders);
166
167
  }
167
168
  }
168
169
  return null;
@@ -9,7 +9,7 @@ export declare function formatHostForUrl(host: string): string;
9
9
  */
10
10
  export declare function launchHttpServer(port: number, host: string, fetchHandler: (req: Request) => Promise<Response>, tls?: ServerTlsOptions): Promise<CoreHttpServerInstance>;
11
11
  /**
12
- * 启动 ActionDock 2.0 原生轻量级 HTTP 服务端。
13
- * 作为 ActionDockHost 与 ActionDockTarget 的薄适配层,负责中间件流转、认证拦截与路由分发。
12
+ * 启动 ActionDock 原生轻量级 HTTP 服务端。
13
+ * 面向 ActionDockService 服务端口,负责中间件流转、认证拦截与路由分发。
14
14
  */
15
15
  export declare function startActionDockServer(options?: ServerOptions): Promise<ActionDockServerInstance>;