@actiondock/cli 2.9.1 → 2.11.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.
- package/README.md +3 -3
- package/bin/ad.js +225 -5
- package/dist/commands/action/create.d.ts +1 -1
- package/dist/commands/action/create.js +6 -14
- package/dist/commands/add.js +1 -1
- package/dist/commands/config/delete.js +3 -1
- package/dist/commands/config/env.js +2 -1
- package/dist/commands/config/get.js +3 -1
- package/dist/commands/config/list.js +6 -5
- package/dist/commands/config/schema.js +16 -10
- package/dist/commands/config/set.js +3 -1
- package/dist/commands/describe.js +6 -4
- package/dist/commands/export.js +34 -16
- package/dist/commands/index.d.ts +37 -24
- package/dist/commands/index.js +346 -48
- package/dist/commands/info.js +34 -9
- package/dist/commands/list.js +13 -2
- package/dist/commands/mcp.js +0 -1
- package/dist/commands/playbook.js +67 -52
- package/dist/commands/profile.js +2 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/run.d.ts +2 -1
- package/dist/commands/run.js +19 -56
- package/dist/commands/runs.d.ts +6 -1
- package/dist/commands/runs.js +274 -11
- package/dist/commands/serve.js +5 -2
- package/dist/commands/state.js +84 -15
- package/dist/errors.d.ts +5 -3
- package/dist/errors.js +23 -7
- package/dist/index.d.ts +10 -2
- package/dist/index.js +15 -12
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.js +6 -14
- package/dist/renderer.d.ts +8 -151
- package/dist/renderer.js +17 -375
- package/dist/renderers/action.d.ts +51 -0
- package/dist/renderers/action.js +112 -0
- package/dist/renderers/config.d.ts +31 -0
- package/dist/renderers/config.js +93 -0
- package/dist/renderers/playbook.d.ts +19 -0
- package/dist/renderers/playbook.js +42 -0
- package/dist/renderers/project.d.ts +45 -0
- package/dist/renderers/project.js +196 -0
- package/dist/renderers/runs.d.ts +15 -0
- package/dist/renderers/runs.js +47 -0
- package/dist/renderers/state.d.ts +4 -0
- package/dist/renderers/state.js +19 -0
- package/dist/services/config-merge.js +1 -2
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/run-watch.d.ts +156 -0
- package/dist/services/run-watch.js +395 -0
- package/dist/types.d.ts +37 -0
- package/dist/utils/cert.js +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/input.d.ts +2 -1
- package/dist/utils/input.js +2 -1
- package/dist/utils/target.d.ts +6 -2
- package/dist/utils/target.js +4 -4
- package/dist/utils/utils.d.ts +1 -1
- package/dist/utils/utils.js +4 -12
- package/dist/utils/warning.d.ts +4 -0
- package/dist/utils/warning.js +4 -0
- package/package.json +5 -5
- package/dist/commands/state-list.d.ts +0 -24
- package/dist/commands/state-list.js +0 -81
- package/dist/standalone/index.d.ts +0 -12
- package/dist/standalone/index.js +0 -13
- package/dist/utils/duration.d.ts +0 -1
- package/dist/utils/duration.js +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isSecretConfigKey, maskSecretValue, } from "@actiondock/core/project";
|
|
2
|
-
import { loadProjectConfig, } from "@actiondock/core";
|
|
3
|
-
import { resolveEnvValue, } from "@actiondock/core/profile";
|
|
2
|
+
import { loadProjectConfig, resolveEnvValue, } from "@actiondock/core";
|
|
4
3
|
/**
|
|
5
4
|
* 解析单个配置键的合并视图(项目包级 > 全局持久化 > 环境变量 > 声明默认值)。
|
|
6
5
|
*
|
package/dist/services/index.d.ts
CHANGED
package/dist/services/index.js
CHANGED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type ActionDockService, type RunRecord } from "@actiondock/core";
|
|
2
|
+
import type { CliContext } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* runs watch 轮询等待引擎。
|
|
5
|
+
*
|
|
6
|
+
* 职责单一:对指定执行环境服务的一组运行标识执行统一轮询循环,
|
|
7
|
+
* 逐轮查询未终态记录直至全部终态、超时、外部中断或宿主丢失;聚合输出与退出判定由调用方消费。
|
|
8
|
+
*
|
|
9
|
+
* 架构决策与原则权衡(关于「事件驱动协作」):
|
|
10
|
+
* ActionDock 核心工程原则倡导事件驱动协作,但在 CLI 运行观察场景中采用只读轮询机制,
|
|
11
|
+
* 是基于客观物理与系统边界约束推导的理性权衡:
|
|
12
|
+
* - 进程边界与底层存储物理约束:CLI 作为独立的短暂只读旁观进程直接连接本地 SQLite 存储文件,
|
|
13
|
+
* 任务宿主进程与 CLI 旁观进程分属完全独立的操作系统进程,SQLite 单文件存储层缺乏跨进程事件推送能力;
|
|
14
|
+
* - 零守护进程哲学:ActionDock 坚持零守护进程(daemonless)本地轻量执行,不强制依赖常驻后台中继服务或事件服务器;
|
|
15
|
+
* - 物理与架构最优解:在上述边界条件下,采用轻量只读、带瞬态错误容忍与自愈能力的定时轮询,
|
|
16
|
+
* 是兼顾零常驻开销、环境免侵入与跨平台确定性的物理与架构最优解。
|
|
17
|
+
*
|
|
18
|
+
* 设计契约:
|
|
19
|
+
* - 只读旁观:仅调用 service.runs.get,不取消任务、不收割运行记录、不写任何状态;
|
|
20
|
+
* - 单一执行环境:一次 watch 绑定单一 Service 实例(本地或远端),不存在跨环境混合轮询;
|
|
21
|
+
* - 瞬态容错:非中止查询异常在未达连续失败阈值前支持自愈,仅连续达到阈值时收敛为观察失败;
|
|
22
|
+
* - 防御与透明:查询失败或宿主丢失不臆断任务到达终态,保留当前真实状态与观察错误,结束原因明确区分。
|
|
23
|
+
*/
|
|
24
|
+
/** 单个运行的归属解析结果。 */
|
|
25
|
+
export interface WatchRunSource {
|
|
26
|
+
/** 运行标识 */
|
|
27
|
+
runId: string;
|
|
28
|
+
/** 归属类型:本地运行库或远端服务 */
|
|
29
|
+
source: "local" | "remote";
|
|
30
|
+
/** 归属解析阶段已取到的记录(若解析时已读到) */
|
|
31
|
+
initialRecord?: RunRecord;
|
|
32
|
+
/** 幂等请求标识(--request-id 反查路径携带,便于结果对因) */
|
|
33
|
+
requestId?: string;
|
|
34
|
+
}
|
|
35
|
+
/** 单个运行的聚合输出条目。 */
|
|
36
|
+
export interface WatchRunOutcome {
|
|
37
|
+
/** 运行标识 */
|
|
38
|
+
runId: string;
|
|
39
|
+
/** 归属类型 */
|
|
40
|
+
source: "local" | "remote";
|
|
41
|
+
/** 幂等请求标识(--request-id 反查路径携带) */
|
|
42
|
+
requestId?: string;
|
|
43
|
+
/** 最终状态:终态时为具体终态;超时中断或观察失败时为当时的最新状态 */
|
|
44
|
+
status: string;
|
|
45
|
+
/** 执行成功时的输出结果快照(取自运行记录的 output 字段) */
|
|
46
|
+
data?: unknown;
|
|
47
|
+
/** 执行失败或查询观察异常信息 */
|
|
48
|
+
error?: unknown;
|
|
49
|
+
/** 观察异常信息(若存在) */
|
|
50
|
+
queryError?: unknown;
|
|
51
|
+
/** 是否到达终态(严格仅由任务状态推导,查询失败或宿主死亡不得置 true) */
|
|
52
|
+
terminal: boolean;
|
|
53
|
+
/** 运行耗时(毫秒,取自运行记录;无记录时为 undefined) */
|
|
54
|
+
durationMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/** 整体等待结束原因。 */
|
|
57
|
+
export type WatchEndReason = "all_terminal" | "timeout" | "interrupted" | "observation_failed";
|
|
58
|
+
/** 聚合等待结果。 */
|
|
59
|
+
export interface WatchAggregation {
|
|
60
|
+
/** 是否全部到达终态且全部执行成功 */
|
|
61
|
+
ok: boolean;
|
|
62
|
+
/** 是否因整体超时提前结束 */
|
|
63
|
+
timedOut: boolean;
|
|
64
|
+
/** 是否因外部中断信号提前结束 */
|
|
65
|
+
interrupted: boolean;
|
|
66
|
+
/** 整体等待结束原因:全部终态、超时、中断或观察失败 */
|
|
67
|
+
reason: WatchEndReason;
|
|
68
|
+
/** 逐运行结果(保持用户输入顺序与 runId 去重) */
|
|
69
|
+
runs: WatchRunOutcome[];
|
|
70
|
+
}
|
|
71
|
+
/** 轮询引擎依赖注入上下文。 */
|
|
72
|
+
export interface WatchPollContext {
|
|
73
|
+
/** 目标执行环境服务端口实例(本地或远端) */
|
|
74
|
+
service?: ActionDockService;
|
|
75
|
+
/** 轮询间隔毫秒 */
|
|
76
|
+
intervalMs: number;
|
|
77
|
+
/** 相对超时毫秒(未指定为无限等待) */
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
/** 统一绝对截止时间毫秒(从命令入口开始计算) */
|
|
80
|
+
deadline?: number;
|
|
81
|
+
/** 最大连续查询错误容忍阈值(未指定使用默认值 MAX_CONSECUTIVE_QUERY_ERRORS) */
|
|
82
|
+
maxConsecutiveErrors?: number;
|
|
83
|
+
/** 外部中断信号(如 SIGINT / 取消 Controller) */
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
/** 整体等待超时专用信号(用于精确区分超时与外部中断) */
|
|
86
|
+
timeoutSignal?: AbortSignal;
|
|
87
|
+
/** 进度输出写入间隔毫秒(0 表示禁用进度输出) */
|
|
88
|
+
progressIntervalMs?: number;
|
|
89
|
+
/** 是否静默模式(--quiet,关闭进度行输出) */
|
|
90
|
+
quiet?: boolean;
|
|
91
|
+
/** CLI 上下文(输出重定向) */
|
|
92
|
+
context?: CliContext;
|
|
93
|
+
/** 可注入的休眠函数(测试确定性控制) */
|
|
94
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
95
|
+
/** 可注入的时钟(测试确定性控制) */
|
|
96
|
+
now?: () => number;
|
|
97
|
+
/** 可注入的进程存活探测替身(测试确定性控制) */
|
|
98
|
+
probe?: (pid: number) => boolean;
|
|
99
|
+
/** 可注入的遗留心跳宽限期毫秒 */
|
|
100
|
+
graceMs?: number;
|
|
101
|
+
}
|
|
102
|
+
/** 默认轮询间隔(毫秒)。 */
|
|
103
|
+
export declare const DEFAULT_WATCH_INTERVAL_MS = 2000;
|
|
104
|
+
/** 默认进度行输出间隔(毫秒)。 */
|
|
105
|
+
export declare const DEFAULT_WATCH_PROGRESS_INTERVAL_MS = 5000;
|
|
106
|
+
/** 单个运行允许的最大连续查询错误次数(超过后判定为致命观察失败)。 */
|
|
107
|
+
export declare const MAX_CONSECUTIVE_QUERY_ERRORS = 3;
|
|
108
|
+
/** requestId 反查默认等待预算(毫秒):nohup 派工进程写入幂等键可能晚于 watch 启动。 */
|
|
109
|
+
export declare const DEFAULT_REQUEST_ID_RESOLVE_MS = 30000;
|
|
110
|
+
/** requestId 反查结果条目。 */
|
|
111
|
+
export interface RequestIdResolution {
|
|
112
|
+
/** 命中的幂等请求标识 */
|
|
113
|
+
requestId: string;
|
|
114
|
+
/** 反查得到的运行标识 */
|
|
115
|
+
runId: string;
|
|
116
|
+
/** 反查时刻已取到的运行记录 */
|
|
117
|
+
record: RunRecord;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 按幂等请求标识反查运行记录(带有限重试与歧义校验)。
|
|
121
|
+
*
|
|
122
|
+
* 语义规范:
|
|
123
|
+
* - 无匹配:在反查预算内继续重试等待;
|
|
124
|
+
* - 唯一匹配:得到 runId,进入等待;
|
|
125
|
+
* - 多条匹配:明确抛出歧义异常,要求缩小包范围或直接指定 runId,严禁隐式选择最新记录;
|
|
126
|
+
* - 查询不传待解析数量作为 limit 上限,防止隐藏并发或跨包重复匹配。
|
|
127
|
+
*
|
|
128
|
+
* @param service 目标作用域 Service 实例(本地或远端)
|
|
129
|
+
* @param requestIds 待反查的幂等请求标识集合
|
|
130
|
+
* @param options 轮询控制(intervalMs、resolveTimeoutMs、deadline、sleep、signal)
|
|
131
|
+
* @returns 命中集合与超预算未命中集合
|
|
132
|
+
*/
|
|
133
|
+
export declare function resolveRunsByRequestIds(service: ActionDockService, requestIds: string[], options: {
|
|
134
|
+
packageId?: string;
|
|
135
|
+
intervalMs: number;
|
|
136
|
+
resolveTimeoutMs?: number;
|
|
137
|
+
deadline?: number;
|
|
138
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
139
|
+
now?: () => number;
|
|
140
|
+
signal?: AbortSignal;
|
|
141
|
+
}): Promise<{
|
|
142
|
+
resolved: RequestIdResolution[];
|
|
143
|
+
unresolved: string[];
|
|
144
|
+
}>;
|
|
145
|
+
/**
|
|
146
|
+
* 执行统一轮询等待循环。
|
|
147
|
+
*
|
|
148
|
+
* @param sources 已完成归属解析的运行标识集合
|
|
149
|
+
* @param poll 轮询依赖上下文
|
|
150
|
+
* @returns 聚合结果(含逐运行终态、输出、错误及整体结束原因)
|
|
151
|
+
*/
|
|
152
|
+
export declare function pollRunsUntilTerminal(sources: WatchRunSource[], poll: WatchPollContext): Promise<WatchAggregation>;
|
|
153
|
+
/**
|
|
154
|
+
* 人读模式渲染聚合结果:每运行一行终态摘要。
|
|
155
|
+
*/
|
|
156
|
+
export declare function renderWatchSummary(agg: WatchAggregation, remoteSuffix?: string): string;
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { isRunHostDead, isTerminalRunStatus, } from "@actiondock/core";
|
|
2
|
+
import { ExecutionError } from "../errors.js";
|
|
3
|
+
import { writeStderr } from "../renderer.js";
|
|
4
|
+
/** 默认轮询间隔(毫秒)。 */
|
|
5
|
+
export const DEFAULT_WATCH_INTERVAL_MS = 2000;
|
|
6
|
+
/** 默认进度行输出间隔(毫秒)。 */
|
|
7
|
+
export const DEFAULT_WATCH_PROGRESS_INTERVAL_MS = 5000;
|
|
8
|
+
/** 单个运行允许的最大连续查询错误次数(超过后判定为致命观察失败)。 */
|
|
9
|
+
export const MAX_CONSECUTIVE_QUERY_ERRORS = 3;
|
|
10
|
+
/** requestId 反查默认等待预算(毫秒):nohup 派工进程写入幂等键可能晚于 watch 启动。 */
|
|
11
|
+
export const DEFAULT_REQUEST_ID_RESOLVE_MS = 30_000;
|
|
12
|
+
/**
|
|
13
|
+
* 按幂等请求标识反查运行记录(带有限重试与歧义校验)。
|
|
14
|
+
*
|
|
15
|
+
* 语义规范:
|
|
16
|
+
* - 无匹配:在反查预算内继续重试等待;
|
|
17
|
+
* - 唯一匹配:得到 runId,进入等待;
|
|
18
|
+
* - 多条匹配:明确抛出歧义异常,要求缩小包范围或直接指定 runId,严禁隐式选择最新记录;
|
|
19
|
+
* - 查询不传待解析数量作为 limit 上限,防止隐藏并发或跨包重复匹配。
|
|
20
|
+
*
|
|
21
|
+
* @param service 目标作用域 Service 实例(本地或远端)
|
|
22
|
+
* @param requestIds 待反查的幂等请求标识集合
|
|
23
|
+
* @param options 轮询控制(intervalMs、resolveTimeoutMs、deadline、sleep、signal)
|
|
24
|
+
* @returns 命中集合与超预算未命中集合
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveRunsByRequestIds(service, requestIds, options) {
|
|
27
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
28
|
+
const now = options.now ?? Date.now;
|
|
29
|
+
const startedAt = now();
|
|
30
|
+
const budgetMs = options.resolveTimeoutMs ?? DEFAULT_REQUEST_ID_RESOLVE_MS;
|
|
31
|
+
const deadline = options.deadline !== undefined
|
|
32
|
+
? Math.min(options.deadline, startedAt + budgetMs)
|
|
33
|
+
: startedAt + budgetMs;
|
|
34
|
+
const resolveTimeoutController = new AbortController();
|
|
35
|
+
const remainingBudget = Math.max(0, deadline - startedAt);
|
|
36
|
+
let resolveTimer;
|
|
37
|
+
if (remainingBudget < Infinity) {
|
|
38
|
+
resolveTimer = setTimeout(() => {
|
|
39
|
+
resolveTimeoutController.abort();
|
|
40
|
+
}, remainingBudget);
|
|
41
|
+
if (resolveTimer.unref)
|
|
42
|
+
resolveTimer.unref();
|
|
43
|
+
}
|
|
44
|
+
const querySignal = options.signal
|
|
45
|
+
? AbortSignal.any([options.signal, resolveTimeoutController.signal])
|
|
46
|
+
: resolveTimeoutController.signal;
|
|
47
|
+
const pending = new Set(requestIds);
|
|
48
|
+
const resolved = [];
|
|
49
|
+
try {
|
|
50
|
+
while (pending.size > 0) {
|
|
51
|
+
if (now() >= deadline || querySignal.aborted)
|
|
52
|
+
break;
|
|
53
|
+
const missing = Array.from(pending);
|
|
54
|
+
let records = [];
|
|
55
|
+
try {
|
|
56
|
+
// 严禁以 missing.length 作为 limit:必须全量拉取匹配记录以暴露潜在歧义
|
|
57
|
+
records = await service.runs.list({ requestIds: missing, packageId: options.packageId }, { signal: querySignal });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (querySignal.aborted) {
|
|
61
|
+
records = [];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 查询返回后,重新校验 now() >= deadline
|
|
68
|
+
if (now() >= deadline || querySignal.aborted) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const hitsByRequestId = new Map();
|
|
72
|
+
for (const record of records) {
|
|
73
|
+
const rid = record.requestId;
|
|
74
|
+
if (rid && pending.has(rid)) {
|
|
75
|
+
const group = hitsByRequestId.get(rid) || [];
|
|
76
|
+
group.push(record);
|
|
77
|
+
hitsByRequestId.set(rid, group);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [rid, hits] of hitsByRequestId.entries()) {
|
|
81
|
+
const distinctRunIds = Array.from(new Set(hits.map((h) => h.id)));
|
|
82
|
+
if (distinctRunIds.length > 1) {
|
|
83
|
+
throw new ExecutionError(`Request ID '${rid}' is ambiguous (${distinctRunIds.length} matching runs found: ${distinctRunIds.join(", ")}). Narrow the package scope with --package or specify the run ID directly.`);
|
|
84
|
+
}
|
|
85
|
+
if (distinctRunIds.length === 1) {
|
|
86
|
+
pending.delete(rid);
|
|
87
|
+
resolved.push({ requestId: rid, runId: distinctRunIds[0], record: hits[0] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (pending.size === 0)
|
|
91
|
+
break;
|
|
92
|
+
const current = now();
|
|
93
|
+
if (current >= deadline || querySignal.aborted)
|
|
94
|
+
break;
|
|
95
|
+
const remaining = deadline - current;
|
|
96
|
+
await sleep(Math.min(options.intervalMs, Math.max(remaining, 1)), querySignal);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (resolveTimer)
|
|
101
|
+
clearTimeout(resolveTimer);
|
|
102
|
+
}
|
|
103
|
+
return { resolved, unresolved: Array.from(pending) };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 中断感知休眠:信号触发时立即返回,超时时间自然到期返回。
|
|
107
|
+
*/
|
|
108
|
+
async function defaultSleep(ms, signal) {
|
|
109
|
+
if (ms <= 0)
|
|
110
|
+
return;
|
|
111
|
+
await new Promise((resolve) => {
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
signal?.removeEventListener("abort", onAbort);
|
|
114
|
+
resolve();
|
|
115
|
+
}, ms);
|
|
116
|
+
const onAbort = () => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
resolve();
|
|
119
|
+
};
|
|
120
|
+
if (signal?.aborted) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function isAborted(poll, querySignal) {
|
|
129
|
+
return Boolean(poll.signal?.aborted || poll.timeoutSignal?.aborted || querySignal?.aborted);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 对单个运行执行一次查询与存活校验。
|
|
133
|
+
*
|
|
134
|
+
* 核心设计:
|
|
135
|
+
* - 任务终态 terminal 必须且仅能由 record.status 推导;
|
|
136
|
+
* - 查询异常与宿主死亡属于观察失败,透传至 queryError,绝不修改 record.terminal;
|
|
137
|
+
* - 瞬态查询错误容忍:非中止查询异常在未达连续失败阈值前保持现有状态重试,成功后重置计数;
|
|
138
|
+
* - 确认宿主死亡时,以 HOST_LOST 立即记录观察失败并结束等待,严禁修改持久化运行记录。
|
|
139
|
+
*/
|
|
140
|
+
async function pollOnce(run, poll) {
|
|
141
|
+
const service = poll.service;
|
|
142
|
+
if (!service) {
|
|
143
|
+
run.queryError = new Error("Watch scope service unavailable");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const querySignal = poll.signal && poll.timeoutSignal
|
|
147
|
+
? AbortSignal.any([poll.signal, poll.timeoutSignal])
|
|
148
|
+
: poll.signal ?? poll.timeoutSignal;
|
|
149
|
+
try {
|
|
150
|
+
const record = await service.runs.get(run.source.runId, { signal: querySignal });
|
|
151
|
+
if (isAborted(poll, querySignal)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (record) {
|
|
155
|
+
run.record = record;
|
|
156
|
+
run.consecutiveErrors = 0;
|
|
157
|
+
run.queryError = undefined;
|
|
158
|
+
run.terminal = isTerminalRunStatus(record.status);
|
|
159
|
+
// 本地非终态任务:检测宿主存活(只读旁观,不修改数据库记录)
|
|
160
|
+
if (!run.terminal && run.source.source === "local") {
|
|
161
|
+
const dead = isRunHostDead(record, {
|
|
162
|
+
probe: poll.probe,
|
|
163
|
+
graceMs: poll.graceMs,
|
|
164
|
+
});
|
|
165
|
+
if (dead) {
|
|
166
|
+
run.queryError = {
|
|
167
|
+
code: "HOST_LOST",
|
|
168
|
+
message: `Execution host for run '${run.source.runId}' is lost or terminated unexpectedly`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (!run.record) {
|
|
174
|
+
// 记录从未出现过:保持等待
|
|
175
|
+
run.consecutiveErrors = 0;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// 曾被读到但当前查询返回空:保持最近已知状态
|
|
179
|
+
run.consecutiveErrors = 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (isAborted(poll, querySignal)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
run.consecutiveErrors += 1;
|
|
187
|
+
const maxErrors = poll.maxConsecutiveErrors ?? MAX_CONSECUTIVE_QUERY_ERRORS;
|
|
188
|
+
if (run.consecutiveErrors >= maxErrors) {
|
|
189
|
+
// 达到连续失败阈值,才真正打上 queryError 并退出该 run 的观察
|
|
190
|
+
run.queryError = err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 将追踪状态转换为聚合输出条目。
|
|
196
|
+
*/
|
|
197
|
+
function toOutcome(run) {
|
|
198
|
+
const record = run.record;
|
|
199
|
+
const outcome = {
|
|
200
|
+
runId: run.source.runId,
|
|
201
|
+
source: run.source.source,
|
|
202
|
+
...(run.source.requestId !== undefined ? { requestId: run.source.requestId } : {}),
|
|
203
|
+
status: record?.status ?? "unknown",
|
|
204
|
+
terminal: run.terminal,
|
|
205
|
+
};
|
|
206
|
+
if (record?.output !== undefined) {
|
|
207
|
+
outcome.data = record.output;
|
|
208
|
+
}
|
|
209
|
+
if (run.queryError !== undefined) {
|
|
210
|
+
outcome.queryError = run.queryError;
|
|
211
|
+
const err = run.queryError;
|
|
212
|
+
if (typeof err === "object" && err !== null && "code" in err && "message" in err) {
|
|
213
|
+
outcome.error = { code: err.code, message: err.message };
|
|
214
|
+
}
|
|
215
|
+
else if (err instanceof Error) {
|
|
216
|
+
outcome.error = { code: err.code ?? "WATCH_QUERY_FAILED", message: err.message };
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
outcome.error = { code: "WATCH_QUERY_FAILED", message: String(err) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (record?.error) {
|
|
223
|
+
outcome.error = record.error;
|
|
224
|
+
}
|
|
225
|
+
if (typeof record?.durationMs === "number") {
|
|
226
|
+
outcome.durationMs = record.durationMs;
|
|
227
|
+
}
|
|
228
|
+
return outcome;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 执行统一轮询等待循环。
|
|
232
|
+
*
|
|
233
|
+
* @param sources 已完成归属解析的运行标识集合
|
|
234
|
+
* @param poll 轮询依赖上下文
|
|
235
|
+
* @returns 聚合结果(含逐运行终态、输出、错误及整体结束原因)
|
|
236
|
+
*/
|
|
237
|
+
export async function pollRunsUntilTerminal(sources, poll) {
|
|
238
|
+
const sleep = poll.sleep ?? defaultSleep;
|
|
239
|
+
const now = poll.now ?? Date.now;
|
|
240
|
+
const progressIntervalMs = poll.progressIntervalMs !== undefined
|
|
241
|
+
? poll.progressIntervalMs
|
|
242
|
+
: DEFAULT_WATCH_PROGRESS_INTERVAL_MS;
|
|
243
|
+
const signal = poll.signal;
|
|
244
|
+
const timeoutSignal = poll.timeoutSignal;
|
|
245
|
+
const effectiveSignal = signal && timeoutSignal
|
|
246
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
247
|
+
: signal ?? timeoutSignal;
|
|
248
|
+
// 空集合边界防护:杜绝空集合假成功
|
|
249
|
+
if (sources.length === 0) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
timedOut: false,
|
|
253
|
+
interrupted: false,
|
|
254
|
+
reason: "observation_failed",
|
|
255
|
+
runs: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const tracked = sources.map((source) => ({
|
|
259
|
+
source,
|
|
260
|
+
record: source.initialRecord,
|
|
261
|
+
consecutiveErrors: 0,
|
|
262
|
+
terminal: source.initialRecord ? isTerminalRunStatus(source.initialRecord.status) : false,
|
|
263
|
+
}));
|
|
264
|
+
const startedAt = now();
|
|
265
|
+
const deadline = poll.deadline !== undefined
|
|
266
|
+
? poll.deadline
|
|
267
|
+
: poll.timeoutMs !== undefined
|
|
268
|
+
? startedAt + poll.timeoutMs
|
|
269
|
+
: undefined;
|
|
270
|
+
let lastProgressAt = startedAt;
|
|
271
|
+
let interrupted = false;
|
|
272
|
+
let timedOut = false;
|
|
273
|
+
while (true) {
|
|
274
|
+
// 待轮询集合:尚未到达终态且未出现致命观察异常
|
|
275
|
+
const pending = tracked.filter((r) => !r.terminal && !r.queryError);
|
|
276
|
+
if (pending.length === 0)
|
|
277
|
+
break;
|
|
278
|
+
await Promise.all(pending.map((r) => pollOnce(r, poll)));
|
|
279
|
+
// 优先识别取消来源
|
|
280
|
+
const current = now();
|
|
281
|
+
const isTimeout = (deadline !== undefined && current >= deadline) ||
|
|
282
|
+
Boolean(timeoutSignal?.aborted);
|
|
283
|
+
if (isTimeout) {
|
|
284
|
+
timedOut = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (signal?.aborted) {
|
|
288
|
+
interrupted = true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
const stillPending = tracked.filter((r) => !r.terminal && !r.queryError);
|
|
292
|
+
if (stillPending.length === 0)
|
|
293
|
+
break;
|
|
294
|
+
if (!poll.quiet &&
|
|
295
|
+
progressIntervalMs > 0 &&
|
|
296
|
+
current - lastProgressAt >= progressIntervalMs) {
|
|
297
|
+
const finished = tracked.length - stillPending.length;
|
|
298
|
+
writeStderr(`Waiting: ${stillPending.length} run(s) still running (${finished}/${tracked.length} settled)...`, poll.context);
|
|
299
|
+
lastProgressAt = current;
|
|
300
|
+
}
|
|
301
|
+
const remainingToDeadline = deadline !== undefined ? deadline - current : undefined;
|
|
302
|
+
const waitMs = remainingToDeadline !== undefined
|
|
303
|
+
? Math.min(poll.intervalMs, Math.max(remainingToDeadline, 1))
|
|
304
|
+
: poll.intervalMs;
|
|
305
|
+
await sleep(waitMs, effectiveSignal);
|
|
306
|
+
const currentAfterSleep = now();
|
|
307
|
+
const isTimeoutAfterSleep = (deadline !== undefined && currentAfterSleep >= deadline) ||
|
|
308
|
+
Boolean(timeoutSignal?.aborted);
|
|
309
|
+
if (isTimeoutAfterSleep) {
|
|
310
|
+
timedOut = true;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
if (signal?.aborted) {
|
|
314
|
+
interrupted = true;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const runs = tracked.map(toOutcome);
|
|
319
|
+
const allTerminal = runs.length > 0 && runs.every((r) => r.terminal);
|
|
320
|
+
const allSuccess = runs.length > 0 && runs.every((r) => r.terminal && r.status === "success" && !r.error);
|
|
321
|
+
const anyObservationError = runs.some((r) => r.error !== undefined && !r.terminal);
|
|
322
|
+
let reason;
|
|
323
|
+
if (timedOut) {
|
|
324
|
+
reason = "timeout";
|
|
325
|
+
}
|
|
326
|
+
else if (interrupted) {
|
|
327
|
+
reason = "interrupted";
|
|
328
|
+
}
|
|
329
|
+
else if (anyObservationError || !allTerminal) {
|
|
330
|
+
reason = "observation_failed";
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
reason = "all_terminal";
|
|
334
|
+
}
|
|
335
|
+
const ok = runs.length > 0 && allTerminal && allSuccess && reason === "all_terminal";
|
|
336
|
+
return { ok, timedOut, interrupted, reason, runs };
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 提取错误条目的错误码与错误信息。
|
|
340
|
+
*/
|
|
341
|
+
function extractRunError(err) {
|
|
342
|
+
if (typeof err === "object" && err !== null) {
|
|
343
|
+
const code = "code" in err && err.code !== undefined && err.code !== null
|
|
344
|
+
? String(err.code)
|
|
345
|
+
: undefined;
|
|
346
|
+
const message = "message" in err && typeof err.message === "string"
|
|
347
|
+
? err.message
|
|
348
|
+
: JSON.stringify(err);
|
|
349
|
+
return { code, message };
|
|
350
|
+
}
|
|
351
|
+
if (typeof err === "string") {
|
|
352
|
+
return { message: err };
|
|
353
|
+
}
|
|
354
|
+
return { message: String(err) };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* 人读模式渲染聚合结果:每运行一行终态摘要。
|
|
358
|
+
*/
|
|
359
|
+
export function renderWatchSummary(agg, remoteSuffix) {
|
|
360
|
+
const lines = [];
|
|
361
|
+
const scopeLabel = remoteSuffix ? `Watched Runs ${remoteSuffix}` : "Watched Runs";
|
|
362
|
+
lines.push(`${scopeLabel} (${agg.runs.length}):\n`);
|
|
363
|
+
lines.push(` ${"RUN ID".padEnd(38)} ${"SOURCE".padEnd(8)} ${"REQUEST ID".padEnd(24)} ${"STATUS".padEnd(12)} DURATION`);
|
|
364
|
+
lines.push(" " + "-".repeat(99));
|
|
365
|
+
for (const r of agg.runs) {
|
|
366
|
+
const duration = typeof r.durationMs === "number" ? `${r.durationMs}ms` : r.terminal ? "-" : "(in flight)";
|
|
367
|
+
const requestId = (r.requestId || "-").padEnd(24);
|
|
368
|
+
lines.push(` ${r.runId.padEnd(38)} ${r.source.padEnd(8)} ${requestId} ${r.status.padEnd(12)} ${duration}`);
|
|
369
|
+
}
|
|
370
|
+
lines.push("");
|
|
371
|
+
if (agg.reason === "timeout") {
|
|
372
|
+
lines.push("Result: timed out before all runs reached a terminal state.");
|
|
373
|
+
}
|
|
374
|
+
else if (agg.reason === "interrupted") {
|
|
375
|
+
lines.push("Result: interrupted before all runs reached a terminal state.");
|
|
376
|
+
}
|
|
377
|
+
else if (agg.reason === "observation_failed") {
|
|
378
|
+
lines.push("Result: observation failed before all runs reached a terminal state (host lost or query error).");
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
lines.push(`Result: ${agg.ok ? "all runs succeeded" : "one or more runs failed"}.`);
|
|
382
|
+
}
|
|
383
|
+
const errorRuns = agg.runs.filter((r) => r.error !== undefined || r.queryError !== undefined);
|
|
384
|
+
if (errorRuns.length > 0) {
|
|
385
|
+
lines.push("");
|
|
386
|
+
lines.push("Errors:");
|
|
387
|
+
for (const r of errorRuns) {
|
|
388
|
+
const err = r.error ?? r.queryError;
|
|
389
|
+
const { code, message } = extractRunError(err);
|
|
390
|
+
const errLabel = code ? `[${code}] ${message}` : message;
|
|
391
|
+
lines.push(` - Run ${r.runId}: ${errLabel}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return lines.join("\n");
|
|
395
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface Envelope<T = unknown> {
|
|
|
27
27
|
message: string;
|
|
28
28
|
details?: unknown;
|
|
29
29
|
};
|
|
30
|
+
hint?: string;
|
|
30
31
|
meta?: Record<string, unknown>;
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
@@ -108,6 +109,42 @@ export interface ProjectDetailInfo {
|
|
|
108
109
|
}>;
|
|
109
110
|
playbooksMap?: Map<string, any>;
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* 机器视图工程配置声明条目。
|
|
114
|
+
*/
|
|
115
|
+
export interface ProjectDetailConfigItem {
|
|
116
|
+
description?: string;
|
|
117
|
+
default?: unknown;
|
|
118
|
+
secret: boolean;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 机器视图规程条目。
|
|
122
|
+
*/
|
|
123
|
+
export interface ProjectDetailPlaybookItem {
|
|
124
|
+
id: string;
|
|
125
|
+
description?: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 机器视图动作条目。
|
|
129
|
+
*/
|
|
130
|
+
export interface ProjectDetailActionItem {
|
|
131
|
+
id: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 机器模式工程详情输出结构。
|
|
136
|
+
*/
|
|
137
|
+
export interface ProjectDetailJson {
|
|
138
|
+
id: string;
|
|
139
|
+
name: string;
|
|
140
|
+
version: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
root: string;
|
|
143
|
+
config: Record<string, ProjectDetailConfigItem>;
|
|
144
|
+
playbooks: ProjectDetailPlaybookItem[];
|
|
145
|
+
actions: ProjectDetailActionItem[];
|
|
146
|
+
hints?: string[];
|
|
147
|
+
}
|
|
111
148
|
/**
|
|
112
149
|
* 聚合 Package 信息结构。
|
|
113
150
|
*/
|
package/dist/utils/cert.js
CHANGED
|
@@ -4,7 +4,6 @@ import { isIP } from "node:net";
|
|
|
4
4
|
import { hostname, networkInterfaces } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { getActionDockHome, } from "@actiondock/core/registry";
|
|
7
|
-
import { generate } from "selfsigned";
|
|
8
7
|
/**
|
|
9
8
|
* 判定字符串是否为有效 IPv4 或 IPv6 地址。
|
|
10
9
|
*/
|
|
@@ -172,6 +171,7 @@ export async function ensureSelfSignedCertificate(options) {
|
|
|
172
171
|
}
|
|
173
172
|
const altNames = collectSanEntries(options?.host);
|
|
174
173
|
const notAfterDate = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000);
|
|
174
|
+
const { generate } = await import("selfsigned");
|
|
175
175
|
const pems = await generate([{ name: "commonName", value: "localhost" }], {
|
|
176
176
|
notAfterDate,
|
|
177
177
|
keySize: 2048,
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/dist/utils/input.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stripBom } from "@actiondock/core/project";
|
|
2
|
-
export { resolveActionInput, type ResolveActionInputOptions, parseJson, InputError, FlatInputError,
|
|
2
|
+
export { resolveActionInput, type ResolveActionInputOptions, parseJson, InputError, FlatInputError, } from "@actiondock/core/project";
|
|
3
|
+
export { formatActionDetail } from "../renderer.js";
|
|
3
4
|
export { stripBom };
|
|
4
5
|
/**
|
|
5
6
|
* 从标准输入流中完整读取全部数据并转换为 UTF-8 字符串。
|
package/dist/utils/input.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readStdinBounded, stripBom } from "@actiondock/core/project";
|
|
2
|
-
export { resolveActionInput, parseJson, InputError, FlatInputError,
|
|
2
|
+
export { resolveActionInput, parseJson, InputError, FlatInputError, } from "@actiondock/core/project";
|
|
3
|
+
export { formatActionDetail } from "../renderer.js";
|
|
3
4
|
// BOM 剥离单一事实源转引:core 已提供逐字一致的实现,此处仅保留导出面兼容。
|
|
4
5
|
export { stripBom };
|
|
5
6
|
/**
|