@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11
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 +144 -9
- 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 +1237 -586
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +8 -4
- 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 +113 -0
- package/dist/worker/console/chat/browser-routes.js +140 -0
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +207 -1178
- package/dist/worker/console/chat/routes.js +208 -71
- 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 +49 -0
- package/dist/worker/console/chat/session-mode.js +219 -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 +348 -0
- package/dist/worker/console/chat/terminal-tools.js +211 -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-CTSK0xbU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
- package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
- package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
- package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- 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/compaction-message.js +7 -0
- 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/turn-process-disclosure.js +10 -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 +44 -24
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
- 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 +263 -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 +742 -268
- 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-closeout.js +1 -1
- 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-prewrite-gate.js +3 -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-context.js +2 -1
- 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/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +65 -167
- 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 +14 -13
- package/package.json +6 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +22 -4
- package/skills/frontend-design-review/SKILL.md +22 -16
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +21 -2
- package/skills/frontend-plan/references/decision-contract.md +42 -23
- package/skills/frontend-review/SKILL.md +22 -29
- package/skills/frontend-review/references/review-findings.md +24 -43
- 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,13 @@ 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) => {
|
|
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
|
+
nextBar?.querySelector(".custom-select-trigger")?.focus({ preventScroll: true });
|
|
3599
|
+
};
|
|
3220
3600
|
const select = createCustomSelect({
|
|
3221
3601
|
ariaLabel: "选择执行分次",
|
|
3222
3602
|
className: "dag-inspector-attempt-select",
|
|
@@ -3225,14 +3605,7 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3225
3605
|
value: option.key,
|
|
3226
3606
|
label: option.label,
|
|
3227
3607
|
})),
|
|
3228
|
-
onChange: (nextKey) =>
|
|
3229
|
-
uiState.dagInspectorAttempt = {
|
|
3230
|
-
dagRunId,
|
|
3231
|
-
nodeId: node.nodeId,
|
|
3232
|
-
key: String(nextKey),
|
|
3233
|
-
};
|
|
3234
|
-
renderDagInspectorForCurrent();
|
|
3235
|
-
},
|
|
3608
|
+
onChange: (nextKey) => choose(nextKey),
|
|
3236
3609
|
});
|
|
3237
3610
|
// Keep raw key on the root for existing integration assertions that read
|
|
3238
3611
|
// `.value` after assign + change (StubNode handler contract).
|
|
@@ -3244,20 +3617,21 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3244
3617
|
options[options.length - 1];
|
|
3245
3618
|
const data = selected.preview ?? {};
|
|
3246
3619
|
const preview = el("div", "dag-inspector-attempt-preview");
|
|
3620
|
+
preview.setAttribute("title", [data.failureCategory ? attemptFailureLabel(data.failureCategory) : "", data.durationMs != null ? `耗时 ${formatMs(data.durationMs)}` : ""].filter(Boolean).join(" · "));
|
|
3247
3621
|
const status = attemptPreviewStatus(data.status);
|
|
3248
3622
|
if (status) {
|
|
3249
3623
|
preview.appendChild(el("span", `badge badge-${status.cls}`, status.label));
|
|
3250
3624
|
}
|
|
3251
|
-
if (status?.cls === "failed"
|
|
3625
|
+
if (status?.cls === "failed") {
|
|
3252
3626
|
// 仅失败尝试显示原因;部分 runtime 会在成功尝试保留
|
|
3253
3627
|
// failureCategory="success",不能将其误映射为「其他失败原因」。
|
|
3254
3628
|
// 未知失败分类仍 fail-closed,raw token 仅保留在 title 诊断中。
|
|
3255
3629
|
const reason = el(
|
|
3256
3630
|
"span",
|
|
3257
3631
|
"dag-inspector-attempt-category",
|
|
3258
|
-
`原因 ${attemptFailureLabel(data.failureCategory)}
|
|
3632
|
+
data.failureCategory ? `原因 ${attemptFailureLabel(data.failureCategory)}` : "未记录失败原因",
|
|
3259
3633
|
);
|
|
3260
|
-
reason.setAttribute("title", `failureCategory ${data.failureCategory}`);
|
|
3634
|
+
if (data.failureCategory) reason.setAttribute("title", `failureCategory ${data.failureCategory}`);
|
|
3261
3635
|
preview.appendChild(reason);
|
|
3262
3636
|
}
|
|
3263
3637
|
if (data.durationMs !== undefined) {
|
|
@@ -3272,6 +3646,19 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3272
3646
|
// Historical bodies load via execution-output endpoint; do not surface
|
|
3273
3647
|
// raw artifact paths as the primary path for full text (AC-003/004).
|
|
3274
3648
|
bar.appendChild(preview);
|
|
3649
|
+
const diagnostic = iconButton("诊断摘要", "ri-file-info-line"); diagnostic.type = "button";
|
|
3650
|
+
diagnostic.addEventListener("click", async () => {
|
|
3651
|
+
diagnostic.disabled = true;
|
|
3652
|
+
try {
|
|
3653
|
+
const record = await recordedAttempt(dagRunId, node, selectedKey);
|
|
3654
|
+
const panel = document.querySelector(".dag-inspector-content");
|
|
3655
|
+
if (!panel || panel.dataset.dagNodeId !== node.nodeId || panel.dataset.executionKey !== selectedKey) return;
|
|
3656
|
+
panel.querySelector(".diagnostic-preview")?.remove();
|
|
3657
|
+
const detail = el("details", "diagnostic-preview"); detail.open = true; detail.appendChild(el("summary", null, "诊断摘要预览(仅当前尝试)"));
|
|
3658
|
+
const text = attemptDiagnostic(dagRunId, node, record); detail.appendChild(el("pre", "process-timeline-code", text));
|
|
3659
|
+
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;
|
|
3660
|
+
} finally { diagnostic.disabled = false; }
|
|
3661
|
+
}); bar.appendChild(diagnostic);
|
|
3275
3662
|
return { bar, selectedKey, latestKey, options };
|
|
3276
3663
|
}
|
|
3277
3664
|
|
|
@@ -3341,11 +3728,22 @@ function composeExecutionErrorText(payload) {
|
|
|
3341
3728
|
return typeof payload.stderr === "string" ? payload.stderr : "";
|
|
3342
3729
|
}
|
|
3343
3730
|
|
|
3731
|
+
function buildExecutionErrorSection(errorText) {
|
|
3732
|
+
const error = el("section", "markdown-error");
|
|
3733
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3734
|
+
error.appendChild(renderInspectorDocument(errorText));
|
|
3735
|
+
return error;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3344
3738
|
function appendOutputBody(content, outputText, errorText, options = null) {
|
|
3345
3739
|
if (!outputText && !errorText) {
|
|
3346
3740
|
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
3347
3741
|
return;
|
|
3348
3742
|
}
|
|
3743
|
+
// 失败节点错误优先:先看错误,再对照正文(正文可能很长)。
|
|
3744
|
+
if (options?.priorityError === true && errorText) {
|
|
3745
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3746
|
+
}
|
|
3349
3747
|
if (outputText) content.appendChild(renderInspectorDocument(outputText));
|
|
3350
3748
|
// Truncation note only for full/cached execution-output bodies (AC-003);
|
|
3351
3749
|
// preview path never invents truncated/maxBytes.
|
|
@@ -3359,15 +3757,13 @@ function appendOutputBody(content, outputText, errorText, options = null) {
|
|
|
3359
3757
|
),
|
|
3360
3758
|
);
|
|
3361
3759
|
}
|
|
3362
|
-
if (errorText) {
|
|
3363
|
-
|
|
3364
|
-
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3365
|
-
error.appendChild(renderInspectorDocument(errorText));
|
|
3366
|
-
content.appendChild(error);
|
|
3760
|
+
if (errorText && options?.priorityError !== true) {
|
|
3761
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3367
3762
|
}
|
|
3368
3763
|
}
|
|
3369
3764
|
|
|
3370
3765
|
function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
3766
|
+
const priorityError = isNodeFailed(node?.status);
|
|
3371
3767
|
const cached = key
|
|
3372
3768
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, key)
|
|
3373
3769
|
: null;
|
|
@@ -3379,6 +3775,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3379
3775
|
{
|
|
3380
3776
|
truncated: cached.body.truncated === true,
|
|
3381
3777
|
maxBytes: cached.body.maxBytes,
|
|
3778
|
+
priorityError,
|
|
3382
3779
|
},
|
|
3383
3780
|
);
|
|
3384
3781
|
return "cached";
|
|
@@ -3388,6 +3785,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3388
3785
|
content,
|
|
3389
3786
|
node.outputPreview ?? "",
|
|
3390
3787
|
node.errorPreview ?? "",
|
|
3788
|
+
{ priorityError },
|
|
3391
3789
|
);
|
|
3392
3790
|
return "preview";
|
|
3393
3791
|
}
|
|
@@ -3653,39 +4051,7 @@ function renderNonPiProcess(content, body) {
|
|
|
3653
4051
|
ul.appendChild(li);
|
|
3654
4052
|
}
|
|
3655
4053
|
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);
|
|
4054
|
+
content.appendChild(createTimelineAutoScrollControl(content));
|
|
3689
4055
|
}
|
|
3690
4056
|
|
|
3691
4057
|
export async function mergeNodeProcess(dagRunId, node) {
|
|
@@ -3736,6 +4102,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3736
4102
|
if (sameNode && cached.kind === "pi-session") {
|
|
3737
4103
|
renderSessionTimeline(content, node.nodeId, {
|
|
3738
4104
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4105
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3739
4106
|
});
|
|
3740
4107
|
return;
|
|
3741
4108
|
}
|
|
@@ -3746,6 +4113,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3746
4113
|
if ((node.executor ?? "pi") === "pi") {
|
|
3747
4114
|
renderSessionTimeline(content, node.nodeId, {
|
|
3748
4115
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4116
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3749
4117
|
});
|
|
3750
4118
|
return;
|
|
3751
4119
|
}
|
|
@@ -3803,6 +4171,10 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3803
4171
|
content.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3804
4172
|
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
3805
4173
|
content.dataset.inspectorTab = activeTab;
|
|
4174
|
+
content.dataset.executionKey = inspectorTabExecutionKey(dagRunId, node, activeTab) ?? "";
|
|
4175
|
+
content._readingState = inspectorReadingState(dagRunId, node.nodeId, activeTab, content.dataset.executionKey, isLatestExecutionSelection(dagRunId, node) && ["running", "pending"].includes(String(node.status ?? "").toLowerCase()));
|
|
4176
|
+
uiState.dagTimelineAutoScrollEnabled = content._readingState.followLatest;
|
|
4177
|
+
uiState.dagTimelineViewportState = { ...content._readingState, dagRunId, nodeId: node.nodeId };
|
|
3806
4178
|
// Drop timeline-only classes when switching tabs so output layout stays clean.
|
|
3807
4179
|
content.className = "dag-inspector-content";
|
|
3808
4180
|
if (activeTab !== "prompt-restart") {
|
|
@@ -3814,7 +4186,9 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3814
4186
|
node.nodeId,
|
|
3815
4187
|
activeTab,
|
|
3816
4188
|
);
|
|
3817
|
-
if (activeTab === "
|
|
4189
|
+
if (activeTab === "compare") {
|
|
4190
|
+
void renderAttemptComparison(content, dagRunId, node);
|
|
4191
|
+
} else if (activeTab === "overview") {
|
|
3818
4192
|
renderNodeOverview(content, dagRunId, node);
|
|
3819
4193
|
} else if (activeTab === "context") {
|
|
3820
4194
|
void renderEffectiveContext(content, dagRunId, node);
|
|
@@ -3827,8 +4201,7 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3827
4201
|
void renderPromptRestart(content, dagRunId, node, run);
|
|
3828
4202
|
} else if (activeTab === "timeline") {
|
|
3829
4203
|
ensureInspectorPageLead(content, "timeline");
|
|
3830
|
-
|
|
3831
|
-
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
4204
|
+
|
|
3832
4205
|
renderExecutorAwareTimeline(content, dagRunId, node);
|
|
3833
4206
|
// Initial content is rendered before it is attached. Restore again on the
|
|
3834
4207
|
// connected animation frame so live-follow sees the final scroll height.
|
|
@@ -3838,8 +4211,7 @@ function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
|
3838
4211
|
} else {
|
|
3839
4212
|
// output tab: attempt bar + latest preview immediate / historical async body.
|
|
3840
4213
|
ensureInspectorPageLead(content, "output");
|
|
3841
|
-
|
|
3842
|
-
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
4214
|
+
|
|
3843
4215
|
const key = effectiveExecutionKey(dagRunId, node);
|
|
3844
4216
|
const latest = isLatestExecutionSelection(dagRunId, node);
|
|
3845
4217
|
const rendered = renderCachedOrPreviewBody(
|
|
@@ -3873,46 +4245,49 @@ function inspectorNodePurpose(node) {
|
|
|
3873
4245
|
return describeDagNodePurpose(node?.nodeId, node);
|
|
3874
4246
|
}
|
|
3875
4247
|
|
|
4248
|
+
/** 纯函数:Tab 条两端是否有被裁切内容(驱动渐隐提示)。 */
|
|
4249
|
+
export function inspectorTabOverflowState({ scrollLeft = 0, clientWidth = 0, scrollWidth = 0 } = {}) {
|
|
4250
|
+
const maxScroll = Math.max(0, scrollWidth - clientWidth);
|
|
4251
|
+
return {
|
|
4252
|
+
start: scrollLeft > 1 && maxScroll > 0,
|
|
4253
|
+
end: scrollLeft < maxScroll - 1 && maxScroll > 0,
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
function syncInspectorTabOverflowClasses(tabs) {
|
|
4258
|
+
if (!tabs?.classList) return;
|
|
4259
|
+
const state = inspectorTabOverflowState({
|
|
4260
|
+
scrollLeft: tabs.scrollLeft ?? 0,
|
|
4261
|
+
clientWidth: tabs.clientWidth ?? 0,
|
|
4262
|
+
scrollWidth: tabs.scrollWidth ?? 0,
|
|
4263
|
+
});
|
|
4264
|
+
tabs.classList.toggle("is-overflow-start", state.start);
|
|
4265
|
+
tabs.classList.toggle("is-overflow-end", state.end);
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
/** 选中 Tab 保持可见:只滚动 Tab 条自身,不牵动外层页面。 */
|
|
4269
|
+
function revealInspectorTab(tabs, button) {
|
|
4270
|
+
if (!tabs || !button) return;
|
|
4271
|
+
const left = button.offsetLeft;
|
|
4272
|
+
const right = left + (button.offsetWidth ?? 0);
|
|
4273
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return;
|
|
4274
|
+
if (left < tabs.scrollLeft + 8) tabs.scrollLeft = Math.max(0, left - 8);
|
|
4275
|
+
else if (right > tabs.scrollLeft + tabs.clientWidth - 8) {
|
|
4276
|
+
tabs.scrollLeft = right - tabs.clientWidth + 8;
|
|
4277
|
+
}
|
|
4278
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4279
|
+
}
|
|
4280
|
+
|
|
3876
4281
|
function appendInspectorPurpose(title, node) {
|
|
3877
4282
|
const purpose = inspectorNodePurpose(node);
|
|
3878
4283
|
if (!purpose) return;
|
|
3879
4284
|
title.appendChild(el("p", "dag-inspector-purpose", purpose));
|
|
3880
4285
|
}
|
|
3881
4286
|
|
|
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
|
-
}
|
|
4287
|
+
function buildDagInspectorTabs(node) {
|
|
4288
|
+
const tabDefs = inspectorTabDefinitions(node);
|
|
3915
4289
|
const tabs = el("div", "dag-inspector-tabs");
|
|
4290
|
+
tabs.dataset.tabsSignature = tabDefs.map(([tab]) => tab).join(",");
|
|
3916
4291
|
tabs.setAttribute("role", "tablist");
|
|
3917
4292
|
tabs.setAttribute("aria-label", "节点检查器面板");
|
|
3918
4293
|
for (const [tab, label, icon] of tabDefs) {
|
|
@@ -3935,6 +4310,10 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3935
4310
|
renderDagInspectorForCurrent();
|
|
3936
4311
|
const next = document.getElementById(`dag-inspector-tab-${tab}`);
|
|
3937
4312
|
next?.focus();
|
|
4313
|
+
revealInspectorTab(
|
|
4314
|
+
document.querySelector(".dag-inspector-tabs"),
|
|
4315
|
+
next,
|
|
4316
|
+
);
|
|
3938
4317
|
});
|
|
3939
4318
|
button.addEventListener("keydown", (event) => {
|
|
3940
4319
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
@@ -3947,17 +4326,106 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3947
4326
|
const nextTab = order[(idx + delta + order.length) % order.length];
|
|
3948
4327
|
uiState.dagInspectorTab = nextTab;
|
|
3949
4328
|
renderDagInspectorForCurrent();
|
|
3950
|
-
document.getElementById(`dag-inspector-tab-${nextTab}`)
|
|
4329
|
+
const next = document.getElementById(`dag-inspector-tab-${nextTab}`);
|
|
4330
|
+
next?.focus();
|
|
4331
|
+
revealInspectorTab(document.querySelector(".dag-inspector-tabs"), next);
|
|
3951
4332
|
});
|
|
3952
4333
|
tabs.appendChild(button);
|
|
3953
4334
|
}
|
|
4335
|
+
// 溢出渐隐与选中可见性:挂载后测量(构建时 offsetLeft 不可用)。
|
|
4336
|
+
tabs.addEventListener("scroll", () => syncInspectorTabOverflowClasses(tabs), { passive: true });
|
|
4337
|
+
if (typeof ResizeObserver === "function") {
|
|
4338
|
+
const overflowObserver = new ResizeObserver(() => {
|
|
4339
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4340
|
+
});
|
|
4341
|
+
overflowObserver.observe(tabs);
|
|
4342
|
+
}
|
|
4343
|
+
if (typeof requestAnimationFrame === "function") {
|
|
4344
|
+
requestAnimationFrame(() => {
|
|
4345
|
+
if (!tabs.isConnected) return;
|
|
4346
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4347
|
+
const selected = tabs.querySelector("[role='tab'][aria-selected='true']");
|
|
4348
|
+
revealInspectorTab(tabs, selected);
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
return tabs;
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
function buildDagInspectorHeader(dagRunId, node) {
|
|
4355
|
+
const header = el("div", "dag-inspector-header");
|
|
4356
|
+
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
4357
|
+
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
4358
|
+
const title = el("div", "dag-inspector-title");
|
|
4359
|
+
title.appendChild(el("h3", null, "节点检查器"));
|
|
4360
|
+
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
4361
|
+
appendInspectorPurpose(title, node);
|
|
4362
|
+
header.appendChild(title);
|
|
4363
|
+
const controls = el("div", "dag-inspector-controls");
|
|
4364
|
+
const status = badge(node.status ?? "unknown");
|
|
4365
|
+
status.classList.add("dag-inspector-status");
|
|
4366
|
+
controls.appendChild(status);
|
|
4367
|
+
const expand = iconButton(uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4368
|
+
expand.type = "button";
|
|
4369
|
+
expand.setAttribute("aria-pressed", String(Boolean(uiState.dagInspectorExpanded)));
|
|
4370
|
+
expand.addEventListener("click", () => {
|
|
4371
|
+
uiState.dagInspectorExpanded = !uiState.dagInspectorExpanded;
|
|
4372
|
+
document.getElementById("dag-inspector")?.classList.toggle("is-expanded", uiState.dagInspectorExpanded);
|
|
4373
|
+
setIconButton(expand, uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4374
|
+
expand.setAttribute("aria-pressed", String(uiState.dagInspectorExpanded));
|
|
4375
|
+
});
|
|
4376
|
+
controls.appendChild(expand);
|
|
4377
|
+
const share = iconButton("复制位置", "ri-link");
|
|
4378
|
+
share.type = "button";
|
|
4379
|
+
share.addEventListener("click", async () => {
|
|
4380
|
+
const selection = inspectorSelection;
|
|
4381
|
+
const query = new URLSearchParams({ node: selection.node.nodeId, tab: activeInspectorTab() });
|
|
4382
|
+
const key = effectiveExecutionKey(selection.dagRunId, selection.node);
|
|
4383
|
+
if (key) query.set("attempt", key);
|
|
4384
|
+
const href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(selection.dagRunId)}?${query}`);
|
|
4385
|
+
const copied = await copyText(new URL(href, location.href).href);
|
|
4386
|
+
setIconButton(share, copied ? "已复制位置" : "复制失败", copied ? "ri-check-line" : "ri-error-warning-line");
|
|
4387
|
+
});
|
|
4388
|
+
controls.appendChild(share);
|
|
4389
|
+
const close = document.createElement("button");
|
|
4390
|
+
close.type = "button";
|
|
4391
|
+
close.className = "dag-inspector-close";
|
|
4392
|
+
close.setAttribute("aria-label", "关闭节点检查器");
|
|
4393
|
+
const closeIcon = el("i", "ri-close-line");
|
|
4394
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
4395
|
+
close.appendChild(closeIcon);
|
|
4396
|
+
close.addEventListener("click", closeDagInspector);
|
|
4397
|
+
controls.appendChild(close);
|
|
4398
|
+
header.appendChild(controls);
|
|
4399
|
+
|
|
4400
|
+
const tabs = buildDagInspectorTabs(node);
|
|
3954
4401
|
header.appendChild(tabs);
|
|
4402
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4403
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4404
|
+
header.dataset.attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
3955
4405
|
return header;
|
|
3956
4406
|
}
|
|
3957
4407
|
|
|
3958
4408
|
function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
3959
4409
|
const header = panel.querySelector(".dag-inspector-header");
|
|
3960
4410
|
if (!header) return;
|
|
4411
|
+
const tabs = header.querySelector(".dag-inspector-tabs");
|
|
4412
|
+
const tabsSignature = inspectorTabDefinitions(node).map(([tab]) => tab).join(",");
|
|
4413
|
+
if (tabs && tabs.dataset.tabsSignature !== tabsSignature) {
|
|
4414
|
+
const restoreFocus = document.activeElement?.getAttribute("role") === "tab" && tabs.contains(document.activeElement);
|
|
4415
|
+
const nextTabs = buildDagInspectorTabs(node);
|
|
4416
|
+
const scrollLeft = tabs.scrollLeft;
|
|
4417
|
+
tabs.replaceWith(nextTabs);
|
|
4418
|
+
nextTabs.scrollLeft = scrollLeft;
|
|
4419
|
+
syncInspectorTabOverflowClasses(nextTabs);
|
|
4420
|
+
if (restoreFocus) document.getElementById(`dag-inspector-tab-${activeInspectorTab()}`)?.focus();
|
|
4421
|
+
}
|
|
4422
|
+
const attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
4423
|
+
if (header.dataset.dagNodeId !== String(node.nodeId) || header.dataset.attemptSignature !== attemptSignature) {
|
|
4424
|
+
header.querySelector(".dag-inspector-attempt-bar")?.remove();
|
|
4425
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4426
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4427
|
+
header.dataset.attemptSignature = attemptSignature;
|
|
4428
|
+
}
|
|
3961
4429
|
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
3962
4430
|
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3963
4431
|
const nodeLabel = header.querySelector(".dag-inspector-node");
|
|
@@ -3987,7 +4455,7 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
|
3987
4455
|
|
|
3988
4456
|
function dagInspectorSignature(dagRunId, node) {
|
|
3989
4457
|
const tab = activeInspectorTab();
|
|
3990
|
-
const execKey =
|
|
4458
|
+
const execKey = inspectorTabExecutionKey(dagRunId, node, tab);
|
|
3991
4459
|
const cachedBody = execKey
|
|
3992
4460
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, execKey)
|
|
3993
4461
|
: null;
|
|
@@ -4024,11 +4492,11 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
4024
4492
|
// the DOM while selection/history are unchanged; switching attempt or a
|
|
4025
4493
|
// changed attempt count rebuilds the selector.
|
|
4026
4494
|
const summary = node.executionSummary;
|
|
4027
|
-
const attemptRevision = [
|
|
4495
|
+
const attemptRevision = tab === "compare" ? attemptSelectorOptions(node) : inspectorTabUsesAttempt(tab, node) ? [
|
|
4028
4496
|
summary?.totalAttemptCount ?? 0,
|
|
4029
4497
|
(node.passes ?? []).join(","),
|
|
4030
4498
|
execKey ?? "latest",
|
|
4031
|
-
];
|
|
4499
|
+
] : null;
|
|
4032
4500
|
return JSON.stringify([
|
|
4033
4501
|
dagRunId,
|
|
4034
4502
|
node.nodeId,
|
|
@@ -4047,6 +4515,8 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4047
4515
|
inspectorSelection.run = run;
|
|
4048
4516
|
const panel = document.getElementById("dag-inspector");
|
|
4049
4517
|
if (!panel) return;
|
|
4518
|
+
// 运行状态标记:回到底部按钮由滚动位置自行显隐(终态同样可用),此标记仅供诊断。
|
|
4519
|
+
panel.dataset.runEnded = run && !isDagRunActive(run) ? "1" : "0";
|
|
4050
4520
|
captureDagTimelineViewportState(panel);
|
|
4051
4521
|
if (!uiState.dagInspectorOpen || !node) {
|
|
4052
4522
|
panel.classList.remove("is-open");
|
|
@@ -4054,6 +4524,8 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4054
4524
|
return;
|
|
4055
4525
|
}
|
|
4056
4526
|
|
|
4527
|
+
uiState.dagInspectorTab = activeInspectorTab();
|
|
4528
|
+
|
|
4057
4529
|
// Defer tearing down the inspector shell while the user is actively
|
|
4058
4530
|
// dragging the resize handle (or the graph) so the pointer-capture target
|
|
4059
4531
|
// is not destroyed mid-gesture; the next poll refreshes the content.
|
|
@@ -4073,6 +4545,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4073
4545
|
if (panel.dataset.contentSignature === nextContentSignature) return;
|
|
4074
4546
|
const previousNode = existingContent.dataset.dagNodeId ?? "";
|
|
4075
4547
|
const previousRun = existingContent.dataset.dagRunId ?? "";
|
|
4548
|
+
const previousExecutionKey = existingContent.dataset.executionKey;
|
|
4076
4549
|
const previousTab =
|
|
4077
4550
|
existingContent.dataset.inspectorTab ?? activeInspectorTab();
|
|
4078
4551
|
const nodeChanged =
|
|
@@ -4104,7 +4577,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4104
4577
|
activeTab,
|
|
4105
4578
|
);
|
|
4106
4579
|
}
|
|
4107
|
-
if (!nodeChanged && previousTab === activeTab) {
|
|
4580
|
+
if (!nodeChanged && previousTab === activeTab && previousExecutionKey === existingContent.dataset.executionKey) {
|
|
4108
4581
|
for (const [index, pre] of [
|
|
4109
4582
|
...existingContent.querySelectorAll("pre"),
|
|
4110
4583
|
].entries()) {
|
|
@@ -4121,6 +4594,7 @@ export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
|
4121
4594
|
clearNode(panel);
|
|
4122
4595
|
panel.classList.add("is-open");
|
|
4123
4596
|
panel.setAttribute("aria-hidden", "false");
|
|
4597
|
+
panel.classList.toggle("is-expanded", Boolean(uiState.dagInspectorExpanded));
|
|
4124
4598
|
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
4125
4599
|
panel.appendChild(buildDagInspectorHeader(dagRunId, node));
|
|
4126
4600
|
|