@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.
- package/README.md +77 -64
- package/dist/catalog/action-catalog.d.ts +56 -0
- package/dist/catalog/action-catalog.js +202 -0
- package/dist/catalog/discovery.d.ts +60 -0
- package/dist/catalog/discovery.js +119 -0
- package/dist/catalog/graph.d.ts +190 -0
- package/dist/catalog/graph.js +589 -0
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +5 -5
- package/dist/catalog/resolve-action.d.ts +43 -0
- package/dist/catalog/resolve-action.js +119 -0
- package/dist/catalog/resolve-playbook.d.ts +26 -0
- package/dist/catalog/resolve-playbook.js +114 -0
- package/dist/doctor/checks/project.js +27 -8
- package/dist/doctor/checks/registry.js +2 -2
- package/dist/doctor/checks/runtime.js +34 -1
- package/dist/errors.d.ts +106 -10
- package/dist/errors.js +105 -10
- package/dist/execution/service.d.ts +18 -16
- package/dist/execution/service.js +183 -252
- package/dist/execution/types.d.ts +25 -69
- package/dist/execution/types.js +2 -1
- package/dist/filter/intent.d.ts +8 -9
- package/dist/filter/intent.js +49 -10
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.js +4 -0
- package/dist/host/host.d.ts +74 -32
- package/dist/host/host.js +585 -336
- package/dist/host/routing.d.ts +15 -48
- package/dist/host/routing.js +90 -153
- package/dist/host/types.d.ts +50 -17
- package/dist/index.d.ts +21 -24
- package/dist/index.js +19 -23
- package/dist/input/advice.d.ts +5 -1
- package/dist/input/advice.js +163 -214
- package/dist/input/describe.d.ts +1 -1
- package/dist/input/flat-errors.d.ts +5 -6
- package/dist/input/flat-errors.js +6 -6
- package/dist/input/flat-materializer.js +42 -44
- package/dist/invocation/index.d.ts +2 -0
- package/dist/invocation/index.js +2 -0
- package/dist/invocation/policy.d.ts +157 -0
- package/dist/invocation/policy.js +231 -0
- package/dist/invocation/types.d.ts +172 -0
- package/dist/invocation/types.js +97 -0
- package/dist/ipc/diagnostic.d.ts +0 -12
- package/dist/ipc/diagnostic.js +0 -21
- package/dist/ipc/host.d.ts +4 -4
- package/dist/ipc/host.js +44 -47
- package/dist/ipc/index.d.ts +1 -1
- package/dist/ipc/index.js +1 -1
- package/dist/ipc/service.d.ts +40 -0
- package/dist/ipc/service.js +349 -0
- package/dist/ipc/types.d.ts +21 -15
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1 -0
- package/dist/node/module-loader.d.ts +63 -0
- package/dist/node/module-loader.js +112 -0
- package/dist/package/index.d.ts +23 -0
- package/dist/package/index.js +18 -0
- package/dist/{app/app.d.ts → package/runtime.d.ts} +41 -20
- package/dist/{app/app.js → package/runtime.js} +140 -95
- package/dist/{app → package}/static-index.js +58 -41
- package/dist/package/storage-view.d.ts +13 -0
- package/dist/package/storage-view.js +22 -0
- package/dist/{app → package}/types.d.ts +76 -57
- package/dist/package/types.js +22 -0
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js +1 -1
- package/dist/platform/node.d.ts +48 -0
- package/dist/platform/node.js +96 -0
- package/dist/platform/types.d.ts +0 -9
- package/dist/process/control-arbiter.d.ts +2 -1
- package/dist/process/control-arbiter.js +1 -2
- package/dist/process/driver.d.ts +9 -8
- package/dist/process/driver.js +11 -22
- package/dist/process/index.d.ts +2 -0
- package/dist/process/index.js +2 -0
- package/dist/process/managed-record.d.ts +2 -2
- package/dist/process/process-driver.d.ts +109 -0
- package/dist/process/process-driver.js +650 -0
- package/dist/process/process-executor.d.ts +47 -0
- package/dist/process/process-executor.js +296 -0
- package/dist/process/process-manager.js +48 -26
- package/dist/process/run-executor.js +12 -12
- package/dist/profile/client-actions.js +2 -2
- package/dist/profile/client-health.js +2 -2
- package/dist/profile/client-query.js +5 -6
- package/dist/profile/client-transport.d.ts +19 -17
- package/dist/profile/client-transport.js +47 -80
- package/dist/profile/client.d.ts +2 -2
- package/dist/profile/client.js +1 -1
- package/dist/profile/index.d.ts +1 -0
- package/dist/profile/index.js +1 -0
- package/dist/profile/manager.d.ts +5 -0
- package/dist/profile/manager.js +41 -6
- package/dist/project/closure.js +2 -2
- package/dist/project/index.d.ts +8 -1
- package/dist/project/index.js +10 -1
- package/dist/project/init.js +2 -2
- package/dist/project/loader.d.ts +1 -1
- package/dist/project/loader.js +26 -29
- package/dist/project/transactions.js +5 -10
- package/dist/project/types.d.ts +5 -0
- package/dist/registry/index.d.ts +3 -1
- package/dist/registry/index.js +2 -1
- package/dist/registry/lock.d.ts +28 -7
- package/dist/registry/lock.js +136 -16
- package/dist/registry/registry.d.ts +1 -5
- package/dist/registry/registry.js +1 -53
- package/dist/registry/scan.d.ts +0 -15
- package/dist/registry/scan.js +0 -59
- package/dist/registry/store.d.ts +42 -0
- package/dist/registry/store.js +43 -0
- package/dist/registry/types.d.ts +0 -24
- package/dist/runtime/action-collection.d.ts +1 -1
- package/dist/runtime/action-registry.d.ts +0 -4
- package/dist/runtime/action-registry.js +0 -7
- package/dist/runtime/context.js +5 -11
- package/dist/runtime/events.js +3 -6
- package/dist/runtime/identity.d.ts +22 -0
- package/dist/runtime/identity.js +10 -0
- package/dist/runtime/index.d.ts +1 -2
- package/dist/runtime/index.js +1 -2
- package/dist/runtime/module-loader.d.ts +2 -34
- package/dist/runtime/module-loader.js +1 -16
- package/dist/runtime/process.d.ts +6 -24
- package/dist/runtime/process.js +1 -118
- package/dist/runtime/run-persistence.js +2 -5
- package/dist/runtime/runner.d.ts +33 -93
- package/dist/runtime/runner.js +132 -364
- package/dist/runtime/standalone.d.ts +23 -8
- package/dist/runtime/standalone.js +100 -147
- package/dist/server/body.d.ts +16 -8
- package/dist/server/body.js +33 -16
- package/dist/server/dispatcher.d.ts +6 -10
- package/dist/server/dispatcher.js +24 -32
- package/dist/server/http-server.d.ts +87 -0
- package/dist/server/http-server.js +304 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +11 -0
- package/dist/server/mcp-endpoint.d.ts +61 -0
- package/dist/server/mcp-endpoint.js +118 -0
- package/dist/server/policy.d.ts +138 -0
- package/dist/server/policy.js +368 -0
- package/dist/server/routes/actions.js +114 -64
- package/dist/server/routes/common.d.ts +18 -33
- package/dist/server/routes/common.js +45 -65
- package/dist/server/routes/config.d.ts +1 -1
- package/dist/server/routes/config.js +55 -65
- package/dist/server/routes/doctor.d.ts +1 -1
- package/dist/server/routes/doctor.js +13 -21
- package/dist/server/routes/health.d.ts +1 -1
- package/dist/server/routes/health.js +4 -5
- package/dist/server/routes/info.d.ts +2 -2
- package/dist/server/routes/info.js +17 -24
- package/dist/server/routes/playbooks.js +23 -36
- package/dist/server/routes/runs.js +151 -141
- package/dist/server/routes/state.d.ts +1 -1
- package/dist/server/routes/state.js +64 -63
- package/dist/server/security.d.ts +11 -0
- package/dist/server/security.js +36 -19
- package/dist/server/server.d.ts +2 -2
- package/dist/server/server.js +224 -238
- package/dist/server/types.d.ts +68 -21
- package/dist/service/factory.d.ts +12 -0
- package/dist/service/factory.js +85 -0
- package/dist/{target → service}/index.d.ts +1 -1
- package/dist/{target → service}/index.js +1 -1
- package/dist/service/local.d.ts +27 -0
- package/dist/service/local.js +138 -0
- package/dist/{target → service}/remote-errors.d.ts +3 -6
- package/dist/service/remote-errors.js +38 -0
- package/dist/{target → service}/remote-polling.d.ts +2 -2
- package/dist/{target → service}/remote-polling.js +8 -8
- package/dist/service/remote.d.ts +34 -0
- package/dist/service/remote.js +634 -0
- package/dist/service/sse-stream.d.ts +24 -0
- package/dist/service/sse-stream.js +139 -0
- package/dist/service/types.d.ts +272 -0
- package/dist/service/types.js +32 -0
- package/dist/storage/data-dir-lock.js +10 -5
- package/dist/storage/driver.d.ts +2 -1
- package/dist/storage/driver.js +3 -52
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/lock-core.d.ts +2 -6
- package/dist/storage/lock-core.js +4 -17
- package/dist/storage/params.d.ts +2 -8
- package/dist/storage/params.js +2 -8
- package/dist/storage/sqlite-driver.d.ts +40 -0
- package/dist/storage/sqlite-driver.js +116 -0
- package/dist/storage/sqlite.js +21 -6
- package/dist/storage/types.d.ts +4 -4
- package/dist/storage/types.js +2 -2
- package/dist/utils/index.d.ts +53 -0
- package/dist/utils/index.js +99 -4
- package/dist/utils/process.d.ts +10 -0
- package/dist/utils/process.js +21 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +41 -3
- package/dist/app/index.d.ts +0 -2
- package/dist/app/index.js +0 -2
- package/dist/app/types.js +0 -1
- package/dist/catalog/action-index.d.ts +0 -9
- package/dist/catalog/action-index.js +0 -95
- package/dist/catalog/action-resolver.d.ts +0 -20
- package/dist/catalog/action-resolver.js +0 -95
- package/dist/catalog/location-registry.d.ts +0 -15
- package/dist/catalog/location-registry.js +0 -138
- package/dist/catalog/package-catalog.d.ts +0 -7
- package/dist/catalog/package-catalog.js +0 -77
- package/dist/catalog/types.d.ts +0 -68
- package/dist/catalog/types.js +0 -1
- package/dist/export/index.d.ts +0 -1
- package/dist/export/index.js +0 -1
- package/dist/export/templates.d.ts +0 -71
- package/dist/export/templates.js +0 -635
- package/dist/ipc/target.d.ts +0 -74
- package/dist/ipc/target.js +0 -365
- package/dist/platform/default.d.ts +0 -38
- package/dist/platform/default.js +0 -78
- package/dist/project/resolver.d.ts +0 -116
- package/dist/project/resolver.js +0 -376
- package/dist/registry/resolve.d.ts +0 -62
- package/dist/registry/resolve.js +0 -195
- package/dist/runtime/execution-manager.d.ts +0 -48
- package/dist/runtime/execution-manager.js +0 -66
- package/dist/runtime/package-runner-factory.d.ts +0 -82
- package/dist/runtime/package-runner-factory.js +0 -186
- package/dist/target/local.d.ts +0 -53
- package/dist/target/local.js +0 -349
- package/dist/target/remote-errors.js +0 -51
- package/dist/target/remote.d.ts +0 -86
- package/dist/target/remote.js +0 -818
- package/dist/target/target.d.ts +0 -6
- package/dist/target/target.js +0 -37
- package/dist/target/types.d.ts +0 -261
- package/dist/target/types.js +0 -35
- /package/dist/{app → package}/static-index.d.ts +0 -0
- /package/dist/{target → service}/sse-parser.d.ts +0 -0
- /package/dist/{target → service}/sse-parser.js +0 -0
package/dist/server/server.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
141
|
-
*
|
|
41
|
+
* 启动 ActionDock 原生轻量级 HTTP 服务端。
|
|
42
|
+
* 面向 ActionDockService 服务端口,负责中间件流转、认证拦截与路由分发。
|
|
142
43
|
*/
|
|
143
44
|
export async function startActionDockServer(options = {}) {
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
62
|
+
let serviceInstance = options.service;
|
|
63
|
+
// 若未传 service,服务端统一通过 createActionDock() 实例化,不再接受或处理外部传入的 Host 实体
|
|
64
|
+
if (!serviceInstance) {
|
|
165
65
|
const scanLinkedPackages = options.scanLinkedPackages ?? !projectRoot;
|
|
166
|
-
|
|
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
|
-
|
|
73
|
+
enableManagement: options.enableManagement,
|
|
174
74
|
});
|
|
175
75
|
}
|
|
176
|
-
if (!
|
|
177
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
target: targetInstance,
|
|
281
|
+
service: serviceInstance,
|
|
227
282
|
options,
|
|
283
|
+
activePolicy: effectivePolicy,
|
|
228
284
|
};
|
|
229
|
-
//
|
|
285
|
+
// 2. 健康检查路由(内部处理独立鉴权逻辑,使用 activePolicy.token)
|
|
230
286
|
const healthResponse = await handleHealthRoute(ctx);
|
|
231
287
|
if (healthResponse) {
|
|
232
288
|
return healthResponse;
|
|
233
289
|
}
|
|
234
|
-
//
|
|
235
|
-
if (
|
|
236
|
-
options.
|
|
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:
|
|
243
|
-
message:
|
|
296
|
+
code: NOT_FOUND,
|
|
297
|
+
message: `MCP endpoint is disabled for view '${activeView.name}'`,
|
|
244
298
|
},
|
|
245
|
-
},
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
//
|
|
314
|
-
if (!verifyBearerToken(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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} ${
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
},
|