@actiondock/core 2.5.1 → 2.7.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 (175) 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 +22 -0
  25. package/dist/errors.js +22 -0
  26. package/dist/execution/service.js +14 -9
  27. package/dist/export/templates.js +42 -30
  28. package/dist/host/host.d.ts +33 -0
  29. package/dist/host/host.js +222 -362
  30. package/dist/host/routing.d.ts +92 -0
  31. package/dist/host/routing.js +238 -0
  32. package/dist/host/types.d.ts +8 -0
  33. package/dist/index.d.ts +3 -1
  34. package/dist/index.js +3 -1
  35. package/dist/input/advice.d.ts +100 -0
  36. package/dist/input/advice.js +767 -0
  37. package/dist/input/describe.d.ts +106 -0
  38. package/dist/input/describe.js +176 -0
  39. package/dist/input/file-input.d.ts +54 -0
  40. package/dist/input/file-input.js +124 -0
  41. package/dist/input/flat-decode.d.ts +11 -0
  42. package/dist/input/flat-decode.js +13 -0
  43. package/dist/input/flat-errors.d.ts +72 -0
  44. package/dist/input/flat-errors.js +87 -0
  45. package/dist/input/flat-materializer.d.ts +29 -0
  46. package/dist/input/flat-materializer.js +179 -0
  47. package/dist/input/flat-parser.d.ts +50 -0
  48. package/dist/input/flat-parser.js +169 -0
  49. package/dist/input/flat-predicates.d.ts +34 -0
  50. package/dist/input/flat-predicates.js +50 -0
  51. package/dist/input/index.d.ts +14 -0
  52. package/dist/input/index.js +14 -0
  53. package/dist/input/input-resolver.d.ts +63 -0
  54. package/dist/input/input-resolver.js +254 -0
  55. package/dist/input/json-depth-scanner.d.ts +17 -0
  56. package/dist/input/json-depth-scanner.js +49 -0
  57. package/dist/input/metadata.d.ts +99 -0
  58. package/dist/input/metadata.js +99 -0
  59. package/dist/input/stdin-input.d.ts +26 -0
  60. package/dist/input/stdin-input.js +125 -0
  61. package/dist/input/utf8.d.ts +19 -0
  62. package/dist/input/utf8.js +36 -0
  63. package/dist/input/validation-mapper.d.ts +27 -0
  64. package/dist/input/validation-mapper.js +144 -0
  65. package/dist/ipc/host.js +38 -0
  66. package/dist/json/index.d.ts +1 -0
  67. package/dist/json/index.js +1 -0
  68. package/dist/json/value-validator.d.ts +88 -0
  69. package/dist/json/value-validator.js +330 -0
  70. package/dist/platform/types.d.ts +7 -0
  71. package/dist/process/control-arbiter.d.ts +107 -0
  72. package/dist/process/control-arbiter.js +251 -0
  73. package/dist/process/diagnostics-sink.d.ts +26 -0
  74. package/dist/process/diagnostics-sink.js +35 -0
  75. package/dist/process/index.d.ts +7 -0
  76. package/dist/process/index.js +7 -0
  77. package/dist/process/input-dispatcher.d.ts +88 -0
  78. package/dist/process/input-dispatcher.js +240 -0
  79. package/dist/process/managed-record.d.ts +78 -0
  80. package/dist/process/managed-record.js +1 -0
  81. package/dist/process/process-manager.d.ts +15 -56
  82. package/dist/process/process-manager.js +119 -707
  83. package/dist/process/reservation-table.d.ts +39 -0
  84. package/dist/process/reservation-table.js +80 -0
  85. package/dist/process/run-executor.d.ts +16 -0
  86. package/dist/process/run-executor.js +155 -0
  87. package/dist/process/terminal-output-cache.d.ts +66 -0
  88. package/dist/process/terminal-output-cache.js +132 -0
  89. package/dist/profile/client-actions.d.ts +56 -0
  90. package/dist/profile/client-actions.js +215 -0
  91. package/dist/profile/client-config.d.ts +20 -0
  92. package/dist/profile/client-config.js +51 -0
  93. package/dist/profile/client-health.d.ts +14 -0
  94. package/dist/profile/client-health.js +69 -0
  95. package/dist/profile/client-playbooks.d.ts +19 -0
  96. package/dist/profile/client-playbooks.js +36 -0
  97. package/dist/profile/client-query.d.ts +30 -0
  98. package/dist/profile/client-query.js +70 -0
  99. package/dist/profile/client-runs.d.ts +42 -0
  100. package/dist/profile/client-runs.js +58 -0
  101. package/dist/profile/client-state.d.ts +56 -0
  102. package/dist/profile/client-state.js +83 -0
  103. package/dist/profile/client-transport.d.ts +61 -0
  104. package/dist/profile/client-transport.js +156 -0
  105. package/dist/profile/client.d.ts +23 -142
  106. package/dist/profile/client.js +28 -588
  107. package/dist/profile/manager.js +1 -1
  108. package/dist/project/closure.d.ts +4 -4
  109. package/dist/project/closure.js +11 -5
  110. package/dist/project/init.js +2 -2
  111. package/dist/project/loader.d.ts +0 -25
  112. package/dist/project/loader.js +1 -154
  113. package/dist/project/transactions.d.ts +12 -14
  114. package/dist/project/transactions.js +60 -836
  115. package/dist/registry/registry.js +6 -4
  116. package/dist/registry/resolve.js +1 -1
  117. package/dist/runtime/action-registry.d.ts +45 -0
  118. package/dist/runtime/action-registry.js +108 -0
  119. package/dist/runtime/clock.d.ts +5 -17
  120. package/dist/runtime/clock.js +5 -15
  121. package/dist/runtime/context.d.ts +3 -0
  122. package/dist/runtime/context.js +17 -56
  123. package/dist/runtime/index.d.ts +3 -0
  124. package/dist/runtime/index.js +3 -0
  125. package/dist/runtime/package-runner-factory.d.ts +82 -0
  126. package/dist/runtime/package-runner-factory.js +186 -0
  127. package/dist/runtime/run-persistence.d.ts +85 -0
  128. package/dist/runtime/run-persistence.js +111 -0
  129. package/dist/runtime/runner.d.ts +25 -45
  130. package/dist/runtime/runner.js +130 -354
  131. package/dist/runtime/standalone.d.ts +25 -2
  132. package/dist/runtime/standalone.js +305 -87
  133. package/dist/schema/index.d.ts +1 -0
  134. package/dist/schema/index.js +1 -0
  135. package/dist/schema/validator.d.ts +10 -0
  136. package/dist/schema/validator.js +48 -54
  137. package/dist/server/dispatcher.d.ts +8 -3
  138. package/dist/server/dispatcher.js +16 -40
  139. package/dist/server/routes/runs.js +6 -4
  140. package/dist/server/security.d.ts +1 -7
  141. package/dist/server/security.js +2 -13
  142. package/dist/storage/clock.d.ts +25 -0
  143. package/dist/storage/clock.js +23 -0
  144. package/dist/storage/data-dir-lock.d.ts +3 -36
  145. package/dist/storage/data-dir-lock.js +48 -881
  146. package/dist/storage/index.d.ts +8 -1
  147. package/dist/storage/index.js +8 -2
  148. package/dist/storage/lazy.js +1 -0
  149. package/dist/storage/lock-core.d.ts +162 -0
  150. package/dist/storage/lock-core.js +885 -0
  151. package/dist/storage/sqlite.d.ts +19 -6
  152. package/dist/storage/sqlite.js +81 -89
  153. package/dist/storage/types.d.ts +27 -5
  154. package/dist/storage/types.js +21 -0
  155. package/dist/storage/utils.d.ts +22 -0
  156. package/dist/storage/utils.js +34 -0
  157. package/dist/target/local.d.ts +1 -0
  158. package/dist/target/local.js +23 -17
  159. package/dist/target/remote-errors.d.ts +21 -0
  160. package/dist/target/remote-errors.js +51 -0
  161. package/dist/target/remote-polling.d.ts +35 -0
  162. package/dist/target/remote-polling.js +104 -0
  163. package/dist/target/remote.d.ts +4 -0
  164. package/dist/target/remote.js +108 -301
  165. package/dist/target/sse-parser.d.ts +35 -0
  166. package/dist/target/sse-parser.js +163 -0
  167. package/dist/target/target.js +2 -0
  168. package/dist/target/types.d.ts +6 -0
  169. package/dist/utils/index.d.ts +1 -0
  170. package/dist/utils/index.js +1 -0
  171. package/dist/utils/net.d.ts +10 -0
  172. package/dist/utils/net.js +16 -0
  173. package/dist/version.d.ts +1 -1
  174. package/dist/version.js +1 -1
  175. package/package.json +2 -2
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 远端通信错误翻译层。
3
+ *
4
+ * 职责单一:将 profile/client 传输层抛出的底层异常翻译为统一的
5
+ * TargetError 结构化异常,供 RemoteActionDockTarget 各操作边界复用;
6
+ * 优先读取传输层透传的结构化错误码,无法识别时原样透传。
7
+ */
8
+ import { STATE_KEY_NOT_FOUND } from "../errors.js";
9
+ import { TARGET_CAPABILITY_UNAVAILABLE, TARGET_PROTOCOL_UNSUPPORTED, TARGET_RESULT_UNKNOWN, TargetError, } from "./types.js";
10
+ /**
11
+ * 将远端通信异常包装翻译为统一的 TargetError。
12
+ *
13
+ * 无法识别的异常按防御与透明原则原样抛出,严禁静默吞没。
14
+ */
15
+ export function wrapRemoteError(err) {
16
+ const msg = String(err?.message || "");
17
+ const code = err?.code || "";
18
+ if (code === "CAPABILITY_UNAVAILABLE" ||
19
+ code === "TARGET_CAPABILITY_UNAVAILABLE" ||
20
+ msg.includes("CAPABILITY_UNAVAILABLE") ||
21
+ msg.includes("TARGET_CAPABILITY_UNAVAILABLE") ||
22
+ msg.includes("Management APIs are not enabled")) {
23
+ throw new TargetError(TARGET_CAPABILITY_UNAVAILABLE, `TARGET_CAPABILITY_UNAVAILABLE: Management APIs are not enabled on remote target`, { originalMessage: msg });
24
+ }
25
+ if (code === "PROTOCOL_UNSUPPORTED" ||
26
+ code === "TARGET_PROTOCOL_UNSUPPORTED" ||
27
+ msg.includes("PROTOCOL_UNSUPPORTED") ||
28
+ msg.includes("TARGET_PROTOCOL_UNSUPPORTED")) {
29
+ throw new TargetError(TARGET_PROTOCOL_UNSUPPORTED, `TARGET_PROTOCOL_UNSUPPORTED: ${msg}`, { originalMessage: msg });
30
+ }
31
+ if (code === "TARGET_RESULT_UNKNOWN" ||
32
+ code === "RESULT_UNKNOWN" ||
33
+ msg.includes("TARGET_RESULT_UNKNOWN")) {
34
+ throw new TargetError(TARGET_RESULT_UNKNOWN, `TARGET_RESULT_UNKNOWN: ${msg}`, { originalMessage: msg });
35
+ }
36
+ throw err;
37
+ }
38
+ /**
39
+ * 判定远端状态键访问异常是否为键不存在。
40
+ *
41
+ * 优先读取传输层透传的结构化错误码(fetchRemoteJson 会将响应体 error.code
42
+ * 附加到抛出异常的 code 字段),仅当旧版服务器未透传 code 时回退到
43
+ * HTTP 状态与消息文本兼容嗅探。
44
+ */
45
+ export function isRemoteStateKeyNotFound(err) {
46
+ if (err?.code === STATE_KEY_NOT_FOUND) {
47
+ return true;
48
+ }
49
+ const msg = String(err?.message || "");
50
+ return err?.status === 404 || msg.includes("404") || msg.includes("not found");
51
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 远端运行轮询兜底。
3
+ *
4
+ * 职责单一:SSE 事件流不可用或超时预算仍有剩余时,按指数退避轮询远端
5
+ * 运行详情直至终态、取消或超时上限;并将终态运行记录翻译为统一
6
+ * ExecutionResult 结构。不感知 SSE 通道与门面生命周期。
7
+ */
8
+ import type { ExecutionResult, RunRecord } from "@actiondock/sdk";
9
+ /**
10
+ * 轮询依赖上下文:由 RemoteActionDockTarget 门面注入自身能力,
11
+ * 保持本模块与门面实现解耦(子模块不得反向依赖 remote.ts)。
12
+ */
13
+ export interface RunPollingContext {
14
+ /** 轮询等待基准底线超时时间(毫秒,默认 60000ms) */
15
+ baseTimeoutMs: number;
16
+ /** 目标是否已关闭 */
17
+ isClosed(): boolean;
18
+ /** 查询远端运行详情(键不存在时返回 undefined) */
19
+ getRun(runId: string): Promise<RunRecord | undefined>;
20
+ }
21
+ /**
22
+ * 将终态运行记录翻译为统一 ExecutionResult 结构。
23
+ */
24
+ export declare function formatTerminalRunResult(run: RunRecord, runId: string): ExecutionResult;
25
+ /**
26
+ * 指数退避轮询远端运行详情直至终态、取消或超时上限。
27
+ *
28
+ * @param ctx 门面注入的依赖上下文
29
+ * @param runId 运行标识
30
+ * @param signal 外部取消信号
31
+ * @param timeoutMs 运行自身声明的超时(未提供总上限时参与计算等待上限)
32
+ * @param startTime 等待起点时间戳(缺省当前时间)
33
+ * @param totalMaxWaitMs 已消耗等待预算对应的总上限(SSE 阶段已等待时传入)
34
+ */
35
+ export declare function pollRunCompletion(ctx: RunPollingContext, runId: string, signal?: AbortSignal, timeoutMs?: number, startTime?: number, totalMaxWaitMs?: number): Promise<ExecutionResult>;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 远端运行轮询兜底。
3
+ *
4
+ * 职责单一:SSE 事件流不可用或超时预算仍有剩余时,按指数退避轮询远端
5
+ * 运行详情直至终态、取消或超时上限;并将终态运行记录翻译为统一
6
+ * ExecutionResult 结构。不感知 SSE 通道与门面生命周期。
7
+ */
8
+ import { ACTION_CANCELLED, EXECUTION_FAILED, TIMEOUT } from "../errors.js";
9
+ import { isTerminalRunStatus } from "../storage/types.js";
10
+ import { TARGET_CLOSED } from "./types.js";
11
+ /**
12
+ * 将终态运行记录翻译为统一 ExecutionResult 结构。
13
+ */
14
+ export function formatTerminalRunResult(run, runId) {
15
+ if (run.status === "success") {
16
+ return { ok: true, runId, data: run.output ?? null };
17
+ }
18
+ if (run.status === "interrupted") {
19
+ return {
20
+ ok: false,
21
+ runId,
22
+ error: run.error || {
23
+ code: "RUN_INTERRUPTED",
24
+ message: `Run '${runId}' was interrupted`,
25
+ },
26
+ };
27
+ }
28
+ return {
29
+ ok: false,
30
+ runId,
31
+ error: run.error || {
32
+ code: EXECUTION_FAILED,
33
+ message: `Run finished with status ${run.status}`,
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * 指数退避轮询远端运行详情直至终态、取消或超时上限。
39
+ *
40
+ * @param ctx 门面注入的依赖上下文
41
+ * @param runId 运行标识
42
+ * @param signal 外部取消信号
43
+ * @param timeoutMs 运行自身声明的超时(未提供总上限时参与计算等待上限)
44
+ * @param startTime 等待起点时间戳(缺省当前时间)
45
+ * @param totalMaxWaitMs 已消耗等待预算对应的总上限(SSE 阶段已等待时传入)
46
+ */
47
+ export async function pollRunCompletion(ctx, runId, signal, timeoutMs, startTime = Date.now(), totalMaxWaitMs) {
48
+ const maxWaitMs = totalMaxWaitMs ?? Math.max(ctx.baseTimeoutMs, timeoutMs ?? 0);
49
+ const remainingWaitMs = Math.max(0, maxWaitMs - (Date.now() - startTime));
50
+ let delayMs = Math.min(150, Math.max(10, Math.floor((remainingWaitMs || maxWaitMs) / 4)));
51
+ const maxDelayMs = 2000;
52
+ while (Date.now() - startTime < maxWaitMs) {
53
+ if (ctx.isClosed()) {
54
+ return {
55
+ ok: false,
56
+ runId,
57
+ error: {
58
+ code: TARGET_CLOSED,
59
+ message: "RemoteActionDockTarget is closed",
60
+ },
61
+ };
62
+ }
63
+ if (signal?.aborted) {
64
+ return {
65
+ ok: false,
66
+ runId,
67
+ error: {
68
+ code: ACTION_CANCELLED,
69
+ message: "Action execution was cancelled",
70
+ },
71
+ };
72
+ }
73
+ try {
74
+ const run = await ctx.getRun(runId);
75
+ if (run && isTerminalRunStatus(run.status)) {
76
+ return formatTerminalRunResult(run, runId);
77
+ }
78
+ }
79
+ catch (err) {
80
+ if (err?.code === TARGET_CLOSED || ctx.isClosed()) {
81
+ return {
82
+ ok: false,
83
+ runId,
84
+ error: {
85
+ code: TARGET_CLOSED,
86
+ message: "RemoteActionDockTarget is closed",
87
+ },
88
+ };
89
+ }
90
+ throw err;
91
+ }
92
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
93
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
94
+ }
95
+ const waitedMs = Date.now() - startTime;
96
+ return {
97
+ ok: false,
98
+ runId,
99
+ error: {
100
+ code: TIMEOUT,
101
+ message: `Timed out waiting for run '${runId}' completion after ${waitedMs}ms`,
102
+ },
103
+ };
104
+ }
@@ -5,6 +5,10 @@ import { type StateEntry } from "../storage/types.js";
5
5
  import { type ActionDockTarget, type ConfigValueView, type ListRunsOptions, type RemoteTargetOptions, type StateScopeOptions, type TargetInfo } from "./types.js";
6
6
  /**
7
7
  * 读取并解析远端 SSE 事件流。
8
+ *
9
+ * 候选路由依次尝试:v2 /events -> v2 /stream -> v1 /stream;
10
+ * 全部候选均不可用(网络异常或非流式响应)时抛出携带各候选 URL 与
11
+ * 失败原因的 REMOTE_STREAM_UNAVAILABLE 聚合错误,严禁以空流伪装正常。
8
12
  */
9
13
  export declare function streamRemoteEvents(serverUrl: string, runId: string, token?: string, options?: {
10
14
  after?: number | string;
@@ -2,11 +2,19 @@ import { ActionResolver } from "../catalog/action-resolver.js";
2
2
  import { cancelRemoteRun, clearRemoteRuns, clearRemoteState, deleteRemoteConfig, deleteRemoteStateKey, executeRemoteAction, fetchRemoteActionShow, fetchRemoteActions, fetchRemoteConfig, fetchRemoteInfo, fetchRemotePlaybookShow, fetchRemotePlaybooks, fetchRemoteRun, fetchRemoteRuns, fetchRemoteStateList, getRemoteStateKey, setRemoteConfig, setRemoteStateKey, assertSecureTransport, } from "../profile/client.js";
3
3
  import { normalizeServerUrl } from "../profile/manager.js";
4
4
  import { isTerminalRunStatus } from "../storage/types.js";
5
- import { ACTIONDOCK_PROTOCOL_VERSION, TargetError, TARGET_PROTOCOL_UNSUPPORTED, TARGET_CAPABILITY_UNAVAILABLE, TARGET_RESULT_UNKNOWN, TARGET_CLOSED, } from "./types.js";
6
- import { ACTION_CANCELLED, EXECUTION_FAILED, STATE_KEY_NOT_FOUND, TIMEOUT } from "../errors.js";
5
+ import { ACTIONDOCK_PROTOCOL_VERSION, TargetError, TARGET_PROTOCOL_UNSUPPORTED, TARGET_CAPABILITY_UNAVAILABLE, TARGET_CLOSED, } from "./types.js";
6
+ import { ACTION_CANCELLED, REMOTE_STREAM_UNAVAILABLE, TIMEOUT } from "../errors.js";
7
7
  import { getInsecureDispatcher } from "../server/dispatcher.js";
8
+ import { listProtocolRouteCandidates } from "../profile/client.js";
9
+ import { isRemoteStateKeyNotFound, wrapRemoteError } from "./remote-errors.js";
10
+ import { formatTerminalRunResult, pollRunCompletion } from "./remote-polling.js";
11
+ import { parseSseMessages } from "./sse-parser.js";
8
12
  /**
9
13
  * 读取并解析远端 SSE 事件流。
14
+ *
15
+ * 候选路由依次尝试:v2 /events -> v2 /stream -> v1 /stream;
16
+ * 全部候选均不可用(网络异常或非流式响应)时抛出携带各候选 URL 与
17
+ * 失败原因的 REMOTE_STREAM_UNAVAILABLE 聚合错误,严禁以空流伪装正常。
10
18
  */
11
19
  export async function* streamRemoteEvents(serverUrl, runId, token, options) {
12
20
  assertSecureTransport(serverUrl, token, {
@@ -14,10 +22,11 @@ export async function* streamRemoteEvents(serverUrl, runId, token, options) {
14
22
  insecure: options?.insecure,
15
23
  });
16
24
  const base = normalizeServerUrl(serverUrl);
25
+ const runRoute = `runs/${encodeURIComponent(runId)}`;
26
+ // 候选优先级:v2 /events -> v2 /stream -> v1 /stream(后两者经共享协议候选工具展开)
17
27
  const candidateUrls = [
18
- `${base}/api/v2/runs/${encodeURIComponent(runId)}/events`,
19
- `${base}/api/v2/runs/${encodeURIComponent(runId)}/stream`,
20
- `${base}/api/v1/runs/${encodeURIComponent(runId)}/stream`,
28
+ `${base}/api/v2/${runRoute}/events`,
29
+ ...listProtocolRouteCandidates(base, `/api/v2/${runRoute}/stream`),
21
30
  ];
22
31
  const headers = {
23
32
  Accept: "text/event-stream",
@@ -28,9 +37,15 @@ export async function* streamRemoteEvents(serverUrl, runId, token, options) {
28
37
  if (options?.after !== undefined) {
29
38
  headers["Last-Event-ID"] = String(options.after);
30
39
  }
31
- let res;
32
- for (const url of candidateUrls) {
33
- try {
40
+ /**
41
+ * 依次尝试全部候选路由,返回首个可用响应。
42
+ *
43
+ * 单个候选失败(网络异常或非流式响应)时记录原因继续尝试后续候选;
44
+ * 所有候选均失败时返回汇总后的失败原因列表供上层抛出聚合错误。
45
+ */
46
+ async function resolveStreamCandidate() {
47
+ const candidateFailures = [];
48
+ for (const url of candidateUrls) {
34
49
  const fetchInit = {
35
50
  headers,
36
51
  signal: options?.signal,
@@ -41,182 +56,63 @@ export async function* streamRemoteEvents(serverUrl, runId, token, options) {
41
56
  else if (options?.insecure) {
42
57
  fetchInit.dispatcher = getInsecureDispatcher();
43
58
  }
44
- const resp = await fetch(url, fetchInit);
45
- if (resp.status === 410) {
46
- let errJson;
47
- try {
48
- errJson = await resp.json();
59
+ try {
60
+ const resp = await fetch(url, fetchInit);
61
+ if (resp.status === 410) {
62
+ let errJson;
63
+ try {
64
+ errJson = await resp.json();
65
+ }
66
+ catch { }
67
+ const err = new Error(errJson?.error?.message || "Event cursor has expired");
68
+ err.code = errJson?.error?.code || "EVENT_CURSOR_EXPIRED";
69
+ err.details = errJson?.error?.details;
70
+ throw err;
49
71
  }
50
- catch { }
51
- const err = new Error(errJson?.error?.message || "Event cursor has expired");
52
- err.code = errJson?.error?.code || "EVENT_CURSOR_EXPIRED";
53
- err.details = errJson?.error?.details;
54
- throw err;
55
- }
56
- if (resp.ok && resp.body) {
57
- res = resp;
58
- break;
59
- }
60
- if (resp.status !== 404) {
61
- res = resp;
62
- break;
63
- }
64
- }
65
- catch (err) {
66
- if (err?.code === "EVENT_CURSOR_EXPIRED") {
67
- throw err;
68
- }
69
- // 忽略单次网络连接异常并尝试备选路由
70
- }
71
- }
72
- if (!res || !res.ok || !res.body) {
73
- return;
74
- }
75
- const reader = res.body.getReader();
76
- const decoder = new TextDecoder();
77
- let buffer = "";
78
- let eventType = "message";
79
- let eventId;
80
- let dataLines = [];
81
- function dispatchCurrentEvent() {
82
- if (dataLines.length === 0) {
83
- eventType = "message";
84
- eventId = undefined;
85
- dataLines = [];
86
- return undefined;
87
- }
88
- const dataStr = dataLines.join("\n");
89
- const currentType = eventType;
90
- const currentId = eventId;
91
- eventType = "message";
92
- eventId = undefined;
93
- dataLines = [];
94
- if (!dataStr) {
95
- return undefined;
96
- }
97
- try {
98
- const data = JSON.parse(dataStr);
99
- if (currentType === "finish") {
100
- return {
101
- eventId: currentId ?? data.eventId,
102
- type: "finish",
103
- runId,
104
- timestamp: new Date().toISOString(),
105
- result: data.result || data,
106
- };
72
+ if (resp.ok && resp.body) {
73
+ return { ok: true, body: resp.body };
74
+ }
75
+ candidateFailures.push({
76
+ url,
77
+ reason: `HTTP ${resp.status} ${resp.statusText || ""}`.trim(),
78
+ });
107
79
  }
108
- else {
109
- return {
110
- eventId: currentId ?? data.eventId,
111
- type: (currentType || "message"),
112
- runId,
113
- timestamp: new Date().toISOString(),
114
- ...data,
115
- };
80
+ catch (err) {
81
+ if (err?.code === "EVENT_CURSOR_EXPIRED") {
82
+ throw err;
83
+ }
84
+ candidateFailures.push({
85
+ url,
86
+ reason: err?.message ? `${err.name || "Error"}: ${err.message}` : String(err),
87
+ });
116
88
  }
117
89
  }
118
- catch {
119
- // 忽略非 JSON 数据行
120
- return undefined;
121
- }
90
+ return { ok: false, candidateFailures };
122
91
  }
123
- function processLine(line) {
124
- // 遇到空行分发事件
125
- if (line === "") {
126
- return dispatchCurrentEvent();
127
- }
128
- // 以冒号开头的为注释忽略
129
- if (line.startsWith(":")) {
130
- return undefined;
131
- }
132
- let field = line;
133
- let value = "";
134
- const colonIdx = line.indexOf(":");
135
- if (colonIdx !== -1) {
136
- field = line.slice(0, colonIdx);
137
- const rawVal = line.slice(colonIdx + 1);
138
- value = rawVal.startsWith(" ") ? rawVal.slice(1) : rawVal;
139
- }
140
- if (field === "event") {
141
- eventType = value;
142
- }
143
- else if (field === "data") {
144
- dataLines.push(value);
145
- }
146
- else if (field === "id") {
147
- if (!value.includes("\0")) {
148
- eventId = value;
149
- }
150
- }
151
- return undefined;
92
+ /**
93
+ * 构造全部候选路由均不可用时的聚合错误。
94
+ */
95
+ function buildStreamUnavailableError(candidateFailures) {
96
+ const summary = candidateFailures
97
+ .map((f) => `${f.url} (${f.reason})`)
98
+ .join(", ");
99
+ const err = new Error(`REMOTE_STREAM_UNAVAILABLE: All event stream candidates failed for run '${runId}': ${summary}`);
100
+ err.code = REMOTE_STREAM_UNAVAILABLE;
101
+ err.details = {
102
+ runId,
103
+ candidates: candidateFailures.map((f) => ({ url: f.url, reason: f.reason })),
104
+ };
105
+ return err;
106
+ }
107
+ const res = await resolveStreamCandidate();
108
+ if (!res.ok) {
109
+ throw buildStreamUnavailableError(res.candidateFailures);
152
110
  }
153
111
  try {
154
- while (true) {
155
- if (options?.signal?.aborted)
156
- break;
157
- const { done, value } = await reader.read();
158
- if (value) {
159
- buffer += decoder.decode(value, { stream: !done });
160
- }
161
- else if (done) {
162
- buffer += decoder.decode();
163
- }
164
- let pos = 0;
165
- while (pos < buffer.length) {
166
- const cr = buffer.indexOf("\r", pos);
167
- const lf = buffer.indexOf("\n", pos);
168
- let nextSepPos = -1;
169
- let sepLen = 0;
170
- if (cr !== -1 && (lf === -1 || cr < lf)) {
171
- if (cr === buffer.length - 1) {
172
- if (!done) {
173
- // 遇到未完结的 \r 暂存等待下个 chunk
174
- break;
175
- }
176
- else {
177
- nextSepPos = cr;
178
- sepLen = 1;
179
- }
180
- }
181
- else {
182
- if (buffer[cr + 1] === "\n") {
183
- nextSepPos = cr;
184
- sepLen = 2;
185
- }
186
- else {
187
- nextSepPos = cr;
188
- sepLen = 1;
189
- }
190
- }
191
- }
192
- else if (lf !== -1 && (cr === -1 || lf < cr)) {
193
- nextSepPos = lf;
194
- sepLen = 1;
195
- }
196
- else {
197
- break;
198
- }
199
- const line = buffer.slice(pos, nextSepPos);
200
- pos = nextSepPos + sepLen;
201
- const evt = processLine(line);
202
- if (evt)
203
- yield evt;
204
- }
205
- buffer = buffer.slice(pos);
206
- if (done) {
207
- if (buffer.length > 0) {
208
- const line = buffer;
209
- buffer = "";
210
- const evt = processLine(line);
211
- if (evt)
212
- yield evt;
213
- }
214
- // 流读取完成时若有剩余待分发事件则进行分发
215
- const remainingEvt = dispatchCurrentEvent();
216
- if (remainingEvt)
217
- yield remainingEvt;
218
- break;
219
- }
112
+ for await (const msg of parseSseMessages(res.body, { signal: options?.signal })) {
113
+ const evt = decodeExecutionEvent(msg, runId);
114
+ if (evt)
115
+ yield evt;
220
116
  }
221
117
  }
222
118
  catch (err) {
@@ -225,18 +121,37 @@ export async function* streamRemoteEvents(serverUrl, runId, token, options) {
225
121
  }
226
122
  throw err;
227
123
  }
228
- finally {
229
- // 防御性取消底层流:仅 releaseLock 会让连接保持挂起,造成连接泄漏
230
- try {
231
- await reader.cancel();
232
- }
233
- catch {
234
- // 流已自然结束或已被取消时忽略次级异常
124
+ }
125
+ /**
126
+ * 将 SSE 消息载荷解码为统一执行事件。
127
+ *
128
+ * 非 JSON 数据行直接忽略;finish 事件取 data.result 字段,其余事件字段展开合并。
129
+ */
130
+ function decodeExecutionEvent(msg, runId) {
131
+ try {
132
+ const data = JSON.parse(msg.data);
133
+ if (msg.event === "finish") {
134
+ return {
135
+ eventId: msg.id ?? data.eventId,
136
+ type: "finish",
137
+ runId,
138
+ timestamp: new Date().toISOString(),
139
+ result: data.result || data,
140
+ };
235
141
  }
236
- try {
237
- reader.releaseLock();
142
+ else {
143
+ return {
144
+ eventId: msg.id ?? data.eventId,
145
+ type: (msg.event || "message"),
146
+ runId,
147
+ timestamp: new Date().toISOString(),
148
+ ...data,
149
+ };
238
150
  }
239
- catch { }
151
+ }
152
+ catch {
153
+ // 忽略非 JSON 数据行
154
+ return undefined;
240
155
  }
241
156
  }
242
157
  /**
@@ -601,85 +516,14 @@ export class RemoteActionDockTarget {
601
516
  * 指数退避轮询远端运行详情直至终态、取消或超时上限。
602
517
  */
603
518
  async pollRunCompletion(runId, signal, timeoutMs, startTime = Date.now(), totalMaxWaitMs) {
604
- const maxWaitMs = totalMaxWaitMs ?? Math.max(this.baseTimeoutMs, timeoutMs ?? 0);
605
- const remainingWaitMs = Math.max(0, maxWaitMs - (Date.now() - startTime));
606
- let delayMs = Math.min(150, Math.max(10, Math.floor((remainingWaitMs || maxWaitMs) / 4)));
607
- const maxDelayMs = 2000;
608
- while (Date.now() - startTime < maxWaitMs) {
609
- if (this.isClosed) {
610
- return {
611
- ok: false,
612
- runId,
613
- error: {
614
- code: TARGET_CLOSED,
615
- message: "RemoteActionDockTarget is closed",
616
- },
617
- };
618
- }
619
- if (signal?.aborted) {
620
- return {
621
- ok: false,
622
- runId,
623
- error: {
624
- code: ACTION_CANCELLED,
625
- message: "Action execution was cancelled",
626
- },
627
- };
628
- }
629
- try {
630
- const run = await this.getRun(runId);
631
- if (run && isTerminalRunStatus(run.status)) {
632
- return this.formatTerminalRunResult(run, runId);
633
- }
634
- }
635
- catch (err) {
636
- if (err?.code === TARGET_CLOSED || this.isClosed) {
637
- return {
638
- ok: false,
639
- runId,
640
- error: {
641
- code: TARGET_CLOSED,
642
- message: "RemoteActionDockTarget is closed",
643
- },
644
- };
645
- }
646
- throw err;
647
- }
648
- await new Promise((resolve) => setTimeout(resolve, delayMs));
649
- delayMs = Math.min(delayMs * 2, maxDelayMs);
650
- }
651
- const waitedMs = Date.now() - startTime;
652
- return {
653
- ok: false,
654
- runId,
655
- error: {
656
- code: TIMEOUT,
657
- message: `Timed out waiting for run '${runId}' completion after ${waitedMs}ms`,
658
- },
659
- };
519
+ return pollRunCompletion({
520
+ baseTimeoutMs: this.baseTimeoutMs,
521
+ isClosed: () => this.isClosed,
522
+ getRun: (id) => this.getRun(id),
523
+ }, runId, signal, timeoutMs, startTime, totalMaxWaitMs);
660
524
  }
661
525
  formatTerminalRunResult(run, runId) {
662
- if (run.status === "success") {
663
- return { ok: true, runId, data: run.output ?? null };
664
- }
665
- if (run.status === "interrupted") {
666
- return {
667
- ok: false,
668
- runId,
669
- error: run.error || {
670
- code: "RUN_INTERRUPTED",
671
- message: `Run '${runId}' was interrupted`,
672
- },
673
- };
674
- }
675
- return {
676
- ok: false,
677
- runId,
678
- error: run.error || {
679
- code: EXECUTION_FAILED,
680
- message: `Run finished with status ${run.status}`,
681
- },
682
- };
526
+ return formatTerminalRunResult(run, runId);
683
527
  }
684
528
  async listRuns(options) {
685
529
  this.assertNotClosed();
@@ -972,40 +816,3 @@ export class RemoteActionDockTarget {
972
816
  this.isClosed = true;
973
817
  }
974
818
  }
975
- /**
976
- * 判定远端状态键访问异常是否为键不存在。
977
- *
978
- * 优先读取传输层透传的结构化错误码(fetchRemoteJson 会将响应体 error.code
979
- * 附加到抛出异常的 code 字段),仅当旧版服务器未透传 code 时回退到
980
- * HTTP 状态与消息文本兼容嗅探。
981
- */
982
- function isRemoteStateKeyNotFound(err) {
983
- if (err?.code === STATE_KEY_NOT_FOUND) {
984
- return true;
985
- }
986
- const msg = String(err?.message || "");
987
- return err?.status === 404 || msg.includes("404") || msg.includes("not found");
988
- }
989
- function wrapRemoteError(err) {
990
- const msg = String(err?.message || "");
991
- const code = err?.code || "";
992
- if (code === "CAPABILITY_UNAVAILABLE" ||
993
- code === "TARGET_CAPABILITY_UNAVAILABLE" ||
994
- msg.includes("CAPABILITY_UNAVAILABLE") ||
995
- msg.includes("TARGET_CAPABILITY_UNAVAILABLE") ||
996
- msg.includes("Management APIs are not enabled")) {
997
- throw new TargetError(TARGET_CAPABILITY_UNAVAILABLE, `TARGET_CAPABILITY_UNAVAILABLE: Management APIs are not enabled on remote target`, { originalMessage: msg });
998
- }
999
- if (code === "PROTOCOL_UNSUPPORTED" ||
1000
- code === "TARGET_PROTOCOL_UNSUPPORTED" ||
1001
- msg.includes("PROTOCOL_UNSUPPORTED") ||
1002
- msg.includes("TARGET_PROTOCOL_UNSUPPORTED")) {
1003
- throw new TargetError(TARGET_PROTOCOL_UNSUPPORTED, `TARGET_PROTOCOL_UNSUPPORTED: ${msg}`, { originalMessage: msg });
1004
- }
1005
- if (code === "TARGET_RESULT_UNKNOWN" ||
1006
- code === "RESULT_UNKNOWN" ||
1007
- msg.includes("TARGET_RESULT_UNKNOWN")) {
1008
- throw new TargetError(TARGET_RESULT_UNKNOWN, `TARGET_RESULT_UNKNOWN: ${msg}`, { originalMessage: msg });
1009
- }
1010
- throw err;
1011
- }