@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.
@@ -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
+ }
@@ -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
- parentRunId: options.parentRunId,
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) {
@@ -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 = Bun.serve({
180
- port,
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}`;
@@ -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
+ }
@@ -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(homedir(), ".actiondock", "data", packageId, "runtime.db");
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 || process.env.ACTIONDOCK_HOME || homedir();
61
+ const baseDir = getActionDockHome(customHome);
61
62
  const dbPath = join(baseDir, ".actiondock", "global.db");
62
63
  return new SqliteRuntimeStorage({ dbPath, packageId: "__global__" });
63
64
  }