@tea-agent/loop-agent 0.42.0 → 0.43.0-next.2
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/CHANGELOG.md +34 -7
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +762 -252
- package/dist/executors/pi-executor.js +20 -1
- package/dist/executors/pi-sdk-executor.js +64 -1
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +22 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +260 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +746 -265
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +48 -10
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-scopes.js +117 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +16 -10
- package/dist/workflows/dag/node-execution.js +32 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/package.json +1 -1
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +2 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createInspectorSubmission, sendInspectorSubmission, hasUnresolvedSubmission } from "../inspector-submission.js";
|
|
1
2
|
/** DAG node inspector panel (Observe UI R5). */
|
|
2
3
|
import {
|
|
3
4
|
DAG_INSPECTOR_DEFAULT_WIDTH,
|
|
@@ -9,8 +10,10 @@ import {
|
|
|
9
10
|
DAG_INSPECTOR_KEYBOARD_STEP,
|
|
10
11
|
UI_TEXT,
|
|
11
12
|
} from "../constants.js";
|
|
12
|
-
import { clearNode, el } from "../dom.js";
|
|
13
|
+
import { clearNode, el, iconButton, setIconButton } from "../dom.js";
|
|
13
14
|
import { attemptFailureLabel } from "../dag-history-labels.js";
|
|
15
|
+
import { isNodeFailed } from "../dag-helpers.js";
|
|
16
|
+
import { isDagRunActive } from "../dag-model.js";
|
|
14
17
|
import {
|
|
15
18
|
contextTransferReasonLabel,
|
|
16
19
|
describeTransferError,
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
} from "../dag-inspector-humanize.js";
|
|
25
28
|
import {
|
|
26
29
|
fetchJson,
|
|
30
|
+
fetchJsonResult,
|
|
27
31
|
fetchSpecEvidenceFile,
|
|
28
32
|
fetchDagNodeInput,
|
|
29
33
|
fetchDagNodeContext,
|
|
@@ -44,9 +48,11 @@ import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
|
|
|
44
48
|
import { describeDagNodePurpose } from "../dag-node-purpose.js";
|
|
45
49
|
import { buildPromptRestartCandidateOptions } from "../prompt-restart-candidates.js";
|
|
46
50
|
import { copyText } from "../copy.js";
|
|
47
|
-
import { navigate } from "../router.js";
|
|
51
|
+
import { navigate, inspectPathToCanonicalHref } from "../router.js";
|
|
48
52
|
import {
|
|
49
53
|
fetchWithWorkspaceScope,
|
|
54
|
+
scopedApiUrl,
|
|
55
|
+
workspaceScope,
|
|
50
56
|
uiState,
|
|
51
57
|
makeSessionEventIdentity,
|
|
52
58
|
makeDagNodeInputIdentity,
|
|
@@ -64,12 +70,15 @@ import {
|
|
|
64
70
|
inspectorViewportRegionKey,
|
|
65
71
|
saveDagInspectorViewportState,
|
|
66
72
|
getDagInspectorViewportState,
|
|
73
|
+
inspectorReadingState,
|
|
74
|
+
inspectorNodeSelections,
|
|
75
|
+
inspectorDrafts,
|
|
76
|
+
inspectorOperations,
|
|
67
77
|
} from "../state.js";
|
|
68
78
|
import {
|
|
69
79
|
formatSessionEventTime,
|
|
70
80
|
renderSessionTimeline,
|
|
71
|
-
|
|
72
|
-
fetchPageStylesheetText,
|
|
81
|
+
createTimelineAutoScrollControl,
|
|
73
82
|
} from "./session-timeline.js";
|
|
74
83
|
import { createCustomSelect } from "../custom-select.js";
|
|
75
84
|
|
|
@@ -154,16 +163,38 @@ const INSPECTOR_PAGE_GUIDANCE = Object.freeze({
|
|
|
154
163
|
},
|
|
155
164
|
});
|
|
156
165
|
|
|
166
|
+
/** 会话级收起记忆:同一 Tab 收起后,本会话内的重绘保持折叠(可再展开)。 */
|
|
167
|
+
const COLLAPSED_INSPECTOR_LEADS = new Set();
|
|
168
|
+
|
|
157
169
|
function buildInspectorPageLead(tab) {
|
|
158
170
|
const copy = INSPECTOR_PAGE_GUIDANCE[tab];
|
|
159
171
|
if (!copy) return null;
|
|
160
172
|
const lead = el("section", "inspector-page-lead");
|
|
161
173
|
lead.dataset.page = tab;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
const collapsed = COLLAPSED_INSPECTOR_LEADS.has(tab);
|
|
175
|
+
if (collapsed) lead.classList.add("is-collapsed");
|
|
176
|
+
const toggle = iconButton(
|
|
177
|
+
collapsed ? "展开说明" : "收起说明",
|
|
178
|
+
collapsed ? "ri-arrow-down-s-line" : "ri-arrow-up-s-line",
|
|
179
|
+
"inspector-lead-toggle",
|
|
166
180
|
);
|
|
181
|
+
toggle.setAttribute("aria-expanded", String(!collapsed));
|
|
182
|
+
toggle.addEventListener("click", () => {
|
|
183
|
+
const nextCollapsed = !lead.classList.contains("is-collapsed");
|
|
184
|
+
if (nextCollapsed) COLLAPSED_INSPECTOR_LEADS.add(tab);
|
|
185
|
+
else COLLAPSED_INSPECTOR_LEADS.delete(tab);
|
|
186
|
+
lead.classList.toggle("is-collapsed", nextCollapsed);
|
|
187
|
+
setIconButton(
|
|
188
|
+
toggle,
|
|
189
|
+
nextCollapsed ? "展开说明" : "收起说明",
|
|
190
|
+
nextCollapsed ? "ri-arrow-down-s-line" : "ri-arrow-up-s-line",
|
|
191
|
+
);
|
|
192
|
+
toggle.setAttribute("aria-expanded", String(!nextCollapsed));
|
|
193
|
+
});
|
|
194
|
+
const eyebrow = el("span", "inspector-page-eyebrow", copy.eyebrow);
|
|
195
|
+
const eyebrowRow = el("div", "inspector-page-eyebrow-row");
|
|
196
|
+
eyebrowRow.append(eyebrow, toggle);
|
|
197
|
+
lead.append(eyebrowRow, el("h4", null, copy.title), el("p", null, copy.body));
|
|
167
198
|
return lead;
|
|
168
199
|
}
|
|
169
200
|
|
|
@@ -494,7 +525,7 @@ function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
|
494
525
|
uiState.dagTimelineViewportState = null;
|
|
495
526
|
// Reset the per-attempt selection so an old node's attempt cannot leak
|
|
496
527
|
// into the newly selected node (always defaults back to the latest).
|
|
497
|
-
uiState.dagInspectorAttempt = null;
|
|
528
|
+
uiState.dagInspectorAttempt = inspectorNodeSelections.get(`${dagRunId}:${nodeId}`) ?? null;
|
|
498
529
|
uiState.sessionEventOffset = 0;
|
|
499
530
|
uiState.sessionEvents = [];
|
|
500
531
|
// Drop any open spec-evidence preview so a stale A detail cannot survive
|
|
@@ -1044,9 +1075,11 @@ function sourceLabel(source) {
|
|
|
1044
1075
|
|
|
1045
1076
|
function appendMetaRow(dl, label, value, source) {
|
|
1046
1077
|
if (value == null || value === "") return;
|
|
1047
|
-
const
|
|
1078
|
+
const isElement = value && typeof value === "object" && value.nodeType != null;
|
|
1079
|
+
const copy = describeInspectorMeta(label, isElement ? "" : value);
|
|
1048
1080
|
const dt = el("dt", null, copy.label);
|
|
1049
1081
|
const dd = el("dd", null, copy.value);
|
|
1082
|
+
if (isElement) dd.appendChild(value);
|
|
1050
1083
|
const labelHelp = [
|
|
1051
1084
|
copy.help ? `含义:${copy.help}` : null,
|
|
1052
1085
|
copy.rawLabel ? `技术字段:${copy.rawLabel}` : null,
|
|
@@ -1564,6 +1597,9 @@ async function renderFrontendGateLedger(content, dagRunId) {
|
|
|
1564
1597
|
* executes it as a Console operation, and on success we jump to the new run.
|
|
1565
1598
|
*/
|
|
1566
1599
|
function buildContextExportSection(dagRunId, node) {
|
|
1600
|
+
const formScope = workspaceScope();
|
|
1601
|
+
let exportAttempt = null;
|
|
1602
|
+
let exportSourceLoading = true;
|
|
1567
1603
|
const zone = el("section", "dag-action-export-zone");
|
|
1568
1604
|
zone.dataset.actionSection = "context-export";
|
|
1569
1605
|
const divider = el("div", "dag-action-export-divider");
|
|
@@ -1682,7 +1718,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1682
1718
|
const protectionValue = appendTicketRow("保护", "自动隐藏匹配到的疑似凭据");
|
|
1683
1719
|
const attemptValue = appendTicketRow(
|
|
1684
1720
|
"来源",
|
|
1685
|
-
`${node.nodeId} ·
|
|
1721
|
+
`${node.nodeId} · ${exportAttempt ? `第 ${exportAttempt} 次尝试` : exportSourceLoading ? "正在确认最新尝试…" : "暂无可用上下文"}`,
|
|
1686
1722
|
"is-mono",
|
|
1687
1723
|
);
|
|
1688
1724
|
ticket.append(
|
|
@@ -1710,7 +1746,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1710
1746
|
recipientValue.textContent = target.description;
|
|
1711
1747
|
contentsValue.textContent = target.contents;
|
|
1712
1748
|
protectionValue.textContent = profile.description;
|
|
1713
|
-
attemptValue.textContent = `${node.nodeId} ·
|
|
1749
|
+
attemptValue.textContent = `${node.nodeId} · ${exportAttempt ? `第 ${exportAttempt} 次尝试` : exportSourceLoading ? "正在确认最新尝试…" : "暂无可用上下文"}`;
|
|
1714
1750
|
const safety = ticket.querySelector(".context-export-ticket-safety");
|
|
1715
1751
|
if (safety) {
|
|
1716
1752
|
safety.textContent = profileValue === "raw" ? "原始内容" : "已脱敏";
|
|
@@ -1730,6 +1766,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1730
1766
|
submit.type = "button";
|
|
1731
1767
|
submit.className = "prompt-restart-submit context-export-submit";
|
|
1732
1768
|
submit.dataset.testid = "context-export-submit";
|
|
1769
|
+
submit.disabled = true;
|
|
1733
1770
|
const submitIcon = el("i", "ri-download-cloud-2-line");
|
|
1734
1771
|
submitIcon.setAttribute("aria-hidden", "true");
|
|
1735
1772
|
submit.append(submitIcon, document.createTextNode(" 生成交接包并下载"));
|
|
@@ -1743,8 +1780,35 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1743
1780
|
status.textContent = message;
|
|
1744
1781
|
status.classList.toggle("prompt-restart-error", error);
|
|
1745
1782
|
};
|
|
1783
|
+
const refreshSource = iconButton("刷新导出来源", "ri-refresh-line", "context-export-refresh-source");
|
|
1784
|
+
refreshSource.type = "button";
|
|
1785
|
+
ticketTop.appendChild(refreshSource);
|
|
1786
|
+
const loadExportSource = async () => {
|
|
1787
|
+
exportAttempt = null;
|
|
1788
|
+
exportSourceLoading = true;
|
|
1789
|
+
submit.disabled = true;
|
|
1790
|
+
refreshSource.disabled = true;
|
|
1791
|
+
updatePreview();
|
|
1792
|
+
setStatus("正在检查最新尝试的上下文…");
|
|
1793
|
+
const result = await fetchDagNodeContext(dagRunId, node.nodeId);
|
|
1794
|
+
if (!section.isConnected || workspaceScope() !== formScope) return;
|
|
1795
|
+
exportSourceLoading = false;
|
|
1796
|
+
refreshSource.disabled = false;
|
|
1797
|
+
const data = result.ok ? result.body : null;
|
|
1798
|
+
if (Number.isInteger(data?.attempt) && data.attempt > 0 && data.effectiveContext?.status === "captured") {
|
|
1799
|
+
exportAttempt = data.attempt;
|
|
1800
|
+
submit.disabled = false;
|
|
1801
|
+
setStatus("交接包将使用上方标明的来源;有新尝试时可刷新来源。");
|
|
1802
|
+
} else {
|
|
1803
|
+
setStatus("最新尝试没有可用的上下文记录。记录生成后,可刷新来源重试。", true);
|
|
1804
|
+
}
|
|
1805
|
+
updatePreview();
|
|
1806
|
+
};
|
|
1807
|
+
refreshSource.addEventListener("click", () => { void loadExportSource(); });
|
|
1808
|
+
void loadExportSource();
|
|
1746
1809
|
submit.addEventListener("click", () => {
|
|
1747
1810
|
void (async () => {
|
|
1811
|
+
if (exportAttempt == null || workspaceScope() !== formScope) return;
|
|
1748
1812
|
const targetValue = selectedValue("context-export-target");
|
|
1749
1813
|
const profileValue = selectedValue("context-export-profile");
|
|
1750
1814
|
if (profileValue === "raw" && !rawAck.checked) {
|
|
@@ -1752,8 +1816,10 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1752
1816
|
return;
|
|
1753
1817
|
}
|
|
1754
1818
|
submit.disabled = true;
|
|
1819
|
+
refreshSource.disabled = true;
|
|
1755
1820
|
try {
|
|
1756
1821
|
setStatus("正在生成带来源校验信息的交接包…");
|
|
1822
|
+
if (workspaceScope() !== formScope) return;
|
|
1757
1823
|
const clientRequestId = `inspect-context-export-${Date.now()}-${Math.random()
|
|
1758
1824
|
.toString(36)
|
|
1759
1825
|
.slice(2, 10)}`;
|
|
@@ -1762,25 +1828,28 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1762
1828
|
{
|
|
1763
1829
|
runId: dagRunId,
|
|
1764
1830
|
nodeId: node.nodeId,
|
|
1765
|
-
attempt:
|
|
1831
|
+
attempt: exportAttempt,
|
|
1766
1832
|
target: targetValue,
|
|
1767
1833
|
profile: profileValue,
|
|
1768
1834
|
rawAcknowledged: profileValue === "raw" && rawAck.checked,
|
|
1769
1835
|
},
|
|
1770
1836
|
clientRequestId,
|
|
1771
1837
|
);
|
|
1838
|
+
if (workspaceScope() !== formScope) return;
|
|
1772
1839
|
const operationId = response.body?.operationId;
|
|
1773
1840
|
if (!response.ok || !operationId) {
|
|
1774
1841
|
throw new Error(response.body?.error?.message ?? "导出操作未被接受");
|
|
1775
1842
|
}
|
|
1776
1843
|
const operation = await pollOperationToTerminal(operationId, (message) =>
|
|
1777
|
-
setStatus(message.replace("续跑", "导出")),
|
|
1844
|
+
setStatus(message.replace("续跑", "导出")), formScope,
|
|
1778
1845
|
);
|
|
1846
|
+
if (workspaceScope() !== formScope) return;
|
|
1779
1847
|
if (operation?.state !== "succeeded" || !operation.result?.downloadUrl) {
|
|
1780
1848
|
throw new Error(operation?.errorMessage ?? "导出未成功完成");
|
|
1781
1849
|
}
|
|
1782
1850
|
setStatus("交接包已生成,正在安全下载…");
|
|
1783
1851
|
const downloaded = await downloadContextExport(operation.result.downloadUrl);
|
|
1852
|
+
if (workspaceScope() !== formScope) return;
|
|
1784
1853
|
const objectUrl = URL.createObjectURL(downloaded.blob);
|
|
1785
1854
|
const link = document.createElement("a");
|
|
1786
1855
|
link.href = objectUrl;
|
|
@@ -1796,6 +1865,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1796
1865
|
setStatus(transferErrorStatusText(error, "导出未成功完成"), true);
|
|
1797
1866
|
} finally {
|
|
1798
1867
|
submit.disabled = false;
|
|
1868
|
+
refreshSource.disabled = false;
|
|
1799
1869
|
}
|
|
1800
1870
|
})();
|
|
1801
1871
|
});
|
|
@@ -1845,16 +1915,11 @@ function buildDagActionAvailability({ title, description, reasonCode, tone = "ne
|
|
|
1845
1915
|
return panel;
|
|
1846
1916
|
}
|
|
1847
1917
|
|
|
1848
|
-
function buildFullContentCta() {
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
return el(
|
|
1854
|
-
"p",
|
|
1855
|
-
"artifact-content-preview-full-cta",
|
|
1856
|
-
"预览有截断;当前版本暂不提供完整文件下载。",
|
|
1857
|
-
);
|
|
1918
|
+
function buildFullContentCta(dagRunId, nodeId, artifactId) {
|
|
1919
|
+
const link = el("a", "artifact-content-preview-full-cta btn btn-secondary", "下载完整脱敏文本");
|
|
1920
|
+
link.href = scopedApiUrl(`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/artifacts/download?artifactId=${encodeURIComponent(artifactId)}`);
|
|
1921
|
+
link.setAttribute("download", `${artifactId}.redacted.txt`);
|
|
1922
|
+
return link;
|
|
1858
1923
|
}
|
|
1859
1924
|
|
|
1860
1925
|
function createArtifactContentPreview(
|
|
@@ -1968,9 +2033,8 @@ function createArtifactContentPreview(
|
|
|
1968
2033
|
if (notes.length > 0) {
|
|
1969
2034
|
host.appendChild(el("p", "artifact-content-preview-note", notes.join(" · ")));
|
|
1970
2035
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}
|
|
2036
|
+
host.appendChild(buildFullContentCta(dagRunId, nodeId, artifact.artifactId));
|
|
2037
|
+
const copy = el("button", "btn btn-secondary", "复制预览文本"); copy.type = "button"; copy.addEventListener("click", async () => { copy.textContent = await copyText(data.content) ? "已复制" : "复制失败"; }); host.appendChild(copy);
|
|
1974
2038
|
};
|
|
1975
2039
|
details.addEventListener("toggle", () => {
|
|
1976
2040
|
if (details.open) void load();
|
|
@@ -2006,6 +2070,7 @@ export function artifactRevisionRunEligibility(run) {
|
|
|
2006
2070
|
}
|
|
2007
2071
|
|
|
2008
2072
|
function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
2073
|
+
const formScope = workspaceScope();
|
|
2009
2074
|
const { section, body } = createDagActionCard({
|
|
2010
2075
|
eyebrow: "替换已有文件",
|
|
2011
2076
|
title: "换掉这个节点的产出后继续",
|
|
@@ -2033,6 +2098,7 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2033
2098
|
);
|
|
2034
2099
|
void (async () => {
|
|
2035
2100
|
const response = await fetchDagNodeArtifacts(dagRunId, node.nodeId);
|
|
2101
|
+
if (workspaceScope() !== formScope || !body.isConnected) return;
|
|
2036
2102
|
clearNode(body);
|
|
2037
2103
|
if (!response.ok || response.body?.binding?.available !== true) {
|
|
2038
2104
|
body.appendChild(
|
|
@@ -2128,14 +2194,36 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2128
2194
|
const planHost = el("div", "artifact-revision-plan");
|
|
2129
2195
|
form.appendChild(planHost);
|
|
2130
2196
|
body.appendChild(form);
|
|
2197
|
+
const draftKey = `${dagRunId}:${node.nodeId}:artifact`;
|
|
2198
|
+
const artifactDraft = inspectorDrafts.get(draftKey) ?? {};
|
|
2199
|
+
inspectorDrafts.set(draftKey, artifactDraft);
|
|
2200
|
+
if (eligible.some((item) => item.artifactId === artifactDraft.artifactId)) artifactSelect.value = artifactDraft.artifactId;
|
|
2201
|
+
reason.value = artifactDraft.reason ?? "";
|
|
2202
|
+
const fileHint = el("p", "muted", artifactDraft.file ? `已保留:${artifactDraft.file.name}` : "");
|
|
2203
|
+
fileLabel.appendChild(fileHint);
|
|
2204
|
+
let draftRevision = 0;
|
|
2205
|
+
const updateDraft = () => {
|
|
2206
|
+
artifactDraft.artifactId = artifactSelect.value;
|
|
2207
|
+
artifactDraft.reason = reason.value;
|
|
2208
|
+
artifactDraft.file = file.files?.[0] ?? artifactDraft.file;
|
|
2209
|
+
fileHint.textContent = artifactDraft.file ? `已保留:${artifactDraft.file.name}` : "";
|
|
2210
|
+
draftRevision += 1; clearNode(planHost);
|
|
2211
|
+
};
|
|
2212
|
+
artifactSelect.addEventListener("change", updateDraft);
|
|
2213
|
+
file.addEventListener("change", updateDraft);
|
|
2214
|
+
reason.addEventListener("input", updateDraft);
|
|
2131
2215
|
const setStatus = (message, error = false) => {
|
|
2132
2216
|
status.textContent = message;
|
|
2133
2217
|
status.classList.toggle("prompt-restart-error", error);
|
|
2134
2218
|
};
|
|
2219
|
+
const syncArtifactRetry = mountSubmissionRetry(form, artifactDraft, dagRunId, node.nodeId, formScope, setStatus);
|
|
2135
2220
|
preview.addEventListener("click", () => {
|
|
2136
2221
|
void (async () => {
|
|
2222
|
+
if (hasUnresolvedSubmission(artifactDraft.submission) || artifactDraft.submission?.operationId) { setStatus("已有提交记录,请使用“重试原提交”或查看操作进度。"); return; }
|
|
2137
2223
|
const selected = eligible.find((artifact) => artifact.artifactId === artifactSelect.value);
|
|
2138
|
-
const selectedFile = file.files?.[0];
|
|
2224
|
+
const selectedFile = file.files?.[0] ?? artifactDraft.file;
|
|
2225
|
+
const requestedRevision = draftRevision;
|
|
2226
|
+
const requestedReason = reason.value.trim();
|
|
2139
2227
|
if (!selected || !selectedFile) {
|
|
2140
2228
|
setStatus("请选择一个替换文件。", true);
|
|
2141
2229
|
return;
|
|
@@ -2160,6 +2248,7 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2160
2248
|
const candidateBase64 = String(dataUrl).split(",", 2)[1] ?? "";
|
|
2161
2249
|
setStatus("正在检查新文件格式并对比差异…");
|
|
2162
2250
|
const stageId = `artifact-stage-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2251
|
+
if (workspaceScope() !== formScope) return;
|
|
2163
2252
|
const staged = await postOperatorAction(
|
|
2164
2253
|
"artifactRevisionStage",
|
|
2165
2254
|
{
|
|
@@ -2171,9 +2260,10 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2171
2260
|
},
|
|
2172
2261
|
stageId,
|
|
2173
2262
|
);
|
|
2263
|
+
if (workspaceScope() !== formScope) return;
|
|
2174
2264
|
if (!staged.ok || !staged.body?.operationId) throw new Error(staged.body?.error?.message ?? "修订 staging 失败");
|
|
2175
2265
|
const stageOp = await pollOperationToTerminal(staged.body.operationId, (message) =>
|
|
2176
|
-
setStatus(message.replace("续跑", "修订校验")),
|
|
2266
|
+
setStatus(message.replace("续跑", "修订校验")), formScope,
|
|
2177
2267
|
);
|
|
2178
2268
|
const revision = stageOp?.result?.revision;
|
|
2179
2269
|
if (stageOp?.state !== "succeeded" || !revision?.revisionId) {
|
|
@@ -2181,17 +2271,21 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2181
2271
|
}
|
|
2182
2272
|
setStatus("文件检查通过,正在计算哪些后续节点需要重新执行…");
|
|
2183
2273
|
const planId = `artifact-plan-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2274
|
+
if (workspaceScope() !== formScope) return;
|
|
2184
2275
|
const planned = await postOperatorAction(
|
|
2185
2276
|
"artifactRevisionPlan",
|
|
2186
2277
|
{ runId: dagRunId, artifactId: selected.artifactId, revisionId: revision.revisionId },
|
|
2187
2278
|
planId,
|
|
2188
2279
|
);
|
|
2280
|
+
if (workspaceScope() !== formScope) return;
|
|
2189
2281
|
if (!planned.ok || !planned.body?.operationId) throw new Error(planned.body?.error?.message ?? "影响预览失败");
|
|
2190
2282
|
const planOp = await pollOperationToTerminal(planned.body.operationId, (message) =>
|
|
2191
|
-
setStatus(message.replace("续跑", "影响分析")),
|
|
2283
|
+
setStatus(message.replace("续跑", "影响分析")), formScope,
|
|
2192
2284
|
);
|
|
2285
|
+
if (workspaceScope() !== formScope) return;
|
|
2193
2286
|
const plan = planOp?.result?.plan;
|
|
2194
2287
|
if (!plan) throw new Error(planOp?.errorMessage ?? "影响预览失败");
|
|
2288
|
+
if (draftRevision !== requestedRevision || !body.isConnected) { setStatus("草稿已变化,请重新预览。"); return; }
|
|
2195
2289
|
planHost.appendChild(el("h5", null, "替换结果与影响范围"));
|
|
2196
2290
|
planHost.appendChild(
|
|
2197
2291
|
transparencyMetaList([
|
|
@@ -2232,23 +2326,22 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2232
2326
|
planHost.appendChild(execute);
|
|
2233
2327
|
execute.addEventListener("click", () => {
|
|
2234
2328
|
void (async () => {
|
|
2329
|
+
if (draftRevision !== requestedRevision || hasUnresolvedSubmission(artifactDraft.submission) || artifactDraft.submission?.operationId) { setStatus("此预览已变化或已经提交,请查看操作进度。", true); return; }
|
|
2235
2330
|
execute.disabled = true;
|
|
2236
2331
|
try {
|
|
2237
2332
|
setStatus("正在创建新运行并重新执行受影响节点;原运行保持不变…");
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
revisionId: revision.revisionId,
|
|
2245
|
-
planHash: plan.planHash,
|
|
2246
|
-
reason: reason.value.trim(),
|
|
2247
|
-
},
|
|
2248
|
-
executeId,
|
|
2249
|
-
);
|
|
2333
|
+
if (workspaceScope() !== formScope) return;
|
|
2334
|
+
artifactDraft.submission = createInspectorSubmission("artifactRevisionExecute", {
|
|
2335
|
+
runId: dagRunId, artifactId: selected.artifactId,
|
|
2336
|
+
revisionId: revision.revisionId, planHash: plan.planHash, reason: requestedReason,
|
|
2337
|
+
});
|
|
2338
|
+
const accepted = await sendInspectorSubmission(artifactDraft.submission, postOperatorAction);
|
|
2250
2339
|
if (!accepted.ok || !accepted.body?.operationId) throw new Error(accepted.body?.error?.message ?? "新运行未被接受");
|
|
2251
|
-
|
|
2340
|
+
if (workspaceScope() !== formScope) return;
|
|
2341
|
+
artifactDraft.submission.operationId = accepted.body.operationId;
|
|
2342
|
+
trackInspectorOperation(dagRunId, node.nodeId, accepted.body.operationId);
|
|
2343
|
+
const operation = await pollOperationToTerminal(accepted.body.operationId, setStatus, formScope);
|
|
2344
|
+
if (workspaceScope() !== formScope) return;
|
|
2252
2345
|
const childRunId = operation?.result?.newRunId;
|
|
2253
2346
|
if (childRunId) {
|
|
2254
2347
|
const open = document.createElement("button");
|
|
@@ -2271,7 +2364,9 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2271
2364
|
} catch (error) {
|
|
2272
2365
|
setStatus(transferErrorStatusText(error, "新运行失败"), true);
|
|
2273
2366
|
} finally {
|
|
2274
|
-
|
|
2367
|
+
if (artifactDraft.submission) artifactDraft.submission.inFlight = false;
|
|
2368
|
+
execute.disabled = Boolean(artifactDraft.submission?.operationId) || hasUnresolvedSubmission(artifactDraft.submission);
|
|
2369
|
+
syncArtifactRetry();
|
|
2275
2370
|
}
|
|
2276
2371
|
})();
|
|
2277
2372
|
});
|
|
@@ -2311,6 +2406,7 @@ function buildDagActionIntro(node) {
|
|
|
2311
2406
|
}
|
|
2312
2407
|
|
|
2313
2408
|
async function renderPromptRestart(content, dagRunId, node, run) {
|
|
2409
|
+
const formScope = workspaceScope();
|
|
2314
2410
|
const nodeId = node.nodeId;
|
|
2315
2411
|
clearNode(content);
|
|
2316
2412
|
const intro = buildDagActionIntro(node);
|
|
@@ -2336,10 +2432,24 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2336
2432
|
["context-export", exportSection],
|
|
2337
2433
|
]);
|
|
2338
2434
|
content.appendChild(intro);
|
|
2435
|
+
const intent = el("div", "dag-action-intents");
|
|
2436
|
+
const intentKey = `${dagRunId}:${nodeId}:intent`;
|
|
2437
|
+
let selectedIntent = inspectorDrafts.get(intentKey) ?? "prompt-restart";
|
|
2438
|
+
const chooseIntent = (id) => {
|
|
2439
|
+
selectedIntent = id; inspectorDrafts.set(intentKey, id);
|
|
2440
|
+
for (const [key, section] of sections) section.hidden = key !== id;
|
|
2441
|
+
for (const button of intent.querySelectorAll("button")) button.setAttribute("aria-pressed", String(button.dataset.intent === id));
|
|
2442
|
+
};
|
|
2443
|
+
for (const [id, label] of [["prompt-restart", "修改提示重跑"], ["artifact-revision", "替换产出继续"], ["context-export", "导出资料"]]) {
|
|
2444
|
+
const button = el("button", "btn btn-secondary", label); button.type = "button"; button.dataset.intent = id; button.setAttribute("aria-pressed", String(selectedIntent === id)); button.addEventListener("click", () => chooseIntent(id)); intent.appendChild(button);
|
|
2445
|
+
}
|
|
2446
|
+
content.appendChild(intent);
|
|
2339
2447
|
for (const sectionId of DAG_ACTION_SECTION_ORDER) {
|
|
2340
2448
|
const section = sections.get(sectionId);
|
|
2341
2449
|
if (section) content.appendChild(section);
|
|
2342
2450
|
}
|
|
2451
|
+
chooseIntent(selectedIntent);
|
|
2452
|
+
mountInspectorOperations(content, dagRunId, nodeId);
|
|
2343
2453
|
const input = await fetchDagNodeInput(dagRunId, nodeId);
|
|
2344
2454
|
if (
|
|
2345
2455
|
!content.isConnected ||
|
|
@@ -2376,7 +2486,10 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2376
2486
|
frozenBasePrompt,
|
|
2377
2487
|
input.body.promptCandidates,
|
|
2378
2488
|
);
|
|
2379
|
-
|
|
2489
|
+
const draftKey = `${dagRunId}:${nodeId}:prompt`;
|
|
2490
|
+
const draft = inspectorDrafts.get(draftKey) ?? { candidateKey: "frozen", candidates: {} };
|
|
2491
|
+
inspectorDrafts.set(draftKey, draft);
|
|
2492
|
+
let selectedCandidate = promptCandidateOptions.find((item) => item.key === draft.candidateKey) ?? promptCandidateOptions[0];
|
|
2380
2493
|
const historicalCandidatesMissing = Array.isArray(input.body.warnings)
|
|
2381
2494
|
? input.body.warnings.some((warning) =>
|
|
2382
2495
|
String(warning).startsWith(
|
|
@@ -2465,14 +2578,12 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2465
2578
|
? "这个较早的运行没有保存失败时实际使用的提示词;新运行会在这里列出。"
|
|
2466
2579
|
: "该节点没有保存过失败时使用的提示词;当前只有原始提示词。",
|
|
2467
2580
|
onChange: (nextKey) => {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2581
|
+
saveDraft();
|
|
2582
|
+
selectedCandidate = promptCandidateOptions.find((option) => option.key === nextKey) ?? promptCandidateOptions[0];
|
|
2583
|
+
draft.candidateKey = selectedCandidate.key;
|
|
2471
2584
|
promptCard.sync(selectedCandidate);
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
modeSelect.value = "replace";
|
|
2475
|
-
}
|
|
2585
|
+
restoreDraft();
|
|
2586
|
+
invalidatePlan();
|
|
2476
2587
|
picker.hintEl.hidden = false;
|
|
2477
2588
|
picker.hintEl.textContent = selectedCandidate.truncated
|
|
2478
2589
|
? "该候选已截断,仅供查看,不能直接提交重启。"
|
|
@@ -2484,7 +2595,7 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2484
2595
|
});
|
|
2485
2596
|
const modeSwitch = createPromptRestartModeSwitch({
|
|
2486
2597
|
value: "append",
|
|
2487
|
-
onChange: () => applyModeVisibility(),
|
|
2598
|
+
onChange: () => { applyModeVisibility(); saveDraft(); invalidatePlan(); },
|
|
2488
2599
|
});
|
|
2489
2600
|
const modeSelect = modeSwitch.group;
|
|
2490
2601
|
const toolbar = el("div", "prompt-restart-toolbar");
|
|
@@ -2527,6 +2638,8 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2527
2638
|
box.appendChild(baseLabel);
|
|
2528
2639
|
|
|
2529
2640
|
const applyModeVisibility = () => {
|
|
2641
|
+
if (selectedCandidate.key !== "frozen") modeSelect.value = "replace";
|
|
2642
|
+
for (const button of modeSelect.querySelectorAll("button")) if (button.dataset.mode === "append" || button.getAttribute("data-value") === "append") button.disabled = selectedCandidate.key !== "frozen";
|
|
2530
2643
|
const append = modeSelect.value === "append";
|
|
2531
2644
|
baseLabel.classList.toggle("is-hidden", !append);
|
|
2532
2645
|
addendumLabel.classList.toggle("is-hidden", !append);
|
|
@@ -2554,10 +2667,44 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2554
2667
|
const submit = document.createElement("button");
|
|
2555
2668
|
submit.type = "button";
|
|
2556
2669
|
submit.className = "prompt-restart-submit";
|
|
2557
|
-
submit.textContent = "
|
|
2670
|
+
submit.textContent = "预览重跑影响";
|
|
2558
2671
|
submit.setAttribute("data-testid", "prompt-restart-submit");
|
|
2559
2672
|
row.appendChild(status);
|
|
2560
2673
|
row.appendChild(submit);
|
|
2674
|
+
const start = el("button", "prompt-restart-submit", "确认并开始新运行");
|
|
2675
|
+
start.type = "button";
|
|
2676
|
+
start.disabled = true;
|
|
2677
|
+
start.dataset.testid = "prompt-restart-execute";
|
|
2678
|
+
row.appendChild(start);
|
|
2679
|
+
const planHost = el("div", "prompt-restart-plan");
|
|
2680
|
+
box.appendChild(planHost);
|
|
2681
|
+
let prepared = null;
|
|
2682
|
+
let submitted = false;
|
|
2683
|
+
draft.submission ??= {};
|
|
2684
|
+
const signature = () => JSON.stringify([selectedCandidate.key, modeSelect.value, addendumArea.value, replaceArea.value, reasonArea.value]);
|
|
2685
|
+
const invalidatePlan = () => { prepared = null; start.disabled = true; clearNode(planHost); };
|
|
2686
|
+
const saveDraft = () => {
|
|
2687
|
+
draft.candidates[selectedCandidate.key] = { mode: modeSelect.value, addendum: addendumArea.value, replacement: replaceArea.value, reason: reasonArea.value };
|
|
2688
|
+
};
|
|
2689
|
+
const restoreDraft = () => {
|
|
2690
|
+
const saved = draft.candidates[selectedCandidate.key];
|
|
2691
|
+
modeSelect.value = saved?.mode ?? (selectedCandidate.key === "frozen" ? "append" : "replace");
|
|
2692
|
+
addendumArea.value = saved?.addendum ?? "";
|
|
2693
|
+
replaceArea.value = saved?.replacement ?? selectedCandidate.prompt;
|
|
2694
|
+
reasonArea.value = saved?.reason ?? `修改失败节点 ${nodeId} 的提示词后重新运行(来源运行:${dagRunId})`;
|
|
2695
|
+
applyModeVisibility();
|
|
2696
|
+
};
|
|
2697
|
+
for (const area of [addendumArea, replaceArea, reasonArea]) area.addEventListener("input", () => { saveDraft(); invalidatePlan(); });
|
|
2698
|
+
restoreDraft();
|
|
2699
|
+
let beforeRestore = null;
|
|
2700
|
+
const restoreOriginal = el("button", "btn btn-secondary", "恢复此版本原文");
|
|
2701
|
+
restoreOriginal.type = "button";
|
|
2702
|
+
restoreOriginal.addEventListener("click", () => {
|
|
2703
|
+
if (beforeRestore === null) { beforeRestore = { key: selectedCandidate.key, text: replaceArea.value }; replaceArea.value = selectedCandidate.prompt; restoreOriginal.textContent = "撤销恢复"; }
|
|
2704
|
+
else { if (beforeRestore.key === selectedCandidate.key) replaceArea.value = beforeRestore.text; beforeRestore = null; restoreOriginal.textContent = "恢复此版本原文"; }
|
|
2705
|
+
saveDraft(); invalidatePlan();
|
|
2706
|
+
});
|
|
2707
|
+
toolbar.appendChild(restoreOriginal);
|
|
2561
2708
|
box.appendChild(row);
|
|
2562
2709
|
|
|
2563
2710
|
promptBody.appendChild(box);
|
|
@@ -2567,7 +2714,11 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2567
2714
|
status.classList.toggle("prompt-restart-error", isError);
|
|
2568
2715
|
};
|
|
2569
2716
|
|
|
2570
|
-
|
|
2717
|
+
const syncPromptRetry = mountSubmissionRetry(box, draft, dagRunId, nodeId, formScope, setStatus);
|
|
2718
|
+
const handleSubmission = (executeRequested) => {
|
|
2719
|
+
if (submitted || draft.submission.inFlight || (executeRequested && !prepared)) return;
|
|
2720
|
+
if (hasUnresolvedSubmission(draft.submission)) { setStatus("上次提交结果尚未确认,请先重试原提交。", true); return; }
|
|
2721
|
+
if (executeRequested && draft.submission.operationId) { setStatus("这份草稿已经提交,请查看上方操作进度。"); return; }
|
|
2571
2722
|
void (async () => {
|
|
2572
2723
|
if (selectedCandidate.truncated) {
|
|
2573
2724
|
setStatus("所选失败提示词已截断,不能作为完整提示词重启。", true);
|
|
@@ -2596,15 +2747,22 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2596
2747
|
return;
|
|
2597
2748
|
}
|
|
2598
2749
|
submit.disabled = true;
|
|
2750
|
+
start.disabled = true;
|
|
2751
|
+
const requestedSignature = signature();
|
|
2599
2752
|
try {
|
|
2600
|
-
setStatus("正在检查这次重跑是否安全可行…");
|
|
2601
2753
|
const overrides = [{ nodeId, prompt, mode }];
|
|
2754
|
+
let plan = prepared?.plan;
|
|
2755
|
+
if (!executeRequested) {
|
|
2756
|
+
invalidatePlan();
|
|
2757
|
+
setStatus("正在检查这次重跑是否安全可行…");
|
|
2758
|
+
if (workspaceScope() !== formScope) return;
|
|
2602
2759
|
const planRes = await postOperatorAction("dagRerunPlan", {
|
|
2603
2760
|
runId: dagRunId,
|
|
2604
2761
|
fromNode: nodeId,
|
|
2605
2762
|
promptOverrides: overrides,
|
|
2606
2763
|
});
|
|
2607
|
-
|
|
2764
|
+
if (workspaceScope() !== formScope) return;
|
|
2765
|
+
plan = planRes.body?.result ?? null;
|
|
2608
2766
|
if (!planRes.ok || plan?.eligible !== true || !plan?.planHash) {
|
|
2609
2767
|
setStatus(
|
|
2610
2768
|
planRes.body?.error?.message ??
|
|
@@ -2615,34 +2773,50 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2615
2773
|
return;
|
|
2616
2774
|
}
|
|
2617
2775
|
|
|
2776
|
+
if (signature() !== requestedSignature) { setStatus("草稿已变化,请重新预览。"); return; }
|
|
2777
|
+
prepared = { plan, signature: requestedSignature };
|
|
2778
|
+
clearNode(planHost);
|
|
2779
|
+
planHost.appendChild(transparencyMetaList([
|
|
2780
|
+
["将重新执行", (plan.resetNodeIds ?? []).join(", ") || "无"],
|
|
2781
|
+
["将沿用结果", (plan.importedNodeIds ?? []).join(", ") || "无"],
|
|
2782
|
+
["预计风险", inspectorValueLabel("风险级别", plan.risk)],
|
|
2783
|
+
["预计执行", `${plan.estimatedExecution?.nodeCount ?? "未记录"} 个节点 · ${plan.estimatedExecution?.piCallCount ?? "未记录"} 次 AI 调用`],
|
|
2784
|
+
]));
|
|
2785
|
+
const change = el("details", "prompt-change-preview");
|
|
2786
|
+
change.appendChild(el("summary", null, mode === "append" ? "查看将追加的指令" : "查看替换前后提示词"));
|
|
2787
|
+
if (mode === "replace") { change.appendChild(el("h5", null, "修改前")); change.appendChild(el("pre", "prompt-restart-readonly", selectedCandidate.prompt)); }
|
|
2788
|
+
change.appendChild(el("h5", null, "本次提交")); change.appendChild(el("pre", "prompt-restart-readonly", prompt));
|
|
2789
|
+
planHost.appendChild(change);
|
|
2790
|
+
start.disabled = false;
|
|
2791
|
+
setStatus("预览已就绪。确认后创建新运行;编辑草稿会使本次预览失效。");
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
if (prepared?.signature !== requestedSignature) { invalidatePlan(); setStatus("草稿已变化,请重新预览。", true); return; }
|
|
2795
|
+
|
|
2618
2796
|
const est = plan.estimatedExecution;
|
|
2619
2797
|
const estText = est
|
|
2620
2798
|
? `(将执行 ${est.nodeCount ?? "?"} 个节点,其中 ${est.piCallCount ?? "?"} 个需要 AI 处理)`
|
|
2621
2799
|
: "";
|
|
2622
2800
|
setStatus(`检查通过${estText},正在创建新运行…`);
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
.
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
runId: dagRunId,
|
|
2630
|
-
fromNode: nodeId,
|
|
2631
|
-
planHash: plan.planHash,
|
|
2632
|
-
reason,
|
|
2633
|
-
promptOverrides: overrides,
|
|
2634
|
-
},
|
|
2635
|
-
clientRequestId,
|
|
2636
|
-
);
|
|
2801
|
+
if (workspaceScope() !== formScope) return;
|
|
2802
|
+
draft.submission = createInspectorSubmission("dagRerun", {
|
|
2803
|
+
runId: dagRunId, fromNode: nodeId, planHash: plan.planHash, reason, promptOverrides: overrides,
|
|
2804
|
+
});
|
|
2805
|
+
const runRes = await sendInspectorSubmission(draft.submission, postOperatorAction);
|
|
2806
|
+
if (workspaceScope() !== formScope) return;
|
|
2637
2807
|
const operationId = runRes.body?.operationId;
|
|
2638
2808
|
if (!runRes.ok || !operationId) {
|
|
2639
2809
|
setStatus(runRes.body?.error?.message ?? "提交续跑失败。", true);
|
|
2640
2810
|
return;
|
|
2641
2811
|
}
|
|
2642
2812
|
|
|
2813
|
+
submitted = true;
|
|
2814
|
+
draft.submission.operationId = operationId;
|
|
2815
|
+
trackInspectorOperation(dagRunId, nodeId, operationId);
|
|
2643
2816
|
const op = await pollOperationToTerminal(operationId, (text) =>
|
|
2644
|
-
setStatus(text),
|
|
2817
|
+
setStatus(text), formScope,
|
|
2645
2818
|
);
|
|
2819
|
+
if (workspaceScope() !== formScope) return;
|
|
2646
2820
|
if (op?.state === "succeeded") {
|
|
2647
2821
|
const newRunId =
|
|
2648
2822
|
op.dagRunId ?? op.result?.newRunId ?? op.runSummary?.runId ?? null;
|
|
@@ -2670,14 +2844,73 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2670
2844
|
} catch (err) {
|
|
2671
2845
|
setStatus(err instanceof Error ? err.message : String(err), true);
|
|
2672
2846
|
} finally {
|
|
2673
|
-
|
|
2847
|
+
draft.submission.inFlight = false;
|
|
2848
|
+
submit.disabled = submitted;
|
|
2849
|
+
start.disabled = submitted || !prepared || hasUnresolvedSubmission(draft.submission);
|
|
2850
|
+
syncPromptRetry();
|
|
2674
2851
|
}
|
|
2675
2852
|
})();
|
|
2853
|
+
};
|
|
2854
|
+
submit.addEventListener("click", () => handleSubmission(false));
|
|
2855
|
+
start.addEventListener("click", () => handleSubmission(true));
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
function mountSubmissionRetry(host, draft, dagRunId, nodeId, scope, setStatus) {
|
|
2859
|
+
const retry = el("button", "btn btn-secondary", "重试原提交");
|
|
2860
|
+
retry.type = "button";
|
|
2861
|
+
retry.setAttribute("data-tooltip", "原样重放已确认的请求,避免重复创建运行");
|
|
2862
|
+
host.appendChild(retry);
|
|
2863
|
+
const sync = () => {
|
|
2864
|
+
retry.hidden = !hasUnresolvedSubmission(draft.submission);
|
|
2865
|
+
retry.disabled = Boolean(draft.submission?.inFlight);
|
|
2866
|
+
};
|
|
2867
|
+
retry.addEventListener("click", async () => {
|
|
2868
|
+
if (workspaceScope() !== scope || !hasUnresolvedSubmission(draft.submission)) return;
|
|
2869
|
+
retry.disabled = true;
|
|
2870
|
+
try {
|
|
2871
|
+
setStatus("正在核对原提交结果…");
|
|
2872
|
+
const response = await sendInspectorSubmission(draft.submission, postOperatorAction);
|
|
2873
|
+
if (workspaceScope() !== scope) return;
|
|
2874
|
+
if (!response.ok || !draft.submission.operationId) {
|
|
2875
|
+
setStatus(response.body?.error?.message ?? "结果仍未确认,可以继续重试原提交。", true);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
trackInspectorOperation(dagRunId, nodeId, draft.submission.operationId);
|
|
2879
|
+
const operation = await pollOperationToTerminal(draft.submission.operationId, setStatus, scope);
|
|
2880
|
+
if (workspaceScope() === scope) setStatus(operation ? `操作状态:${inspectorValueLabel("status", operation.state)}` : "等待结束,可在操作台查看进度。");
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
setStatus(error instanceof Error ? error.message : "结果未确认,请重试原提交。", true);
|
|
2883
|
+
} finally { sync(); }
|
|
2676
2884
|
});
|
|
2885
|
+
sync();
|
|
2886
|
+
return sync;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
function trackInspectorOperation(dagRunId, nodeId, operationId) {
|
|
2890
|
+
inspectorOperations.set(operationId, { dagRunId, nodeId, operationId, operation: { state: "queued" } });
|
|
2891
|
+
refreshInspectorOperations();
|
|
2892
|
+
}
|
|
2893
|
+
function mountInspectorOperations(content, dagRunId, nodeId) {
|
|
2894
|
+
let host = content.querySelector(".inspector-operations");
|
|
2895
|
+
if (!host) { host = el("div", "inspector-operations"); content.insertBefore(host, content.firstChild); }
|
|
2896
|
+
clearNode(host);
|
|
2897
|
+
for (const tracked of inspectorOperations.values()) {
|
|
2898
|
+
if (tracked.dagRunId !== dagRunId || tracked.nodeId !== nodeId) continue;
|
|
2899
|
+
const card = el("div", "inspector-operation");
|
|
2900
|
+
card.appendChild(el("p", null, `已提交 · ${tracked.operationId} · ${inspectorValueLabel("status", tracked.operation.state)}`));
|
|
2901
|
+
const runId = tracked.operation.dagRunId ?? tracked.operation.result?.newRunId ?? tracked.operation.runSummary?.runId;
|
|
2902
|
+
if (runId) { const link = el("a", "object-link", "打开新运行"); link.href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(runId)}`); card.appendChild(link); }
|
|
2903
|
+
if (tracked.operation.errorMessage) card.appendChild(el("p", "node-input-error", tracked.operation.errorMessage));
|
|
2904
|
+
host.appendChild(card);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
function refreshInspectorOperations() {
|
|
2908
|
+
const content = document.querySelector(".dag-inspector-content[data-inspector-tab='prompt-restart']");
|
|
2909
|
+
if (content) mountInspectorOperations(content, content.dataset.dagRunId, content.dataset.dagNodeId);
|
|
2677
2910
|
}
|
|
2678
2911
|
|
|
2679
2912
|
/** Poll a Console operation until a terminal state; null on poll budget end. */
|
|
2680
|
-
async function pollOperationToTerminal(operationId, onProgress) {
|
|
2913
|
+
async function pollOperationToTerminal(operationId, onProgress, operationScope = workspaceScope()) {
|
|
2681
2914
|
const terminal = new Set([
|
|
2682
2915
|
"succeeded",
|
|
2683
2916
|
"failed",
|
|
@@ -2685,9 +2918,12 @@ async function pollOperationToTerminal(operationId, onProgress) {
|
|
|
2685
2918
|
"needs-reconcile",
|
|
2686
2919
|
]);
|
|
2687
2920
|
for (let attempt = 0; attempt < 1200; attempt += 1) {
|
|
2921
|
+
if (workspaceScope() !== operationScope) return null;
|
|
2688
2922
|
const res = await fetchOperatorOperation(operationId);
|
|
2923
|
+
if (workspaceScope() !== operationScope) return null;
|
|
2689
2924
|
const op = res.body?.operation ?? res.body;
|
|
2690
2925
|
const state = op?.state ?? "";
|
|
2926
|
+
if (res.ok && inspectorOperations.has(operationId)) { Object.assign(inspectorOperations.get(operationId), { operation: op }); refreshInspectorOperations(); }
|
|
2691
2927
|
if (res.ok && terminal.has(state)) {
|
|
2692
2928
|
return op;
|
|
2693
2929
|
}
|
|
@@ -2748,28 +2984,28 @@ function selectedAttemptNumber(dagRunId, node) {
|
|
|
2748
2984
|
}
|
|
2749
2985
|
|
|
2750
2986
|
async function renderEffectiveContext(content, dagRunId, node) {
|
|
2987
|
+
const requestToken = Symbol("context-request");
|
|
2988
|
+
content._contextRequestToken = requestToken;
|
|
2989
|
+
const requestedExecutionKey = effectiveExecutionKey(dagRunId, node);
|
|
2990
|
+
const requestedScope = workspaceScope();
|
|
2751
2991
|
ensureInspectorPageLead(content, "context");
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
dagRunId,
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
);
|
|
2992
|
+
if (requestedExecutionKey?.startsWith("pass-")) {
|
|
2993
|
+
content.appendChild(el("p", "node-transparency-omission", "该修复轮次没有独立保存的上下文记录。现有记录无法区分各个修复轮次。"));
|
|
2994
|
+
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "context");
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
content.appendChild(el("p", "muted", "正在读取这次尝试的上下文…"));
|
|
2998
|
+
const result = await fetchDagNodeContext(dagRunId, node.nodeId, selectedAttemptNumber(dagRunId, node));
|
|
2758
2999
|
if (
|
|
2759
|
-
!content.isConnected ||
|
|
3000
|
+
!content.isConnected || content._contextRequestToken !== requestToken || workspaceScope() !== requestedScope ||
|
|
2760
3001
|
content.dataset.dagRunId !== String(dagRunId) ||
|
|
2761
3002
|
content.dataset.dagNodeId !== String(node.nodeId) ||
|
|
2762
|
-
activeInspectorTab() !== "context"
|
|
2763
|
-
)
|
|
2764
|
-
return;
|
|
3003
|
+
activeInspectorTab() !== "context" || requestedExecutionKey !== effectiveExecutionKey(dagRunId, node)
|
|
3004
|
+
) return;
|
|
2765
3005
|
clearNode(content);
|
|
2766
3006
|
ensureInspectorPageLead(content, "context");
|
|
2767
3007
|
if (!result.ok || !result.body) {
|
|
2768
|
-
const error = el(
|
|
2769
|
-
"p",
|
|
2770
|
-
"node-input-error",
|
|
2771
|
-
result.body?.error || "无法加载有效上下文事实。",
|
|
2772
|
-
);
|
|
3008
|
+
const error = el("p", "node-input-error", result.body?.error || "无法读取这次尝试的上下文。");
|
|
2773
3009
|
error.setAttribute("role", "alert");
|
|
2774
3010
|
content.appendChild(error);
|
|
2775
3011
|
return;
|
|
@@ -2777,75 +3013,155 @@ async function renderEffectiveContext(content, dagRunId, node) {
|
|
|
2777
3013
|
const data = result.body;
|
|
2778
3014
|
const receipt = data.effectiveContext;
|
|
2779
3015
|
const root = el("div", "node-transparency-root");
|
|
2780
|
-
root.appendChild(
|
|
2781
|
-
transparencyMetaList([
|
|
2782
|
-
["attempt", data.attempt],
|
|
2783
|
-
["declared", data.declared?.status],
|
|
2784
|
-
["dispatched", data.dispatched?.status],
|
|
2785
|
-
["node status", data.dispatched?.nodeStatus],
|
|
2786
|
-
]),
|
|
2787
|
-
);
|
|
3016
|
+
root.appendChild(el("p", "context-record-heading", `第 ${data.attempt} 次尝试 · ${receipt?.status === "captured" ? "已保存上下文" : "上下文不可用"}`));
|
|
2788
3017
|
if (receipt?.status !== "captured") {
|
|
2789
3018
|
const missing = el("div", "node-transparency-omission");
|
|
2790
3019
|
const reasonCode = receipt?.reasonCode ?? "CONTEXT_RECEIPT_MISSING";
|
|
2791
3020
|
const missingLabel = contextTransferReasonLabel(reasonCode);
|
|
2792
|
-
missing.appendChild(
|
|
2793
|
-
el(
|
|
2794
|
-
"h4",
|
|
2795
|
-
null,
|
|
2796
|
-
receipt?.status === "invalid" ? "执行时记录不可用" : "没有执行时记录",
|
|
2797
|
-
),
|
|
2798
|
-
);
|
|
3021
|
+
missing.appendChild(el("h4", null, receipt?.status === "invalid" ? "记录未通过完整性检查" : "没有保存这次尝试的上下文"));
|
|
2799
3022
|
const reason = el("p", "muted", missingLabel ?? reasonCode);
|
|
2800
3023
|
if (missingLabel) reason.setAttribute("title", `技术代码:${reasonCode}`);
|
|
2801
3024
|
missing.appendChild(reason);
|
|
2802
|
-
missing.appendChild(
|
|
2803
|
-
el(
|
|
2804
|
-
"p",
|
|
2805
|
-
"muted",
|
|
2806
|
-
"当前只有运行前设置和提示内容校验码,无法证明执行时真正收到了哪些内容。较早的运行不会事后补写这份记录。",
|
|
2807
|
-
),
|
|
2808
|
-
);
|
|
3025
|
+
missing.appendChild(el("p", "muted", "无法确认当时实际收到的材料。你可以在“执行设置”查看运行前的配置;较早的运行不会事后补写上下文记录。"));
|
|
2809
3026
|
root.appendChild(missing);
|
|
2810
3027
|
} else {
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
"
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
)
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
(
|
|
2838
|
-
|
|
2839
|
-
|
|
3028
|
+
const components = receipt.components ?? [];
|
|
3029
|
+
const savedCount = components.filter((part) => part.previewAvailable).length;
|
|
3030
|
+
const referenceCount = components.filter((part) => part.inclusion === "reference-only").length;
|
|
3031
|
+
const omittedCount = components.filter((part) => part.inclusion === "omitted").length;
|
|
3032
|
+
root.appendChild(el("p", "context-record-meta", [
|
|
3033
|
+
receipt.runtime?.model, receipt.capturedAt ? `记录于 ${new Date(receipt.capturedAt).toLocaleString("zh-CN", { hour12: false })}` : null,
|
|
3034
|
+
`${savedCount} 项可查看正文`, referenceCount ? `${referenceCount} 项仅有引用` : null,
|
|
3035
|
+
omittedCount ? `${omittedCount} 项未包含` : null,
|
|
3036
|
+
].filter(Boolean).join(" · ")));
|
|
3037
|
+
const captured = el("section", "node-input-section context-materials");
|
|
3038
|
+
captured.appendChild(el("h4", null, "这次尝试的材料"));
|
|
3039
|
+
captured.appendChild(el("p", "muted", "展开材料查看当时保存的正文。仅有引用的材料未保存独立正文。"));
|
|
3040
|
+
for (const component of components) {
|
|
3041
|
+
const details = el("details", "context-component");
|
|
3042
|
+
const summary = el("summary", null);
|
|
3043
|
+
const kind = inspectorValueLabel("kind", component.kind);
|
|
3044
|
+
const name = inspectorValueLabel("kind", component.logicalName || component.kind);
|
|
3045
|
+
summary.appendChild(el("span", "context-component-name", name));
|
|
3046
|
+
summary.appendChild(el("span", "context-component-meta", [name === kind ? null : kind, inspectorValueLabel("inclusion", component.inclusion)].filter(Boolean).join(" · ")));
|
|
3047
|
+
details.appendChild(summary);
|
|
3048
|
+
if (!component.previewAvailable) {
|
|
3049
|
+
details.appendChild(el("p", "muted", component.inclusion === "omitted" ? "这项材料未包含在本次上下文中。" : "仅登记了材料信息,未保存当时的独立正文。"));
|
|
3050
|
+
} else {
|
|
3051
|
+
let loading = false, loaded = false;
|
|
3052
|
+
const body = el("div", "context-component-body");
|
|
3053
|
+
details.appendChild(body);
|
|
3054
|
+
details.addEventListener("toggle", () => {
|
|
3055
|
+
if (!details.open || loaded || loading) return;
|
|
3056
|
+
loading = true; body.textContent = "正在读取保存的正文…";
|
|
3057
|
+
void fetchJson(`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(node.nodeId)}/context/component?attempt=${data.attempt}&componentIndex=${component.index}`).then((part) => {
|
|
3058
|
+
loading = false;
|
|
3059
|
+
if (!body.isConnected || workspaceScope() !== requestedScope) return;
|
|
3060
|
+
clearNode(body);
|
|
3061
|
+
if (typeof part?.content !== "string") { body.appendChild(el("p", "node-input-error", "正文不可用,收起后可重试。")); return; }
|
|
3062
|
+
loaded = true;
|
|
3063
|
+
body.appendChild(renderInspectorDocument(part.content));
|
|
3064
|
+
const copy = iconButton("复制已显示正文", "ri-file-copy-line");
|
|
3065
|
+
copy.type = "button";
|
|
3066
|
+
copy.addEventListener("click", async () => { const copied = await copyText(part.content); setIconButton(copy, copied ? "已复制" : "复制失败", copied ? "ri-check-line" : "ri-error-warning-line"); });
|
|
3067
|
+
body.appendChild(copy);
|
|
3068
|
+
if (part.truncated || part.redacted) body.appendChild(el("p", "muted", [part.truncated ? "正文预览已截断" : "", part.redacted ? "已脱敏" : ""].filter(Boolean).join(" · ")));
|
|
3069
|
+
});
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
const source = el("details", "context-source-details");
|
|
3073
|
+
source.appendChild(el("summary", null, "材料来源信息"));
|
|
3074
|
+
source.appendChild(transparencyMetaList([["来源", component.sourceIdentity], ["大小", `${component.bytes} 字节`], ["记录说明", component.omissionReason]]));
|
|
3075
|
+
details.appendChild(source);
|
|
3076
|
+
captured.appendChild(details);
|
|
3077
|
+
}
|
|
2840
3078
|
root.appendChild(captured);
|
|
3079
|
+
const diagnostics = el("details", "context-source-details");
|
|
3080
|
+
diagnostics.appendChild(el("summary", null, "来源与校验信息"));
|
|
3081
|
+
diagnostics.appendChild(transparencyMetaList([
|
|
3082
|
+
["context digest", receipt.contextDigest], ["captured at", receipt.capturedAt],
|
|
3083
|
+
["executor", receipt.runtime?.executor], ["components", receipt.componentCount],
|
|
3084
|
+
["truncations", receipt.truncationCount], ["skills", receipt.runtime?.skillCount],
|
|
3085
|
+
["extensions", receipt.runtime?.extensionCount], ["tools", receipt.runtime?.toolCount],
|
|
3086
|
+
]));
|
|
3087
|
+
root.appendChild(diagnostics);
|
|
2841
3088
|
}
|
|
2842
3089
|
content.appendChild(root);
|
|
2843
3090
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "context");
|
|
2844
3091
|
}
|
|
2845
3092
|
|
|
3093
|
+
async function recordedAttempt(dagRunId, node, key) {
|
|
3094
|
+
const [output, context] = await Promise.all([
|
|
3095
|
+
fetchJsonResult(`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(node.nodeId)}/execution-output?key=${encodeURIComponent(key)}`),
|
|
3096
|
+
/^attempt-/.test(key) ? fetchDagNodeContext(dagRunId, node.nodeId, Number(key.slice(8))) : Promise.resolve(null),
|
|
3097
|
+
]);
|
|
3098
|
+
return { key, output: output.ok ? output.body : null, preview: attemptSelectorOptions(node).find((item) => item.key === key)?.preview, model: context?.ok && context.body?.effectiveContext?.status === "captured" ? context.body.effectiveContext.runtime?.model : null };
|
|
3099
|
+
}
|
|
3100
|
+
function attemptDiagnostic(dagRunId, node, recorded) {
|
|
3101
|
+
const output = recorded.output;
|
|
3102
|
+
return [`# 节点诊断`, `运行:${dagRunId}`, `节点:${node.nodeId}`, `执行:${recorded.key}`, `状态:${recorded.preview?.status ?? "未记录"}`, `失败分类:${recorded.preview?.failureCategory ?? "未记录"}`, `耗时:${recorded.preview?.durationMs != null ? formatMs(recorded.preview.durationMs) : "未记录"}`, `模型:${recorded.model ?? "未记录"}`, `记录时间:${output?.timestamp ?? "未记录"}`, `范围:当前尝试保存的输出${output?.truncated ? "(接口已截断)" : ""}`, `位置:${new URL(inspectPathToCanonicalHref(`/dag/${encodeURIComponent(dagRunId)}?node=${encodeURIComponent(node.nodeId)}&tab=output&attempt=${recorded.key}`), location.href).href}`, "", "错误输出:", output?.stderr || "未记录"].join("\n");
|
|
3103
|
+
}
|
|
3104
|
+
async function renderAttemptComparison(content, dagRunId, node) {
|
|
3105
|
+
const options = attemptSelectorOptions(node);
|
|
3106
|
+
if (options.length < 2) return;
|
|
3107
|
+
const toolbar = el("div", "attempt-comparison-toolbar"), body = el("div", "attempt-comparison-columns");
|
|
3108
|
+
const selects = [];
|
|
3109
|
+
const reading = content._readingState;
|
|
3110
|
+
for (const [index, label] of ["左侧执行", "右侧执行"].entries()) {
|
|
3111
|
+
const select = el("select"); select.setAttribute("aria-label", label);
|
|
3112
|
+
for (const option of options) { const item = el("option", null, option.label); item.value = option.key; select.appendChild(item); }
|
|
3113
|
+
const previous = reading?.comparisonKeys?.[index];
|
|
3114
|
+
select.value = options.some((option) => option.key === previous) ? previous : options[Math.max(0, options.length - 2 + index)].key;
|
|
3115
|
+
toolbar.appendChild(select); selects.push(select);
|
|
3116
|
+
}
|
|
3117
|
+
content.appendChild(toolbar); content.appendChild(body);
|
|
3118
|
+
let generation = 0;
|
|
3119
|
+
const render = async () => {
|
|
3120
|
+
if (reading) reading.comparisonKeys = selects.map((select) => select.value);
|
|
3121
|
+
const request = ++generation; body.textContent = "正在读取各次执行记录…";
|
|
3122
|
+
const records = await Promise.all(selects.map((select) => recordedAttempt(dagRunId, node, select.value)));
|
|
3123
|
+
if (request !== generation || !body.isConnected) return;
|
|
3124
|
+
clearNode(body);
|
|
3125
|
+
for (const record of records) {
|
|
3126
|
+
const column = el("section", "attempt-comparison-column");
|
|
3127
|
+
column.appendChild(el("h4", null, options.find((item) => item.key === record.key)?.label));
|
|
3128
|
+
column.appendChild(transparencyMetaList([["状态", record.preview?.status ?? "未记录"], ["失败分类", record.preview?.failureCategory ?? "未记录"], ["耗时", record.preview?.durationMs != null ? formatMs(record.preview.durationMs) : "未记录"], ["模型", record.model ?? "未记录"]]));
|
|
3129
|
+
column.appendChild(el("p", "muted", record.output?.truncated ? "本次输出已截断" : "本次已保存的输出"));
|
|
3130
|
+
column.appendChild(renderInspectorDocument(record.output?.assistantText || record.output?.stdout || "未记录正文"));
|
|
3131
|
+
if (record.output?.stderr) column.appendChild(el("pre", "node-input-error", record.output.stderr));
|
|
3132
|
+
body.appendChild(column);
|
|
3133
|
+
}
|
|
3134
|
+
const left = records[0].output, right = records[1].output;
|
|
3135
|
+
const summary = el("p", "muted", left && right ? (JSON.stringify([left.assistantText, left.stdout, left.stderr]) === JSON.stringify([right.assistantText, right.stdout, right.stderr]) ? "两次已保存的输出相同。" : "两次已保存的输出有差异,可逐列对照;模型、耗时仅使用各次记录。") : "部分执行缺少正文,无法判断完整差异。");
|
|
3136
|
+
body.insertBefore(summary, body.firstChild);
|
|
3137
|
+
};
|
|
3138
|
+
for (const select of selects) select.addEventListener("change", () => { void render(); });
|
|
3139
|
+
await render();
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
function inspectorNodeLink(dagRunId, nodeId, label = nodeId) {
|
|
3143
|
+
const link = el("a", "object-link", label);
|
|
3144
|
+
link.href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(dagRunId)}?node=${encodeURIComponent(nodeId)}&tab=overview`);
|
|
3145
|
+
return link;
|
|
3146
|
+
}
|
|
3147
|
+
function appendNodeLinks(parent, title, dagRunId, nodeIds) {
|
|
3148
|
+
if (!nodeIds.length) return;
|
|
3149
|
+
const section = el("div", "node-relation-links"); section.appendChild(el("h5", null, title));
|
|
3150
|
+
for (const id of nodeIds) section.appendChild(inspectorNodeLink(dagRunId, id));
|
|
3151
|
+
parent.appendChild(section);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/** 长校验码截断显示:中段省略,完整值保留在 title(人工可读优先)。 */
|
|
3155
|
+
function shortHashDisplay(value) {
|
|
3156
|
+
const raw = String(value ?? "");
|
|
3157
|
+
if (raw.length <= 24) return raw;
|
|
3158
|
+
return `${raw.slice(0, 10)}…${raw.slice(-6)}`;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
2846
3161
|
function artifactInventorySection(data, dagRunId, nodeId) {
|
|
2847
3162
|
const root = el("section", "node-input-section node-artifact-inventory");
|
|
2848
3163
|
root.appendChild(el("h4", null, "本节点生成和使用的文件"));
|
|
3164
|
+
root.appendChild(el("p", "muted", "文件关系属于整个节点;下方执行输出随尝试切换。"));
|
|
2849
3165
|
if (!data?.binding?.available) {
|
|
2850
3166
|
const warning = el("div", "node-transparency-omission");
|
|
2851
3167
|
const bindingCode =
|
|
@@ -2864,11 +3180,19 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2864
3180
|
root.appendChild(warning);
|
|
2865
3181
|
return root;
|
|
2866
3182
|
}
|
|
3183
|
+
const graphSha = data.binding.artifactGraphSha256;
|
|
3184
|
+
const graphShaCell = graphSha
|
|
3185
|
+
? (() => {
|
|
3186
|
+
const code = el("code", "artifact-hash-code", shortHashDisplay(graphSha));
|
|
3187
|
+
code.setAttribute("title", `完整校验码:${graphSha}`);
|
|
3188
|
+
return code;
|
|
3189
|
+
})()
|
|
3190
|
+
: null;
|
|
2867
3191
|
root.appendChild(
|
|
2868
3192
|
transparencyMetaList([
|
|
2869
3193
|
["origin", data.origin?.kind ?? "unknown"],
|
|
2870
3194
|
["parent", data.origin?.parentRunId ?? null],
|
|
2871
|
-
["graph sha256",
|
|
3195
|
+
["graph sha256", graphShaCell],
|
|
2872
3196
|
]),
|
|
2873
3197
|
);
|
|
2874
3198
|
for (const artifact of data.produces ?? []) {
|
|
@@ -2894,11 +3218,7 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2894
3218
|
],
|
|
2895
3219
|
]),
|
|
2896
3220
|
);
|
|
2897
|
-
|
|
2898
|
-
card,
|
|
2899
|
-
"会直接使用此文件的节点",
|
|
2900
|
-
artifact.directConsumerNodeIds ?? [],
|
|
2901
|
-
);
|
|
3221
|
+
appendNodeLinks(card, "会直接使用此文件的节点", dagRunId, artifact.directConsumerNodeIds ?? []);
|
|
2902
3222
|
if (artifact.materialization?.status === "materialized") {
|
|
2903
3223
|
card.appendChild(
|
|
2904
3224
|
createArtifactContentPreview(dagRunId, nodeId, () => artifact, {
|
|
@@ -2913,7 +3233,7 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2913
3233
|
card.appendChild(el("h5", null, `使用 ${artifact.artifactId}`));
|
|
2914
3234
|
card.appendChild(
|
|
2915
3235
|
transparencyMetaList([
|
|
2916
|
-
["producer", artifact.producerNodeId],
|
|
3236
|
+
["producer", inspectorNodeLink(dagRunId, artifact.producerNodeId)],
|
|
2917
3237
|
["path", artifact.path],
|
|
2918
3238
|
["required", artifact.required ? "yes" : "no"],
|
|
2919
3239
|
]),
|
|
@@ -2943,6 +3263,23 @@ async function prependArtifactInventory(content, dagRunId, node) {
|
|
|
2943
3263
|
activeInspectorTab() !== "output"
|
|
2944
3264
|
)
|
|
2945
3265
|
return;
|
|
3266
|
+
// 失败节点优先暴露错误:没有登记任何文件关系时空卡不占首屏,
|
|
3267
|
+
// 「没有登记」这一事实由错误输出之后的一行说明保留。
|
|
3268
|
+
const body = result.ok ? result.body : null;
|
|
3269
|
+
const emptyInventory =
|
|
3270
|
+
body?.binding?.available &&
|
|
3271
|
+
(body.produces ?? []).length === 0 &&
|
|
3272
|
+
(body.consumes ?? []).length === 0;
|
|
3273
|
+
if (isNodeFailed(node?.status) && emptyInventory) {
|
|
3274
|
+
const note = el(
|
|
3275
|
+
"p",
|
|
3276
|
+
"empty node-artifact-inventory-empty",
|
|
3277
|
+
"该节点没有登记会生成或使用的文件。",
|
|
3278
|
+
);
|
|
3279
|
+
const lead = content.querySelector(".inspector-page-lead");
|
|
3280
|
+
content.insertBefore(note, lead?.nextSibling ?? content.firstChild);
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
2946
3283
|
const section =
|
|
2947
3284
|
result.ok && result.body
|
|
2948
3285
|
? artifactInventorySection(result.body, dagRunId, node.nodeId)
|
|
@@ -2979,15 +3316,15 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2979
3316
|
root.appendChild(
|
|
2980
3317
|
transparencyMetaList([
|
|
2981
3318
|
["运行类型", data.kind === "root" ? "首次运行" : "派生运行"],
|
|
2982
|
-
["来源运行", data.parentRunId
|
|
3319
|
+
["来源运行", data.parentRunId ? inspectorNodeLink(data.parentRunId, data.operatorSelectedNodeId || node.nodeId, data.parentRunId) : null],
|
|
2983
3320
|
["发起方式", inspectorValueLabel("发起方式", data.trigger)],
|
|
2984
3321
|
["人工选择节点", data.operatorSelectedNodeId ?? null],
|
|
2985
3322
|
["从此节点重新执行", data.effectiveFromNodeId ?? null],
|
|
2986
3323
|
["重跑计划哈希", data.planHash ?? null],
|
|
2987
3324
|
]),
|
|
2988
3325
|
);
|
|
2989
|
-
|
|
2990
|
-
|
|
3326
|
+
appendNodeLinks(root, "沿用既有结果的节点", dagRunId, data.importedNodeIds ?? []);
|
|
3327
|
+
appendNodeLinks(root, "替换结果的节点", dagRunId, data.overriddenNodeIds ?? []);
|
|
2991
3328
|
if (data.kind === "root")
|
|
2992
3329
|
root.appendChild(
|
|
2993
3330
|
el("p", "muted", "这是首次运行,没有来源运行。"),
|
|
@@ -2996,19 +3333,37 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2996
3333
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "lineage");
|
|
2997
3334
|
}
|
|
2998
3335
|
|
|
3336
|
+
function inspectorTabDefinitions(node = inspectorSelection.node) {
|
|
3337
|
+
const options = node ? attemptSelectorOptions(node) : [];
|
|
3338
|
+
const hasPassOnlyHistory = options[0]?.key.startsWith("pass-");
|
|
3339
|
+
const tabDefs = DAG_INSPECTOR_READ_ONLY_TABS.filter(([tab]) => {
|
|
3340
|
+
if (tab === "context") return !["shell", "static"].includes(node?.executor) && !hasPassOnlyHistory;
|
|
3341
|
+
if (tab === "timeline") return node?.executor !== "static";
|
|
3342
|
+
return true;
|
|
3343
|
+
}).map((definition) => [...definition]);
|
|
3344
|
+
if (options.length >= 2) {
|
|
3345
|
+
tabDefs.splice(tabDefs.findIndex(([tab]) => tab === "spec-evidence"), 0, ["compare", "尝试对比", "ri-layout-column-line"]);
|
|
3346
|
+
}
|
|
3347
|
+
// A scoped credential controls the mutation surface independently of data.
|
|
3348
|
+
if (globalThis.__INSPECT_MUTATION_TOKEN__) tabDefs.push([...DAG_INSPECTOR_MUTATION_TAB]);
|
|
3349
|
+
return tabDefs;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
2999
3352
|
function activeInspectorTab() {
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3353
|
+
const available = inspectorTabDefinitions().map(([tab]) => tab);
|
|
3354
|
+
const requested = uiState.dagInspectorTab;
|
|
3355
|
+
if (available.includes(requested)) return requested;
|
|
3356
|
+
if (requested === "compare" && available.includes("timeline")) return "timeline";
|
|
3357
|
+
return ["compare", "context", "timeline"].includes(requested) ? "overview" : "output";
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
function inspectorTabUsesAttempt(tab, node) {
|
|
3361
|
+
return tab === "timeline" || tab === "output" ||
|
|
3362
|
+
(tab === "context" && (node.executionSummary?.attempts?.length ?? 0) > 1);
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
function inspectorTabExecutionKey(dagRunId, node, tab = activeInspectorTab()) {
|
|
3366
|
+
return inspectorTabUsesAttempt(tab, node) ? effectiveExecutionKey(dagRunId, node) : null;
|
|
3012
3367
|
}
|
|
3013
3368
|
|
|
3014
3369
|
function saveInspectorContentScroll(
|
|
@@ -3019,7 +3374,14 @@ function saveInspectorContentScroll(
|
|
|
3019
3374
|
options = {},
|
|
3020
3375
|
) {
|
|
3021
3376
|
if (!content) return null;
|
|
3022
|
-
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab);
|
|
3377
|
+
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab, content.dataset.executionKey);
|
|
3378
|
+
if (content._readingState) {
|
|
3379
|
+
const reading = content._readingState;
|
|
3380
|
+
if (!(options.preserveIntent && content.scrollTop === 0 && reading.scrollTop > 0)) {
|
|
3381
|
+
reading.scrollTop = content.scrollTop;
|
|
3382
|
+
reading.scrollLeft = content.scrollLeft;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3023
3385
|
const saved = saveDagInspectorViewportState(
|
|
3024
3386
|
regionKey,
|
|
3025
3387
|
content.scrollLeft,
|
|
@@ -3055,7 +3417,8 @@ function saveInspectorContentScroll(
|
|
|
3055
3417
|
|
|
3056
3418
|
function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
3057
3419
|
if (!content) return;
|
|
3058
|
-
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab);
|
|
3420
|
+
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab, content.dataset.executionKey);
|
|
3421
|
+
if (tab === "output" && !content.querySelector(".process-timeline-autoscroll-dock")) content.insertBefore(createTimelineAutoScrollControl(content), content.firstChild);
|
|
3059
3422
|
const apply = () => {
|
|
3060
3423
|
if (
|
|
3061
3424
|
!content.isConnected ||
|
|
@@ -3065,19 +3428,14 @@ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
|
3065
3428
|
) {
|
|
3066
3429
|
return;
|
|
3067
3430
|
}
|
|
3068
|
-
if (tab === "timeline"
|
|
3431
|
+
if ((tab === "timeline" || tab === "output") && content._readingState?.followLatest) {
|
|
3069
3432
|
content.scrollLeft = uiState.dagTimelineViewportState?.scrollLeft ?? 0;
|
|
3070
3433
|
content.scrollTop = content.scrollHeight;
|
|
3434
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3435
|
+
content._syncTimelineFollow?.();
|
|
3071
3436
|
return;
|
|
3072
3437
|
}
|
|
3073
|
-
|
|
3074
|
-
const saved = uiState.dagNodeOutputViewportState;
|
|
3075
|
-
if (saved?.regionKey === regionKey && saved.followLatest) {
|
|
3076
|
-
content.scrollTop = content.scrollHeight;
|
|
3077
|
-
content.scrollLeft = saved.scrollLeft ?? 0;
|
|
3078
|
-
return;
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3438
|
+
|
|
3081
3439
|
const saved =
|
|
3082
3440
|
getDagInspectorViewportState(regionKey) ??
|
|
3083
3441
|
(tab === "input" &&
|
|
@@ -3092,8 +3450,10 @@ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
|
3092
3450
|
uiState.dagNodeSpecEvidenceViewportState?.regionKey === regionKey
|
|
3093
3451
|
? uiState.dagNodeSpecEvidenceViewportState
|
|
3094
3452
|
: null);
|
|
3095
|
-
content.scrollLeft = saved?.scrollLeft ?? 0;
|
|
3096
|
-
content.scrollTop = saved?.scrollTop ?? 0;
|
|
3453
|
+
content.scrollLeft = content._readingState?.scrollLeft ?? saved?.scrollLeft ?? 0;
|
|
3454
|
+
content.scrollTop = content._readingState?.scrollTop ?? saved?.scrollTop ?? 0;
|
|
3455
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3456
|
+
content._syncTimelineFollow?.();
|
|
3097
3457
|
};
|
|
3098
3458
|
apply();
|
|
3099
3459
|
if (typeof requestAnimationFrame === "function") {
|
|
@@ -3109,7 +3469,13 @@ function ensureInspectorContentScrollCapture(content, dagRunId, nodeId, tab) {
|
|
|
3109
3469
|
() => {
|
|
3110
3470
|
const runId = content.dataset.dagRunId;
|
|
3111
3471
|
const nId = content.dataset.dagNodeId;
|
|
3112
|
-
const active =
|
|
3472
|
+
const active = content.dataset.inspectorTab;
|
|
3473
|
+
if (["timeline", "output"].includes(active) && content._readingState && content.scrollTop < (content._lastRestoredTop ?? content._readingState.scrollTop) - 1 && content.scrollHeight - content.clientHeight - content.scrollTop > 48) {
|
|
3474
|
+
content._readingState.followLatest = false;
|
|
3475
|
+
uiState.dagTimelineAutoScrollEnabled = false;
|
|
3476
|
+
}
|
|
3477
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3478
|
+
content._syncTimelineFollow?.();
|
|
3113
3479
|
if (!runId || !nId || !active) return;
|
|
3114
3480
|
saveInspectorContentScroll(content, runId, nId, active, {
|
|
3115
3481
|
// Clearing a tall input pane for its loading shell can emit a native
|
|
@@ -3217,6 +3583,21 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3217
3583
|
|
|
3218
3584
|
const bar = el("div", "dag-inspector-attempt-bar");
|
|
3219
3585
|
bar.appendChild(el("label", "dag-inspector-attempt-label", "执行分次"));
|
|
3586
|
+
const choose = (nextKey, source = "select") => {
|
|
3587
|
+
uiState.dagInspectorAttempt = { dagRunId, nodeId: node.nodeId, key: String(nextKey) };
|
|
3588
|
+
inspectorNodeSelections.set(`${dagRunId}:${node.nodeId}`, uiState.dagInspectorAttempt);
|
|
3589
|
+
renderDagInspectorForCurrent();
|
|
3590
|
+
const nextBar = document.querySelector(".dag-inspector-header .dag-inspector-attempt-bar");
|
|
3591
|
+
const control = nextBar?.querySelector(`[data-attempt-action="${source}"]`);
|
|
3592
|
+
(control && !control.disabled ? control : nextBar?.querySelector(".custom-select-trigger"))?.focus({ preventScroll: true });
|
|
3593
|
+
};
|
|
3594
|
+
const selectedIndex = options.findIndex((option) => option.key === selectedKey);
|
|
3595
|
+
const previous = iconButton("上一次尝试", "ri-arrow-left-s-line");
|
|
3596
|
+
previous.type = "button";
|
|
3597
|
+
previous.dataset.attemptAction = "previous";
|
|
3598
|
+
previous.disabled = selectedIndex <= 0;
|
|
3599
|
+
previous.addEventListener("click", () => choose(options[selectedIndex - 1].key, "previous"));
|
|
3600
|
+
bar.appendChild(previous);
|
|
3220
3601
|
const select = createCustomSelect({
|
|
3221
3602
|
ariaLabel: "选择执行分次",
|
|
3222
3603
|
className: "dag-inspector-attempt-select",
|
|
@@ -3225,25 +3606,25 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3225
3606
|
value: option.key,
|
|
3226
3607
|
label: option.label,
|
|
3227
3608
|
})),
|
|
3228
|
-
onChange: (nextKey) =>
|
|
3229
|
-
uiState.dagInspectorAttempt = {
|
|
3230
|
-
dagRunId,
|
|
3231
|
-
nodeId: node.nodeId,
|
|
3232
|
-
key: String(nextKey),
|
|
3233
|
-
};
|
|
3234
|
-
renderDagInspectorForCurrent();
|
|
3235
|
-
},
|
|
3609
|
+
onChange: (nextKey) => choose(nextKey),
|
|
3236
3610
|
});
|
|
3237
3611
|
// Keep raw key on the root for existing integration assertions that read
|
|
3238
3612
|
// `.value` after assign + change (StubNode handler contract).
|
|
3239
3613
|
select.value = selectedKey;
|
|
3240
3614
|
bar.appendChild(select);
|
|
3615
|
+
const next = iconButton("下一次尝试", "ri-arrow-right-s-line");
|
|
3616
|
+
next.type = "button";
|
|
3617
|
+
next.dataset.attemptAction = "next";
|
|
3618
|
+
next.disabled = selectedIndex >= options.length - 1;
|
|
3619
|
+
next.addEventListener("click", () => choose(options[selectedIndex + 1].key, "next"));
|
|
3620
|
+
bar.appendChild(next);
|
|
3241
3621
|
|
|
3242
3622
|
const selected =
|
|
3243
3623
|
options.find((option) => option.key === selectedKey) ??
|
|
3244
3624
|
options[options.length - 1];
|
|
3245
3625
|
const data = selected.preview ?? {};
|
|
3246
3626
|
const preview = el("div", "dag-inspector-attempt-preview");
|
|
3627
|
+
preview.setAttribute("title", [data.failureCategory ? attemptFailureLabel(data.failureCategory) : "", data.durationMs != null ? `耗时 ${formatMs(data.durationMs)}` : ""].filter(Boolean).join(" · "));
|
|
3247
3628
|
const status = attemptPreviewStatus(data.status);
|
|
3248
3629
|
if (status) {
|
|
3249
3630
|
preview.appendChild(el("span", `badge badge-${status.cls}`, status.label));
|
|
@@ -3272,6 +3653,19 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3272
3653
|
// Historical bodies load via execution-output endpoint; do not surface
|
|
3273
3654
|
// raw artifact paths as the primary path for full text (AC-003/004).
|
|
3274
3655
|
bar.appendChild(preview);
|
|
3656
|
+
const diagnostic = iconButton("诊断摘要", "ri-file-info-line"); diagnostic.type = "button";
|
|
3657
|
+
diagnostic.addEventListener("click", async () => {
|
|
3658
|
+
diagnostic.disabled = true;
|
|
3659
|
+
try {
|
|
3660
|
+
const record = await recordedAttempt(dagRunId, node, selectedKey);
|
|
3661
|
+
const panel = document.querySelector(".dag-inspector-content");
|
|
3662
|
+
if (!panel || panel.dataset.dagNodeId !== node.nodeId || panel.dataset.executionKey !== selectedKey) return;
|
|
3663
|
+
panel.querySelector(".diagnostic-preview")?.remove();
|
|
3664
|
+
const detail = el("details", "diagnostic-preview"); detail.open = true; detail.appendChild(el("summary", null, "诊断摘要预览(仅当前尝试)"));
|
|
3665
|
+
const text = attemptDiagnostic(dagRunId, node, record); detail.appendChild(el("pre", "process-timeline-code", text));
|
|
3666
|
+
const copy = el("button", "btn btn-secondary", "复制摘要"); copy.type = "button"; copy.addEventListener("click", async () => { copy.textContent = await copyText(text) ? "已复制" : "复制失败"; }); detail.appendChild(copy); panel.insertBefore(detail, panel.firstChild); panel.scrollTop = 0; if (panel._readingState) panel._readingState.followLatest = false;
|
|
3667
|
+
} finally { diagnostic.disabled = false; }
|
|
3668
|
+
}); bar.appendChild(diagnostic);
|
|
3275
3669
|
return { bar, selectedKey, latestKey, options };
|
|
3276
3670
|
}
|
|
3277
3671
|
|
|
@@ -3341,11 +3735,22 @@ function composeExecutionErrorText(payload) {
|
|
|
3341
3735
|
return typeof payload.stderr === "string" ? payload.stderr : "";
|
|
3342
3736
|
}
|
|
3343
3737
|
|
|
3738
|
+
function buildExecutionErrorSection(errorText) {
|
|
3739
|
+
const error = el("section", "markdown-error");
|
|
3740
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3741
|
+
error.appendChild(renderInspectorDocument(errorText));
|
|
3742
|
+
return error;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3344
3745
|
function appendOutputBody(content, outputText, errorText, options = null) {
|
|
3345
3746
|
if (!outputText && !errorText) {
|
|
3346
3747
|
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
3347
3748
|
return;
|
|
3348
3749
|
}
|
|
3750
|
+
// 失败节点错误优先:先看错误,再对照正文(正文可能很长)。
|
|
3751
|
+
if (options?.priorityError === true && errorText) {
|
|
3752
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3753
|
+
}
|
|
3349
3754
|
if (outputText) content.appendChild(renderInspectorDocument(outputText));
|
|
3350
3755
|
// Truncation note only for full/cached execution-output bodies (AC-003);
|
|
3351
3756
|
// preview path never invents truncated/maxBytes.
|
|
@@ -3359,15 +3764,13 @@ function appendOutputBody(content, outputText, errorText, options = null) {
|
|
|
3359
3764
|
),
|
|
3360
3765
|
);
|
|
3361
3766
|
}
|
|
3362
|
-
if (errorText) {
|
|
3363
|
-
|
|
3364
|
-
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3365
|
-
error.appendChild(renderInspectorDocument(errorText));
|
|
3366
|
-
content.appendChild(error);
|
|
3767
|
+
if (errorText && options?.priorityError !== true) {
|
|
3768
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3367
3769
|
}
|
|
3368
3770
|
}
|
|
3369
3771
|
|
|
3370
3772
|
function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
3773
|
+
const priorityError = isNodeFailed(node?.status);
|
|
3371
3774
|
const cached = key
|
|
3372
3775
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, key)
|
|
3373
3776
|
: null;
|
|
@@ -3379,6 +3782,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3379
3782
|
{
|
|
3380
3783
|
truncated: cached.body.truncated === true,
|
|
3381
3784
|
maxBytes: cached.body.maxBytes,
|
|
3785
|
+
priorityError,
|
|
3382
3786
|
},
|
|
3383
3787
|
);
|
|
3384
3788
|
return "cached";
|
|
@@ -3388,6 +3792,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3388
3792
|
content,
|
|
3389
3793
|
node.outputPreview ?? "",
|
|
3390
3794
|
node.errorPreview ?? "",
|
|
3795
|
+
{ priorityError },
|
|
3391
3796
|
);
|
|
3392
3797
|
return "preview";
|
|
3393
3798
|
}
|
|
@@ -3653,39 +4058,7 @@ function renderNonPiProcess(content, body) {
|
|
|
3653
4058
|
ul.appendChild(li);
|
|
3654
4059
|
}
|
|
3655
4060
|
content.appendChild(ul);
|
|
3656
|
-
|
|
3657
|
-
}
|
|
3658
|
-
|
|
3659
|
-
function downloadTextFile(filename, text) {
|
|
3660
|
-
const blob = new Blob([text], { type: "text/html;charset=utf-8" });
|
|
3661
|
-
const url = URL.createObjectURL(blob);
|
|
3662
|
-
const a = document.createElement("a");
|
|
3663
|
-
a.href = url;
|
|
3664
|
-
a.download = filename;
|
|
3665
|
-
document.body.appendChild(a);
|
|
3666
|
-
a.click();
|
|
3667
|
-
a.remove();
|
|
3668
|
-
URL.revokeObjectURL(url);
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
function appendProcessExportButton(content, body, meta) {
|
|
3672
|
-
if (!body || ((body.commands ?? []).length === 0 && (body.steps ?? []).length === 0)) return;
|
|
3673
|
-
const toolbar = el("div", "process-timeline-export-bar");
|
|
3674
|
-
const btn = el("button", "btn btn-secondary", "导出 HTML");
|
|
3675
|
-
btn.type = "button";
|
|
3676
|
-
btn.addEventListener("click", async () => {
|
|
3677
|
-
const cssText = await fetchPageStylesheetText();
|
|
3678
|
-
// 复用页面真实渲染 DOM,保证导出排版与页面一致。
|
|
3679
|
-
const clone = content.cloneNode(true);
|
|
3680
|
-
const bar = clone.querySelector(".process-timeline-export-bar");
|
|
3681
|
-
if (bar) bar.remove();
|
|
3682
|
-
downloadTextFile(
|
|
3683
|
-
`node-process-${meta?.nodeId || "node"}-${Date.now()}.html`,
|
|
3684
|
-
buildPageStyledDoc(clone.innerHTML, meta ?? {}, cssText),
|
|
3685
|
-
);
|
|
3686
|
-
});
|
|
3687
|
-
toolbar.appendChild(btn);
|
|
3688
|
-
content.insertBefore(toolbar, content.firstChild);
|
|
4061
|
+
content.appendChild(createTimelineAutoScrollControl(content));
|
|
3689
4062
|
}
|
|
3690
4063
|
|
|
3691
4064
|
export async function mergeNodeProcess(dagRunId, node) {
|
|
@@ -3736,6 +4109,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3736
4109
|
if (sameNode && cached.kind === "pi-session") {
|
|
3737
4110
|
renderSessionTimeline(content, node.nodeId, {
|
|
3738
4111
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4112
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3739
4113
|
});
|
|
3740
4114
|
return;
|
|
3741
4115
|
}
|
|
@@ -3746,6 +4120,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3746
4120
|
if ((node.executor ?? "pi") === "pi") {
|
|
3747
4121
|
renderSessionTimeline(content, node.nodeId, {
|
|
3748
4122
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4123
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3749
4124
|
});
|
|
3750
4125
|
return;
|
|
3751
4126
|
}
|
|
@@ -3803,6 +4178,10 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3803
4178
|
content.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3804
4179
|
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
3805
4180
|
content.dataset.inspectorTab = activeTab;
|
|
4181
|
+
content.dataset.executionKey = inspectorTabExecutionKey(dagRunId, node, activeTab) ?? "";
|
|
4182
|
+
content._readingState = inspectorReadingState(dagRunId, node.nodeId, activeTab, content.dataset.executionKey, isLatestExecutionSelection(dagRunId, node) && ["running", "pending"].includes(String(node.status ?? "").toLowerCase()));
|
|
4183
|
+
uiState.dagTimelineAutoScrollEnabled = content._readingState.followLatest;
|
|
4184
|
+
uiState.dagTimelineViewportState = { ...content._readingState, dagRunId, nodeId: node.nodeId };
|
|
3806
4185
|
// Drop timeline-only classes when switching tabs so output layout stays clean.
|
|
3807
4186
|
content.className = "dag-inspector-content";
|
|
3808
4187
|
if (activeTab !== "prompt-restart") {
|
|
@@ -3814,7 +4193,9 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3814
4193
|
node.nodeId,
|
|
3815
4194
|
activeTab,
|
|
3816
4195
|
);
|
|
3817
|
-
if (activeTab === "
|
|
4196
|
+
if (activeTab === "compare") {
|
|
4197
|
+
void renderAttemptComparison(content, dagRunId, node);
|
|
4198
|
+
} else if (activeTab === "overview") {
|
|
3818
4199
|
renderNodeOverview(content, dagRunId, node);
|
|
3819
4200
|
} else if (activeTab === "context") {
|
|
3820
4201
|
void renderEffectiveContext(content, dagRunId, node);
|
|
@@ -3827,8 +4208,7 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3827
4208
|
void renderPromptRestart(content, dagRunId, node, run);
|
|
3828
4209
|
} else if (activeTab === "timeline") {
|
|
3829
4210
|
ensureInspectorPageLead(content, "timeline");
|
|
3830
|
-
|
|
3831
|
-
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
4211
|
+
|
|
3832
4212
|
renderExecutorAwareTimeline(content, dagRunId, node);
|
|
3833
4213
|
// Initial content is rendered before it is attached. Restore again on the
|
|
3834
4214
|
// connected animation frame so live-follow sees the final scroll height.
|
|
@@ -3838,8 +4218,7 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3838
4218
|
} else {
|
|
3839
4219
|
// output tab: attempt bar + latest preview immediate / historical async body.
|
|
3840
4220
|
ensureInspectorPageLead(content, "output");
|
|
3841
|
-
|
|
3842
|
-
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
4221
|
+
|
|
3843
4222
|
const key = effectiveExecutionKey(dagRunId, node);
|
|
3844
4223
|
const latest = isLatestExecutionSelection(dagRunId, node);
|
|
3845
4224
|
const rendered = renderCachedOrPreviewBody(
|
|
@@ -3873,46 +4252,49 @@ function inspectorNodePurpose(node) {
|
|
|
3873
4252
|
return describeDagNodePurpose(node?.nodeId, node);
|
|
3874
4253
|
}
|
|
3875
4254
|
|
|
4255
|
+
/** 纯函数:Tab 条两端是否有被裁切内容(驱动渐隐提示)。 */
|
|
4256
|
+
export function inspectorTabOverflowState({ scrollLeft = 0, clientWidth = 0, scrollWidth = 0 } = {}) {
|
|
4257
|
+
const maxScroll = Math.max(0, scrollWidth - clientWidth);
|
|
4258
|
+
return {
|
|
4259
|
+
start: scrollLeft > 1 && maxScroll > 0,
|
|
4260
|
+
end: scrollLeft < maxScroll - 1 && maxScroll > 0,
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
function syncInspectorTabOverflowClasses(tabs) {
|
|
4265
|
+
if (!tabs?.classList) return;
|
|
4266
|
+
const state = inspectorTabOverflowState({
|
|
4267
|
+
scrollLeft: tabs.scrollLeft ?? 0,
|
|
4268
|
+
clientWidth: tabs.clientWidth ?? 0,
|
|
4269
|
+
scrollWidth: tabs.scrollWidth ?? 0,
|
|
4270
|
+
});
|
|
4271
|
+
tabs.classList.toggle("is-overflow-start", state.start);
|
|
4272
|
+
tabs.classList.toggle("is-overflow-end", state.end);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
/** 选中 Tab 保持可见:只滚动 Tab 条自身,不牵动外层页面。 */
|
|
4276
|
+
function revealInspectorTab(tabs, button) {
|
|
4277
|
+
if (!tabs || !button) return;
|
|
4278
|
+
const left = button.offsetLeft;
|
|
4279
|
+
const right = left + (button.offsetWidth ?? 0);
|
|
4280
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return;
|
|
4281
|
+
if (left < tabs.scrollLeft + 8) tabs.scrollLeft = Math.max(0, left - 8);
|
|
4282
|
+
else if (right > tabs.scrollLeft + tabs.clientWidth - 8) {
|
|
4283
|
+
tabs.scrollLeft = right - tabs.clientWidth + 8;
|
|
4284
|
+
}
|
|
4285
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4286
|
+
}
|
|
4287
|
+
|
|
3876
4288
|
function appendInspectorPurpose(title, node) {
|
|
3877
4289
|
const purpose = inspectorNodePurpose(node);
|
|
3878
4290
|
if (!purpose) return;
|
|
3879
4291
|
title.appendChild(el("p", "dag-inspector-purpose", purpose));
|
|
3880
4292
|
}
|
|
3881
4293
|
|
|
3882
|
-
function
|
|
3883
|
-
const
|
|
3884
|
-
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
3885
|
-
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3886
|
-
const title = el("div", "dag-inspector-title");
|
|
3887
|
-
title.appendChild(el("h3", null, "节点检查器"));
|
|
3888
|
-
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
3889
|
-
appendInspectorPurpose(title, node);
|
|
3890
|
-
header.appendChild(title);
|
|
3891
|
-
const controls = el("div", "dag-inspector-controls");
|
|
3892
|
-
const status = badge(node.status ?? "unknown");
|
|
3893
|
-
status.classList.add("dag-inspector-status");
|
|
3894
|
-
controls.appendChild(status);
|
|
3895
|
-
const close = document.createElement("button");
|
|
3896
|
-
close.type = "button";
|
|
3897
|
-
close.className = "dag-inspector-close";
|
|
3898
|
-
close.setAttribute("aria-label", "关闭节点检查器");
|
|
3899
|
-
const closeIcon = el("i", "ri-close-line");
|
|
3900
|
-
closeIcon.setAttribute("aria-hidden", "true");
|
|
3901
|
-
close.appendChild(closeIcon);
|
|
3902
|
-
close.addEventListener("click", closeDagInspector);
|
|
3903
|
-
controls.appendChild(close);
|
|
3904
|
-
header.appendChild(controls);
|
|
3905
|
-
|
|
3906
|
-
const tabDefs = DAG_INSPECTOR_READ_ONLY_TABS.map((definition) => [
|
|
3907
|
-
...definition,
|
|
3908
|
-
]);
|
|
3909
|
-
// Export/rerun mutation surface: the server injects a SCOPED inspect token.
|
|
3910
|
-
// The emergency kill switch suppresses the token;
|
|
3911
|
-
// no token → no tab → the inspector stays strictly read-only.
|
|
3912
|
-
if (globalThis.__INSPECT_MUTATION_TOKEN__) {
|
|
3913
|
-
tabDefs.push([...DAG_INSPECTOR_MUTATION_TAB]);
|
|
3914
|
-
}
|
|
4294
|
+
function buildDagInspectorTabs(node) {
|
|
4295
|
+
const tabDefs = inspectorTabDefinitions(node);
|
|
3915
4296
|
const tabs = el("div", "dag-inspector-tabs");
|
|
4297
|
+
tabs.dataset.tabsSignature = tabDefs.map(([tab]) => tab).join(",");
|
|
3916
4298
|
tabs.setAttribute("role", "tablist");
|
|
3917
4299
|
tabs.setAttribute("aria-label", "节点检查器面板");
|
|
3918
4300
|
for (const [tab, label, icon] of tabDefs) {
|
|
@@ -3935,6 +4317,10 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3935
4317
|
renderDagInspectorForCurrent();
|
|
3936
4318
|
const next = document.getElementById(`dag-inspector-tab-${tab}`);
|
|
3937
4319
|
next?.focus();
|
|
4320
|
+
revealInspectorTab(
|
|
4321
|
+
document.querySelector(".dag-inspector-tabs"),
|
|
4322
|
+
next,
|
|
4323
|
+
);
|
|
3938
4324
|
});
|
|
3939
4325
|
button.addEventListener("keydown", (event) => {
|
|
3940
4326
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
@@ -3947,17 +4333,106 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3947
4333
|
const nextTab = order[(idx + delta + order.length) % order.length];
|
|
3948
4334
|
uiState.dagInspectorTab = nextTab;
|
|
3949
4335
|
renderDagInspectorForCurrent();
|
|
3950
|
-
document.getElementById(`dag-inspector-tab-${nextTab}`)
|
|
4336
|
+
const next = document.getElementById(`dag-inspector-tab-${nextTab}`);
|
|
4337
|
+
next?.focus();
|
|
4338
|
+
revealInspectorTab(document.querySelector(".dag-inspector-tabs"), next);
|
|
3951
4339
|
});
|
|
3952
4340
|
tabs.appendChild(button);
|
|
3953
4341
|
}
|
|
4342
|
+
// 溢出渐隐与选中可见性:挂载后测量(构建时 offsetLeft 不可用)。
|
|
4343
|
+
tabs.addEventListener("scroll", () => syncInspectorTabOverflowClasses(tabs), { passive: true });
|
|
4344
|
+
if (typeof ResizeObserver === "function") {
|
|
4345
|
+
const overflowObserver = new ResizeObserver(() => {
|
|
4346
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4347
|
+
});
|
|
4348
|
+
overflowObserver.observe(tabs);
|
|
4349
|
+
}
|
|
4350
|
+
if (typeof requestAnimationFrame === "function") {
|
|
4351
|
+
requestAnimationFrame(() => {
|
|
4352
|
+
if (!tabs.isConnected) return;
|
|
4353
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4354
|
+
const selected = tabs.querySelector("[role='tab'][aria-selected='true']");
|
|
4355
|
+
revealInspectorTab(tabs, selected);
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
return tabs;
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
function buildDagInspectorHeader(dagRunId, node) {
|
|
4362
|
+
const header = el("div", "dag-inspector-header");
|
|
4363
|
+
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
4364
|
+
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
4365
|
+
const title = el("div", "dag-inspector-title");
|
|
4366
|
+
title.appendChild(el("h3", null, "节点检查器"));
|
|
4367
|
+
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
4368
|
+
appendInspectorPurpose(title, node);
|
|
4369
|
+
header.appendChild(title);
|
|
4370
|
+
const controls = el("div", "dag-inspector-controls");
|
|
4371
|
+
const status = badge(node.status ?? "unknown");
|
|
4372
|
+
status.classList.add("dag-inspector-status");
|
|
4373
|
+
controls.appendChild(status);
|
|
4374
|
+
const expand = iconButton(uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4375
|
+
expand.type = "button";
|
|
4376
|
+
expand.setAttribute("aria-pressed", String(Boolean(uiState.dagInspectorExpanded)));
|
|
4377
|
+
expand.addEventListener("click", () => {
|
|
4378
|
+
uiState.dagInspectorExpanded = !uiState.dagInspectorExpanded;
|
|
4379
|
+
document.getElementById("dag-inspector")?.classList.toggle("is-expanded", uiState.dagInspectorExpanded);
|
|
4380
|
+
setIconButton(expand, uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4381
|
+
expand.setAttribute("aria-pressed", String(uiState.dagInspectorExpanded));
|
|
4382
|
+
});
|
|
4383
|
+
controls.appendChild(expand);
|
|
4384
|
+
const share = iconButton("复制位置", "ri-link");
|
|
4385
|
+
share.type = "button";
|
|
4386
|
+
share.addEventListener("click", async () => {
|
|
4387
|
+
const selection = inspectorSelection;
|
|
4388
|
+
const query = new URLSearchParams({ node: selection.node.nodeId, tab: activeInspectorTab() });
|
|
4389
|
+
const key = effectiveExecutionKey(selection.dagRunId, selection.node);
|
|
4390
|
+
if (key) query.set("attempt", key);
|
|
4391
|
+
const href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(selection.dagRunId)}?${query}`);
|
|
4392
|
+
const copied = await copyText(new URL(href, location.href).href);
|
|
4393
|
+
setIconButton(share, copied ? "已复制位置" : "复制失败", copied ? "ri-check-line" : "ri-error-warning-line");
|
|
4394
|
+
});
|
|
4395
|
+
controls.appendChild(share);
|
|
4396
|
+
const close = document.createElement("button");
|
|
4397
|
+
close.type = "button";
|
|
4398
|
+
close.className = "dag-inspector-close";
|
|
4399
|
+
close.setAttribute("aria-label", "关闭节点检查器");
|
|
4400
|
+
const closeIcon = el("i", "ri-close-line");
|
|
4401
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
4402
|
+
close.appendChild(closeIcon);
|
|
4403
|
+
close.addEventListener("click", closeDagInspector);
|
|
4404
|
+
controls.appendChild(close);
|
|
4405
|
+
header.appendChild(controls);
|
|
4406
|
+
|
|
4407
|
+
const tabs = buildDagInspectorTabs(node);
|
|
3954
4408
|
header.appendChild(tabs);
|
|
4409
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4410
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4411
|
+
header.dataset.attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
3955
4412
|
return header;
|
|
3956
4413
|
}
|
|
3957
4414
|
|
|
3958
4415
|
function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
3959
4416
|
const header = panel.querySelector(".dag-inspector-header");
|
|
3960
4417
|
if (!header) return;
|
|
4418
|
+
const tabs = header.querySelector(".dag-inspector-tabs");
|
|
4419
|
+
const tabsSignature = inspectorTabDefinitions(node).map(([tab]) => tab).join(",");
|
|
4420
|
+
if (tabs && tabs.dataset.tabsSignature !== tabsSignature) {
|
|
4421
|
+
const restoreFocus = document.activeElement?.getAttribute("role") === "tab" && tabs.contains(document.activeElement);
|
|
4422
|
+
const nextTabs = buildDagInspectorTabs(node);
|
|
4423
|
+
const scrollLeft = tabs.scrollLeft;
|
|
4424
|
+
tabs.replaceWith(nextTabs);
|
|
4425
|
+
nextTabs.scrollLeft = scrollLeft;
|
|
4426
|
+
syncInspectorTabOverflowClasses(nextTabs);
|
|
4427
|
+
if (restoreFocus) document.getElementById(`dag-inspector-tab-${activeInspectorTab()}`)?.focus();
|
|
4428
|
+
}
|
|
4429
|
+
const attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
4430
|
+
if (header.dataset.dagNodeId !== String(node.nodeId) || header.dataset.attemptSignature !== attemptSignature) {
|
|
4431
|
+
header.querySelector(".dag-inspector-attempt-bar")?.remove();
|
|
4432
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4433
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4434
|
+
header.dataset.attemptSignature = attemptSignature;
|
|
4435
|
+
}
|
|
3961
4436
|
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
3962
4437
|
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3963
4438
|
const nodeLabel = header.querySelector(".dag-inspector-node");
|
|
@@ -3987,7 +4462,7 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
|
3987
4462
|
|
|
3988
4463
|
function dagInspectorSignature(dagRunId, node) {
|
|
3989
4464
|
const tab = activeInspectorTab();
|
|
3990
|
-
const execKey =
|
|
4465
|
+
const execKey = inspectorTabExecutionKey(dagRunId, node, tab);
|
|
3991
4466
|
const cachedBody = execKey
|
|
3992
4467
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, execKey)
|
|
3993
4468
|
: null;
|
|
@@ -4024,11 +4499,11 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
4024
4499
|
// the DOM while selection/history are unchanged; switching attempt or a
|
|
4025
4500
|
// changed attempt count rebuilds the selector.
|
|
4026
4501
|
const summary = node.executionSummary;
|
|
4027
|
-
const attemptRevision = [
|
|
4502
|
+
const attemptRevision = tab === "compare" ? attemptSelectorOptions(node) : inspectorTabUsesAttempt(tab, node) ? [
|
|
4028
4503
|
summary?.totalAttemptCount ?? 0,
|
|
4029
4504
|
(node.passes ?? []).join(","),
|
|
4030
4505
|
execKey ?? "latest",
|
|
4031
|
-
];
|
|
4506
|
+
] : null;
|
|
4032
4507
|
return JSON.stringify([
|
|
4033
4508
|
dagRunId,
|
|
4034
4509
|
node.nodeId,
|
|
@@ -4047,6 +4522,8 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4047
4522
|
inspectorSelection.run = run;
|
|
4048
4523
|
const panel = document.getElementById("dag-inspector");
|
|
4049
4524
|
if (!panel) return;
|
|
4525
|
+
// 运行状态标记:回到底部按钮由滚动位置自行显隐(终态同样可用),此标记仅供诊断。
|
|
4526
|
+
panel.dataset.runEnded = run && !isDagRunActive(run) ? "1" : "0";
|
|
4050
4527
|
captureDagTimelineViewportState(panel);
|
|
4051
4528
|
if (!uiState.dagInspectorOpen || !node) {
|
|
4052
4529
|
panel.classList.remove("is-open");
|
|
@@ -4054,6 +4531,8 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4054
4531
|
return;
|
|
4055
4532
|
}
|
|
4056
4533
|
|
|
4534
|
+
uiState.dagInspectorTab = activeInspectorTab();
|
|
4535
|
+
|
|
4057
4536
|
// Defer tearing down the inspector shell while the user is actively
|
|
4058
4537
|
// dragging the resize handle (or the graph) so the pointer-capture target
|
|
4059
4538
|
// is not destroyed mid-gesture; the next poll refreshes the content.
|
|
@@ -4073,6 +4552,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4073
4552
|
if (panel.dataset.contentSignature === nextContentSignature) return;
|
|
4074
4553
|
const previousNode = existingContent.dataset.dagNodeId ?? "";
|
|
4075
4554
|
const previousRun = existingContent.dataset.dagRunId ?? "";
|
|
4555
|
+
const previousExecutionKey = existingContent.dataset.executionKey;
|
|
4076
4556
|
const previousTab =
|
|
4077
4557
|
existingContent.dataset.inspectorTab ?? activeInspectorTab();
|
|
4078
4558
|
const nodeChanged =
|
|
@@ -4104,7 +4584,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4104
4584
|
activeTab,
|
|
4105
4585
|
);
|
|
4106
4586
|
}
|
|
4107
|
-
if (!nodeChanged && previousTab === activeTab) {
|
|
4587
|
+
if (!nodeChanged && previousTab === activeTab && previousExecutionKey === existingContent.dataset.executionKey) {
|
|
4108
4588
|
for (const [index, pre] of [
|
|
4109
4589
|
...existingContent.querySelectorAll("pre"),
|
|
4110
4590
|
].entries()) {
|
|
@@ -4121,6 +4601,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4121
4601
|
clearNode(panel);
|
|
4122
4602
|
panel.classList.add("is-open");
|
|
4123
4603
|
panel.setAttribute("aria-hidden", "false");
|
|
4604
|
+
panel.classList.toggle("is-expanded", Boolean(uiState.dagInspectorExpanded));
|
|
4124
4605
|
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
4125
4606
|
panel.appendChild(buildDagInspectorHeader(dagRunId, node));
|
|
4126
4607
|
|