dsh-github-flow 0.6.0 → 0.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.
- package/README.en.md +98 -15
- package/README.md +101 -15
- package/docs/UPGRADE-NOTES.md +12 -11
- package/docs/architecture.md +56 -13
- package/lib/api/route-helpers.d.ts +19 -0
- package/lib/api/route-helpers.js +54 -0
- package/lib/api/routes-actions.js +75 -1
- package/lib/api/routes-lifecycle.js +8 -1
- package/lib/api/routes-overview.d.ts +26 -0
- package/lib/api/routes-overview.js +276 -34
- package/lib/api/routes-proxy.js +8 -1
- package/lib/api/routes.d.ts +11 -1
- package/lib/api/routes.js +15 -2
- package/lib/cache.d.ts +0 -21
- package/lib/cache.js +0 -37
- package/lib/client.d.ts +2 -1
- package/lib/client.js +1585 -429
- package/lib/destructive.d.ts +59 -0
- package/lib/destructive.js +285 -0
- package/lib/executor/args-guard.d.ts +17 -0
- package/lib/executor/args-guard.js +23 -0
- package/lib/executor/failure-anchors.d.ts +52 -0
- package/lib/executor/failure-anchors.js +116 -0
- package/lib/executor/pr-review.d.ts +95 -0
- package/lib/executor/pr-review.js +175 -0
- package/lib/executor/run-log.d.ts +36 -0
- package/lib/executor/run-log.js +78 -0
- package/lib/executor.d.ts +156 -3
- package/lib/executor.js +594 -28
- package/lib/github-service.d.ts +1 -1
- package/lib/main.d.ts +92 -18
- package/lib/main.js +186 -95
- package/lib/renderers/common.d.ts +0 -3
- package/lib/renderers/common.js +8 -44
- package/lib/renderers/markdown.d.ts +1 -0
- package/lib/renderers/markdown.js +1 -0
- package/lib/renderers/pr.js +26 -1
- package/lib/renderers/task.d.ts +9 -0
- package/lib/renderers/task.js +120 -0
- package/lib/review-rules.d.ts +60 -0
- package/lib/review-rules.js +204 -0
- package/lib/shared/state-badges.d.ts +42 -0
- package/lib/shared/state-badges.js +102 -0
- package/lib/task-primitives.d.ts +109 -0
- package/lib/task-primitives.js +194 -0
- package/lib/tool-tiers.d.ts +57 -0
- package/lib/tool-tiers.js +168 -0
- package/lib/tools/agent-task.js +4 -4
- package/lib/tools/api.js +18 -6
- package/lib/tools/cache.js +42 -40
- package/lib/tools/codespace.js +4 -12
- package/lib/tools/discussion.js +4 -4
- package/lib/tools/gist.d.ts +1 -39
- package/lib/tools/gist.js +4 -14
- package/lib/tools/help.js +52 -13
- package/lib/tools/issue.js +16 -28
- package/lib/tools/label.js +4 -15
- package/lib/tools/pr.d.ts +47 -0
- package/lib/tools/pr.js +170 -33
- package/lib/tools/project.js +4 -13
- package/lib/tools/release.js +11 -22
- package/lib/tools/repo.js +4 -4
- package/lib/tools/ruleset.js +9 -6
- package/lib/tools/run.js +76 -24
- package/lib/tools/secret.js +11 -17
- package/lib/tools/skill.js +18 -22
- package/lib/tools/status.js +4 -4
- package/lib/tools/task.d.ts +219 -0
- package/lib/tools/task.js +457 -0
- package/lib/types.d.ts +183 -1
- package/lib/ui/help-modal.d.ts +1 -1
- package/lib/ui/help-modal.js +20 -10
- package/lib/ui/issue-modal.js +7 -3
- package/lib/ui/pr-modal.js +50 -11
- package/lib/ui/run-modal.d.ts +26 -0
- package/lib/ui/run-modal.js +486 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +370 -44
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/workspace-panel.js +78 -23
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { ciBadge } from "../shared/state-badges.js";
|
|
2
|
+
/**
|
|
3
|
+
* 日志请求的客户端超时。
|
|
4
|
+
*
|
|
5
|
+
* 服务端总预算 90s,这里放 100s:让服务端的「超预算」显式错误先到;
|
|
6
|
+
* 而连接卡在代理/DNS 层永远不返回时,客户端的 AbortController 是最后一道闸——
|
|
7
|
+
* 否则按钮会永远停在「正在下载日志…」,用户只能刷新页面。
|
|
8
|
+
*/
|
|
9
|
+
var LOG_FETCH_TIMEOUT_MS = 100_000;
|
|
10
|
+
/**
|
|
11
|
+
* 结论 → 中文文案。
|
|
12
|
+
*
|
|
13
|
+
* ciBadge 只统一了 tone 与英文 label(服务端 Markdown 与右侧栏共用同一份映射),
|
|
14
|
+
* 中文必须在本组件单独补一层;改 ciBadge 会连带改掉 CLI 与服务端输出。
|
|
15
|
+
*/
|
|
16
|
+
var RUN_CONCLUSION_LABEL = {
|
|
17
|
+
success: "成功",
|
|
18
|
+
failure: "失败",
|
|
19
|
+
cancelled: "已取消",
|
|
20
|
+
skipped: "已跳过",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 未命中中文表时保留 GitHub 原始英文(running / queued / timed_out 等):
|
|
24
|
+
* 这些取值没有稳定的中文对应,硬翻反而让用户对不上 GitHub 页面。
|
|
25
|
+
*/
|
|
26
|
+
function zhConclusion(conclusion, fallback) {
|
|
27
|
+
var zh = RUN_CONCLUSION_LABEL[String(conclusion || "").toLowerCase()];
|
|
28
|
+
return zh || fallback;
|
|
29
|
+
}
|
|
30
|
+
function pad2(n) {
|
|
31
|
+
return n < 10 ? "0" + n : String(n);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Actions run / job / step 的耗时格式化(导出以便单测直接覆盖)。
|
|
35
|
+
*
|
|
36
|
+
* 为什么不用 formatRelativeTime:那个函数答的是「发生在多久前」,这里是「跑了多久」;
|
|
37
|
+
* 而且 run → job → step 三层共用同一套精度规则(<60s → `12s`、<60m → `3m20s`、
|
|
38
|
+
* 其余 `1h05m`),分钟/秒补零才能让三层的列宽稳定对齐。
|
|
39
|
+
* 任一端缺失或不可解析一律返回空串:排队中/已跳过的 job 没有 startedAt/completedAt,
|
|
40
|
+
* 硬算只会渲染出「0s」这种假精确值,比不显示更误导。
|
|
41
|
+
*/
|
|
42
|
+
export function formatRunDuration(startISO, endISO) {
|
|
43
|
+
var startT = typeof startISO === "string" ? Date.parse(startISO) : NaN;
|
|
44
|
+
var endT = typeof endISO === "string" ? Date.parse(endISO) : NaN;
|
|
45
|
+
// endT < startT 视为非法区间(时钟漂移 / 字段错配),同样不显示
|
|
46
|
+
if (isNaN(startT) || isNaN(endT) || endT < startT)
|
|
47
|
+
return "";
|
|
48
|
+
var totalSec = Math.round((endT - startT) / 1000);
|
|
49
|
+
if (totalSec < 60)
|
|
50
|
+
return totalSec + "s";
|
|
51
|
+
var totalMin = Math.floor(totalSec / 60);
|
|
52
|
+
if (totalMin < 60)
|
|
53
|
+
return totalMin + "m" + pad2(totalSec % 60) + "s";
|
|
54
|
+
return Math.floor(totalMin / 60) + "h" + pad2(totalMin % 60) + "m";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 初始展开哪一个 job:优先第一个失败的(排障入口就在那里),全绿时展开第一个。
|
|
58
|
+
* 必须在 setData 之后用真实数据计算——写死 {} 会让「默认展开」永远不生效。
|
|
59
|
+
*/
|
|
60
|
+
function computeInitialExpanded(d) {
|
|
61
|
+
var init = {};
|
|
62
|
+
var jobs = d && Array.isArray(d.jobs) ? d.jobs : [];
|
|
63
|
+
if (jobs.length === 0)
|
|
64
|
+
return init;
|
|
65
|
+
var target = jobs[0];
|
|
66
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
67
|
+
var job = jobs[i];
|
|
68
|
+
if (String(job && job.conclusion ? job.conclusion : "").toLowerCase() === "failure") {
|
|
69
|
+
target = job;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (target && target.databaseId !== undefined && target.databaseId !== null) {
|
|
74
|
+
init[target.databaseId] = true;
|
|
75
|
+
}
|
|
76
|
+
return init;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Actions Run 详情弹窗(只读:run 元信息 + job/step 树 + 失败日志片段)。
|
|
80
|
+
*
|
|
81
|
+
* 单列纵向:run 的信息天然是「job 列表 + 一条日志流」的阅读顺序,没有需要并排
|
|
82
|
+
* 对照的第二个信息源(PR 才有 diff 双栏)。
|
|
83
|
+
* 日志刻意渲染成 <pre> 原文而不是 markdown-lite:原始日志里的 `#`、`|`、`---`
|
|
84
|
+
* 是 shell 注释、表格与 diff 分隔符,交给 Markdown 解析会把堆栈与 diff 撕碎。
|
|
85
|
+
*/
|
|
86
|
+
export function RunDetailModal(props) {
|
|
87
|
+
var runId = props.runId;
|
|
88
|
+
var repoNameWithOwner = props.repoNameWithOwner || "";
|
|
89
|
+
var cwd = props.cwd;
|
|
90
|
+
var onClose = props.onClose;
|
|
91
|
+
var showToast = props.showToast || (function () { });
|
|
92
|
+
// Hooks 必须先于任何 early return 无条件执行(见 issue-modal 的同类注释):
|
|
93
|
+
// runId 由空变有值时两次渲染的 Hook 数量必须一致,否则 React 直接抛错。
|
|
94
|
+
// 因此 useState 与两个 useEffect 全部集中在这里,`if (!runId) return null`
|
|
95
|
+
// 只能出现在它们之后。
|
|
96
|
+
var [data, setData] = React.useState(null);
|
|
97
|
+
var [loading, setLoading] = React.useState(true);
|
|
98
|
+
var [error, setError] = React.useState(null);
|
|
99
|
+
var [expanded, setExpanded] = React.useState({});
|
|
100
|
+
var [logState, setLogState] = React.useState({ status: "idle" });
|
|
101
|
+
React.useEffect(function () {
|
|
102
|
+
var onKeyDown = function (e) {
|
|
103
|
+
if (e.key === "Escape" || e.keyCode === 27)
|
|
104
|
+
onClose();
|
|
105
|
+
};
|
|
106
|
+
window.addEventListener("keydown", onKeyDown);
|
|
107
|
+
// 必须成对移除:弹窗在面板里会被反复开关,漏掉会累积监听器(Esc 一次关多个)
|
|
108
|
+
return function () { window.removeEventListener("keydown", onKeyDown); };
|
|
109
|
+
}, [onClose]);
|
|
110
|
+
React.useEffect(function () {
|
|
111
|
+
if (!runId) {
|
|
112
|
+
setLoading(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setLoading(true);
|
|
116
|
+
setError(null);
|
|
117
|
+
// 换了 run 就是全新上下文:展开态与日志结果都不能沿用上一个 run 的
|
|
118
|
+
setExpanded({});
|
|
119
|
+
setLogState({ status: "idle" });
|
|
120
|
+
var url = "/api/github/run-detail?run_id=" +
|
|
121
|
+
encodeURIComponent(String(runId)) +
|
|
122
|
+
(repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
|
|
123
|
+
(cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
|
|
124
|
+
fetch(url)
|
|
125
|
+
.then(async function (res) {
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
// 非 2xx 时 body 里可能带一段很长的 gh 原始报错,优先原样透出给用户
|
|
128
|
+
var errText = await res.text().catch(function () { return ""; });
|
|
129
|
+
var j = null;
|
|
130
|
+
try {
|
|
131
|
+
j = JSON.parse(errText);
|
|
132
|
+
}
|
|
133
|
+
catch (e) { }
|
|
134
|
+
throw new Error((j && j.error) || errText || ("服务响应异常 (" + res.status + ")"));
|
|
135
|
+
}
|
|
136
|
+
return res.json();
|
|
137
|
+
})
|
|
138
|
+
.then(function (json) {
|
|
139
|
+
setLoading(false);
|
|
140
|
+
if (json && json.ok && json.data) {
|
|
141
|
+
setData(json.data);
|
|
142
|
+
// 默认展开必须基于刚拿到的数据算,不能预设 {}
|
|
143
|
+
setExpanded(computeInitialExpanded(json.data));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
setError((json && json.error) || "获取 Actions Run 详情失败");
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
.catch(function (err) {
|
|
150
|
+
setLoading(false);
|
|
151
|
+
setError((err && err.message) || "网络请求异常");
|
|
152
|
+
});
|
|
153
|
+
}, [runId, repoNameWithOwner, cwd]);
|
|
154
|
+
if (!runId)
|
|
155
|
+
return null;
|
|
156
|
+
var h = React.createElement;
|
|
157
|
+
var toggleJob = function (jobId) {
|
|
158
|
+
setExpanded(function (prev) {
|
|
159
|
+
// 必须新建对象:原地改 prev 引用不变,React 判定状态未变,不会重渲染
|
|
160
|
+
var next = {};
|
|
161
|
+
for (var k in prev) {
|
|
162
|
+
if (Object.prototype.hasOwnProperty.call(prev, k))
|
|
163
|
+
next[k] = prev[k];
|
|
164
|
+
}
|
|
165
|
+
next[jobId] = !next[jobId];
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
/** 结论徽章:tone 来自 shared/state-badges,文案走中文表 */
|
|
170
|
+
var badgeEl = function (conclusion, status) {
|
|
171
|
+
var b = ciBadge(status, conclusion);
|
|
172
|
+
if (!b)
|
|
173
|
+
return null;
|
|
174
|
+
return h("span", {
|
|
175
|
+
className: "dsh-github-badge dsh-badge-" + b.tone,
|
|
176
|
+
style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 },
|
|
177
|
+
}, zhConclusion(conclusion, b.label));
|
|
178
|
+
};
|
|
179
|
+
var renderMeta = function (d) {
|
|
180
|
+
var parts = [];
|
|
181
|
+
var ev = eventLabel(d.event);
|
|
182
|
+
if (ev)
|
|
183
|
+
parts.push(h("span", { key: "ev" }, ev));
|
|
184
|
+
if (d.headBranch) {
|
|
185
|
+
parts.push(h("span", { key: "br", className: "dsh-github-run-mono", title: "分支 " + d.headBranch }, d.headBranch));
|
|
186
|
+
}
|
|
187
|
+
if (d.headSha) {
|
|
188
|
+
// 短 SHA 只留 7 位(GitHub 约定长度),完整值放 title,方便核对而不是复制错
|
|
189
|
+
parts.push(h("span", { key: "sha", className: "dsh-github-run-mono", title: "完整 SHA: " + d.headSha }, String(d.headSha).slice(0, 7)));
|
|
190
|
+
}
|
|
191
|
+
if (d.createdAt)
|
|
192
|
+
parts.push(h("span", { key: "created" }, formatRelativeTime(d.createdAt)));
|
|
193
|
+
// run 没有 completedAt 字段,用 updatedAt 作终点:运行结束后它就是完成时间
|
|
194
|
+
var dur = formatRunDuration(d.startedAt, d.updatedAt);
|
|
195
|
+
if (dur)
|
|
196
|
+
parts.push(h("span", { key: "dur" }, "耗时 " + dur));
|
|
197
|
+
if (d.fieldsDegraded) {
|
|
198
|
+
parts.push(h("span", { key: "degraded", className: "dsh-github-run-degraded", title: "部分字段未取到(job 列表或结论缺失),服务端已降级返回" }, "⚠️ 部分字段降级"));
|
|
199
|
+
}
|
|
200
|
+
if (parts.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
return h("div", { className: "dsh-github-run-meta" }, ...parts);
|
|
203
|
+
};
|
|
204
|
+
var renderSteps = function (job) {
|
|
205
|
+
var steps = job && Array.isArray(job.steps) ? job.steps : [];
|
|
206
|
+
if (steps.length === 0) {
|
|
207
|
+
return h("div", { className: "dsh-github-run-steps" }, h("div", { className: "dsh-github-run-empty" }, "该 job 没有可读步骤(可能仍在排队)"));
|
|
208
|
+
}
|
|
209
|
+
return h("div", { className: "dsh-github-run-steps" }, steps.map(function (s, i) {
|
|
210
|
+
var failed = String((s && s.conclusion) || "").toLowerCase() === "failure";
|
|
211
|
+
return h("div", {
|
|
212
|
+
key: "step-" + (s && s.number !== undefined ? s.number : i),
|
|
213
|
+
className: "dsh-github-run-step" + (failed ? " dsh-github-run-step-failed" : ""),
|
|
214
|
+
}, h("span", null, String(s && s.number !== undefined ? s.number : i + 1)), h("span", { title: (s && s.name) || "" }, (s && s.name) || "未命名步骤"),
|
|
215
|
+
// 徽章外层固定包一个 span:徽章为空时若不占位,grid 第 3 列消失,
|
|
216
|
+
// 后面的耗时列会左移到徽章列,四列对不齐
|
|
217
|
+
h("span", null, badgeEl(s && s.conclusion, s && s.status)), h("span", { className: "dsh-github-run-duration" }, formatRunDuration(s && s.startedAt, s && s.completedAt)));
|
|
218
|
+
}));
|
|
219
|
+
};
|
|
220
|
+
var renderJob = function (job, idx) {
|
|
221
|
+
var steps = job && Array.isArray(job.steps) ? job.steps : [];
|
|
222
|
+
var failedSteps = 0;
|
|
223
|
+
steps.forEach(function (s) {
|
|
224
|
+
if (String((s && s.conclusion) || "").toLowerCase() === "failure")
|
|
225
|
+
failedSteps++;
|
|
226
|
+
});
|
|
227
|
+
var jobId = job && job.databaseId;
|
|
228
|
+
var isOpen = Boolean(expanded[jobId]);
|
|
229
|
+
var jobDur = formatRunDuration(job && job.startedAt, job && job.completedAt);
|
|
230
|
+
return h("div", { key: "job-" + (jobId !== undefined && jobId !== null ? jobId : idx), className: "dsh-github-run-job" }, h("button", {
|
|
231
|
+
type: "button",
|
|
232
|
+
className: "dsh-github-run-job-header",
|
|
233
|
+
"aria-expanded": isOpen ? "true" : "false",
|
|
234
|
+
onClick: function () { toggleJob(jobId); },
|
|
235
|
+
title: isOpen ? "收起该 job 的步骤" : "展开该 job 的步骤",
|
|
236
|
+
}, h("span", { className: "dsh-github-run-caret", "aria-hidden": "true" }, isOpen ? "▾" : "▸"), h("span", { className: "dsh-github-run-job-name", title: (job && job.name) || "" }, (job && job.name) || "未命名 job"), badgeEl(job && job.conclusion, job && job.status), jobDur ? h("span", { className: "dsh-github-run-duration" }, jobDur) : null, steps.length > 0
|
|
237
|
+
? h("span", { className: "dsh-github-run-failed-count" + (failedSteps > 0 ? " is-hit" : "") }, failedSteps + "/" + steps.length + " 步失败")
|
|
238
|
+
: null), isOpen ? renderSteps(job) : null);
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* 选出该拉日志的 job。
|
|
242
|
+
*
|
|
243
|
+
* 为什么不能只看 `job.conclusion === 'failure'`:按钮的出现条件是「有失败 step
|
|
244
|
+
* 或失败 job」,而 `timed_out` / `cancelled` 的 job 完全可能带着失败 step
|
|
245
|
+
* (`gh run view --help` 对 `--log-failed` 的定义是 "any failed steps")。
|
|
246
|
+
* 两处口径不一致,用户就会看到「按钮在、点下去说没有失败的 job」。
|
|
247
|
+
*/
|
|
248
|
+
var pickLogJobIds = function (jobs) {
|
|
249
|
+
var ids = [];
|
|
250
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
251
|
+
var job = jobs[i];
|
|
252
|
+
var jobId = job && job.databaseId;
|
|
253
|
+
// databaseId 缺失或为 0 的 job 无法定向拉取(服务端 requirement 是正整数)
|
|
254
|
+
if (typeof jobId !== "number" || jobId <= 0)
|
|
255
|
+
continue;
|
|
256
|
+
var jobFailed = String((job && job.conclusion) || "").toLowerCase() === "failure";
|
|
257
|
+
var stepFailed = Array.isArray(job && job.steps) &&
|
|
258
|
+
job.steps.some(function (s) {
|
|
259
|
+
return String((s && s.conclusion) || "").toLowerCase() === "failure";
|
|
260
|
+
});
|
|
261
|
+
if (jobFailed || stepFailed)
|
|
262
|
+
ids.push(jobId);
|
|
263
|
+
}
|
|
264
|
+
return ids;
|
|
265
|
+
};
|
|
266
|
+
var loadLogs = function () {
|
|
267
|
+
if (!data || logState.status === "loading")
|
|
268
|
+
return;
|
|
269
|
+
var jobs = Array.isArray(data.jobs) ? data.jobs : [];
|
|
270
|
+
var allIds = pickLogJobIds(jobs);
|
|
271
|
+
if (allIds.length === 0) {
|
|
272
|
+
showToast("⚠️ 没有失败的 job 可提取日志");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// 服务端上限 3 个(超标直接 400)。这里先按上限请求,并在界面上明示「只取了前 3 个」,
|
|
276
|
+
// 不让人以为这次把全部失败 job 都拉完了。
|
|
277
|
+
var jobIds = allIds.slice(0, 3);
|
|
278
|
+
setLogState({ status: "loading" });
|
|
279
|
+
showToast("⏳ 正在下载失败日志(最长 90 秒)");
|
|
280
|
+
var url = "/api/github/run-logs?run_id=" +
|
|
281
|
+
encodeURIComponent(String(runId)) +
|
|
282
|
+
// job_id 是逗号分隔的数字列表:数字与逗号都是 URL 安全字符,不再编码,
|
|
283
|
+
// 免得某些服务端实现按未解码的原始串切分
|
|
284
|
+
"&job_id=" + jobIds.join(",") +
|
|
285
|
+
(repoNameWithOwner ? "&repo=" + encodeURIComponent(repoNameWithOwner) : "") +
|
|
286
|
+
(cwd ? "&cwd=" + encodeURIComponent(cwd) : "");
|
|
287
|
+
/*
|
|
288
|
+
* 客户端超时不是可选项:服务端总预算 90s,但连接可能停在 DNS/代理层永远不返回。
|
|
289
|
+
* 不设闸门的话按钮会永远停在「正在下载日志…」,用户只能刷新页面。
|
|
290
|
+
* 100s 比服务端预算略长,让服务端的显式错误优先到达。
|
|
291
|
+
*/
|
|
292
|
+
var controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
293
|
+
// 闭包里不能再依赖可空收窄(TS 对捕获变量不做 narrow),在 if 内绑一个非空引用
|
|
294
|
+
var timer = null;
|
|
295
|
+
if (controller) {
|
|
296
|
+
var ctrl = controller;
|
|
297
|
+
timer = setTimeout(function () { ctrl.abort(); }, LOG_FETCH_TIMEOUT_MS);
|
|
298
|
+
}
|
|
299
|
+
var clearTimer = function () {
|
|
300
|
+
if (timer) {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
timer = null;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
fetch(url, controller ? { signal: controller.signal } : undefined)
|
|
306
|
+
.then(async function (res) {
|
|
307
|
+
if (!res.ok) {
|
|
308
|
+
var errText = await res.text().catch(function () { return ""; });
|
|
309
|
+
var j = null;
|
|
310
|
+
try {
|
|
311
|
+
j = JSON.parse(errText);
|
|
312
|
+
}
|
|
313
|
+
catch (e) { }
|
|
314
|
+
throw new Error((j && j.error) || errText || ("日志接口响应异常 (" + res.status + ")"));
|
|
315
|
+
}
|
|
316
|
+
return res.json();
|
|
317
|
+
})
|
|
318
|
+
.then(function (json) {
|
|
319
|
+
clearTimer();
|
|
320
|
+
if (json && json.ok && json.data) {
|
|
321
|
+
setLogState({ status: "ok", data: json.data });
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
setLogState({ status: "error", error: (json && json.error) || "获取运行日志失败" });
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
.catch(function (err) {
|
|
328
|
+
clearTimer();
|
|
329
|
+
var aborted = err && (err.name === "AbortError" || err.code === 20);
|
|
330
|
+
setLogState({
|
|
331
|
+
status: "error",
|
|
332
|
+
error: aborted
|
|
333
|
+
? "日志拉取超时(100 秒)已中止。GitHub 日志接口在代理下常常长时间无响应,请稍后重试或直接点右上角「GitHub ↗」查看。"
|
|
334
|
+
: (err && err.message) || "网络请求异常(日志链路较慢,可直接重试)",
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
var renderLogSection = function () {
|
|
339
|
+
if (!data)
|
|
340
|
+
return null;
|
|
341
|
+
// failedSteps 只覆盖「step 级失败」;job 级失败但零失败 step 是真实形态
|
|
342
|
+
// (setup 阶段失败、被 cancellation 打断),此时日志按钮同样必须出现。
|
|
343
|
+
var hasFailed = Number(data.failedSteps) > 0 || Number(data.failedJobs) > 0;
|
|
344
|
+
if (!hasFailed && logState.status === "idle")
|
|
345
|
+
return null;
|
|
346
|
+
var loadingLog = logState.status === "loading";
|
|
347
|
+
var children = [];
|
|
348
|
+
if (hasFailed) {
|
|
349
|
+
children.push(h("button", {
|
|
350
|
+
key: "log-btn",
|
|
351
|
+
type: "button",
|
|
352
|
+
className: "dsh-github-run-log-btn",
|
|
353
|
+
onClick: loadLogs,
|
|
354
|
+
disabled: loadingLog,
|
|
355
|
+
title: "从 GitHub 拉取失败 job 的原始日志片段(较慢,最长 90 秒)",
|
|
356
|
+
}, loadingLog ? "正在下载日志…" : logState.status === "ok" ? "📄 重新提取失败日志" : "📄 提取失败日志"));
|
|
357
|
+
// 服务端一次最多 3 个 job:超出的必须说出来,否则用户以为全拉完了
|
|
358
|
+
var failedJobTotal = pickLogJobIds(Array.isArray(data.jobs) ? data.jobs : []).length;
|
|
359
|
+
if (failedJobTotal > 3) {
|
|
360
|
+
children.push(h("div", { key: "log-cap", className: "dsh-github-run-log-hint" }, "本次仅提取前 3 个失败 job 的日志(共 " + failedJobTotal + " 个),其余请到 GitHub 查看。"));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (loadingLog) {
|
|
364
|
+
children.push(h("div", { key: "log-hint", className: "dsh-github-run-log-hint" }, "⏳ 正在拉取失败日志——GitHub 日志接口较慢(最长 90 秒),请不要关闭弹窗。"));
|
|
365
|
+
}
|
|
366
|
+
if (logState.status === "error") {
|
|
367
|
+
children.push(h("div", { key: "log-error", className: "dsh-github-run-error" }, h("div", { className: "dsh-github-run-error-title" }, "⚠️ 提取失败日志失败"), h("div", { className: "dsh-github-run-error-text" }, logState.error || "未知错误"),
|
|
368
|
+
// 只有显式点击才重试:慢链路不能自动重试(见 RunLogState 注释)
|
|
369
|
+
h("button", { key: "retry", type: "button", className: "dsh-github-run-log-btn", onClick: loadLogs }, "🔁 重试")));
|
|
370
|
+
}
|
|
371
|
+
if (logState.status === "ok" && logState.data) {
|
|
372
|
+
var logData = logState.data;
|
|
373
|
+
var logJobs = Array.isArray(logData.jobs) ? logData.jobs : [];
|
|
374
|
+
var logErrors = Array.isArray(logData.errors) ? logData.errors : [];
|
|
375
|
+
if (logJobs.length === 0 && logErrors.length === 0) {
|
|
376
|
+
children.push(h("div", { key: "log-none", className: "dsh-github-run-empty" }, "GitHub 未返回可读日志(可能日志已过期或权限不足)"));
|
|
377
|
+
}
|
|
378
|
+
logJobs.forEach(function (block, i) {
|
|
379
|
+
children.push(h("div", { key: "log-block-" + i, className: "dsh-github-run-log-block" }, h("div", { className: "dsh-github-run-log-title" }, "📄 " + (block.jobName || ("job " + block.jobId))), block.truncated
|
|
380
|
+
? h("div", { className: "dsh-github-run-log-truncated" }, "⚠️ 原始日志超出上限,仅显示已获取部分")
|
|
381
|
+
: null,
|
|
382
|
+
// <pre> 原文直出:日志里的 #、|、--- 不能进 markdown-lite
|
|
383
|
+
h("pre", { className: "dsh-github-run-log" }, block.excerpt || "(该 job 无日志输出)")));
|
|
384
|
+
});
|
|
385
|
+
logErrors.forEach(function (e, i) {
|
|
386
|
+
children.push(h("div", { key: "log-err-" + i, className: "dsh-github-run-error" }, h("div", { className: "dsh-github-run-error-title" }, "⚠️ " + (e.jobName || "job") + " 日志获取失败"), h("div", { className: "dsh-github-run-error-text" }, e.error || "未知错误")));
|
|
387
|
+
});
|
|
388
|
+
if (logData.fetchedAt) {
|
|
389
|
+
children.push(h("div", { key: "log-at", className: "dsh-github-run-log-hint" }, "日志获取于 " + formatRelativeTime(logData.fetchedAt)));
|
|
390
|
+
}
|
|
391
|
+
// 名称降级必须可见(服务端 details 降级时 jobName 会退化成 job #<id>)
|
|
392
|
+
if (logData.namesUnavailable) {
|
|
393
|
+
children.push(h("div", { key: "log-names", className: "dsh-github-run-log-hint" }, "⚠️ job 名称未取到(详情字段降级),以 job #编号 显示。"));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return h("div", { key: "logs", className: "dsh-github-run-log-section" }, ...children);
|
|
397
|
+
};
|
|
398
|
+
var headerTitle = data
|
|
399
|
+
? data.workflowName || data.name || "Actions Run"
|
|
400
|
+
: "Actions Run";
|
|
401
|
+
var ghHref = (data && data.url) || (repoNameWithOwner ? "https://github.com/" + repoNameWithOwner + "/actions/runs/" + runId : "");
|
|
402
|
+
var header = h("div", { className: "dsh-github-modal-header dsh-github-run-modal-header" }, h("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, flex: 1 } }, h("span", { style: { fontSize: "16px", lineHeight: 1, flexShrink: 0 }, "aria-hidden": "true" }, "⚡"), h("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 } }, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" } }, h("h3", {
|
|
403
|
+
style: {
|
|
404
|
+
margin: 0,
|
|
405
|
+
fontSize: "14px",
|
|
406
|
+
fontWeight: 600,
|
|
407
|
+
overflow: "hidden",
|
|
408
|
+
textOverflow: "ellipsis",
|
|
409
|
+
whiteSpace: "nowrap",
|
|
410
|
+
maxWidth: "520px",
|
|
411
|
+
},
|
|
412
|
+
title: headerTitle + " #" + runId,
|
|
413
|
+
}, headerTitle + " #" + runId), badgeEl(data ? data.conclusion : undefined, data ? data.status : undefined), data && Number(data.attempt) > 1
|
|
414
|
+
? h("span", { className: "dsh-github-badge dsh-badge-yellow", style: { fontSize: "10px", padding: "1px 6px", flexShrink: 0 } }, "第 " + data.attempt + " 次尝试")
|
|
415
|
+
: null), data && (data.displayTitle || repoNameWithOwner)
|
|
416
|
+
? h("div", {
|
|
417
|
+
style: {
|
|
418
|
+
fontSize: "11px",
|
|
419
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
420
|
+
display: "flex",
|
|
421
|
+
gap: "6px",
|
|
422
|
+
alignItems: "center",
|
|
423
|
+
flexWrap: "wrap",
|
|
424
|
+
minWidth: 0,
|
|
425
|
+
},
|
|
426
|
+
}, data.displayTitle
|
|
427
|
+
? h("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, title: data.displayTitle }, data.displayTitle)
|
|
428
|
+
: null, repoNameWithOwner
|
|
429
|
+
? h("span", { style: { fontFamily: "monospace", opacity: 0.85, flexShrink: 0 } }, repoNameWithOwner)
|
|
430
|
+
: null)
|
|
431
|
+
: null)), h("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 } }, ghHref
|
|
432
|
+
? h("a", {
|
|
433
|
+
href: ghHref,
|
|
434
|
+
target: "_blank",
|
|
435
|
+
rel: "noopener noreferrer",
|
|
436
|
+
className: "dsh-github-pr-gh-btn",
|
|
437
|
+
title: "在 GitHub 中打开该次运行",
|
|
438
|
+
}, "GitHub ↗")
|
|
439
|
+
: null, h("button", {
|
|
440
|
+
type: "button",
|
|
441
|
+
"aria-label": "关闭",
|
|
442
|
+
title: "关闭 (Esc)",
|
|
443
|
+
style: {
|
|
444
|
+
background: "none",
|
|
445
|
+
border: "none",
|
|
446
|
+
cursor: "pointer",
|
|
447
|
+
fontSize: "18px",
|
|
448
|
+
color: "var(--dsw-alias-label-secondary, #656d76)",
|
|
449
|
+
padding: "0 6px",
|
|
450
|
+
},
|
|
451
|
+
onClick: onClose,
|
|
452
|
+
}, "✕")));
|
|
453
|
+
var jobs = data && Array.isArray(data.jobs) ? data.jobs : [];
|
|
454
|
+
var bodyChildren = [];
|
|
455
|
+
if (data) {
|
|
456
|
+
if (jobs.length === 0) {
|
|
457
|
+
bodyChildren.push(h("div", { key: "empty", className: "dsh-github-run-empty" }, "暂无可读 job(可能仍在排队或已跳过)"));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
jobs.forEach(function (job, i) {
|
|
461
|
+
bodyChildren.push(renderJob(job, i));
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
var logSection = renderLogSection();
|
|
465
|
+
if (logSection)
|
|
466
|
+
bodyChildren.push(logSection);
|
|
467
|
+
}
|
|
468
|
+
var bodyNode = loading
|
|
469
|
+
? h("div", { style: { padding: "64px 0", textAlign: "center", fontSize: "13px", opacity: 0.7 } }, "⏳ 正在加载 Actions Run 详情…")
|
|
470
|
+
: error
|
|
471
|
+
? h("div", { className: "dsh-github-run-error" }, h("div", { className: "dsh-github-run-error-title" }, "⚠️ 读取 Actions Run 详情失败"),
|
|
472
|
+
// 服务端会把 gh 的原始报错透传上来,可能很长,交给 .dsh-github-run-error-text 滚动
|
|
473
|
+
h("div", { className: "dsh-github-run-error-text" }, error),
|
|
474
|
+
// 不给「重试」按钮:详情 effect 只依赖 [runId, repoNameWithOwner, cwd],
|
|
475
|
+
// 三者不变时重建同一份 effect 不会重新发起 fetch,按钮只会空转。
|
|
476
|
+
// 需要重试的用户可直接用头部的 GitHub 外链核对。
|
|
477
|
+
h("div", { className: "dsh-github-run-log-hint" }, "可关闭本弹窗后重新打开,或点击右上角「GitHub ↗」直接查看。"))
|
|
478
|
+
: bodyChildren;
|
|
479
|
+
return h("div", { className: "dsh-github-modal-overlay", onClick: onClose, style: { zIndex: 100000 } }, h("div", {
|
|
480
|
+
className: "dsh-github-modal-dialog dsh-github-run-modal-dialog",
|
|
481
|
+
role: "dialog",
|
|
482
|
+
"aria-modal": "true",
|
|
483
|
+
"aria-label": "Actions Run #" + runId + " 详情",
|
|
484
|
+
onClick: function (e) { e.stopPropagation(); },
|
|
485
|
+
}, header, data ? renderMeta(data) : null, h("div", { className: "dsh-github-run-body" }, bodyNode)));
|
|
486
|
+
}
|