@tea-agent/loop-agent 0.42.0 → 0.42.1
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 +70 -1
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1229 -584
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +23 -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 +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +131 -95
- package/dist/worker/console/chat/routes.js +180 -54
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +31 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- 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 +753 -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 +49 -11
- 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-capacity.js +9 -0
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- 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 +13 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -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 +139 -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-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +30 -1
- 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 +38 -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 +6 -3
- 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 +9 -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
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
|
@@ -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
|
|
|
@@ -487,6 +518,9 @@ function renderDagInspectorForCurrent() {
|
|
|
487
518
|
|
|
488
519
|
function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
489
520
|
uiState.selectedDagNodeId = nodeId;
|
|
521
|
+
// Resolve the default after loading this node, rather than inheriting
|
|
522
|
+
// another node's tab. Explicit links and later tab clicks still win.
|
|
523
|
+
uiState.dagInspectorTab = null;
|
|
490
524
|
uiState.dagInspectorOpen = true;
|
|
491
525
|
// Each node opens in live-follow mode by default. The explicit floating
|
|
492
526
|
// control can pause it without losing the reader's current position.
|
|
@@ -494,7 +528,7 @@ function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
|
494
528
|
uiState.dagTimelineViewportState = null;
|
|
495
529
|
// Reset the per-attempt selection so an old node's attempt cannot leak
|
|
496
530
|
// into the newly selected node (always defaults back to the latest).
|
|
497
|
-
uiState.dagInspectorAttempt = null;
|
|
531
|
+
uiState.dagInspectorAttempt = inspectorNodeSelections.get(`${dagRunId}:${nodeId}`) ?? null;
|
|
498
532
|
uiState.sessionEventOffset = 0;
|
|
499
533
|
uiState.sessionEvents = [];
|
|
500
534
|
// Drop any open spec-evidence preview so a stale A detail cannot survive
|
|
@@ -1044,9 +1078,11 @@ function sourceLabel(source) {
|
|
|
1044
1078
|
|
|
1045
1079
|
function appendMetaRow(dl, label, value, source) {
|
|
1046
1080
|
if (value == null || value === "") return;
|
|
1047
|
-
const
|
|
1081
|
+
const isElement = value && typeof value === "object" && value.nodeType != null;
|
|
1082
|
+
const copy = describeInspectorMeta(label, isElement ? "" : value);
|
|
1048
1083
|
const dt = el("dt", null, copy.label);
|
|
1049
1084
|
const dd = el("dd", null, copy.value);
|
|
1085
|
+
if (isElement) dd.appendChild(value);
|
|
1050
1086
|
const labelHelp = [
|
|
1051
1087
|
copy.help ? `含义:${copy.help}` : null,
|
|
1052
1088
|
copy.rawLabel ? `技术字段:${copy.rawLabel}` : null,
|
|
@@ -1564,6 +1600,9 @@ async function renderFrontendGateLedger(content, dagRunId) {
|
|
|
1564
1600
|
* executes it as a Console operation, and on success we jump to the new run.
|
|
1565
1601
|
*/
|
|
1566
1602
|
function buildContextExportSection(dagRunId, node) {
|
|
1603
|
+
const formScope = workspaceScope();
|
|
1604
|
+
let exportAttempt = null;
|
|
1605
|
+
let exportSourceLoading = true;
|
|
1567
1606
|
const zone = el("section", "dag-action-export-zone");
|
|
1568
1607
|
zone.dataset.actionSection = "context-export";
|
|
1569
1608
|
const divider = el("div", "dag-action-export-divider");
|
|
@@ -1682,7 +1721,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1682
1721
|
const protectionValue = appendTicketRow("保护", "自动隐藏匹配到的疑似凭据");
|
|
1683
1722
|
const attemptValue = appendTicketRow(
|
|
1684
1723
|
"来源",
|
|
1685
|
-
`${node.nodeId} ·
|
|
1724
|
+
`${node.nodeId} · ${exportAttempt ? `第 ${exportAttempt} 次尝试` : exportSourceLoading ? "正在确认最新尝试…" : "暂无可用上下文"}`,
|
|
1686
1725
|
"is-mono",
|
|
1687
1726
|
);
|
|
1688
1727
|
ticket.append(
|
|
@@ -1710,7 +1749,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1710
1749
|
recipientValue.textContent = target.description;
|
|
1711
1750
|
contentsValue.textContent = target.contents;
|
|
1712
1751
|
protectionValue.textContent = profile.description;
|
|
1713
|
-
attemptValue.textContent = `${node.nodeId} ·
|
|
1752
|
+
attemptValue.textContent = `${node.nodeId} · ${exportAttempt ? `第 ${exportAttempt} 次尝试` : exportSourceLoading ? "正在确认最新尝试…" : "暂无可用上下文"}`;
|
|
1714
1753
|
const safety = ticket.querySelector(".context-export-ticket-safety");
|
|
1715
1754
|
if (safety) {
|
|
1716
1755
|
safety.textContent = profileValue === "raw" ? "原始内容" : "已脱敏";
|
|
@@ -1730,6 +1769,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1730
1769
|
submit.type = "button";
|
|
1731
1770
|
submit.className = "prompt-restart-submit context-export-submit";
|
|
1732
1771
|
submit.dataset.testid = "context-export-submit";
|
|
1772
|
+
submit.disabled = true;
|
|
1733
1773
|
const submitIcon = el("i", "ri-download-cloud-2-line");
|
|
1734
1774
|
submitIcon.setAttribute("aria-hidden", "true");
|
|
1735
1775
|
submit.append(submitIcon, document.createTextNode(" 生成交接包并下载"));
|
|
@@ -1743,8 +1783,35 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1743
1783
|
status.textContent = message;
|
|
1744
1784
|
status.classList.toggle("prompt-restart-error", error);
|
|
1745
1785
|
};
|
|
1786
|
+
const refreshSource = iconButton("刷新导出来源", "ri-refresh-line", "context-export-refresh-source");
|
|
1787
|
+
refreshSource.type = "button";
|
|
1788
|
+
ticketTop.appendChild(refreshSource);
|
|
1789
|
+
const loadExportSource = async () => {
|
|
1790
|
+
exportAttempt = null;
|
|
1791
|
+
exportSourceLoading = true;
|
|
1792
|
+
submit.disabled = true;
|
|
1793
|
+
refreshSource.disabled = true;
|
|
1794
|
+
updatePreview();
|
|
1795
|
+
setStatus("正在检查最新尝试的上下文…");
|
|
1796
|
+
const result = await fetchDagNodeContext(dagRunId, node.nodeId);
|
|
1797
|
+
if (!section.isConnected || workspaceScope() !== formScope) return;
|
|
1798
|
+
exportSourceLoading = false;
|
|
1799
|
+
refreshSource.disabled = false;
|
|
1800
|
+
const data = result.ok ? result.body : null;
|
|
1801
|
+
if (Number.isInteger(data?.attempt) && data.attempt > 0 && data.effectiveContext?.status === "captured") {
|
|
1802
|
+
exportAttempt = data.attempt;
|
|
1803
|
+
submit.disabled = false;
|
|
1804
|
+
setStatus("交接包将使用上方标明的来源;有新尝试时可刷新来源。");
|
|
1805
|
+
} else {
|
|
1806
|
+
setStatus("最新尝试没有可用的上下文记录。记录生成后,可刷新来源重试。", true);
|
|
1807
|
+
}
|
|
1808
|
+
updatePreview();
|
|
1809
|
+
};
|
|
1810
|
+
refreshSource.addEventListener("click", () => { void loadExportSource(); });
|
|
1811
|
+
void loadExportSource();
|
|
1746
1812
|
submit.addEventListener("click", () => {
|
|
1747
1813
|
void (async () => {
|
|
1814
|
+
if (exportAttempt == null || workspaceScope() !== formScope) return;
|
|
1748
1815
|
const targetValue = selectedValue("context-export-target");
|
|
1749
1816
|
const profileValue = selectedValue("context-export-profile");
|
|
1750
1817
|
if (profileValue === "raw" && !rawAck.checked) {
|
|
@@ -1752,8 +1819,10 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1752
1819
|
return;
|
|
1753
1820
|
}
|
|
1754
1821
|
submit.disabled = true;
|
|
1822
|
+
refreshSource.disabled = true;
|
|
1755
1823
|
try {
|
|
1756
1824
|
setStatus("正在生成带来源校验信息的交接包…");
|
|
1825
|
+
if (workspaceScope() !== formScope) return;
|
|
1757
1826
|
const clientRequestId = `inspect-context-export-${Date.now()}-${Math.random()
|
|
1758
1827
|
.toString(36)
|
|
1759
1828
|
.slice(2, 10)}`;
|
|
@@ -1762,25 +1831,28 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1762
1831
|
{
|
|
1763
1832
|
runId: dagRunId,
|
|
1764
1833
|
nodeId: node.nodeId,
|
|
1765
|
-
attempt:
|
|
1834
|
+
attempt: exportAttempt,
|
|
1766
1835
|
target: targetValue,
|
|
1767
1836
|
profile: profileValue,
|
|
1768
1837
|
rawAcknowledged: profileValue === "raw" && rawAck.checked,
|
|
1769
1838
|
},
|
|
1770
1839
|
clientRequestId,
|
|
1771
1840
|
);
|
|
1841
|
+
if (workspaceScope() !== formScope) return;
|
|
1772
1842
|
const operationId = response.body?.operationId;
|
|
1773
1843
|
if (!response.ok || !operationId) {
|
|
1774
1844
|
throw new Error(response.body?.error?.message ?? "导出操作未被接受");
|
|
1775
1845
|
}
|
|
1776
1846
|
const operation = await pollOperationToTerminal(operationId, (message) =>
|
|
1777
|
-
setStatus(message.replace("续跑", "导出")),
|
|
1847
|
+
setStatus(message.replace("续跑", "导出")), formScope,
|
|
1778
1848
|
);
|
|
1849
|
+
if (workspaceScope() !== formScope) return;
|
|
1779
1850
|
if (operation?.state !== "succeeded" || !operation.result?.downloadUrl) {
|
|
1780
1851
|
throw new Error(operation?.errorMessage ?? "导出未成功完成");
|
|
1781
1852
|
}
|
|
1782
1853
|
setStatus("交接包已生成,正在安全下载…");
|
|
1783
1854
|
const downloaded = await downloadContextExport(operation.result.downloadUrl);
|
|
1855
|
+
if (workspaceScope() !== formScope) return;
|
|
1784
1856
|
const objectUrl = URL.createObjectURL(downloaded.blob);
|
|
1785
1857
|
const link = document.createElement("a");
|
|
1786
1858
|
link.href = objectUrl;
|
|
@@ -1796,6 +1868,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1796
1868
|
setStatus(transferErrorStatusText(error, "导出未成功完成"), true);
|
|
1797
1869
|
} finally {
|
|
1798
1870
|
submit.disabled = false;
|
|
1871
|
+
refreshSource.disabled = false;
|
|
1799
1872
|
}
|
|
1800
1873
|
})();
|
|
1801
1874
|
});
|
|
@@ -1845,16 +1918,11 @@ function buildDagActionAvailability({ title, description, reasonCode, tone = "ne
|
|
|
1845
1918
|
return panel;
|
|
1846
1919
|
}
|
|
1847
1920
|
|
|
1848
|
-
function buildFullContentCta() {
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
return el(
|
|
1854
|
-
"p",
|
|
1855
|
-
"artifact-content-preview-full-cta",
|
|
1856
|
-
"预览有截断;当前版本暂不提供完整文件下载。",
|
|
1857
|
-
);
|
|
1921
|
+
function buildFullContentCta(dagRunId, nodeId, artifactId) {
|
|
1922
|
+
const link = el("a", "artifact-content-preview-full-cta btn btn-secondary", "下载完整脱敏文本");
|
|
1923
|
+
link.href = scopedApiUrl(`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/artifacts/download?artifactId=${encodeURIComponent(artifactId)}`);
|
|
1924
|
+
link.setAttribute("download", `${artifactId}.redacted.txt`);
|
|
1925
|
+
return link;
|
|
1858
1926
|
}
|
|
1859
1927
|
|
|
1860
1928
|
function createArtifactContentPreview(
|
|
@@ -1968,9 +2036,8 @@ function createArtifactContentPreview(
|
|
|
1968
2036
|
if (notes.length > 0) {
|
|
1969
2037
|
host.appendChild(el("p", "artifact-content-preview-note", notes.join(" · ")));
|
|
1970
2038
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}
|
|
2039
|
+
host.appendChild(buildFullContentCta(dagRunId, nodeId, artifact.artifactId));
|
|
2040
|
+
const copy = el("button", "btn btn-secondary", "复制预览文本"); copy.type = "button"; copy.addEventListener("click", async () => { copy.textContent = await copyText(data.content) ? "已复制" : "复制失败"; }); host.appendChild(copy);
|
|
1974
2041
|
};
|
|
1975
2042
|
details.addEventListener("toggle", () => {
|
|
1976
2043
|
if (details.open) void load();
|
|
@@ -2006,6 +2073,7 @@ export function artifactRevisionRunEligibility(run) {
|
|
|
2006
2073
|
}
|
|
2007
2074
|
|
|
2008
2075
|
function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
2076
|
+
const formScope = workspaceScope();
|
|
2009
2077
|
const { section, body } = createDagActionCard({
|
|
2010
2078
|
eyebrow: "替换已有文件",
|
|
2011
2079
|
title: "换掉这个节点的产出后继续",
|
|
@@ -2033,6 +2101,7 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2033
2101
|
);
|
|
2034
2102
|
void (async () => {
|
|
2035
2103
|
const response = await fetchDagNodeArtifacts(dagRunId, node.nodeId);
|
|
2104
|
+
if (workspaceScope() !== formScope || !body.isConnected) return;
|
|
2036
2105
|
clearNode(body);
|
|
2037
2106
|
if (!response.ok || response.body?.binding?.available !== true) {
|
|
2038
2107
|
body.appendChild(
|
|
@@ -2128,14 +2197,36 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2128
2197
|
const planHost = el("div", "artifact-revision-plan");
|
|
2129
2198
|
form.appendChild(planHost);
|
|
2130
2199
|
body.appendChild(form);
|
|
2200
|
+
const draftKey = `${dagRunId}:${node.nodeId}:artifact`;
|
|
2201
|
+
const artifactDraft = inspectorDrafts.get(draftKey) ?? {};
|
|
2202
|
+
inspectorDrafts.set(draftKey, artifactDraft);
|
|
2203
|
+
if (eligible.some((item) => item.artifactId === artifactDraft.artifactId)) artifactSelect.value = artifactDraft.artifactId;
|
|
2204
|
+
reason.value = artifactDraft.reason ?? "";
|
|
2205
|
+
const fileHint = el("p", "muted", artifactDraft.file ? `已保留:${artifactDraft.file.name}` : "");
|
|
2206
|
+
fileLabel.appendChild(fileHint);
|
|
2207
|
+
let draftRevision = 0;
|
|
2208
|
+
const updateDraft = () => {
|
|
2209
|
+
artifactDraft.artifactId = artifactSelect.value;
|
|
2210
|
+
artifactDraft.reason = reason.value;
|
|
2211
|
+
artifactDraft.file = file.files?.[0] ?? artifactDraft.file;
|
|
2212
|
+
fileHint.textContent = artifactDraft.file ? `已保留:${artifactDraft.file.name}` : "";
|
|
2213
|
+
draftRevision += 1; clearNode(planHost);
|
|
2214
|
+
};
|
|
2215
|
+
artifactSelect.addEventListener("change", updateDraft);
|
|
2216
|
+
file.addEventListener("change", updateDraft);
|
|
2217
|
+
reason.addEventListener("input", updateDraft);
|
|
2131
2218
|
const setStatus = (message, error = false) => {
|
|
2132
2219
|
status.textContent = message;
|
|
2133
2220
|
status.classList.toggle("prompt-restart-error", error);
|
|
2134
2221
|
};
|
|
2222
|
+
const syncArtifactRetry = mountSubmissionRetry(form, artifactDraft, dagRunId, node.nodeId, formScope, setStatus);
|
|
2135
2223
|
preview.addEventListener("click", () => {
|
|
2136
2224
|
void (async () => {
|
|
2225
|
+
if (hasUnresolvedSubmission(artifactDraft.submission) || artifactDraft.submission?.operationId) { setStatus("已有提交记录,请使用“重试原提交”或查看操作进度。"); return; }
|
|
2137
2226
|
const selected = eligible.find((artifact) => artifact.artifactId === artifactSelect.value);
|
|
2138
|
-
const selectedFile = file.files?.[0];
|
|
2227
|
+
const selectedFile = file.files?.[0] ?? artifactDraft.file;
|
|
2228
|
+
const requestedRevision = draftRevision;
|
|
2229
|
+
const requestedReason = reason.value.trim();
|
|
2139
2230
|
if (!selected || !selectedFile) {
|
|
2140
2231
|
setStatus("请选择一个替换文件。", true);
|
|
2141
2232
|
return;
|
|
@@ -2160,6 +2251,7 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2160
2251
|
const candidateBase64 = String(dataUrl).split(",", 2)[1] ?? "";
|
|
2161
2252
|
setStatus("正在检查新文件格式并对比差异…");
|
|
2162
2253
|
const stageId = `artifact-stage-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2254
|
+
if (workspaceScope() !== formScope) return;
|
|
2163
2255
|
const staged = await postOperatorAction(
|
|
2164
2256
|
"artifactRevisionStage",
|
|
2165
2257
|
{
|
|
@@ -2171,9 +2263,10 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2171
2263
|
},
|
|
2172
2264
|
stageId,
|
|
2173
2265
|
);
|
|
2266
|
+
if (workspaceScope() !== formScope) return;
|
|
2174
2267
|
if (!staged.ok || !staged.body?.operationId) throw new Error(staged.body?.error?.message ?? "修订 staging 失败");
|
|
2175
2268
|
const stageOp = await pollOperationToTerminal(staged.body.operationId, (message) =>
|
|
2176
|
-
setStatus(message.replace("续跑", "修订校验")),
|
|
2269
|
+
setStatus(message.replace("续跑", "修订校验")), formScope,
|
|
2177
2270
|
);
|
|
2178
2271
|
const revision = stageOp?.result?.revision;
|
|
2179
2272
|
if (stageOp?.state !== "succeeded" || !revision?.revisionId) {
|
|
@@ -2181,17 +2274,21 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2181
2274
|
}
|
|
2182
2275
|
setStatus("文件检查通过,正在计算哪些后续节点需要重新执行…");
|
|
2183
2276
|
const planId = `artifact-plan-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2277
|
+
if (workspaceScope() !== formScope) return;
|
|
2184
2278
|
const planned = await postOperatorAction(
|
|
2185
2279
|
"artifactRevisionPlan",
|
|
2186
2280
|
{ runId: dagRunId, artifactId: selected.artifactId, revisionId: revision.revisionId },
|
|
2187
2281
|
planId,
|
|
2188
2282
|
);
|
|
2283
|
+
if (workspaceScope() !== formScope) return;
|
|
2189
2284
|
if (!planned.ok || !planned.body?.operationId) throw new Error(planned.body?.error?.message ?? "影响预览失败");
|
|
2190
2285
|
const planOp = await pollOperationToTerminal(planned.body.operationId, (message) =>
|
|
2191
|
-
setStatus(message.replace("续跑", "影响分析")),
|
|
2286
|
+
setStatus(message.replace("续跑", "影响分析")), formScope,
|
|
2192
2287
|
);
|
|
2288
|
+
if (workspaceScope() !== formScope) return;
|
|
2193
2289
|
const plan = planOp?.result?.plan;
|
|
2194
2290
|
if (!plan) throw new Error(planOp?.errorMessage ?? "影响预览失败");
|
|
2291
|
+
if (draftRevision !== requestedRevision || !body.isConnected) { setStatus("草稿已变化,请重新预览。"); return; }
|
|
2195
2292
|
planHost.appendChild(el("h5", null, "替换结果与影响范围"));
|
|
2196
2293
|
planHost.appendChild(
|
|
2197
2294
|
transparencyMetaList([
|
|
@@ -2232,23 +2329,22 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2232
2329
|
planHost.appendChild(execute);
|
|
2233
2330
|
execute.addEventListener("click", () => {
|
|
2234
2331
|
void (async () => {
|
|
2332
|
+
if (draftRevision !== requestedRevision || hasUnresolvedSubmission(artifactDraft.submission) || artifactDraft.submission?.operationId) { setStatus("此预览已变化或已经提交,请查看操作进度。", true); return; }
|
|
2235
2333
|
execute.disabled = true;
|
|
2236
2334
|
try {
|
|
2237
2335
|
setStatus("正在创建新运行并重新执行受影响节点;原运行保持不变…");
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
revisionId: revision.revisionId,
|
|
2245
|
-
planHash: plan.planHash,
|
|
2246
|
-
reason: reason.value.trim(),
|
|
2247
|
-
},
|
|
2248
|
-
executeId,
|
|
2249
|
-
);
|
|
2336
|
+
if (workspaceScope() !== formScope) return;
|
|
2337
|
+
artifactDraft.submission = createInspectorSubmission("artifactRevisionExecute", {
|
|
2338
|
+
runId: dagRunId, artifactId: selected.artifactId,
|
|
2339
|
+
revisionId: revision.revisionId, planHash: plan.planHash, reason: requestedReason,
|
|
2340
|
+
});
|
|
2341
|
+
const accepted = await sendInspectorSubmission(artifactDraft.submission, postOperatorAction);
|
|
2250
2342
|
if (!accepted.ok || !accepted.body?.operationId) throw new Error(accepted.body?.error?.message ?? "新运行未被接受");
|
|
2251
|
-
|
|
2343
|
+
if (workspaceScope() !== formScope) return;
|
|
2344
|
+
artifactDraft.submission.operationId = accepted.body.operationId;
|
|
2345
|
+
trackInspectorOperation(dagRunId, node.nodeId, accepted.body.operationId);
|
|
2346
|
+
const operation = await pollOperationToTerminal(accepted.body.operationId, setStatus, formScope);
|
|
2347
|
+
if (workspaceScope() !== formScope) return;
|
|
2252
2348
|
const childRunId = operation?.result?.newRunId;
|
|
2253
2349
|
if (childRunId) {
|
|
2254
2350
|
const open = document.createElement("button");
|
|
@@ -2271,7 +2367,9 @@ function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
|
2271
2367
|
} catch (error) {
|
|
2272
2368
|
setStatus(transferErrorStatusText(error, "新运行失败"), true);
|
|
2273
2369
|
} finally {
|
|
2274
|
-
|
|
2370
|
+
if (artifactDraft.submission) artifactDraft.submission.inFlight = false;
|
|
2371
|
+
execute.disabled = Boolean(artifactDraft.submission?.operationId) || hasUnresolvedSubmission(artifactDraft.submission);
|
|
2372
|
+
syncArtifactRetry();
|
|
2275
2373
|
}
|
|
2276
2374
|
})();
|
|
2277
2375
|
});
|
|
@@ -2311,6 +2409,7 @@ function buildDagActionIntro(node) {
|
|
|
2311
2409
|
}
|
|
2312
2410
|
|
|
2313
2411
|
async function renderPromptRestart(content, dagRunId, node, run) {
|
|
2412
|
+
const formScope = workspaceScope();
|
|
2314
2413
|
const nodeId = node.nodeId;
|
|
2315
2414
|
clearNode(content);
|
|
2316
2415
|
const intro = buildDagActionIntro(node);
|
|
@@ -2336,10 +2435,24 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2336
2435
|
["context-export", exportSection],
|
|
2337
2436
|
]);
|
|
2338
2437
|
content.appendChild(intro);
|
|
2438
|
+
const intent = el("div", "dag-action-intents");
|
|
2439
|
+
const intentKey = `${dagRunId}:${nodeId}:intent`;
|
|
2440
|
+
let selectedIntent = inspectorDrafts.get(intentKey) ?? "prompt-restart";
|
|
2441
|
+
const chooseIntent = (id) => {
|
|
2442
|
+
selectedIntent = id; inspectorDrafts.set(intentKey, id);
|
|
2443
|
+
for (const [key, section] of sections) section.hidden = key !== id;
|
|
2444
|
+
for (const button of intent.querySelectorAll("button")) button.setAttribute("aria-pressed", String(button.dataset.intent === id));
|
|
2445
|
+
};
|
|
2446
|
+
for (const [id, label] of [["prompt-restart", "修改提示重跑"], ["artifact-revision", "替换产出继续"], ["context-export", "导出资料"]]) {
|
|
2447
|
+
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);
|
|
2448
|
+
}
|
|
2449
|
+
content.appendChild(intent);
|
|
2339
2450
|
for (const sectionId of DAG_ACTION_SECTION_ORDER) {
|
|
2340
2451
|
const section = sections.get(sectionId);
|
|
2341
2452
|
if (section) content.appendChild(section);
|
|
2342
2453
|
}
|
|
2454
|
+
chooseIntent(selectedIntent);
|
|
2455
|
+
mountInspectorOperations(content, dagRunId, nodeId);
|
|
2343
2456
|
const input = await fetchDagNodeInput(dagRunId, nodeId);
|
|
2344
2457
|
if (
|
|
2345
2458
|
!content.isConnected ||
|
|
@@ -2376,7 +2489,10 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2376
2489
|
frozenBasePrompt,
|
|
2377
2490
|
input.body.promptCandidates,
|
|
2378
2491
|
);
|
|
2379
|
-
|
|
2492
|
+
const draftKey = `${dagRunId}:${nodeId}:prompt`;
|
|
2493
|
+
const draft = inspectorDrafts.get(draftKey) ?? { candidateKey: "frozen", candidates: {} };
|
|
2494
|
+
inspectorDrafts.set(draftKey, draft);
|
|
2495
|
+
let selectedCandidate = promptCandidateOptions.find((item) => item.key === draft.candidateKey) ?? promptCandidateOptions[0];
|
|
2380
2496
|
const historicalCandidatesMissing = Array.isArray(input.body.warnings)
|
|
2381
2497
|
? input.body.warnings.some((warning) =>
|
|
2382
2498
|
String(warning).startsWith(
|
|
@@ -2465,14 +2581,12 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2465
2581
|
? "这个较早的运行没有保存失败时实际使用的提示词;新运行会在这里列出。"
|
|
2466
2582
|
: "该节点没有保存过失败时使用的提示词;当前只有原始提示词。",
|
|
2467
2583
|
onChange: (nextKey) => {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2584
|
+
saveDraft();
|
|
2585
|
+
selectedCandidate = promptCandidateOptions.find((option) => option.key === nextKey) ?? promptCandidateOptions[0];
|
|
2586
|
+
draft.candidateKey = selectedCandidate.key;
|
|
2471
2587
|
promptCard.sync(selectedCandidate);
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
modeSelect.value = "replace";
|
|
2475
|
-
}
|
|
2588
|
+
restoreDraft();
|
|
2589
|
+
invalidatePlan();
|
|
2476
2590
|
picker.hintEl.hidden = false;
|
|
2477
2591
|
picker.hintEl.textContent = selectedCandidate.truncated
|
|
2478
2592
|
? "该候选已截断,仅供查看,不能直接提交重启。"
|
|
@@ -2484,7 +2598,7 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2484
2598
|
});
|
|
2485
2599
|
const modeSwitch = createPromptRestartModeSwitch({
|
|
2486
2600
|
value: "append",
|
|
2487
|
-
onChange: () => applyModeVisibility(),
|
|
2601
|
+
onChange: () => { applyModeVisibility(); saveDraft(); invalidatePlan(); },
|
|
2488
2602
|
});
|
|
2489
2603
|
const modeSelect = modeSwitch.group;
|
|
2490
2604
|
const toolbar = el("div", "prompt-restart-toolbar");
|
|
@@ -2527,6 +2641,8 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2527
2641
|
box.appendChild(baseLabel);
|
|
2528
2642
|
|
|
2529
2643
|
const applyModeVisibility = () => {
|
|
2644
|
+
if (selectedCandidate.key !== "frozen") modeSelect.value = "replace";
|
|
2645
|
+
for (const button of modeSelect.querySelectorAll("button")) if (button.dataset.mode === "append" || button.getAttribute("data-value") === "append") button.disabled = selectedCandidate.key !== "frozen";
|
|
2530
2646
|
const append = modeSelect.value === "append";
|
|
2531
2647
|
baseLabel.classList.toggle("is-hidden", !append);
|
|
2532
2648
|
addendumLabel.classList.toggle("is-hidden", !append);
|
|
@@ -2554,10 +2670,44 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2554
2670
|
const submit = document.createElement("button");
|
|
2555
2671
|
submit.type = "button";
|
|
2556
2672
|
submit.className = "prompt-restart-submit";
|
|
2557
|
-
submit.textContent = "
|
|
2673
|
+
submit.textContent = "预览重跑影响";
|
|
2558
2674
|
submit.setAttribute("data-testid", "prompt-restart-submit");
|
|
2559
2675
|
row.appendChild(status);
|
|
2560
2676
|
row.appendChild(submit);
|
|
2677
|
+
const start = el("button", "prompt-restart-submit", "确认并开始新运行");
|
|
2678
|
+
start.type = "button";
|
|
2679
|
+
start.disabled = true;
|
|
2680
|
+
start.dataset.testid = "prompt-restart-execute";
|
|
2681
|
+
row.appendChild(start);
|
|
2682
|
+
const planHost = el("div", "prompt-restart-plan");
|
|
2683
|
+
box.appendChild(planHost);
|
|
2684
|
+
let prepared = null;
|
|
2685
|
+
let submitted = false;
|
|
2686
|
+
draft.submission ??= {};
|
|
2687
|
+
const signature = () => JSON.stringify([selectedCandidate.key, modeSelect.value, addendumArea.value, replaceArea.value, reasonArea.value]);
|
|
2688
|
+
const invalidatePlan = () => { prepared = null; start.disabled = true; clearNode(planHost); };
|
|
2689
|
+
const saveDraft = () => {
|
|
2690
|
+
draft.candidates[selectedCandidate.key] = { mode: modeSelect.value, addendum: addendumArea.value, replacement: replaceArea.value, reason: reasonArea.value };
|
|
2691
|
+
};
|
|
2692
|
+
const restoreDraft = () => {
|
|
2693
|
+
const saved = draft.candidates[selectedCandidate.key];
|
|
2694
|
+
modeSelect.value = saved?.mode ?? (selectedCandidate.key === "frozen" ? "append" : "replace");
|
|
2695
|
+
addendumArea.value = saved?.addendum ?? "";
|
|
2696
|
+
replaceArea.value = saved?.replacement ?? selectedCandidate.prompt;
|
|
2697
|
+
reasonArea.value = saved?.reason ?? `修改失败节点 ${nodeId} 的提示词后重新运行(来源运行:${dagRunId})`;
|
|
2698
|
+
applyModeVisibility();
|
|
2699
|
+
};
|
|
2700
|
+
for (const area of [addendumArea, replaceArea, reasonArea]) area.addEventListener("input", () => { saveDraft(); invalidatePlan(); });
|
|
2701
|
+
restoreDraft();
|
|
2702
|
+
let beforeRestore = null;
|
|
2703
|
+
const restoreOriginal = el("button", "btn btn-secondary", "恢复此版本原文");
|
|
2704
|
+
restoreOriginal.type = "button";
|
|
2705
|
+
restoreOriginal.addEventListener("click", () => {
|
|
2706
|
+
if (beforeRestore === null) { beforeRestore = { key: selectedCandidate.key, text: replaceArea.value }; replaceArea.value = selectedCandidate.prompt; restoreOriginal.textContent = "撤销恢复"; }
|
|
2707
|
+
else { if (beforeRestore.key === selectedCandidate.key) replaceArea.value = beforeRestore.text; beforeRestore = null; restoreOriginal.textContent = "恢复此版本原文"; }
|
|
2708
|
+
saveDraft(); invalidatePlan();
|
|
2709
|
+
});
|
|
2710
|
+
toolbar.appendChild(restoreOriginal);
|
|
2561
2711
|
box.appendChild(row);
|
|
2562
2712
|
|
|
2563
2713
|
promptBody.appendChild(box);
|
|
@@ -2567,7 +2717,11 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2567
2717
|
status.classList.toggle("prompt-restart-error", isError);
|
|
2568
2718
|
};
|
|
2569
2719
|
|
|
2570
|
-
|
|
2720
|
+
const syncPromptRetry = mountSubmissionRetry(box, draft, dagRunId, nodeId, formScope, setStatus);
|
|
2721
|
+
const handleSubmission = (executeRequested) => {
|
|
2722
|
+
if (submitted || draft.submission.inFlight || (executeRequested && !prepared)) return;
|
|
2723
|
+
if (hasUnresolvedSubmission(draft.submission)) { setStatus("上次提交结果尚未确认,请先重试原提交。", true); return; }
|
|
2724
|
+
if (executeRequested && draft.submission.operationId) { setStatus("这份草稿已经提交,请查看上方操作进度。"); return; }
|
|
2571
2725
|
void (async () => {
|
|
2572
2726
|
if (selectedCandidate.truncated) {
|
|
2573
2727
|
setStatus("所选失败提示词已截断,不能作为完整提示词重启。", true);
|
|
@@ -2596,15 +2750,22 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2596
2750
|
return;
|
|
2597
2751
|
}
|
|
2598
2752
|
submit.disabled = true;
|
|
2753
|
+
start.disabled = true;
|
|
2754
|
+
const requestedSignature = signature();
|
|
2599
2755
|
try {
|
|
2600
|
-
setStatus("正在检查这次重跑是否安全可行…");
|
|
2601
2756
|
const overrides = [{ nodeId, prompt, mode }];
|
|
2757
|
+
let plan = prepared?.plan;
|
|
2758
|
+
if (!executeRequested) {
|
|
2759
|
+
invalidatePlan();
|
|
2760
|
+
setStatus("正在检查这次重跑是否安全可行…");
|
|
2761
|
+
if (workspaceScope() !== formScope) return;
|
|
2602
2762
|
const planRes = await postOperatorAction("dagRerunPlan", {
|
|
2603
2763
|
runId: dagRunId,
|
|
2604
2764
|
fromNode: nodeId,
|
|
2605
2765
|
promptOverrides: overrides,
|
|
2606
2766
|
});
|
|
2607
|
-
|
|
2767
|
+
if (workspaceScope() !== formScope) return;
|
|
2768
|
+
plan = planRes.body?.result ?? null;
|
|
2608
2769
|
if (!planRes.ok || plan?.eligible !== true || !plan?.planHash) {
|
|
2609
2770
|
setStatus(
|
|
2610
2771
|
planRes.body?.error?.message ??
|
|
@@ -2615,34 +2776,50 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2615
2776
|
return;
|
|
2616
2777
|
}
|
|
2617
2778
|
|
|
2779
|
+
if (signature() !== requestedSignature) { setStatus("草稿已变化,请重新预览。"); return; }
|
|
2780
|
+
prepared = { plan, signature: requestedSignature };
|
|
2781
|
+
clearNode(planHost);
|
|
2782
|
+
planHost.appendChild(transparencyMetaList([
|
|
2783
|
+
["将重新执行", (plan.resetNodeIds ?? []).join(", ") || "无"],
|
|
2784
|
+
["将沿用结果", (plan.importedNodeIds ?? []).join(", ") || "无"],
|
|
2785
|
+
["预计风险", inspectorValueLabel("风险级别", plan.risk)],
|
|
2786
|
+
["预计执行", `${plan.estimatedExecution?.nodeCount ?? "未记录"} 个节点 · ${plan.estimatedExecution?.piCallCount ?? "未记录"} 次 AI 调用`],
|
|
2787
|
+
]));
|
|
2788
|
+
const change = el("details", "prompt-change-preview");
|
|
2789
|
+
change.appendChild(el("summary", null, mode === "append" ? "查看将追加的指令" : "查看替换前后提示词"));
|
|
2790
|
+
if (mode === "replace") { change.appendChild(el("h5", null, "修改前")); change.appendChild(el("pre", "prompt-restart-readonly", selectedCandidate.prompt)); }
|
|
2791
|
+
change.appendChild(el("h5", null, "本次提交")); change.appendChild(el("pre", "prompt-restart-readonly", prompt));
|
|
2792
|
+
planHost.appendChild(change);
|
|
2793
|
+
start.disabled = false;
|
|
2794
|
+
setStatus("预览已就绪。确认后创建新运行;编辑草稿会使本次预览失效。");
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
if (prepared?.signature !== requestedSignature) { invalidatePlan(); setStatus("草稿已变化,请重新预览。", true); return; }
|
|
2798
|
+
|
|
2618
2799
|
const est = plan.estimatedExecution;
|
|
2619
2800
|
const estText = est
|
|
2620
2801
|
? `(将执行 ${est.nodeCount ?? "?"} 个节点,其中 ${est.piCallCount ?? "?"} 个需要 AI 处理)`
|
|
2621
2802
|
: "";
|
|
2622
2803
|
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
|
-
);
|
|
2804
|
+
if (workspaceScope() !== formScope) return;
|
|
2805
|
+
draft.submission = createInspectorSubmission("dagRerun", {
|
|
2806
|
+
runId: dagRunId, fromNode: nodeId, planHash: plan.planHash, reason, promptOverrides: overrides,
|
|
2807
|
+
});
|
|
2808
|
+
const runRes = await sendInspectorSubmission(draft.submission, postOperatorAction);
|
|
2809
|
+
if (workspaceScope() !== formScope) return;
|
|
2637
2810
|
const operationId = runRes.body?.operationId;
|
|
2638
2811
|
if (!runRes.ok || !operationId) {
|
|
2639
2812
|
setStatus(runRes.body?.error?.message ?? "提交续跑失败。", true);
|
|
2640
2813
|
return;
|
|
2641
2814
|
}
|
|
2642
2815
|
|
|
2816
|
+
submitted = true;
|
|
2817
|
+
draft.submission.operationId = operationId;
|
|
2818
|
+
trackInspectorOperation(dagRunId, nodeId, operationId);
|
|
2643
2819
|
const op = await pollOperationToTerminal(operationId, (text) =>
|
|
2644
|
-
setStatus(text),
|
|
2820
|
+
setStatus(text), formScope,
|
|
2645
2821
|
);
|
|
2822
|
+
if (workspaceScope() !== formScope) return;
|
|
2646
2823
|
if (op?.state === "succeeded") {
|
|
2647
2824
|
const newRunId =
|
|
2648
2825
|
op.dagRunId ?? op.result?.newRunId ?? op.runSummary?.runId ?? null;
|
|
@@ -2670,14 +2847,73 @@ async function renderPromptRestart(content, dagRunId, node, run) {
|
|
|
2670
2847
|
} catch (err) {
|
|
2671
2848
|
setStatus(err instanceof Error ? err.message : String(err), true);
|
|
2672
2849
|
} finally {
|
|
2673
|
-
|
|
2850
|
+
draft.submission.inFlight = false;
|
|
2851
|
+
submit.disabled = submitted;
|
|
2852
|
+
start.disabled = submitted || !prepared || hasUnresolvedSubmission(draft.submission);
|
|
2853
|
+
syncPromptRetry();
|
|
2674
2854
|
}
|
|
2675
2855
|
})();
|
|
2856
|
+
};
|
|
2857
|
+
submit.addEventListener("click", () => handleSubmission(false));
|
|
2858
|
+
start.addEventListener("click", () => handleSubmission(true));
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
function mountSubmissionRetry(host, draft, dagRunId, nodeId, scope, setStatus) {
|
|
2862
|
+
const retry = el("button", "btn btn-secondary", "重试原提交");
|
|
2863
|
+
retry.type = "button";
|
|
2864
|
+
retry.setAttribute("data-tooltip", "原样重放已确认的请求,避免重复创建运行");
|
|
2865
|
+
host.appendChild(retry);
|
|
2866
|
+
const sync = () => {
|
|
2867
|
+
retry.hidden = !hasUnresolvedSubmission(draft.submission);
|
|
2868
|
+
retry.disabled = Boolean(draft.submission?.inFlight);
|
|
2869
|
+
};
|
|
2870
|
+
retry.addEventListener("click", async () => {
|
|
2871
|
+
if (workspaceScope() !== scope || !hasUnresolvedSubmission(draft.submission)) return;
|
|
2872
|
+
retry.disabled = true;
|
|
2873
|
+
try {
|
|
2874
|
+
setStatus("正在核对原提交结果…");
|
|
2875
|
+
const response = await sendInspectorSubmission(draft.submission, postOperatorAction);
|
|
2876
|
+
if (workspaceScope() !== scope) return;
|
|
2877
|
+
if (!response.ok || !draft.submission.operationId) {
|
|
2878
|
+
setStatus(response.body?.error?.message ?? "结果仍未确认,可以继续重试原提交。", true);
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
trackInspectorOperation(dagRunId, nodeId, draft.submission.operationId);
|
|
2882
|
+
const operation = await pollOperationToTerminal(draft.submission.operationId, setStatus, scope);
|
|
2883
|
+
if (workspaceScope() === scope) setStatus(operation ? `操作状态:${inspectorValueLabel("status", operation.state)}` : "等待结束,可在操作台查看进度。");
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
setStatus(error instanceof Error ? error.message : "结果未确认,请重试原提交。", true);
|
|
2886
|
+
} finally { sync(); }
|
|
2676
2887
|
});
|
|
2888
|
+
sync();
|
|
2889
|
+
return sync;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function trackInspectorOperation(dagRunId, nodeId, operationId) {
|
|
2893
|
+
inspectorOperations.set(operationId, { dagRunId, nodeId, operationId, operation: { state: "queued" } });
|
|
2894
|
+
refreshInspectorOperations();
|
|
2895
|
+
}
|
|
2896
|
+
function mountInspectorOperations(content, dagRunId, nodeId) {
|
|
2897
|
+
let host = content.querySelector(".inspector-operations");
|
|
2898
|
+
if (!host) { host = el("div", "inspector-operations"); content.insertBefore(host, content.firstChild); }
|
|
2899
|
+
clearNode(host);
|
|
2900
|
+
for (const tracked of inspectorOperations.values()) {
|
|
2901
|
+
if (tracked.dagRunId !== dagRunId || tracked.nodeId !== nodeId) continue;
|
|
2902
|
+
const card = el("div", "inspector-operation");
|
|
2903
|
+
card.appendChild(el("p", null, `已提交 · ${tracked.operationId} · ${inspectorValueLabel("status", tracked.operation.state)}`));
|
|
2904
|
+
const runId = tracked.operation.dagRunId ?? tracked.operation.result?.newRunId ?? tracked.operation.runSummary?.runId;
|
|
2905
|
+
if (runId) { const link = el("a", "object-link", "打开新运行"); link.href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(runId)}`); card.appendChild(link); }
|
|
2906
|
+
if (tracked.operation.errorMessage) card.appendChild(el("p", "node-input-error", tracked.operation.errorMessage));
|
|
2907
|
+
host.appendChild(card);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
function refreshInspectorOperations() {
|
|
2911
|
+
const content = document.querySelector(".dag-inspector-content[data-inspector-tab='prompt-restart']");
|
|
2912
|
+
if (content) mountInspectorOperations(content, content.dataset.dagRunId, content.dataset.dagNodeId);
|
|
2677
2913
|
}
|
|
2678
2914
|
|
|
2679
2915
|
/** Poll a Console operation until a terminal state; null on poll budget end. */
|
|
2680
|
-
async function pollOperationToTerminal(operationId, onProgress) {
|
|
2916
|
+
async function pollOperationToTerminal(operationId, onProgress, operationScope = workspaceScope()) {
|
|
2681
2917
|
const terminal = new Set([
|
|
2682
2918
|
"succeeded",
|
|
2683
2919
|
"failed",
|
|
@@ -2685,9 +2921,12 @@ async function pollOperationToTerminal(operationId, onProgress) {
|
|
|
2685
2921
|
"needs-reconcile",
|
|
2686
2922
|
]);
|
|
2687
2923
|
for (let attempt = 0; attempt < 1200; attempt += 1) {
|
|
2924
|
+
if (workspaceScope() !== operationScope) return null;
|
|
2688
2925
|
const res = await fetchOperatorOperation(operationId);
|
|
2926
|
+
if (workspaceScope() !== operationScope) return null;
|
|
2689
2927
|
const op = res.body?.operation ?? res.body;
|
|
2690
2928
|
const state = op?.state ?? "";
|
|
2929
|
+
if (res.ok && inspectorOperations.has(operationId)) { Object.assign(inspectorOperations.get(operationId), { operation: op }); refreshInspectorOperations(); }
|
|
2691
2930
|
if (res.ok && terminal.has(state)) {
|
|
2692
2931
|
return op;
|
|
2693
2932
|
}
|
|
@@ -2748,28 +2987,28 @@ function selectedAttemptNumber(dagRunId, node) {
|
|
|
2748
2987
|
}
|
|
2749
2988
|
|
|
2750
2989
|
async function renderEffectiveContext(content, dagRunId, node) {
|
|
2990
|
+
const requestToken = Symbol("context-request");
|
|
2991
|
+
content._contextRequestToken = requestToken;
|
|
2992
|
+
const requestedExecutionKey = effectiveExecutionKey(dagRunId, node);
|
|
2993
|
+
const requestedScope = workspaceScope();
|
|
2751
2994
|
ensureInspectorPageLead(content, "context");
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
dagRunId,
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
);
|
|
2995
|
+
if (requestedExecutionKey?.startsWith("pass-")) {
|
|
2996
|
+
content.appendChild(el("p", "node-transparency-omission", "该修复轮次没有独立保存的上下文记录。现有记录无法区分各个修复轮次。"));
|
|
2997
|
+
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "context");
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
content.appendChild(el("p", "muted", "正在读取这次尝试的上下文…"));
|
|
3001
|
+
const result = await fetchDagNodeContext(dagRunId, node.nodeId, selectedAttemptNumber(dagRunId, node));
|
|
2758
3002
|
if (
|
|
2759
|
-
!content.isConnected ||
|
|
3003
|
+
!content.isConnected || content._contextRequestToken !== requestToken || workspaceScope() !== requestedScope ||
|
|
2760
3004
|
content.dataset.dagRunId !== String(dagRunId) ||
|
|
2761
3005
|
content.dataset.dagNodeId !== String(node.nodeId) ||
|
|
2762
|
-
activeInspectorTab() !== "context"
|
|
2763
|
-
)
|
|
2764
|
-
return;
|
|
3006
|
+
activeInspectorTab() !== "context" || requestedExecutionKey !== effectiveExecutionKey(dagRunId, node)
|
|
3007
|
+
) return;
|
|
2765
3008
|
clearNode(content);
|
|
2766
3009
|
ensureInspectorPageLead(content, "context");
|
|
2767
3010
|
if (!result.ok || !result.body) {
|
|
2768
|
-
const error = el(
|
|
2769
|
-
"p",
|
|
2770
|
-
"node-input-error",
|
|
2771
|
-
result.body?.error || "无法加载有效上下文事实。",
|
|
2772
|
-
);
|
|
3011
|
+
const error = el("p", "node-input-error", result.body?.error || "无法读取这次尝试的上下文。");
|
|
2773
3012
|
error.setAttribute("role", "alert");
|
|
2774
3013
|
content.appendChild(error);
|
|
2775
3014
|
return;
|
|
@@ -2777,75 +3016,155 @@ async function renderEffectiveContext(content, dagRunId, node) {
|
|
|
2777
3016
|
const data = result.body;
|
|
2778
3017
|
const receipt = data.effectiveContext;
|
|
2779
3018
|
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
|
-
);
|
|
3019
|
+
root.appendChild(el("p", "context-record-heading", `第 ${data.attempt} 次尝试 · ${receipt?.status === "captured" ? "已保存上下文" : "上下文不可用"}`));
|
|
2788
3020
|
if (receipt?.status !== "captured") {
|
|
2789
3021
|
const missing = el("div", "node-transparency-omission");
|
|
2790
3022
|
const reasonCode = receipt?.reasonCode ?? "CONTEXT_RECEIPT_MISSING";
|
|
2791
3023
|
const missingLabel = contextTransferReasonLabel(reasonCode);
|
|
2792
|
-
missing.appendChild(
|
|
2793
|
-
el(
|
|
2794
|
-
"h4",
|
|
2795
|
-
null,
|
|
2796
|
-
receipt?.status === "invalid" ? "执行时记录不可用" : "没有执行时记录",
|
|
2797
|
-
),
|
|
2798
|
-
);
|
|
3024
|
+
missing.appendChild(el("h4", null, receipt?.status === "invalid" ? "记录未通过完整性检查" : "没有保存这次尝试的上下文"));
|
|
2799
3025
|
const reason = el("p", "muted", missingLabel ?? reasonCode);
|
|
2800
3026
|
if (missingLabel) reason.setAttribute("title", `技术代码:${reasonCode}`);
|
|
2801
3027
|
missing.appendChild(reason);
|
|
2802
|
-
missing.appendChild(
|
|
2803
|
-
el(
|
|
2804
|
-
"p",
|
|
2805
|
-
"muted",
|
|
2806
|
-
"当前只有运行前设置和提示内容校验码,无法证明执行时真正收到了哪些内容。较早的运行不会事后补写这份记录。",
|
|
2807
|
-
),
|
|
2808
|
-
);
|
|
3028
|
+
missing.appendChild(el("p", "muted", "无法确认当时实际收到的材料。你可以在“执行设置”查看运行前的配置;较早的运行不会事后补写上下文记录。"));
|
|
2809
3029
|
root.appendChild(missing);
|
|
2810
3030
|
} 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
|
-
|
|
3031
|
+
const components = receipt.components ?? [];
|
|
3032
|
+
const savedCount = components.filter((part) => part.previewAvailable).length;
|
|
3033
|
+
const referenceCount = components.filter((part) => part.inclusion === "reference-only").length;
|
|
3034
|
+
const omittedCount = components.filter((part) => part.inclusion === "omitted").length;
|
|
3035
|
+
root.appendChild(el("p", "context-record-meta", [
|
|
3036
|
+
receipt.runtime?.model, receipt.capturedAt ? `记录于 ${new Date(receipt.capturedAt).toLocaleString("zh-CN", { hour12: false })}` : null,
|
|
3037
|
+
`${savedCount} 项可查看正文`, referenceCount ? `${referenceCount} 项仅有引用` : null,
|
|
3038
|
+
omittedCount ? `${omittedCount} 项未包含` : null,
|
|
3039
|
+
].filter(Boolean).join(" · ")));
|
|
3040
|
+
const captured = el("section", "node-input-section context-materials");
|
|
3041
|
+
captured.appendChild(el("h4", null, "这次尝试的材料"));
|
|
3042
|
+
captured.appendChild(el("p", "muted", "展开材料查看当时保存的正文。仅有引用的材料未保存独立正文。"));
|
|
3043
|
+
for (const component of components) {
|
|
3044
|
+
const details = el("details", "context-component");
|
|
3045
|
+
const summary = el("summary", null);
|
|
3046
|
+
const kind = inspectorValueLabel("kind", component.kind);
|
|
3047
|
+
const name = inspectorValueLabel("kind", component.logicalName || component.kind);
|
|
3048
|
+
summary.appendChild(el("span", "context-component-name", name));
|
|
3049
|
+
summary.appendChild(el("span", "context-component-meta", [name === kind ? null : kind, inspectorValueLabel("inclusion", component.inclusion)].filter(Boolean).join(" · ")));
|
|
3050
|
+
details.appendChild(summary);
|
|
3051
|
+
if (!component.previewAvailable) {
|
|
3052
|
+
details.appendChild(el("p", "muted", component.inclusion === "omitted" ? "这项材料未包含在本次上下文中。" : "仅登记了材料信息,未保存当时的独立正文。"));
|
|
3053
|
+
} else {
|
|
3054
|
+
let loading = false, loaded = false;
|
|
3055
|
+
const body = el("div", "context-component-body");
|
|
3056
|
+
details.appendChild(body);
|
|
3057
|
+
details.addEventListener("toggle", () => {
|
|
3058
|
+
if (!details.open || loaded || loading) return;
|
|
3059
|
+
loading = true; body.textContent = "正在读取保存的正文…";
|
|
3060
|
+
void fetchJson(`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(node.nodeId)}/context/component?attempt=${data.attempt}&componentIndex=${component.index}`).then((part) => {
|
|
3061
|
+
loading = false;
|
|
3062
|
+
if (!body.isConnected || workspaceScope() !== requestedScope) return;
|
|
3063
|
+
clearNode(body);
|
|
3064
|
+
if (typeof part?.content !== "string") { body.appendChild(el("p", "node-input-error", "正文不可用,收起后可重试。")); return; }
|
|
3065
|
+
loaded = true;
|
|
3066
|
+
body.appendChild(renderInspectorDocument(part.content));
|
|
3067
|
+
const copy = iconButton("复制已显示正文", "ri-file-copy-line");
|
|
3068
|
+
copy.type = "button";
|
|
3069
|
+
copy.addEventListener("click", async () => { const copied = await copyText(part.content); setIconButton(copy, copied ? "已复制" : "复制失败", copied ? "ri-check-line" : "ri-error-warning-line"); });
|
|
3070
|
+
body.appendChild(copy);
|
|
3071
|
+
if (part.truncated || part.redacted) body.appendChild(el("p", "muted", [part.truncated ? "正文预览已截断" : "", part.redacted ? "已脱敏" : ""].filter(Boolean).join(" · ")));
|
|
3072
|
+
});
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
const source = el("details", "context-source-details");
|
|
3076
|
+
source.appendChild(el("summary", null, "材料来源信息"));
|
|
3077
|
+
source.appendChild(transparencyMetaList([["来源", component.sourceIdentity], ["大小", `${component.bytes} 字节`], ["记录说明", component.omissionReason]]));
|
|
3078
|
+
details.appendChild(source);
|
|
3079
|
+
captured.appendChild(details);
|
|
3080
|
+
}
|
|
2840
3081
|
root.appendChild(captured);
|
|
3082
|
+
const diagnostics = el("details", "context-source-details");
|
|
3083
|
+
diagnostics.appendChild(el("summary", null, "来源与校验信息"));
|
|
3084
|
+
diagnostics.appendChild(transparencyMetaList([
|
|
3085
|
+
["context digest", receipt.contextDigest], ["captured at", receipt.capturedAt],
|
|
3086
|
+
["executor", receipt.runtime?.executor], ["components", receipt.componentCount],
|
|
3087
|
+
["truncations", receipt.truncationCount], ["skills", receipt.runtime?.skillCount],
|
|
3088
|
+
["extensions", receipt.runtime?.extensionCount], ["tools", receipt.runtime?.toolCount],
|
|
3089
|
+
]));
|
|
3090
|
+
root.appendChild(diagnostics);
|
|
2841
3091
|
}
|
|
2842
3092
|
content.appendChild(root);
|
|
2843
3093
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "context");
|
|
2844
3094
|
}
|
|
2845
3095
|
|
|
3096
|
+
async function recordedAttempt(dagRunId, node, key) {
|
|
3097
|
+
const [output, context] = await Promise.all([
|
|
3098
|
+
fetchJsonResult(`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(node.nodeId)}/execution-output?key=${encodeURIComponent(key)}`),
|
|
3099
|
+
/^attempt-/.test(key) ? fetchDagNodeContext(dagRunId, node.nodeId, Number(key.slice(8))) : Promise.resolve(null),
|
|
3100
|
+
]);
|
|
3101
|
+
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 };
|
|
3102
|
+
}
|
|
3103
|
+
function attemptDiagnostic(dagRunId, node, recorded) {
|
|
3104
|
+
const output = recorded.output;
|
|
3105
|
+
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");
|
|
3106
|
+
}
|
|
3107
|
+
async function renderAttemptComparison(content, dagRunId, node) {
|
|
3108
|
+
const options = attemptSelectorOptions(node);
|
|
3109
|
+
if (options.length < 2) return;
|
|
3110
|
+
const toolbar = el("div", "attempt-comparison-toolbar"), body = el("div", "attempt-comparison-columns");
|
|
3111
|
+
const selects = [];
|
|
3112
|
+
const reading = content._readingState;
|
|
3113
|
+
for (const [index, label] of ["左侧执行", "右侧执行"].entries()) {
|
|
3114
|
+
const select = el("select"); select.setAttribute("aria-label", label);
|
|
3115
|
+
for (const option of options) { const item = el("option", null, option.label); item.value = option.key; select.appendChild(item); }
|
|
3116
|
+
const previous = reading?.comparisonKeys?.[index];
|
|
3117
|
+
select.value = options.some((option) => option.key === previous) ? previous : options[Math.max(0, options.length - 2 + index)].key;
|
|
3118
|
+
toolbar.appendChild(select); selects.push(select);
|
|
3119
|
+
}
|
|
3120
|
+
content.appendChild(toolbar); content.appendChild(body);
|
|
3121
|
+
let generation = 0;
|
|
3122
|
+
const render = async () => {
|
|
3123
|
+
if (reading) reading.comparisonKeys = selects.map((select) => select.value);
|
|
3124
|
+
const request = ++generation; body.textContent = "正在读取各次执行记录…";
|
|
3125
|
+
const records = await Promise.all(selects.map((select) => recordedAttempt(dagRunId, node, select.value)));
|
|
3126
|
+
if (request !== generation || !body.isConnected) return;
|
|
3127
|
+
clearNode(body);
|
|
3128
|
+
for (const record of records) {
|
|
3129
|
+
const column = el("section", "attempt-comparison-column");
|
|
3130
|
+
column.appendChild(el("h4", null, options.find((item) => item.key === record.key)?.label));
|
|
3131
|
+
column.appendChild(transparencyMetaList([["状态", record.preview?.status ?? "未记录"], ["失败分类", record.preview?.failureCategory ?? "未记录"], ["耗时", record.preview?.durationMs != null ? formatMs(record.preview.durationMs) : "未记录"], ["模型", record.model ?? "未记录"]]));
|
|
3132
|
+
column.appendChild(el("p", "muted", record.output?.truncated ? "本次输出已截断" : "本次已保存的输出"));
|
|
3133
|
+
column.appendChild(renderInspectorDocument(record.output?.assistantText || record.output?.stdout || "未记录正文"));
|
|
3134
|
+
if (record.output?.stderr) column.appendChild(el("pre", "node-input-error", record.output.stderr));
|
|
3135
|
+
body.appendChild(column);
|
|
3136
|
+
}
|
|
3137
|
+
const left = records[0].output, right = records[1].output;
|
|
3138
|
+
const summary = el("p", "muted", left && right ? (JSON.stringify([left.assistantText, left.stdout, left.stderr]) === JSON.stringify([right.assistantText, right.stdout, right.stderr]) ? "两次已保存的输出相同。" : "两次已保存的输出有差异,可逐列对照;模型、耗时仅使用各次记录。") : "部分执行缺少正文,无法判断完整差异。");
|
|
3139
|
+
body.insertBefore(summary, body.firstChild);
|
|
3140
|
+
};
|
|
3141
|
+
for (const select of selects) select.addEventListener("change", () => { void render(); });
|
|
3142
|
+
await render();
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function inspectorNodeLink(dagRunId, nodeId, label = nodeId) {
|
|
3146
|
+
const link = el("a", "object-link", label);
|
|
3147
|
+
link.href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(dagRunId)}?node=${encodeURIComponent(nodeId)}&tab=overview`);
|
|
3148
|
+
return link;
|
|
3149
|
+
}
|
|
3150
|
+
function appendNodeLinks(parent, title, dagRunId, nodeIds) {
|
|
3151
|
+
if (!nodeIds.length) return;
|
|
3152
|
+
const section = el("div", "node-relation-links"); section.appendChild(el("h5", null, title));
|
|
3153
|
+
for (const id of nodeIds) section.appendChild(inspectorNodeLink(dagRunId, id));
|
|
3154
|
+
parent.appendChild(section);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
/** 长校验码截断显示:中段省略,完整值保留在 title(人工可读优先)。 */
|
|
3158
|
+
function shortHashDisplay(value) {
|
|
3159
|
+
const raw = String(value ?? "");
|
|
3160
|
+
if (raw.length <= 24) return raw;
|
|
3161
|
+
return `${raw.slice(0, 10)}…${raw.slice(-6)}`;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
2846
3164
|
function artifactInventorySection(data, dagRunId, nodeId) {
|
|
2847
3165
|
const root = el("section", "node-input-section node-artifact-inventory");
|
|
2848
3166
|
root.appendChild(el("h4", null, "本节点生成和使用的文件"));
|
|
3167
|
+
root.appendChild(el("p", "muted", "文件关系属于整个节点;下方执行输出随尝试切换。"));
|
|
2849
3168
|
if (!data?.binding?.available) {
|
|
2850
3169
|
const warning = el("div", "node-transparency-omission");
|
|
2851
3170
|
const bindingCode =
|
|
@@ -2864,11 +3183,19 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2864
3183
|
root.appendChild(warning);
|
|
2865
3184
|
return root;
|
|
2866
3185
|
}
|
|
3186
|
+
const graphSha = data.binding.artifactGraphSha256;
|
|
3187
|
+
const graphShaCell = graphSha
|
|
3188
|
+
? (() => {
|
|
3189
|
+
const code = el("code", "artifact-hash-code", shortHashDisplay(graphSha));
|
|
3190
|
+
code.setAttribute("title", `完整校验码:${graphSha}`);
|
|
3191
|
+
return code;
|
|
3192
|
+
})()
|
|
3193
|
+
: null;
|
|
2867
3194
|
root.appendChild(
|
|
2868
3195
|
transparencyMetaList([
|
|
2869
3196
|
["origin", data.origin?.kind ?? "unknown"],
|
|
2870
3197
|
["parent", data.origin?.parentRunId ?? null],
|
|
2871
|
-
["graph sha256",
|
|
3198
|
+
["graph sha256", graphShaCell],
|
|
2872
3199
|
]),
|
|
2873
3200
|
);
|
|
2874
3201
|
for (const artifact of data.produces ?? []) {
|
|
@@ -2894,11 +3221,7 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2894
3221
|
],
|
|
2895
3222
|
]),
|
|
2896
3223
|
);
|
|
2897
|
-
|
|
2898
|
-
card,
|
|
2899
|
-
"会直接使用此文件的节点",
|
|
2900
|
-
artifact.directConsumerNodeIds ?? [],
|
|
2901
|
-
);
|
|
3224
|
+
appendNodeLinks(card, "会直接使用此文件的节点", dagRunId, artifact.directConsumerNodeIds ?? []);
|
|
2902
3225
|
if (artifact.materialization?.status === "materialized") {
|
|
2903
3226
|
card.appendChild(
|
|
2904
3227
|
createArtifactContentPreview(dagRunId, nodeId, () => artifact, {
|
|
@@ -2913,7 +3236,7 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2913
3236
|
card.appendChild(el("h5", null, `使用 ${artifact.artifactId}`));
|
|
2914
3237
|
card.appendChild(
|
|
2915
3238
|
transparencyMetaList([
|
|
2916
|
-
["producer", artifact.producerNodeId],
|
|
3239
|
+
["producer", inspectorNodeLink(dagRunId, artifact.producerNodeId)],
|
|
2917
3240
|
["path", artifact.path],
|
|
2918
3241
|
["required", artifact.required ? "yes" : "no"],
|
|
2919
3242
|
]),
|
|
@@ -2943,6 +3266,23 @@ async function prependArtifactInventory(content, dagRunId, node) {
|
|
|
2943
3266
|
activeInspectorTab() !== "output"
|
|
2944
3267
|
)
|
|
2945
3268
|
return;
|
|
3269
|
+
// 失败节点优先暴露错误:没有登记任何文件关系时空卡不占首屏,
|
|
3270
|
+
// 「没有登记」这一事实由错误输出之后的一行说明保留。
|
|
3271
|
+
const body = result.ok ? result.body : null;
|
|
3272
|
+
const emptyInventory =
|
|
3273
|
+
body?.binding?.available &&
|
|
3274
|
+
(body.produces ?? []).length === 0 &&
|
|
3275
|
+
(body.consumes ?? []).length === 0;
|
|
3276
|
+
if (isNodeFailed(node?.status) && emptyInventory) {
|
|
3277
|
+
const note = el(
|
|
3278
|
+
"p",
|
|
3279
|
+
"empty node-artifact-inventory-empty",
|
|
3280
|
+
"该节点没有登记会生成或使用的文件。",
|
|
3281
|
+
);
|
|
3282
|
+
const lead = content.querySelector(".inspector-page-lead");
|
|
3283
|
+
content.insertBefore(note, lead?.nextSibling ?? content.firstChild);
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
2946
3286
|
const section =
|
|
2947
3287
|
result.ok && result.body
|
|
2948
3288
|
? artifactInventorySection(result.body, dagRunId, node.nodeId)
|
|
@@ -2979,15 +3319,15 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2979
3319
|
root.appendChild(
|
|
2980
3320
|
transparencyMetaList([
|
|
2981
3321
|
["运行类型", data.kind === "root" ? "首次运行" : "派生运行"],
|
|
2982
|
-
["来源运行", data.parentRunId
|
|
3322
|
+
["来源运行", data.parentRunId ? inspectorNodeLink(data.parentRunId, data.operatorSelectedNodeId || node.nodeId, data.parentRunId) : null],
|
|
2983
3323
|
["发起方式", inspectorValueLabel("发起方式", data.trigger)],
|
|
2984
3324
|
["人工选择节点", data.operatorSelectedNodeId ?? null],
|
|
2985
3325
|
["从此节点重新执行", data.effectiveFromNodeId ?? null],
|
|
2986
3326
|
["重跑计划哈希", data.planHash ?? null],
|
|
2987
3327
|
]),
|
|
2988
3328
|
);
|
|
2989
|
-
|
|
2990
|
-
|
|
3329
|
+
appendNodeLinks(root, "沿用既有结果的节点", dagRunId, data.importedNodeIds ?? []);
|
|
3330
|
+
appendNodeLinks(root, "替换结果的节点", dagRunId, data.overriddenNodeIds ?? []);
|
|
2991
3331
|
if (data.kind === "root")
|
|
2992
3332
|
root.appendChild(
|
|
2993
3333
|
el("p", "muted", "这是首次运行,没有来源运行。"),
|
|
@@ -2996,19 +3336,41 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2996
3336
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "lineage");
|
|
2997
3337
|
}
|
|
2998
3338
|
|
|
3339
|
+
function inspectorTabDefinitions(node = inspectorSelection.node) {
|
|
3340
|
+
const options = node ? attemptSelectorOptions(node) : [];
|
|
3341
|
+
const hasPassOnlyHistory = options[0]?.key.startsWith("pass-");
|
|
3342
|
+
const tabDefs = DAG_INSPECTOR_READ_ONLY_TABS.filter(([tab]) => {
|
|
3343
|
+
if (tab === "lineage") return Boolean(inspectorSelection.run?.continuation?.parentRunId);
|
|
3344
|
+
if (tab === "context") return !["shell", "static"].includes(node?.executor) && !hasPassOnlyHistory;
|
|
3345
|
+
if (tab === "timeline") return node?.executor !== "static";
|
|
3346
|
+
return true;
|
|
3347
|
+
}).map((definition) => [...definition]);
|
|
3348
|
+
if (options.length >= 2) {
|
|
3349
|
+
tabDefs.splice(tabDefs.findIndex(([tab]) => tab === "spec-evidence"), 0, ["compare", "尝试对比", "ri-layout-column-line"]);
|
|
3350
|
+
}
|
|
3351
|
+
// A scoped credential controls the mutation surface independently of data.
|
|
3352
|
+
if (globalThis.__INSPECT_MUTATION_TOKEN__) tabDefs.push([...DAG_INSPECTOR_MUTATION_TAB]);
|
|
3353
|
+
return tabDefs;
|
|
3354
|
+
}
|
|
3355
|
+
|
|
2999
3356
|
function activeInspectorTab() {
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
"timeline"
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3357
|
+
const available = inspectorTabDefinitions().map(([tab]) => tab);
|
|
3358
|
+
const requested = uiState.dagInspectorTab;
|
|
3359
|
+
if (available.includes(requested)) return requested;
|
|
3360
|
+
if (requested == null && ["running", "started"].includes(String(inspectorSelection.node?.status ?? "").toLowerCase())) {
|
|
3361
|
+
return available.includes("timeline") ? "timeline" : "overview";
|
|
3362
|
+
}
|
|
3363
|
+
if (requested === "compare" && available.includes("timeline")) return "timeline";
|
|
3364
|
+
return ["compare", "context", "timeline"].includes(requested) ? "overview" : "output";
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
function inspectorTabUsesAttempt(tab, node) {
|
|
3368
|
+
return tab === "timeline" || tab === "output" ||
|
|
3369
|
+
(tab === "context" && (node.executionSummary?.attempts?.length ?? 0) > 1);
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
function inspectorTabExecutionKey(dagRunId, node, tab = activeInspectorTab()) {
|
|
3373
|
+
return inspectorTabUsesAttempt(tab, node) ? effectiveExecutionKey(dagRunId, node) : null;
|
|
3012
3374
|
}
|
|
3013
3375
|
|
|
3014
3376
|
function saveInspectorContentScroll(
|
|
@@ -3019,7 +3381,14 @@ function saveInspectorContentScroll(
|
|
|
3019
3381
|
options = {},
|
|
3020
3382
|
) {
|
|
3021
3383
|
if (!content) return null;
|
|
3022
|
-
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab);
|
|
3384
|
+
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab, content.dataset.executionKey);
|
|
3385
|
+
if (content._readingState) {
|
|
3386
|
+
const reading = content._readingState;
|
|
3387
|
+
if (!(options.preserveIntent && content.scrollTop === 0 && reading.scrollTop > 0)) {
|
|
3388
|
+
reading.scrollTop = content.scrollTop;
|
|
3389
|
+
reading.scrollLeft = content.scrollLeft;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3023
3392
|
const saved = saveDagInspectorViewportState(
|
|
3024
3393
|
regionKey,
|
|
3025
3394
|
content.scrollLeft,
|
|
@@ -3055,7 +3424,8 @@ function saveInspectorContentScroll(
|
|
|
3055
3424
|
|
|
3056
3425
|
function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
3057
3426
|
if (!content) return;
|
|
3058
|
-
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab);
|
|
3427
|
+
const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab, content.dataset.executionKey);
|
|
3428
|
+
if (tab === "output" && !content.querySelector(".process-timeline-autoscroll-dock")) content.insertBefore(createTimelineAutoScrollControl(content), content.firstChild);
|
|
3059
3429
|
const apply = () => {
|
|
3060
3430
|
if (
|
|
3061
3431
|
!content.isConnected ||
|
|
@@ -3065,19 +3435,14 @@ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
|
3065
3435
|
) {
|
|
3066
3436
|
return;
|
|
3067
3437
|
}
|
|
3068
|
-
if (tab === "timeline"
|
|
3438
|
+
if ((tab === "timeline" || tab === "output") && content._readingState?.followLatest) {
|
|
3069
3439
|
content.scrollLeft = uiState.dagTimelineViewportState?.scrollLeft ?? 0;
|
|
3070
3440
|
content.scrollTop = content.scrollHeight;
|
|
3441
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3442
|
+
content._syncTimelineFollow?.();
|
|
3071
3443
|
return;
|
|
3072
3444
|
}
|
|
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
|
-
}
|
|
3445
|
+
|
|
3081
3446
|
const saved =
|
|
3082
3447
|
getDagInspectorViewportState(regionKey) ??
|
|
3083
3448
|
(tab === "input" &&
|
|
@@ -3092,8 +3457,10 @@ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
|
3092
3457
|
uiState.dagNodeSpecEvidenceViewportState?.regionKey === regionKey
|
|
3093
3458
|
? uiState.dagNodeSpecEvidenceViewportState
|
|
3094
3459
|
: null);
|
|
3095
|
-
content.scrollLeft = saved?.scrollLeft ?? 0;
|
|
3096
|
-
content.scrollTop = saved?.scrollTop ?? 0;
|
|
3460
|
+
content.scrollLeft = content._readingState?.scrollLeft ?? saved?.scrollLeft ?? 0;
|
|
3461
|
+
content.scrollTop = content._readingState?.scrollTop ?? saved?.scrollTop ?? 0;
|
|
3462
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3463
|
+
content._syncTimelineFollow?.();
|
|
3097
3464
|
};
|
|
3098
3465
|
apply();
|
|
3099
3466
|
if (typeof requestAnimationFrame === "function") {
|
|
@@ -3109,7 +3476,13 @@ function ensureInspectorContentScrollCapture(content, dagRunId, nodeId, tab) {
|
|
|
3109
3476
|
() => {
|
|
3110
3477
|
const runId = content.dataset.dagRunId;
|
|
3111
3478
|
const nId = content.dataset.dagNodeId;
|
|
3112
|
-
const active =
|
|
3479
|
+
const active = content.dataset.inspectorTab;
|
|
3480
|
+
if (["timeline", "output"].includes(active) && content._readingState && content.scrollTop < (content._lastRestoredTop ?? content._readingState.scrollTop) - 1 && content.scrollHeight - content.clientHeight - content.scrollTop > 48) {
|
|
3481
|
+
content._readingState.followLatest = false;
|
|
3482
|
+
uiState.dagTimelineAutoScrollEnabled = false;
|
|
3483
|
+
}
|
|
3484
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3485
|
+
content._syncTimelineFollow?.();
|
|
3113
3486
|
if (!runId || !nId || !active) return;
|
|
3114
3487
|
saveInspectorContentScroll(content, runId, nId, active, {
|
|
3115
3488
|
// Clearing a tall input pane for its loading shell can emit a native
|
|
@@ -3217,6 +3590,21 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3217
3590
|
|
|
3218
3591
|
const bar = el("div", "dag-inspector-attempt-bar");
|
|
3219
3592
|
bar.appendChild(el("label", "dag-inspector-attempt-label", "执行分次"));
|
|
3593
|
+
const choose = (nextKey, source = "select") => {
|
|
3594
|
+
uiState.dagInspectorAttempt = { dagRunId, nodeId: node.nodeId, key: String(nextKey) };
|
|
3595
|
+
inspectorNodeSelections.set(`${dagRunId}:${node.nodeId}`, uiState.dagInspectorAttempt);
|
|
3596
|
+
renderDagInspectorForCurrent();
|
|
3597
|
+
const nextBar = document.querySelector(".dag-inspector-header .dag-inspector-attempt-bar");
|
|
3598
|
+
const control = nextBar?.querySelector(`[data-attempt-action="${source}"]`);
|
|
3599
|
+
(control && !control.disabled ? control : nextBar?.querySelector(".custom-select-trigger"))?.focus({ preventScroll: true });
|
|
3600
|
+
};
|
|
3601
|
+
const selectedIndex = options.findIndex((option) => option.key === selectedKey);
|
|
3602
|
+
const previous = iconButton("上一次尝试", "ri-arrow-left-s-line");
|
|
3603
|
+
previous.type = "button";
|
|
3604
|
+
previous.dataset.attemptAction = "previous";
|
|
3605
|
+
previous.disabled = selectedIndex <= 0;
|
|
3606
|
+
previous.addEventListener("click", () => choose(options[selectedIndex - 1].key, "previous"));
|
|
3607
|
+
bar.appendChild(previous);
|
|
3220
3608
|
const select = createCustomSelect({
|
|
3221
3609
|
ariaLabel: "选择执行分次",
|
|
3222
3610
|
className: "dag-inspector-attempt-select",
|
|
@@ -3225,25 +3613,25 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3225
3613
|
value: option.key,
|
|
3226
3614
|
label: option.label,
|
|
3227
3615
|
})),
|
|
3228
|
-
onChange: (nextKey) =>
|
|
3229
|
-
uiState.dagInspectorAttempt = {
|
|
3230
|
-
dagRunId,
|
|
3231
|
-
nodeId: node.nodeId,
|
|
3232
|
-
key: String(nextKey),
|
|
3233
|
-
};
|
|
3234
|
-
renderDagInspectorForCurrent();
|
|
3235
|
-
},
|
|
3616
|
+
onChange: (nextKey) => choose(nextKey),
|
|
3236
3617
|
});
|
|
3237
3618
|
// Keep raw key on the root for existing integration assertions that read
|
|
3238
3619
|
// `.value` after assign + change (StubNode handler contract).
|
|
3239
3620
|
select.value = selectedKey;
|
|
3240
3621
|
bar.appendChild(select);
|
|
3622
|
+
const next = iconButton("下一次尝试", "ri-arrow-right-s-line");
|
|
3623
|
+
next.type = "button";
|
|
3624
|
+
next.dataset.attemptAction = "next";
|
|
3625
|
+
next.disabled = selectedIndex >= options.length - 1;
|
|
3626
|
+
next.addEventListener("click", () => choose(options[selectedIndex + 1].key, "next"));
|
|
3627
|
+
bar.appendChild(next);
|
|
3241
3628
|
|
|
3242
3629
|
const selected =
|
|
3243
3630
|
options.find((option) => option.key === selectedKey) ??
|
|
3244
3631
|
options[options.length - 1];
|
|
3245
3632
|
const data = selected.preview ?? {};
|
|
3246
3633
|
const preview = el("div", "dag-inspector-attempt-preview");
|
|
3634
|
+
preview.setAttribute("title", [data.failureCategory ? attemptFailureLabel(data.failureCategory) : "", data.durationMs != null ? `耗时 ${formatMs(data.durationMs)}` : ""].filter(Boolean).join(" · "));
|
|
3247
3635
|
const status = attemptPreviewStatus(data.status);
|
|
3248
3636
|
if (status) {
|
|
3249
3637
|
preview.appendChild(el("span", `badge badge-${status.cls}`, status.label));
|
|
@@ -3272,6 +3660,19 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3272
3660
|
// Historical bodies load via execution-output endpoint; do not surface
|
|
3273
3661
|
// raw artifact paths as the primary path for full text (AC-003/004).
|
|
3274
3662
|
bar.appendChild(preview);
|
|
3663
|
+
const diagnostic = iconButton("诊断摘要", "ri-file-info-line"); diagnostic.type = "button";
|
|
3664
|
+
diagnostic.addEventListener("click", async () => {
|
|
3665
|
+
diagnostic.disabled = true;
|
|
3666
|
+
try {
|
|
3667
|
+
const record = await recordedAttempt(dagRunId, node, selectedKey);
|
|
3668
|
+
const panel = document.querySelector(".dag-inspector-content");
|
|
3669
|
+
if (!panel || panel.dataset.dagNodeId !== node.nodeId || panel.dataset.executionKey !== selectedKey) return;
|
|
3670
|
+
panel.querySelector(".diagnostic-preview")?.remove();
|
|
3671
|
+
const detail = el("details", "diagnostic-preview"); detail.open = true; detail.appendChild(el("summary", null, "诊断摘要预览(仅当前尝试)"));
|
|
3672
|
+
const text = attemptDiagnostic(dagRunId, node, record); detail.appendChild(el("pre", "process-timeline-code", text));
|
|
3673
|
+
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;
|
|
3674
|
+
} finally { diagnostic.disabled = false; }
|
|
3675
|
+
}); bar.appendChild(diagnostic);
|
|
3275
3676
|
return { bar, selectedKey, latestKey, options };
|
|
3276
3677
|
}
|
|
3277
3678
|
|
|
@@ -3341,11 +3742,22 @@ function composeExecutionErrorText(payload) {
|
|
|
3341
3742
|
return typeof payload.stderr === "string" ? payload.stderr : "";
|
|
3342
3743
|
}
|
|
3343
3744
|
|
|
3745
|
+
function buildExecutionErrorSection(errorText) {
|
|
3746
|
+
const error = el("section", "markdown-error");
|
|
3747
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3748
|
+
error.appendChild(renderInspectorDocument(errorText));
|
|
3749
|
+
return error;
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3344
3752
|
function appendOutputBody(content, outputText, errorText, options = null) {
|
|
3345
3753
|
if (!outputText && !errorText) {
|
|
3346
3754
|
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
3347
3755
|
return;
|
|
3348
3756
|
}
|
|
3757
|
+
// 失败节点错误优先:先看错误,再对照正文(正文可能很长)。
|
|
3758
|
+
if (options?.priorityError === true && errorText) {
|
|
3759
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3760
|
+
}
|
|
3349
3761
|
if (outputText) content.appendChild(renderInspectorDocument(outputText));
|
|
3350
3762
|
// Truncation note only for full/cached execution-output bodies (AC-003);
|
|
3351
3763
|
// preview path never invents truncated/maxBytes.
|
|
@@ -3359,15 +3771,13 @@ function appendOutputBody(content, outputText, errorText, options = null) {
|
|
|
3359
3771
|
),
|
|
3360
3772
|
);
|
|
3361
3773
|
}
|
|
3362
|
-
if (errorText) {
|
|
3363
|
-
|
|
3364
|
-
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3365
|
-
error.appendChild(renderInspectorDocument(errorText));
|
|
3366
|
-
content.appendChild(error);
|
|
3774
|
+
if (errorText && options?.priorityError !== true) {
|
|
3775
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3367
3776
|
}
|
|
3368
3777
|
}
|
|
3369
3778
|
|
|
3370
3779
|
function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
3780
|
+
const priorityError = isNodeFailed(node?.status);
|
|
3371
3781
|
const cached = key
|
|
3372
3782
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, key)
|
|
3373
3783
|
: null;
|
|
@@ -3379,6 +3789,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3379
3789
|
{
|
|
3380
3790
|
truncated: cached.body.truncated === true,
|
|
3381
3791
|
maxBytes: cached.body.maxBytes,
|
|
3792
|
+
priorityError,
|
|
3382
3793
|
},
|
|
3383
3794
|
);
|
|
3384
3795
|
return "cached";
|
|
@@ -3388,6 +3799,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3388
3799
|
content,
|
|
3389
3800
|
node.outputPreview ?? "",
|
|
3390
3801
|
node.errorPreview ?? "",
|
|
3802
|
+
{ priorityError },
|
|
3391
3803
|
);
|
|
3392
3804
|
return "preview";
|
|
3393
3805
|
}
|
|
@@ -3653,39 +4065,7 @@ function renderNonPiProcess(content, body) {
|
|
|
3653
4065
|
ul.appendChild(li);
|
|
3654
4066
|
}
|
|
3655
4067
|
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);
|
|
4068
|
+
content.appendChild(createTimelineAutoScrollControl(content));
|
|
3689
4069
|
}
|
|
3690
4070
|
|
|
3691
4071
|
export async function mergeNodeProcess(dagRunId, node) {
|
|
@@ -3736,6 +4116,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3736
4116
|
if (sameNode && cached.kind === "pi-session") {
|
|
3737
4117
|
renderSessionTimeline(content, node.nodeId, {
|
|
3738
4118
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4119
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3739
4120
|
});
|
|
3740
4121
|
return;
|
|
3741
4122
|
}
|
|
@@ -3746,6 +4127,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3746
4127
|
if ((node.executor ?? "pi") === "pi") {
|
|
3747
4128
|
renderSessionTimeline(content, node.nodeId, {
|
|
3748
4129
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4130
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3749
4131
|
});
|
|
3750
4132
|
return;
|
|
3751
4133
|
}
|
|
@@ -3803,6 +4185,10 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3803
4185
|
content.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3804
4186
|
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
3805
4187
|
content.dataset.inspectorTab = activeTab;
|
|
4188
|
+
content.dataset.executionKey = inspectorTabExecutionKey(dagRunId, node, activeTab) ?? "";
|
|
4189
|
+
content._readingState = inspectorReadingState(dagRunId, node.nodeId, activeTab, content.dataset.executionKey, isLatestExecutionSelection(dagRunId, node) && ["running", "pending"].includes(String(node.status ?? "").toLowerCase()));
|
|
4190
|
+
uiState.dagTimelineAutoScrollEnabled = content._readingState.followLatest;
|
|
4191
|
+
uiState.dagTimelineViewportState = { ...content._readingState, dagRunId, nodeId: node.nodeId };
|
|
3806
4192
|
// Drop timeline-only classes when switching tabs so output layout stays clean.
|
|
3807
4193
|
content.className = "dag-inspector-content";
|
|
3808
4194
|
if (activeTab !== "prompt-restart") {
|
|
@@ -3814,7 +4200,9 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3814
4200
|
node.nodeId,
|
|
3815
4201
|
activeTab,
|
|
3816
4202
|
);
|
|
3817
|
-
if (activeTab === "
|
|
4203
|
+
if (activeTab === "compare") {
|
|
4204
|
+
void renderAttemptComparison(content, dagRunId, node);
|
|
4205
|
+
} else if (activeTab === "overview") {
|
|
3818
4206
|
renderNodeOverview(content, dagRunId, node);
|
|
3819
4207
|
} else if (activeTab === "context") {
|
|
3820
4208
|
void renderEffectiveContext(content, dagRunId, node);
|
|
@@ -3827,8 +4215,7 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3827
4215
|
void renderPromptRestart(content, dagRunId, node, run);
|
|
3828
4216
|
} else if (activeTab === "timeline") {
|
|
3829
4217
|
ensureInspectorPageLead(content, "timeline");
|
|
3830
|
-
|
|
3831
|
-
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
4218
|
+
|
|
3832
4219
|
renderExecutorAwareTimeline(content, dagRunId, node);
|
|
3833
4220
|
// Initial content is rendered before it is attached. Restore again on the
|
|
3834
4221
|
// connected animation frame so live-follow sees the final scroll height.
|
|
@@ -3838,8 +4225,7 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3838
4225
|
} else {
|
|
3839
4226
|
// output tab: attempt bar + latest preview immediate / historical async body.
|
|
3840
4227
|
ensureInspectorPageLead(content, "output");
|
|
3841
|
-
|
|
3842
|
-
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
4228
|
+
|
|
3843
4229
|
const key = effectiveExecutionKey(dagRunId, node);
|
|
3844
4230
|
const latest = isLatestExecutionSelection(dagRunId, node);
|
|
3845
4231
|
const rendered = renderCachedOrPreviewBody(
|
|
@@ -3873,46 +4259,49 @@ function inspectorNodePurpose(node) {
|
|
|
3873
4259
|
return describeDagNodePurpose(node?.nodeId, node);
|
|
3874
4260
|
}
|
|
3875
4261
|
|
|
4262
|
+
/** 纯函数:Tab 条两端是否有被裁切内容(驱动渐隐提示)。 */
|
|
4263
|
+
export function inspectorTabOverflowState({ scrollLeft = 0, clientWidth = 0, scrollWidth = 0 } = {}) {
|
|
4264
|
+
const maxScroll = Math.max(0, scrollWidth - clientWidth);
|
|
4265
|
+
return {
|
|
4266
|
+
start: scrollLeft > 1 && maxScroll > 0,
|
|
4267
|
+
end: scrollLeft < maxScroll - 1 && maxScroll > 0,
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
function syncInspectorTabOverflowClasses(tabs) {
|
|
4272
|
+
if (!tabs?.classList) return;
|
|
4273
|
+
const state = inspectorTabOverflowState({
|
|
4274
|
+
scrollLeft: tabs.scrollLeft ?? 0,
|
|
4275
|
+
clientWidth: tabs.clientWidth ?? 0,
|
|
4276
|
+
scrollWidth: tabs.scrollWidth ?? 0,
|
|
4277
|
+
});
|
|
4278
|
+
tabs.classList.toggle("is-overflow-start", state.start);
|
|
4279
|
+
tabs.classList.toggle("is-overflow-end", state.end);
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
/** 选中 Tab 保持可见:只滚动 Tab 条自身,不牵动外层页面。 */
|
|
4283
|
+
function revealInspectorTab(tabs, button) {
|
|
4284
|
+
if (!tabs || !button) return;
|
|
4285
|
+
const left = button.offsetLeft;
|
|
4286
|
+
const right = left + (button.offsetWidth ?? 0);
|
|
4287
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return;
|
|
4288
|
+
if (left < tabs.scrollLeft + 8) tabs.scrollLeft = Math.max(0, left - 8);
|
|
4289
|
+
else if (right > tabs.scrollLeft + tabs.clientWidth - 8) {
|
|
4290
|
+
tabs.scrollLeft = right - tabs.clientWidth + 8;
|
|
4291
|
+
}
|
|
4292
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
3876
4295
|
function appendInspectorPurpose(title, node) {
|
|
3877
4296
|
const purpose = inspectorNodePurpose(node);
|
|
3878
4297
|
if (!purpose) return;
|
|
3879
4298
|
title.appendChild(el("p", "dag-inspector-purpose", purpose));
|
|
3880
4299
|
}
|
|
3881
4300
|
|
|
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
|
-
}
|
|
4301
|
+
function buildDagInspectorTabs(node) {
|
|
4302
|
+
const tabDefs = inspectorTabDefinitions(node);
|
|
3915
4303
|
const tabs = el("div", "dag-inspector-tabs");
|
|
4304
|
+
tabs.dataset.tabsSignature = tabDefs.map(([tab]) => tab).join(",");
|
|
3916
4305
|
tabs.setAttribute("role", "tablist");
|
|
3917
4306
|
tabs.setAttribute("aria-label", "节点检查器面板");
|
|
3918
4307
|
for (const [tab, label, icon] of tabDefs) {
|
|
@@ -3935,6 +4324,10 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3935
4324
|
renderDagInspectorForCurrent();
|
|
3936
4325
|
const next = document.getElementById(`dag-inspector-tab-${tab}`);
|
|
3937
4326
|
next?.focus();
|
|
4327
|
+
revealInspectorTab(
|
|
4328
|
+
document.querySelector(".dag-inspector-tabs"),
|
|
4329
|
+
next,
|
|
4330
|
+
);
|
|
3938
4331
|
});
|
|
3939
4332
|
button.addEventListener("keydown", (event) => {
|
|
3940
4333
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
@@ -3947,17 +4340,106 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3947
4340
|
const nextTab = order[(idx + delta + order.length) % order.length];
|
|
3948
4341
|
uiState.dagInspectorTab = nextTab;
|
|
3949
4342
|
renderDagInspectorForCurrent();
|
|
3950
|
-
document.getElementById(`dag-inspector-tab-${nextTab}`)
|
|
4343
|
+
const next = document.getElementById(`dag-inspector-tab-${nextTab}`);
|
|
4344
|
+
next?.focus();
|
|
4345
|
+
revealInspectorTab(document.querySelector(".dag-inspector-tabs"), next);
|
|
3951
4346
|
});
|
|
3952
4347
|
tabs.appendChild(button);
|
|
3953
4348
|
}
|
|
4349
|
+
// 溢出渐隐与选中可见性:挂载后测量(构建时 offsetLeft 不可用)。
|
|
4350
|
+
tabs.addEventListener("scroll", () => syncInspectorTabOverflowClasses(tabs), { passive: true });
|
|
4351
|
+
if (typeof ResizeObserver === "function") {
|
|
4352
|
+
const overflowObserver = new ResizeObserver(() => {
|
|
4353
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4354
|
+
});
|
|
4355
|
+
overflowObserver.observe(tabs);
|
|
4356
|
+
}
|
|
4357
|
+
if (typeof requestAnimationFrame === "function") {
|
|
4358
|
+
requestAnimationFrame(() => {
|
|
4359
|
+
if (!tabs.isConnected) return;
|
|
4360
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4361
|
+
const selected = tabs.querySelector("[role='tab'][aria-selected='true']");
|
|
4362
|
+
revealInspectorTab(tabs, selected);
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
return tabs;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
function buildDagInspectorHeader(dagRunId, node) {
|
|
4369
|
+
const header = el("div", "dag-inspector-header");
|
|
4370
|
+
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
4371
|
+
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
4372
|
+
const title = el("div", "dag-inspector-title");
|
|
4373
|
+
title.appendChild(el("h3", null, "节点检查器"));
|
|
4374
|
+
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
4375
|
+
appendInspectorPurpose(title, node);
|
|
4376
|
+
header.appendChild(title);
|
|
4377
|
+
const controls = el("div", "dag-inspector-controls");
|
|
4378
|
+
const status = badge(node.status ?? "unknown");
|
|
4379
|
+
status.classList.add("dag-inspector-status");
|
|
4380
|
+
controls.appendChild(status);
|
|
4381
|
+
const expand = iconButton(uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4382
|
+
expand.type = "button";
|
|
4383
|
+
expand.setAttribute("aria-pressed", String(Boolean(uiState.dagInspectorExpanded)));
|
|
4384
|
+
expand.addEventListener("click", () => {
|
|
4385
|
+
uiState.dagInspectorExpanded = !uiState.dagInspectorExpanded;
|
|
4386
|
+
document.getElementById("dag-inspector")?.classList.toggle("is-expanded", uiState.dagInspectorExpanded);
|
|
4387
|
+
setIconButton(expand, uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4388
|
+
expand.setAttribute("aria-pressed", String(uiState.dagInspectorExpanded));
|
|
4389
|
+
});
|
|
4390
|
+
controls.appendChild(expand);
|
|
4391
|
+
const share = iconButton("复制位置", "ri-link");
|
|
4392
|
+
share.type = "button";
|
|
4393
|
+
share.addEventListener("click", async () => {
|
|
4394
|
+
const selection = inspectorSelection;
|
|
4395
|
+
const query = new URLSearchParams({ node: selection.node.nodeId, tab: activeInspectorTab() });
|
|
4396
|
+
const key = effectiveExecutionKey(selection.dagRunId, selection.node);
|
|
4397
|
+
if (key) query.set("attempt", key);
|
|
4398
|
+
const href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(selection.dagRunId)}?${query}`);
|
|
4399
|
+
const copied = await copyText(new URL(href, location.href).href);
|
|
4400
|
+
setIconButton(share, copied ? "已复制位置" : "复制失败", copied ? "ri-check-line" : "ri-error-warning-line");
|
|
4401
|
+
});
|
|
4402
|
+
controls.appendChild(share);
|
|
4403
|
+
const close = document.createElement("button");
|
|
4404
|
+
close.type = "button";
|
|
4405
|
+
close.className = "dag-inspector-close";
|
|
4406
|
+
close.setAttribute("aria-label", "关闭节点检查器");
|
|
4407
|
+
const closeIcon = el("i", "ri-close-line");
|
|
4408
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
4409
|
+
close.appendChild(closeIcon);
|
|
4410
|
+
close.addEventListener("click", closeDagInspector);
|
|
4411
|
+
controls.appendChild(close);
|
|
4412
|
+
header.appendChild(controls);
|
|
4413
|
+
|
|
4414
|
+
const tabs = buildDagInspectorTabs(node);
|
|
3954
4415
|
header.appendChild(tabs);
|
|
4416
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4417
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4418
|
+
header.dataset.attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
3955
4419
|
return header;
|
|
3956
4420
|
}
|
|
3957
4421
|
|
|
3958
4422
|
function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
3959
4423
|
const header = panel.querySelector(".dag-inspector-header");
|
|
3960
4424
|
if (!header) return;
|
|
4425
|
+
const tabs = header.querySelector(".dag-inspector-tabs");
|
|
4426
|
+
const tabsSignature = inspectorTabDefinitions(node).map(([tab]) => tab).join(",");
|
|
4427
|
+
if (tabs && tabs.dataset.tabsSignature !== tabsSignature) {
|
|
4428
|
+
const restoreFocus = document.activeElement?.getAttribute("role") === "tab" && tabs.contains(document.activeElement);
|
|
4429
|
+
const nextTabs = buildDagInspectorTabs(node);
|
|
4430
|
+
const scrollLeft = tabs.scrollLeft;
|
|
4431
|
+
tabs.replaceWith(nextTabs);
|
|
4432
|
+
nextTabs.scrollLeft = scrollLeft;
|
|
4433
|
+
syncInspectorTabOverflowClasses(nextTabs);
|
|
4434
|
+
if (restoreFocus) document.getElementById(`dag-inspector-tab-${activeInspectorTab()}`)?.focus();
|
|
4435
|
+
}
|
|
4436
|
+
const attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
4437
|
+
if (header.dataset.dagNodeId !== String(node.nodeId) || header.dataset.attemptSignature !== attemptSignature) {
|
|
4438
|
+
header.querySelector(".dag-inspector-attempt-bar")?.remove();
|
|
4439
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4440
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4441
|
+
header.dataset.attemptSignature = attemptSignature;
|
|
4442
|
+
}
|
|
3961
4443
|
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
3962
4444
|
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3963
4445
|
const nodeLabel = header.querySelector(".dag-inspector-node");
|
|
@@ -3987,7 +4469,7 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
|
3987
4469
|
|
|
3988
4470
|
function dagInspectorSignature(dagRunId, node) {
|
|
3989
4471
|
const tab = activeInspectorTab();
|
|
3990
|
-
const execKey =
|
|
4472
|
+
const execKey = inspectorTabExecutionKey(dagRunId, node, tab);
|
|
3991
4473
|
const cachedBody = execKey
|
|
3992
4474
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, execKey)
|
|
3993
4475
|
: null;
|
|
@@ -4024,11 +4506,11 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
4024
4506
|
// the DOM while selection/history are unchanged; switching attempt or a
|
|
4025
4507
|
// changed attempt count rebuilds the selector.
|
|
4026
4508
|
const summary = node.executionSummary;
|
|
4027
|
-
const attemptRevision = [
|
|
4509
|
+
const attemptRevision = tab === "compare" ? attemptSelectorOptions(node) : inspectorTabUsesAttempt(tab, node) ? [
|
|
4028
4510
|
summary?.totalAttemptCount ?? 0,
|
|
4029
4511
|
(node.passes ?? []).join(","),
|
|
4030
4512
|
execKey ?? "latest",
|
|
4031
|
-
];
|
|
4513
|
+
] : null;
|
|
4032
4514
|
return JSON.stringify([
|
|
4033
4515
|
dagRunId,
|
|
4034
4516
|
node.nodeId,
|
|
@@ -4047,6 +4529,8 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4047
4529
|
inspectorSelection.run = run;
|
|
4048
4530
|
const panel = document.getElementById("dag-inspector");
|
|
4049
4531
|
if (!panel) return;
|
|
4532
|
+
// 运行状态标记:回到底部按钮由滚动位置自行显隐(终态同样可用),此标记仅供诊断。
|
|
4533
|
+
panel.dataset.runEnded = run && !isDagRunActive(run) ? "1" : "0";
|
|
4050
4534
|
captureDagTimelineViewportState(panel);
|
|
4051
4535
|
if (!uiState.dagInspectorOpen || !node) {
|
|
4052
4536
|
panel.classList.remove("is-open");
|
|
@@ -4054,6 +4538,8 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4054
4538
|
return;
|
|
4055
4539
|
}
|
|
4056
4540
|
|
|
4541
|
+
uiState.dagInspectorTab = activeInspectorTab();
|
|
4542
|
+
|
|
4057
4543
|
// Defer tearing down the inspector shell while the user is actively
|
|
4058
4544
|
// dragging the resize handle (or the graph) so the pointer-capture target
|
|
4059
4545
|
// is not destroyed mid-gesture; the next poll refreshes the content.
|
|
@@ -4073,6 +4559,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4073
4559
|
if (panel.dataset.contentSignature === nextContentSignature) return;
|
|
4074
4560
|
const previousNode = existingContent.dataset.dagNodeId ?? "";
|
|
4075
4561
|
const previousRun = existingContent.dataset.dagRunId ?? "";
|
|
4562
|
+
const previousExecutionKey = existingContent.dataset.executionKey;
|
|
4076
4563
|
const previousTab =
|
|
4077
4564
|
existingContent.dataset.inspectorTab ?? activeInspectorTab();
|
|
4078
4565
|
const nodeChanged =
|
|
@@ -4104,7 +4591,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4104
4591
|
activeTab,
|
|
4105
4592
|
);
|
|
4106
4593
|
}
|
|
4107
|
-
if (!nodeChanged && previousTab === activeTab) {
|
|
4594
|
+
if (!nodeChanged && previousTab === activeTab && previousExecutionKey === existingContent.dataset.executionKey) {
|
|
4108
4595
|
for (const [index, pre] of [
|
|
4109
4596
|
...existingContent.querySelectorAll("pre"),
|
|
4110
4597
|
].entries()) {
|
|
@@ -4121,6 +4608,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4121
4608
|
clearNode(panel);
|
|
4122
4609
|
panel.classList.add("is-open");
|
|
4123
4610
|
panel.setAttribute("aria-hidden", "false");
|
|
4611
|
+
panel.classList.toggle("is-expanded", Boolean(uiState.dagInspectorExpanded));
|
|
4124
4612
|
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
4125
4613
|
panel.appendChild(buildDagInspectorHeader(dagRunId, node));
|
|
4126
4614
|
|