@tea-agent/loop-agent 0.8.0 → 0.9.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/AGENTS.md +2 -0
- package/CHANGELOG.md +22 -3
- package/dist/application/dag/args.js +9 -2
- package/dist/executors/shell-executor.js +74 -8
- package/dist/shared/reference-context.js +48 -22
- package/dist/task/config-types.js +1 -1
- package/dist/task/runtime.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +51 -10
- package/dist/worker/observe/routes.js +78 -20
- package/dist/worker/observe/server.js +8 -6
- package/dist/worker/observe/static/app.js +628 -142
- package/dist/worker/observe/static/index.html +68 -43
- package/dist/worker/observe/static/styles.css +478 -602
- package/docs/README.md +4 -0
- package/docs/design/README.md +2 -2
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +8 -0
- package/docs/reports/README.md +3 -0
- package/package.json +1 -1
- package/scripts/check-task-pool-root.sh +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { layoutDag } from "./dag-layout.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const ACTIVE_DETAIL_POLL_MS = 2000;
|
|
4
|
+
const HIDDEN_POLL_MS = 30000;
|
|
4
5
|
const DASHBOARD_POLL_MS = 5000;
|
|
5
6
|
|
|
6
7
|
const UI_TEXT = {
|
|
@@ -132,6 +133,172 @@ let selectedDagNodeId = null;
|
|
|
132
133
|
let sessionEventOffset = 0;
|
|
133
134
|
let sessionEvents = [];
|
|
134
135
|
let lastSnapshot = null;
|
|
136
|
+
let dagInspectorOpen = false;
|
|
137
|
+
let dagInspectorTab = "output";
|
|
138
|
+
let dagGraphViewportState = null;
|
|
139
|
+
const dagGraphViewportStates = new Map();
|
|
140
|
+
let dagTimelineViewportState = null;
|
|
141
|
+
let pollingGeneration = 0;
|
|
142
|
+
|
|
143
|
+
function isPageVisible() {
|
|
144
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function detailPollDelay(status, isVisible) {
|
|
148
|
+
const normalizedStatus = (status ?? "").toLowerCase().replace(/-/g, "_");
|
|
149
|
+
if (TERMINAL_RUN_STATUSES.has(normalizedStatus)) return null;
|
|
150
|
+
return isVisible ? ACTIVE_DETAIL_POLL_MS : HIDDEN_POLL_MS;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function dashboardPollDelay() {
|
|
154
|
+
return isPageVisible() ? DASHBOARD_POLL_MS : HIDDEN_POLL_MS;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseMarkdownBlocks(markdown) {
|
|
158
|
+
const lines = String(markdown ?? "").replace(/\r\n?/g, "\n").split("\n");
|
|
159
|
+
const blocks = [];
|
|
160
|
+
let paragraph = [];
|
|
161
|
+
let list = null;
|
|
162
|
+
let code = null;
|
|
163
|
+
|
|
164
|
+
const flushParagraph = () => {
|
|
165
|
+
const text = paragraph.join(" ").trim();
|
|
166
|
+
if (text) blocks.push({ type: "paragraph", text });
|
|
167
|
+
paragraph = [];
|
|
168
|
+
};
|
|
169
|
+
const flushList = () => {
|
|
170
|
+
if (list?.items.length) blocks.push(list);
|
|
171
|
+
list = null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
175
|
+
const line = lines[index] ?? "";
|
|
176
|
+
if (code) {
|
|
177
|
+
if (/^```\s*$/.test(line)) {
|
|
178
|
+
blocks.push({ type: "code", language: code.language, text: code.lines.join("\n") });
|
|
179
|
+
code = null;
|
|
180
|
+
} else {
|
|
181
|
+
code.lines.push(line);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fence = line.match(/^```([^\s]*)\s*$/);
|
|
187
|
+
if (fence) {
|
|
188
|
+
flushParagraph();
|
|
189
|
+
flushList();
|
|
190
|
+
code = { language: fence[1] ?? "", lines: [] };
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!line.trim()) {
|
|
194
|
+
flushParagraph();
|
|
195
|
+
flushList();
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
|
199
|
+
if (heading) {
|
|
200
|
+
flushParagraph();
|
|
201
|
+
flushList();
|
|
202
|
+
blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
|
|
206
|
+
flushParagraph();
|
|
207
|
+
flushList();
|
|
208
|
+
blocks.push({ type: "rule" });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (line.startsWith(">")) {
|
|
212
|
+
flushParagraph();
|
|
213
|
+
flushList();
|
|
214
|
+
blocks.push({ type: "quote", text: line.replace(/^>\s?/, "") });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const unordered = line.match(/^[-*+]\s+(.+)$/);
|
|
218
|
+
const ordered = line.match(/^\d+[.)]\s+(.+)$/);
|
|
219
|
+
if (unordered || ordered) {
|
|
220
|
+
flushParagraph();
|
|
221
|
+
const orderedList = Boolean(ordered);
|
|
222
|
+
if (!list || list.ordered !== orderedList) {
|
|
223
|
+
flushList();
|
|
224
|
+
list = { type: "list", ordered: orderedList, items: [] };
|
|
225
|
+
}
|
|
226
|
+
list.items.push((ordered?.[1] ?? unordered?.[1] ?? "").trim());
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.includes("|") && /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1] ?? "")) {
|
|
230
|
+
flushParagraph();
|
|
231
|
+
flushList();
|
|
232
|
+
const parseCells = (row) => row.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
|
|
233
|
+
const headers = parseCells(line);
|
|
234
|
+
const rows = [];
|
|
235
|
+
index += 2;
|
|
236
|
+
while (index < lines.length && lines[index].includes("|")) {
|
|
237
|
+
rows.push(parseCells(lines[index]));
|
|
238
|
+
index += 1;
|
|
239
|
+
}
|
|
240
|
+
index -= 1;
|
|
241
|
+
blocks.push({ type: "table", headers, rows });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
flushList();
|
|
245
|
+
paragraph.push(line.trim());
|
|
246
|
+
}
|
|
247
|
+
if (code) blocks.push({ type: "code", language: code.language, text: code.lines.join("\n") });
|
|
248
|
+
flushParagraph();
|
|
249
|
+
flushList();
|
|
250
|
+
return blocks;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function markdownLinkHref(value) {
|
|
254
|
+
const href = typeof value === "string" ? value.trim() : "";
|
|
255
|
+
if (!href) return null;
|
|
256
|
+
if (href.startsWith("#") || href.startsWith("/")) return href;
|
|
257
|
+
try {
|
|
258
|
+
const parsed = new URL(
|
|
259
|
+
href,
|
|
260
|
+
typeof location === "undefined" ? "http://localhost" : location.href,
|
|
261
|
+
);
|
|
262
|
+
return ["http:", "https:", "mailto:"].includes(parsed.protocol) ? href : null;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function updateDagGraphViewportState(_previous, dagRunId, scrollLeft, scrollTop) {
|
|
269
|
+
return {
|
|
270
|
+
dagRunId,
|
|
271
|
+
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
272
|
+
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function captureDagGraphViewportState(previous, dagRunId, viewport) {
|
|
277
|
+
if (!viewport || viewport.dataset?.dagRunId !== dagRunId) return previous;
|
|
278
|
+
return updateDagGraphViewportState(
|
|
279
|
+
previous,
|
|
280
|
+
dagRunId,
|
|
281
|
+
viewport.scrollLeft,
|
|
282
|
+
viewport.scrollTop,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function updateDagTimelineViewportState(
|
|
287
|
+
_previous,
|
|
288
|
+
dagRunId,
|
|
289
|
+
nodeId,
|
|
290
|
+
scrollLeft,
|
|
291
|
+
scrollTop,
|
|
292
|
+
followLatest,
|
|
293
|
+
) {
|
|
294
|
+
return {
|
|
295
|
+
dagRunId,
|
|
296
|
+
nodeId,
|
|
297
|
+
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
298
|
+
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
299
|
+
followLatest: Boolean(followLatest),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
135
302
|
|
|
136
303
|
export function parseHashRoute(hashValue) {
|
|
137
304
|
const hash = hashValue.replace(/^#/, "") || "/";
|
|
@@ -268,13 +435,20 @@ function livenessBadge(liveness) {
|
|
|
268
435
|
}
|
|
269
436
|
|
|
270
437
|
function showView(name) {
|
|
438
|
+
document.body.dataset.view = name;
|
|
439
|
+
if (name !== "dag") closeDagInspector();
|
|
271
440
|
document.querySelectorAll("[data-view]").forEach((section) => {
|
|
272
441
|
section.hidden = section.dataset.view !== name;
|
|
273
442
|
});
|
|
443
|
+
const activeHref = name === "failures" ? "#/failures" : "#/";
|
|
444
|
+
document.querySelectorAll(".nav-link").forEach((link) => {
|
|
445
|
+
link.classList.toggle("nav-link-active", link.getAttribute("href") === activeHref);
|
|
446
|
+
});
|
|
274
447
|
}
|
|
275
448
|
|
|
276
449
|
function setBreadcrumb(parts) {
|
|
277
450
|
const nav = document.getElementById("breadcrumb");
|
|
451
|
+
nav.hidden = parts.length <= 1;
|
|
278
452
|
clearNode(nav);
|
|
279
453
|
parts.forEach((part, i) => {
|
|
280
454
|
if (i > 0) nav.appendChild(document.createTextNode(" / "));
|
|
@@ -304,16 +478,17 @@ function updateHeaderRefresh(generatedAt) {
|
|
|
304
478
|
}
|
|
305
479
|
|
|
306
480
|
function stopTimers() {
|
|
481
|
+
pollingGeneration += 1;
|
|
307
482
|
if (dashboardTimer) {
|
|
308
|
-
|
|
483
|
+
clearTimeout(dashboardTimer);
|
|
309
484
|
dashboardTimer = null;
|
|
310
485
|
}
|
|
311
486
|
if (runPollTimer) {
|
|
312
|
-
|
|
487
|
+
clearTimeout(runPollTimer);
|
|
313
488
|
runPollTimer = null;
|
|
314
489
|
}
|
|
315
490
|
if (dagPollTimer) {
|
|
316
|
-
|
|
491
|
+
clearTimeout(dagPollTimer);
|
|
317
492
|
dagPollTimer = null;
|
|
318
493
|
}
|
|
319
494
|
currentRunId = null;
|
|
@@ -427,26 +602,6 @@ function isNodeFailed(status) {
|
|
|
427
602
|
return s === "error" || s === "failed" || s === "partial_failed";
|
|
428
603
|
}
|
|
429
604
|
|
|
430
|
-
function pickDefaultDagNode(nodes) {
|
|
431
|
-
if (!nodes?.length) return null;
|
|
432
|
-
const sorted = [...nodes].sort((a, b) => {
|
|
433
|
-
const ra = Number(a.rank);
|
|
434
|
-
const rb = Number(b.rank);
|
|
435
|
-
if (!Number.isNaN(ra) && !Number.isNaN(rb) && ra !== rb) return ra - rb;
|
|
436
|
-
if (a.rank !== b.rank) return (a.rank ?? "").localeCompare(b.rank ?? "");
|
|
437
|
-
return a.nodeId.localeCompare(b.nodeId);
|
|
438
|
-
});
|
|
439
|
-
const failed = sorted.find((n) => isNodeFailed(n.status));
|
|
440
|
-
if (failed) return failed.nodeId;
|
|
441
|
-
const active = sorted.find((n) => isNodeActive(n.status));
|
|
442
|
-
if (active) return active.nodeId;
|
|
443
|
-
const withOutput = sorted.find(
|
|
444
|
-
(n) => (n.outputPreview || n.errorPreview) && isNodeFinished(n.status),
|
|
445
|
-
);
|
|
446
|
-
if (withOutput) return withOutput.nodeId;
|
|
447
|
-
return sorted[0]?.nodeId ?? null;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
605
|
function truncateText(text, maxLen) {
|
|
451
606
|
if (!text) return "—";
|
|
452
607
|
const normalized = String(text).replace(/\s+/g, " ").trim();
|
|
@@ -517,32 +672,50 @@ async function mergeSessionEvents(dagRunId, nodeId) {
|
|
|
517
672
|
}
|
|
518
673
|
|
|
519
674
|
function selectDagNode(dagRunId, nodeId) {
|
|
520
|
-
|
|
675
|
+
const nodeChanged = selectedDagNodeId !== nodeId;
|
|
521
676
|
selectedDagNodeId = nodeId;
|
|
522
|
-
|
|
523
|
-
|
|
677
|
+
dagInspectorOpen = true;
|
|
678
|
+
if (nodeChanged) {
|
|
679
|
+
sessionEventOffset = 0;
|
|
680
|
+
sessionEvents = [];
|
|
681
|
+
}
|
|
524
682
|
void renderDagDetail(dagRunId, false);
|
|
525
683
|
}
|
|
526
684
|
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
685
|
+
function closeDagInspector() {
|
|
686
|
+
dagInspectorOpen = false;
|
|
687
|
+
const panel = document.getElementById("dag-inspector");
|
|
688
|
+
if (panel) {
|
|
689
|
+
panel.classList.remove("is-open");
|
|
690
|
+
panel.setAttribute("aria-hidden", "true");
|
|
691
|
+
}
|
|
692
|
+
}
|
|
533
693
|
|
|
534
|
-
|
|
535
|
-
panel.
|
|
536
|
-
if (nodeId) {
|
|
537
|
-
|
|
694
|
+
function captureDagTimelineViewportState(panel) {
|
|
695
|
+
const scrollEl = panel.querySelector(".process-timeline-scroll");
|
|
696
|
+
if (scrollEl?.dataset.dagRunId && scrollEl.dataset.nodeId) {
|
|
697
|
+
const followLatest =
|
|
698
|
+
scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 48;
|
|
699
|
+
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
700
|
+
dagTimelineViewportState,
|
|
701
|
+
scrollEl.dataset.dagRunId,
|
|
702
|
+
scrollEl.dataset.nodeId,
|
|
703
|
+
scrollEl.scrollLeft,
|
|
704
|
+
scrollEl.scrollTop,
|
|
705
|
+
followLatest,
|
|
706
|
+
);
|
|
538
707
|
}
|
|
708
|
+
}
|
|
539
709
|
|
|
710
|
+
function renderSessionTimeline(content, nodeId) {
|
|
711
|
+
content.dataset.dagRunId = currentDagRunId ?? "";
|
|
712
|
+
content.dataset.nodeId = nodeId ?? "";
|
|
713
|
+
content.classList.add("process-timeline-scroll");
|
|
540
714
|
if (!nodeId || sessionEvents.length === 0) {
|
|
541
|
-
|
|
715
|
+
content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
|
|
542
716
|
return;
|
|
543
717
|
}
|
|
544
718
|
|
|
545
|
-
const container = el("div", "process-timeline-scroll");
|
|
546
719
|
const ul = el("ul", "process-timeline");
|
|
547
720
|
for (const event of sessionEvents) {
|
|
548
721
|
const li = el("li", "process-timeline-item");
|
|
@@ -550,21 +723,43 @@ function renderSessionTimeline(nodeId) {
|
|
|
550
723
|
li.appendChild(el("span", "process-timeline-label", formatSessionEventLabel(event)));
|
|
551
724
|
ul.appendChild(li);
|
|
552
725
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
726
|
+
content.appendChild(ul);
|
|
727
|
+
|
|
728
|
+
const timelineState =
|
|
729
|
+
dagTimelineViewportState?.dagRunId === currentDagRunId &&
|
|
730
|
+
dagTimelineViewportState.nodeId === nodeId
|
|
731
|
+
? dagTimelineViewportState
|
|
732
|
+
: null;
|
|
733
|
+
content.addEventListener(
|
|
734
|
+
"scroll",
|
|
735
|
+
() => {
|
|
736
|
+
const followLatest =
|
|
737
|
+
content.scrollHeight - content.scrollTop - content.clientHeight < 48;
|
|
738
|
+
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
739
|
+
dagTimelineViewportState,
|
|
740
|
+
currentDagRunId,
|
|
741
|
+
nodeId,
|
|
742
|
+
content.scrollLeft,
|
|
743
|
+
content.scrollTop,
|
|
744
|
+
followLatest,
|
|
745
|
+
);
|
|
746
|
+
},
|
|
747
|
+
{ passive: true },
|
|
748
|
+
);
|
|
749
|
+
if (timelineState?.followLatest ?? true) {
|
|
750
|
+
content.scrollLeft = timelineState?.scrollLeft ?? 0;
|
|
751
|
+
content.scrollTop = content.scrollHeight;
|
|
752
|
+
} else {
|
|
753
|
+
content.scrollLeft = timelineState.scrollLeft;
|
|
754
|
+
content.scrollTop = timelineState.scrollTop;
|
|
558
755
|
}
|
|
559
756
|
}
|
|
560
757
|
|
|
561
|
-
function isDagRunActive(dag) {
|
|
758
|
+
export function isDagRunActive(dag) {
|
|
562
759
|
const status = (dag.status ?? "").toLowerCase();
|
|
563
|
-
if (["running", "pending", "started"].includes(status)) return true;
|
|
564
|
-
const hasActiveNode = (dag.nodes ?? []).some((n) => isNodeActive(n.status));
|
|
565
|
-
if (hasActiveNode) return true;
|
|
566
760
|
if (TERMINAL_RUN_STATUSES.has(status)) return false;
|
|
567
|
-
return
|
|
761
|
+
if (["running", "pending", "started"].includes(status)) return true;
|
|
762
|
+
return (dag.nodes ?? []).some((n) => isNodeActive(n.status));
|
|
568
763
|
}
|
|
569
764
|
|
|
570
765
|
function dagProgress(dag) {
|
|
@@ -733,6 +928,7 @@ function renderDagGraph(dag, dagRunId) {
|
|
|
733
928
|
wrap.appendChild(toolbar);
|
|
734
929
|
if (edges.length === 0) wrap.appendChild(el("p", "dag-graph-fallback", "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。"));
|
|
735
930
|
const viewport = el("div", "dag-graph-viewport");
|
|
931
|
+
viewport.dataset.dagRunId = dagRunId;
|
|
736
932
|
const svg = svgEl("svg", { class: "dag-graph", viewBox: `0 0 ${layout.width} ${layout.height}`, width: layout.width, height: layout.height, role: "img", "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点` });
|
|
737
933
|
const defs = svgEl("defs");
|
|
738
934
|
const marker = svgEl("marker", { id: "dag-arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse" });
|
|
@@ -760,17 +956,226 @@ function renderDagGraph(dag, dagRunId) {
|
|
|
760
956
|
svg.appendChild(group);
|
|
761
957
|
}
|
|
762
958
|
viewport.appendChild(svg); wrap.appendChild(viewport);
|
|
763
|
-
|
|
959
|
+
const graphViewportState =
|
|
960
|
+
dagGraphViewportStates.get(dagRunId) ??
|
|
961
|
+
(dagGraphViewportState?.dagRunId === dagRunId
|
|
962
|
+
? dagGraphViewportState
|
|
963
|
+
: null);
|
|
964
|
+
viewport.addEventListener(
|
|
965
|
+
"scroll",
|
|
966
|
+
() => {
|
|
967
|
+
dagGraphViewportState = updateDagGraphViewportState(
|
|
968
|
+
dagGraphViewportState,
|
|
969
|
+
dagRunId,
|
|
970
|
+
viewport.scrollLeft,
|
|
971
|
+
viewport.scrollTop,
|
|
972
|
+
);
|
|
973
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
974
|
+
},
|
|
975
|
+
{ passive: true },
|
|
976
|
+
);
|
|
977
|
+
if (graphViewportState) {
|
|
978
|
+
const restoreViewport = () => {
|
|
979
|
+
viewport.scrollLeft = graphViewportState.scrollLeft;
|
|
980
|
+
viewport.scrollTop = graphViewportState.scrollTop;
|
|
981
|
+
};
|
|
982
|
+
if (typeof requestAnimationFrame === "function") {
|
|
983
|
+
requestAnimationFrame(restoreViewport);
|
|
984
|
+
} else {
|
|
985
|
+
restoreViewport();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
reset.addEventListener("click", () => {
|
|
989
|
+
dagGraphViewportState = updateDagGraphViewportState(
|
|
990
|
+
dagGraphViewportState,
|
|
991
|
+
dagRunId,
|
|
992
|
+
0,
|
|
993
|
+
0,
|
|
994
|
+
);
|
|
995
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
996
|
+
viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
|
|
997
|
+
});
|
|
764
998
|
return wrap;
|
|
765
999
|
}
|
|
766
1000
|
|
|
767
|
-
function
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1001
|
+
function appendInlineMarkdown(target, text) {
|
|
1002
|
+
const tokenPattern = /(`[^`]*`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g;
|
|
1003
|
+
let cursor = 0;
|
|
1004
|
+
for (const match of text.matchAll(tokenPattern)) {
|
|
1005
|
+
const index = match.index ?? 0;
|
|
1006
|
+
if (index > cursor) target.appendChild(document.createTextNode(text.slice(cursor, index)));
|
|
1007
|
+
const token = match[0];
|
|
1008
|
+
if (token.startsWith("`")) {
|
|
1009
|
+
target.appendChild(el("code", null, token.slice(1, -1)));
|
|
1010
|
+
} else if (token.startsWith("**")) {
|
|
1011
|
+
const strong = document.createElement("strong");
|
|
1012
|
+
strong.textContent = token.slice(2, -2);
|
|
1013
|
+
target.appendChild(strong);
|
|
1014
|
+
} else if (token.startsWith("*")) {
|
|
1015
|
+
const emphasis = document.createElement("em");
|
|
1016
|
+
emphasis.textContent = token.slice(1, -1);
|
|
1017
|
+
target.appendChild(emphasis);
|
|
1018
|
+
} else {
|
|
1019
|
+
const link = token.match(/^\[([^\]]+)\]\((.+)\)$/);
|
|
1020
|
+
const href = markdownLinkHref(link?.[2]);
|
|
1021
|
+
if (link && href) {
|
|
1022
|
+
const anchor = document.createElement("a");
|
|
1023
|
+
anchor.href = href;
|
|
1024
|
+
anchor.target = "_blank";
|
|
1025
|
+
anchor.rel = "noopener noreferrer";
|
|
1026
|
+
anchor.textContent = link[1];
|
|
1027
|
+
target.appendChild(anchor);
|
|
1028
|
+
} else {
|
|
1029
|
+
target.appendChild(document.createTextNode(token));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
cursor = index + token.length;
|
|
1033
|
+
}
|
|
1034
|
+
if (cursor < text.length) target.appendChild(document.createTextNode(text.slice(cursor)));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function renderMarkdown(content) {
|
|
1038
|
+
const article = el("article", "markdown-output");
|
|
1039
|
+
for (const block of parseMarkdownBlocks(content)) {
|
|
1040
|
+
switch (block.type) {
|
|
1041
|
+
case "heading": {
|
|
1042
|
+
const heading = document.createElement(`h${block.level}`);
|
|
1043
|
+
appendInlineMarkdown(heading, block.text);
|
|
1044
|
+
article.appendChild(heading);
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
case "paragraph": {
|
|
1048
|
+
const paragraph = document.createElement("p");
|
|
1049
|
+
appendInlineMarkdown(paragraph, block.text);
|
|
1050
|
+
article.appendChild(paragraph);
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
case "quote": {
|
|
1054
|
+
const quote = document.createElement("blockquote");
|
|
1055
|
+
appendInlineMarkdown(quote, block.text);
|
|
1056
|
+
article.appendChild(quote);
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
case "list": {
|
|
1060
|
+
const list = document.createElement(block.ordered ? "ol" : "ul");
|
|
1061
|
+
for (const item of block.items) {
|
|
1062
|
+
const listItem = document.createElement("li");
|
|
1063
|
+
appendInlineMarkdown(listItem, item);
|
|
1064
|
+
list.appendChild(listItem);
|
|
1065
|
+
}
|
|
1066
|
+
article.appendChild(list);
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
case "code": {
|
|
1070
|
+
const pre = document.createElement("pre");
|
|
1071
|
+
const code = document.createElement("code");
|
|
1072
|
+
if (block.language) code.dataset.language = block.language;
|
|
1073
|
+
code.textContent = block.text;
|
|
1074
|
+
pre.appendChild(code);
|
|
1075
|
+
article.appendChild(pre);
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
case "table": {
|
|
1079
|
+
const table = document.createElement("table");
|
|
1080
|
+
const head = document.createElement("thead");
|
|
1081
|
+
const headRow = document.createElement("tr");
|
|
1082
|
+
for (const value of block.headers) {
|
|
1083
|
+
const cell = document.createElement("th");
|
|
1084
|
+
appendInlineMarkdown(cell, value);
|
|
1085
|
+
headRow.appendChild(cell);
|
|
1086
|
+
}
|
|
1087
|
+
head.appendChild(headRow);
|
|
1088
|
+
table.appendChild(head);
|
|
1089
|
+
const body = document.createElement("tbody");
|
|
1090
|
+
for (const row of block.rows) {
|
|
1091
|
+
const tableRow = document.createElement("tr");
|
|
1092
|
+
for (const value of row) {
|
|
1093
|
+
const cell = document.createElement("td");
|
|
1094
|
+
appendInlineMarkdown(cell, value);
|
|
1095
|
+
tableRow.appendChild(cell);
|
|
1096
|
+
}
|
|
1097
|
+
body.appendChild(tableRow);
|
|
1098
|
+
}
|
|
1099
|
+
table.appendChild(body);
|
|
1100
|
+
article.appendChild(table);
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case "rule":
|
|
1104
|
+
article.appendChild(document.createElement("hr"));
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return article;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function renderDagInspector(dagRunId, node) {
|
|
1112
|
+
const panel = document.getElementById("dag-inspector");
|
|
1113
|
+
if (!panel) return;
|
|
1114
|
+
captureDagTimelineViewportState(panel);
|
|
1115
|
+
if (!dagInspectorOpen || !node) {
|
|
1116
|
+
panel.classList.remove("is-open");
|
|
1117
|
+
panel.setAttribute("aria-hidden", "true");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
clearNode(panel);
|
|
1122
|
+
panel.classList.add("is-open");
|
|
1123
|
+
panel.setAttribute("aria-hidden", "false");
|
|
1124
|
+
const header = el("div", "dag-inspector-header");
|
|
1125
|
+
const title = el("div", "dag-inspector-title");
|
|
1126
|
+
title.appendChild(el("h3", null, "节点检查器"));
|
|
1127
|
+
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
1128
|
+
header.appendChild(title);
|
|
1129
|
+
const controls = el("div", "dag-inspector-controls");
|
|
1130
|
+
controls.appendChild(badge(node.status ?? "unknown"));
|
|
1131
|
+
const close = document.createElement("button");
|
|
1132
|
+
close.type = "button";
|
|
1133
|
+
close.className = "dag-inspector-close";
|
|
1134
|
+
close.setAttribute("aria-label", "关闭节点检查器");
|
|
1135
|
+
const closeIcon = el("i", "ri-close-line");
|
|
1136
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
1137
|
+
close.appendChild(closeIcon);
|
|
1138
|
+
close.addEventListener("click", closeDagInspector);
|
|
1139
|
+
controls.appendChild(close);
|
|
1140
|
+
header.appendChild(controls);
|
|
1141
|
+
|
|
1142
|
+
const tabs = el("div", "dag-inspector-tabs");
|
|
1143
|
+
tabs.setAttribute("role", "tablist");
|
|
1144
|
+
for (const [tab, label, icon] of [["output", "节点输出", "ri-file-text-line"], ["timeline", "执行过程", "ri-route-line"]]) {
|
|
1145
|
+
const button = document.createElement("button");
|
|
1146
|
+
button.type = "button";
|
|
1147
|
+
button.className = "dag-inspector-tab";
|
|
1148
|
+
button.setAttribute("role", "tab");
|
|
1149
|
+
button.setAttribute("aria-selected", String(dagInspectorTab === tab));
|
|
1150
|
+
const tabIcon = el("i", icon);
|
|
1151
|
+
tabIcon.setAttribute("aria-hidden", "true");
|
|
1152
|
+
button.appendChild(tabIcon);
|
|
1153
|
+
button.appendChild(document.createTextNode(` ${label}`));
|
|
1154
|
+
button.addEventListener("click", () => {
|
|
1155
|
+
dagInspectorTab = tab;
|
|
1156
|
+
renderDagInspector(dagRunId, node);
|
|
1157
|
+
});
|
|
1158
|
+
tabs.appendChild(button);
|
|
1159
|
+
}
|
|
1160
|
+
header.appendChild(tabs);
|
|
1161
|
+
panel.appendChild(header);
|
|
1162
|
+
|
|
1163
|
+
const content = el("div", "dag-inspector-content");
|
|
1164
|
+
content.setAttribute("role", "tabpanel");
|
|
1165
|
+
if (dagInspectorTab === "timeline") {
|
|
1166
|
+
renderSessionTimeline(content, node.nodeId);
|
|
1167
|
+
} else if (!node.outputPreview && !node.errorPreview) {
|
|
1168
|
+
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1169
|
+
} else {
|
|
1170
|
+
if (node.outputPreview) content.appendChild(renderMarkdown(node.outputPreview));
|
|
1171
|
+
if (node.errorPreview) {
|
|
1172
|
+
const error = el("section", "markdown-error");
|
|
1173
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
1174
|
+
error.appendChild(renderMarkdown(node.errorPreview));
|
|
1175
|
+
content.appendChild(error);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
panel.appendChild(content);
|
|
774
1179
|
}
|
|
775
1180
|
|
|
776
1181
|
async function renderFailures() {
|
|
@@ -838,6 +1243,7 @@ async function renderDashboard(scrollTo) {
|
|
|
838
1243
|
|
|
839
1244
|
const kpiEl = document.getElementById("dashboard-kpi");
|
|
840
1245
|
const activeEl = document.getElementById("dashboard-active-dags");
|
|
1246
|
+
const riskEl = document.getElementById("dashboard-risk");
|
|
841
1247
|
const dagsEl = document.getElementById("dashboard-dags");
|
|
842
1248
|
const batchesEl = document.getElementById("dashboard-batches");
|
|
843
1249
|
|
|
@@ -855,13 +1261,19 @@ async function renderDashboard(scrollTo) {
|
|
|
855
1261
|
.slice(0, 10);
|
|
856
1262
|
|
|
857
1263
|
clearNode(kpiEl);
|
|
1264
|
+
const quietCount = snapshot.health?.quietCount ?? 0;
|
|
1265
|
+
const staleCount = snapshot.health?.staleCount ?? 0;
|
|
1266
|
+
const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
|
|
1267
|
+
const failuresCount = snapshot.health?.failuresCount ?? 0;
|
|
858
1268
|
kpiEl.appendChild(kpiCard("活跃 DAG", activeDags.length, "kpi-highlight"));
|
|
859
1269
|
kpiEl.appendChild(kpiCard("活跃 Task", snapshot.health?.activeTasks ?? 0));
|
|
860
|
-
kpiEl.appendChild(kpiCard("无输出",
|
|
861
|
-
kpiEl.appendChild(kpiCard("心跳失联",
|
|
862
|
-
kpiEl.appendChild(
|
|
1270
|
+
kpiEl.appendChild(kpiCard("无输出", quietCount, quietCount > 0 ? "kpi-muted" : ""));
|
|
1271
|
+
kpiEl.appendChild(kpiCard("心跳失联", staleCount, staleCount > 0 ? "kpi-risk" : ""));
|
|
1272
|
+
kpiEl.appendChild(
|
|
1273
|
+
kpiCard("即将超时", timeoutRiskCount, timeoutRiskCount > 0 ? "kpi-risk" : ""),
|
|
1274
|
+
);
|
|
863
1275
|
kpiEl.appendChild(
|
|
864
|
-
kpiCard("失败",
|
|
1276
|
+
kpiCard("失败", failuresCount, failuresCount > 0 ? "kpi-danger" : ""),
|
|
865
1277
|
);
|
|
866
1278
|
|
|
867
1279
|
const nowMs = Date.now();
|
|
@@ -880,6 +1292,30 @@ async function renderDashboard(scrollTo) {
|
|
|
880
1292
|
activeEl.appendChild(list);
|
|
881
1293
|
}
|
|
882
1294
|
|
|
1295
|
+
const riskCount = failuresCount + staleCount + timeoutRiskCount;
|
|
1296
|
+
clearNode(riskEl);
|
|
1297
|
+
riskEl.classList.toggle("panel-risk-attention", riskCount > 0);
|
|
1298
|
+
riskEl.appendChild(el("h3", "section-heading", "运行风险"));
|
|
1299
|
+
if (riskCount === 0) {
|
|
1300
|
+
riskEl.appendChild(el("p", "risk-empty", "当前没有需要处理的异常。"));
|
|
1301
|
+
} else {
|
|
1302
|
+
riskEl.appendChild(el("div", "risk-total", String(riskCount)));
|
|
1303
|
+
riskEl.appendChild(
|
|
1304
|
+
el(
|
|
1305
|
+
"p",
|
|
1306
|
+
"risk-summary",
|
|
1307
|
+
`失败 ${failuresCount} · 心跳失联 ${staleCount} · 即将超时 ${timeoutRiskCount}`,
|
|
1308
|
+
),
|
|
1309
|
+
);
|
|
1310
|
+
const link = el("a", "risk-link", "查看异常 Task →");
|
|
1311
|
+
link.href = "#/failures";
|
|
1312
|
+
link.addEventListener("click", (event) => {
|
|
1313
|
+
event.preventDefault();
|
|
1314
|
+
navigate("/failures");
|
|
1315
|
+
});
|
|
1316
|
+
riskEl.appendChild(link);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
883
1319
|
clearNode(dagsEl);
|
|
884
1320
|
dagsEl.id = "dashboard-dags";
|
|
885
1321
|
dagsEl.appendChild(el("h3", "section-heading", UI_TEXT.recentDags));
|
|
@@ -887,24 +1323,45 @@ async function renderDashboard(scrollTo) {
|
|
|
887
1323
|
dagsEl.appendChild(el("p", "empty", UI_TEXT.noRecentDags));
|
|
888
1324
|
} else if (recentDags.length === 0) {
|
|
889
1325
|
dagsEl.appendChild(el("p", "empty", "暂无历史 DAG 记录"));
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1326
|
+
} else {
|
|
1327
|
+
const timeline = el("div", "dag-run-timeline");
|
|
1328
|
+
for (const dag of recentDags) {
|
|
1329
|
+
const progress = dagProgress(dag);
|
|
1330
|
+
const item = el("a", "dag-run-timeline-item");
|
|
1331
|
+
item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
|
|
1332
|
+
item.title = dag.title || dag.dagRunId;
|
|
1333
|
+
item.addEventListener("click", (event) => {
|
|
1334
|
+
event.preventDefault();
|
|
1335
|
+
navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
const rail = el("span", "dag-run-timeline-rail");
|
|
1339
|
+
rail.appendChild(
|
|
1340
|
+
el(
|
|
1341
|
+
"span",
|
|
1342
|
+
`dag-run-timeline-dot status-${String(dag.status ?? "unknown").toLowerCase()}`,
|
|
1343
|
+
),
|
|
1344
|
+
);
|
|
1345
|
+
item.appendChild(rail);
|
|
1346
|
+
|
|
1347
|
+
const content = el("span", "dag-run-timeline-content");
|
|
1348
|
+
content.appendChild(el("span", "dag-run-timeline-title", dag.title || dag.dagRunId));
|
|
1349
|
+
content.appendChild(
|
|
1350
|
+
el(
|
|
1351
|
+
"span",
|
|
1352
|
+
"dag-run-timeline-meta",
|
|
1353
|
+
`${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`,
|
|
1354
|
+
),
|
|
1355
|
+
);
|
|
1356
|
+
item.appendChild(content);
|
|
1357
|
+
|
|
1358
|
+
const state = el("span", "dag-run-timeline-status");
|
|
1359
|
+
state.appendChild(badge(dag.status));
|
|
1360
|
+
item.appendChild(state);
|
|
1361
|
+
timeline.appendChild(item);
|
|
1362
|
+
}
|
|
1363
|
+
dagsEl.appendChild(timeline);
|
|
1364
|
+
}
|
|
908
1365
|
|
|
909
1366
|
clearNode(batchesEl);
|
|
910
1367
|
batchesEl.appendChild(el("h3", "section-heading", UI_TEXT.batches));
|
|
@@ -927,15 +1384,6 @@ async function renderDashboard(scrollTo) {
|
|
|
927
1384
|
);
|
|
928
1385
|
}
|
|
929
1386
|
|
|
930
|
-
const failuresFooter = el("p", "dashboard-footer");
|
|
931
|
-
failuresFooter.appendChild(document.createTextNode("失败/失联合计:"));
|
|
932
|
-
failuresFooter.appendChild(
|
|
933
|
-
failuresInboxLink(
|
|
934
|
-
(snapshot.health?.failuresCount ?? 0) + (snapshot.health?.staleCount ?? 0),
|
|
935
|
-
),
|
|
936
|
-
);
|
|
937
|
-
batchesEl.appendChild(failuresFooter);
|
|
938
|
-
|
|
939
1387
|
if (scrollTo === "dags") {
|
|
940
1388
|
dagsEl.scrollIntoView({ behavior: "smooth" });
|
|
941
1389
|
}
|
|
@@ -1287,7 +1735,7 @@ async function renderRun(workerRunId, initial = true) {
|
|
|
1287
1735
|
.getElementById("run-meta")
|
|
1288
1736
|
.appendChild(el("p", "empty", "找不到该运行。"));
|
|
1289
1737
|
}
|
|
1290
|
-
return;
|
|
1738
|
+
return false;
|
|
1291
1739
|
}
|
|
1292
1740
|
|
|
1293
1741
|
if (snapshot) {
|
|
@@ -1298,13 +1746,22 @@ async function renderRun(workerRunId, initial = true) {
|
|
|
1298
1746
|
currentBatchRunId = task.batchRunId ?? currentBatchRunId;
|
|
1299
1747
|
await mergeRunEvents(workerRunId);
|
|
1300
1748
|
renderRunPanels(task, runEvents, snapshot ?? lastSnapshot);
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
runPollTimer
|
|
1306
|
-
|
|
1307
|
-
|
|
1749
|
+
return !TERMINAL_RUN_STATUSES.has((task.status ?? "").toLowerCase());
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function startRunPolling(workerRunId, initial = true) {
|
|
1753
|
+
if (runPollTimer) clearTimeout(runPollTimer);
|
|
1754
|
+
const generation = ++pollingGeneration;
|
|
1755
|
+
const poll = async (isInitial) => {
|
|
1756
|
+
const active = await renderRun(workerRunId, isInitial);
|
|
1757
|
+
if (generation !== pollingGeneration || !active) return;
|
|
1758
|
+
const delay = detailPollDelay("running", isPageVisible());
|
|
1759
|
+
runPollTimer = setTimeout(() => {
|
|
1760
|
+
runPollTimer = null;
|
|
1761
|
+
void poll(false);
|
|
1762
|
+
}, delay);
|
|
1763
|
+
};
|
|
1764
|
+
void poll(initial);
|
|
1308
1765
|
}
|
|
1309
1766
|
|
|
1310
1767
|
async function renderDagDetail(dagRunId, initial = true) {
|
|
@@ -1319,24 +1776,36 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1319
1776
|
selectedDagNodeId = null;
|
|
1320
1777
|
sessionEventOffset = 0;
|
|
1321
1778
|
sessionEvents = [];
|
|
1779
|
+
dagInspectorOpen = false;
|
|
1780
|
+
dagInspectorTab = "output";
|
|
1781
|
+
dagGraphViewportState = null;
|
|
1782
|
+
dagTimelineViewportState = null;
|
|
1322
1783
|
}
|
|
1323
1784
|
|
|
1324
1785
|
const metaEl = document.getElementById("dag-meta");
|
|
1325
1786
|
const ranksEl = document.getElementById("dag-ranks");
|
|
1326
1787
|
const nodesEl = document.getElementById("dag-nodes");
|
|
1327
|
-
|
|
1788
|
+
dagGraphViewportState = captureDagGraphViewportState(
|
|
1789
|
+
dagGraphViewportState,
|
|
1790
|
+
dagRunId,
|
|
1791
|
+
ranksEl.querySelector(".dag-graph-viewport"),
|
|
1792
|
+
);
|
|
1793
|
+
if (dagGraphViewportState?.dagRunId === dagRunId) {
|
|
1794
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
1795
|
+
}
|
|
1328
1796
|
|
|
1329
1797
|
const dag = await fetchJson(`/api/dag-runs/${encodeURIComponent(dagRunId)}`);
|
|
1330
1798
|
if (!dag) {
|
|
1331
1799
|
clearNode(metaEl);
|
|
1332
1800
|
metaEl.appendChild(el("p", "empty", "找不到该 DAG 运行。"));
|
|
1333
|
-
|
|
1334
|
-
return;
|
|
1801
|
+
closeDagInspector();
|
|
1802
|
+
return false;
|
|
1335
1803
|
}
|
|
1336
1804
|
|
|
1337
1805
|
const nodes = dag.nodes ?? [];
|
|
1338
|
-
if (
|
|
1339
|
-
selectedDagNodeId =
|
|
1806
|
+
if (selectedDagNodeId && !nodes.some((n) => n.nodeId === selectedDagNodeId)) {
|
|
1807
|
+
selectedDagNodeId = null;
|
|
1808
|
+
dagInspectorOpen = false;
|
|
1340
1809
|
if (initial) {
|
|
1341
1810
|
sessionEventOffset = 0;
|
|
1342
1811
|
sessionEvents = [];
|
|
@@ -1375,7 +1844,10 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1375
1844
|
}
|
|
1376
1845
|
|
|
1377
1846
|
clearNode(nodesEl);
|
|
1378
|
-
|
|
1847
|
+
const nodeHeading = el("div", "dag-section-heading");
|
|
1848
|
+
nodeHeading.appendChild(el("h3", null, "节点"));
|
|
1849
|
+
nodeHeading.appendChild(el("span", "dag-section-meta", `共 ${nodes.length} 个节点`));
|
|
1850
|
+
nodesEl.appendChild(nodeHeading);
|
|
1379
1851
|
|
|
1380
1852
|
if (nodes.length === 0) {
|
|
1381
1853
|
nodesEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
@@ -1411,62 +1883,69 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1411
1883
|
],
|
|
1412
1884
|
};
|
|
1413
1885
|
});
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
rows,
|
|
1418
|
-
),
|
|
1886
|
+
const nodeTable = buildTable(
|
|
1887
|
+
["节点 ID", "依赖层", "执行方式", "模型", "状态", "耗时", "备注"],
|
|
1888
|
+
rows,
|
|
1419
1889
|
);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
clearNode(outputEl);
|
|
1426
|
-
outputEl.appendChild(el("h3", null, "节点输出"));
|
|
1427
|
-
const withOutput = nodes.filter((n) => n.outputPreview || n.errorPreview);
|
|
1428
|
-
if (withOutput.length === 0) {
|
|
1429
|
-
outputEl.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1430
|
-
} else {
|
|
1431
|
-
for (const node of withOutput) {
|
|
1432
|
-
if (node.outputPreview) {
|
|
1433
|
-
outputEl.appendChild(
|
|
1434
|
-
expandablePreview(`${node.nodeId} · 输出`, node.outputPreview),
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
if (node.errorPreview) {
|
|
1438
|
-
outputEl.appendChild(
|
|
1439
|
-
expandablePreview(
|
|
1440
|
-
`${node.nodeId} · 错误`,
|
|
1441
|
-
node.errorPreview,
|
|
1442
|
-
"output-preview error-preview",
|
|
1443
|
-
),
|
|
1444
|
-
);
|
|
1890
|
+
nodeTable.classList.add("dag-node-table");
|
|
1891
|
+
for (const row of nodeTable.tBodies[0]?.rows ?? []) {
|
|
1892
|
+
for (const columnIndex of [0, 3, 6]) {
|
|
1893
|
+
const cell = row.cells[columnIndex];
|
|
1894
|
+
if (cell?.textContent) cell.title = cell.textContent;
|
|
1445
1895
|
}
|
|
1446
1896
|
}
|
|
1897
|
+
nodesEl.appendChild(nodeTable);
|
|
1447
1898
|
}
|
|
1448
|
-
}
|
|
1449
1899
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1900
|
+
if (dagInspectorOpen && selectedDagNodeId) {
|
|
1901
|
+
await mergeSessionEvents(dagRunId, selectedDagNodeId);
|
|
1902
|
+
}
|
|
1903
|
+
renderDagInspector(
|
|
1904
|
+
dagRunId,
|
|
1905
|
+
nodes.find((node) => node.nodeId === selectedDagNodeId),
|
|
1906
|
+
);
|
|
1907
|
+
return isDagRunActive(dag);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function startDagPolling(dagRunId, initial = true) {
|
|
1911
|
+
if (dagPollTimer) clearTimeout(dagPollTimer);
|
|
1912
|
+
const generation = ++pollingGeneration;
|
|
1913
|
+
const poll = async (isInitial) => {
|
|
1914
|
+
const active = await renderDagDetail(dagRunId, isInitial);
|
|
1915
|
+
if (generation !== pollingGeneration || !active) return;
|
|
1916
|
+
const delay = detailPollDelay("running", isPageVisible());
|
|
1917
|
+
dagPollTimer = setTimeout(() => {
|
|
1918
|
+
dagPollTimer = null;
|
|
1919
|
+
void poll(false);
|
|
1920
|
+
}, delay);
|
|
1921
|
+
};
|
|
1922
|
+
void poll(initial);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function startDashboardPolling(scrollTo) {
|
|
1926
|
+
if (dashboardTimer) clearTimeout(dashboardTimer);
|
|
1927
|
+
const generation = ++pollingGeneration;
|
|
1928
|
+
const poll = async (initialScrollTo) => {
|
|
1929
|
+
await renderDashboard(initialScrollTo);
|
|
1930
|
+
if (generation !== pollingGeneration) return;
|
|
1931
|
+
dashboardTimer = setTimeout(() => {
|
|
1932
|
+
dashboardTimer = null;
|
|
1933
|
+
void poll();
|
|
1934
|
+
}, dashboardPollDelay());
|
|
1935
|
+
};
|
|
1936
|
+
void poll(scrollTo);
|
|
1455
1937
|
}
|
|
1456
1938
|
|
|
1457
1939
|
function route() {
|
|
1458
1940
|
stopTimers();
|
|
1459
1941
|
const r = parseRoute();
|
|
1460
1942
|
if (r.view === "dashboard") {
|
|
1461
|
-
|
|
1462
|
-
dashboardTimer = setInterval(() => void renderDashboard(), DASHBOARD_POLL_MS);
|
|
1943
|
+
startDashboardPolling(r.scrollTo);
|
|
1463
1944
|
} else if (r.view === "batch") {
|
|
1464
1945
|
void renderBatch(r.batchRunId);
|
|
1465
1946
|
} else if (r.view === "run") {
|
|
1466
|
-
void renderRun(r.workerRunId, true);
|
|
1467
1947
|
startRunPolling(r.workerRunId);
|
|
1468
1948
|
} else if (r.view === "dag") {
|
|
1469
|
-
void renderDagDetail(r.dagRunId, true);
|
|
1470
1949
|
startDagPolling(r.dagRunId);
|
|
1471
1950
|
} else if (r.view === "failures") {
|
|
1472
1951
|
void renderFailures();
|
|
@@ -1476,4 +1955,11 @@ function route() {
|
|
|
1476
1955
|
if (typeof window !== "undefined") {
|
|
1477
1956
|
window.addEventListener("hashchange", route);
|
|
1478
1957
|
window.addEventListener("load", route);
|
|
1958
|
+
document.addEventListener("visibilitychange", () => {
|
|
1959
|
+
if (!isPageVisible()) return;
|
|
1960
|
+
const r = parseRoute();
|
|
1961
|
+
if (r.view === "dashboard") startDashboardPolling();
|
|
1962
|
+
if (r.view === "run") startRunPolling(r.workerRunId, false);
|
|
1963
|
+
if (r.view === "dag") startDagPolling(r.dagRunId, false);
|
|
1964
|
+
});
|
|
1479
1965
|
}
|