@actiondock/core 2.5.1 → 2.6.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.
Files changed (134) hide show
  1. package/dist/app/app.d.ts +14 -1
  2. package/dist/app/app.js +88 -222
  3. package/dist/app/static-index.d.ts +39 -0
  4. package/dist/app/static-index.js +164 -0
  5. package/dist/app/types.d.ts +5 -0
  6. package/dist/catalog/action-index.js +6 -4
  7. package/dist/catalog/package-catalog.js +2 -1
  8. package/dist/doctor/checks/index.d.ts +5 -0
  9. package/dist/doctor/checks/index.js +5 -0
  10. package/dist/doctor/checks/project-locate.d.ts +6 -0
  11. package/dist/doctor/checks/project-locate.js +36 -0
  12. package/dist/doctor/checks/project.d.ts +6 -0
  13. package/dist/doctor/checks/project.js +383 -0
  14. package/dist/doctor/checks/registry.d.ts +13 -0
  15. package/dist/doctor/checks/registry.js +207 -0
  16. package/dist/doctor/checks/runtime.d.ts +13 -0
  17. package/dist/doctor/checks/runtime.js +90 -0
  18. package/dist/doctor/checks/storage.d.ts +5 -0
  19. package/dist/doctor/checks/storage.js +34 -0
  20. package/dist/doctor/context.d.ts +35 -0
  21. package/dist/doctor/context.js +11 -0
  22. package/dist/doctor/doctor.d.ts +5 -0
  23. package/dist/doctor/doctor.js +33 -602
  24. package/dist/errors.d.ts +2 -0
  25. package/dist/errors.js +2 -0
  26. package/dist/execution/service.js +14 -9
  27. package/dist/host/host.d.ts +33 -0
  28. package/dist/host/host.js +222 -362
  29. package/dist/host/routing.d.ts +92 -0
  30. package/dist/host/routing.js +238 -0
  31. package/dist/host/types.d.ts +8 -0
  32. package/dist/ipc/host.js +38 -0
  33. package/dist/platform/types.d.ts +7 -0
  34. package/dist/process/control-arbiter.d.ts +107 -0
  35. package/dist/process/control-arbiter.js +251 -0
  36. package/dist/process/diagnostics-sink.d.ts +26 -0
  37. package/dist/process/diagnostics-sink.js +35 -0
  38. package/dist/process/index.d.ts +7 -0
  39. package/dist/process/index.js +7 -0
  40. package/dist/process/input-dispatcher.d.ts +88 -0
  41. package/dist/process/input-dispatcher.js +240 -0
  42. package/dist/process/managed-record.d.ts +78 -0
  43. package/dist/process/managed-record.js +1 -0
  44. package/dist/process/process-manager.d.ts +15 -56
  45. package/dist/process/process-manager.js +119 -707
  46. package/dist/process/reservation-table.d.ts +39 -0
  47. package/dist/process/reservation-table.js +80 -0
  48. package/dist/process/run-executor.d.ts +16 -0
  49. package/dist/process/run-executor.js +155 -0
  50. package/dist/process/terminal-output-cache.d.ts +66 -0
  51. package/dist/process/terminal-output-cache.js +132 -0
  52. package/dist/profile/client-actions.d.ts +56 -0
  53. package/dist/profile/client-actions.js +215 -0
  54. package/dist/profile/client-config.d.ts +20 -0
  55. package/dist/profile/client-config.js +51 -0
  56. package/dist/profile/client-health.d.ts +14 -0
  57. package/dist/profile/client-health.js +68 -0
  58. package/dist/profile/client-playbooks.d.ts +19 -0
  59. package/dist/profile/client-playbooks.js +36 -0
  60. package/dist/profile/client-query.d.ts +30 -0
  61. package/dist/profile/client-query.js +70 -0
  62. package/dist/profile/client-runs.d.ts +42 -0
  63. package/dist/profile/client-runs.js +58 -0
  64. package/dist/profile/client-state.d.ts +56 -0
  65. package/dist/profile/client-state.js +83 -0
  66. package/dist/profile/client-transport.d.ts +61 -0
  67. package/dist/profile/client-transport.js +155 -0
  68. package/dist/profile/client.d.ts +23 -142
  69. package/dist/profile/client.js +28 -588
  70. package/dist/profile/manager.js +1 -1
  71. package/dist/project/closure.d.ts +4 -4
  72. package/dist/project/closure.js +11 -5
  73. package/dist/project/init.js +2 -2
  74. package/dist/project/loader.d.ts +0 -25
  75. package/dist/project/loader.js +1 -154
  76. package/dist/project/transactions.d.ts +12 -14
  77. package/dist/project/transactions.js +60 -836
  78. package/dist/registry/registry.js +6 -4
  79. package/dist/registry/resolve.js +1 -1
  80. package/dist/runtime/action-registry.d.ts +45 -0
  81. package/dist/runtime/action-registry.js +108 -0
  82. package/dist/runtime/clock.d.ts +5 -17
  83. package/dist/runtime/clock.js +5 -15
  84. package/dist/runtime/context.d.ts +3 -0
  85. package/dist/runtime/context.js +17 -56
  86. package/dist/runtime/index.d.ts +3 -0
  87. package/dist/runtime/index.js +3 -0
  88. package/dist/runtime/package-runner-factory.d.ts +82 -0
  89. package/dist/runtime/package-runner-factory.js +186 -0
  90. package/dist/runtime/run-persistence.d.ts +85 -0
  91. package/dist/runtime/run-persistence.js +111 -0
  92. package/dist/runtime/runner.d.ts +23 -33
  93. package/dist/runtime/runner.js +92 -286
  94. package/dist/runtime/standalone.d.ts +11 -1
  95. package/dist/runtime/standalone.js +137 -25
  96. package/dist/server/dispatcher.d.ts +8 -3
  97. package/dist/server/dispatcher.js +16 -40
  98. package/dist/server/routes/runs.js +6 -4
  99. package/dist/server/security.d.ts +1 -7
  100. package/dist/server/security.js +2 -13
  101. package/dist/storage/clock.d.ts +25 -0
  102. package/dist/storage/clock.js +23 -0
  103. package/dist/storage/data-dir-lock.d.ts +3 -36
  104. package/dist/storage/data-dir-lock.js +48 -881
  105. package/dist/storage/index.d.ts +8 -1
  106. package/dist/storage/index.js +8 -2
  107. package/dist/storage/lazy.js +1 -0
  108. package/dist/storage/lock-core.d.ts +162 -0
  109. package/dist/storage/lock-core.js +885 -0
  110. package/dist/storage/sqlite.d.ts +19 -6
  111. package/dist/storage/sqlite.js +81 -89
  112. package/dist/storage/types.d.ts +27 -5
  113. package/dist/storage/types.js +21 -0
  114. package/dist/storage/utils.d.ts +22 -0
  115. package/dist/storage/utils.js +34 -0
  116. package/dist/target/local.d.ts +1 -0
  117. package/dist/target/local.js +23 -17
  118. package/dist/target/remote-errors.d.ts +21 -0
  119. package/dist/target/remote-errors.js +51 -0
  120. package/dist/target/remote-polling.d.ts +35 -0
  121. package/dist/target/remote-polling.js +104 -0
  122. package/dist/target/remote.d.ts +4 -0
  123. package/dist/target/remote.js +108 -301
  124. package/dist/target/sse-parser.d.ts +35 -0
  125. package/dist/target/sse-parser.js +163 -0
  126. package/dist/target/target.js +2 -0
  127. package/dist/target/types.d.ts +6 -0
  128. package/dist/utils/index.d.ts +1 -0
  129. package/dist/utils/index.js +1 -0
  130. package/dist/utils/net.d.ts +10 -0
  131. package/dist/utils/net.js +16 -0
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { decodeStateKey } from "@actiondock/sdk";
2
3
  import { filterWithFallbackInfo } from "../filter/index.js";
3
4
  import { createActionDockTarget } from "../target/target.js";
4
5
  import { STANDALONE_ASYNC_UNSUPPORTED } from "../errors.js";
@@ -119,7 +120,7 @@ export class StandaloneDispatcher {
119
120
  const subArgs = filteredArgs.slice(1);
120
121
  // 2. 独立入口拒绝异步启动语义
121
122
  if (args.includes("--async")) {
122
- const isJson = args.includes("--json") || args.includes("--envelope");
123
+ const isJson = args.includes("--json");
123
124
  if (isJson) {
124
125
  this.writeOut(JSON.stringify({
125
126
  ok: false,
@@ -193,8 +194,7 @@ export class StandaloneDispatcher {
193
194
  }
194
195
  }
195
196
  async handleList(target, subArgs) {
196
- const isJson = subArgs.includes("--json") || subArgs.includes("--envelope");
197
- const useEnvelope = subArgs.includes("--envelope");
197
+ const isJson = subArgs.includes("--json");
198
198
  const noFallback = subArgs.includes("--no-fallback");
199
199
  let intent;
200
200
  const positionalPatterns = [];
@@ -222,10 +222,7 @@ export class StandaloneDispatcher {
222
222
  }));
223
223
  const filterRes = filterWithFallbackInfo(list, effectiveIntent, [(a) => a.id, (a) => a.description], !noFallback);
224
224
  if (isJson) {
225
- const payload = useEnvelope
226
- ? { ok: true, data: filterRes.items }
227
- : filterRes.items;
228
- this.writeOut(JSON.stringify(payload, null, 2));
225
+ this.writeOut(JSON.stringify(filterRes.items, null, 2));
229
226
  }
230
227
  else {
231
228
  let text = `Actions in ${this.options.packageId} (v${this.options.version}):\n\n`;
@@ -238,8 +235,7 @@ export class StandaloneDispatcher {
238
235
  }
239
236
  async handleDescribe(target, subArgs) {
240
237
  const id = subArgs.find((a) => !a.startsWith("-"));
241
- const isJson = subArgs.includes("--json") || subArgs.includes("--envelope");
242
- const useEnvelope = subArgs.includes("--envelope");
238
+ const isJson = subArgs.includes("--json");
243
239
  if (!id) {
244
240
  this.writeErr("Error: Action ID is required for describe");
245
241
  return ExitCode.INVALID_ARGUMENT;
@@ -260,8 +256,7 @@ export class StandaloneDispatcher {
260
256
  outputSchema: action.outputSchema,
261
257
  };
262
258
  if (isJson) {
263
- const payload = useEnvelope ? { ok: true, data: detail } : detail;
264
- this.writeOut(JSON.stringify(payload, null, 2));
259
+ this.writeOut(JSON.stringify(detail, null, 2));
265
260
  }
266
261
  else {
267
262
  let text = `Action: ${action.id}\n`;
@@ -285,9 +280,13 @@ export class StandaloneDispatcher {
285
280
  }
286
281
  let input = {};
287
282
  let timeoutMs;
283
+ let isJson = false;
288
284
  for (let i = 0; i < subArgs.length; i++) {
289
285
  const arg = subArgs[i];
290
- if (arg === "--timeout" && i + 1 < subArgs.length) {
286
+ if (arg === "--json") {
287
+ isJson = true;
288
+ }
289
+ else if (arg === "--timeout" && i + 1 < subArgs.length) {
291
290
  timeoutMs = parseDuration(subArgs[++i]);
292
291
  }
293
292
  else if (arg.startsWith("--timeout=")) {
@@ -325,7 +324,89 @@ export class StandaloneDispatcher {
325
324
  signal,
326
325
  timeoutMs,
327
326
  });
328
- this.writeOut(JSON.stringify(result, null, 2));
327
+ if (isJson) {
328
+ this.writeOut(JSON.stringify(result, null, 2));
329
+ }
330
+ else {
331
+ if (result.ok) {
332
+ const data = result.data;
333
+ let rawText;
334
+ let metaInfo;
335
+ if (typeof data === "string") {
336
+ rawText = data;
337
+ }
338
+ else if (data !== null && typeof data === "object") {
339
+ if ("content" in data && data.content !== undefined) {
340
+ rawText =
341
+ typeof data.content === "object" && data.content !== null
342
+ ? JSON.stringify(data.content, null, 2)
343
+ : String(data.content);
344
+ const { content, ...rest } = data;
345
+ if (Object.keys(rest).length > 0) {
346
+ metaInfo = rest;
347
+ }
348
+ }
349
+ else if ("text" in data && typeof data.text === "string") {
350
+ rawText = data.text;
351
+ const { text, ...rest } = data;
352
+ if (Object.keys(rest).length > 0) {
353
+ metaInfo = rest;
354
+ }
355
+ }
356
+ else if ("message" in data && typeof data.message === "string") {
357
+ rawText = data.message;
358
+ const { message, ...rest } = data;
359
+ if (Object.keys(rest).length > 0) {
360
+ metaInfo = rest;
361
+ }
362
+ }
363
+ else {
364
+ rawText = JSON.stringify(data, null, 2);
365
+ }
366
+ }
367
+ else if (data !== undefined) {
368
+ rawText = String(data);
369
+ }
370
+ else {
371
+ rawText = "";
372
+ }
373
+ if (metaInfo) {
374
+ const parts = [];
375
+ const title = metaInfo.path ? String(metaInfo.path) : id;
376
+ parts.push(title);
377
+ if (metaInfo.startLine !== undefined && metaInfo.endLine !== undefined) {
378
+ parts.push(`lines ${metaInfo.startLine}-${metaInfo.endLine}`);
379
+ }
380
+ else if (metaInfo.line !== undefined) {
381
+ parts.push(`line ${metaInfo.line}`);
382
+ }
383
+ if (metaInfo.hasMore !== undefined) {
384
+ parts.push(`hasMore: ${metaInfo.hasMore}`);
385
+ }
386
+ if (metaInfo.truncated) {
387
+ parts.push("truncated: true");
388
+ }
389
+ const handled = new Set(["path", "startLine", "endLine", "line", "hasMore", "truncated"]);
390
+ for (const [k, v] of Object.entries(metaInfo)) {
391
+ if (!handled.has(k) && v !== undefined) {
392
+ parts.push(`${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
393
+ }
394
+ }
395
+ if (parts.length > 0) {
396
+ this.writeErr(`[${parts.join(" | ")}]`);
397
+ }
398
+ }
399
+ this.writeOut(rawText);
400
+ }
401
+ else {
402
+ this.writeErr(`Error [${result.error.code}]: ${result.error.message}`);
403
+ if (result.error.details) {
404
+ this.writeErr(typeof result.error.details === "string"
405
+ ? result.error.details
406
+ : JSON.stringify(result.error.details, null, 2));
407
+ }
408
+ }
409
+ }
329
410
  return result.ok ? ExitCode.SUCCESS : ExitCode.FAILURE;
330
411
  }
331
412
  async handleConfig(target, subArgs) {
@@ -352,9 +433,15 @@ export class StandaloneDispatcher {
352
433
  this.writeOut(item.configured ? JSON.stringify(item.value) : "undefined");
353
434
  return ExitCode.SUCCESS;
354
435
  }
355
- catch {
356
- this.writeOut("undefined");
357
- return ExitCode.SUCCESS;
436
+ catch (err) {
437
+ // 目标层能力缺失(如远端未开启管理接口)视为键未配置,输出 undefined
438
+ if (this.isTargetCapabilityError(err)) {
439
+ this.writeOut("undefined");
440
+ return ExitCode.SUCCESS;
441
+ }
442
+ // 内部异常严禁吞没:透传 stderr 并以 FAILURE 退出码暴露
443
+ this.writeErr(`Error reading config '${key}': ${err?.message || String(err)}`);
444
+ return ExitCode.FAILURE;
358
445
  }
359
446
  }
360
447
  if (sub === "set") {
@@ -425,10 +512,10 @@ export class StandaloneDispatcher {
425
512
  }
426
513
  let ns = namespace;
427
514
  let actualKey = key;
428
- if (ns === undefined && key.includes(":")) {
429
- const colonIdx = key.indexOf(":");
430
- ns = key.slice(0, colonIdx);
431
- actualKey = key.slice(colonIdx + 1);
515
+ if (ns === undefined) {
516
+ const decoded = this.decodeStateKeyArg(key);
517
+ ns = decoded.namespace || undefined;
518
+ actualKey = decoded.key;
432
519
  }
433
520
  const val = await target.getState(this.options.packageId, "", actualKey, { namespace: ns });
434
521
  if (isJson) {
@@ -448,10 +535,10 @@ export class StandaloneDispatcher {
448
535
  }
449
536
  let ns = namespace || "";
450
537
  let actualKey = key;
451
- if (namespace === undefined && key.includes(":")) {
452
- const colonIdx = key.indexOf(":");
453
- ns = key.slice(0, colonIdx);
454
- actualKey = key.slice(colonIdx + 1);
538
+ if (namespace === undefined) {
539
+ const decoded = this.decodeStateKeyArg(key);
540
+ ns = decoded.namespace;
541
+ actualKey = decoded.key;
455
542
  }
456
543
  let parsed = rawVal;
457
544
  try {
@@ -501,6 +588,30 @@ export class StandaloneDispatcher {
501
588
  this.writeErr(`Unknown state subcommand: '${sub}'`);
502
589
  return ExitCode.INVALID_ARGUMENT;
503
590
  }
591
+ /**
592
+ * 判定配置读取异常是否为目标层能力缺失(键未配置语义)而非内部故障。
593
+ */
594
+ isTargetCapabilityError(err) {
595
+ const code = String(err?.code || "");
596
+ const msg = String(err?.message || "");
597
+ return (code === "TARGET_CAPABILITY_UNAVAILABLE" ||
598
+ code === "CAPABILITY_UNAVAILABLE" ||
599
+ msg.includes("TARGET_CAPABILITY_UNAVAILABLE") ||
600
+ msg.includes("Management APIs are not enabled"));
601
+ }
602
+ /**
603
+ * 解析携带命名空间前缀的状态键参数:委托 sdk 单一事实源 decodeStateKey。
604
+ *
605
+ * 解析失败(如存在多个未转义冒号的歧义键)时按纯键处理,交由存储层兜底。
606
+ */
607
+ decodeStateKeyArg(key) {
608
+ try {
609
+ return decodeStateKey(key);
610
+ }
611
+ catch {
612
+ return { namespace: "", key };
613
+ }
614
+ }
504
615
  printHelp() {
505
616
  this.writeOut(`${this.options.packageId} (v${this.options.version})`);
506
617
  if (this.options.description)
@@ -508,7 +619,8 @@ export class StandaloneDispatcher {
508
619
  this.writeOut("Usage:");
509
620
  this.writeOut(" <cmd> list [--json] List available actions");
510
621
  this.writeOut(" <cmd> describe <id> [--json] Show action details and schemas");
511
- this.writeOut(" <cmd> run <id> [--input '<json>'] Execute action with JSON input");
622
+ this.writeOut(" <cmd> run <id> [--input '<json>'] Execute action (raw text output by default)");
623
+ this.writeOut(" <cmd> run <id> [--json] Output standard execution result in JSON format");
512
624
  this.writeOut(" <cmd> config list/get/set/delete Manage package configuration");
513
625
  this.writeOut(" <cmd> state list/get/set/delete Manage shared state store");
514
626
  this.writeOut("\nGlobal options:");
@@ -4,6 +4,8 @@
4
4
  export type FetchDispatcherProvider = () => unknown;
5
5
  /**
6
6
  * 注册全局用于忽略服务端证书校验的调度器提供者。
7
+ * 唯一注册通道:平台适配层(如 @actiondock/runtime-node 的
8
+ * installInsecureDispatcher)显式调用完成注册,不再读取全局符号注入。
7
9
  */
8
10
  export declare function setInsecureDispatcherProvider(provider?: FetchDispatcherProvider): void;
9
11
  /**
@@ -13,12 +15,15 @@ export declare function getInsecureDispatcherProvider(): FetchDispatcherProvider
13
15
  /**
14
16
  * 获取用于忽略服务端证书校验的调度器实例。
15
17
  * 纯粹解耦设计:
16
- * 1. 优先调用显式注册的提供者(如由 @actiondock/runtime-node 注册);
17
- * 2. 检查全局符号注入的提供者;
18
- * 3. 若在 Node 环境下且前述未注入,安全动态加载兜底连接池,绝不让 core 包产生静态硬编译依赖。
18
+ * 1. 优先调用显式注册的提供者(如由 @actiondock/runtime-node 显式安装);
19
+ * 2. 若在 Node 环境下且无注册提供者,动态加载 @actiondock/runtime-node
20
+ * (其入口聚合点会显式完成安装),绝不让 core 包产生静态硬编译依赖;
21
+ * 3. 仍不可得时返回 undefined,由调用方回退默认 fetch,绝不在 core 内
22
+ * 直接探测加载 undici,避免与平台适配层形成层次倒置与隐藏依赖环。
19
23
  */
20
24
  export declare function getInsecureDispatcher(): unknown;
21
25
  /**
22
26
  * 显式强制重置并销毁调度器连接池(仅用于单元测试重置或进程退出)。
27
+ * core 自身不再持有任何兜底连接池,关闭语义完全委托已注册的平台提供者宿主。
23
28
  */
24
29
  export declare function closeInsecureDispatcher(): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  let insecureDispatcherProvider;
3
- let fallbackInsecureAgent;
4
3
  /**
5
4
  * 注册全局用于忽略服务端证书校验的调度器提供者。
5
+ * 唯一注册通道:平台适配层(如 @actiondock/runtime-node 的
6
+ * installInsecureDispatcher)显式调用完成注册,不再读取全局符号注入。
6
7
  */
7
8
  export function setInsecureDispatcherProvider(provider) {
8
9
  insecureDispatcherProvider = provider;
@@ -16,18 +17,16 @@ export function getInsecureDispatcherProvider() {
16
17
  /**
17
18
  * 获取用于忽略服务端证书校验的调度器实例。
18
19
  * 纯粹解耦设计:
19
- * 1. 优先调用显式注册的提供者(如由 @actiondock/runtime-node 注册);
20
- * 2. 检查全局符号注入的提供者;
21
- * 3. 若在 Node 环境下且前述未注入,安全动态加载兜底连接池,绝不让 core 包产生静态硬编译依赖。
20
+ * 1. 优先调用显式注册的提供者(如由 @actiondock/runtime-node 显式安装);
21
+ * 2. 若在 Node 环境下且无注册提供者,动态加载 @actiondock/runtime-node
22
+ * (其入口聚合点会显式完成安装),绝不让 core 包产生静态硬编译依赖;
23
+ * 3. 仍不可得时返回 undefined,由调用方回退默认 fetch,绝不在 core 内
24
+ * 直接探测加载 undici,避免与平台适配层形成层次倒置与隐藏依赖环。
22
25
  */
23
26
  export function getInsecureDispatcher() {
24
27
  if (insecureDispatcherProvider) {
25
28
  return insecureDispatcherProvider();
26
29
  }
27
- const globalProvider = globalThis[Symbol.for("actiondock.insecureDispatcherProvider")];
28
- if (typeof globalProvider === "function") {
29
- return globalProvider();
30
- }
31
30
  if (typeof process !== "undefined" && process.versions?.node) {
32
31
  try {
33
32
  const req = createRequire(import.meta.url);
@@ -39,48 +38,25 @@ export function getInsecureDispatcher() {
39
38
  catch {
40
39
  // 忽略未安装 @actiondock/runtime-node 的情况
41
40
  }
42
- if (!fallbackInsecureAgent || fallbackInsecureAgent.closed || fallbackInsecureAgent.destroyed) {
43
- try {
44
- const req = createRequire(import.meta.url);
45
- const undici = req("undici");
46
- if (undici?.Agent) {
47
- fallbackInsecureAgent = new undici.Agent({
48
- connect: {
49
- rejectUnauthorized: false,
50
- },
51
- bodyTimeout: 0,
52
- });
53
- }
54
- }
55
- catch {
56
- // 忽略非 Node 环境或未安装 undici 的异常
57
- }
58
- }
59
- return fallbackInsecureAgent;
60
41
  }
61
42
  return undefined;
62
43
  }
63
44
  /**
64
45
  * 显式强制重置并销毁调度器连接池(仅用于单元测试重置或进程退出)。
46
+ * core 自身不再持有任何兜底连接池,关闭语义完全委托已注册的平台提供者宿主。
65
47
  */
66
48
  export async function closeInsecureDispatcher() {
67
- const globalClose = globalThis[Symbol.for("actiondock.closeInsecureDispatcher")];
68
- if (typeof globalClose === "function") {
69
- try {
70
- await globalClose();
71
- }
72
- catch {
73
- // 忽略关闭异常
74
- }
75
- }
76
- if (fallbackInsecureAgent) {
77
- const agent = fallbackInsecureAgent;
78
- fallbackInsecureAgent = undefined;
49
+ if (insecureDispatcherProvider) {
50
+ // 提供者由平台层注册:优先委托提供者宿主的关闭语义(如可用)
79
51
  try {
80
- await agent.close();
52
+ const req = createRequire(import.meta.url);
53
+ const runtimeNode = req("@actiondock/runtime-node");
54
+ if (typeof runtimeNode?.closeInsecureDispatcher === "function") {
55
+ await runtimeNode.closeInsecureDispatcher();
56
+ }
81
57
  }
82
58
  catch {
83
- // 忽略关闭异常
59
+ // 忽略未安装 @actiondock/runtime-node 的情况
84
60
  }
85
61
  }
86
62
  }
@@ -49,12 +49,11 @@ export async function handleRunsRoutes(ctx) {
49
49
  }
50
50
  if (app.storage && typeof app.storage.listRuns === "function") {
51
51
  try {
52
- const records = app.storage.listRuns({ actionId, limit });
52
+ // 状态过滤下推到存储层 SQL,避免先分页后过滤导致某包前 N 条非目标状态时贡献 0
53
+ const records = app.storage.listRuns({ actionId, status, limit });
53
54
  for (const r of records) {
54
55
  if (!seenRunIds.has(r.id)) {
55
56
  seenRunIds.add(r.id);
56
- if (status && r.status !== status)
57
- continue;
58
57
  if (options.packageAllowlist &&
59
58
  options.packageAllowlist.length > 0 &&
60
59
  r.packageId &&
@@ -65,7 +64,10 @@ export async function handleRunsRoutes(ctx) {
65
64
  }
66
65
  }
67
66
  }
68
- catch { }
67
+ catch (err) {
68
+ // 单包查询失败不阻断整体列表响应,但必须可观测
69
+ console.warn(`[actiondock] listRuns failed for package '${app.packageId}': ${err instanceof Error ? err.message : String(err)}`);
70
+ }
69
71
  }
70
72
  }
71
73
  allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
@@ -1,3 +1,4 @@
1
+ export { isLoopbackHost } from "../utils/net.js";
1
2
  /**
2
3
  * 恒定时间字符串比较(Constant-time comparison)。
3
4
  * 使用底层的 `crypto.timingSafeEqual` 防范时序攻击(Timing Attack)。
@@ -7,13 +8,6 @@
7
8
  * @returns 两个字符串内容是否完全一致
8
9
  */
9
10
  export declare function safeEqual(a: string, b: string): boolean;
10
- /**
11
- * 检查指定的主机地址是否为本地回环接口(Loopback Host)。
12
- * 支持 127.0.0.1, localhost, ::1 等形式。
13
- *
14
- * @param host 主机名或 IP 字符串
15
- */
16
- export declare function isLoopbackHost(host: string): boolean;
17
11
  /**
18
12
  * 校验 HTTP 请求中的鉴权令牌选项。
19
13
  */
@@ -1,4 +1,6 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
+ // isLoopbackHost 已上移至 utils/net.ts(通用谓词单一事实源),此处仅保留 re-export 兼容旧引用路径
3
+ export { isLoopbackHost } from "../utils/net.js";
2
4
  /**
3
5
  * 恒定时间字符串比较(Constant-time comparison)。
4
6
  * 使用底层的 `crypto.timingSafeEqual` 防范时序攻击(Timing Attack)。
@@ -15,19 +17,6 @@ export function safeEqual(a, b) {
15
17
  }
16
18
  return timingSafeEqual(aa, bb);
17
19
  }
18
- /**
19
- * 检查指定的主机地址是否为本地回环接口(Loopback Host)。
20
- * 支持 127.0.0.1, localhost, ::1 等形式。
21
- *
22
- * @param host 主机名或 IP 字符串
23
- */
24
- export function isLoopbackHost(host) {
25
- const trimmed = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
26
- return (trimmed === "127.0.0.1" ||
27
- trimmed === "::1" ||
28
- trimmed === "localhost" ||
29
- trimmed === "0:0:0:0:0:0:0:1");
30
- }
31
20
  /**
32
21
  * 校验 HTTP 请求中的鉴权令牌是否有效。
33
22
  *
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 统一时间与时钟基础契约。
3
+ *
4
+ * 单一事实源:Clock 接口与 SystemClock 实现定义于此,runtime/clock.ts 与
5
+ * storage/types.ts 一律从本文件引用或转发,禁止重复实现。
6
+ */
7
+ /**
8
+ * 统一时间与时钟接口。
9
+ */
10
+ export interface Clock {
11
+ /** 获取当前系统墙上时间 */
12
+ now(): Date;
13
+ /** 获取单调递增时间戳(单位:毫秒) */
14
+ monotonic(): number;
15
+ /** 异步休眠指定毫秒 */
16
+ sleep(ms: number): Promise<void>;
17
+ }
18
+ /**
19
+ * 生产环境系统时钟实现。
20
+ */
21
+ export declare class SystemClock implements Clock {
22
+ now(): Date;
23
+ monotonic(): number;
24
+ sleep(ms: number): Promise<void>;
25
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 统一时间与时钟基础契约。
3
+ *
4
+ * 单一事实源:Clock 接口与 SystemClock 实现定义于此,runtime/clock.ts 与
5
+ * storage/types.ts 一律从本文件引用或转发,禁止重复实现。
6
+ */
7
+ /**
8
+ * 生产环境系统时钟实现。
9
+ */
10
+ export class SystemClock {
11
+ now() {
12
+ return new Date();
13
+ }
14
+ monotonic() {
15
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
16
+ return performance.now();
17
+ }
18
+ return Date.now();
19
+ }
20
+ sleep(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+ }
@@ -1,3 +1,4 @@
1
+ import { cleanStaleQuarantines, isProcessAlive, parseQuarantineTimestamp, safeRemoveStaleReclaimGuard, safeRollbackLock } from "./lock-core.js";
1
2
  /**
2
3
  * 数据目录排他锁元数据契约。
3
4
  */
@@ -17,41 +18,7 @@ export interface DataDirLockInfo {
17
18
  /** 宿主会话标识 */
18
19
  hostSessionId?: string;
19
20
  }
20
- /**
21
- * 检查目标进程是否处于存活状态。
22
- *
23
- * @param pid 待检测的进程标识符
24
- */
25
- export declare function isProcessAlive(pid: number): boolean;
26
- /**
27
- * 安全清理陈旧接管守卫(reclaim guard)。
28
- * 采用原子重命名检疫并严格核对 guardToken 与持有者存活状态,确保仅删除目标陈旧 guard,严禁误删活跃守卫或并发新守卫。
29
- *
30
- * @param reclaimPath 目标守卫路径
31
- * @param expectedGuardToken 预期持有的守卫令牌(必填,拒绝未指定令牌的盲目清理)
32
- */
33
- export declare function safeRemoveStaleReclaimGuard(reclaimPath: string, expectedGuardToken: string): void;
34
- /**
35
- * 从隔离或回滚临时目录名称中解析操作者 PID 与隔离生成时间戳。
36
- * 命名规范:*.(quarantine|rollback|release).<pid>.<timestamp>.<uuid>
37
- */
38
- export declare function parseQuarantineTimestamp(entryName: string): {
39
- operatorPid?: number;
40
- timestamp?: number;
41
- };
42
- /**
43
- * 清理过期的隔离目录(GC 回收机制)。
44
- * 实施分流存活校验,防止误删活跃持锁者与回滚中目录,同时防止孤儿目录无限泄漏:
45
- * - 共同前置条件:存活年龄必须超过 maxAgeMs(默认 10000ms),未超期前严禁清理(保持宽限期)。
46
- * - 活跃隔离目录(quarantine / rollback / release):若 operatorPid 存活或内部 metadata.json 记录的 pid 与 childPids 存活,跳过不予清理。
47
- * - 孤儿脱离目录(orphan):内部旧 metadata.pid 不再作为续命依据;提取其内部 orphanToken,仅当当前规范主路径(主锁目录与 reclaim 目录)依然持有该凭证时保守跳过;规范路径不存在或已变更凭据时直接安全清理。
48
- */
49
- export declare function cleanStaleQuarantines(parentDir: string, basePrefix: string, maxAgeMs?: number, deadline?: number): void;
50
- /**
51
- * 安全回滚当前进程创建的主锁。
52
- * 优先核对 lockToken,回退核对 sessionToken,防止误删接管者或并发新锁。
53
- */
54
- export declare function safeRollbackLock(lockPath: string, expectedSessionToken: string, expectedLockToken?: string): void;
21
+ export { isProcessAlive, parseQuarantineTimestamp, cleanStaleQuarantines, safeRemoveStaleReclaimGuard, safeRollbackLock, };
55
22
  /**
56
23
  * 安全释放主锁。
57
24
  * 仅当锁目录中持有当前 lockToken / sessionToken 时才删除,杜绝删除他人新锁。
@@ -95,7 +62,7 @@ export declare class DataDirLock {
95
62
  /**
96
63
  * 尝试获取指定数据目录的排他锁。
97
64
  *
98
- * 仲裁规则:
65
+ * 仲裁规则由目录锁内核(lock-core)统一承载:
99
66
  * - 引入所有竞争者均遵守的原子 reclaim guard 机制(lockDirPath.reclaim),每个 guard 具备全局唯一 guardToken。
100
67
  * - 基于原子目录创建 mkdirSync(lockDirPath, { mode: 0o700 }) 确立所有权,并在其下存放 metadata.json。
101
68
  * - 兼容已有常规文件锁(如老版本或测试 mock 场景)。