@actiondock/cli 2.8.0 → 2.9.1
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 +3 -2
- package/bin/ad.js +52 -23
- package/dist/commands/run.js +31 -0
- package/dist/commands/runs.js +41 -5
- package/dist/commands/serve.js +122 -24
- package/dist/utils/server-launch.d.ts +55 -2
- package/dist/utils/server-launch.js +163 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -108,6 +108,7 @@ ad pack
|
|
|
108
108
|
|
|
109
109
|
- 严格目标定位:通过 `-P, --package <id|path>` 指定目标包。系统严格区分物理路径与注册表包标识符,若目标不存在则不向当前目录或父级目录隐式回退,直接以参数校验错误退出。
|
|
110
110
|
- 多目标检索契约:在执行多目标检索(如 `ad info` 或 `ad list`)时,若无任何匹配项,在机器模式下始终返回确定性空数组结构并以状态码 0 退出,不因空搜索产生异常中断。
|
|
111
|
+
- 服务端细粒度白名单:通过 `ad serve` 启动微服务时,支持通过 `-P, --package <package-id>` 与 `-A, --action <action-ref>` 分别指定包白名单与动作白名单(支持短名与全限定名,可多次指定或逗号分隔)。同时配置时取权限交集。未授权动作的详情调阅与执行均被阻断并返回 403 错误(错误码 `ACTION_FORBIDDEN`),历史运行记录与 MCP 协议工具暴露同步过滤。
|
|
111
112
|
|
|
112
113
|
### Action 执行入参契约
|
|
113
114
|
|
|
@@ -173,14 +174,14 @@ $data | ConvertTo-Json -Depth 100 | ad run complex-action --input-file -
|
|
|
173
174
|
| `ad playbook list` / `show` | 查看智能体操作规程 Playbook |
|
|
174
175
|
| `ad config list` / `get` / `set` | 管理包运行时配置与环境变量绑定 |
|
|
175
176
|
| `ad state list` / `get` / `set` / `delete` / `clear` | 查看与维护 SQLite 持久化状态 |
|
|
176
|
-
| `ad runs list` / `show` |
|
|
177
|
+
| `ad runs list` / `show` / `clear` | 查询任务执行历史与追踪记录,或按时间与数量策略清理历史数据 |
|
|
177
178
|
| `ad test` | 执行快速单元测试 |
|
|
178
179
|
| `ad build` | 构建 Node.js 目录交付产物(支持 `--vendor-deps` 与 `--archive`) |
|
|
179
180
|
| `ad pack` | 打包为标准 npm tarball(`.tgz`),支持 `--dry-run` |
|
|
180
181
|
| `ad export skill` | 导出 Agent Skill 技能包(支持 `--mode source` 与 `--mode node`) |
|
|
181
182
|
| `ad link` / `unlink` | 注册或注销工作区全局路由与符号链接 |
|
|
182
183
|
| `ad profile` | 管理远程执行节点凭证与环境配置 |
|
|
183
|
-
| `ad serve` | 启动远程 HTTP/HTTPS
|
|
184
|
+
| `ad serve` | 启动远程 HTTP/HTTPS 执行调度微服务(支持 `-P` 包白名单与 `-A` 动作白名单细粒度权限控制,原生支持 `--https` 自签名与生产证书) |
|
|
184
185
|
| `ad mcp` | 以 STDIO 或 HTTP 协议启动 MCP 服务 |
|
|
185
186
|
|
|
186
187
|
---
|
package/bin/ad.js
CHANGED
|
@@ -1,42 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const isBun = typeof process.versions.bun !== "undefined";
|
|
9
|
+
const hasTsx =
|
|
10
|
+
process.execArgv.some((arg, i) => arg === "--import" && process.execArgv[i + 1]?.includes("tsx")) ||
|
|
11
|
+
process.execArgv.some((arg) => arg.includes("tsx"));
|
|
9
12
|
|
|
10
|
-
if (
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
process.execArgv.some((arg, i) => arg === "--import" && process.execArgv[i + 1]?.includes("tsx")) ||
|
|
17
|
-
process.execArgv.some((arg) => arg.includes("tsx"));
|
|
18
|
-
|
|
19
|
-
if (!isBun && !hasTsx) {
|
|
20
|
-
const require = createRequire(import.meta.url);
|
|
21
|
-
let tsxSpecifier = null;
|
|
13
|
+
if (!isBun && !hasTsx) {
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
let tsxSpecifier = null;
|
|
16
|
+
try {
|
|
17
|
+
tsxSpecifier = pathToFileURL(require.resolve("tsx", { paths: [import.meta.dirname, process.cwd()] })).href;
|
|
18
|
+
} catch {
|
|
22
19
|
try {
|
|
23
20
|
tsxSpecifier = pathToFileURL(require.resolve("tsx")).href;
|
|
24
21
|
} catch {
|
|
25
22
|
// 未检测到 tsx 模块
|
|
26
23
|
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (tsxSpecifier) {
|
|
27
|
+
const child = spawn(
|
|
28
|
+
process.execPath,
|
|
29
|
+
["--import", tsxSpecifier, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
|
|
30
|
+
{
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const forwardSignal = (sig) => {
|
|
36
|
+
if (child.pid && !child.killed) {
|
|
37
|
+
try {
|
|
38
|
+
child.kill(sig);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
44
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
45
|
+
process.on("SIGHUP", () => forwardSignal("SIGHUP"));
|
|
27
46
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
await new Promise((resolvePromise) => {
|
|
48
|
+
child.on("exit", (code, signal) => {
|
|
49
|
+
if (signal) {
|
|
50
|
+
process.removeListener("SIGTERM", forwardSignal);
|
|
51
|
+
process.removeListener("SIGINT", forwardSignal);
|
|
52
|
+
process.removeListener("SIGHUP", forwardSignal);
|
|
53
|
+
process.kill(process.pid, signal);
|
|
54
|
+
} else {
|
|
55
|
+
process.exit(code ?? 0);
|
|
34
56
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
57
|
+
resolvePromise();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
38
60
|
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const distEntry = resolve(import.meta.dirname, "../dist/index.js");
|
|
39
64
|
|
|
65
|
+
if (existsSync(distEntry)) {
|
|
66
|
+
const { runCliProcess } = await import(pathToFileURL(distEntry).href);
|
|
67
|
+
await runCliProcess(process.argv);
|
|
68
|
+
} else {
|
|
40
69
|
const { runCliProcess } = await import("../src/index.ts");
|
|
41
70
|
await runCliProcess(process.argv);
|
|
42
71
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -216,6 +216,36 @@ export async function executeAction(id, options, context, flatArgs, control) {
|
|
|
216
216
|
throw err;
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
+
const RUN_HELP_AFTER_TEXT = `
|
|
220
|
+
Introspection & Guidance:
|
|
221
|
+
If uncertain about an action's inputs or schemas:
|
|
222
|
+
ad describe <id> Inspect action schema, required fields, and assignment examples
|
|
223
|
+
If handling multi-step tasks or complex workflows:
|
|
224
|
+
ad playbook list List available Standard Operating Procedures (SOPs)
|
|
225
|
+
ad playbook show <id> View step-by-step procedure and execution rules
|
|
226
|
+
|
|
227
|
+
Flat Input Syntax & Examples:
|
|
228
|
+
Standard invocation syntax:
|
|
229
|
+
ad run <id> [control-options] [-- <assignments...>]
|
|
230
|
+
|
|
231
|
+
Flat parameter assignments:
|
|
232
|
+
# String assignment (= keeps raw string, no type coercion)
|
|
233
|
+
ad run <id> -- key="value" path="src/index.ts"
|
|
234
|
+
|
|
235
|
+
# Typed / JSON assignment (:= parses numbers, booleans, arrays, objects)
|
|
236
|
+
ad run <id> -- count:=10 enabled:=true timeoutMs:=5000
|
|
237
|
+
|
|
238
|
+
# Sequential array indexing (zero-indexed, consecutive)
|
|
239
|
+
ad run <id> -- paths.0="src" paths.1="docs"
|
|
240
|
+
# Or direct JSON array
|
|
241
|
+
ad run <id> -- paths:='["src", "docs"]'
|
|
242
|
+
|
|
243
|
+
# Nested object properties
|
|
244
|
+
ad run <id> -- user.name="alice" user.age:=30
|
|
245
|
+
|
|
246
|
+
Complex or multiline inputs:
|
|
247
|
+
# Pass via JSON file with --input-file to avoid shell escaping issues
|
|
248
|
+
ad run <id> --input-file input.json`;
|
|
219
249
|
/**
|
|
220
250
|
* 挂载 run 子命令至指定 Commander 节点。
|
|
221
251
|
*
|
|
@@ -236,6 +266,7 @@ export function attachRunCommand(parent, context) {
|
|
|
236
266
|
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
237
267
|
.option("--data-dir <path>", "Custom database directory")
|
|
238
268
|
.option("--json", "Output as JSON")
|
|
269
|
+
.addHelpText("after", RUN_HELP_AFTER_TEXT)
|
|
239
270
|
.action(async (id, params, rawOptions, cmd) => {
|
|
240
271
|
let flatArgs;
|
|
241
272
|
let effectiveRawOptions = rawOptions;
|
package/dist/commands/runs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
|
|
1
|
+
import { findProjectRoot, loadProjectConfig, parseDuration, } from "@actiondock/core";
|
|
2
2
|
import { filterWithFallbackInfo, } from "@actiondock/core/project";
|
|
3
3
|
import { listLinkedPackages, resolvePackageRoot, } from "@actiondock/core/registry";
|
|
4
4
|
import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, packageNotFoundError, } from "../errors.js";
|
|
@@ -177,7 +177,10 @@ export function registerRunsCommands(program, context) {
|
|
|
177
177
|
.command("clear")
|
|
178
178
|
.description("Clear execution run records")
|
|
179
179
|
.option("-P, --package <id>", "Target package ID or path")
|
|
180
|
-
.option("-a, --action <actionId>", "Filter by action ID")
|
|
180
|
+
.option("-a, --action <actionId>", "Filter by action ID")
|
|
181
|
+
.option("--status <status>", "Filter by run status (e.g. success, failed)")
|
|
182
|
+
.option("--older-than <duration>", "Only clear runs older than specified duration (e.g. 14d, 7d, 24h, 30m)")
|
|
183
|
+
.option("--keep <count>", "Keep the most recent N runs and clear older ones"))
|
|
181
184
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
182
185
|
.option("--json", "Output as JSON")
|
|
183
186
|
.action(async (rawOptions, cmd) => {
|
|
@@ -193,16 +196,49 @@ export function registerRunsCommands(program, context) {
|
|
|
193
196
|
// 清单损坏时保留原始 -P 参数作为包标识
|
|
194
197
|
}
|
|
195
198
|
}
|
|
199
|
+
let olderThanMs;
|
|
200
|
+
if (options.olderThan) {
|
|
201
|
+
try {
|
|
202
|
+
olderThanMs = parseDuration(options.olderThan);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
throw new ArgumentError(`Invalid --older-than argument: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let keep;
|
|
209
|
+
if (options.keep !== undefined) {
|
|
210
|
+
const parsed = parseInt(String(options.keep), 10);
|
|
211
|
+
if (isNaN(parsed) || parsed < 0) {
|
|
212
|
+
throw new ArgumentError("Invalid --keep argument: must be a non-negative integer");
|
|
213
|
+
}
|
|
214
|
+
keep = parsed;
|
|
215
|
+
}
|
|
196
216
|
await withService(options, context, async (service, resolved) => {
|
|
197
217
|
const count = service.runs.clear
|
|
198
|
-
? await service.runs.clear({
|
|
218
|
+
? await service.runs.clear({
|
|
219
|
+
packageId,
|
|
220
|
+
actionId: options.action,
|
|
221
|
+
status: options.status,
|
|
222
|
+
olderThanMs,
|
|
223
|
+
keep,
|
|
224
|
+
})
|
|
199
225
|
: 0;
|
|
226
|
+
const filters = [];
|
|
227
|
+
if (options.action)
|
|
228
|
+
filters.push(`action='${options.action}'`);
|
|
229
|
+
if (options.status)
|
|
230
|
+
filters.push(`status='${options.status}'`);
|
|
231
|
+
if (options.olderThan)
|
|
232
|
+
filters.push(`older than ${options.olderThan}`);
|
|
233
|
+
if (keep !== undefined)
|
|
234
|
+
filters.push(`keeping newest ${keep}`);
|
|
235
|
+
const filterDesc = filters.length > 0 ? ` (${filters.join(", ")})` : "";
|
|
200
236
|
const payload = { ok: true, clearedCount: count };
|
|
201
237
|
renderResult(payload, {
|
|
202
238
|
json: options.json,
|
|
203
239
|
humanFormatter: () => resolved.type === "remote"
|
|
204
|
-
? `Cleared ${count} execution run(s) on remote server.`
|
|
205
|
-
: `Cleared ${count} execution run(s) in package '${packageId}'.`,
|
|
240
|
+
? `Cleared ${count} execution run(s) on remote server${filterDesc}.`
|
|
241
|
+
: `Cleared ${count} execution run(s) in package '${packageId}'${filterDesc}.`,
|
|
206
242
|
context,
|
|
207
243
|
});
|
|
208
244
|
}, { localRoot: targetPackageRoot });
|
package/dist/commands/serve.js
CHANGED
|
@@ -2,10 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { createActionDock, createNodePlatform, findProjectRoot, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
|
|
4
4
|
import { resolvePackageRoot } from "@actiondock/core/registry";
|
|
5
|
+
import { parseActionRef } from "@actiondock/core/graph";
|
|
5
6
|
import { formatHostForUrl, } from "@actiondock/core/server";
|
|
6
7
|
import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
|
|
7
8
|
import { writeStderr, writeStdout } from "../renderer.js";
|
|
8
|
-
import { ensureSelfSignedCertificate, getEffectiveOptions, normalizeCorsOrigins, registerStopSignalHandler, printServerBanner, resolveMaxBodyBytes, resolveServerHost, resolveServerPort, resolveServerToken, parseListOption, } from "../utils/index.js";
|
|
9
|
+
import { ensureSelfSignedCertificate, getEffectiveOptions, loadViewsFromFile, mergeServerViews, normalizeCorsOrigins, registerStopSignalHandler, printServerBanner, resolveMaxBodyBytes, resolveServerHost, resolveServerPort, resolveServerToken, parseListOption, } from "../utils/index.js";
|
|
9
10
|
/**
|
|
10
11
|
* 注册 serve HTTP 服务启动命令。
|
|
11
12
|
*
|
|
@@ -33,6 +34,9 @@ export function registerServeCommand(program, context) {
|
|
|
33
34
|
.option("--tls-passphrase <passphrase>", "Passphrase for TLS private key (or set ACTIONDOCK_TLS_PASSPHRASE)")
|
|
34
35
|
.option("-d, --dir <path>", "Project root directory (default: current working directory)")
|
|
35
36
|
.option("-P, --package <package-id>", "Specific package ID(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
|
|
37
|
+
.option("-A, --action <action-ref>", "Specific action ref(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
|
|
38
|
+
.option("--views-file <path>", "Path to JSON file configuring virtual projection views")
|
|
39
|
+
.option("--views <json>", "JSON string configuring virtual projection views")
|
|
36
40
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
37
41
|
.action(async (rawOptions, cmd) => {
|
|
38
42
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
@@ -87,10 +91,42 @@ export function registerServeCommand(program, context) {
|
|
|
87
91
|
const projectRoot = options.dir
|
|
88
92
|
? resolve(options.dir)
|
|
89
93
|
: findProjectRoot(process.cwd());
|
|
94
|
+
let projectConfig;
|
|
95
|
+
let projectName = "Global Registry Mode";
|
|
96
|
+
if (projectRoot) {
|
|
97
|
+
try {
|
|
98
|
+
projectConfig = loadProjectConfig(projectRoot);
|
|
99
|
+
projectName = `${projectConfig.name} (${projectConfig.id})`;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// 降级处理
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const fileViews = options.viewsFile
|
|
106
|
+
? loadViewsFromFile(options.viewsFile)
|
|
107
|
+
: undefined;
|
|
108
|
+
let cliViews;
|
|
109
|
+
if (options.views) {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = typeof options.views === "string" ? JSON.parse(options.views) : options.views;
|
|
112
|
+
cliViews =
|
|
113
|
+
parsed && typeof parsed === "object" && "views" in parsed && (typeof parsed.views === "object" || Array.isArray(parsed.views))
|
|
114
|
+
? parsed.views
|
|
115
|
+
: parsed;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
throw new ArgumentError(`Failed to parse --views option: ${msg}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const configViews = projectConfig?.server?.views;
|
|
123
|
+
const viewsConfig = mergeServerViews(mergeServerViews(configViews, fileViews), cliViews);
|
|
90
124
|
const rawPackages = options.package || [];
|
|
91
125
|
const packageAllowlist = rawPackages.length > 0 ? [] : undefined;
|
|
92
126
|
const resolvedPackageIds = [];
|
|
93
127
|
const explicitPackages = [];
|
|
128
|
+
const rawActions = options.action || [];
|
|
129
|
+
const actionAllowlist = rawActions.length > 0 ? [...rawActions] : undefined;
|
|
94
130
|
if (rawPackages.length > 0) {
|
|
95
131
|
for (const pkgId of rawPackages) {
|
|
96
132
|
const root = resolvePackageRoot(pkgId, projectRoot || undefined, context?.customHome);
|
|
@@ -123,14 +159,64 @@ export function registerServeCommand(program, context) {
|
|
|
123
159
|
}
|
|
124
160
|
}
|
|
125
161
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
if (rawActions.length > 0) {
|
|
163
|
+
for (const actRef of rawActions) {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = parseActionRef(actRef);
|
|
166
|
+
if (parsed.packageId) {
|
|
167
|
+
const root = resolvePackageRoot(parsed.packageId, projectRoot || undefined, context?.customHome);
|
|
168
|
+
if (root) {
|
|
169
|
+
if (!projectRoot || root !== projectRoot) {
|
|
170
|
+
if (!explicitPackages.some((p) => p.packageRoot === root)) {
|
|
171
|
+
explicitPackages.push({ packageRoot: root });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
writeStderr(`Warning: Package '${parsed.packageId}' specified in action '${actRef}' was not found.`, context);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// 忽略非标准或短引用形态
|
|
182
|
+
}
|
|
131
183
|
}
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
}
|
|
185
|
+
if (viewsConfig) {
|
|
186
|
+
const viewItems = Array.isArray(viewsConfig) ? viewsConfig : Object.values(viewsConfig);
|
|
187
|
+
for (const item of viewItems) {
|
|
188
|
+
if (!item || typeof item !== "object")
|
|
189
|
+
continue;
|
|
190
|
+
if (Array.isArray(item.packageAllowlist)) {
|
|
191
|
+
for (const pkgId of item.packageAllowlist) {
|
|
192
|
+
if (typeof pkgId === "string") {
|
|
193
|
+
const root = resolvePackageRoot(pkgId, projectRoot || undefined, context?.customHome);
|
|
194
|
+
if (root && (!projectRoot || root !== projectRoot)) {
|
|
195
|
+
if (!explicitPackages.some((p) => p.packageRoot === root)) {
|
|
196
|
+
explicitPackages.push({ packageRoot: root });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(item.actionAllowlist)) {
|
|
203
|
+
for (const actRef of item.actionAllowlist) {
|
|
204
|
+
if (typeof actRef === "string") {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = parseActionRef(actRef);
|
|
207
|
+
if (parsed.packageId) {
|
|
208
|
+
const root = resolvePackageRoot(parsed.packageId, projectRoot || undefined, context?.customHome);
|
|
209
|
+
if (root && (!projectRoot || root !== projectRoot)) {
|
|
210
|
+
if (!explicitPackages.some((p) => p.packageRoot === root)) {
|
|
211
|
+
explicitPackages.push({ packageRoot: root });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch { }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
134
220
|
}
|
|
135
221
|
}
|
|
136
222
|
const platform = createNodePlatform({
|
|
@@ -145,9 +231,9 @@ export function registerServeCommand(program, context) {
|
|
|
145
231
|
customHome: context?.customHome,
|
|
146
232
|
dataDir: options.dataDir || context?.dataDir,
|
|
147
233
|
platform,
|
|
148
|
-
scanLinkedPackages: !projectRoot || rawPackages.length > 0,
|
|
234
|
+
scanLinkedPackages: !projectRoot || rawPackages.length > 0 || rawActions.length > 0 || Boolean(viewsConfig),
|
|
149
235
|
});
|
|
150
|
-
let
|
|
236
|
+
let mcpHandlerFactory;
|
|
151
237
|
const enableMcp = options.mcp !== false;
|
|
152
238
|
if (enableMcp) {
|
|
153
239
|
// MCP 处理器创建失败时直接终止启动,避免横幅宣称不存在的端点
|
|
@@ -156,19 +242,22 @@ export function registerServeCommand(program, context) {
|
|
|
156
242
|
import("@actiondock/mcp"),
|
|
157
243
|
import("@modelcontextprotocol/server"),
|
|
158
244
|
]);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
245
|
+
mcpHandlerFactory = (view) => {
|
|
246
|
+
const handler = createMcpHandler(() => {
|
|
247
|
+
return createActionDockMcpServer({
|
|
248
|
+
service,
|
|
249
|
+
packageAllowlist: view.packageAllowlist,
|
|
250
|
+
packageIds: view.packageAllowlist,
|
|
251
|
+
actionAllowlist: view.actionAllowlist,
|
|
252
|
+
});
|
|
253
|
+
}, {
|
|
254
|
+
onerror: (err) => {
|
|
255
|
+
writeStderr(`[MCP HTTP Error] ${err?.message || String(err)}`, context);
|
|
256
|
+
},
|
|
164
257
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
mcpHandler = async (req) => {
|
|
171
|
-
return handler.fetch(req);
|
|
258
|
+
return async (req) => {
|
|
259
|
+
return handler.fetch(req);
|
|
260
|
+
};
|
|
172
261
|
};
|
|
173
262
|
}
|
|
174
263
|
catch (err) {
|
|
@@ -188,22 +277,31 @@ export function registerServeCommand(program, context) {
|
|
|
188
277
|
exposeDebugInfo,
|
|
189
278
|
enableMcp,
|
|
190
279
|
enableManagement,
|
|
191
|
-
|
|
280
|
+
mcpHandlerFactory,
|
|
192
281
|
tls,
|
|
193
282
|
projectRoot: projectRoot || undefined,
|
|
194
283
|
service,
|
|
195
284
|
packageAllowlist,
|
|
285
|
+
actionAllowlist,
|
|
286
|
+
views: viewsConfig,
|
|
196
287
|
});
|
|
197
288
|
const displayHost = formatHostForUrl(host);
|
|
198
289
|
const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
|
|
199
290
|
const scheme = tls ? "https" : "http";
|
|
200
|
-
printServerBanner(`ActionDock 2.0 HTTP Runner Server`, scheme, displayHost, server.port, context
|
|
291
|
+
printServerBanner(`ActionDock 2.0 HTTP Runner Server`, scheme, displayHost, server.port, context, {
|
|
292
|
+
views: viewsConfig,
|
|
293
|
+
endpointHost: actualEndpointHost,
|
|
294
|
+
enableMcp,
|
|
295
|
+
});
|
|
201
296
|
if (resolvedPackageIds.length > 0) {
|
|
202
297
|
writeStdout(` * Packages: ${resolvedPackageIds.join(", ")}`, context);
|
|
203
298
|
}
|
|
204
299
|
else {
|
|
205
300
|
writeStdout(` * Project: ${projectName}`, context);
|
|
206
301
|
}
|
|
302
|
+
if (actionAllowlist && actionAllowlist.length > 0) {
|
|
303
|
+
writeStdout(` * Actions: ${actionAllowlist.join(", ")}`, context);
|
|
304
|
+
}
|
|
207
305
|
if (projectRoot && exposeDebugInfo) {
|
|
208
306
|
writeStdout(` * Root Path: ${projectRoot}`, context);
|
|
209
307
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServerViewOptions } from "@actiondock/core/server";
|
|
1
2
|
import type { CliContext } from "../types.js";
|
|
2
3
|
/**
|
|
3
4
|
* 服务类命令(ad serve、ad mcp serve、ad mcp、ad run)的共享启动脚手架(单一事实源)。
|
|
@@ -51,11 +52,63 @@ export declare function registerStopSignalHandler(server: {
|
|
|
51
52
|
stop(): Promise<unknown> | unknown;
|
|
52
53
|
}, serverName: string, context?: CliContext): void;
|
|
53
54
|
/**
|
|
54
|
-
*
|
|
55
|
+
* 虚拟视图简要展示信息。
|
|
56
|
+
*/
|
|
57
|
+
export interface ServerViewSummary {
|
|
58
|
+
name: string;
|
|
59
|
+
enableMcp?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 打印服务横幅的可选配置参数。
|
|
63
|
+
*/
|
|
64
|
+
export interface PrintServerBannerOptions {
|
|
65
|
+
views?: Record<string, ServerViewOptions> | ServerViewOptions[];
|
|
66
|
+
endpointHost?: string;
|
|
67
|
+
enableMcp?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 从指定文件路径加载虚拟视图配置字典。
|
|
71
|
+
*
|
|
72
|
+
* @param filePath 视图配置文件路径
|
|
73
|
+
* @returns 解析后的视图字典或数组
|
|
74
|
+
*/
|
|
75
|
+
export declare function loadViewsFromFile(filePath: string): Record<string, ServerViewOptions> | ServerViewOptions[];
|
|
76
|
+
/**
|
|
77
|
+
* 合并项目配置与独立配置文件中的虚拟视图定义。
|
|
78
|
+
* 命令行 --views-file 优先级高于项目配置文件 actiondock.json 中的 server.views。
|
|
79
|
+
*
|
|
80
|
+
* @param configViews 项目配置中的 views
|
|
81
|
+
* @param fileViews 独立配置文件中的 views
|
|
82
|
+
*/
|
|
83
|
+
export declare function mergeServerViews(configViews?: Record<string, ServerViewOptions> | ServerViewOptions[], fileViews?: Record<string, ServerViewOptions> | ServerViewOptions[]): Record<string, ServerViewOptions> | ServerViewOptions[] | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* 从服务视图配置中提取所有命名空间视图列表(排除 default 默认视图)。
|
|
86
|
+
*
|
|
87
|
+
* @param views 视图字典或数组
|
|
88
|
+
* @returns 命名空间视图简要信息列表
|
|
89
|
+
*/
|
|
90
|
+
export declare function extractNamespacedViews(views?: Record<string, ServerViewOptions> | ServerViewOptions[]): ServerViewSummary[];
|
|
91
|
+
/**
|
|
92
|
+
* 打印服务命名空间虚拟视图列表。
|
|
93
|
+
*
|
|
94
|
+
* @param views 视图配置字典或数组
|
|
95
|
+
* @param scheme 访问协议(http 或 https)
|
|
96
|
+
* @param endpointHost 面向调用的端点主机地址
|
|
97
|
+
* @param port 监听端口
|
|
98
|
+
* @param context CLI 上下文
|
|
99
|
+
* @param options 额外选项(如全局 enableMcp)
|
|
100
|
+
*/
|
|
101
|
+
export declare function printServerViews(views: Record<string, ServerViewOptions> | ServerViewOptions[] | undefined, scheme: string, endpointHost: string, port: number, context?: CliContext, options?: {
|
|
102
|
+
enableMcp?: boolean;
|
|
103
|
+
}): void;
|
|
104
|
+
/**
|
|
105
|
+
* 打印服务就绪横幅(顶部固定段,支持展示命名空间视图)。
|
|
55
106
|
*
|
|
56
107
|
* @param title 横幅主体标题(如 ActionDock 2.0 HTTP Runner Server)
|
|
57
108
|
* @param scheme 端点协议(http 或 https)
|
|
58
109
|
* @param displayHost 面向展示的主机地址
|
|
59
110
|
* @param port 实际监听端口
|
|
111
|
+
* @param contextOrOptions 命令行上下文或横幅配置选项
|
|
112
|
+
* @param maybeOptions 横幅配置选项(包含 views 等信息)
|
|
60
113
|
*/
|
|
61
|
-
export declare function printServerBanner(title: string, scheme: string, displayHost: string, port: number,
|
|
114
|
+
export declare function printServerBanner(title: string, scheme: string, displayHost: string, port: number, contextOrOptions?: CliContext | PrintServerBannerOptions, maybeOptions?: PrintServerBannerOptions): void;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { parseDuration, } from "@actiondock/core/project";
|
|
2
4
|
import { ArgumentError } from "../errors.js";
|
|
3
5
|
import { writeStderr, writeStdout } from "../renderer.js";
|
|
@@ -99,16 +101,175 @@ export function registerStopSignalHandler(server, serverName, context) {
|
|
|
99
101
|
process.once("SIGTERM", stopSignalHandler);
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
102
|
-
*
|
|
104
|
+
* 从指定文件路径加载虚拟视图配置字典。
|
|
105
|
+
*
|
|
106
|
+
* @param filePath 视图配置文件路径
|
|
107
|
+
* @returns 解析后的视图字典或数组
|
|
108
|
+
*/
|
|
109
|
+
export function loadViewsFromFile(filePath) {
|
|
110
|
+
const resolvedPath = resolve(filePath);
|
|
111
|
+
if (!existsSync(resolvedPath)) {
|
|
112
|
+
throw new ArgumentError(`Views file not found: ${resolvedPath}`);
|
|
113
|
+
}
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
const content = readFileSync(resolvedPath, "utf-8");
|
|
117
|
+
parsed = JSON.parse(content);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
throw new ArgumentError(`Failed to read views file: ${msg}`);
|
|
122
|
+
}
|
|
123
|
+
if (!parsed || typeof parsed !== "object") {
|
|
124
|
+
throw new ArgumentError("Invalid views file: expected a JSON object or array");
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(parsed)) {
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
const obj = parsed;
|
|
130
|
+
if (obj.views && typeof obj.views === "object") {
|
|
131
|
+
return obj.views;
|
|
132
|
+
}
|
|
133
|
+
if (obj.server && typeof obj.server === "object" && obj.server.views) {
|
|
134
|
+
const srv = obj.server;
|
|
135
|
+
return srv.views;
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 合并项目配置与独立配置文件中的虚拟视图定义。
|
|
141
|
+
* 命令行 --views-file 优先级高于项目配置文件 actiondock.json 中的 server.views。
|
|
142
|
+
*
|
|
143
|
+
* @param configViews 项目配置中的 views
|
|
144
|
+
* @param fileViews 独立配置文件中的 views
|
|
145
|
+
*/
|
|
146
|
+
export function mergeServerViews(configViews, fileViews) {
|
|
147
|
+
if (!configViews && !fileViews) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
if (!configViews) {
|
|
151
|
+
return fileViews;
|
|
152
|
+
}
|
|
153
|
+
if (!fileViews) {
|
|
154
|
+
return configViews;
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(configViews) && !Array.isArray(fileViews)) {
|
|
157
|
+
return { ...configViews, ...fileViews };
|
|
158
|
+
}
|
|
159
|
+
const merged = {};
|
|
160
|
+
const addEntry = (item, fallbackKey) => {
|
|
161
|
+
const key = item.name?.trim() || fallbackKey;
|
|
162
|
+
merged[key] = { ...item, name: key };
|
|
163
|
+
};
|
|
164
|
+
if (Array.isArray(configViews)) {
|
|
165
|
+
configViews.forEach((item, index) => addEntry(item, item.name || `view_${index}`));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
Object.entries(configViews).forEach(([k, v]) => addEntry(v, k));
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(fileViews)) {
|
|
171
|
+
fileViews.forEach((item, index) => addEntry(item, item.name || `view_${index}`));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
Object.entries(fileViews).forEach(([k, v]) => addEntry(v, k));
|
|
175
|
+
}
|
|
176
|
+
return merged;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 从服务视图配置中提取所有命名空间视图列表(排除 default 默认视图)。
|
|
180
|
+
*
|
|
181
|
+
* @param views 视图字典或数组
|
|
182
|
+
* @returns 命名空间视图简要信息列表
|
|
183
|
+
*/
|
|
184
|
+
export function extractNamespacedViews(views) {
|
|
185
|
+
if (!views)
|
|
186
|
+
return [];
|
|
187
|
+
const result = [];
|
|
188
|
+
if (Array.isArray(views)) {
|
|
189
|
+
for (let i = 0; i < views.length; i++) {
|
|
190
|
+
const item = views[i];
|
|
191
|
+
if (!item)
|
|
192
|
+
continue;
|
|
193
|
+
const name = item.name?.trim() || `view_${i}`;
|
|
194
|
+
if (name === "default")
|
|
195
|
+
continue;
|
|
196
|
+
result.push({
|
|
197
|
+
name,
|
|
198
|
+
enableMcp: item.enableMcp,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (typeof views === "object") {
|
|
203
|
+
for (const [key, item] of Object.entries(views)) {
|
|
204
|
+
if (!item)
|
|
205
|
+
continue;
|
|
206
|
+
const name = item.name?.trim() || key.trim();
|
|
207
|
+
if (name === "default")
|
|
208
|
+
continue;
|
|
209
|
+
result.push({
|
|
210
|
+
name,
|
|
211
|
+
enableMcp: item.enableMcp,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 打印服务命名空间虚拟视图列表。
|
|
219
|
+
*
|
|
220
|
+
* @param views 视图配置字典或数组
|
|
221
|
+
* @param scheme 访问协议(http 或 https)
|
|
222
|
+
* @param endpointHost 面向调用的端点主机地址
|
|
223
|
+
* @param port 监听端口
|
|
224
|
+
* @param context CLI 上下文
|
|
225
|
+
* @param options 额外选项(如全局 enableMcp)
|
|
226
|
+
*/
|
|
227
|
+
export function printServerViews(views, scheme, endpointHost, port, context, options) {
|
|
228
|
+
const namespaced = extractNamespacedViews(views);
|
|
229
|
+
if (namespaced.length === 0) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
writeStdout(` * Views:`, context);
|
|
233
|
+
for (const v of namespaced) {
|
|
234
|
+
const encodedName = encodeURIComponent(v.name);
|
|
235
|
+
writeStdout(` - ${v.name}:`, context);
|
|
236
|
+
writeStdout(` - HTTP Root: ${scheme}://${endpointHost}:${port}/views/${encodedName}`, context);
|
|
237
|
+
const mcpEnabled = options?.enableMcp !== false && v.enableMcp !== false;
|
|
238
|
+
const mcpDesc = mcpEnabled
|
|
239
|
+
? `${scheme}://${endpointHost}:${port}/views/${encodedName}/mcp`
|
|
240
|
+
: "Disabled";
|
|
241
|
+
writeStdout(` - MCP Endpoint: ${mcpDesc}`, context);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 打印服务就绪横幅(顶部固定段,支持展示命名空间视图)。
|
|
103
246
|
*
|
|
104
247
|
* @param title 横幅主体标题(如 ActionDock 2.0 HTTP Runner Server)
|
|
105
248
|
* @param scheme 端点协议(http 或 https)
|
|
106
249
|
* @param displayHost 面向展示的主机地址
|
|
107
250
|
* @param port 实际监听端口
|
|
251
|
+
* @param contextOrOptions 命令行上下文或横幅配置选项
|
|
252
|
+
* @param maybeOptions 横幅配置选项(包含 views 等信息)
|
|
108
253
|
*/
|
|
109
|
-
export function printServerBanner(title, scheme, displayHost, port,
|
|
254
|
+
export function printServerBanner(title, scheme, displayHost, port, contextOrOptions, maybeOptions) {
|
|
255
|
+
let context;
|
|
256
|
+
let options;
|
|
257
|
+
if (contextOrOptions &&
|
|
258
|
+
("views" in contextOrOptions || "endpointHost" in contextOrOptions || "enableMcp" in contextOrOptions)) {
|
|
259
|
+
options = contextOrOptions;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
context = contextOrOptions;
|
|
263
|
+
options = maybeOptions;
|
|
264
|
+
}
|
|
110
265
|
writeStdout(`\n======================================================`, context);
|
|
111
266
|
writeStdout(` ${title}`, context);
|
|
112
267
|
writeStdout(`======================================================`, context);
|
|
113
268
|
writeStdout(` * Listening on: ${scheme}://${displayHost}:${port}`, context);
|
|
269
|
+
if (options?.views) {
|
|
270
|
+
const endpointHost = options.endpointHost || displayHost;
|
|
271
|
+
printServerViews(options.views, scheme, endpointHost, port, context, {
|
|
272
|
+
enableMcp: options.enableMcp,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
114
275
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "ActionDock CLI facade for developing, testing, building, and exporting standalone Agent Actions and Skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"test": "node ../../scripts/run-tests.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@actiondock/builder": "^2.
|
|
38
|
-
"@actiondock/core": "^2.
|
|
39
|
-
"@actiondock/mcp": "^2.
|
|
40
|
-
"@actiondock/sdk": "^2.
|
|
37
|
+
"@actiondock/builder": "^2.9.1",
|
|
38
|
+
"@actiondock/core": "^2.9.1",
|
|
39
|
+
"@actiondock/mcp": "^2.9.1",
|
|
40
|
+
"@actiondock/sdk": "^2.9.1",
|
|
41
41
|
"commander": "^13.1.0",
|
|
42
42
|
"selfsigned": "^5.5.0"
|
|
43
43
|
},
|