@actiondock/core 2.0.12 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -30
- package/dist/app/app.d.ts +73 -0
- package/dist/app/app.js +700 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/types.d.ts +283 -0
- package/dist/app/types.js +1 -0
- package/dist/catalog/action-index.d.ts +1 -1
- package/dist/catalog/action-index.js +93 -0
- package/dist/catalog/action-resolver.d.ts +2 -2
- package/dist/catalog/action-resolver.js +95 -0
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +5 -0
- package/dist/catalog/location-registry.d.ts +5 -5
- package/dist/catalog/location-registry.js +138 -0
- package/dist/catalog/package-catalog.d.ts +2 -2
- package/dist/catalog/package-catalog.js +76 -0
- package/dist/catalog/types.d.ts +2 -2
- package/dist/catalog/types.js +1 -0
- package/dist/doctor/doctor.d.ts +2 -1
- package/dist/doctor/doctor.js +613 -0
- package/dist/doctor/index.d.ts +2 -2
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/types.js +1 -0
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +123 -0
- package/dist/execution/index.d.ts +2 -2
- package/dist/execution/index.js +2 -0
- package/dist/execution/service.d.ts +46 -41
- package/dist/execution/service.js +633 -0
- package/dist/execution/types.d.ts +59 -2
- package/dist/execution/types.js +1 -0
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +1 -0
- package/dist/export/templates.d.ts +45 -7
- package/dist/export/templates.js +621 -0
- package/dist/filter/index.d.ts +1 -1
- package/dist/filter/index.js +1 -0
- package/dist/filter/intent.js +123 -0
- package/dist/host/host.d.ts +55 -0
- package/dist/host/host.js +740 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.js +2 -0
- package/dist/host/types.d.ts +81 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +21 -16
- package/dist/index.js +20 -8458
- package/dist/ipc/diagnostic.d.ts +65 -0
- package/dist/ipc/diagnostic.js +129 -0
- package/dist/ipc/host.d.ts +7 -0
- package/dist/ipc/host.js +93 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +4 -0
- package/dist/ipc/target.d.ts +56 -0
- package/dist/ipc/target.js +297 -0
- package/dist/ipc/types.d.ts +42 -0
- package/dist/ipc/types.js +1 -0
- package/dist/platform/default.d.ts +32 -0
- package/dist/platform/default.js +40 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/node-fs.d.ts +37 -0
- package/dist/platform/node-fs.js +81 -0
- package/dist/platform/types.d.ts +104 -0
- package/dist/platform/types.js +1 -0
- package/dist/profile/client.d.ts +58 -12
- package/dist/profile/client.js +467 -0
- package/dist/profile/index.d.ts +3 -3
- package/dist/profile/index.js +3 -0
- package/dist/profile/manager.d.ts +2 -2
- package/dist/profile/manager.js +294 -0
- package/dist/profile/types.d.ts +1 -1
- package/dist/profile/types.js +1 -0
- package/dist/project/closure.js +68 -0
- package/dist/project/digest.d.ts +29 -0
- package/dist/project/digest.js +324 -0
- package/dist/project/index.d.ts +10 -5
- package/dist/project/index.js +10 -0
- package/dist/project/init.d.ts +3 -4
- package/dist/project/init.js +175 -0
- package/dist/project/loader.d.ts +21 -27
- package/dist/project/loader.js +484 -0
- package/dist/project/lockfile.d.ts +55 -0
- package/dist/project/lockfile.js +175 -0
- package/dist/project/manifest.d.ts +6 -28
- package/dist/project/manifest.js +220 -0
- package/dist/project/resolver.d.ts +116 -0
- package/dist/project/resolver.js +376 -0
- package/dist/project/transactions.d.ts +103 -0
- package/dist/project/transactions.js +1203 -0
- package/dist/project/types-generator.d.ts +33 -0
- package/dist/project/types-generator.js +168 -0
- package/dist/project/types.d.ts +58 -82
- package/dist/project/types.js +1 -0
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +5 -0
- package/dist/registry/lock.d.ts +27 -0
- package/dist/registry/lock.js +93 -0
- package/dist/registry/registry.d.ts +32 -11
- package/dist/registry/registry.js +611 -0
- package/dist/registry/resolve.d.ts +62 -0
- package/dist/registry/resolve.js +195 -0
- package/dist/registry/scan.d.ts +38 -0
- package/dist/registry/scan.js +135 -0
- package/dist/registry/types.d.ts +1 -1
- package/dist/registry/types.js +1 -0
- package/dist/runtime/action-collection.d.ts +27 -0
- package/dist/runtime/action-collection.js +57 -0
- package/dist/runtime/clock.d.ts +0 -2
- package/dist/runtime/clock.js +17 -0
- package/dist/runtime/context.d.ts +16 -5
- package/dist/runtime/context.js +242 -0
- package/dist/runtime/env.d.ts +3 -1
- package/dist/runtime/env.js +159 -0
- package/dist/runtime/events.d.ts +46 -14
- package/dist/runtime/events.js +604 -0
- package/dist/runtime/execution-manager.d.ts +1 -1
- package/dist/runtime/execution-manager.js +66 -0
- package/dist/runtime/index.d.ts +10 -9
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/module-loader.d.ts +0 -8
- package/dist/runtime/module-loader.js +16 -0
- package/dist/runtime/process.d.ts +2 -4
- package/dist/runtime/process.js +146 -0
- package/dist/runtime/runner.d.ts +117 -10
- package/dist/runtime/runner.js +984 -0
- package/dist/runtime/standalone.d.ts +68 -19
- package/dist/runtime/standalone.js +536 -0
- package/dist/schema/validator.js +116 -0
- package/dist/server/body.js +93 -0
- package/dist/server/index.d.ts +5 -6
- package/dist/server/index.js +5 -0
- package/dist/server/routes/actions.d.ts +2 -2
- package/dist/server/routes/actions.js +298 -0
- package/dist/server/routes/common.d.ts +28 -17
- package/dist/server/routes/common.js +104 -0
- package/dist/server/routes/config.d.ts +2 -1
- package/dist/server/routes/config.js +173 -0
- package/dist/server/routes/doctor.d.ts +2 -2
- package/dist/server/routes/doctor.js +63 -0
- package/dist/server/routes/health.d.ts +2 -2
- package/dist/server/routes/health.js +35 -0
- package/dist/server/routes/index.d.ts +9 -9
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/info.d.ts +4 -2
- package/dist/server/routes/info.js +158 -0
- package/dist/server/routes/playbooks.d.ts +2 -2
- package/dist/server/routes/playbooks.js +134 -0
- package/dist/server/routes/runs.d.ts +2 -2
- package/dist/server/routes/runs.js +343 -0
- package/dist/server/routes/state.d.ts +2 -1
- package/dist/server/routes/state.js +169 -0
- package/dist/server/security.d.ts +11 -4
- package/dist/server/security.js +101 -0
- package/dist/server/server.d.ts +6 -6
- package/dist/server/server.js +355 -0
- package/dist/server/types.d.ts +27 -8
- package/dist/server/types.js +1 -0
- package/dist/storage/data-dir-lock.d.ts +119 -0
- package/dist/storage/data-dir-lock.js +1016 -0
- package/dist/storage/driver.d.ts +4 -10
- package/dist/storage/driver.js +59 -0
- package/dist/storage/index.d.ts +30 -10
- package/dist/storage/index.js +98 -0
- package/dist/storage/lazy.d.ts +8 -0
- package/dist/storage/lazy.js +44 -0
- package/dist/storage/mask.d.ts +12 -5
- package/dist/storage/mask.js +48 -0
- package/dist/storage/params.d.ts +13 -0
- package/dist/storage/params.js +32 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +689 -0
- package/dist/storage/types.d.ts +57 -6
- package/dist/storage/types.js +14 -0
- package/dist/target/index.d.ts +4 -0
- package/dist/target/index.js +4 -0
- package/dist/target/local.d.ts +52 -0
- package/dist/target/local.js +343 -0
- package/dist/target/remote.d.ts +76 -0
- package/dist/target/remote.js +839 -0
- package/dist/target/target.d.ts +6 -0
- package/dist/target/target.js +35 -0
- package/dist/target/types.d.ts +246 -0
- package/dist/target/types.js +34 -0
- package/dist/utils/index.d.ts +22 -2
- package/dist/utils/index.js +180 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +4 -0
- package/package.json +6 -7
- package/dist/build/index.d.ts +0 -1
- package/dist/build/templates.d.ts +0 -19
- package/dist/server/runtime-registry.d.ts +0 -78
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* 诊断日志限流排空转发器配置选项。
|
|
4
|
+
*/
|
|
5
|
+
export interface DiagnosticForwarderOptions {
|
|
6
|
+
/** 允许转发的累计最大字节数(默认 512 KB) */
|
|
7
|
+
maxBytes?: number;
|
|
8
|
+
/** 每秒允许转发的最大字节速率(默认 64 KB/秒) */
|
|
9
|
+
maxRateBytesPerSec?: number;
|
|
10
|
+
/** 目标输出流(默认 process.stderr) */
|
|
11
|
+
target?: Writable;
|
|
12
|
+
/** 诊断流标识前缀 */
|
|
13
|
+
prefix?: string;
|
|
14
|
+
/** 触发超限截断时的回调通知 */
|
|
15
|
+
onTruncated?: (reason: "size_limit" | "rate_limit") => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 诊断流受控转发与限流排空器(DiagnosticForwarder)。
|
|
19
|
+
*
|
|
20
|
+
* 职责:
|
|
21
|
+
* 1. 独占捕获子进程的标准输出与标准错误流,杜绝第三方代码污染父进程标准协议通道。
|
|
22
|
+
* 2. 实时监测字节总量与秒级传输速率上限。
|
|
23
|
+
* 3. 达到阈值后立即停止对外转发、向诊断通道记录截断事件。
|
|
24
|
+
* 4. 持续以非阻塞方式静默排空后续数据流,避免子进程因管道背压堆积死锁。
|
|
25
|
+
*/
|
|
26
|
+
export declare class DiagnosticForwarder {
|
|
27
|
+
private readonly maxBytes;
|
|
28
|
+
private readonly maxRateBytesPerSec;
|
|
29
|
+
private readonly target;
|
|
30
|
+
private readonly prefix;
|
|
31
|
+
private readonly onTruncated?;
|
|
32
|
+
private totalBytesEmitted;
|
|
33
|
+
private windowStartMs;
|
|
34
|
+
private bytesInCurrentWindow;
|
|
35
|
+
private truncated;
|
|
36
|
+
private attachedStreams;
|
|
37
|
+
constructor(options?: DiagnosticForwarderOptions);
|
|
38
|
+
/**
|
|
39
|
+
* 是否已达到上限并处于截断状态。
|
|
40
|
+
*/
|
|
41
|
+
get isTruncated(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* 已成功转发的累计字节数。
|
|
44
|
+
*/
|
|
45
|
+
get bytesEmitted(): number;
|
|
46
|
+
/**
|
|
47
|
+
* 绑定并监听可读流(如 childProcess.stdout 或 childProcess.stderr)。
|
|
48
|
+
*
|
|
49
|
+
* @param stream 待转发的可读流
|
|
50
|
+
* @param label 流标识(如 "stdout" 或 "stderr")
|
|
51
|
+
*/
|
|
52
|
+
attach(stream: Readable, label?: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* 解绑指定的流监听。
|
|
55
|
+
*/
|
|
56
|
+
detach(stream: Readable): void;
|
|
57
|
+
/**
|
|
58
|
+
* 解绑所有已连接的流。
|
|
59
|
+
*/
|
|
60
|
+
detachAll(): void;
|
|
61
|
+
/**
|
|
62
|
+
* 重置计数器与截断标志。
|
|
63
|
+
*/
|
|
64
|
+
reset(): void;
|
|
65
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 诊断流受控转发与限流排空器(DiagnosticForwarder)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 独占捕获子进程的标准输出与标准错误流,杜绝第三方代码污染父进程标准协议通道。
|
|
6
|
+
* 2. 实时监测字节总量与秒级传输速率上限。
|
|
7
|
+
* 3. 达到阈值后立即停止对外转发、向诊断通道记录截断事件。
|
|
8
|
+
* 4. 持续以非阻塞方式静默排空后续数据流,避免子进程因管道背压堆积死锁。
|
|
9
|
+
*/
|
|
10
|
+
export class DiagnosticForwarder {
|
|
11
|
+
maxBytes;
|
|
12
|
+
maxRateBytesPerSec;
|
|
13
|
+
target;
|
|
14
|
+
prefix;
|
|
15
|
+
onTruncated;
|
|
16
|
+
totalBytesEmitted = 0;
|
|
17
|
+
windowStartMs = Date.now();
|
|
18
|
+
bytesInCurrentWindow = 0;
|
|
19
|
+
truncated = false;
|
|
20
|
+
attachedStreams = new Map();
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.maxBytes = options.maxBytes ?? 512 * 1024;
|
|
23
|
+
this.maxRateBytesPerSec = options.maxRateBytesPerSec ?? 64 * 1024;
|
|
24
|
+
this.target = options.target ?? process.stderr;
|
|
25
|
+
this.prefix = options.prefix ?? "";
|
|
26
|
+
this.onTruncated = options.onTruncated;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 是否已达到上限并处于截断状态。
|
|
30
|
+
*/
|
|
31
|
+
get isTruncated() {
|
|
32
|
+
return this.truncated;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 已成功转发的累计字节数。
|
|
36
|
+
*/
|
|
37
|
+
get bytesEmitted() {
|
|
38
|
+
return this.totalBytesEmitted;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 绑定并监听可读流(如 childProcess.stdout 或 childProcess.stderr)。
|
|
42
|
+
*
|
|
43
|
+
* @param stream 待转发的可读流
|
|
44
|
+
* @param label 流标识(如 "stdout" 或 "stderr")
|
|
45
|
+
*/
|
|
46
|
+
attach(stream, label) {
|
|
47
|
+
if (this.attachedStreams.has(stream))
|
|
48
|
+
return;
|
|
49
|
+
const handler = (chunk) => {
|
|
50
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf-8") : chunk;
|
|
51
|
+
const len = buf.length;
|
|
52
|
+
// 刷新秒级速率统计窗口
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (now - this.windowStartMs >= 1000) {
|
|
55
|
+
this.windowStartMs = now;
|
|
56
|
+
this.bytesInCurrentWindow = 0;
|
|
57
|
+
}
|
|
58
|
+
if (this.truncated) {
|
|
59
|
+
// 已截断:仅排空丢弃数据,不转发,保持流处于读取排空状态
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// 检查容量与速率上限
|
|
63
|
+
let exceedReason = null;
|
|
64
|
+
if (this.totalBytesEmitted + len > this.maxBytes) {
|
|
65
|
+
exceedReason = "size_limit";
|
|
66
|
+
}
|
|
67
|
+
else if (this.bytesInCurrentWindow + len > this.maxRateBytesPerSec) {
|
|
68
|
+
exceedReason = "rate_limit";
|
|
69
|
+
}
|
|
70
|
+
if (exceedReason) {
|
|
71
|
+
this.truncated = true;
|
|
72
|
+
const tag = label ? `[${label}] ` : "";
|
|
73
|
+
const notice = `\n[Supervisor] ${tag}Diagnostic output exceeded ${exceedReason === "size_limit" ? `size limit (${this.maxBytes} bytes)` : `rate limit (${this.maxRateBytesPerSec} B/s)`}. Forwarding suspended; draining stream.\n`;
|
|
74
|
+
try {
|
|
75
|
+
this.target.write(notice);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// 忽略写入失败
|
|
79
|
+
}
|
|
80
|
+
this.onTruncated?.(exceedReason);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// 正常转发
|
|
84
|
+
this.totalBytesEmitted += len;
|
|
85
|
+
this.bytesInCurrentWindow += len;
|
|
86
|
+
try {
|
|
87
|
+
if (this.prefix) {
|
|
88
|
+
this.target.write(`${this.prefix}${buf.toString("utf-8")}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.target.write(buf);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// 忽略目标写入异常
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
stream.on("data", handler);
|
|
99
|
+
this.attachedStreams.set(stream, handler);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 解绑指定的流监听。
|
|
103
|
+
*/
|
|
104
|
+
detach(stream) {
|
|
105
|
+
const handler = this.attachedStreams.get(stream);
|
|
106
|
+
if (handler) {
|
|
107
|
+
stream.removeListener("data", handler);
|
|
108
|
+
this.attachedStreams.delete(stream);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 解绑所有已连接的流。
|
|
113
|
+
*/
|
|
114
|
+
detachAll() {
|
|
115
|
+
for (const [stream, handler] of this.attachedStreams.entries()) {
|
|
116
|
+
stream.removeListener("data", handler);
|
|
117
|
+
}
|
|
118
|
+
this.attachedStreams.clear();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 重置计数器与截断标志。
|
|
122
|
+
*/
|
|
123
|
+
reset() {
|
|
124
|
+
this.totalBytesEmitted = 0;
|
|
125
|
+
this.bytesInCurrentWindow = 0;
|
|
126
|
+
this.windowStartMs = Date.now();
|
|
127
|
+
this.truncated = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/ipc/host.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在 Host 子进程中启动 Node IPC 服务,向父进程暴露 ActionDockTarget 门面能力。
|
|
3
|
+
*
|
|
4
|
+
* @param target 已构造好的 ActionDockTarget 实例
|
|
5
|
+
*/
|
|
6
|
+
export async function serveParentIpc(target) {
|
|
7
|
+
if (!process.send) {
|
|
8
|
+
// 若当前未以 IPC 模式运行,直接退出
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
let isClosing = false;
|
|
12
|
+
const safeClose = async () => {
|
|
13
|
+
if (isClosing)
|
|
14
|
+
return;
|
|
15
|
+
isClosing = true;
|
|
16
|
+
try {
|
|
17
|
+
await target.close();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// 忽略关闭异常
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
// 监听父进程断连或退出信号
|
|
24
|
+
process.once("disconnect", async () => {
|
|
25
|
+
await safeClose();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
});
|
|
28
|
+
process.once("SIGINT", async () => {
|
|
29
|
+
await safeClose();
|
|
30
|
+
process.exit(130);
|
|
31
|
+
});
|
|
32
|
+
process.once("SIGTERM", async () => {
|
|
33
|
+
await safeClose();
|
|
34
|
+
process.exit(143);
|
|
35
|
+
});
|
|
36
|
+
// 处理父进程 IPC 调用请求
|
|
37
|
+
process.on("message", async (msg) => {
|
|
38
|
+
if (!msg || typeof msg !== "object")
|
|
39
|
+
return;
|
|
40
|
+
if (msg.type === "call") {
|
|
41
|
+
const callMsg = msg;
|
|
42
|
+
const { id, method, args } = callMsg;
|
|
43
|
+
try {
|
|
44
|
+
const fn = target[method];
|
|
45
|
+
if (typeof fn !== "function") {
|
|
46
|
+
throw new Error(`Target method '${method}' not found`);
|
|
47
|
+
}
|
|
48
|
+
const result = await fn.apply(target, args || []);
|
|
49
|
+
const response = {
|
|
50
|
+
id,
|
|
51
|
+
type: "response",
|
|
52
|
+
ok: true,
|
|
53
|
+
data: result,
|
|
54
|
+
};
|
|
55
|
+
if (process.send) {
|
|
56
|
+
process.send(response);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const response = {
|
|
61
|
+
id,
|
|
62
|
+
type: "response",
|
|
63
|
+
ok: false,
|
|
64
|
+
error: {
|
|
65
|
+
code: err?.code || "TARGET_ERROR",
|
|
66
|
+
message: err?.message || String(err),
|
|
67
|
+
stack: err?.stack,
|
|
68
|
+
details: err?.details,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
if (process.send) {
|
|
72
|
+
process.send(response);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (msg.type === "close") {
|
|
77
|
+
await safeClose();
|
|
78
|
+
if (process.send) {
|
|
79
|
+
process.send({ id: msg.id, type: "response", ok: true });
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
else if (msg.type === "ping") {
|
|
84
|
+
if (process.send) {
|
|
85
|
+
process.send({ type: "pong" });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// 通知监督父进程:Host 已就绪
|
|
90
|
+
if (process.send) {
|
|
91
|
+
process.send({ type: "ready" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import type { ActionRef, ExecutionEvent, ExecutionResult, JsonValue, RunRecord } from "@actiondock/sdk";
|
|
3
|
+
import type { ActionSpec, ActionSummary, ListActionsOptions, PackageInfo, PlaybookSpec, PlaybookSummary } from "../app/types.js";
|
|
4
|
+
import type { CancelResult, ExecuteOptions, ExecutionTicket } from "../execution/types.js";
|
|
5
|
+
import type { ActionDockTarget, ConfigValueView, IpcTargetOptions, StateScopeOptions, TargetInfo } from "../target/types.js";
|
|
6
|
+
/**
|
|
7
|
+
* 基于 Node IPC 监督进程通信通道的 ActionDockTarget 实现。
|
|
8
|
+
*
|
|
9
|
+
* 职责:
|
|
10
|
+
* 1. 监督并隔离子进程的生命周期与标准输出,保证父进程标准输入输出通道 100% 纯净。
|
|
11
|
+
* 2. 将子进程标准输出/错误通过 DiagnosticForwarder 进行受控排空与诊断流限流转发。
|
|
12
|
+
* 3. 当子进程异常终止时,立即将当前活跃调用转换为结构化 HOST_PROCESS_EXITED 错误,防止调用方死锁。
|
|
13
|
+
*/
|
|
14
|
+
export declare class IpcActionDockTarget implements ActionDockTarget {
|
|
15
|
+
private child;
|
|
16
|
+
private forwarder;
|
|
17
|
+
private pendingCalls;
|
|
18
|
+
private isClosed;
|
|
19
|
+
private exitError?;
|
|
20
|
+
private readyPromise;
|
|
21
|
+
constructor(options: IpcTargetOptions);
|
|
22
|
+
/**
|
|
23
|
+
* 等待宿主子进程初始化就绪。
|
|
24
|
+
*/
|
|
25
|
+
waitReady(timeoutMs?: number): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* 获取底层子进程实例。
|
|
28
|
+
*/
|
|
29
|
+
get process(): ChildProcess;
|
|
30
|
+
private callRemote;
|
|
31
|
+
info(): Promise<TargetInfo>;
|
|
32
|
+
listPackages(): Promise<PackageInfo[]>;
|
|
33
|
+
listActions(options?: ListActionsOptions): Promise<ActionSummary[]>;
|
|
34
|
+
describeAction(ref: ActionRef | string): Promise<ActionSpec>;
|
|
35
|
+
listPlaybooks(options?: {
|
|
36
|
+
intent?: string;
|
|
37
|
+
package?: string;
|
|
38
|
+
}): Promise<PlaybookSummary[]>;
|
|
39
|
+
describePlaybook(id: string): Promise<PlaybookSpec>;
|
|
40
|
+
runAction(ref: ActionRef | string, input: JsonValue, options?: ExecuteOptions): Promise<ExecutionResult>;
|
|
41
|
+
startAction(ref: ActionRef | string, input: JsonValue, options?: ExecuteOptions): Promise<ExecutionTicket>;
|
|
42
|
+
getRun(runId: string): Promise<RunRecord | undefined>;
|
|
43
|
+
cancelRun(runId: string): Promise<CancelResult>;
|
|
44
|
+
listRuns(options?: any): Promise<RunRecord[]>;
|
|
45
|
+
events(_runId: string, _options?: any): AsyncIterable<ExecutionEvent>;
|
|
46
|
+
listConfig(packageId: string): Promise<ConfigValueView[]>;
|
|
47
|
+
getConfig(packageId: string, key: string): Promise<ConfigValueView>;
|
|
48
|
+
setConfig(packageId: string, key: string, value: JsonValue): Promise<void>;
|
|
49
|
+
deleteConfig(packageId: string, key: string): Promise<boolean>;
|
|
50
|
+
getState<T extends JsonValue = JsonValue>(packageId: string, actionId: string, key: string, options?: StateScopeOptions): Promise<T | undefined>;
|
|
51
|
+
setState<T extends JsonValue = JsonValue>(packageId: string, actionId: string, key: string, value: T, options?: StateScopeOptions): Promise<void>;
|
|
52
|
+
deleteState(packageId: string, actionId: string, key: string, options?: StateScopeOptions): Promise<boolean>;
|
|
53
|
+
listStateKeys(packageId: string, actionId?: string, options?: StateScopeOptions): Promise<string[]>;
|
|
54
|
+
clearState(packageId: string, actionId: string, options?: StateScopeOptions): Promise<number>;
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { fork } from "node:child_process";
|
|
3
|
+
import { DiagnosticForwarder } from "./diagnostic.js";
|
|
4
|
+
import { EXECUTION_ABORTED, HOST_PROCESS_EXITED } from "../errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* 基于 Node IPC 监督进程通信通道的 ActionDockTarget 实现。
|
|
7
|
+
*
|
|
8
|
+
* 职责:
|
|
9
|
+
* 1. 监督并隔离子进程的生命周期与标准输出,保证父进程标准输入输出通道 100% 纯净。
|
|
10
|
+
* 2. 将子进程标准输出/错误通过 DiagnosticForwarder 进行受控排空与诊断流限流转发。
|
|
11
|
+
* 3. 当子进程异常终止时,立即将当前活跃调用转换为结构化 HOST_PROCESS_EXITED 错误,防止调用方死锁。
|
|
12
|
+
*/
|
|
13
|
+
export class IpcActionDockTarget {
|
|
14
|
+
child;
|
|
15
|
+
forwarder;
|
|
16
|
+
pendingCalls = new Map();
|
|
17
|
+
isClosed = false;
|
|
18
|
+
exitError;
|
|
19
|
+
readyPromise;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.forwarder = new DiagnosticForwarder({
|
|
22
|
+
maxBytes: options.maxDiagnosticBytes,
|
|
23
|
+
maxRateBytesPerSec: options.maxDiagnosticRate,
|
|
24
|
+
target: options.diagnosticTarget,
|
|
25
|
+
});
|
|
26
|
+
if (options.childProcess) {
|
|
27
|
+
this.child = options.childProcess;
|
|
28
|
+
}
|
|
29
|
+
else if (options.scriptPath) {
|
|
30
|
+
this.child = fork(options.scriptPath, options.scriptArgs || [], {
|
|
31
|
+
cwd: options.cwd || process.cwd(),
|
|
32
|
+
env: options.env || process.env,
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
throw new Error("IpcActionDockTarget requires either 'childProcess' or 'scriptPath'");
|
|
38
|
+
}
|
|
39
|
+
// 绑定子进程输出至受控限流排空器,绝不泄露至 process.stdout
|
|
40
|
+
if (this.child.stdout) {
|
|
41
|
+
this.forwarder.attach(this.child.stdout, "stdout");
|
|
42
|
+
}
|
|
43
|
+
if (this.child.stderr) {
|
|
44
|
+
this.forwarder.attach(this.child.stderr, "stderr");
|
|
45
|
+
}
|
|
46
|
+
let resolveReady;
|
|
47
|
+
this.readyPromise = new Promise((r) => {
|
|
48
|
+
resolveReady = r;
|
|
49
|
+
});
|
|
50
|
+
this.child.on("message", (msg) => {
|
|
51
|
+
if (!msg || typeof msg !== "object")
|
|
52
|
+
return;
|
|
53
|
+
if (msg.type === "ready") {
|
|
54
|
+
resolveReady();
|
|
55
|
+
}
|
|
56
|
+
else if (msg.type === "response") {
|
|
57
|
+
const resp = msg;
|
|
58
|
+
const pending = this.pendingCalls.get(resp.id);
|
|
59
|
+
if (pending) {
|
|
60
|
+
this.pendingCalls.delete(resp.id);
|
|
61
|
+
if (resp.ok) {
|
|
62
|
+
pending.resolve(resp.data);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const err = new Error(resp.error?.message || "IPC Target call failed");
|
|
66
|
+
if (resp.error?.code)
|
|
67
|
+
err.code = resp.error.code;
|
|
68
|
+
if (resp.error?.details)
|
|
69
|
+
err.details = resp.error.details;
|
|
70
|
+
pending.reject(err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
this.child.on("exit", (code, signal) => {
|
|
76
|
+
const exitMsg = `Host process exited prematurely with code ${code ?? signal ?? "unknown"}`;
|
|
77
|
+
const err = new Error(exitMsg);
|
|
78
|
+
err.code = "HOST_PROCESS_EXITED";
|
|
79
|
+
this.exitError = err;
|
|
80
|
+
this.isClosed = true;
|
|
81
|
+
// 快速失败所有待处理调用
|
|
82
|
+
for (const pending of this.pendingCalls.values()) {
|
|
83
|
+
if (pending.method === "runAction") {
|
|
84
|
+
pending.resolve({
|
|
85
|
+
ok: false,
|
|
86
|
+
runId: randomUUID(),
|
|
87
|
+
error: {
|
|
88
|
+
code: HOST_PROCESS_EXITED,
|
|
89
|
+
message: exitMsg,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
pending.reject(err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this.pendingCalls.clear();
|
|
98
|
+
resolveReady(); // 避免就绪死锁
|
|
99
|
+
});
|
|
100
|
+
this.child.on("error", (err) => {
|
|
101
|
+
this.exitError = err;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 等待宿主子进程初始化就绪。
|
|
106
|
+
*/
|
|
107
|
+
async waitReady(timeoutMs = 5000) {
|
|
108
|
+
if (this.exitError)
|
|
109
|
+
throw this.exitError;
|
|
110
|
+
let timer;
|
|
111
|
+
const timeout = new Promise((_, reject) => {
|
|
112
|
+
timer = setTimeout(() => reject(new Error(`Timeout waiting for host process ready after ${timeoutMs}ms`)), timeoutMs);
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
await Promise.race([this.readyPromise, timeout]);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (timer !== undefined) {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 获取底层子进程实例。
|
|
125
|
+
*/
|
|
126
|
+
get process() {
|
|
127
|
+
return this.child;
|
|
128
|
+
}
|
|
129
|
+
async callRemote(method, args) {
|
|
130
|
+
if (this.isClosed || this.exitError) {
|
|
131
|
+
if (method === "runAction") {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
runId: randomUUID(),
|
|
135
|
+
error: {
|
|
136
|
+
code: HOST_PROCESS_EXITED,
|
|
137
|
+
message: this.exitError?.message || "Host process is already closed",
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
throw this.exitError || new Error("Host process is already closed");
|
|
142
|
+
}
|
|
143
|
+
const id = randomUUID();
|
|
144
|
+
const msg = {
|
|
145
|
+
id,
|
|
146
|
+
type: "call",
|
|
147
|
+
method,
|
|
148
|
+
args,
|
|
149
|
+
};
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
this.pendingCalls.set(id, { resolve, reject, method });
|
|
152
|
+
try {
|
|
153
|
+
if (this.child.send) {
|
|
154
|
+
this.child.send(msg);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.pendingCalls.delete(id);
|
|
158
|
+
reject(new Error("IPC channel unavailable on child process"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
this.pendingCalls.delete(id);
|
|
163
|
+
reject(err);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async info() {
|
|
168
|
+
return this.callRemote("info", []);
|
|
169
|
+
}
|
|
170
|
+
async listPackages() {
|
|
171
|
+
return this.callRemote("listPackages", []);
|
|
172
|
+
}
|
|
173
|
+
async listActions(options) {
|
|
174
|
+
return this.callRemote("listActions", [options]);
|
|
175
|
+
}
|
|
176
|
+
async describeAction(ref) {
|
|
177
|
+
return this.callRemote("describeAction", [ref]);
|
|
178
|
+
}
|
|
179
|
+
async listPlaybooks(options) {
|
|
180
|
+
return this.callRemote("listPlaybooks", [options]);
|
|
181
|
+
}
|
|
182
|
+
async describePlaybook(id) {
|
|
183
|
+
return this.callRemote("describePlaybook", [id]);
|
|
184
|
+
}
|
|
185
|
+
async runAction(ref, input, options) {
|
|
186
|
+
const { signal, ...serializableOptions } = options || {};
|
|
187
|
+
if (signal?.aborted) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
runId: randomUUID(),
|
|
191
|
+
error: {
|
|
192
|
+
code: EXECUTION_ABORTED,
|
|
193
|
+
message: "Execution was aborted before starting",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return this.callRemote("runAction", [ref, input, serializableOptions]);
|
|
198
|
+
}
|
|
199
|
+
async startAction(ref, input, options) {
|
|
200
|
+
const { signal, ...serializableOptions } = options || {};
|
|
201
|
+
return this.callRemote("startAction", [ref, input, serializableOptions]);
|
|
202
|
+
}
|
|
203
|
+
async getRun(runId) {
|
|
204
|
+
const res = await this.callRemote("getRun", [runId]);
|
|
205
|
+
return res ?? undefined;
|
|
206
|
+
}
|
|
207
|
+
async cancelRun(runId) {
|
|
208
|
+
return this.callRemote("cancelRun", [runId]);
|
|
209
|
+
}
|
|
210
|
+
async listRuns(options) {
|
|
211
|
+
return this.callRemote("listRuns", [options]);
|
|
212
|
+
}
|
|
213
|
+
async *events(_runId, _options) {
|
|
214
|
+
// 基础 IPC 通道暂不流式下发历史事件
|
|
215
|
+
}
|
|
216
|
+
async listConfig(packageId) {
|
|
217
|
+
return this.callRemote("listConfig", [packageId]);
|
|
218
|
+
}
|
|
219
|
+
async getConfig(packageId, key) {
|
|
220
|
+
return this.callRemote("getConfig", [packageId, key]);
|
|
221
|
+
}
|
|
222
|
+
async setConfig(packageId, key, value) {
|
|
223
|
+
return this.callRemote("setConfig", [packageId, key, value]);
|
|
224
|
+
}
|
|
225
|
+
async deleteConfig(packageId, key) {
|
|
226
|
+
return this.callRemote("deleteConfig", [packageId, key]);
|
|
227
|
+
}
|
|
228
|
+
async getState(packageId, actionId, key, options) {
|
|
229
|
+
return this.callRemote("getState", [
|
|
230
|
+
packageId,
|
|
231
|
+
actionId,
|
|
232
|
+
key,
|
|
233
|
+
options,
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
async setState(packageId, actionId, key, value, options) {
|
|
237
|
+
return this.callRemote("setState", [packageId, actionId, key, value, options]);
|
|
238
|
+
}
|
|
239
|
+
async deleteState(packageId, actionId, key, options) {
|
|
240
|
+
return this.callRemote("deleteState", [packageId, actionId, key, options]);
|
|
241
|
+
}
|
|
242
|
+
async listStateKeys(packageId, actionId, options) {
|
|
243
|
+
return this.callRemote("listStateKeys", [packageId, actionId, options]);
|
|
244
|
+
}
|
|
245
|
+
async clearState(packageId, actionId, options) {
|
|
246
|
+
return this.callRemote("clearState", [packageId, actionId, options]);
|
|
247
|
+
}
|
|
248
|
+
async close() {
|
|
249
|
+
if (this.isClosed)
|
|
250
|
+
return;
|
|
251
|
+
if (this.child.exitCode !== null) {
|
|
252
|
+
this.forwarder.detachAll();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const exitPromise = new Promise((resolve) => {
|
|
256
|
+
if (this.child.exitCode !== null) {
|
|
257
|
+
resolve();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.child.once("exit", () => resolve());
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
if (this.child.connected) {
|
|
265
|
+
this.child.send({ type: "close" });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// 忽略发送关闭消息异常
|
|
270
|
+
}
|
|
271
|
+
// 若子进程超时未退出,先 SIGTERM 再 SIGKILL
|
|
272
|
+
const termTimer = setTimeout(() => {
|
|
273
|
+
if (this.child.exitCode === null) {
|
|
274
|
+
try {
|
|
275
|
+
this.child.kill("SIGTERM");
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
}
|
|
279
|
+
}, 1500);
|
|
280
|
+
const killTimer = setTimeout(() => {
|
|
281
|
+
if (this.child.exitCode === null) {
|
|
282
|
+
try {
|
|
283
|
+
this.child.kill("SIGKILL");
|
|
284
|
+
}
|
|
285
|
+
catch { }
|
|
286
|
+
}
|
|
287
|
+
}, 3000);
|
|
288
|
+
try {
|
|
289
|
+
await exitPromise;
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
clearTimeout(termTimer);
|
|
293
|
+
clearTimeout(killTimer);
|
|
294
|
+
this.forwarder.detachAll();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|