@actiondock/core 2.0.2 → 2.0.4
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/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/server.ts +102 -8
- package/src/server/types.ts +11 -0
- package/src/storage/driver.ts +126 -0
- package/src/storage/index.ts +4 -3
- package/src/storage/sqlite.ts +331 -251
- package/src/storage/types.ts +41 -23
|
@@ -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) {
|
package/src/server/server.ts
CHANGED
|
@@ -18,10 +18,107 @@ import {
|
|
|
18
18
|
} from "../registry/registry";
|
|
19
19
|
import { ActionRunner } from "../runtime/runner";
|
|
20
20
|
import type { RuntimeStorage } from "../storage/types";
|
|
21
|
+
import { createServer as createNodeHttpServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
22
|
+
import { Readable } from "node:stream";
|
|
23
|
+
import { pipeline } from "node:stream/promises";
|
|
21
24
|
import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "./body";
|
|
22
25
|
import { ServerRuntimeRegistry } from "./runtime-registry";
|
|
23
26
|
import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security";
|
|
24
|
-
import type { ActionDockServerInstance, ServerOptions } from "./types";
|
|
27
|
+
import type { ActionDockServerInstance, CoreHttpServerFactory, CoreHttpServerInstance, ServerOptions } from "./types";
|
|
28
|
+
|
|
29
|
+
let customHttpServerFactory: CoreHttpServerFactory | undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 注册自定义 HTTP 服务端工厂(用于 Node.js / Bun 运行时环境适配)。
|
|
33
|
+
*/
|
|
34
|
+
export function setHttpServerFactory(factory: CoreHttpServerFactory): void {
|
|
35
|
+
customHttpServerFactory = factory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
|
|
40
|
+
*/
|
|
41
|
+
export function launchHttpServer(
|
|
42
|
+
port: number,
|
|
43
|
+
host: string,
|
|
44
|
+
fetchHandler: (req: Request) => Promise<Response>
|
|
45
|
+
): CoreHttpServerInstance {
|
|
46
|
+
if (customHttpServerFactory) {
|
|
47
|
+
return customHttpServerFactory({ port, host, fetch: fetchHandler }) as CoreHttpServerInstance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 若处于原生 Bun 运行时
|
|
51
|
+
if (typeof (globalThis as any).Bun !== "undefined" && typeof (globalThis as any).Bun.serve === "function") {
|
|
52
|
+
const bunServer = (globalThis as any).Bun.serve({
|
|
53
|
+
port,
|
|
54
|
+
hostname: host,
|
|
55
|
+
fetch: fetchHandler,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
port: bunServer.port,
|
|
59
|
+
stop: (closeActive?: boolean) => bunServer.stop(closeActive),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Node.js 原生 node:http 兜底实现
|
|
64
|
+
const srv = createNodeHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
65
|
+
try {
|
|
66
|
+
const protocol = (req.socket as any)?.encrypted ? "https" : "http";
|
|
67
|
+
const hostHeader = req.headers.host || "127.0.0.1";
|
|
68
|
+
const url = new URL(req.url || "/", `${protocol}://${hostHeader}`).href;
|
|
69
|
+
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
72
|
+
if (v === undefined) continue;
|
|
73
|
+
if (Array.isArray(v)) {
|
|
74
|
+
for (const item of v) headers.append(k, item);
|
|
75
|
+
} else {
|
|
76
|
+
headers.set(k, v);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const method = (req.method || "GET").toUpperCase();
|
|
81
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
82
|
+
const init: RequestInit = { method, headers };
|
|
83
|
+
if (hasBody) {
|
|
84
|
+
(init as any).body = Readable.toWeb(req);
|
|
85
|
+
(init as any).duplex = "half";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const webReq = new Request(url, init);
|
|
89
|
+
const webRes = await fetchHandler(webReq);
|
|
90
|
+
|
|
91
|
+
res.statusCode = webRes.status;
|
|
92
|
+
if (webRes.statusText) res.statusMessage = webRes.statusText;
|
|
93
|
+
webRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
94
|
+
|
|
95
|
+
if (!webRes.body) {
|
|
96
|
+
res.end();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await pipeline(Readable.fromWeb(webRes.body as any), res);
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
if (!res.headersSent) {
|
|
102
|
+
res.statusCode = 500;
|
|
103
|
+
res.end(JSON.stringify({ error: err?.message || String(err) }));
|
|
104
|
+
} else {
|
|
105
|
+
res.destroy(err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
srv.listen(port, host);
|
|
111
|
+
const addr = srv.address();
|
|
112
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
port: actualPort,
|
|
116
|
+
stop: () => {
|
|
117
|
+
srv.close();
|
|
118
|
+
(srv as any).closeAllConnections?.();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
25
122
|
|
|
26
123
|
/**
|
|
27
124
|
* 辅助函数:快速构造带 CORS 头的 JSON HTTP 响应。
|
|
@@ -176,11 +273,8 @@ export function startActionDockServer(
|
|
|
176
273
|
|
|
177
274
|
const runtimeRegistry = new ServerRuntimeRegistry();
|
|
178
275
|
|
|
179
|
-
const server =
|
|
180
|
-
|
|
181
|
-
hostname: host,
|
|
182
|
-
async fetch(req) {
|
|
183
|
-
const origin = req.headers.get("origin");
|
|
276
|
+
const server = launchHttpServer(port, host, async (req) => {
|
|
277
|
+
const origin = req.headers.get("origin");
|
|
184
278
|
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
185
279
|
|
|
186
280
|
if (req.method === "OPTIONS") {
|
|
@@ -1379,8 +1473,8 @@ export function startActionDockServer(
|
|
|
1379
1473
|
404,
|
|
1380
1474
|
corsHeaders
|
|
1381
1475
|
);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1384
1478
|
|
|
1385
1479
|
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
1386
1480
|
const url = `http://${actualHost}:${server.port}`;
|
package/src/server/types.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { ServerRuntimeRegistry } from "./runtime-registry";
|
|
2
2
|
|
|
3
|
+
export interface CoreHttpServerInstance {
|
|
4
|
+
port: number;
|
|
5
|
+
stop: (closeActiveConnections?: boolean) => void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CoreHttpServerFactory = (options: {
|
|
9
|
+
port: number;
|
|
10
|
+
host: string;
|
|
11
|
+
fetch: (req: Request) => Promise<Response>;
|
|
12
|
+
}) => CoreHttpServerInstance | Promise<CoreHttpServerInstance>;
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* 启动 ActionDock HTTP Runner 服务端的配置选项。
|
|
5
16
|
*/
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import type { SqliteDriver } from "./types";
|
|
3
|
+
|
|
4
|
+
export type SqliteDriverFactory = (dbPath: string) => SqliteDriver;
|
|
5
|
+
|
|
6
|
+
/** ESM 环境下可用的 CommonJS require,用于动态加载 node:sqlite / bun:sqlite */
|
|
7
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
let customDriverFactory: SqliteDriverFactory | undefined;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 注册全局默认 SQLite 驱动工厂。
|
|
13
|
+
*/
|
|
14
|
+
export function setSqliteDriverFactory(factory: SqliteDriverFactory): void {
|
|
15
|
+
customDriverFactory = factory;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 创建默认 SQLite 驱动实例。
|
|
20
|
+
* 优先使用外部注册的工厂,其次根据当前运行时环境自动适配。
|
|
21
|
+
*/
|
|
22
|
+
export function createDefaultSqliteDriver(dbPath: string): SqliteDriver {
|
|
23
|
+
if (customDriverFactory) {
|
|
24
|
+
return customDriverFactory(dbPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 检查是否在 Bun 运行时环境
|
|
28
|
+
if (typeof (globalThis as any).Bun !== "undefined") {
|
|
29
|
+
try {
|
|
30
|
+
const { Database } = (globalThis as any).Bun.sqlite || cjsRequire("bun:sqlite");
|
|
31
|
+
const db = new Database(dbPath);
|
|
32
|
+
// bun:sqlite 的 statement 未 finalize 时会一直持有数据库文件句柄,
|
|
33
|
+
// Windows 下导致 db.close() 后文件仍被锁(EBUSY 无法删除)。
|
|
34
|
+
// 跟踪全部 prepared statement,close() 时统一 finalize 释放句柄。
|
|
35
|
+
const openStatements = new Set<any>();
|
|
36
|
+
return {
|
|
37
|
+
exec(sql: string) {
|
|
38
|
+
db.exec(sql);
|
|
39
|
+
},
|
|
40
|
+
prepare(sql: string) {
|
|
41
|
+
const stmt = db.prepare(sql);
|
|
42
|
+
openStatements.add(stmt);
|
|
43
|
+
return {
|
|
44
|
+
run(...args: any[]) {
|
|
45
|
+
const params = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
46
|
+
const res = stmt.run(...params);
|
|
47
|
+
return { changes: res.changes, lastInsertRowid: res.lastInsertRowid };
|
|
48
|
+
},
|
|
49
|
+
get<T>(...args: any[]): T | undefined {
|
|
50
|
+
const params = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
51
|
+
return stmt.get(...params) as T | undefined;
|
|
52
|
+
},
|
|
53
|
+
all<T>(...args: any[]): T[] {
|
|
54
|
+
const params = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
55
|
+
return stmt.all(...params) as T[];
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
transaction<T>(fn: () => T extends PromiseLike<unknown> ? never : T): T {
|
|
60
|
+
return db.transaction(fn)() as T;
|
|
61
|
+
},
|
|
62
|
+
close() {
|
|
63
|
+
for (const stmt of openStatements) {
|
|
64
|
+
try {
|
|
65
|
+
stmt.finalize();
|
|
66
|
+
} catch {
|
|
67
|
+
// 已 finalize 或重复释放时忽略
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
openStatements.clear();
|
|
71
|
+
db.close();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
} catch {
|
|
75
|
+
// 若在 Bun 下获取 bun:sqlite 失败,回退到标准 Node 驱动尝试
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 在 Node.js 环境下使用 node:sqlite
|
|
80
|
+
try {
|
|
81
|
+
const { DatabaseSync } = cjsRequire("node:sqlite");
|
|
82
|
+
const db = new DatabaseSync(dbPath);
|
|
83
|
+
return {
|
|
84
|
+
exec(sql: string) {
|
|
85
|
+
db.exec(sql);
|
|
86
|
+
},
|
|
87
|
+
prepare(sql: string) {
|
|
88
|
+
const stmt = db.prepare(sql);
|
|
89
|
+
return {
|
|
90
|
+
run(...args: any[]) {
|
|
91
|
+
const params = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
92
|
+
const res = stmt.run(...params);
|
|
93
|
+
return { changes: res.changes, lastInsertRowid: res.lastInsertRowid };
|
|
94
|
+
},
|
|
95
|
+
get<T>(...args: any[]): T | undefined {
|
|
96
|
+
const params = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
97
|
+
return stmt.get(...params) as T | undefined;
|
|
98
|
+
},
|
|
99
|
+
all<T>(...args: any[]): T[] {
|
|
100
|
+
const params = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
101
|
+
return stmt.all(...params) as T[];
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
transaction<T>(fn: () => T extends PromiseLike<unknown> ? never : T): T {
|
|
106
|
+
db.exec("BEGIN");
|
|
107
|
+
try {
|
|
108
|
+
const res = fn();
|
|
109
|
+
if (res && typeof (res as any).then === "function") {
|
|
110
|
+
throw new Error("Async transactions are not allowed in SQLite");
|
|
111
|
+
}
|
|
112
|
+
db.exec("COMMIT");
|
|
113
|
+
return res;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
db.exec("ROLLBACK");
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
close() {
|
|
120
|
+
db.close();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
throw new Error(`Failed to initialize SQLite driver: ${err?.message || String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
1
|
import { join } from "node:path";
|
|
2
|
+
import { getActionDockHome } from "../utils";
|
|
3
3
|
import { SqliteRuntimeStorage } from "./sqlite";
|
|
4
4
|
import type { RuntimeStorage, StorageOptions } from "./types";
|
|
5
5
|
|
|
6
|
+
export * from "./driver";
|
|
6
7
|
export * from "./mask";
|
|
7
8
|
export * from "./sqlite";
|
|
8
9
|
export * from "./types";
|
|
@@ -33,7 +34,7 @@ export function resolveDatabasePath(
|
|
|
33
34
|
return join(options.projectRoot, ".actiondock", "runtime.db");
|
|
34
35
|
}
|
|
35
36
|
// 独立执行二进制默认存储路径: ~/.actiondock/data/<package-id>/runtime.db
|
|
36
|
-
return join(
|
|
37
|
+
return join(getActionDockHome(), ".actiondock", "data", packageId, "runtime.db");
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -57,7 +58,7 @@ export function createStorage(
|
|
|
57
58
|
* @param customHome 自定义家目录路径(可选)
|
|
58
59
|
*/
|
|
59
60
|
export function createGlobalStorage(customHome?: string): RuntimeStorage {
|
|
60
|
-
const baseDir = customHome
|
|
61
|
+
const baseDir = getActionDockHome(customHome);
|
|
61
62
|
const dbPath = join(baseDir, ".actiondock", "global.db");
|
|
62
63
|
return new SqliteRuntimeStorage({ dbPath, packageId: "__global__" });
|
|
63
64
|
}
|