@actiondock/core 2.7.0 → 2.9.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 (243) hide show
  1. package/README.md +77 -64
  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 +106 -10
  18. package/dist/errors.js +105 -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 -24
  33. package/dist/index.js +19 -23
  34. package/dist/input/advice.d.ts +5 -1
  35. package/dist/input/advice.js +163 -214
  36. package/dist/input/describe.d.ts +1 -1
  37. package/dist/input/flat-errors.d.ts +5 -6
  38. package/dist/input/flat-errors.js +6 -6
  39. package/dist/input/flat-materializer.js +42 -44
  40. package/dist/invocation/index.d.ts +2 -0
  41. package/dist/invocation/index.js +2 -0
  42. package/dist/invocation/policy.d.ts +157 -0
  43. package/dist/invocation/policy.js +231 -0
  44. package/dist/invocation/types.d.ts +172 -0
  45. package/dist/invocation/types.js +97 -0
  46. package/dist/ipc/diagnostic.d.ts +0 -12
  47. package/dist/ipc/diagnostic.js +0 -21
  48. package/dist/ipc/host.d.ts +4 -4
  49. package/dist/ipc/host.js +44 -47
  50. package/dist/ipc/index.d.ts +1 -1
  51. package/dist/ipc/index.js +1 -1
  52. package/dist/ipc/service.d.ts +40 -0
  53. package/dist/ipc/service.js +349 -0
  54. package/dist/ipc/types.d.ts +21 -15
  55. package/dist/node/index.d.ts +1 -0
  56. package/dist/node/index.js +1 -0
  57. package/dist/node/module-loader.d.ts +63 -0
  58. package/dist/node/module-loader.js +112 -0
  59. package/dist/package/index.d.ts +23 -0
  60. package/dist/package/index.js +18 -0
  61. package/dist/{app/app.d.ts → package/runtime.d.ts} +41 -20
  62. package/dist/{app/app.js → package/runtime.js} +140 -95
  63. package/dist/{app → package}/static-index.js +58 -41
  64. package/dist/package/storage-view.d.ts +13 -0
  65. package/dist/package/storage-view.js +22 -0
  66. package/dist/{app → package}/types.d.ts +76 -57
  67. package/dist/package/types.js +22 -0
  68. package/dist/platform/index.d.ts +1 -1
  69. package/dist/platform/index.js +1 -1
  70. package/dist/platform/node.d.ts +48 -0
  71. package/dist/platform/node.js +96 -0
  72. package/dist/platform/types.d.ts +0 -9
  73. package/dist/process/control-arbiter.d.ts +2 -1
  74. package/dist/process/control-arbiter.js +1 -2
  75. package/dist/process/driver.d.ts +9 -8
  76. package/dist/process/driver.js +11 -22
  77. package/dist/process/index.d.ts +2 -0
  78. package/dist/process/index.js +2 -0
  79. package/dist/process/managed-record.d.ts +2 -2
  80. package/dist/process/process-driver.d.ts +109 -0
  81. package/dist/process/process-driver.js +650 -0
  82. package/dist/process/process-executor.d.ts +47 -0
  83. package/dist/process/process-executor.js +296 -0
  84. package/dist/process/process-manager.js +48 -26
  85. package/dist/process/run-executor.js +12 -12
  86. package/dist/profile/client-actions.js +2 -2
  87. package/dist/profile/client-health.js +2 -2
  88. package/dist/profile/client-query.js +5 -6
  89. package/dist/profile/client-transport.d.ts +19 -17
  90. package/dist/profile/client-transport.js +47 -80
  91. package/dist/profile/client.d.ts +2 -2
  92. package/dist/profile/client.js +1 -1
  93. package/dist/profile/index.d.ts +1 -0
  94. package/dist/profile/index.js +1 -0
  95. package/dist/profile/manager.d.ts +5 -0
  96. package/dist/profile/manager.js +41 -6
  97. package/dist/project/closure.js +2 -2
  98. package/dist/project/index.d.ts +8 -1
  99. package/dist/project/index.js +10 -1
  100. package/dist/project/init.js +2 -2
  101. package/dist/project/loader.d.ts +1 -1
  102. package/dist/project/loader.js +26 -29
  103. package/dist/project/transactions.js +5 -10
  104. package/dist/project/types.d.ts +5 -0
  105. package/dist/registry/index.d.ts +3 -1
  106. package/dist/registry/index.js +2 -1
  107. package/dist/registry/lock.d.ts +28 -7
  108. package/dist/registry/lock.js +136 -16
  109. package/dist/registry/registry.d.ts +1 -5
  110. package/dist/registry/registry.js +1 -53
  111. package/dist/registry/scan.d.ts +0 -15
  112. package/dist/registry/scan.js +0 -59
  113. package/dist/registry/store.d.ts +42 -0
  114. package/dist/registry/store.js +43 -0
  115. package/dist/registry/types.d.ts +0 -24
  116. package/dist/runtime/action-collection.d.ts +1 -1
  117. package/dist/runtime/action-registry.d.ts +0 -4
  118. package/dist/runtime/action-registry.js +0 -7
  119. package/dist/runtime/context.js +5 -11
  120. package/dist/runtime/events.js +3 -6
  121. package/dist/runtime/identity.d.ts +22 -0
  122. package/dist/runtime/identity.js +10 -0
  123. package/dist/runtime/index.d.ts +1 -2
  124. package/dist/runtime/index.js +1 -2
  125. package/dist/runtime/module-loader.d.ts +2 -34
  126. package/dist/runtime/module-loader.js +1 -16
  127. package/dist/runtime/process.d.ts +6 -24
  128. package/dist/runtime/process.js +1 -118
  129. package/dist/runtime/run-persistence.js +2 -5
  130. package/dist/runtime/runner.d.ts +33 -93
  131. package/dist/runtime/runner.js +132 -364
  132. package/dist/runtime/standalone.d.ts +23 -8
  133. package/dist/runtime/standalone.js +100 -147
  134. package/dist/server/body.d.ts +16 -8
  135. package/dist/server/body.js +33 -16
  136. package/dist/server/dispatcher.d.ts +6 -10
  137. package/dist/server/dispatcher.js +24 -32
  138. package/dist/server/http-server.d.ts +87 -0
  139. package/dist/server/http-server.js +304 -0
  140. package/dist/server/index.d.ts +12 -0
  141. package/dist/server/index.js +11 -0
  142. package/dist/server/mcp-endpoint.d.ts +61 -0
  143. package/dist/server/mcp-endpoint.js +118 -0
  144. package/dist/server/policy.d.ts +138 -0
  145. package/dist/server/policy.js +368 -0
  146. package/dist/server/routes/actions.js +114 -64
  147. package/dist/server/routes/common.d.ts +18 -33
  148. package/dist/server/routes/common.js +45 -65
  149. package/dist/server/routes/config.d.ts +1 -1
  150. package/dist/server/routes/config.js +55 -65
  151. package/dist/server/routes/doctor.d.ts +1 -1
  152. package/dist/server/routes/doctor.js +13 -21
  153. package/dist/server/routes/health.d.ts +1 -1
  154. package/dist/server/routes/health.js +4 -5
  155. package/dist/server/routes/info.d.ts +2 -2
  156. package/dist/server/routes/info.js +17 -24
  157. package/dist/server/routes/playbooks.js +23 -36
  158. package/dist/server/routes/runs.js +151 -141
  159. package/dist/server/routes/state.d.ts +1 -1
  160. package/dist/server/routes/state.js +64 -63
  161. package/dist/server/security.d.ts +11 -0
  162. package/dist/server/security.js +36 -19
  163. package/dist/server/server.d.ts +2 -2
  164. package/dist/server/server.js +224 -238
  165. package/dist/server/types.d.ts +68 -21
  166. package/dist/service/factory.d.ts +12 -0
  167. package/dist/service/factory.js +85 -0
  168. package/dist/{target → service}/index.d.ts +1 -1
  169. package/dist/{target → service}/index.js +1 -1
  170. package/dist/service/local.d.ts +27 -0
  171. package/dist/service/local.js +138 -0
  172. package/dist/{target → service}/remote-errors.d.ts +3 -6
  173. package/dist/service/remote-errors.js +38 -0
  174. package/dist/{target → service}/remote-polling.d.ts +2 -2
  175. package/dist/{target → service}/remote-polling.js +8 -8
  176. package/dist/service/remote.d.ts +34 -0
  177. package/dist/service/remote.js +634 -0
  178. package/dist/service/sse-stream.d.ts +24 -0
  179. package/dist/service/sse-stream.js +139 -0
  180. package/dist/service/types.d.ts +272 -0
  181. package/dist/service/types.js +32 -0
  182. package/dist/storage/data-dir-lock.js +10 -5
  183. package/dist/storage/driver.d.ts +2 -1
  184. package/dist/storage/driver.js +3 -52
  185. package/dist/storage/index.d.ts +1 -0
  186. package/dist/storage/index.js +1 -0
  187. package/dist/storage/lock-core.d.ts +2 -6
  188. package/dist/storage/lock-core.js +4 -17
  189. package/dist/storage/params.d.ts +2 -8
  190. package/dist/storage/params.js +2 -8
  191. package/dist/storage/sqlite-driver.d.ts +40 -0
  192. package/dist/storage/sqlite-driver.js +116 -0
  193. package/dist/storage/sqlite.js +21 -6
  194. package/dist/storage/types.d.ts +4 -4
  195. package/dist/storage/types.js +2 -2
  196. package/dist/utils/index.d.ts +53 -0
  197. package/dist/utils/index.js +99 -4
  198. package/dist/utils/process.d.ts +10 -0
  199. package/dist/utils/process.js +21 -0
  200. package/dist/version.d.ts +1 -1
  201. package/dist/version.js +1 -1
  202. package/package.json +41 -3
  203. package/dist/app/index.d.ts +0 -2
  204. package/dist/app/index.js +0 -2
  205. package/dist/app/types.js +0 -1
  206. package/dist/catalog/action-index.d.ts +0 -9
  207. package/dist/catalog/action-index.js +0 -95
  208. package/dist/catalog/action-resolver.d.ts +0 -20
  209. package/dist/catalog/action-resolver.js +0 -95
  210. package/dist/catalog/location-registry.d.ts +0 -15
  211. package/dist/catalog/location-registry.js +0 -138
  212. package/dist/catalog/package-catalog.d.ts +0 -7
  213. package/dist/catalog/package-catalog.js +0 -77
  214. package/dist/catalog/types.d.ts +0 -68
  215. package/dist/catalog/types.js +0 -1
  216. package/dist/export/index.d.ts +0 -1
  217. package/dist/export/index.js +0 -1
  218. package/dist/export/templates.d.ts +0 -71
  219. package/dist/export/templates.js +0 -635
  220. package/dist/ipc/target.d.ts +0 -74
  221. package/dist/ipc/target.js +0 -365
  222. package/dist/platform/default.d.ts +0 -38
  223. package/dist/platform/default.js +0 -78
  224. package/dist/project/resolver.d.ts +0 -116
  225. package/dist/project/resolver.js +0 -376
  226. package/dist/registry/resolve.d.ts +0 -62
  227. package/dist/registry/resolve.js +0 -195
  228. package/dist/runtime/execution-manager.d.ts +0 -48
  229. package/dist/runtime/execution-manager.js +0 -66
  230. package/dist/runtime/package-runner-factory.d.ts +0 -82
  231. package/dist/runtime/package-runner-factory.js +0 -186
  232. package/dist/target/local.d.ts +0 -53
  233. package/dist/target/local.js +0 -349
  234. package/dist/target/remote-errors.js +0 -51
  235. package/dist/target/remote.d.ts +0 -86
  236. package/dist/target/remote.js +0 -818
  237. package/dist/target/target.d.ts +0 -6
  238. package/dist/target/target.js +0 -37
  239. package/dist/target/types.d.ts +0 -261
  240. package/dist/target/types.js +0 -35
  241. /package/dist/{app → package}/static-index.d.ts +0 -0
  242. /package/dist/{target → service}/sse-parser.d.ts +0 -0
  243. /package/dist/{target → service}/sse-parser.js +0 -0
@@ -1,17 +1,15 @@
1
- import { createServer as createNodeHttpServer } from "node:http";
2
- import { readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { Readable } from "node:stream";
5
- import { pipeline } from "node:stream/promises";
6
- import { createActionDockHost } from "../host/host.js";
1
+ import { posix, resolve } from "node:path";
2
+ import { NodeHttpServer } from "./http-server.js";
3
+ import { createActionDock } from "../service/factory.js";
7
4
  import { NOT_FOUND, UNAUTHORIZED } from "../errors.js";
5
+ import { parseActionRef } from "../catalog/resolve-action.js";
8
6
  import { ensureDependencyClosure } from "../project/closure.js";
9
7
  import { findProjectRoot } from "../project/loader.js";
10
8
  import { listLinkedPackages, resolvePackageRoot } from "../registry/registry.js";
11
- import { LocalActionDockTarget } from "../target/local.js";
12
9
  import { handleActionsRoutes, handleConfigRoutes, handleDoctorRoute, handleHealthRoute, handleInfoRoute, handlePlaybooksRoutes, handleRunsRoutes, handleStateRoutes, jsonResponse, } from "./routes/index.js";
13
- import { DEFAULT_MAX_BODY_BYTES } from "./body.js";
14
10
  import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security.js";
11
+ import { createMcpEndpointHandler } from "./mcp-endpoint.js";
12
+ import { extractBearerToken, matchViewByToken, normalizeServerViews, } from "./policy.js";
15
13
  /**
16
14
  * 规范化主机地址用于拼接 URL。
17
15
  * 若 host 包含冒号 : 且不以 [ 开头,则添加中括号包裹(IPv6 标准格式)。
@@ -23,132 +21,28 @@ export function formatHostForUrl(host) {
23
21
  * 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
24
22
  */
25
23
  export async function launchHttpServer(port, host, fetchHandler, tls) {
26
- const requestListener = async (req, res) => {
27
- const ac = new AbortController();
28
- const onReqClose = () => {
29
- if (!req.complete) {
30
- ac.abort(new Error("Request aborted by client"));
31
- }
32
- };
33
- const onResClose = () => {
34
- if (!res.writableFinished) {
35
- ac.abort(new Error("Request aborted by client"));
36
- }
37
- };
38
- req.on("close", onReqClose);
39
- res.on("close", onResClose);
40
- try {
41
- const protocol = req.socket?.encrypted ? "https" : "http";
42
- const hostHeader = req.headers.host || "127.0.0.1";
43
- const url = new URL(req.url || "/", `${protocol}://${hostHeader}`).href;
44
- const headers = new Headers();
45
- for (const [k, v] of Object.entries(req.headers)) {
46
- if (v === undefined)
47
- continue;
48
- if (Array.isArray(v)) {
49
- for (const item of v)
50
- headers.append(k, item);
51
- }
52
- else {
53
- headers.set(k, v);
54
- }
55
- }
56
- const method = (req.method || "GET").toUpperCase();
57
- const hasBody = method !== "GET" && method !== "HEAD";
58
- const init = { method, headers, signal: ac.signal };
59
- if (hasBody) {
60
- init.body = Readable.toWeb(req);
61
- init.duplex = "half";
62
- }
63
- const webReq = new Request(url, init);
64
- const webRes = await fetchHandler(webReq);
65
- res.statusCode = webRes.status;
66
- if (webRes.statusText)
67
- res.statusMessage = webRes.statusText;
68
- webRes.headers.forEach((v, k) => res.setHeader(k, v));
69
- if (!webRes.body) {
70
- res.end();
71
- return;
72
- }
73
- await pipeline(Readable.fromWeb(webRes.body), res);
74
- }
75
- catch (err) {
76
- if (!res.headersSent) {
77
- res.statusCode = 500;
78
- res.end(JSON.stringify({ error: err?.message || String(err) }));
79
- }
80
- else {
81
- res.destroy(err);
82
- }
83
- }
84
- finally {
85
- req.removeListener("close", onReqClose);
86
- res.removeListener("close", onResClose);
87
- }
88
- };
89
- let srv;
90
- if (tls) {
91
- let cert = tls.cert;
92
- if (!cert && tls.certPath) {
93
- cert = readFileSync(tls.certPath);
94
- }
95
- let key = tls.key;
96
- if (!key && tls.keyPath) {
97
- key = readFileSync(tls.keyPath);
98
- }
99
- const httpsOptions = {
100
- cert,
101
- key,
102
- };
103
- if (tls.ca) {
104
- httpsOptions.ca = tls.ca;
105
- }
106
- if (tls.passphrase) {
107
- httpsOptions.passphrase = tls.passphrase;
108
- }
109
- const { createServer: createHttpsServer } = await import("node:https");
110
- srv = createHttpsServer(httpsOptions, requestListener);
111
- }
112
- else {
113
- srv = createNodeHttpServer(requestListener);
114
- }
115
- const instance = {
24
+ const server = new NodeHttpServer({
116
25
  port,
26
+ host,
27
+ fetch: fetchHandler,
28
+ tls,
29
+ });
30
+ await server.listen(port, host);
31
+ const instance = {
32
+ port: server.port,
117
33
  stop: async () => {
118
- await new Promise((resolve) => {
119
- srv.close(() => resolve());
120
- srv.closeAllConnections?.();
121
- });
34
+ await server.close();
122
35
  },
36
+ ready: Promise.resolve(),
123
37
  };
124
- await new Promise((resolve, reject) => {
125
- srv.once("error", (err) => {
126
- reject(err);
127
- });
128
- srv.listen(port, host, () => {
129
- const addr = srv.address();
130
- if (typeof addr === "object" && addr) {
131
- instance.port = addr.port;
132
- }
133
- resolve();
134
- });
135
- });
136
- instance.ready = Promise.resolve();
137
38
  return instance;
138
39
  }
139
40
  /**
140
- * 启动 ActionDock 2.0 原生轻量级 HTTP 服务端。
141
- * 作为 ActionDockHost 与 ActionDockTarget 的薄适配层,负责中间件流转、认证拦截与路由分发。
41
+ * 启动 ActionDock 原生轻量级 HTTP 服务端。
42
+ * 面向 ActionDockService 服务端口,负责中间件流转、认证拦截与路由分发。
142
43
  */
143
44
  export async function startActionDockServer(options = {}) {
144
- let hostInstance = options.hostInstance ??
145
- (options.host && typeof options.host === "object" && "listActions" in options.host
146
- ? options.host
147
- : undefined);
148
- let targetInstance = options.target;
149
- const hostString = typeof options.host === "string"
150
- ? options.host
151
- : (options.hostname ?? "127.0.0.1");
45
+ const hostString = options.hostname ?? options.host ?? "127.0.0.1";
152
46
  const port = options.port ?? 5177;
153
47
  const host = hostString;
154
48
  const token = options.token;
@@ -156,38 +50,53 @@ export async function startActionDockServer(options = {}) {
156
50
  const projectRoot = options.projectRoot
157
51
  ? resolve(options.projectRoot)
158
52
  : findProjectRoot(process.cwd());
53
+ // 1. 归一化解析视图配置集合(含默认视图与自定义视图)
54
+ const { defaultView, views } = normalizeServerViews(options);
159
55
  // 非回环地址强制要求配置 Token 鉴权(防裸奔)
160
- if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
56
+ const hasTokenConfigured = Boolean(token ||
57
+ defaultView.policy.token ||
58
+ Array.from(views.values()).some((v) => Boolean(v.policy.token)));
59
+ if (!isLoopbackHost(host) && !hasTokenConfigured && !options.allowInsecureNoAuth) {
161
60
  throw new Error("Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
162
61
  }
163
- // 若调用方未传入 host 或 target,通过 projectRoot、customHome、platform 等直接创建宿主
164
- if (!targetInstance && !hostInstance) {
62
+ let serviceInstance = options.service;
63
+ // 若未传 service,服务端统一通过 createActionDock() 实例化,不再接受或处理外部传入的 Host 实体
64
+ if (!serviceInstance) {
165
65
  const scanLinkedPackages = options.scanLinkedPackages ?? !projectRoot;
166
- hostInstance = await createActionDockHost({
66
+ serviceInstance = await createActionDock({
167
67
  projectRoot: projectRoot || undefined,
168
68
  customHome,
169
69
  platform: options.platform,
170
70
  dataDir: options.dataDir,
171
71
  inMemory: options.inMemory,
172
72
  scanLinkedPackages,
173
- autoLoadCurrentProject: true,
73
+ enableManagement: options.enableManagement,
174
74
  });
175
75
  }
176
- if (!targetInstance && hostInstance) {
177
- targetInstance = new LocalActionDockTarget(hostInstance);
178
- }
179
- else if (targetInstance && !hostInstance) {
180
- const inner = targetInstance?.unwrap?.();
181
- if (inner && "listApps" in inner) {
182
- hostInstance = inner;
183
- }
76
+ if (!serviceInstance) {
77
+ throw new Error("Failed to initialize ActionDockService for server");
184
78
  }
79
+ // 2. 聚合所有 views 声明的依赖包与动作,确保依赖闭包满足
185
80
  const roots = [];
186
81
  if (projectRoot) {
187
82
  roots.push(projectRoot);
188
83
  }
189
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
190
- for (const pkgId of options.packageAllowlist) {
84
+ const aggregatedPackageAllowlist = new Set();
85
+ const aggregatedActionAllowlist = new Set();
86
+ for (const v of views.values()) {
87
+ if (v.policy.packageAllowlist) {
88
+ for (const pkg of v.policy.packageAllowlist) {
89
+ aggregatedPackageAllowlist.add(pkg);
90
+ }
91
+ }
92
+ if (v.policy.actionAllowlist) {
93
+ for (const act of v.policy.actionAllowlist) {
94
+ aggregatedActionAllowlist.add(act);
95
+ }
96
+ }
97
+ }
98
+ if (aggregatedPackageAllowlist.size > 0) {
99
+ for (const pkgId of aggregatedPackageAllowlist) {
191
100
  const r = resolvePackageRoot(pkgId, projectRoot || undefined, customHome);
192
101
  if (r && !roots.includes(r)) {
193
102
  roots.push(r);
@@ -201,9 +110,85 @@ export async function startActionDockServer(options = {}) {
201
110
  }
202
111
  }
203
112
  }
113
+ if (aggregatedActionAllowlist.size > 0) {
114
+ for (const actRef of aggregatedActionAllowlist) {
115
+ try {
116
+ const parsed = parseActionRef(actRef);
117
+ if (parsed.packageId) {
118
+ const r = resolvePackageRoot(parsed.packageId, projectRoot || undefined, customHome);
119
+ if (r && !roots.includes(r)) {
120
+ roots.push(r);
121
+ }
122
+ }
123
+ }
124
+ catch { }
125
+ }
126
+ }
204
127
  if (roots.length > 0) {
205
128
  await ensureDependencyClosure(roots, { customHome });
206
129
  }
130
+ // 3. 为每个启用 MCP 的视图独立装配 MCP 处理器
131
+ if (options.enableMcp !== false) {
132
+ for (const v of views.values()) {
133
+ if (v.enableMcp !== false) {
134
+ let delegate;
135
+ if (v.rawOptions.mcpHandler) {
136
+ if (v.rawOptions.mcpHandler.length >= 2) {
137
+ delegate = (req) => v.rawOptions.mcpHandler(req, v.policy);
138
+ }
139
+ else {
140
+ let factoryResult;
141
+ try {
142
+ factoryResult = v.rawOptions.mcpHandler(v.policy);
143
+ }
144
+ catch {
145
+ factoryResult = null;
146
+ }
147
+ if (typeof factoryResult === "function") {
148
+ delegate = factoryResult;
149
+ }
150
+ else {
151
+ delegate = (req) => v.rawOptions.mcpHandler(req, v.policy);
152
+ }
153
+ }
154
+ }
155
+ else if (options.mcpHandlerFactory) {
156
+ delegate = options.mcpHandlerFactory(v.policy);
157
+ }
158
+ else if (options.mcpHandler) {
159
+ if (options.mcpHandler.length >= 2) {
160
+ delegate = (req) => options.mcpHandler(req, v.policy);
161
+ }
162
+ else {
163
+ let factoryResult;
164
+ try {
165
+ factoryResult = options.mcpHandler(v.policy);
166
+ }
167
+ catch {
168
+ factoryResult = null;
169
+ }
170
+ if (typeof factoryResult === "function") {
171
+ delegate = factoryResult;
172
+ }
173
+ else {
174
+ if (factoryResult && typeof factoryResult.catch === "function") {
175
+ factoryResult.catch(() => { });
176
+ }
177
+ delegate = (req) => options.mcpHandler(req, v.policy);
178
+ }
179
+ }
180
+ }
181
+ if (delegate) {
182
+ v.mcpEndpointHandler = createMcpEndpointHandler(delegate, {
183
+ token: v.policy.token,
184
+ allowQueryToken: options.allowQueryToken,
185
+ corsOrigins: options.corsOrigins,
186
+ maxBodyBytes: options.maxBodyBytes,
187
+ });
188
+ }
189
+ }
190
+ }
191
+ }
207
192
  const fetchHandler = async (req) => {
208
193
  const origin = req.headers.get("origin");
209
194
  const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
@@ -214,104 +199,113 @@ export async function startActionDockServer(options = {}) {
214
199
  });
215
200
  }
216
201
  const url = new URL(req.url);
217
- const pathname = url.pathname;
202
+ const rawPathname = url.pathname;
203
+ let activeView;
204
+ let effectivePathname;
205
+ let effectiveReq = req;
206
+ // 1. 检查是否为 /views/:viewName/... 命名空间路由
207
+ const viewMatch = rawPathname.match(/^\/views\/([^/]+)(\/.*)?$/);
208
+ if (viewMatch) {
209
+ let viewName;
210
+ try {
211
+ viewName = decodeURIComponent(viewMatch[1]);
212
+ }
213
+ catch {
214
+ return jsonResponse({
215
+ ok: false,
216
+ error: {
217
+ code: NOT_FOUND,
218
+ message: `Invalid view name encoding: '${viewMatch[1]}'`,
219
+ },
220
+ }, 400, corsHeaders);
221
+ }
222
+ const matched = views.get(viewName);
223
+ if (!matched) {
224
+ return jsonResponse({
225
+ ok: false,
226
+ error: {
227
+ code: NOT_FOUND,
228
+ message: `View '${viewName}' not found`,
229
+ },
230
+ }, 404, corsHeaders);
231
+ }
232
+ activeView = matched;
233
+ // 规范化子路径,防范路径穿透与多斜杠混淆
234
+ let rawEffective = viewMatch[2] || "/";
235
+ if (!rawEffective.startsWith("/")) {
236
+ rawEffective = "/" + rawEffective;
237
+ }
238
+ const normalizedPath = posix.normalize(rawEffective);
239
+ effectivePathname = normalizedPath.startsWith("/") ? normalizedPath : "/" + normalizedPath;
240
+ // 剥离 /views/:viewName 前缀后重构 URL 与 Request
241
+ const rewrittenUrl = new URL(req.url);
242
+ rewrittenUrl.pathname = effectivePathname;
243
+ try {
244
+ const init = {
245
+ method: req.method,
246
+ headers: req.headers,
247
+ signal: req.signal,
248
+ };
249
+ if (req.body && req.method !== "GET" && req.method !== "HEAD") {
250
+ init.body = req.body;
251
+ init.duplex = "half";
252
+ }
253
+ effectiveReq = new Request(rewrittenUrl.toString(), init);
254
+ }
255
+ catch {
256
+ effectiveReq = req;
257
+ }
258
+ }
259
+ else {
260
+ // 根路径请求:智能根据 Bearer Token 匹配视图或回退默认视图(遍历全量视图以防范时序差异)
261
+ effectivePathname = rawPathname;
262
+ const clientToken = extractBearerToken(req, options.allowQueryToken);
263
+ if (clientToken) {
264
+ const matchedByToken = matchViewByToken(clientToken, views.values());
265
+ activeView = matchedByToken ?? defaultView;
266
+ }
267
+ else {
268
+ activeView = defaultView;
269
+ }
270
+ }
271
+ const effectivePolicy = activeView.policy;
272
+ const effectiveUrl = new URL(effectiveReq.url);
273
+ effectiveUrl.pathname = effectivePathname;
218
274
  const ctx = {
219
- req,
220
- url,
221
- pathname,
275
+ req: effectiveReq,
276
+ url: effectiveUrl,
277
+ pathname: effectivePathname,
222
278
  corsHeaders,
223
279
  projectRoot,
224
280
  customHome,
225
- host: hostInstance,
226
- target: targetInstance,
281
+ service: serviceInstance,
227
282
  options,
283
+ activePolicy: effectivePolicy,
228
284
  };
229
- // 1. 健康检查路由(内部处理独立鉴权逻辑)
285
+ // 2. 健康检查路由(内部处理独立鉴权逻辑,使用 activePolicy.token)
230
286
  const healthResponse = await handleHealthRoute(ctx);
231
287
  if (healthResponse) {
232
288
  return healthResponse;
233
289
  }
234
- // 2. MCP 统一网关端点
235
- if (options.enableMcp !== false &&
236
- options.mcpHandler &&
237
- (pathname === "/mcp" || pathname.startsWith("/mcp/"))) {
238
- if (!verifyBearerToken(req, token, options)) {
290
+ // 3. MCP 统一网关端点(委托当前视图的端点处理器)
291
+ if (effectivePathname === "/mcp" || effectivePathname.startsWith("/mcp/")) {
292
+ if (activeView.enableMcp === false || options.enableMcp === false) {
239
293
  return jsonResponse({
240
294
  ok: false,
241
295
  error: {
242
- code: UNAUTHORIZED,
243
- message: "Invalid or missing Bearer token",
296
+ code: NOT_FOUND,
297
+ message: `MCP endpoint is disabled for view '${activeView.name}'`,
244
298
  },
245
- }, 401, corsHeaders);
246
- }
247
- // 请求体体积限制保护
248
- const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
249
- const contentLengthHeader = req.headers.get("content-length");
250
- if (contentLengthHeader) {
251
- const parsedLength = parseInt(contentLengthHeader, 10);
252
- if (!isNaN(parsedLength) && parsedLength > maxBytes) {
253
- return jsonResponse({
254
- ok: false,
255
- error: {
256
- code: "REQUEST_TOO_LARGE",
257
- message: "Request body exceeds maximum allowed size",
258
- },
259
- }, 413, corsHeaders);
260
- }
299
+ }, 404, corsHeaders);
261
300
  }
262
- let mcpReq = req;
263
- if (req.body && req.method !== "GET" && req.method !== "HEAD") {
264
- const reader = req.body.getReader();
265
- const chunks = [];
266
- let totalBytes = 0;
267
- let tooLarge = false;
268
- try {
269
- while (true) {
270
- const { done, value } = await reader.read();
271
- if (done)
272
- break;
273
- if (value) {
274
- totalBytes += value.byteLength;
275
- if (totalBytes > maxBytes) {
276
- tooLarge = true;
277
- await reader.cancel();
278
- break;
279
- }
280
- chunks.push(value);
281
- }
282
- }
283
- }
284
- finally {
285
- reader.releaseLock();
286
- }
287
- if (tooLarge) {
288
- return jsonResponse({
289
- ok: false,
290
- error: {
291
- code: "REQUEST_TOO_LARGE",
292
- message: "Request body exceeds maximum allowed size",
293
- },
294
- }, 413, corsHeaders);
295
- }
296
- const merged = new Uint8Array(totalBytes);
297
- let offset = 0;
298
- for (const chunk of chunks) {
299
- merged.set(chunk, offset);
300
- offset += chunk.byteLength;
301
- }
302
- mcpReq = new Request(req.url, {
303
- method: req.method,
304
- headers: req.headers,
305
- body: merged,
306
- signal: req.signal,
307
- });
301
+ if (activeView.mcpEndpointHandler) {
302
+ const mcpRes = await activeView.mcpEndpointHandler(effectiveReq);
303
+ if (mcpRes)
304
+ return mcpRes;
308
305
  }
309
- const mcpRes = await options.mcpHandler(mcpReq);
310
- if (mcpRes)
311
- return mcpRes;
312
306
  }
313
- // 3. 全局 API 认证鉴权拦截
314
- if (!verifyBearerToken(req, token, options)) {
307
+ // 4. API 认证鉴权拦截(基于当前生效策略中的 Token)
308
+ if (!verifyBearerToken(effectiveReq, effectivePolicy.token, options)) {
315
309
  return jsonResponse({
316
310
  ok: false,
317
311
  error: {
@@ -320,7 +314,7 @@ export async function startActionDockServer(options = {}) {
320
314
  },
321
315
  }, 401, corsHeaders);
322
316
  }
323
- // 4. 业务领域路由分发(完全委托 Target / Host)
317
+ // 5. 业务领域路由分发(完全委托 Service 与 RouteContext 中的 activePolicy)
324
318
  const routeResponse = (await handleInfoRoute(ctx)) ||
325
319
  (await handleDoctorRoute(ctx)) ||
326
320
  (await handleActionsRoutes(ctx)) ||
@@ -331,12 +325,12 @@ export async function startActionDockServer(options = {}) {
331
325
  if (routeResponse) {
332
326
  return routeResponse;
333
327
  }
334
- // 5. 404 路由兜底
328
+ // 6. 404 路由兜底
335
329
  return jsonResponse({
336
330
  ok: false,
337
331
  error: {
338
332
  code: NOT_FOUND,
339
- message: `Route not found: ${req.method} ${pathname}`,
333
+ message: `Route not found: ${req.method} ${rawPathname}`,
340
334
  },
341
335
  }, 404, corsHeaders);
342
336
  };
@@ -353,28 +347,20 @@ export async function startActionDockServer(options = {}) {
353
347
  set port(val) {
354
348
  server.port = val;
355
349
  },
356
- host: hostInstance,
357
- target: targetInstance,
350
+ service: serviceInstance,
358
351
  get url() {
359
352
  return `${protocol}://${formatHostForUrl(actualHost)}:${this.port}`;
360
353
  },
361
354
  ready: Promise.resolve(),
362
355
  stop: async (stopOptions) => {
363
356
  await server.stop(true);
364
- if (targetInstance) {
365
- try {
366
- await targetInstance.close(stopOptions?.graceMs !== undefined ? { timeoutMs: stopOptions.graceMs } : undefined);
367
- }
368
- catch {
369
- // 忽略关闭异常
370
- }
371
- }
372
- if (hostInstance && hostInstance !== targetInstance?.unwrap?.()) {
357
+ if (serviceInstance) {
373
358
  try {
374
- await hostInstance.close(stopOptions);
359
+ await serviceInstance.close(stopOptions);
375
360
  }
376
- catch {
377
- // 忽略宿主关闭异常
361
+ catch (err) {
362
+ // 服务关闭失败不阻断停机流程,但保留可观测诊断
363
+ console.warn(`[ActionDockServer] Service close failed during stop: ${err instanceof Error ? err.message : String(err)}`);
378
364
  }
379
365
  }
380
366
  },