@actiondock/core 2.0.12 → 2.2.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 (192) hide show
  1. package/README.md +61 -30
  2. package/dist/app/app.d.ts +73 -0
  3. package/dist/app/app.js +700 -0
  4. package/dist/app/index.d.ts +2 -0
  5. package/dist/app/index.js +2 -0
  6. package/dist/app/types.d.ts +283 -0
  7. package/dist/app/types.js +1 -0
  8. package/dist/catalog/action-index.d.ts +1 -1
  9. package/dist/catalog/action-index.js +93 -0
  10. package/dist/catalog/action-resolver.d.ts +2 -2
  11. package/dist/catalog/action-resolver.js +95 -0
  12. package/dist/catalog/index.d.ts +5 -5
  13. package/dist/catalog/index.js +5 -0
  14. package/dist/catalog/location-registry.d.ts +5 -5
  15. package/dist/catalog/location-registry.js +138 -0
  16. package/dist/catalog/package-catalog.d.ts +2 -2
  17. package/dist/catalog/package-catalog.js +76 -0
  18. package/dist/catalog/types.d.ts +2 -2
  19. package/dist/catalog/types.js +1 -0
  20. package/dist/doctor/doctor.d.ts +2 -1
  21. package/dist/doctor/doctor.js +613 -0
  22. package/dist/doctor/index.d.ts +2 -2
  23. package/dist/doctor/index.js +2 -0
  24. package/dist/doctor/types.js +1 -0
  25. package/dist/errors.d.ts +122 -0
  26. package/dist/errors.js +123 -0
  27. package/dist/execution/index.d.ts +2 -2
  28. package/dist/execution/index.js +2 -0
  29. package/dist/execution/service.d.ts +46 -41
  30. package/dist/execution/service.js +633 -0
  31. package/dist/execution/types.d.ts +59 -2
  32. package/dist/execution/types.js +1 -0
  33. package/dist/export/index.d.ts +1 -1
  34. package/dist/export/index.js +1 -0
  35. package/dist/export/templates.d.ts +45 -7
  36. package/dist/export/templates.js +621 -0
  37. package/dist/filter/index.d.ts +1 -1
  38. package/dist/filter/index.js +1 -0
  39. package/dist/filter/intent.js +123 -0
  40. package/dist/host/host.d.ts +55 -0
  41. package/dist/host/host.js +740 -0
  42. package/dist/host/index.d.ts +2 -0
  43. package/dist/host/index.js +2 -0
  44. package/dist/host/types.d.ts +81 -0
  45. package/dist/host/types.js +1 -0
  46. package/dist/index.d.ts +21 -16
  47. package/dist/index.js +20 -8458
  48. package/dist/ipc/diagnostic.d.ts +65 -0
  49. package/dist/ipc/diagnostic.js +129 -0
  50. package/dist/ipc/host.d.ts +7 -0
  51. package/dist/ipc/host.js +93 -0
  52. package/dist/ipc/index.d.ts +4 -0
  53. package/dist/ipc/index.js +4 -0
  54. package/dist/ipc/target.d.ts +56 -0
  55. package/dist/ipc/target.js +297 -0
  56. package/dist/ipc/types.d.ts +42 -0
  57. package/dist/ipc/types.js +1 -0
  58. package/dist/platform/default.d.ts +32 -0
  59. package/dist/platform/default.js +40 -0
  60. package/dist/platform/index.d.ts +3 -0
  61. package/dist/platform/index.js +3 -0
  62. package/dist/platform/node-fs.d.ts +37 -0
  63. package/dist/platform/node-fs.js +81 -0
  64. package/dist/platform/types.d.ts +104 -0
  65. package/dist/platform/types.js +1 -0
  66. package/dist/profile/client.d.ts +58 -12
  67. package/dist/profile/client.js +467 -0
  68. package/dist/profile/index.d.ts +3 -3
  69. package/dist/profile/index.js +3 -0
  70. package/dist/profile/manager.d.ts +2 -2
  71. package/dist/profile/manager.js +294 -0
  72. package/dist/profile/types.d.ts +1 -1
  73. package/dist/profile/types.js +1 -0
  74. package/dist/project/closure.js +68 -0
  75. package/dist/project/digest.d.ts +29 -0
  76. package/dist/project/digest.js +324 -0
  77. package/dist/project/index.d.ts +10 -5
  78. package/dist/project/index.js +10 -0
  79. package/dist/project/init.d.ts +3 -4
  80. package/dist/project/init.js +175 -0
  81. package/dist/project/loader.d.ts +21 -27
  82. package/dist/project/loader.js +484 -0
  83. package/dist/project/lockfile.d.ts +55 -0
  84. package/dist/project/lockfile.js +175 -0
  85. package/dist/project/manifest.d.ts +6 -28
  86. package/dist/project/manifest.js +220 -0
  87. package/dist/project/resolver.d.ts +116 -0
  88. package/dist/project/resolver.js +376 -0
  89. package/dist/project/transactions.d.ts +103 -0
  90. package/dist/project/transactions.js +1203 -0
  91. package/dist/project/types-generator.d.ts +33 -0
  92. package/dist/project/types-generator.js +168 -0
  93. package/dist/project/types.d.ts +58 -82
  94. package/dist/project/types.js +1 -0
  95. package/dist/registry/index.d.ts +5 -2
  96. package/dist/registry/index.js +5 -0
  97. package/dist/registry/lock.d.ts +27 -0
  98. package/dist/registry/lock.js +93 -0
  99. package/dist/registry/registry.d.ts +32 -11
  100. package/dist/registry/registry.js +611 -0
  101. package/dist/registry/resolve.d.ts +62 -0
  102. package/dist/registry/resolve.js +195 -0
  103. package/dist/registry/scan.d.ts +38 -0
  104. package/dist/registry/scan.js +135 -0
  105. package/dist/registry/types.d.ts +1 -1
  106. package/dist/registry/types.js +1 -0
  107. package/dist/runtime/action-collection.d.ts +27 -0
  108. package/dist/runtime/action-collection.js +57 -0
  109. package/dist/runtime/clock.d.ts +0 -2
  110. package/dist/runtime/clock.js +17 -0
  111. package/dist/runtime/context.d.ts +16 -5
  112. package/dist/runtime/context.js +242 -0
  113. package/dist/runtime/env.d.ts +3 -1
  114. package/dist/runtime/env.js +159 -0
  115. package/dist/runtime/events.d.ts +46 -14
  116. package/dist/runtime/events.js +604 -0
  117. package/dist/runtime/execution-manager.d.ts +1 -1
  118. package/dist/runtime/execution-manager.js +66 -0
  119. package/dist/runtime/index.d.ts +10 -9
  120. package/dist/runtime/index.js +10 -0
  121. package/dist/runtime/module-loader.d.ts +0 -8
  122. package/dist/runtime/module-loader.js +16 -0
  123. package/dist/runtime/process.d.ts +2 -4
  124. package/dist/runtime/process.js +146 -0
  125. package/dist/runtime/runner.d.ts +117 -10
  126. package/dist/runtime/runner.js +984 -0
  127. package/dist/runtime/standalone.d.ts +68 -19
  128. package/dist/runtime/standalone.js +536 -0
  129. package/dist/schema/validator.js +116 -0
  130. package/dist/server/body.js +93 -0
  131. package/dist/server/index.d.ts +5 -6
  132. package/dist/server/index.js +5 -0
  133. package/dist/server/routes/actions.d.ts +2 -2
  134. package/dist/server/routes/actions.js +298 -0
  135. package/dist/server/routes/common.d.ts +28 -17
  136. package/dist/server/routes/common.js +104 -0
  137. package/dist/server/routes/config.d.ts +2 -1
  138. package/dist/server/routes/config.js +173 -0
  139. package/dist/server/routes/doctor.d.ts +2 -2
  140. package/dist/server/routes/doctor.js +63 -0
  141. package/dist/server/routes/health.d.ts +2 -2
  142. package/dist/server/routes/health.js +35 -0
  143. package/dist/server/routes/index.d.ts +9 -9
  144. package/dist/server/routes/index.js +9 -0
  145. package/dist/server/routes/info.d.ts +4 -2
  146. package/dist/server/routes/info.js +158 -0
  147. package/dist/server/routes/playbooks.d.ts +2 -2
  148. package/dist/server/routes/playbooks.js +134 -0
  149. package/dist/server/routes/runs.d.ts +2 -2
  150. package/dist/server/routes/runs.js +343 -0
  151. package/dist/server/routes/state.d.ts +2 -1
  152. package/dist/server/routes/state.js +169 -0
  153. package/dist/server/security.d.ts +11 -4
  154. package/dist/server/security.js +101 -0
  155. package/dist/server/server.d.ts +6 -6
  156. package/dist/server/server.js +355 -0
  157. package/dist/server/types.d.ts +27 -8
  158. package/dist/server/types.js +1 -0
  159. package/dist/storage/data-dir-lock.d.ts +119 -0
  160. package/dist/storage/data-dir-lock.js +1016 -0
  161. package/dist/storage/driver.d.ts +4 -10
  162. package/dist/storage/driver.js +59 -0
  163. package/dist/storage/index.d.ts +30 -10
  164. package/dist/storage/index.js +98 -0
  165. package/dist/storage/lazy.d.ts +8 -0
  166. package/dist/storage/lazy.js +44 -0
  167. package/dist/storage/mask.d.ts +12 -5
  168. package/dist/storage/mask.js +48 -0
  169. package/dist/storage/params.d.ts +13 -0
  170. package/dist/storage/params.js +32 -0
  171. package/dist/storage/sqlite.d.ts +19 -6
  172. package/dist/storage/sqlite.js +689 -0
  173. package/dist/storage/types.d.ts +57 -6
  174. package/dist/storage/types.js +14 -0
  175. package/dist/target/index.d.ts +4 -0
  176. package/dist/target/index.js +4 -0
  177. package/dist/target/local.d.ts +52 -0
  178. package/dist/target/local.js +343 -0
  179. package/dist/target/remote.d.ts +76 -0
  180. package/dist/target/remote.js +839 -0
  181. package/dist/target/target.d.ts +6 -0
  182. package/dist/target/target.js +35 -0
  183. package/dist/target/types.d.ts +246 -0
  184. package/dist/target/types.js +34 -0
  185. package/dist/utils/index.d.ts +22 -2
  186. package/dist/utils/index.js +180 -0
  187. package/dist/version.d.ts +1 -1
  188. package/dist/version.js +4 -0
  189. package/package.json +6 -7
  190. package/dist/build/index.d.ts +0 -1
  191. package/dist/build/templates.d.ts +0 -19
  192. package/dist/server/runtime-registry.d.ts +0 -78
@@ -0,0 +1,134 @@
1
+ import { filterByIntent } from "../../filter/index.js";
2
+ import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
3
+ /**
4
+ * 处理 Playbook 规程相关的 HTTP 路由(列表与 SOP 详情查询)。
5
+ */
6
+ export async function handlePlaybooksRoutes(ctx) {
7
+ const { req, url, pathname, corsHeaders, target, host, options } = ctx;
8
+ const subpath = getSubPath(pathname);
9
+ // 1. Playbooks List: GET /api/v2/playbooks, GET /playbooks
10
+ if (subpath === "/playbooks" && req.method === "GET") {
11
+ try {
12
+ const intent = url.searchParams.get("intent") || undefined;
13
+ const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
14
+ if (targetPkg) {
15
+ assertPackageAllowed(targetPkg, options);
16
+ }
17
+ let pbs = await target.listPlaybooks({ intent, package: targetPkg });
18
+ if (targetPkg) {
19
+ pbs = pbs.filter((p) => p.packageId === targetPkg);
20
+ }
21
+ if (options.packageAllowlist && Array.isArray(options.packageAllowlist) && options.packageAllowlist.length > 0) {
22
+ pbs = pbs.filter((p) => p.packageId && options.packageAllowlist.includes(p.packageId));
23
+ }
24
+ if (intent) {
25
+ pbs = filterByIntent(pbs, intent, [(p) => p.id, (p) => p.description || "", (p) => p.packageId || "", (p) => (p.actions || []).join(" ")], false);
26
+ }
27
+ return jsonResponse(pbs, 200, corsHeaders);
28
+ }
29
+ catch (err) {
30
+ if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
31
+ return jsonResponse({
32
+ ok: false,
33
+ error: {
34
+ code: "PACKAGE_NOT_ALLOWED",
35
+ message: err.message || "Package is not in the allowed package list",
36
+ },
37
+ }, 403, corsHeaders);
38
+ }
39
+ return jsonResponse({ ok: false, error: { code: "PLAYBOOKS_LIST_ERROR", message: err.message } }, 500, corsHeaders);
40
+ }
41
+ }
42
+ // 2. Multi-package Playbook Show: GET /api/v2/packages/:packageId/playbooks/:playbookId
43
+ const pkgPlaybookMatch = subpath.match(/^\/packages\/([^/]+)\/playbooks\/([^/]+)$/);
44
+ if (pkgPlaybookMatch && req.method === "GET") {
45
+ const packageId = decodeURIComponent(pkgPlaybookMatch[1]);
46
+ const playbookId = decodeURIComponent(pkgPlaybookMatch[2]);
47
+ try {
48
+ assertPackageAllowed(packageId, options);
49
+ }
50
+ catch (err) {
51
+ return jsonResponse({
52
+ ok: false,
53
+ error: {
54
+ code: "PACKAGE_NOT_ALLOWED",
55
+ message: err.message || `Package '${packageId}' is not in the allowed package list`,
56
+ },
57
+ }, 403, corsHeaders);
58
+ }
59
+ const ref = `${packageId}/${playbookId}`;
60
+ try {
61
+ const pb = await target.describePlaybook(ref);
62
+ return jsonResponse(pb, 200, corsHeaders);
63
+ }
64
+ catch (err) {
65
+ return jsonResponse({
66
+ ok: false,
67
+ error: {
68
+ code: "PLAYBOOK_NOT_FOUND",
69
+ message: err.message || `Playbook '${playbookId}' not found in package '${packageId}'`,
70
+ },
71
+ }, 404, corsHeaders);
72
+ }
73
+ }
74
+ // 3. Short Playbook Show: GET /api/v2/playbooks/:id, GET /playbooks/:id
75
+ const playbookMatch = subpath.match(/^\/playbooks\/(.+)$/);
76
+ if (playbookMatch && req.method === "GET") {
77
+ const playbookId = decodeURIComponent(playbookMatch[1]);
78
+ if (playbookId.includes("/")) {
79
+ const pkgFromRef = playbookId.slice(0, playbookId.lastIndexOf("/"));
80
+ try {
81
+ assertPackageAllowed(pkgFromRef, options);
82
+ }
83
+ catch (err) {
84
+ return jsonResponse({
85
+ ok: false,
86
+ error: {
87
+ code: "PACKAGE_NOT_ALLOWED",
88
+ message: err.message || `Package '${pkgFromRef}' is not in the allowed package list`,
89
+ },
90
+ }, 403, corsHeaders);
91
+ }
92
+ }
93
+ try {
94
+ const pb = await target.describePlaybook(playbookId);
95
+ const unwrapped = target.unwrap?.();
96
+ let pkgId = pb.packageId || (unwrapped && "packageId" in unwrapped ? unwrapped.packageId : undefined);
97
+ if (!pkgId && host) {
98
+ for (const app of host.listApps()) {
99
+ try {
100
+ const map = app.getStaticPlaybookMap?.();
101
+ const cleanId = playbookId.replace(/\.md$/, "");
102
+ if (map?.has(cleanId) || map?.has(playbookId)) {
103
+ pkgId = app.packageId;
104
+ break;
105
+ }
106
+ }
107
+ catch { }
108
+ }
109
+ }
110
+ if (!pkgId) {
111
+ const allPbs = await target.listPlaybooks().catch(() => []);
112
+ const matched = allPbs.find((p) => p.id === playbookId || p.id.endsWith(`/${playbookId}`));
113
+ if (matched?.packageId) {
114
+ pkgId = matched.packageId;
115
+ }
116
+ }
117
+ assertPackageAllowed(pkgId, options);
118
+ return jsonResponse(pb, 200, corsHeaders);
119
+ }
120
+ catch (err) {
121
+ if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
122
+ return jsonResponse({
123
+ ok: false,
124
+ error: {
125
+ code: "PACKAGE_NOT_ALLOWED",
126
+ message: err.message || "Package is not in the allowed package list",
127
+ },
128
+ }, 403, corsHeaders);
129
+ }
130
+ return jsonResponse({ ok: false, error: { code: "PLAYBOOK_NOT_FOUND", message: err.message } }, 404, corsHeaders);
131
+ }
132
+ }
133
+ return null;
134
+ }
@@ -1,5 +1,5 @@
1
- import { type RouteContext } from "./common";
1
+ import { type RouteContext } from "./common.js";
2
2
  /**
3
- * 处理历史运行记录检索、清理、详情及 SSE 流式日志接口。
3
+ * 处理历史运行记录查询、清理、详情及 SSE 流式日志接口。
4
4
  */
5
5
  export declare function handleRunsRoutes(ctx: RouteContext): Promise<Response | null>;
@@ -0,0 +1,343 @@
1
+ import { filterByIntent } from "../../filter/index.js";
2
+ import { EXECUTION_FAILED } from "../../errors.js";
3
+ import { isTerminalRunStatus } from "../../storage/types.js";
4
+ import { readJsonBody } from "../body.js";
5
+ import { getSubPath, jsonResponse } from "./common.js";
6
+ /**
7
+ * 处理历史运行记录查询、清理、详情及 SSE 流式日志接口。
8
+ */
9
+ export async function handleRunsRoutes(ctx) {
10
+ const { req, url, pathname, corsHeaders, options, target, host } = ctx;
11
+ const subpath = getSubPath(pathname);
12
+ // 1. Runs List: GET /api/v2/runs, GET /runs
13
+ if (subpath === "/runs" && req.method === "GET") {
14
+ try {
15
+ const status = url.searchParams.get("status") || undefined;
16
+ const actionId = url.searchParams.get("actionId") || undefined;
17
+ const packageId = url.searchParams.get("packageId") || undefined;
18
+ const intent = url.searchParams.get("intent") || undefined;
19
+ const limit = parseInt(url.searchParams.get("limit") || "50", 10);
20
+ if (packageId &&
21
+ options.packageAllowlist &&
22
+ options.packageAllowlist.length > 0 &&
23
+ !options.packageAllowlist.includes(packageId)) {
24
+ return jsonResponse({
25
+ ok: false,
26
+ error: {
27
+ code: "PACKAGE_FORBIDDEN",
28
+ message: `Package '${packageId}' is not in the allowed package list`,
29
+ },
30
+ }, 403, corsHeaders);
31
+ }
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
+ const records = app.storage.listRuns({ actionId, limit });
53
+ for (const r of records) {
54
+ if (!seenRunIds.has(r.id)) {
55
+ seenRunIds.add(r.id);
56
+ if (status && r.status !== status)
57
+ continue;
58
+ if (options.packageAllowlist &&
59
+ options.packageAllowlist.length > 0 &&
60
+ r.packageId &&
61
+ !options.packageAllowlist.includes(r.packageId)) {
62
+ continue;
63
+ }
64
+ allRuns.push(r);
65
+ }
66
+ }
67
+ }
68
+ catch { }
69
+ }
70
+ }
71
+ allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
72
+ const filtered = intent
73
+ ? filterByIntent(allRuns, intent, [(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.packageId], false)
74
+ : allRuns;
75
+ const sliced = filtered.slice(0, limit);
76
+ return jsonResponse({ ok: true, total: filtered.length, items: sliced }, 200, corsHeaders);
77
+ }
78
+ catch (err) {
79
+ return jsonResponse({ ok: false, error: { code: "RUNS_LIST_ERROR", message: err.message } }, 500, corsHeaders);
80
+ }
81
+ }
82
+ // 2. Runs Clear: POST /api/v2/runs/clear, DELETE /api/v2/runs, etc.
83
+ if ((subpath === "/runs/clear" && req.method === "POST") ||
84
+ (subpath === "/runs" && req.method === "DELETE")) {
85
+ try {
86
+ let body = {};
87
+ if (req.method === "POST" || req.headers.get("content-type")?.includes("json")) {
88
+ body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
89
+ }
90
+ const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
91
+ const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
92
+ const status = url.searchParams.get("status") || body.status || undefined;
93
+ if (packageId &&
94
+ options.packageAllowlist &&
95
+ options.packageAllowlist.length > 0 &&
96
+ !options.packageAllowlist.includes(packageId)) {
97
+ return jsonResponse({
98
+ ok: false,
99
+ error: {
100
+ code: "PACKAGE_FORBIDDEN",
101
+ message: `Package '${packageId}' is not in the allowed package list`,
102
+ },
103
+ }, 403, corsHeaders);
104
+ }
105
+ let clearedCount = 0;
106
+ const apps = host
107
+ ? host.listApps()
108
+ : (() => {
109
+ const inner = target?.unwrap?.();
110
+ return inner && "listApps" in inner ? inner.listApps() : [inner].filter(Boolean);
111
+ })();
112
+ for (const app of apps) {
113
+ if (!app)
114
+ continue;
115
+ if (packageId && app.packageId !== packageId)
116
+ continue;
117
+ if (options.packageAllowlist && options.packageAllowlist.length > 0) {
118
+ if (!options.packageAllowlist.includes(app.packageId)) {
119
+ continue;
120
+ }
121
+ }
122
+ if (app.storage && typeof app.storage.clearRuns === "function") {
123
+ clearedCount += app.storage.clearRuns({ actionId, status });
124
+ }
125
+ }
126
+ return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
127
+ }
128
+ catch (err) {
129
+ return jsonResponse({ ok: false, error: { code: "RUNS_CLEAR_ERROR", message: err.message } }, 500, corsHeaders);
130
+ }
131
+ }
132
+ // 3. Run Events SSE: GET /api/v2/runs/:runId/events (and stream alias)
133
+ const runEventsMatch = subpath.match(/^\/runs\/([^/]+)\/(events|stream)$/);
134
+ if (runEventsMatch && req.method === "GET") {
135
+ const runId = decodeURIComponent(runEventsMatch[1]);
136
+ const run = await target.getRun(runId);
137
+ if (!run) {
138
+ return jsonResponse({ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run '${runId}' not found` } }, 404, corsHeaders);
139
+ }
140
+ if (options.packageAllowlist &&
141
+ options.packageAllowlist.length > 0 &&
142
+ (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
143
+ return jsonResponse({
144
+ ok: false,
145
+ error: {
146
+ code: "PACKAGE_FORBIDDEN",
147
+ message: `Package '${run.packageId}' is not in the allowed package list`,
148
+ },
149
+ }, 403, corsHeaders);
150
+ }
151
+ // 解析 Last-Event-ID 请求头或 query 参数 after 游标
152
+ const lastEventIdHeader = req.headers.get("Last-Event-ID") ||
153
+ req.headers.get("last-event-id") ||
154
+ url.searchParams.get("after") ||
155
+ undefined;
156
+ let afterCursor = undefined;
157
+ if (lastEventIdHeader !== undefined && lastEventIdHeader.trim() !== "") {
158
+ const trimmed = lastEventIdHeader.trim();
159
+ if (/^-?\d+$/.test(trimmed)) {
160
+ afterCursor = parseInt(trimmed, 10);
161
+ }
162
+ else {
163
+ afterCursor = trimmed;
164
+ }
165
+ }
166
+ const eventStream = target.events(runId, { after: afterCursor, signal: req.signal });
167
+ const iterator = eventStream[Symbol.asyncIterator]();
168
+ // 检查游标是否在建流前已过期:拉取首个事件,若抛出 EVENT_CURSOR_EXPIRED 直接返回 410
169
+ let firstResult;
170
+ try {
171
+ firstResult = await iterator.next();
172
+ }
173
+ catch (err) {
174
+ if (err?.code === "EVENT_CURSOR_EXPIRED") {
175
+ return jsonResponse({
176
+ ok: false,
177
+ error: {
178
+ code: "EVENT_CURSOR_EXPIRED",
179
+ message: err.message || "Event cursor has expired",
180
+ details: err.details,
181
+ },
182
+ }, 410, corsHeaders);
183
+ }
184
+ throw err;
185
+ }
186
+ const stream = new ReadableStream({
187
+ async start(controller) {
188
+ const encoder = new TextEncoder();
189
+ let sawFinish = false;
190
+ let eventsCount = 0;
191
+ try {
192
+ // 先消费首个事件结果
193
+ if (!firstResult.done) {
194
+ eventsCount++;
195
+ const evt = firstResult.value;
196
+ if (evt.type === "finish") {
197
+ sawFinish = true;
198
+ }
199
+ const eventType = evt.type || "message";
200
+ const idField = evt.eventId ? `id: ${evt.eventId}\n` : `id: ${evt.sequence}\n`;
201
+ controller.enqueue(encoder.encode(`${idField}event: ${eventType}\ndata: ${JSON.stringify(evt)}\n\n`));
202
+ // 若首条事件为背压终止事件,立即关闭通道
203
+ if (evt.type === "error" && evt.error?.code === "EVENT_BACKPRESSURE_LIMIT") {
204
+ controller.close();
205
+ return;
206
+ }
207
+ }
208
+ // 循环消费后续实时或队列事件
209
+ while (!req.signal.aborted) {
210
+ const nextResult = await iterator.next();
211
+ if (nextResult.done)
212
+ break;
213
+ eventsCount++;
214
+ const evt = nextResult.value;
215
+ if (evt.type === "finish") {
216
+ sawFinish = true;
217
+ }
218
+ const eventType = evt.type || "message";
219
+ const idField = evt.eventId ? `id: ${evt.eventId}\n` : `id: ${evt.sequence}\n`;
220
+ controller.enqueue(encoder.encode(`${idField}event: ${eventType}\ndata: ${JSON.stringify(evt)}\n\n`));
221
+ // 若收到背压截断终止事件,正常关闭流通道
222
+ if (evt.type === "error" && evt.error?.code === "EVENT_BACKPRESSURE_LIMIT") {
223
+ break;
224
+ }
225
+ }
226
+ if (!sawFinish && run && isTerminalRunStatus(run.status)) {
227
+ const finishEvt = {
228
+ type: "finish",
229
+ runId,
230
+ timestamp: run.finishedAt || new Date().toISOString(),
231
+ result: run.status === "success"
232
+ ? { ok: true, runId, data: run.output ?? null }
233
+ : {
234
+ ok: false,
235
+ runId,
236
+ error: run.error || {
237
+ code: run.status === "interrupted" ? "RUN_INTERRUPTED" : EXECUTION_FAILED,
238
+ message: `Run finished with status ${run.status}`,
239
+ },
240
+ },
241
+ };
242
+ controller.enqueue(encoder.encode(`event: finish\ndata: ${JSON.stringify(finishEvt)}\n\n`));
243
+ }
244
+ }
245
+ catch {
246
+ // 忽略中断
247
+ }
248
+ finally {
249
+ try {
250
+ controller.close();
251
+ }
252
+ catch { }
253
+ }
254
+ },
255
+ cancel() { },
256
+ });
257
+ return new Response(stream, {
258
+ status: 200,
259
+ headers: {
260
+ "Content-Type": "text/event-stream",
261
+ "Cache-Control": "no-cache",
262
+ "Connection": "keep-alive",
263
+ ...corsHeaders,
264
+ },
265
+ });
266
+ }
267
+ // 4. Run Show: GET /api/v2/runs/:runId, GET /runs/:runId
268
+ const runShowMatch = subpath.match(/^\/runs\/([^/]+)$/);
269
+ if (runShowMatch && req.method === "GET") {
270
+ const runId = decodeURIComponent(runShowMatch[1]);
271
+ const run = await target.getRun(runId);
272
+ if (!run) {
273
+ return jsonResponse({
274
+ ok: false,
275
+ error: {
276
+ code: "RUN_NOT_FOUND",
277
+ message: `Run '${runId}' not found`,
278
+ },
279
+ }, 404, corsHeaders);
280
+ }
281
+ if (options.packageAllowlist &&
282
+ options.packageAllowlist.length > 0 &&
283
+ (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
284
+ return jsonResponse({
285
+ ok: false,
286
+ error: {
287
+ code: "PACKAGE_FORBIDDEN",
288
+ message: `Package '${run.packageId || "unknown"}' is not in the allowed package list`,
289
+ },
290
+ }, 403, corsHeaders);
291
+ }
292
+ return jsonResponse(run, 200, corsHeaders);
293
+ }
294
+ // 5. Run Cancel: POST /api/v2/runs/:runId/cancel, POST /runs/:runId/cancel
295
+ const runCancelMatch = subpath.match(/^\/runs\/([^/]+)\/cancel$/);
296
+ if (runCancelMatch && req.method === "POST") {
297
+ const runId = decodeURIComponent(runCancelMatch[1]);
298
+ if (options.packageAllowlist && options.packageAllowlist.length > 0) {
299
+ const run = await target.getRun(runId);
300
+ if (run && (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
301
+ return jsonResponse({
302
+ ok: false,
303
+ error: {
304
+ code: "PACKAGE_FORBIDDEN",
305
+ message: `Package '${run.packageId}' is not in the allowed package list`,
306
+ },
307
+ }, 403, corsHeaders);
308
+ }
309
+ }
310
+ let body = {};
311
+ try {
312
+ body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
313
+ }
314
+ catch { }
315
+ const reason = body?.reason || "Cancelled by client request";
316
+ const cancelResult = await target.cancelRun(runId, reason);
317
+ if (cancelResult.outcome === "not_found") {
318
+ return jsonResponse({
319
+ ok: false,
320
+ error: {
321
+ code: "RUN_NOT_FOUND",
322
+ message: `Run '${runId}' not found`,
323
+ },
324
+ }, 404, corsHeaders);
325
+ }
326
+ if (cancelResult.outcome === "already_terminal") {
327
+ return jsonResponse({
328
+ ok: false,
329
+ error: {
330
+ code: "RUN_ALREADY_FINISHED",
331
+ message: `Run '${runId}' has already finished with status '${cancelResult.status}'`,
332
+ status: cancelResult.status,
333
+ },
334
+ }, 409, corsHeaders);
335
+ }
336
+ return jsonResponse({
337
+ ok: true,
338
+ runId,
339
+ status: "cancelled",
340
+ }, 200, corsHeaders);
341
+ }
342
+ return null;
343
+ }
@@ -1,5 +1,6 @@
1
- import { type RouteContext } from "./common";
1
+ import { type RouteContext } from "./common.js";
2
2
  /**
3
3
  * 处理状态键名列表、读取、写入、删除及清空接口。
4
+ * 在未显式开启管理功能 (options.enableManagement !== true) 时返回 403 拒绝。
4
5
  */
5
6
  export declare function handleStateRoutes(ctx: RouteContext): Promise<Response | null>;
@@ -0,0 +1,169 @@
1
+ import { decodeStateKey } from "../../storage/index.js";
2
+ import { CAPABILITY_UNAVAILABLE, STATE_KEY_NOT_FOUND } from "../../errors.js";
3
+ import { readJsonBody } from "../body.js";
4
+ import { assertPackageAllowed, getSubPath, jsonResponse, resolveAppForPackage } from "./common.js";
5
+ /**
6
+ * 处理状态键名列表、读取、写入、删除及清空接口。
7
+ * 在未显式开启管理功能 (options.enableManagement !== true) 时返回 403 拒绝。
8
+ */
9
+ export async function handleStateRoutes(ctx) {
10
+ const { req, url, pathname, corsHeaders, options, host, target } = ctx;
11
+ const subpath = getSubPath(pathname);
12
+ const isStateRoute = subpath === "/state" ||
13
+ subpath.startsWith("/state/") ||
14
+ subpath.startsWith("/management/state");
15
+ if (!isStateRoute) {
16
+ return null;
17
+ }
18
+ // 校验管理能力开关
19
+ if (options.enableManagement !== true) {
20
+ return jsonResponse({
21
+ ok: false,
22
+ error: {
23
+ code: CAPABILITY_UNAVAILABLE,
24
+ message: "Management APIs are not enabled on this server. Set enableManagement: true to enable.",
25
+ },
26
+ }, 403, corsHeaders);
27
+ }
28
+ // 1. State List: GET /state
29
+ if (subpath === "/state" && req.method === "GET") {
30
+ try {
31
+ const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
32
+ const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || "";
33
+ const nsParam = url.searchParams.get("namespace") ?? undefined;
34
+ 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);
45
+ }
46
+ catch (err) {
47
+ if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
48
+ return jsonResponse({
49
+ ok: false,
50
+ error: {
51
+ code: "PACKAGE_NOT_ALLOWED",
52
+ message: err.message || "Package is not in the allowed package list",
53
+ },
54
+ }, 403, corsHeaders);
55
+ }
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);
60
+ }
61
+ }
62
+ // 2. State Clear: POST /state/clear
63
+ if (subpath === "/state/clear" && req.method === "POST") {
64
+ try {
65
+ const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
66
+ const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || body.package || undefined;
67
+ const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || body.action || body.actionId || "";
68
+ 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,
77
+ all: Boolean(body.all ?? url.searchParams.get("all") === "true"),
78
+ prefix: body.prefix ?? (url.searchParams.get("prefix") || undefined),
79
+ });
80
+ return jsonResponse({ ok: true, packageId: app.packageId, clearedCount }, 200, corsHeaders);
81
+ }
82
+ catch (err) {
83
+ if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
84
+ return jsonResponse({
85
+ ok: false,
86
+ error: {
87
+ code: "PACKAGE_NOT_ALLOWED",
88
+ message: err.message || "Package is not in the allowed package list",
89
+ },
90
+ }, 403, corsHeaders);
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);
96
+ }
97
+ }
98
+ // 3. State Key CRUD: GET / PUT / POST / DELETE /state/:key
99
+ const stateKeyMatch = subpath.match(/^\/state\/([^/]+)$/);
100
+ if (stateKeyMatch) {
101
+ try {
102
+ const key = decodeURIComponent(stateKeyMatch[1]);
103
+ const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
104
+ const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || "";
105
+ 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);
112
+ if (req.method === "GET") {
113
+ const entry = await app.storage.findState(key, effectiveNs);
114
+ if (!entry || entry.value === undefined) {
115
+ return jsonResponse({ ok: false, error: { code: STATE_KEY_NOT_FOUND, message: `State key '${key}' not found` } }, 404, corsHeaders);
116
+ }
117
+ return jsonResponse({
118
+ ok: true,
119
+ packageId: app.packageId,
120
+ key: entry.key,
121
+ namespace: entry.namespace,
122
+ value: entry.value,
123
+ expiresAt: entry.expiresAt,
124
+ }, 200, corsHeaders);
125
+ }
126
+ if (req.method === "PUT" || req.method === "POST") {
127
+ const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
128
+ const val = body.value !== undefined ? body.value : body;
129
+ const ttl = typeof body.ttl === "number" ? body.ttl : undefined;
130
+ const bodyAction = body.action || body.actionId || actionParam;
131
+ const explicitNs = body.namespace || nsParam;
132
+ const combinedNs = bodyAction ? (explicitNs ? `${bodyAction}:${explicitNs}` : bodyAction) : explicitNs;
133
+ let actualKey = key;
134
+ let ns = combinedNs || "";
135
+ if (!combinedNs) {
136
+ const decoded = decodeStateKey(key);
137
+ ns = decoded.namespace;
138
+ actualKey = decoded.key;
139
+ }
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);
142
+ }
143
+ if (req.method === "DELETE") {
144
+ const deleted = await app.storage.deleteStateSmart(key, effectiveNs);
145
+ if (!deleted) {
146
+ return jsonResponse({ ok: false, error: { code: STATE_KEY_NOT_FOUND, message: `State key '${key}' not found` } }, 404, corsHeaders);
147
+ }
148
+ return jsonResponse({ ok: true, packageId: app.packageId, key, deleted: true }, 200, corsHeaders);
149
+ }
150
+ }
151
+ catch (err) {
152
+ if (err.code === "PACKAGE_NOT_ALLOWED" || err.status === 403) {
153
+ return jsonResponse({
154
+ ok: false,
155
+ error: {
156
+ code: "PACKAGE_NOT_ALLOWED",
157
+ message: err.message || "Package is not in the allowed package list",
158
+ },
159
+ }, 403, corsHeaders);
160
+ }
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);
166
+ }
167
+ }
168
+ return null;
169
+ }