@actiondock/core 2.0.1 → 2.0.3
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 +59 -27
- package/package.json +2 -2
- package/src/build/builder.ts +20 -7
- package/src/catalog/action-index.ts +92 -0
- package/src/catalog/action-resolver.ts +120 -0
- package/src/catalog/index.ts +5 -0
- package/src/catalog/location-registry.ts +134 -0
- package/src/catalog/package-catalog.ts +87 -0
- package/src/catalog/types.ts +83 -0
- package/src/doctor/doctor.ts +59 -33
- package/src/execution/index.ts +2 -0
- package/src/execution/service.ts +282 -0
- package/src/execution/types.ts +73 -0
- package/src/export/skill.ts +7 -6
- package/src/export/templates.ts +1 -1
- package/src/index.ts +2 -0
- package/src/profile/client.ts +252 -2
- package/src/project/index.ts +1 -0
- package/src/project/init.ts +80 -33
- package/src/project/loader.ts +11 -12
- package/src/project/manifest.ts +96 -0
- package/src/project/types.ts +30 -0
- package/src/runtime/clock.ts +41 -0
- package/src/runtime/context.ts +24 -2
- package/src/runtime/events.ts +142 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/process.ts +244 -0
- package/src/runtime/runner.ts +37 -3
- package/src/server/runtime-registry.ts +35 -0
- package/src/server/server.ts +994 -74
- package/src/server/types.ts +15 -0
- package/src/storage/driver.ts +126 -0
- package/src/storage/index.ts +4 -3
- package/src/storage/sqlite.ts +365 -258
- package/src/storage/types.ts +42 -22
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一时间与时钟接口。
|
|
3
|
+
*/
|
|
4
|
+
export interface Clock {
|
|
5
|
+
/** 获取当前系统墙上时间 */
|
|
6
|
+
now(): Date;
|
|
7
|
+
/** 获取单调递增时间戳(单位:毫秒) */
|
|
8
|
+
monotonic(): number;
|
|
9
|
+
/** 异步休眠指定毫秒 */
|
|
10
|
+
sleep(ms: number): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 生产环境系统时钟实现。
|
|
15
|
+
*/
|
|
16
|
+
export class SystemClock implements Clock {
|
|
17
|
+
now(): Date {
|
|
18
|
+
return new Date();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
monotonic(): number {
|
|
22
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
23
|
+
return performance.now();
|
|
24
|
+
}
|
|
25
|
+
return Date.now();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sleep(ms: number): Promise<void> {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let defaultClock: Clock = new SystemClock();
|
|
34
|
+
|
|
35
|
+
export function getSystemClock(): Clock {
|
|
36
|
+
return defaultClock;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setSystemClock(clock: Clock): void {
|
|
40
|
+
defaultClock = clock;
|
|
41
|
+
}
|
package/src/runtime/context.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type {
|
|
2
3
|
ActionContext,
|
|
3
4
|
ActionDefinition,
|
|
4
5
|
ActionInvoker,
|
|
5
6
|
Config,
|
|
6
7
|
Logger,
|
|
8
|
+
ProcessAPI,
|
|
9
|
+
ProgressReporter,
|
|
7
10
|
StateStore,
|
|
8
11
|
} from "@actiondock/sdk";
|
|
9
12
|
import type { ProjectConfig } from "../project/types";
|
|
10
13
|
import { createGlobalStorage } from "../storage";
|
|
11
14
|
import type { RuntimeStorage } from "../storage/types";
|
|
12
15
|
import { resolveEnvValue } from "./env";
|
|
16
|
+
import { getProcessExecutor } from "./process";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* 生产级配置解析器实现。
|
|
@@ -184,8 +188,12 @@ export interface ContextOptions {
|
|
|
184
188
|
overrides?: Record<string, unknown>;
|
|
185
189
|
projectConfig?: ProjectConfig;
|
|
186
190
|
parentRunId?: string;
|
|
191
|
+
runId?: string;
|
|
192
|
+
rootRunId?: string;
|
|
187
193
|
callStack?: string[];
|
|
188
194
|
signal?: AbortSignal;
|
|
195
|
+
process?: ProcessAPI;
|
|
196
|
+
progress?: ProgressReporter;
|
|
189
197
|
onActionInvoke?: (
|
|
190
198
|
action: ActionDefinition,
|
|
191
199
|
input: unknown,
|
|
@@ -196,7 +204,7 @@ export interface ContextOptions {
|
|
|
196
204
|
/**
|
|
197
205
|
* 构建并装配传递给 Action 的完整 ActionContext 运行时上下文。
|
|
198
206
|
*
|
|
199
|
-
* @param options
|
|
207
|
+
* @param options 上下文构建参数
|
|
200
208
|
* @returns 组装完毕的 ActionContext 实例
|
|
201
209
|
*/
|
|
202
210
|
export function createActionContext(options: ContextOptions): ActionContext {
|
|
@@ -208,6 +216,8 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
208
216
|
const state = new RuntimeStateStore(options.storage);
|
|
209
217
|
const log = new StderrLogger();
|
|
210
218
|
const signal = options.signal ?? new AbortController().signal;
|
|
219
|
+
const currentRunId = options.runId || randomUUID();
|
|
220
|
+
const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
|
|
211
221
|
|
|
212
222
|
const invoker: ActionInvoker = {
|
|
213
223
|
async invoke<I, O>(action: ActionDefinition<I, O>, input: I): Promise<O> {
|
|
@@ -215,18 +225,30 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
215
225
|
return (await options.onActionInvoke(
|
|
216
226
|
action as any,
|
|
217
227
|
input,
|
|
218
|
-
|
|
228
|
+
currentRunId
|
|
219
229
|
)) as O;
|
|
220
230
|
}
|
|
221
231
|
throw new Error("ActionInvoker not configured with an invocation delegate");
|
|
222
232
|
},
|
|
223
233
|
};
|
|
224
234
|
|
|
235
|
+
const processApi = options.process || getProcessExecutor();
|
|
236
|
+
const progressApi: ProgressReporter = options.progress || {
|
|
237
|
+
report() {},
|
|
238
|
+
};
|
|
239
|
+
|
|
225
240
|
return {
|
|
226
241
|
config,
|
|
227
242
|
state,
|
|
228
243
|
actions: invoker,
|
|
244
|
+
process: processApi,
|
|
229
245
|
log,
|
|
246
|
+
progress: progressApi,
|
|
230
247
|
signal,
|
|
248
|
+
run: {
|
|
249
|
+
id: currentRunId,
|
|
250
|
+
rootId: currentRootRunId,
|
|
251
|
+
parentId: options.parentRunId,
|
|
252
|
+
},
|
|
231
253
|
};
|
|
232
254
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ExecutionEvent } from "@actiondock/sdk";
|
|
2
|
+
|
|
3
|
+
export interface EventSink {
|
|
4
|
+
emit(event: ExecutionEvent): void;
|
|
5
|
+
subscribe(
|
|
6
|
+
runId: string,
|
|
7
|
+
options?: { after?: number; signal?: AbortSignal }
|
|
8
|
+
): AsyncIterable<ExecutionEvent>;
|
|
9
|
+
clear(runId: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 进程内有界事件缓冲区实现。
|
|
14
|
+
* 遵循设计文档:每个运行最多保留 1024 条或 1MB 事件,支持单调序号。
|
|
15
|
+
*/
|
|
16
|
+
export class InMemoryEventSink implements EventSink {
|
|
17
|
+
private eventsByRun = new Map<string, ExecutionEvent[]>();
|
|
18
|
+
private listeners = new Map<string, Set<(event: ExecutionEvent) => void>>();
|
|
19
|
+
private maxEventsPerRun = 1024;
|
|
20
|
+
|
|
21
|
+
emit(event: ExecutionEvent): void {
|
|
22
|
+
let list = this.eventsByRun.get(event.runId);
|
|
23
|
+
if (!list) {
|
|
24
|
+
list = [];
|
|
25
|
+
this.eventsByRun.set(event.runId, list);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (list.length >= this.maxEventsPerRun) {
|
|
29
|
+
// 淘汰旧日志和进度事件,保留状态和结果
|
|
30
|
+
const nonEssentialIndex = list.findIndex((e) => e.type === "log" || e.type === "progress");
|
|
31
|
+
if (nonEssentialIndex >= 0) {
|
|
32
|
+
list.splice(nonEssentialIndex, 1);
|
|
33
|
+
} else {
|
|
34
|
+
list.shift();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
list.push(event);
|
|
38
|
+
|
|
39
|
+
const subs = this.listeners.get(event.runId);
|
|
40
|
+
if (subs) {
|
|
41
|
+
for (const listener of subs) {
|
|
42
|
+
try {
|
|
43
|
+
listener(event);
|
|
44
|
+
} catch {
|
|
45
|
+
// 忽略单个监听器内部异常
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async *subscribe(
|
|
52
|
+
runId: string,
|
|
53
|
+
options: { after?: number; signal?: AbortSignal } = {}
|
|
54
|
+
): AsyncIterable<ExecutionEvent> {
|
|
55
|
+
const after = options.after ?? -1;
|
|
56
|
+
const history = this.eventsByRun.get(runId) || [];
|
|
57
|
+
|
|
58
|
+
for (const evt of history) {
|
|
59
|
+
if (evt.sequence > after) {
|
|
60
|
+
yield evt;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lastEvt = history[history.length - 1];
|
|
65
|
+
if (lastEvt && (lastEvt.type === "finish" || lastEvt.type === "status" && lastEvt.status !== "running")) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const queue: ExecutionEvent[] = [];
|
|
70
|
+
let notify: (() => void) | null = null;
|
|
71
|
+
let done = false;
|
|
72
|
+
|
|
73
|
+
const listener = (evt: ExecutionEvent) => {
|
|
74
|
+
if (evt.sequence > after) {
|
|
75
|
+
queue.push(evt);
|
|
76
|
+
if (notify) {
|
|
77
|
+
notify();
|
|
78
|
+
notify = null;
|
|
79
|
+
}
|
|
80
|
+
if (evt.type === "finish") {
|
|
81
|
+
done = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let subs = this.listeners.get(runId);
|
|
87
|
+
if (!subs) {
|
|
88
|
+
subs = new Set();
|
|
89
|
+
this.listeners.set(runId, subs);
|
|
90
|
+
}
|
|
91
|
+
subs.add(listener);
|
|
92
|
+
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
subs?.delete(listener);
|
|
95
|
+
if (subs && subs.size === 0) {
|
|
96
|
+
this.listeners.delete(runId);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (options.signal) {
|
|
101
|
+
options.signal.addEventListener("abort", () => {
|
|
102
|
+
done = true;
|
|
103
|
+
if (notify) {
|
|
104
|
+
notify();
|
|
105
|
+
notify = null;
|
|
106
|
+
}
|
|
107
|
+
}, { once: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
while (!done && !options.signal?.aborted) {
|
|
112
|
+
if (queue.length > 0) {
|
|
113
|
+
yield queue.shift()!;
|
|
114
|
+
} else {
|
|
115
|
+
await new Promise<void>((resolve) => {
|
|
116
|
+
notify = resolve;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
while (queue.length > 0) {
|
|
121
|
+
yield queue.shift()!;
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
cleanup();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
clear(runId: string): void {
|
|
129
|
+
this.eventsByRun.delete(runId);
|
|
130
|
+
this.listeners.delete(runId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let defaultEventSink: EventSink = new InMemoryEventSink();
|
|
135
|
+
|
|
136
|
+
export function getDefaultEventSink(): EventSink {
|
|
137
|
+
return defaultEventSink;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function setDefaultEventSink(sink: EventSink): void {
|
|
141
|
+
defaultEventSink = sink;
|
|
142
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type {
|
|
3
|
+
DetachedProcessOptions,
|
|
4
|
+
DetachedProcessResult,
|
|
5
|
+
ProcessAPI,
|
|
6
|
+
ProcessExecOptions,
|
|
7
|
+
ProcessResult,
|
|
8
|
+
RuntimeError,
|
|
9
|
+
} from "@actiondock/sdk";
|
|
10
|
+
|
|
11
|
+
export type ProcessExecutor = ProcessAPI;
|
|
12
|
+
|
|
13
|
+
let globalProcessExecutor: ProcessExecutor | undefined;
|
|
14
|
+
|
|
15
|
+
export function setProcessExecutor(executor: ProcessExecutor): void {
|
|
16
|
+
globalProcessExecutor = executor;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getProcessExecutor(): ProcessExecutor {
|
|
20
|
+
if (!globalProcessExecutor) {
|
|
21
|
+
globalProcessExecutor = new DefaultProcessExecutor();
|
|
22
|
+
}
|
|
23
|
+
return globalProcessExecutor;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 基于 Node.js 标准 child_process 实现的基础进程执行器。
|
|
28
|
+
*/
|
|
29
|
+
export class DefaultProcessExecutor implements ProcessExecutor {
|
|
30
|
+
async exec(
|
|
31
|
+
command: string,
|
|
32
|
+
args: string[] = [],
|
|
33
|
+
options: ProcessExecOptions = {}
|
|
34
|
+
): Promise<ProcessResult> {
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
const maxOutputBytes = options.maxOutputBytes ?? 10 * 1024 * 1024;
|
|
37
|
+
|
|
38
|
+
return new Promise<ProcessResult>((resolve, reject) => {
|
|
39
|
+
let stdoutBuf = "";
|
|
40
|
+
let stderrBuf = "";
|
|
41
|
+
let totalBytes = 0;
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
let error: RuntimeError | undefined;
|
|
45
|
+
|
|
46
|
+
const cp = spawn(command, args, {
|
|
47
|
+
cwd: options.cwd,
|
|
48
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
49
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (options.input) {
|
|
53
|
+
cp.stdin.write(options.input);
|
|
54
|
+
cp.stdin.end();
|
|
55
|
+
} else {
|
|
56
|
+
cp.stdin.end();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
61
|
+
timer = setTimeout(() => {
|
|
62
|
+
timedOut = true;
|
|
63
|
+
cp.kill("SIGTERM");
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
if (!cp.killed) cp.kill("SIGKILL");
|
|
66
|
+
}, 1000);
|
|
67
|
+
}, options.timeoutMs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const onAbort = () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
cp.kill("SIGTERM");
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (!cp.killed) cp.kill("SIGKILL");
|
|
75
|
+
}, 1000);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (options.signal) {
|
|
79
|
+
if (options.signal.aborted) {
|
|
80
|
+
onAbort();
|
|
81
|
+
} else {
|
|
82
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cp.stdout?.on("data", (chunk: Buffer) => {
|
|
87
|
+
totalBytes += chunk.length;
|
|
88
|
+
if (totalBytes > maxOutputBytes) {
|
|
89
|
+
error = {
|
|
90
|
+
code: "PROCESS_OUTPUT_LIMIT",
|
|
91
|
+
message: `Process output exceeded limit of ${maxOutputBytes} bytes`,
|
|
92
|
+
};
|
|
93
|
+
cp.kill("SIGKILL");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
stdoutBuf += chunk.toString("utf-8");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
cp.stderr?.on("data", (chunk: Buffer) => {
|
|
100
|
+
totalBytes += chunk.length;
|
|
101
|
+
if (totalBytes > maxOutputBytes) {
|
|
102
|
+
error = {
|
|
103
|
+
code: "PROCESS_OUTPUT_LIMIT",
|
|
104
|
+
message: `Process output exceeded limit of ${maxOutputBytes} bytes`,
|
|
105
|
+
};
|
|
106
|
+
cp.kill("SIGKILL");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
stderrBuf += chunk.toString("utf-8");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
cp.on("error", (err) => {
|
|
113
|
+
if (timer) clearTimeout(timer);
|
|
114
|
+
const durationMs = Date.now() - startTime;
|
|
115
|
+
const res: ProcessResult = {
|
|
116
|
+
ok: false,
|
|
117
|
+
exitCode: null,
|
|
118
|
+
stdout: stdoutBuf.trim(),
|
|
119
|
+
stderr: stderrBuf.trim() || err.message,
|
|
120
|
+
raw: new TextEncoder().encode(stdoutBuf),
|
|
121
|
+
timedOut,
|
|
122
|
+
cancelled,
|
|
123
|
+
durationMs,
|
|
124
|
+
error: error || {
|
|
125
|
+
code: "PROCESS_SPAWN_ERROR",
|
|
126
|
+
message: err.message,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
if (options.throwOnError) {
|
|
130
|
+
reject(new Error(err.message));
|
|
131
|
+
} else {
|
|
132
|
+
resolve(res);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
cp.on("close", (exitCode, signal) => {
|
|
137
|
+
if (timer) clearTimeout(timer);
|
|
138
|
+
const durationMs = Date.now() - startTime;
|
|
139
|
+
const ok = exitCode === 0 && !timedOut && !cancelled && !error;
|
|
140
|
+
|
|
141
|
+
const res: ProcessResult = {
|
|
142
|
+
ok,
|
|
143
|
+
exitCode,
|
|
144
|
+
signal: signal || undefined,
|
|
145
|
+
stdout: stdoutBuf.trim(),
|
|
146
|
+
stderr: stderrBuf.trim(),
|
|
147
|
+
raw: new TextEncoder().encode(stdoutBuf),
|
|
148
|
+
timedOut,
|
|
149
|
+
cancelled,
|
|
150
|
+
durationMs,
|
|
151
|
+
error,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (!ok && options.throwOnError) {
|
|
155
|
+
reject(new Error(stderrBuf.trim() || `Process exited with code ${exitCode}`));
|
|
156
|
+
} else {
|
|
157
|
+
resolve(res);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async spawnDetached(options: DetachedProcessOptions): Promise<DetachedProcessResult> {
|
|
164
|
+
const startTime = Date.now();
|
|
165
|
+
try {
|
|
166
|
+
const child = spawn(options.command, options.args || [], {
|
|
167
|
+
cwd: options.cwd,
|
|
168
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
169
|
+
detached: true,
|
|
170
|
+
stdio: "ignore",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.unref();
|
|
174
|
+
|
|
175
|
+
if (!options.probe) {
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
pid: child.pid,
|
|
179
|
+
ready: true,
|
|
180
|
+
durationMs: Date.now() - startTime,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const probeInterval = options.probeIntervalMs ?? 200;
|
|
185
|
+
const probeTimeout = options.probeTimeoutMs ?? 5000;
|
|
186
|
+
const deadline = Date.now() + probeTimeout;
|
|
187
|
+
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
if (options.signal?.aborted) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
pid: child.pid,
|
|
193
|
+
ready: false,
|
|
194
|
+
durationMs: Date.now() - startTime,
|
|
195
|
+
error: {
|
|
196
|
+
code: "PROCESS_CANCELLED",
|
|
197
|
+
message: "Probe was cancelled by AbortSignal",
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const checkRes = await this.exec(options.command, ["--version"], {
|
|
204
|
+
timeoutMs: 1000,
|
|
205
|
+
});
|
|
206
|
+
const isReady = await options.probe(checkRes);
|
|
207
|
+
if (isReady) {
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
pid: child.pid,
|
|
211
|
+
ready: true,
|
|
212
|
+
durationMs: Date.now() - startTime,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// 探测失败继续轮询
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await new Promise((r) => setTimeout(r, probeInterval));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
pid: child.pid,
|
|
225
|
+
ready: false,
|
|
226
|
+
durationMs: Date.now() - startTime,
|
|
227
|
+
error: {
|
|
228
|
+
code: "PROCESS_PROBE_TIMEOUT",
|
|
229
|
+
message: `Process probe timed out after ${probeTimeout}ms`,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
} catch (err: any) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
ready: false,
|
|
236
|
+
durationMs: Date.now() - startTime,
|
|
237
|
+
error: {
|
|
238
|
+
code: "PROCESS_DETACHED_FAILED",
|
|
239
|
+
message: err.message,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/runtime/runner.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type {
|
|
|
3
3
|
ActionContext,
|
|
4
4
|
ActionDefinition,
|
|
5
5
|
ExecutionResult,
|
|
6
|
+
JsonValue,
|
|
7
|
+
ProcessAPI,
|
|
8
|
+
ProgressReporter,
|
|
6
9
|
RuntimeError,
|
|
7
10
|
RunRecord,
|
|
8
11
|
} from "@actiondock/sdk";
|
|
@@ -10,6 +13,7 @@ import type { ProjectConfig } from "../project/types";
|
|
|
10
13
|
import { validateSchema } from "../schema/validator";
|
|
11
14
|
import type { RuntimeStorage, TerminalRunStatus } from "../storage/types";
|
|
12
15
|
import { RuntimeConfig, RuntimeStateStore, StderrLogger } from "./context";
|
|
16
|
+
import { getProcessExecutor } from "./process";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* ActionRunner 初始化配置选项。
|
|
@@ -25,20 +29,34 @@ export interface RunnerOptions {
|
|
|
25
29
|
configOverrides?: Record<string, unknown>;
|
|
26
30
|
/** 预加载的 Action 映射表 */
|
|
27
31
|
actions?: Map<string, ActionDefinition>;
|
|
32
|
+
/** 外部注入的进程执行器 */
|
|
33
|
+
process?: ProcessAPI;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* 启动 Action 执行时的可选控制参数。
|
|
32
38
|
*/
|
|
33
39
|
export interface ExecutionStartOptions {
|
|
40
|
+
/** 根运行 ID */
|
|
41
|
+
rootRunId?: string;
|
|
34
42
|
/** 父级运行 ID(嵌套调用场景下建立调用链树) */
|
|
35
43
|
parentRunId?: string;
|
|
44
|
+
/** 包物理实例标识 */
|
|
45
|
+
packageInstanceId?: string;
|
|
46
|
+
/** 快照代次标识 */
|
|
47
|
+
generationId?: string;
|
|
48
|
+
/** 执行所有者标识 */
|
|
49
|
+
ownerId?: string;
|
|
36
50
|
/** 调用栈数组(用于检测 A -> B -> A 环路死锁) */
|
|
37
51
|
callStack?: string[];
|
|
38
52
|
/** 外部传入的 AbortSignal 取消信号 */
|
|
39
53
|
signal?: AbortSignal;
|
|
40
54
|
/** 最大超时时间(毫秒),超时将自动中止执行并标记为 ACTION_TIMEOUT */
|
|
41
55
|
timeoutMs?: number;
|
|
56
|
+
/** 外部注入的进程执行器 */
|
|
57
|
+
process?: ProcessAPI;
|
|
58
|
+
/** 外部注入的进度报告器 */
|
|
59
|
+
progress?: ProgressReporter;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
62
|
/**
|
|
@@ -173,11 +191,15 @@ export class ActionRunner {
|
|
|
173
191
|
// 3. 插入初始运行记录 (状态: running)
|
|
174
192
|
const initialRun: RunRecord = {
|
|
175
193
|
id: runId,
|
|
194
|
+
rootRunId: options.rootRunId || options.parentRunId || runId,
|
|
195
|
+
parentRunId: options.parentRunId,
|
|
176
196
|
packageId: this.packageId,
|
|
197
|
+
packageInstanceId: options.packageInstanceId || this.packageId,
|
|
177
198
|
actionId: action.id,
|
|
178
|
-
|
|
199
|
+
generationId: options.generationId || "1",
|
|
200
|
+
ownerId: options.ownerId || "local",
|
|
179
201
|
status: "running",
|
|
180
|
-
input,
|
|
202
|
+
input: input as JsonValue | undefined,
|
|
181
203
|
startedAt,
|
|
182
204
|
};
|
|
183
205
|
this.storage.createRun(initialRun);
|
|
@@ -235,9 +257,12 @@ export class ActionRunner {
|
|
|
235
257
|
childInput: I
|
|
236
258
|
): Promise<O> => {
|
|
237
259
|
const childResult = await this.execute(childAction, childInput, {
|
|
260
|
+
rootRunId: initialRun.rootRunId,
|
|
238
261
|
parentRunId: runId,
|
|
239
262
|
callStack,
|
|
240
263
|
signal: controller.signal,
|
|
264
|
+
process: options.process,
|
|
265
|
+
progress: options.progress,
|
|
241
266
|
});
|
|
242
267
|
if (!childResult.ok) {
|
|
243
268
|
const err = new Error(childResult.error.message);
|
|
@@ -253,8 +278,17 @@ export class ActionRunner {
|
|
|
253
278
|
config,
|
|
254
279
|
state,
|
|
255
280
|
actions: invoker,
|
|
281
|
+
process: options.process || getProcessExecutor(),
|
|
256
282
|
log,
|
|
283
|
+
progress: options.progress || {
|
|
284
|
+
report() {},
|
|
285
|
+
},
|
|
257
286
|
signal: controller.signal,
|
|
287
|
+
run: {
|
|
288
|
+
id: runId,
|
|
289
|
+
rootId: initialRun.rootRunId,
|
|
290
|
+
parentId: options.parentRunId,
|
|
291
|
+
},
|
|
258
292
|
};
|
|
259
293
|
|
|
260
294
|
// 6. 执行 Action 业务逻辑并与取消/超时信号进行竞态
|
|
@@ -295,7 +329,7 @@ export class ActionRunner {
|
|
|
295
329
|
return {
|
|
296
330
|
ok: true,
|
|
297
331
|
runId,
|
|
298
|
-
data: rawOutput,
|
|
332
|
+
data: rawOutput as JsonValue,
|
|
299
333
|
};
|
|
300
334
|
} catch (err: any) {
|
|
301
335
|
if (isTimeout) {
|
|
@@ -12,12 +12,47 @@ import type { RuntimeStorage } from "../storage/types";
|
|
|
12
12
|
*/
|
|
13
13
|
export class ServerRuntimeRegistry {
|
|
14
14
|
private storages = new Map<string, RuntimeStorage>();
|
|
15
|
+
private listeners = new Map<string, Set<(event: { type: string; data: any }) => void>>();
|
|
15
16
|
public executionManager: ExecutionManager;
|
|
16
17
|
|
|
17
18
|
constructor() {
|
|
18
19
|
this.executionManager = new ExecutionManager();
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 订阅指定 runId 的事件(用于 SSE 流式推送)。
|
|
24
|
+
*/
|
|
25
|
+
public subscribe(runId: string, listener: (event: { type: string; data: any }) => void): () => void {
|
|
26
|
+
let set = this.listeners.get(runId);
|
|
27
|
+
if (!set) {
|
|
28
|
+
set = new Set();
|
|
29
|
+
this.listeners.set(runId, set);
|
|
30
|
+
}
|
|
31
|
+
set.add(listener);
|
|
32
|
+
return () => {
|
|
33
|
+
set?.delete(listener);
|
|
34
|
+
if (set && set.size === 0) {
|
|
35
|
+
this.listeners.delete(runId);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 向指定 runId 的订阅者广播事件。
|
|
42
|
+
*/
|
|
43
|
+
public emit(runId: string, event: { type: string; data: any }): void {
|
|
44
|
+
const set = this.listeners.get(runId);
|
|
45
|
+
if (set) {
|
|
46
|
+
for (const listener of set) {
|
|
47
|
+
try {
|
|
48
|
+
listener(event);
|
|
49
|
+
} catch {
|
|
50
|
+
// 忽略单个监听器错误
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
21
56
|
/**
|
|
22
57
|
* 获取或懒加载指定 Package 和 projectRoot 的缓存 RuntimeStorage 实例。
|
|
23
58
|
*
|