@tea-agent/loop-agent 0.42.0-next.9 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -45
- package/dist/application/dag/run-dag.js +8 -2
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +26 -8
- 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/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +2584 -600
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +135 -55
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/contract/apply.js +36 -2
- 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/parse-intent.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +173 -94
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +208 -66
- 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 +78 -11
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +27 -7
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +21 -0
- package/dist/worker/console/chat/tools.js +13 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- 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 +142 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -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 +33 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -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/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- 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/index.html +4 -4
- 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/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -5
- 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 +431 -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 +51 -4
- package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
- 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 +64 -22
- 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 +194 -243
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +103 -17
- 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 +423 -41
- 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 +21 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +146 -16
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +46 -7
- package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +70 -35
- package/dist/workflows/dag/node-execution.js +225 -226
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +96 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +246 -19
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +68 -4
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/harness.json +1 -1
- package/package.json +8 -6
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +22 -2
- package/skills/frontend-plan/references/decision-contract.md +114 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.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,15 +10,24 @@ 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,
|
|
17
20
|
formatContextTransferReasonList,
|
|
18
21
|
} from "../dag-context-reason-labels.js";
|
|
22
|
+
import {
|
|
23
|
+
describeInspectorMeta,
|
|
24
|
+
inspectorFieldLabel,
|
|
25
|
+
inspectorListValueLabel,
|
|
26
|
+
inspectorValueLabel,
|
|
27
|
+
} from "../dag-inspector-humanize.js";
|
|
19
28
|
import {
|
|
20
29
|
fetchJson,
|
|
30
|
+
fetchJsonResult,
|
|
21
31
|
fetchSpecEvidenceFile,
|
|
22
32
|
fetchDagNodeInput,
|
|
23
33
|
fetchDagNodeContext,
|
|
@@ -31,7 +41,6 @@ import {
|
|
|
31
41
|
} from "../api.js";
|
|
32
42
|
import {
|
|
33
43
|
badge,
|
|
34
|
-
formatLedgerSummary,
|
|
35
44
|
formatMs,
|
|
36
45
|
prettyJsonBlock,
|
|
37
46
|
} from "../format.js";
|
|
@@ -39,9 +48,11 @@ import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
|
|
|
39
48
|
import { describeDagNodePurpose } from "../dag-node-purpose.js";
|
|
40
49
|
import { buildPromptRestartCandidateOptions } from "../prompt-restart-candidates.js";
|
|
41
50
|
import { copyText } from "../copy.js";
|
|
42
|
-
import { navigate } from "../router.js";
|
|
51
|
+
import { navigate, inspectPathToCanonicalHref } from "../router.js";
|
|
43
52
|
import {
|
|
44
53
|
fetchWithWorkspaceScope,
|
|
54
|
+
scopedApiUrl,
|
|
55
|
+
workspaceScope,
|
|
45
56
|
uiState,
|
|
46
57
|
makeSessionEventIdentity,
|
|
47
58
|
makeDagNodeInputIdentity,
|
|
@@ -59,12 +70,15 @@ import {
|
|
|
59
70
|
inspectorViewportRegionKey,
|
|
60
71
|
saveDagInspectorViewportState,
|
|
61
72
|
getDagInspectorViewportState,
|
|
73
|
+
inspectorReadingState,
|
|
74
|
+
inspectorNodeSelections,
|
|
75
|
+
inspectorDrafts,
|
|
76
|
+
inspectorOperations,
|
|
62
77
|
} from "../state.js";
|
|
63
78
|
import {
|
|
64
79
|
formatSessionEventTime,
|
|
65
80
|
renderSessionTimeline,
|
|
66
|
-
|
|
67
|
-
fetchPageStylesheetText,
|
|
81
|
+
createTimelineAutoScrollControl,
|
|
68
82
|
} from "./session-timeline.js";
|
|
69
83
|
import { createCustomSelect } from "../custom-select.js";
|
|
70
84
|
|
|
@@ -72,7 +86,7 @@ import { createCustomSelect } from "../custom-select.js";
|
|
|
72
86
|
// reused inspector shell never keeps stale node closures: every render writes
|
|
73
87
|
// the latest {dagRunId, node} here and tab clicks re-render from the same
|
|
74
88
|
// reference, always resolving the currently selected node identity.
|
|
75
|
-
const inspectorSelection = { dagRunId: null, node: null };
|
|
89
|
+
const inspectorSelection = { dagRunId: null, node: null, run: null };
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
92
|
* 失败状态行统一入口:错误信息中嵌入的已知 reason code 先给中文语义
|
|
@@ -111,34 +125,117 @@ export const ARTIFACT_FILE_PREVIEW_SURFACES = Object.freeze({
|
|
|
111
125
|
revisionSelection: "artifact-revision-current-preview",
|
|
112
126
|
});
|
|
113
127
|
|
|
128
|
+
const INSPECTOR_PAGE_GUIDANCE = Object.freeze({
|
|
129
|
+
overview: {
|
|
130
|
+
eyebrow: "先看结论",
|
|
131
|
+
title: "这个节点做了什么",
|
|
132
|
+
body: "先看当前状态、执行方式和结果来源;需要追细节时,再进入后面的页面。",
|
|
133
|
+
},
|
|
134
|
+
input: {
|
|
135
|
+
eyebrow: "运行前",
|
|
136
|
+
title: "系统原本准备怎么做",
|
|
137
|
+
body: "这里是开始执行前保存的任务说明、依赖关系和读写范围,不代表这些内容后来一定被实际使用。",
|
|
138
|
+
},
|
|
139
|
+
context: {
|
|
140
|
+
eyebrow: "执行时",
|
|
141
|
+
title: "AI 实际拿到了哪些资料",
|
|
142
|
+
body: "这一页只展示系统在下发任务时真正记录到的内容;没有记录时会明确说明,不会用运行前设置代替。",
|
|
143
|
+
},
|
|
144
|
+
timeline: {
|
|
145
|
+
eyebrow: "执行过程",
|
|
146
|
+
title: "这一步实际发生了什么",
|
|
147
|
+
body: "按顺序查看 AI 的回复、工具调用或命令结果;失败时优先看标红步骤和错误输出。",
|
|
148
|
+
},
|
|
149
|
+
"spec-evidence": {
|
|
150
|
+
eyebrow: "需求依据",
|
|
151
|
+
title: "它有没有真的读过需求",
|
|
152
|
+
body: "只有成功读取文件或查询知识库的记录才算证据;模型自己提到文件名不算。",
|
|
153
|
+
},
|
|
154
|
+
output: {
|
|
155
|
+
eyebrow: "执行结果",
|
|
156
|
+
title: "它产出了什么,谁会用",
|
|
157
|
+
body: "先确认文件是否真实生成、能否替换以及后续谁会使用,再查看具体输出内容。",
|
|
158
|
+
},
|
|
159
|
+
lineage: {
|
|
160
|
+
eyebrow: "来源与影响",
|
|
161
|
+
title: "这一步从哪里来,会影响哪里",
|
|
162
|
+
body: "用于判断结果是本次生成还是沿用旧运行,以及替换或重跑会从哪个位置继续。",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/** 会话级收起记忆:同一 Tab 收起后,本会话内的重绘保持折叠(可再展开)。 */
|
|
167
|
+
const COLLAPSED_INSPECTOR_LEADS = new Set();
|
|
168
|
+
|
|
169
|
+
function buildInspectorPageLead(tab) {
|
|
170
|
+
const copy = INSPECTOR_PAGE_GUIDANCE[tab];
|
|
171
|
+
if (!copy) return null;
|
|
172
|
+
const lead = el("section", "inspector-page-lead");
|
|
173
|
+
lead.dataset.page = tab;
|
|
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",
|
|
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));
|
|
198
|
+
return lead;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function ensureInspectorPageLead(content, tab) {
|
|
202
|
+
const existing = content.querySelector(
|
|
203
|
+
`.inspector-page-lead[data-page="${tab}"]`,
|
|
204
|
+
);
|
|
205
|
+
if (existing) return existing;
|
|
206
|
+
const lead = buildInspectorPageLead(tab);
|
|
207
|
+
if (lead) content.insertBefore(lead, content.firstChild);
|
|
208
|
+
return lead;
|
|
209
|
+
}
|
|
210
|
+
|
|
114
211
|
export const CONTEXT_EXPORT_TARGETS = Object.freeze([
|
|
115
212
|
{
|
|
116
213
|
value: "generic",
|
|
117
|
-
label: "通用
|
|
118
|
-
description: "适合交给其他
|
|
214
|
+
label: "通用 AI 交接包",
|
|
215
|
+
description: "适合交给其他 AI 工具继续处理",
|
|
119
216
|
icon: "ri-robot-2-line",
|
|
120
|
-
contents: "
|
|
217
|
+
contents: "执行时记录 · 文件清单 · 交接说明",
|
|
121
218
|
},
|
|
122
219
|
{
|
|
123
220
|
value: "pi-session",
|
|
124
|
-
label: "Pi
|
|
125
|
-
description: "
|
|
221
|
+
label: "可在 Pi 中恢复的会话",
|
|
222
|
+
description: "如果记录兼容,会附带可直接恢复的会话文件",
|
|
126
223
|
icon: "ri-terminal-box-line",
|
|
127
|
-
contents: "
|
|
224
|
+
contents: "执行时记录 · 文件清单 · 可恢复的 Pi 会话(兼容时)",
|
|
128
225
|
},
|
|
129
226
|
{
|
|
130
227
|
value: "readable",
|
|
131
228
|
label: "可读执行记录",
|
|
132
229
|
description: "便于人工审阅,不承诺完整重现执行",
|
|
133
230
|
icon: "ri-file-list-3-line",
|
|
134
|
-
contents: "
|
|
231
|
+
contents: "执行时记录 · 文件清单 · 可读对话记录",
|
|
135
232
|
},
|
|
136
233
|
{
|
|
137
234
|
value: "pi-handoff",
|
|
138
|
-
label: "Pi
|
|
139
|
-
description: "
|
|
235
|
+
label: "Pi 专用交接包",
|
|
236
|
+
description: "用于把资料交给 Pi 的继续处理流程",
|
|
140
237
|
icon: "ri-share-forward-box-line",
|
|
141
|
-
contents: "
|
|
238
|
+
contents: "执行时记录 · 文件清单 · Pi 交接说明",
|
|
142
239
|
},
|
|
143
240
|
]);
|
|
144
241
|
|
|
@@ -398,7 +495,7 @@ function createPromptRestartPromptCard({
|
|
|
398
495
|
const next = candidate ?? readCandidate?.();
|
|
399
496
|
titleEl.textContent =
|
|
400
497
|
next?.key === "frozen"
|
|
401
|
-
? "
|
|
498
|
+
? "运行开始时的提示词"
|
|
402
499
|
: (next?.label ?? title);
|
|
403
500
|
pre.textContent = next?.prompt ?? "";
|
|
404
501
|
metaEl.textContent = formatPromptRestartMeta(next?.prompt ?? "");
|
|
@@ -415,11 +512,15 @@ function renderDagInspectorForCurrent() {
|
|
|
415
512
|
inspectorSelection.dagRunId,
|
|
416
513
|
inspectorSelection.node,
|
|
417
514
|
false,
|
|
515
|
+
inspectorSelection.run,
|
|
418
516
|
);
|
|
419
517
|
}
|
|
420
518
|
|
|
421
519
|
function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
422
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;
|
|
423
524
|
uiState.dagInspectorOpen = true;
|
|
424
525
|
// Each node opens in live-follow mode by default. The explicit floating
|
|
425
526
|
// control can pause it without losing the reader's current position.
|
|
@@ -427,7 +528,7 @@ function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
|
427
528
|
uiState.dagTimelineViewportState = null;
|
|
428
529
|
// Reset the per-attempt selection so an old node's attempt cannot leak
|
|
429
530
|
// into the newly selected node (always defaults back to the latest).
|
|
430
|
-
uiState.dagInspectorAttempt = null;
|
|
531
|
+
uiState.dagInspectorAttempt = inspectorNodeSelections.get(`${dagRunId}:${nodeId}`) ?? null;
|
|
431
532
|
uiState.sessionEventOffset = 0;
|
|
432
533
|
uiState.sessionEvents = [];
|
|
433
534
|
// Drop any open spec-evidence preview so a stale A detail cannot survive
|
|
@@ -628,6 +729,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
628
729
|
restoreInspectorContentScroll(content, dagRunId, nodeId, "spec-evidence");
|
|
629
730
|
return;
|
|
630
731
|
}
|
|
732
|
+
ensureInspectorPageLead(content, "spec-evidence");
|
|
631
733
|
if (!evidence) {
|
|
632
734
|
content.appendChild(el("p", "empty", "无法加载规范证据。"));
|
|
633
735
|
restoreInspectorContentScroll(content, dagRunId, nodeId, "spec-evidence");
|
|
@@ -638,8 +740,8 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
638
740
|
const statusLabels = {
|
|
639
741
|
"spec-read": "已读取规范",
|
|
640
742
|
"source-bound": "已绑定任务源",
|
|
641
|
-
"spec-injected": "
|
|
642
|
-
"search-only": "
|
|
743
|
+
"spec-injected": "已提供阅读能力,但没有读取记录",
|
|
744
|
+
"search-only": "只搜索过,没有读取内容",
|
|
643
745
|
"kb-queried": "已观察知识库查询",
|
|
644
746
|
"no-evidence": "未观察到读取",
|
|
645
747
|
};
|
|
@@ -680,7 +782,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
680
782
|
};
|
|
681
783
|
|
|
682
784
|
appendListSection(
|
|
683
|
-
"
|
|
785
|
+
"提供给节点的技能",
|
|
684
786
|
evidence.skillInjection?.skills,
|
|
685
787
|
(skill) => {
|
|
686
788
|
const item = el("li", null);
|
|
@@ -705,9 +807,9 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
705
807
|
icon.setAttribute("aria-hidden", "true");
|
|
706
808
|
button.append(
|
|
707
809
|
icon,
|
|
708
|
-
document.createTextNode(` ${source.kind}: `),
|
|
810
|
+
document.createTextNode(` ${inspectorListValueLabel(source.kind)}: `),
|
|
709
811
|
el("code", null, source.path),
|
|
710
|
-
document.createTextNode(` ·
|
|
812
|
+
document.createTextNode(` · 内容校验码 ${source.sha256.slice(0, 12)}…`),
|
|
711
813
|
);
|
|
712
814
|
button.addEventListener("click", () =>
|
|
713
815
|
onSpecEvidenceFileClick(
|
|
@@ -767,7 +869,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
767
869
|
item.appendChild(button);
|
|
768
870
|
return item;
|
|
769
871
|
},
|
|
770
|
-
"
|
|
872
|
+
"没有记录到节点读取规范文件。",
|
|
771
873
|
);
|
|
772
874
|
appendListSection(
|
|
773
875
|
`规范检索(${evidence.specSearches?.length ?? 0})`,
|
|
@@ -782,7 +884,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
782
884
|
);
|
|
783
885
|
return item;
|
|
784
886
|
},
|
|
785
|
-
"
|
|
887
|
+
"没有记录到节点检索规范。",
|
|
786
888
|
);
|
|
787
889
|
appendListSection(
|
|
788
890
|
`知识库查询(${evidence.knowledgeBaseQueries?.length ?? 0})`,
|
|
@@ -794,7 +896,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
794
896
|
item.append(icon, document.createTextNode(` ${query.connector}`));
|
|
795
897
|
return item;
|
|
796
898
|
},
|
|
797
|
-
"
|
|
899
|
+
"没有记录到知识库查询;可能未配置知识库连接,或节点没有使用它。",
|
|
798
900
|
);
|
|
799
901
|
|
|
800
902
|
if (
|
|
@@ -809,10 +911,10 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
809
911
|
icon,
|
|
810
912
|
document.createTextNode(
|
|
811
913
|
evidence.status === "source-bound"
|
|
812
|
-
? "
|
|
914
|
+
? "系统已确认任务来源和文件版本,但没有记录到节点成功读取完整文件。仅绑定文件不能证明节点真的读过内容。"
|
|
813
915
|
: evidence.status === "no-evidence"
|
|
814
|
-
? "
|
|
815
|
-
: "
|
|
916
|
+
? "没有记录到节点读取规范。模型总结里提到文件名,不等于节点真正读取了文件。"
|
|
917
|
+
: "已把规范阅读能力提供给节点,但没有记录到它读取规范或查询知识库;只搜索到文件名不等于读过文件内容。",
|
|
816
918
|
),
|
|
817
919
|
);
|
|
818
920
|
content.appendChild(warning);
|
|
@@ -923,7 +1025,7 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
|
|
|
923
1025
|
el(
|
|
924
1026
|
"span",
|
|
925
1027
|
"spec-evidence-detail-hash",
|
|
926
|
-
|
|
1028
|
+
`登记的内容校验码 ${String(detail.data.sha256 ?? "").slice(0, 12)}… · 当前文件${detail.data.hashMatch ? "一致" : "不一致"}`,
|
|
927
1029
|
),
|
|
928
1030
|
);
|
|
929
1031
|
} else if (detail.source === "read" && detail.data?.readAt) {
|
|
@@ -969,15 +1071,31 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
|
|
|
969
1071
|
|
|
970
1072
|
function sourceLabel(source) {
|
|
971
1073
|
if (source === "defaults") return "继承默认值";
|
|
972
|
-
if (source === "schema-default") return "
|
|
973
|
-
if (source === "merged") return "
|
|
1074
|
+
if (source === "schema-default") return "系统默认值";
|
|
1075
|
+
if (source === "merged") return "合并后的值";
|
|
974
1076
|
return null;
|
|
975
1077
|
}
|
|
976
1078
|
|
|
977
1079
|
function appendMetaRow(dl, label, value, source) {
|
|
978
1080
|
if (value == null || value === "") return;
|
|
979
|
-
const
|
|
980
|
-
const
|
|
1081
|
+
const isElement = value && typeof value === "object" && value.nodeType != null;
|
|
1082
|
+
const copy = describeInspectorMeta(label, isElement ? "" : value);
|
|
1083
|
+
const dt = el("dt", null, copy.label);
|
|
1084
|
+
const dd = el("dd", null, copy.value);
|
|
1085
|
+
if (isElement) dd.appendChild(value);
|
|
1086
|
+
const labelHelp = [
|
|
1087
|
+
copy.help ? `含义:${copy.help}` : null,
|
|
1088
|
+
copy.rawLabel ? `技术字段:${copy.rawLabel}` : null,
|
|
1089
|
+
].filter(Boolean);
|
|
1090
|
+
if (labelHelp.length > 0) {
|
|
1091
|
+
const tooltipText = labelHelp.join("\n");
|
|
1092
|
+
const help = el("span", "node-input-summary-help", "?");
|
|
1093
|
+
help.tabIndex = 0;
|
|
1094
|
+
help.setAttribute("data-tooltip", tooltipText);
|
|
1095
|
+
help.setAttribute("aria-label", tooltipText);
|
|
1096
|
+
dt.appendChild(help);
|
|
1097
|
+
}
|
|
1098
|
+
if (copy.rawValue) dd.setAttribute("title", `原始值:${copy.rawValue}`);
|
|
981
1099
|
const inherited = sourceLabel(source);
|
|
982
1100
|
if (inherited) {
|
|
983
1101
|
dd.appendChild(document.createTextNode(" "));
|
|
@@ -990,12 +1108,20 @@ function appendMetaRow(dl, label, value, source) {
|
|
|
990
1108
|
function appendCodeList(parent, title, values) {
|
|
991
1109
|
if (!values || values.length === 0) return;
|
|
992
1110
|
const section = el("section", "node-input-section");
|
|
993
|
-
|
|
1111
|
+
const heading = el("h4", null, inspectorFieldLabel(title));
|
|
1112
|
+
if (inspectorFieldLabel(title) !== title) {
|
|
1113
|
+
heading.setAttribute("title", `技术字段:${title}`);
|
|
1114
|
+
}
|
|
1115
|
+
section.appendChild(heading);
|
|
994
1116
|
const list = el("ul", "node-input-code-list");
|
|
995
1117
|
for (const value of values) {
|
|
996
1118
|
const item = document.createElement("li");
|
|
997
1119
|
const code = document.createElement("code");
|
|
998
|
-
|
|
1120
|
+
const rawValue = String(value);
|
|
1121
|
+
code.textContent = inspectorListValueLabel(rawValue);
|
|
1122
|
+
if (code.textContent !== rawValue) {
|
|
1123
|
+
code.setAttribute("title", `原始值:${rawValue}`);
|
|
1124
|
+
}
|
|
999
1125
|
item.appendChild(code);
|
|
1000
1126
|
list.appendChild(item);
|
|
1001
1127
|
}
|
|
@@ -1063,9 +1189,9 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
1063
1189
|
section.appendChild(el("h4", null, "任务正文"));
|
|
1064
1190
|
if (data.prompt.source?.path) {
|
|
1065
1191
|
const meta = el("p", "muted");
|
|
1066
|
-
meta.textContent =
|
|
1192
|
+
meta.textContent = `任务说明来源:${data.prompt.source.path}${
|
|
1067
1193
|
data.prompt.source.sha256Prefix
|
|
1068
|
-
?
|
|
1194
|
+
? `(内容校验码 ${data.prompt.source.sha256Prefix}…)`
|
|
1069
1195
|
: ""
|
|
1070
1196
|
}`;
|
|
1071
1197
|
section.appendChild(meta);
|
|
@@ -1085,7 +1211,7 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
1085
1211
|
|
|
1086
1212
|
if (data.executorInput) {
|
|
1087
1213
|
const section = el("section", "node-input-section");
|
|
1088
|
-
section.appendChild(el("h4", null, "
|
|
1214
|
+
section.appendChild(el("h4", null, "将如何执行"));
|
|
1089
1215
|
const dl = el("dl", "node-input-summary");
|
|
1090
1216
|
appendMetaRow(dl, "kind", data.executorInput.kind, null);
|
|
1091
1217
|
if (data.executorInput.modelHint) {
|
|
@@ -1158,7 +1284,7 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
1158
1284
|
|
|
1159
1285
|
if (data.boundaries) {
|
|
1160
1286
|
const section = el("section", "node-input-section");
|
|
1161
|
-
section.appendChild(el("h4", null, "
|
|
1287
|
+
section.appendChild(el("h4", null, "读写范围与输出要求"));
|
|
1162
1288
|
const dl = el("dl", "node-input-summary");
|
|
1163
1289
|
appendMetaRow(dl, "outputMode", data.boundaries.outputMode, null);
|
|
1164
1290
|
appendMetaRow(
|
|
@@ -1169,7 +1295,7 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
1169
1295
|
);
|
|
1170
1296
|
section.appendChild(dl);
|
|
1171
1297
|
if (data.boundaries.outputContract) {
|
|
1172
|
-
section.appendChild(el("h5", null, "
|
|
1298
|
+
section.appendChild(el("h5", null, "输出内容要求"));
|
|
1173
1299
|
section.appendChild(renderMarkdown(data.boundaries.outputContract));
|
|
1174
1300
|
}
|
|
1175
1301
|
appendCodeList(section, "allowedPaths", data.boundaries.allowedPaths);
|
|
@@ -1181,7 +1307,7 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
1181
1307
|
if (data.piExtensionRunFacts) {
|
|
1182
1308
|
const facts = data.piExtensionRunFacts;
|
|
1183
1309
|
const section = el("section", "node-input-section");
|
|
1184
|
-
section.appendChild(el("h4", null, "
|
|
1310
|
+
section.appendChild(el("h4", null, "AI 扩展加载结果"));
|
|
1185
1311
|
if (!facts.exists) {
|
|
1186
1312
|
section.appendChild(
|
|
1187
1313
|
el(
|
|
@@ -1222,7 +1348,7 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
1222
1348
|
|
|
1223
1349
|
if (data.promptFingerprint?.exists) {
|
|
1224
1350
|
const section = el("section", "node-input-section");
|
|
1225
|
-
section.appendChild(el("h4", null, "
|
|
1351
|
+
section.appendChild(el("h4", null, "最终提示内容校验信息"));
|
|
1226
1352
|
const dl = el("dl", "node-input-summary");
|
|
1227
1353
|
appendMetaRow(
|
|
1228
1354
|
dl,
|
|
@@ -1274,6 +1400,7 @@ async function renderDagNodeInput(content, dagRunId, node) {
|
|
|
1274
1400
|
preserveIntent: true,
|
|
1275
1401
|
});
|
|
1276
1402
|
clearNode(content);
|
|
1403
|
+
ensureInspectorPageLead(content, "input");
|
|
1277
1404
|
const loading = el("p", "muted", UI_TEXT.inputLoading);
|
|
1278
1405
|
loading.setAttribute("role", "status");
|
|
1279
1406
|
loading.setAttribute("aria-live", "polite");
|
|
@@ -1293,6 +1420,7 @@ async function renderDagNodeInput(content, dagRunId, node) {
|
|
|
1293
1420
|
}
|
|
1294
1421
|
|
|
1295
1422
|
clearNode(content);
|
|
1423
|
+
ensureInspectorPageLead(content, "input");
|
|
1296
1424
|
const finishInputRender = () => {
|
|
1297
1425
|
restoreInspectorContentScroll(content, dagRunId, nodeId, "input");
|
|
1298
1426
|
uiState.dagNodeInputLoading = false;
|
|
@@ -1355,31 +1483,45 @@ export function frontendGateLedgerLines(input) {
|
|
|
1355
1483
|
if (prewriteResult?.ok && prewriteResult.body) {
|
|
1356
1484
|
const parsed = prewriteResult.body;
|
|
1357
1485
|
if (parsed?.classification) {
|
|
1358
|
-
|
|
1486
|
+
const classifications = {
|
|
1487
|
+
accepted: "通过,可以进入写入阶段",
|
|
1488
|
+
blocked: "未通过,已阻止写入",
|
|
1489
|
+
warning: "有提醒,需要留意",
|
|
1490
|
+
};
|
|
1491
|
+
lines.push(
|
|
1492
|
+
`写入前检查:${classifications[parsed.classification] ?? String(parsed.classification)}`,
|
|
1493
|
+
);
|
|
1359
1494
|
if (parsed.failureCode) {
|
|
1360
|
-
lines.push(
|
|
1495
|
+
lines.push(`问题编号:${String(parsed.failureCode)}`);
|
|
1361
1496
|
}
|
|
1362
1497
|
if (parsed.failureReason) {
|
|
1363
|
-
lines.push(
|
|
1498
|
+
lines.push(`原因:${String(parsed.failureReason)}`);
|
|
1364
1499
|
}
|
|
1365
1500
|
} else {
|
|
1366
|
-
lines.push("
|
|
1501
|
+
lines.push("写入前检查结果无法解析,请查看运行记录。");
|
|
1367
1502
|
}
|
|
1368
1503
|
} else if (prewriteResult && prewriteResult.status === 404) {
|
|
1369
|
-
lines.push("
|
|
1504
|
+
lines.push("写入前检查尚未执行,或没有留下检查结果。");
|
|
1370
1505
|
} else if (prewriteResult) {
|
|
1371
1506
|
lines.push(
|
|
1372
|
-
"
|
|
1507
|
+
"暂时无法读取写入前检查结果;为安全起见,不把它当成已通过。",
|
|
1373
1508
|
);
|
|
1374
1509
|
} else {
|
|
1375
|
-
lines.push("
|
|
1510
|
+
lines.push("写入前检查尚未执行,或没有留下检查结果。");
|
|
1376
1511
|
}
|
|
1377
1512
|
if (ledgerBody && ledgerBody.present === true) {
|
|
1378
|
-
|
|
1513
|
+
const value = (candidate) =>
|
|
1514
|
+
typeof candidate === "number" ? candidate : "未知";
|
|
1515
|
+
lines.push(
|
|
1516
|
+
`需求覆盖记录:原始片段 ${value(ledgerBody.fragmentCount)} 个;整理后的需求 ${value(ledgerBody.canonicalRequirementCount)} 条;待处理 ${value(ledgerBody.unresolvedCount)} 条;冲突 ${value(ledgerBody.conflictCount)} 条。`,
|
|
1517
|
+
);
|
|
1518
|
+
if (ledgerBody.stale === true) {
|
|
1519
|
+
lines.push("提醒:任务来源后来发生过变化,这份需求覆盖记录可能已经过时。");
|
|
1520
|
+
}
|
|
1379
1521
|
} else if (ledgerBody) {
|
|
1380
|
-
lines.push("
|
|
1522
|
+
lines.push("没有需求覆盖记录;可能是任务没有引用资料,或记录尚未生成。不会猜测数量。");
|
|
1381
1523
|
} else if (taskId) {
|
|
1382
|
-
lines.push("
|
|
1524
|
+
lines.push("需求覆盖记录暂时无法查看;不会把读取失败当成没有问题。");
|
|
1383
1525
|
}
|
|
1384
1526
|
return lines;
|
|
1385
1527
|
}
|
|
@@ -1426,7 +1568,7 @@ export async function fetchFrontendGateLedgerData(dagRunId) {
|
|
|
1426
1568
|
async function renderFrontendGateLedger(content, dagRunId) {
|
|
1427
1569
|
const section = el("section", "node-input-section");
|
|
1428
1570
|
section.appendChild(
|
|
1429
|
-
el("h4", null, "
|
|
1571
|
+
el("h4", null, "前端写入前检查与需求覆盖记录"),
|
|
1430
1572
|
);
|
|
1431
1573
|
const status = el("p", "muted", "加载中…");
|
|
1432
1574
|
status.setAttribute("role", "status");
|
|
@@ -1458,6 +1600,9 @@ async function renderFrontendGateLedger(content, dagRunId) {
|
|
|
1458
1600
|
* executes it as a Console operation, and on success we jump to the new run.
|
|
1459
1601
|
*/
|
|
1460
1602
|
function buildContextExportSection(dagRunId, node) {
|
|
1603
|
+
const formScope = workspaceScope();
|
|
1604
|
+
let exportAttempt = null;
|
|
1605
|
+
let exportSourceLoading = true;
|
|
1461
1606
|
const zone = el("section", "dag-action-export-zone");
|
|
1462
1607
|
zone.dataset.actionSection = "context-export";
|
|
1463
1608
|
const divider = el("div", "dag-action-export-divider");
|
|
@@ -1472,11 +1617,11 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1472
1617
|
const heading = el("div", "context-export-heading");
|
|
1473
1618
|
const headingCopy = el("div", null);
|
|
1474
1619
|
headingCopy.appendChild(el("span", "context-export-eyebrow", "上下文交接包"));
|
|
1475
|
-
const title = el("h4", null, "
|
|
1620
|
+
const title = el("h4", null, "把这个节点的资料交给另一个 AI");
|
|
1476
1621
|
title.id = "context-export-title";
|
|
1477
1622
|
headingCopy.appendChild(title);
|
|
1478
1623
|
headingCopy.appendChild(
|
|
1479
|
-
el("p", null, "
|
|
1624
|
+
el("p", null, "选择接收方式和是否脱敏,系统会生成一个带来源校验信息的 ZIP 交接包。"),
|
|
1480
1625
|
);
|
|
1481
1626
|
const invariant = el("div", "context-export-invariant");
|
|
1482
1627
|
const lockIcon = el("i", "ri-lock-2-line");
|
|
@@ -1560,10 +1705,10 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1560
1705
|
ticket.setAttribute("aria-live", "polite");
|
|
1561
1706
|
const ticketTop = el("div", "context-export-ticket-top");
|
|
1562
1707
|
ticketTop.append(
|
|
1563
|
-
el("span", "context-export-ticket-type", "
|
|
1708
|
+
el("span", "context-export-ticket-type", "交接资料 / ZIP"),
|
|
1564
1709
|
el("span", "context-export-ticket-safety", "已脱敏"),
|
|
1565
1710
|
);
|
|
1566
|
-
const ticketTitle = el("strong", "context-export-ticket-title", "通用
|
|
1711
|
+
const ticketTitle = el("strong", "context-export-ticket-title", "通用 AI 交接包");
|
|
1567
1712
|
const ticketRows = el("dl", "context-export-ticket-rows");
|
|
1568
1713
|
const appendTicketRow = (label, value, className) => {
|
|
1569
1714
|
const term = el("dt", null, label);
|
|
@@ -1571,12 +1716,12 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1571
1716
|
ticketRows.append(term, detail);
|
|
1572
1717
|
return detail;
|
|
1573
1718
|
};
|
|
1574
|
-
const recipientValue = appendTicketRow("用途", "适合交给其他
|
|
1719
|
+
const recipientValue = appendTicketRow("用途", "适合交给其他 AI 工具继续处理");
|
|
1575
1720
|
const contentsValue = appendTicketRow("包含", CONTEXT_EXPORT_TARGETS[0].contents, "is-mono");
|
|
1576
1721
|
const protectionValue = appendTicketRow("保护", "自动隐藏匹配到的疑似凭据");
|
|
1577
1722
|
const attemptValue = appendTicketRow(
|
|
1578
1723
|
"来源",
|
|
1579
|
-
`${node.nodeId} ·
|
|
1724
|
+
`${node.nodeId} · ${exportAttempt ? `第 ${exportAttempt} 次尝试` : exportSourceLoading ? "正在确认最新尝试…" : "暂无可用上下文"}`,
|
|
1580
1725
|
"is-mono",
|
|
1581
1726
|
);
|
|
1582
1727
|
ticket.append(
|
|
@@ -1584,7 +1729,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1584
1729
|
el("span", "context-export-ticket-label", "即将生成"),
|
|
1585
1730
|
ticketTitle,
|
|
1586
1731
|
ticketRows,
|
|
1587
|
-
el("p", "context-export-ticket-foot", "
|
|
1732
|
+
el("p", "context-export-ticket-foot", "附带文件清单和内容校验码,可以核对资料来源与完整性"),
|
|
1588
1733
|
);
|
|
1589
1734
|
workspace.appendChild(ticket);
|
|
1590
1735
|
section.appendChild(workspace);
|
|
@@ -1604,7 +1749,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1604
1749
|
recipientValue.textContent = target.description;
|
|
1605
1750
|
contentsValue.textContent = target.contents;
|
|
1606
1751
|
protectionValue.textContent = profile.description;
|
|
1607
|
-
attemptValue.textContent = `${node.nodeId} ·
|
|
1752
|
+
attemptValue.textContent = `${node.nodeId} · ${exportAttempt ? `第 ${exportAttempt} 次尝试` : exportSourceLoading ? "正在确认最新尝试…" : "暂无可用上下文"}`;
|
|
1608
1753
|
const safety = ticket.querySelector(".context-export-ticket-safety");
|
|
1609
1754
|
if (safety) {
|
|
1610
1755
|
safety.textContent = profileValue === "raw" ? "原始内容" : "已脱敏";
|
|
@@ -1624,6 +1769,7 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1624
1769
|
submit.type = "button";
|
|
1625
1770
|
submit.className = "prompt-restart-submit context-export-submit";
|
|
1626
1771
|
submit.dataset.testid = "context-export-submit";
|
|
1772
|
+
submit.disabled = true;
|
|
1627
1773
|
const submitIcon = el("i", "ri-download-cloud-2-line");
|
|
1628
1774
|
submitIcon.setAttribute("aria-hidden", "true");
|
|
1629
1775
|
submit.append(submitIcon, document.createTextNode(" 生成交接包并下载"));
|
|
@@ -1637,8 +1783,35 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1637
1783
|
status.textContent = message;
|
|
1638
1784
|
status.classList.toggle("prompt-restart-error", error);
|
|
1639
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();
|
|
1640
1812
|
submit.addEventListener("click", () => {
|
|
1641
1813
|
void (async () => {
|
|
1814
|
+
if (exportAttempt == null || workspaceScope() !== formScope) return;
|
|
1642
1815
|
const targetValue = selectedValue("context-export-target");
|
|
1643
1816
|
const profileValue = selectedValue("context-export-profile");
|
|
1644
1817
|
if (profileValue === "raw" && !rawAck.checked) {
|
|
@@ -1646,8 +1819,10 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1646
1819
|
return;
|
|
1647
1820
|
}
|
|
1648
1821
|
submit.disabled = true;
|
|
1822
|
+
refreshSource.disabled = true;
|
|
1649
1823
|
try {
|
|
1650
|
-
setStatus("
|
|
1824
|
+
setStatus("正在生成带来源校验信息的交接包…");
|
|
1825
|
+
if (workspaceScope() !== formScope) return;
|
|
1651
1826
|
const clientRequestId = `inspect-context-export-${Date.now()}-${Math.random()
|
|
1652
1827
|
.toString(36)
|
|
1653
1828
|
.slice(2, 10)}`;
|
|
@@ -1656,25 +1831,28 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1656
1831
|
{
|
|
1657
1832
|
runId: dagRunId,
|
|
1658
1833
|
nodeId: node.nodeId,
|
|
1659
|
-
attempt:
|
|
1834
|
+
attempt: exportAttempt,
|
|
1660
1835
|
target: targetValue,
|
|
1661
1836
|
profile: profileValue,
|
|
1662
1837
|
rawAcknowledged: profileValue === "raw" && rawAck.checked,
|
|
1663
1838
|
},
|
|
1664
1839
|
clientRequestId,
|
|
1665
1840
|
);
|
|
1841
|
+
if (workspaceScope() !== formScope) return;
|
|
1666
1842
|
const operationId = response.body?.operationId;
|
|
1667
1843
|
if (!response.ok || !operationId) {
|
|
1668
1844
|
throw new Error(response.body?.error?.message ?? "导出操作未被接受");
|
|
1669
1845
|
}
|
|
1670
1846
|
const operation = await pollOperationToTerminal(operationId, (message) =>
|
|
1671
|
-
setStatus(message.replace("续跑", "导出")),
|
|
1847
|
+
setStatus(message.replace("续跑", "导出")), formScope,
|
|
1672
1848
|
);
|
|
1849
|
+
if (workspaceScope() !== formScope) return;
|
|
1673
1850
|
if (operation?.state !== "succeeded" || !operation.result?.downloadUrl) {
|
|
1674
1851
|
throw new Error(operation?.errorMessage ?? "导出未成功完成");
|
|
1675
1852
|
}
|
|
1676
|
-
setStatus("
|
|
1853
|
+
setStatus("交接包已生成,正在安全下载…");
|
|
1677
1854
|
const downloaded = await downloadContextExport(operation.result.downloadUrl);
|
|
1855
|
+
if (workspaceScope() !== formScope) return;
|
|
1678
1856
|
const objectUrl = URL.createObjectURL(downloaded.blob);
|
|
1679
1857
|
const link = document.createElement("a");
|
|
1680
1858
|
link.href = objectUrl;
|
|
@@ -1684,12 +1862,13 @@ function buildContextExportSection(dagRunId, node) {
|
|
|
1684
1862
|
link.remove();
|
|
1685
1863
|
URL.revokeObjectURL(objectUrl);
|
|
1686
1864
|
setStatus(
|
|
1687
|
-
`下载完成 · ${operation.result.bundleId} · ${new Intl.NumberFormat("zh-CN").format(operation.result.bytes)}
|
|
1865
|
+
`下载完成 · ${operation.result.bundleId} · ${new Intl.NumberFormat("zh-CN").format(operation.result.bytes)} 字节\n内容校验码 ${operation.result.bundleSha256}`,
|
|
1688
1866
|
);
|
|
1689
1867
|
} catch (error) {
|
|
1690
1868
|
setStatus(transferErrorStatusText(error, "导出未成功完成"), true);
|
|
1691
1869
|
} finally {
|
|
1692
1870
|
submit.disabled = false;
|
|
1871
|
+
refreshSource.disabled = false;
|
|
1693
1872
|
}
|
|
1694
1873
|
})();
|
|
1695
1874
|
});
|
|
@@ -1739,16 +1918,11 @@ function buildDagActionAvailability({ title, description, reasonCode, tone = "ne
|
|
|
1739
1918
|
return panel;
|
|
1740
1919
|
}
|
|
1741
1920
|
|
|
1742
|
-
function buildFullContentCta() {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
return el(
|
|
1748
|
-
"p",
|
|
1749
|
-
"artifact-content-preview-full-cta",
|
|
1750
|
-
"预览有截断;当前版本暂不提供完整文件下载。",
|
|
1751
|
-
);
|
|
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;
|
|
1752
1926
|
}
|
|
1753
1927
|
|
|
1754
1928
|
function createArtifactContentPreview(
|
|
@@ -1816,14 +1990,14 @@ function createArtifactContentPreview(
|
|
|
1816
1990
|
el(
|
|
1817
1991
|
"span",
|
|
1818
1992
|
null,
|
|
1819
|
-
`${data.mediaType ?? "
|
|
1993
|
+
`${data.mediaType ?? "未知类型"} · ${new Intl.NumberFormat("zh-CN").format(data.sourceBytes ?? 0)} 字节`,
|
|
1820
1994
|
),
|
|
1821
1995
|
];
|
|
1822
1996
|
if (typeof data.sha256 === "string" && data.sha256.length >= 12) {
|
|
1823
1997
|
const sha = el(
|
|
1824
1998
|
"span",
|
|
1825
1999
|
null,
|
|
1826
|
-
` ·
|
|
2000
|
+
` · 内容校验码 ${data.sha256.slice(0, 12)}…`,
|
|
1827
2001
|
);
|
|
1828
2002
|
sha.title = data.sha256;
|
|
1829
2003
|
metaChildren.push(sha);
|
|
@@ -1835,7 +2009,7 @@ function createArtifactContentPreview(
|
|
|
1835
2009
|
el(
|
|
1836
2010
|
"p",
|
|
1837
2011
|
"artifact-content-preview-unavailable",
|
|
1838
|
-
"
|
|
2012
|
+
"该文件类型不适合直接在页面中显示;系统已经核对文件位置、大小和内容校验码。",
|
|
1839
2013
|
),
|
|
1840
2014
|
);
|
|
1841
2015
|
return;
|
|
@@ -1856,15 +2030,14 @@ function createArtifactContentPreview(
|
|
|
1856
2030
|
if (data.redacted) notes.push("疑似凭据已脱敏");
|
|
1857
2031
|
if (data.truncated) {
|
|
1858
2032
|
notes.push(
|
|
1859
|
-
`仅显示前 ${new Intl.NumberFormat("zh-CN").format(data.maxBytes ?? 0)}
|
|
2033
|
+
`仅显示前 ${new Intl.NumberFormat("zh-CN").format(data.maxBytes ?? 0)} 字节`,
|
|
1860
2034
|
);
|
|
1861
2035
|
}
|
|
1862
2036
|
if (notes.length > 0) {
|
|
1863
2037
|
host.appendChild(el("p", "artifact-content-preview-note", notes.join(" · ")));
|
|
1864
2038
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
}
|
|
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);
|
|
1868
2041
|
};
|
|
1869
2042
|
details.addEventListener("toggle", () => {
|
|
1870
2043
|
if (details.open) void load();
|
|
@@ -1872,29 +2045,69 @@ function createArtifactContentPreview(
|
|
|
1872
2045
|
return { element: details, reset };
|
|
1873
2046
|
}
|
|
1874
2047
|
|
|
1875
|
-
function
|
|
2048
|
+
export function artifactRevisionRunEligibility(run) {
|
|
2049
|
+
// File-level eligibility only says that a file has a known replacement
|
|
2050
|
+
// contract. Creating a derived run has stricter, run-level safeguards.
|
|
2051
|
+
// Keep this check here so the UI does not invite an action the server will
|
|
2052
|
+
// necessarily reject later.
|
|
2053
|
+
if (!run) return { eligible: true };
|
|
2054
|
+
const lifecycle = String(run.lifecycle ?? "").toLowerCase();
|
|
2055
|
+
const status = String(run.status ?? "").toLowerCase();
|
|
2056
|
+
if (lifecycle !== "completed") {
|
|
2057
|
+
return {
|
|
2058
|
+
eligible: false,
|
|
2059
|
+
title: "这次运行还没有完整结束",
|
|
2060
|
+
description:
|
|
2061
|
+
"请等待运行收尾为“已完成”。进行中、暂停中,或尚未收尾的报错,都不能从替换产出继续。",
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
if (!new Set(["failed", "partial_failed"]).has(status)) {
|
|
2065
|
+
return {
|
|
2066
|
+
eligible: false,
|
|
2067
|
+
title: "这次运行不支持替换产出后继续",
|
|
2068
|
+
description:
|
|
2069
|
+
"只有已完整结束且结果为“失败”或“部分失败”的运行,才能换文件后继续。当前运行已经成功完成;文件本身可替换,不代表可以创建后续运行。",
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
return { eligible: true };
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
function buildArtifactRevisionSection(dagRunId, node, run) {
|
|
2076
|
+
const formScope = workspaceScope();
|
|
1876
2077
|
const { section, body } = createDagActionCard({
|
|
1877
|
-
eyebrow: "
|
|
1878
|
-
title: "
|
|
1879
|
-
description: "
|
|
2078
|
+
eyebrow: "替换已有文件",
|
|
2079
|
+
title: "换掉这个节点的产出后继续",
|
|
2080
|
+
description: "上传新文件后,系统会先检查格式并告诉你哪些后续节点需要重做;确认前不会改动原运行。",
|
|
1880
2081
|
icon: "ri-file-transfer-line",
|
|
1881
2082
|
className: "artifact-revision-box",
|
|
1882
2083
|
});
|
|
1883
2084
|
section.dataset.actionSection = "artifact-revision";
|
|
2085
|
+
const runEligibility = artifactRevisionRunEligibility(run);
|
|
2086
|
+
if (!runEligibility.eligible) {
|
|
2087
|
+
body.appendChild(
|
|
2088
|
+
buildDagActionAvailability({
|
|
2089
|
+
tone: "blocked",
|
|
2090
|
+
title: runEligibility.title,
|
|
2091
|
+
description: runEligibility.description,
|
|
2092
|
+
}),
|
|
2093
|
+
);
|
|
2094
|
+
return section;
|
|
2095
|
+
}
|
|
1884
2096
|
body.appendChild(
|
|
1885
2097
|
buildDagActionAvailability({
|
|
1886
|
-
title: "
|
|
1887
|
-
description: "
|
|
2098
|
+
title: "正在检查这个文件能不能安全替换",
|
|
2099
|
+
description: "系统正在读取运行开始前保存的文件关系和替换规则。",
|
|
1888
2100
|
}),
|
|
1889
2101
|
);
|
|
1890
2102
|
void (async () => {
|
|
1891
2103
|
const response = await fetchDagNodeArtifacts(dagRunId, node.nodeId);
|
|
2104
|
+
if (workspaceScope() !== formScope || !body.isConnected) return;
|
|
1892
2105
|
clearNode(body);
|
|
1893
2106
|
if (!response.ok || response.body?.binding?.available !== true) {
|
|
1894
2107
|
body.appendChild(
|
|
1895
2108
|
buildDagActionAvailability({
|
|
1896
2109
|
tone: "blocked",
|
|
1897
|
-
title: "
|
|
2110
|
+
title: "这个运行暂时不能安全替换产出",
|
|
1898
2111
|
description:
|
|
1899
2112
|
"缺少结构化的产出关系,系统无法可靠判断哪些后续节点会受影响,也不会根据文件名或提示词猜测。",
|
|
1900
2113
|
reasonCode:
|
|
@@ -1921,8 +2134,8 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
1921
2134
|
body.appendChild(
|
|
1922
2135
|
buildDagActionAvailability({
|
|
1923
2136
|
tone: "ready",
|
|
1924
|
-
title:
|
|
1925
|
-
description: "
|
|
2137
|
+
title: `可以替换 ${eligible.length} 个已生成文件`,
|
|
2138
|
+
description: "原运行不会被修改;确认后系统会另外创建一次新运行。",
|
|
1926
2139
|
}),
|
|
1927
2140
|
);
|
|
1928
2141
|
const form = el("div", "dag-action-form artifact-revision-form");
|
|
@@ -1951,7 +2164,7 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
1951
2164
|
form.appendChild(currentArtifactPreview.element);
|
|
1952
2165
|
artifactSelect.addEventListener("change", currentArtifactPreview.reset);
|
|
1953
2166
|
const fileLabel = el("label", "prompt-restart-field");
|
|
1954
|
-
fileLabel.appendChild(el("span", "dag-action-field-label", "选择新文件(最多 10
|
|
2167
|
+
fileLabel.appendChild(el("span", "dag-action-field-label", "选择新文件(最多 10 MB)"));
|
|
1955
2168
|
const file = document.createElement("input");
|
|
1956
2169
|
file.type = "file";
|
|
1957
2170
|
file.className = "dag-action-file-input";
|
|
@@ -1963,7 +2176,7 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
1963
2176
|
const reason = document.createElement("textarea");
|
|
1964
2177
|
reason.className = "prompt-restart-textarea";
|
|
1965
2178
|
reason.rows = 3;
|
|
1966
|
-
reason.placeholder = "
|
|
2179
|
+
reason.placeholder = "例如:计划遗漏了一个验收条件,需要更正后继续";
|
|
1967
2180
|
reason.dataset.testid = "artifact-revision-reason";
|
|
1968
2181
|
reasonLabel.appendChild(reason);
|
|
1969
2182
|
form.appendChild(reasonLabel);
|
|
@@ -1984,20 +2197,42 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
1984
2197
|
const planHost = el("div", "artifact-revision-plan");
|
|
1985
2198
|
form.appendChild(planHost);
|
|
1986
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);
|
|
1987
2218
|
const setStatus = (message, error = false) => {
|
|
1988
2219
|
status.textContent = message;
|
|
1989
2220
|
status.classList.toggle("prompt-restart-error", error);
|
|
1990
2221
|
};
|
|
2222
|
+
const syncArtifactRetry = mountSubmissionRetry(form, artifactDraft, dagRunId, node.nodeId, formScope, setStatus);
|
|
1991
2223
|
preview.addEventListener("click", () => {
|
|
1992
2224
|
void (async () => {
|
|
2225
|
+
if (hasUnresolvedSubmission(artifactDraft.submission) || artifactDraft.submission?.operationId) { setStatus("已有提交记录,请使用“重试原提交”或查看操作进度。"); return; }
|
|
1993
2226
|
const selected = eligible.find((artifact) => artifact.artifactId === artifactSelect.value);
|
|
1994
|
-
const selectedFile = file.files?.[0];
|
|
2227
|
+
const selectedFile = file.files?.[0] ?? artifactDraft.file;
|
|
2228
|
+
const requestedRevision = draftRevision;
|
|
2229
|
+
const requestedReason = reason.value.trim();
|
|
1995
2230
|
if (!selected || !selectedFile) {
|
|
1996
2231
|
setStatus("请选择一个替换文件。", true);
|
|
1997
2232
|
return;
|
|
1998
2233
|
}
|
|
1999
2234
|
if (selectedFile.size > 10 * 1024 * 1024) {
|
|
2000
|
-
setStatus("文件超过 10
|
|
2235
|
+
setStatus("文件超过 10 MB 限制。", true);
|
|
2001
2236
|
return;
|
|
2002
2237
|
}
|
|
2003
2238
|
if (!reason.value.trim()) {
|
|
@@ -2014,8 +2249,9 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
2014
2249
|
reader.readAsDataURL(selectedFile);
|
|
2015
2250
|
});
|
|
2016
2251
|
const candidateBase64 = String(dataUrl).split(",", 2)[1] ?? "";
|
|
2017
|
-
setStatus("
|
|
2252
|
+
setStatus("正在检查新文件格式并对比差异…");
|
|
2018
2253
|
const stageId = `artifact-stage-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2254
|
+
if (workspaceScope() !== formScope) return;
|
|
2019
2255
|
const staged = await postOperatorAction(
|
|
2020
2256
|
"artifactRevisionStage",
|
|
2021
2257
|
{
|
|
@@ -2027,38 +2263,49 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
2027
2263
|
},
|
|
2028
2264
|
stageId,
|
|
2029
2265
|
);
|
|
2266
|
+
if (workspaceScope() !== formScope) return;
|
|
2030
2267
|
if (!staged.ok || !staged.body?.operationId) throw new Error(staged.body?.error?.message ?? "修订 staging 失败");
|
|
2031
2268
|
const stageOp = await pollOperationToTerminal(staged.body.operationId, (message) =>
|
|
2032
|
-
setStatus(message.replace("续跑", "修订校验")),
|
|
2269
|
+
setStatus(message.replace("续跑", "修订校验")), formScope,
|
|
2033
2270
|
);
|
|
2034
2271
|
const revision = stageOp?.result?.revision;
|
|
2035
2272
|
if (stageOp?.state !== "succeeded" || !revision?.revisionId) {
|
|
2036
2273
|
throw new Error(stageOp?.errorMessage ?? "修订校验失败");
|
|
2037
2274
|
}
|
|
2038
|
-
setStatus("
|
|
2275
|
+
setStatus("文件检查通过,正在计算哪些后续节点需要重新执行…");
|
|
2039
2276
|
const planId = `artifact-plan-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2277
|
+
if (workspaceScope() !== formScope) return;
|
|
2040
2278
|
const planned = await postOperatorAction(
|
|
2041
2279
|
"artifactRevisionPlan",
|
|
2042
2280
|
{ runId: dagRunId, artifactId: selected.artifactId, revisionId: revision.revisionId },
|
|
2043
2281
|
planId,
|
|
2044
2282
|
);
|
|
2283
|
+
if (workspaceScope() !== formScope) return;
|
|
2045
2284
|
if (!planned.ok || !planned.body?.operationId) throw new Error(planned.body?.error?.message ?? "影响预览失败");
|
|
2046
2285
|
const planOp = await pollOperationToTerminal(planned.body.operationId, (message) =>
|
|
2047
|
-
setStatus(message.replace("续跑", "影响分析")),
|
|
2286
|
+
setStatus(message.replace("续跑", "影响分析")), formScope,
|
|
2048
2287
|
);
|
|
2288
|
+
if (workspaceScope() !== formScope) return;
|
|
2049
2289
|
const plan = planOp?.result?.plan;
|
|
2050
2290
|
if (!plan) throw new Error(planOp?.errorMessage ?? "影响预览失败");
|
|
2051
|
-
|
|
2291
|
+
if (draftRevision !== requestedRevision || !body.isConnected) { setStatus("草稿已变化,请重新预览。"); return; }
|
|
2292
|
+
planHost.appendChild(el("h5", null, "替换结果与影响范围"));
|
|
2052
2293
|
planHost.appendChild(
|
|
2053
2294
|
transparencyMetaList([
|
|
2054
|
-
["
|
|
2055
|
-
["
|
|
2056
|
-
[
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
[
|
|
2061
|
-
|
|
2295
|
+
["本次替换编号", revision.revisionId],
|
|
2296
|
+
["新文件校验码", revision.candidate?.sha256],
|
|
2297
|
+
[
|
|
2298
|
+
"文件内容类型",
|
|
2299
|
+
inspectorValueLabel("差异类型", revision.diff?.kind),
|
|
2300
|
+
],
|
|
2301
|
+
[
|
|
2302
|
+
"预计风险",
|
|
2303
|
+
inspectorValueLabel("风险级别", plan.risk),
|
|
2304
|
+
],
|
|
2305
|
+
["最先受影响的节点", plan.impact?.directConsumerNodeIds?.join(", ") || "无"],
|
|
2306
|
+
["会重新执行的节点", plan.impact?.affectedNodeIds?.join(", ") || "无"],
|
|
2307
|
+
["会沿用结果的节点", plan.impact?.importedNodeIds?.join(", ") || "无"],
|
|
2308
|
+
["本次计划校验码", plan.planHash],
|
|
2062
2309
|
]),
|
|
2063
2310
|
);
|
|
2064
2311
|
if (revision.diff?.summary) {
|
|
@@ -2069,7 +2316,7 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
2069
2316
|
}
|
|
2070
2317
|
if (plan.eligible !== true) {
|
|
2071
2318
|
setStatus(
|
|
2072
|
-
|
|
2319
|
+
`暂时不能创建新运行:${formatContextTransferReasonList(plan.blockedReasons) ?? "当前不满足继续运行的条件"}`,
|
|
2073
2320
|
true,
|
|
2074
2321
|
);
|
|
2075
2322
|
return;
|
|
@@ -2082,23 +2329,22 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
2082
2329
|
planHost.appendChild(execute);
|
|
2083
2330
|
execute.addEventListener("click", () => {
|
|
2084
2331
|
void (async () => {
|
|
2332
|
+
if (draftRevision !== requestedRevision || hasUnresolvedSubmission(artifactDraft.submission) || artifactDraft.submission?.operationId) { setStatus("此预览已变化或已经提交,请查看操作进度。", true); return; }
|
|
2085
2333
|
execute.disabled = true;
|
|
2086
2334
|
try {
|
|
2087
|
-
setStatus("
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
);
|
|
2100
|
-
if (!accepted.ok || !accepted.body?.operationId) throw new Error(accepted.body?.error?.message ?? "派生运行未被接受");
|
|
2101
|
-
const operation = await pollOperationToTerminal(accepted.body.operationId, setStatus);
|
|
2335
|
+
setStatus("正在创建新运行并重新执行受影响节点;原运行保持不变…");
|
|
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);
|
|
2342
|
+
if (!accepted.ok || !accepted.body?.operationId) throw new Error(accepted.body?.error?.message ?? "新运行未被接受");
|
|
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;
|
|
2102
2348
|
const childRunId = operation?.result?.newRunId;
|
|
2103
2349
|
if (childRunId) {
|
|
2104
2350
|
const open = document.createElement("button");
|
|
@@ -2115,17 +2361,19 @@ function buildArtifactRevisionSection(dagRunId, node) {
|
|
|
2115
2361
|
planHost.appendChild(open);
|
|
2116
2362
|
}
|
|
2117
2363
|
if (operation?.state !== "succeeded" || !operation.result?.newRunId) {
|
|
2118
|
-
throw new Error(operation?.errorMessage ?? operation?.result?.message ?? "
|
|
2364
|
+
throw new Error(operation?.errorMessage ?? operation?.result?.message ?? "新运行失败");
|
|
2119
2365
|
}
|
|
2120
|
-
setStatus(
|
|
2366
|
+
setStatus(`新运行完成:${operation.result.newRunId}`);
|
|
2121
2367
|
} catch (error) {
|
|
2122
|
-
setStatus(transferErrorStatusText(error, "
|
|
2368
|
+
setStatus(transferErrorStatusText(error, "新运行失败"), true);
|
|
2123
2369
|
} finally {
|
|
2124
|
-
|
|
2370
|
+
if (artifactDraft.submission) artifactDraft.submission.inFlight = false;
|
|
2371
|
+
execute.disabled = Boolean(artifactDraft.submission?.operationId) || hasUnresolvedSubmission(artifactDraft.submission);
|
|
2372
|
+
syncArtifactRetry();
|
|
2125
2373
|
}
|
|
2126
2374
|
})();
|
|
2127
2375
|
});
|
|
2128
|
-
setStatus("
|
|
2376
|
+
setStatus("影响范围已经锁定;确认时系统会再次检查文件、运行和计划是否有变化。" );
|
|
2129
2377
|
} catch (error) {
|
|
2130
2378
|
setStatus(transferErrorStatusText(error, "影响预览失败"), true);
|
|
2131
2379
|
} finally {
|
|
@@ -2149,20 +2397,27 @@ function buildDagActionIntro(node) {
|
|
|
2149
2397
|
"重跑会创建新的运行;当前运行及其已经保存的事实始终保持不变。",
|
|
2150
2398
|
),
|
|
2151
2399
|
);
|
|
2152
|
-
const
|
|
2400
|
+
const rawState = node.status ?? "unknown";
|
|
2401
|
+
const state = el(
|
|
2402
|
+
"span",
|
|
2403
|
+
"dag-action-node-state",
|
|
2404
|
+
inspectorValueLabel("status", rawState),
|
|
2405
|
+
);
|
|
2406
|
+
if (state.textContent !== rawState) state.setAttribute("title", `原始值:${rawState}`);
|
|
2153
2407
|
intro.append(copy, state);
|
|
2154
2408
|
return intro;
|
|
2155
2409
|
}
|
|
2156
2410
|
|
|
2157
|
-
async function renderPromptRestart(content, dagRunId, node) {
|
|
2411
|
+
async function renderPromptRestart(content, dagRunId, node, run) {
|
|
2412
|
+
const formScope = workspaceScope();
|
|
2158
2413
|
const nodeId = node.nodeId;
|
|
2159
2414
|
clearNode(content);
|
|
2160
2415
|
const intro = buildDagActionIntro(node);
|
|
2161
|
-
const revisionSection = buildArtifactRevisionSection(dagRunId, node);
|
|
2416
|
+
const revisionSection = buildArtifactRevisionSection(dagRunId, node, run);
|
|
2162
2417
|
const { section: promptSection, body: promptBody } = createDagActionCard({
|
|
2163
2418
|
eyebrow: "提示词重跑",
|
|
2164
2419
|
title: "修改失败提示后重新执行",
|
|
2165
|
-
description: "
|
|
2420
|
+
description: "只有执行失败的节点可以修改提示词并从这里继续;其他节点只能查看运行开始前保存的提示词。",
|
|
2166
2421
|
icon: "ri-restart-line",
|
|
2167
2422
|
className: "prompt-rerun-action-box",
|
|
2168
2423
|
});
|
|
@@ -2170,7 +2425,7 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2170
2425
|
promptBody.appendChild(
|
|
2171
2426
|
buildDagActionAvailability({
|
|
2172
2427
|
title: "正在读取节点提示",
|
|
2173
|
-
description: "
|
|
2428
|
+
description: "检查运行开始前保存的版本,以及每次失败时实际使用的版本。",
|
|
2174
2429
|
}),
|
|
2175
2430
|
);
|
|
2176
2431
|
const exportSection = buildContextExportSection(dagRunId, node);
|
|
@@ -2180,10 +2435,24 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2180
2435
|
["context-export", exportSection],
|
|
2181
2436
|
]);
|
|
2182
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);
|
|
2183
2450
|
for (const sectionId of DAG_ACTION_SECTION_ORDER) {
|
|
2184
2451
|
const section = sections.get(sectionId);
|
|
2185
2452
|
if (section) content.appendChild(section);
|
|
2186
2453
|
}
|
|
2454
|
+
chooseIntent(selectedIntent);
|
|
2455
|
+
mountInspectorOperations(content, dagRunId, nodeId);
|
|
2187
2456
|
const input = await fetchDagNodeInput(dagRunId, nodeId);
|
|
2188
2457
|
if (
|
|
2189
2458
|
!content.isConnected ||
|
|
@@ -2199,7 +2468,7 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2199
2468
|
promptBody.appendChild(
|
|
2200
2469
|
buildDagActionAvailability({
|
|
2201
2470
|
tone: "blocked",
|
|
2202
|
-
title: "
|
|
2471
|
+
title: "无法读取这个节点原先保存的提示词",
|
|
2203
2472
|
description: "当前不能安全预览或修改提示词。可以继续使用最下方的上下文导出。",
|
|
2204
2473
|
}),
|
|
2205
2474
|
);
|
|
@@ -2210,7 +2479,7 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2210
2479
|
if (!frozenBasePrompt) {
|
|
2211
2480
|
promptBody.appendChild(
|
|
2212
2481
|
buildDagActionAvailability({
|
|
2213
|
-
title: "
|
|
2482
|
+
title: "这个节点没有保存原始提示词",
|
|
2214
2483
|
description: "因此不能从这里修改提示词重跑。",
|
|
2215
2484
|
}),
|
|
2216
2485
|
);
|
|
@@ -2220,7 +2489,10 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2220
2489
|
frozenBasePrompt,
|
|
2221
2490
|
input.body.promptCandidates,
|
|
2222
2491
|
);
|
|
2223
|
-
|
|
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];
|
|
2224
2496
|
const historicalCandidatesMissing = Array.isArray(input.body.warnings)
|
|
2225
2497
|
? input.body.warnings.some((warning) =>
|
|
2226
2498
|
String(warning).startsWith(
|
|
@@ -2236,12 +2508,12 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2236
2508
|
createPromptRestartBanner({
|
|
2237
2509
|
tone: "readonly",
|
|
2238
2510
|
kicker: "只读",
|
|
2239
|
-
title:
|
|
2240
|
-
body: "
|
|
2511
|
+
title: `当前节点:${inspectorValueLabel("status", node.status ?? "unknown")}`,
|
|
2512
|
+
body: "只有执行失败的节点支持编辑提示词并重启。下面可以查看原始提示词,以及历次失败时实际使用的提示词。",
|
|
2241
2513
|
}),
|
|
2242
2514
|
);
|
|
2243
2515
|
const promptCard = createPromptRestartPromptCard({
|
|
2244
|
-
title: "
|
|
2516
|
+
title: "运行开始前保存的提示词",
|
|
2245
2517
|
badge: "只读",
|
|
2246
2518
|
readCandidate: () => selectedCandidate,
|
|
2247
2519
|
preTestId: "prompt-restart-readonly",
|
|
@@ -2253,9 +2525,9 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2253
2525
|
hint:
|
|
2254
2526
|
promptCandidateOptions.length === 1
|
|
2255
2527
|
? historicalCandidatesMissing
|
|
2256
|
-
? "
|
|
2257
|
-
: "
|
|
2258
|
-
: "
|
|
2528
|
+
? "这个较早的运行没有保存失败时实际使用的提示词;新运行会在这里列出。"
|
|
2529
|
+
: "该节点没有保存过失败时使用的提示词。"
|
|
2530
|
+
: "可以切换查看原始版本和历次失败时使用的版本。",
|
|
2259
2531
|
onChange: (nextKey) => {
|
|
2260
2532
|
selectedCandidate =
|
|
2261
2533
|
promptCandidateOptions.find((option) => option.key === nextKey) ??
|
|
@@ -2273,7 +2545,7 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2273
2545
|
promptBody.appendChild(
|
|
2274
2546
|
buildDagActionAvailability({
|
|
2275
2547
|
tone: "blocked",
|
|
2276
|
-
title: "
|
|
2548
|
+
title: "保存的提示词不完整,无法安全编辑",
|
|
2277
2549
|
description:
|
|
2278
2550
|
"为避免用截断文本覆盖完整提示,请改用 CLI:dag rerun --prompt-override。",
|
|
2279
2551
|
}),
|
|
@@ -2287,11 +2559,11 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2287
2559
|
tone: "edit",
|
|
2288
2560
|
kicker: "可编辑",
|
|
2289
2561
|
title: "从失败节点编辑提示词并重启",
|
|
2290
|
-
body: "
|
|
2562
|
+
body: "原运行的设置和记录保持不变;系统会另建一次使用新提示词的运行。",
|
|
2291
2563
|
}),
|
|
2292
2564
|
);
|
|
2293
2565
|
const promptCard = createPromptRestartPromptCard({
|
|
2294
|
-
title:
|
|
2566
|
+
title: `原始提示词(只读 · ${nodeId})`,
|
|
2295
2567
|
badge: "底稿",
|
|
2296
2568
|
readCandidate: () => selectedCandidate,
|
|
2297
2569
|
preTestId: "prompt-restart-base",
|
|
@@ -2304,31 +2576,29 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2304
2576
|
label: "提示词版本",
|
|
2305
2577
|
hint:
|
|
2306
2578
|
promptCandidateOptions.length > 1
|
|
2307
|
-
? "
|
|
2579
|
+
? "选择某次失败使用的版本后,会载入完整文本供你修改;原始提示词仍可选择只追加补充说明。"
|
|
2308
2580
|
: historicalCandidatesMissing
|
|
2309
|
-
? "
|
|
2310
|
-
: "
|
|
2581
|
+
? "这个较早的运行没有保存失败时实际使用的提示词;新运行会在这里列出。"
|
|
2582
|
+
: "该节点没有保存过失败时使用的提示词;当前只有原始提示词。",
|
|
2311
2583
|
onChange: (nextKey) => {
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2584
|
+
saveDraft();
|
|
2585
|
+
selectedCandidate = promptCandidateOptions.find((option) => option.key === nextKey) ?? promptCandidateOptions[0];
|
|
2586
|
+
draft.candidateKey = selectedCandidate.key;
|
|
2315
2587
|
promptCard.sync(selectedCandidate);
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
modeSelect.value = "replace";
|
|
2319
|
-
}
|
|
2588
|
+
restoreDraft();
|
|
2589
|
+
invalidatePlan();
|
|
2320
2590
|
picker.hintEl.hidden = false;
|
|
2321
2591
|
picker.hintEl.textContent = selectedCandidate.truncated
|
|
2322
2592
|
? "该候选已截断,仅供查看,不能直接提交重启。"
|
|
2323
2593
|
: selectedCandidate.key === "frozen"
|
|
2324
|
-
? "
|
|
2325
|
-
: "
|
|
2594
|
+
? "原始提示词可以选择追加说明,也可以全文替换。"
|
|
2595
|
+
: "已载入这次失败时使用的提示词;修改完整文本后即可重启。";
|
|
2326
2596
|
applyModeVisibility();
|
|
2327
2597
|
},
|
|
2328
2598
|
});
|
|
2329
2599
|
const modeSwitch = createPromptRestartModeSwitch({
|
|
2330
2600
|
value: "append",
|
|
2331
|
-
onChange: () => applyModeVisibility(),
|
|
2601
|
+
onChange: () => { applyModeVisibility(); saveDraft(); invalidatePlan(); },
|
|
2332
2602
|
});
|
|
2333
2603
|
const modeSelect = modeSwitch.group;
|
|
2334
2604
|
const toolbar = el("div", "prompt-restart-toolbar");
|
|
@@ -2344,7 +2614,7 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2344
2614
|
el(
|
|
2345
2615
|
"p",
|
|
2346
2616
|
"prompt-restart-hint",
|
|
2347
|
-
"
|
|
2617
|
+
"这段补充会加在原提示词末尾,不会改写原文(内部记录为 operator_addendum)。",
|
|
2348
2618
|
),
|
|
2349
2619
|
);
|
|
2350
2620
|
const addendumArea = document.createElement("textarea");
|
|
@@ -2371,6 +2641,8 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2371
2641
|
box.appendChild(baseLabel);
|
|
2372
2642
|
|
|
2373
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";
|
|
2374
2646
|
const append = modeSelect.value === "append";
|
|
2375
2647
|
baseLabel.classList.toggle("is-hidden", !append);
|
|
2376
2648
|
addendumLabel.classList.toggle("is-hidden", !append);
|
|
@@ -2379,14 +2651,14 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2379
2651
|
applyModeVisibility();
|
|
2380
2652
|
|
|
2381
2653
|
const reasonLabel = el("label", "prompt-restart-field");
|
|
2382
|
-
reasonLabel.appendChild(el("span", "prompt-restart-label", "
|
|
2654
|
+
reasonLabel.appendChild(el("span", "prompt-restart-label", "为什么要重启?"));
|
|
2383
2655
|
reasonLabel.appendChild(
|
|
2384
|
-
el("p", "prompt-restart-hint", "
|
|
2656
|
+
el("p", "prompt-restart-hint", "这段说明会随新运行一起保存,方便以后追溯。"),
|
|
2385
2657
|
);
|
|
2386
2658
|
const reasonArea = document.createElement("textarea");
|
|
2387
2659
|
reasonArea.className = "prompt-restart-textarea prompt-restart-textarea-compact";
|
|
2388
2660
|
reasonArea.rows = 3;
|
|
2389
|
-
reasonArea.value =
|
|
2661
|
+
reasonArea.value = `修改失败节点 ${nodeId} 的提示词后重新运行(来源运行:${dagRunId})`;
|
|
2390
2662
|
reasonArea.setAttribute("data-testid", "prompt-restart-reason");
|
|
2391
2663
|
reasonLabel.appendChild(reasonArea);
|
|
2392
2664
|
box.appendChild(reasonLabel);
|
|
@@ -2398,10 +2670,44 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2398
2670
|
const submit = document.createElement("button");
|
|
2399
2671
|
submit.type = "button";
|
|
2400
2672
|
submit.className = "prompt-restart-submit";
|
|
2401
|
-
submit.textContent = "
|
|
2673
|
+
submit.textContent = "预览重跑影响";
|
|
2402
2674
|
submit.setAttribute("data-testid", "prompt-restart-submit");
|
|
2403
2675
|
row.appendChild(status);
|
|
2404
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);
|
|
2405
2711
|
box.appendChild(row);
|
|
2406
2712
|
|
|
2407
2713
|
promptBody.appendChild(box);
|
|
@@ -2411,7 +2717,11 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2411
2717
|
status.classList.toggle("prompt-restart-error", isError);
|
|
2412
2718
|
};
|
|
2413
2719
|
|
|
2414
|
-
|
|
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; }
|
|
2415
2725
|
void (async () => {
|
|
2416
2726
|
if (selectedCandidate.truncated) {
|
|
2417
2727
|
setStatus("所选失败提示词已截断,不能作为完整提示词重启。", true);
|
|
@@ -2440,15 +2750,22 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2440
2750
|
return;
|
|
2441
2751
|
}
|
|
2442
2752
|
submit.disabled = true;
|
|
2753
|
+
start.disabled = true;
|
|
2754
|
+
const requestedSignature = signature();
|
|
2443
2755
|
try {
|
|
2444
|
-
setStatus("正在做重跑资格预检(dagRerunPlan)…");
|
|
2445
2756
|
const overrides = [{ nodeId, prompt, mode }];
|
|
2757
|
+
let plan = prepared?.plan;
|
|
2758
|
+
if (!executeRequested) {
|
|
2759
|
+
invalidatePlan();
|
|
2760
|
+
setStatus("正在检查这次重跑是否安全可行…");
|
|
2761
|
+
if (workspaceScope() !== formScope) return;
|
|
2446
2762
|
const planRes = await postOperatorAction("dagRerunPlan", {
|
|
2447
2763
|
runId: dagRunId,
|
|
2448
2764
|
fromNode: nodeId,
|
|
2449
2765
|
promptOverrides: overrides,
|
|
2450
2766
|
});
|
|
2451
|
-
|
|
2767
|
+
if (workspaceScope() !== formScope) return;
|
|
2768
|
+
plan = planRes.body?.result ?? null;
|
|
2452
2769
|
if (!planRes.ok || plan?.eligible !== true || !plan?.planHash) {
|
|
2453
2770
|
setStatus(
|
|
2454
2771
|
planRes.body?.error?.message ??
|
|
@@ -2459,53 +2776,69 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2459
2776
|
return;
|
|
2460
2777
|
}
|
|
2461
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
|
+
|
|
2462
2799
|
const est = plan.estimatedExecution;
|
|
2463
2800
|
const estText = est
|
|
2464
|
-
? `(将执行 ${est.nodeCount ?? "?"}
|
|
2801
|
+
? `(将执行 ${est.nodeCount ?? "?"} 个节点,其中 ${est.piCallCount ?? "?"} 个需要 AI 处理)`
|
|
2465
2802
|
: "";
|
|
2466
|
-
setStatus(
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
.
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
runId: dagRunId,
|
|
2474
|
-
fromNode: nodeId,
|
|
2475
|
-
planHash: plan.planHash,
|
|
2476
|
-
reason,
|
|
2477
|
-
promptOverrides: overrides,
|
|
2478
|
-
},
|
|
2479
|
-
clientRequestId,
|
|
2480
|
-
);
|
|
2803
|
+
setStatus(`检查通过${estText},正在创建新运行…`);
|
|
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;
|
|
2481
2810
|
const operationId = runRes.body?.operationId;
|
|
2482
2811
|
if (!runRes.ok || !operationId) {
|
|
2483
2812
|
setStatus(runRes.body?.error?.message ?? "提交续跑失败。", true);
|
|
2484
2813
|
return;
|
|
2485
2814
|
}
|
|
2486
2815
|
|
|
2816
|
+
submitted = true;
|
|
2817
|
+
draft.submission.operationId = operationId;
|
|
2818
|
+
trackInspectorOperation(dagRunId, nodeId, operationId);
|
|
2487
2819
|
const op = await pollOperationToTerminal(operationId, (text) =>
|
|
2488
|
-
setStatus(text),
|
|
2820
|
+
setStatus(text), formScope,
|
|
2489
2821
|
);
|
|
2822
|
+
if (workspaceScope() !== formScope) return;
|
|
2490
2823
|
if (op?.state === "succeeded") {
|
|
2491
2824
|
const newRunId =
|
|
2492
2825
|
op.dagRunId ?? op.result?.newRunId ?? op.runSummary?.runId ?? null;
|
|
2493
2826
|
if (newRunId) {
|
|
2494
|
-
setStatus(
|
|
2827
|
+
setStatus(`重启完成,新运行:${newRunId}`);
|
|
2495
2828
|
const open = document.createElement("button");
|
|
2496
2829
|
open.type = "button";
|
|
2497
2830
|
open.className = "prompt-restart-submit is-secondary";
|
|
2498
|
-
open.textContent = "
|
|
2831
|
+
open.textContent = "打开新运行";
|
|
2499
2832
|
open.addEventListener("click", () => {
|
|
2500
2833
|
navigate(`/dag/${encodeURIComponent(String(newRunId))}`);
|
|
2501
2834
|
});
|
|
2502
2835
|
row.appendChild(open);
|
|
2503
2836
|
} else {
|
|
2504
|
-
setStatus("
|
|
2837
|
+
setStatus("重启完成(暂时无法取得新运行编号,请在 DAG 历史中查看)。");
|
|
2505
2838
|
}
|
|
2506
2839
|
} else if (op) {
|
|
2507
2840
|
setStatus(
|
|
2508
|
-
|
|
2841
|
+
`新运行最终状态:${inspectorValueLabel("status", op.state)}${op.errorMessage ? ` — ${op.errorMessage}` : ""}`,
|
|
2509
2842
|
true,
|
|
2510
2843
|
);
|
|
2511
2844
|
} else {
|
|
@@ -2514,14 +2847,73 @@ async function renderPromptRestart(content, dagRunId, node) {
|
|
|
2514
2847
|
} catch (err) {
|
|
2515
2848
|
setStatus(err instanceof Error ? err.message : String(err), true);
|
|
2516
2849
|
} finally {
|
|
2517
|
-
|
|
2850
|
+
draft.submission.inFlight = false;
|
|
2851
|
+
submit.disabled = submitted;
|
|
2852
|
+
start.disabled = submitted || !prepared || hasUnresolvedSubmission(draft.submission);
|
|
2853
|
+
syncPromptRetry();
|
|
2518
2854
|
}
|
|
2519
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(); }
|
|
2520
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);
|
|
2521
2913
|
}
|
|
2522
2914
|
|
|
2523
2915
|
/** Poll a Console operation until a terminal state; null on poll budget end. */
|
|
2524
|
-
async function pollOperationToTerminal(operationId, onProgress) {
|
|
2916
|
+
async function pollOperationToTerminal(operationId, onProgress, operationScope = workspaceScope()) {
|
|
2525
2917
|
const terminal = new Set([
|
|
2526
2918
|
"succeeded",
|
|
2527
2919
|
"failed",
|
|
@@ -2529,9 +2921,12 @@ async function pollOperationToTerminal(operationId, onProgress) {
|
|
|
2529
2921
|
"needs-reconcile",
|
|
2530
2922
|
]);
|
|
2531
2923
|
for (let attempt = 0; attempt < 1200; attempt += 1) {
|
|
2924
|
+
if (workspaceScope() !== operationScope) return null;
|
|
2532
2925
|
const res = await fetchOperatorOperation(operationId);
|
|
2926
|
+
if (workspaceScope() !== operationScope) return null;
|
|
2533
2927
|
const op = res.body?.operation ?? res.body;
|
|
2534
2928
|
const state = op?.state ?? "";
|
|
2929
|
+
if (res.ok && inspectorOperations.has(operationId)) { Object.assign(inspectorOperations.get(operationId), { operation: op }); refreshInspectorOperations(); }
|
|
2535
2930
|
if (res.ok && terminal.has(state)) {
|
|
2536
2931
|
return op;
|
|
2537
2932
|
}
|
|
@@ -2552,8 +2947,9 @@ function transparencyMetaList(rows) {
|
|
|
2552
2947
|
|
|
2553
2948
|
function renderNodeOverview(content, dagRunId, node) {
|
|
2554
2949
|
const root = el("div", "node-transparency-root");
|
|
2950
|
+
root.appendChild(buildInspectorPageLead("overview"));
|
|
2555
2951
|
root.appendChild(
|
|
2556
|
-
el("p", "node-input-semantics", "
|
|
2952
|
+
el("p", "node-input-semantics", "这里展示系统为当前运行保存的真实记录;字段已转换为易懂名称,悬停可查看原始技术字段。"),
|
|
2557
2953
|
);
|
|
2558
2954
|
const summary = el("section", "node-input-section");
|
|
2559
2955
|
summary.appendChild(el("h4", null, "节点事实"));
|
|
@@ -2571,12 +2967,12 @@ function renderNodeOverview(content, dagRunId, node) {
|
|
|
2571
2967
|
);
|
|
2572
2968
|
root.appendChild(summary);
|
|
2573
2969
|
const semantics = el("section", "node-input-section");
|
|
2574
|
-
semantics.appendChild(el("h4", null, "
|
|
2970
|
+
semantics.appendChild(el("h4", null, "这些信息分别在什么时候产生"));
|
|
2575
2971
|
semantics.appendChild(
|
|
2576
2972
|
el(
|
|
2577
2973
|
"p",
|
|
2578
2974
|
"muted",
|
|
2579
|
-
"
|
|
2975
|
+
"“执行设置”是运行开始前保存的配置;“实际上下文”是下发任务时真正交给节点的内容;“产出”是执行结束后保存的文件。缺少哪个阶段会明确显示,预览内容不会被当成已经发生的事实。",
|
|
2580
2976
|
),
|
|
2581
2977
|
);
|
|
2582
2978
|
root.appendChild(semantics);
|
|
@@ -2591,26 +2987,28 @@ function selectedAttemptNumber(dagRunId, node) {
|
|
|
2591
2987
|
}
|
|
2592
2988
|
|
|
2593
2989
|
async function renderEffectiveContext(content, dagRunId, node) {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
)
|
|
2990
|
+
const requestToken = Symbol("context-request");
|
|
2991
|
+
content._contextRequestToken = requestToken;
|
|
2992
|
+
const requestedExecutionKey = effectiveExecutionKey(dagRunId, node);
|
|
2993
|
+
const requestedScope = workspaceScope();
|
|
2994
|
+
ensureInspectorPageLead(content, "context");
|
|
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));
|
|
2600
3002
|
if (
|
|
2601
|
-
!content.isConnected ||
|
|
3003
|
+
!content.isConnected || content._contextRequestToken !== requestToken || workspaceScope() !== requestedScope ||
|
|
2602
3004
|
content.dataset.dagRunId !== String(dagRunId) ||
|
|
2603
3005
|
content.dataset.dagNodeId !== String(node.nodeId) ||
|
|
2604
|
-
activeInspectorTab() !== "context"
|
|
2605
|
-
)
|
|
2606
|
-
return;
|
|
3006
|
+
activeInspectorTab() !== "context" || requestedExecutionKey !== effectiveExecutionKey(dagRunId, node)
|
|
3007
|
+
) return;
|
|
2607
3008
|
clearNode(content);
|
|
3009
|
+
ensureInspectorPageLead(content, "context");
|
|
2608
3010
|
if (!result.ok || !result.body) {
|
|
2609
|
-
const error = el(
|
|
2610
|
-
"p",
|
|
2611
|
-
"node-input-error",
|
|
2612
|
-
result.body?.error || "无法加载有效上下文事实。",
|
|
2613
|
-
);
|
|
3011
|
+
const error = el("p", "node-input-error", result.body?.error || "无法读取这次尝试的上下文。");
|
|
2614
3012
|
error.setAttribute("role", "alert");
|
|
2615
3013
|
content.appendChild(error);
|
|
2616
3014
|
return;
|
|
@@ -2618,103 +3016,186 @@ async function renderEffectiveContext(content, dagRunId, node) {
|
|
|
2618
3016
|
const data = result.body;
|
|
2619
3017
|
const receipt = data.effectiveContext;
|
|
2620
3018
|
const root = el("div", "node-transparency-root");
|
|
2621
|
-
root.appendChild(
|
|
2622
|
-
transparencyMetaList([
|
|
2623
|
-
["attempt", data.attempt],
|
|
2624
|
-
["declared", data.declared?.status],
|
|
2625
|
-
["dispatched", data.dispatched?.status],
|
|
2626
|
-
["node status", data.dispatched?.nodeStatus],
|
|
2627
|
-
]),
|
|
2628
|
-
);
|
|
3019
|
+
root.appendChild(el("p", "context-record-heading", `第 ${data.attempt} 次尝试 · ${receipt?.status === "captured" ? "已保存上下文" : "上下文不可用"}`));
|
|
2629
3020
|
if (receipt?.status !== "captured") {
|
|
2630
3021
|
const missing = el("div", "node-transparency-omission");
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
);
|
|
2638
|
-
missing.appendChild(
|
|
2639
|
-
el("code", null, receipt?.reasonCode ?? "CONTEXT_RECEIPT_MISSING"),
|
|
2640
|
-
);
|
|
2641
|
-
const missingLabel = contextTransferReasonLabel(
|
|
2642
|
-
receipt?.reasonCode ?? "CONTEXT_RECEIPT_MISSING",
|
|
2643
|
-
);
|
|
2644
|
-
if (missingLabel) {
|
|
2645
|
-
missing.appendChild(el("p", "muted", missingLabel));
|
|
2646
|
-
}
|
|
2647
|
-
missing.appendChild(
|
|
2648
|
-
el(
|
|
2649
|
-
"p",
|
|
2650
|
-
"muted",
|
|
2651
|
-
"当前只有 frozen input / prompt 指纹;它们不是 Effective Context。历史 run 不做事后回填。",
|
|
2652
|
-
),
|
|
2653
|
-
);
|
|
3022
|
+
const reasonCode = receipt?.reasonCode ?? "CONTEXT_RECEIPT_MISSING";
|
|
3023
|
+
const missingLabel = contextTransferReasonLabel(reasonCode);
|
|
3024
|
+
missing.appendChild(el("h4", null, receipt?.status === "invalid" ? "记录未通过完整性检查" : "没有保存这次尝试的上下文"));
|
|
3025
|
+
const reason = el("p", "muted", missingLabel ?? reasonCode);
|
|
3026
|
+
if (missingLabel) reason.setAttribute("title", `技术代码:${reasonCode}`);
|
|
3027
|
+
missing.appendChild(reason);
|
|
3028
|
+
missing.appendChild(el("p", "muted", "无法确认当时实际收到的材料。你可以在“执行设置”查看运行前的配置;较早的运行不会事后补写上下文记录。"));
|
|
2654
3029
|
root.appendChild(missing);
|
|
2655
3030
|
} else {
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
"
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
)
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
(
|
|
2683
|
-
|
|
2684
|
-
|
|
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
|
+
}
|
|
2685
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);
|
|
2686
3091
|
}
|
|
2687
3092
|
content.appendChild(root);
|
|
2688
3093
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "context");
|
|
2689
3094
|
}
|
|
2690
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
|
+
|
|
2691
3164
|
function artifactInventorySection(data, dagRunId, nodeId) {
|
|
2692
3165
|
const root = el("section", "node-input-section node-artifact-inventory");
|
|
2693
|
-
root.appendChild(el("h4", null, "
|
|
3166
|
+
root.appendChild(el("h4", null, "本节点生成和使用的文件"));
|
|
3167
|
+
root.appendChild(el("p", "muted", "文件关系属于整个节点;下方执行输出随尝试切换。"));
|
|
2694
3168
|
if (!data?.binding?.available) {
|
|
2695
3169
|
const warning = el("div", "node-transparency-omission");
|
|
2696
3170
|
const bindingCode =
|
|
2697
3171
|
data?.binding?.reasonCode ?? "ARTIFACT_BINDING_UNAVAILABLE";
|
|
2698
|
-
warning.appendChild(el("code", null, bindingCode));
|
|
2699
3172
|
const bindingLabel = contextTransferReasonLabel(bindingCode);
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3173
|
+
const reason = el("p", "muted", bindingLabel ?? bindingCode);
|
|
3174
|
+
if (bindingLabel) reason.setAttribute("title", `技术代码:${bindingCode}`);
|
|
3175
|
+
warning.appendChild(reason);
|
|
2703
3176
|
warning.appendChild(
|
|
2704
3177
|
el(
|
|
2705
3178
|
"p",
|
|
2706
3179
|
"muted",
|
|
2707
|
-
"
|
|
3180
|
+
"这个较早的运行没有保存明确的文件使用关系;系统不会根据提示词、输出要求或文件名猜测谁会使用它。",
|
|
2708
3181
|
),
|
|
2709
3182
|
);
|
|
2710
3183
|
root.appendChild(warning);
|
|
2711
3184
|
return root;
|
|
2712
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;
|
|
2713
3194
|
root.appendChild(
|
|
2714
3195
|
transparencyMetaList([
|
|
2715
3196
|
["origin", data.origin?.kind ?? "unknown"],
|
|
2716
3197
|
["parent", data.origin?.parentRunId ?? null],
|
|
2717
|
-
["graph sha256",
|
|
3198
|
+
["graph sha256", graphShaCell],
|
|
2718
3199
|
]),
|
|
2719
3200
|
);
|
|
2720
3201
|
for (const artifact of data.produces ?? []) {
|
|
@@ -2740,11 +3221,7 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2740
3221
|
],
|
|
2741
3222
|
]),
|
|
2742
3223
|
);
|
|
2743
|
-
|
|
2744
|
-
card,
|
|
2745
|
-
"Direct consumers",
|
|
2746
|
-
artifact.directConsumerNodeIds ?? [],
|
|
2747
|
-
);
|
|
3224
|
+
appendNodeLinks(card, "会直接使用此文件的节点", dagRunId, artifact.directConsumerNodeIds ?? []);
|
|
2748
3225
|
if (artifact.materialization?.status === "materialized") {
|
|
2749
3226
|
card.appendChild(
|
|
2750
3227
|
createArtifactContentPreview(dagRunId, nodeId, () => artifact, {
|
|
@@ -2756,10 +3233,10 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2756
3233
|
}
|
|
2757
3234
|
for (const artifact of data.consumes ?? []) {
|
|
2758
3235
|
const card = el("div", "node-artifact-card is-consumed");
|
|
2759
|
-
card.appendChild(el("h5", null,
|
|
3236
|
+
card.appendChild(el("h5", null, `使用 ${artifact.artifactId}`));
|
|
2760
3237
|
card.appendChild(
|
|
2761
3238
|
transparencyMetaList([
|
|
2762
|
-
["producer", artifact.producerNodeId],
|
|
3239
|
+
["producer", inspectorNodeLink(dagRunId, artifact.producerNodeId)],
|
|
2763
3240
|
["path", artifact.path],
|
|
2764
3241
|
["required", artifact.required ? "yes" : "no"],
|
|
2765
3242
|
]),
|
|
@@ -2775,7 +3252,7 @@ function artifactInventorySection(data, dagRunId, nodeId) {
|
|
|
2775
3252
|
(data.produces ?? []).length === 0 &&
|
|
2776
3253
|
(data.consumes ?? []).length === 0
|
|
2777
3254
|
) {
|
|
2778
|
-
root.appendChild(el("p", "empty", "
|
|
3255
|
+
root.appendChild(el("p", "empty", "该节点没有登记会生成或使用的文件。"));
|
|
2779
3256
|
}
|
|
2780
3257
|
return root;
|
|
2781
3258
|
}
|
|
@@ -2789,6 +3266,23 @@ async function prependArtifactInventory(content, dagRunId, node) {
|
|
|
2789
3266
|
activeInspectorTab() !== "output"
|
|
2790
3267
|
)
|
|
2791
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
|
+
}
|
|
2792
3286
|
const section =
|
|
2793
3287
|
result.ok && result.body
|
|
2794
3288
|
? artifactInventorySection(result.body, dagRunId, node.nodeId)
|
|
@@ -2797,10 +3291,12 @@ async function prependArtifactInventory(content, dagRunId, node) {
|
|
|
2797
3291
|
"node-input-error node-artifact-inventory",
|
|
2798
3292
|
result.body?.error || "无法加载产物绑定事实。",
|
|
2799
3293
|
);
|
|
2800
|
-
content.
|
|
3294
|
+
const lead = content.querySelector(".inspector-page-lead");
|
|
3295
|
+
content.insertBefore(section, lead?.nextSibling ?? content.firstChild);
|
|
2801
3296
|
}
|
|
2802
3297
|
|
|
2803
3298
|
async function renderRunLineage(content, dagRunId, node) {
|
|
3299
|
+
ensureInspectorPageLead(content, "lineage");
|
|
2804
3300
|
content.appendChild(el("p", "muted", "正在加载来源与影响关系…"));
|
|
2805
3301
|
const result = await fetchDagRunLineage(dagRunId);
|
|
2806
3302
|
if (
|
|
@@ -2811,6 +3307,7 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2811
3307
|
)
|
|
2812
3308
|
return;
|
|
2813
3309
|
clearNode(content);
|
|
3310
|
+
ensureInspectorPageLead(content, "lineage");
|
|
2814
3311
|
if (!result.ok || !result.body) {
|
|
2815
3312
|
content.appendChild(
|
|
2816
3313
|
el("p", "node-input-error", result.body?.error || "无法加载来源与影响关系。"),
|
|
@@ -2819,28 +3316,18 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2819
3316
|
}
|
|
2820
3317
|
const data = result.body;
|
|
2821
3318
|
const root = el("div", "node-transparency-root");
|
|
2822
|
-
const intro = el("section", "node-lineage-intro");
|
|
2823
|
-
intro.append(
|
|
2824
|
-
el("h4", null, "这个节点从哪里来,会影响哪里"),
|
|
2825
|
-
el(
|
|
2826
|
-
"p",
|
|
2827
|
-
"muted",
|
|
2828
|
-
"这里展示派生运行继承、替换或重新执行了哪些节点。产出文件的直接使用者请到“产出”中查看。",
|
|
2829
|
-
),
|
|
2830
|
-
);
|
|
2831
|
-
root.appendChild(intro);
|
|
2832
3319
|
root.appendChild(
|
|
2833
3320
|
transparencyMetaList([
|
|
2834
3321
|
["运行类型", data.kind === "root" ? "首次运行" : "派生运行"],
|
|
2835
|
-
["来源运行", data.parentRunId
|
|
2836
|
-
["发起方式", data.trigger
|
|
3322
|
+
["来源运行", data.parentRunId ? inspectorNodeLink(data.parentRunId, data.operatorSelectedNodeId || node.nodeId, data.parentRunId) : null],
|
|
3323
|
+
["发起方式", inspectorValueLabel("发起方式", data.trigger)],
|
|
2837
3324
|
["人工选择节点", data.operatorSelectedNodeId ?? null],
|
|
2838
3325
|
["从此节点重新执行", data.effectiveFromNodeId ?? null],
|
|
2839
3326
|
["重跑计划哈希", data.planHash ?? null],
|
|
2840
3327
|
]),
|
|
2841
3328
|
);
|
|
2842
|
-
|
|
2843
|
-
|
|
3329
|
+
appendNodeLinks(root, "沿用既有结果的节点", dagRunId, data.importedNodeIds ?? []);
|
|
3330
|
+
appendNodeLinks(root, "替换结果的节点", dagRunId, data.overriddenNodeIds ?? []);
|
|
2844
3331
|
if (data.kind === "root")
|
|
2845
3332
|
root.appendChild(
|
|
2846
3333
|
el("p", "muted", "这是首次运行,没有来源运行。"),
|
|
@@ -2849,19 +3336,41 @@ async function renderRunLineage(content, dagRunId, node) {
|
|
|
2849
3336
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "lineage");
|
|
2850
3337
|
}
|
|
2851
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
|
+
|
|
2852
3356
|
function activeInspectorTab() {
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
"timeline"
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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;
|
|
2865
3374
|
}
|
|
2866
3375
|
|
|
2867
3376
|
function saveInspectorContentScroll(
|
|
@@ -2872,7 +3381,14 @@ function saveInspectorContentScroll(
|
|
|
2872
3381
|
options = {},
|
|
2873
3382
|
) {
|
|
2874
3383
|
if (!content) return null;
|
|
2875
|
-
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
|
+
}
|
|
2876
3392
|
const saved = saveDagInspectorViewportState(
|
|
2877
3393
|
regionKey,
|
|
2878
3394
|
content.scrollLeft,
|
|
@@ -2908,7 +3424,8 @@ function saveInspectorContentScroll(
|
|
|
2908
3424
|
|
|
2909
3425
|
function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
2910
3426
|
if (!content) return;
|
|
2911
|
-
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);
|
|
2912
3429
|
const apply = () => {
|
|
2913
3430
|
if (
|
|
2914
3431
|
!content.isConnected ||
|
|
@@ -2918,19 +3435,14 @@ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
|
2918
3435
|
) {
|
|
2919
3436
|
return;
|
|
2920
3437
|
}
|
|
2921
|
-
if (tab === "timeline"
|
|
3438
|
+
if ((tab === "timeline" || tab === "output") && content._readingState?.followLatest) {
|
|
2922
3439
|
content.scrollLeft = uiState.dagTimelineViewportState?.scrollLeft ?? 0;
|
|
2923
3440
|
content.scrollTop = content.scrollHeight;
|
|
3441
|
+
content._lastRestoredTop = content.scrollTop;
|
|
3442
|
+
content._syncTimelineFollow?.();
|
|
2924
3443
|
return;
|
|
2925
3444
|
}
|
|
2926
|
-
|
|
2927
|
-
const saved = uiState.dagNodeOutputViewportState;
|
|
2928
|
-
if (saved?.regionKey === regionKey && saved.followLatest) {
|
|
2929
|
-
content.scrollTop = content.scrollHeight;
|
|
2930
|
-
content.scrollLeft = saved.scrollLeft ?? 0;
|
|
2931
|
-
return;
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
3445
|
+
|
|
2934
3446
|
const saved =
|
|
2935
3447
|
getDagInspectorViewportState(regionKey) ??
|
|
2936
3448
|
(tab === "input" &&
|
|
@@ -2945,8 +3457,10 @@ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
|
|
|
2945
3457
|
uiState.dagNodeSpecEvidenceViewportState?.regionKey === regionKey
|
|
2946
3458
|
? uiState.dagNodeSpecEvidenceViewportState
|
|
2947
3459
|
: null);
|
|
2948
|
-
content.scrollLeft = saved?.scrollLeft ?? 0;
|
|
2949
|
-
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?.();
|
|
2950
3464
|
};
|
|
2951
3465
|
apply();
|
|
2952
3466
|
if (typeof requestAnimationFrame === "function") {
|
|
@@ -2962,7 +3476,13 @@ function ensureInspectorContentScrollCapture(content, dagRunId, nodeId, tab) {
|
|
|
2962
3476
|
() => {
|
|
2963
3477
|
const runId = content.dataset.dagRunId;
|
|
2964
3478
|
const nId = content.dataset.dagNodeId;
|
|
2965
|
-
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?.();
|
|
2966
3486
|
if (!runId || !nId || !active) return;
|
|
2967
3487
|
saveInspectorContentScroll(content, runId, nId, active, {
|
|
2968
3488
|
// Clearing a tall input pane for its loading shell can emit a native
|
|
@@ -3070,6 +3590,21 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3070
3590
|
|
|
3071
3591
|
const bar = el("div", "dag-inspector-attempt-bar");
|
|
3072
3592
|
bar.appendChild(el("label", "dag-inspector-attempt-label", "执行分次"));
|
|
3593
|
+
const choose = (nextKey, source = "select") => {
|
|
3594
|
+
uiState.dagInspectorAttempt = { dagRunId, nodeId: node.nodeId, key: String(nextKey) };
|
|
3595
|
+
inspectorNodeSelections.set(`${dagRunId}:${node.nodeId}`, uiState.dagInspectorAttempt);
|
|
3596
|
+
renderDagInspectorForCurrent();
|
|
3597
|
+
const nextBar = document.querySelector(".dag-inspector-header .dag-inspector-attempt-bar");
|
|
3598
|
+
const control = nextBar?.querySelector(`[data-attempt-action="${source}"]`);
|
|
3599
|
+
(control && !control.disabled ? control : nextBar?.querySelector(".custom-select-trigger"))?.focus({ preventScroll: true });
|
|
3600
|
+
};
|
|
3601
|
+
const selectedIndex = options.findIndex((option) => option.key === selectedKey);
|
|
3602
|
+
const previous = iconButton("上一次尝试", "ri-arrow-left-s-line");
|
|
3603
|
+
previous.type = "button";
|
|
3604
|
+
previous.dataset.attemptAction = "previous";
|
|
3605
|
+
previous.disabled = selectedIndex <= 0;
|
|
3606
|
+
previous.addEventListener("click", () => choose(options[selectedIndex - 1].key, "previous"));
|
|
3607
|
+
bar.appendChild(previous);
|
|
3073
3608
|
const select = createCustomSelect({
|
|
3074
3609
|
ariaLabel: "选择执行分次",
|
|
3075
3610
|
className: "dag-inspector-attempt-select",
|
|
@@ -3078,25 +3613,25 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3078
3613
|
value: option.key,
|
|
3079
3614
|
label: option.label,
|
|
3080
3615
|
})),
|
|
3081
|
-
onChange: (nextKey) =>
|
|
3082
|
-
uiState.dagInspectorAttempt = {
|
|
3083
|
-
dagRunId,
|
|
3084
|
-
nodeId: node.nodeId,
|
|
3085
|
-
key: String(nextKey),
|
|
3086
|
-
};
|
|
3087
|
-
renderDagInspectorForCurrent();
|
|
3088
|
-
},
|
|
3616
|
+
onChange: (nextKey) => choose(nextKey),
|
|
3089
3617
|
});
|
|
3090
3618
|
// Keep raw key on the root for existing integration assertions that read
|
|
3091
3619
|
// `.value` after assign + change (StubNode handler contract).
|
|
3092
3620
|
select.value = selectedKey;
|
|
3093
3621
|
bar.appendChild(select);
|
|
3622
|
+
const next = iconButton("下一次尝试", "ri-arrow-right-s-line");
|
|
3623
|
+
next.type = "button";
|
|
3624
|
+
next.dataset.attemptAction = "next";
|
|
3625
|
+
next.disabled = selectedIndex >= options.length - 1;
|
|
3626
|
+
next.addEventListener("click", () => choose(options[selectedIndex + 1].key, "next"));
|
|
3627
|
+
bar.appendChild(next);
|
|
3094
3628
|
|
|
3095
3629
|
const selected =
|
|
3096
3630
|
options.find((option) => option.key === selectedKey) ??
|
|
3097
3631
|
options[options.length - 1];
|
|
3098
3632
|
const data = selected.preview ?? {};
|
|
3099
3633
|
const preview = el("div", "dag-inspector-attempt-preview");
|
|
3634
|
+
preview.setAttribute("title", [data.failureCategory ? attemptFailureLabel(data.failureCategory) : "", data.durationMs != null ? `耗时 ${formatMs(data.durationMs)}` : ""].filter(Boolean).join(" · "));
|
|
3100
3635
|
const status = attemptPreviewStatus(data.status);
|
|
3101
3636
|
if (status) {
|
|
3102
3637
|
preview.appendChild(el("span", `badge badge-${status.cls}`, status.label));
|
|
@@ -3125,6 +3660,19 @@ function buildAttemptBar(dagRunId, node) {
|
|
|
3125
3660
|
// Historical bodies load via execution-output endpoint; do not surface
|
|
3126
3661
|
// raw artifact paths as the primary path for full text (AC-003/004).
|
|
3127
3662
|
bar.appendChild(preview);
|
|
3663
|
+
const diagnostic = iconButton("诊断摘要", "ri-file-info-line"); diagnostic.type = "button";
|
|
3664
|
+
diagnostic.addEventListener("click", async () => {
|
|
3665
|
+
diagnostic.disabled = true;
|
|
3666
|
+
try {
|
|
3667
|
+
const record = await recordedAttempt(dagRunId, node, selectedKey);
|
|
3668
|
+
const panel = document.querySelector(".dag-inspector-content");
|
|
3669
|
+
if (!panel || panel.dataset.dagNodeId !== node.nodeId || panel.dataset.executionKey !== selectedKey) return;
|
|
3670
|
+
panel.querySelector(".diagnostic-preview")?.remove();
|
|
3671
|
+
const detail = el("details", "diagnostic-preview"); detail.open = true; detail.appendChild(el("summary", null, "诊断摘要预览(仅当前尝试)"));
|
|
3672
|
+
const text = attemptDiagnostic(dagRunId, node, record); detail.appendChild(el("pre", "process-timeline-code", text));
|
|
3673
|
+
const copy = el("button", "btn btn-secondary", "复制摘要"); copy.type = "button"; copy.addEventListener("click", async () => { copy.textContent = await copyText(text) ? "已复制" : "复制失败"; }); detail.appendChild(copy); panel.insertBefore(detail, panel.firstChild); panel.scrollTop = 0; if (panel._readingState) panel._readingState.followLatest = false;
|
|
3674
|
+
} finally { diagnostic.disabled = false; }
|
|
3675
|
+
}); bar.appendChild(diagnostic);
|
|
3128
3676
|
return { bar, selectedKey, latestKey, options };
|
|
3129
3677
|
}
|
|
3130
3678
|
|
|
@@ -3194,11 +3742,22 @@ function composeExecutionErrorText(payload) {
|
|
|
3194
3742
|
return typeof payload.stderr === "string" ? payload.stderr : "";
|
|
3195
3743
|
}
|
|
3196
3744
|
|
|
3745
|
+
function buildExecutionErrorSection(errorText) {
|
|
3746
|
+
const error = el("section", "markdown-error");
|
|
3747
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3748
|
+
error.appendChild(renderInspectorDocument(errorText));
|
|
3749
|
+
return error;
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3197
3752
|
function appendOutputBody(content, outputText, errorText, options = null) {
|
|
3198
3753
|
if (!outputText && !errorText) {
|
|
3199
3754
|
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
3200
3755
|
return;
|
|
3201
3756
|
}
|
|
3757
|
+
// 失败节点错误优先:先看错误,再对照正文(正文可能很长)。
|
|
3758
|
+
if (options?.priorityError === true && errorText) {
|
|
3759
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3760
|
+
}
|
|
3202
3761
|
if (outputText) content.appendChild(renderInspectorDocument(outputText));
|
|
3203
3762
|
// Truncation note only for full/cached execution-output bodies (AC-003);
|
|
3204
3763
|
// preview path never invents truncated/maxBytes.
|
|
@@ -3212,15 +3771,13 @@ function appendOutputBody(content, outputText, errorText, options = null) {
|
|
|
3212
3771
|
),
|
|
3213
3772
|
);
|
|
3214
3773
|
}
|
|
3215
|
-
if (errorText) {
|
|
3216
|
-
|
|
3217
|
-
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
3218
|
-
error.appendChild(renderInspectorDocument(errorText));
|
|
3219
|
-
content.appendChild(error);
|
|
3774
|
+
if (errorText && options?.priorityError !== true) {
|
|
3775
|
+
content.appendChild(buildExecutionErrorSection(errorText));
|
|
3220
3776
|
}
|
|
3221
3777
|
}
|
|
3222
3778
|
|
|
3223
3779
|
function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
3780
|
+
const priorityError = isNodeFailed(node?.status);
|
|
3224
3781
|
const cached = key
|
|
3225
3782
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, key)
|
|
3226
3783
|
: null;
|
|
@@ -3232,6 +3789,7 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3232
3789
|
{
|
|
3233
3790
|
truncated: cached.body.truncated === true,
|
|
3234
3791
|
maxBytes: cached.body.maxBytes,
|
|
3792
|
+
priorityError,
|
|
3235
3793
|
},
|
|
3236
3794
|
);
|
|
3237
3795
|
return "cached";
|
|
@@ -3241,12 +3799,13 @@ function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
|
3241
3799
|
content,
|
|
3242
3800
|
node.outputPreview ?? "",
|
|
3243
3801
|
node.errorPreview ?? "",
|
|
3802
|
+
{ priorityError },
|
|
3244
3803
|
);
|
|
3245
3804
|
return "preview";
|
|
3246
3805
|
}
|
|
3247
3806
|
if (cached && cached.ok === false) {
|
|
3248
3807
|
const message =
|
|
3249
|
-
cached.error || "
|
|
3808
|
+
cached.error || "该次执行输出暂不可用(记录文件缺失或无法读取)。";
|
|
3250
3809
|
const error = el("p", "empty", message);
|
|
3251
3810
|
error.setAttribute("role", "status");
|
|
3252
3811
|
content.appendChild(error);
|
|
@@ -3290,7 +3849,7 @@ async function loadAndRenderExecutionOutput(
|
|
|
3290
3849
|
(result.status === 0
|
|
3291
3850
|
? "加载执行输出失败(网络错误)。"
|
|
3292
3851
|
: result.status === 404
|
|
3293
|
-
? "
|
|
3852
|
+
? "该次执行输出暂不可用(记录文件缺失或无法读取)。"
|
|
3294
3853
|
: `加载执行输出失败(HTTP ${result.status})。`);
|
|
3295
3854
|
// Cache the miss for this key only — never wipe other valid bodies.
|
|
3296
3855
|
setDagNodeExecutionOutputCache(dagRunId, nodeId, key, {
|
|
@@ -3328,7 +3887,8 @@ function repaintOutputTabBody(content, dagRunId, node) {
|
|
|
3328
3887
|
// Remove everything after the attempt bar (or all if no bar).
|
|
3329
3888
|
const inventory = content.querySelector(".node-artifact-inventory");
|
|
3330
3889
|
const bar = content.querySelector(".dag-inspector-attempt-bar");
|
|
3331
|
-
const
|
|
3890
|
+
const lead = content.querySelector(".inspector-page-lead");
|
|
3891
|
+
const keep = [lead, inventory, bar].filter(Boolean);
|
|
3332
3892
|
while (content.firstChild) content.removeChild(content.firstChild);
|
|
3333
3893
|
for (const nodeEl of keep) content.appendChild(nodeEl);
|
|
3334
3894
|
|
|
@@ -3394,7 +3954,7 @@ function renderProcessCommandCard(command) {
|
|
|
3394
3954
|
details.appendChild(summary);
|
|
3395
3955
|
const body = el("div", "process-timeline-card-body");
|
|
3396
3956
|
const meta = [
|
|
3397
|
-
command.exitCode != null ?
|
|
3957
|
+
command.exitCode != null ? `退出码 ${command.exitCode}` : null,
|
|
3398
3958
|
command.durationMs != null ? `耗时 ${formatMs(command.durationMs)}` : null,
|
|
3399
3959
|
command.failureCategory ? command.failureCategory : null,
|
|
3400
3960
|
].filter(Boolean);
|
|
@@ -3404,11 +3964,11 @@ function renderProcessCommandCard(command) {
|
|
|
3404
3964
|
);
|
|
3405
3965
|
}
|
|
3406
3966
|
if (command.stdout) {
|
|
3407
|
-
body.appendChild(el("div", "process-timeline-section-label", "
|
|
3967
|
+
body.appendChild(el("div", "process-timeline-section-label", "正常输出"));
|
|
3408
3968
|
appendProcessPre(body, command.stdout);
|
|
3409
3969
|
}
|
|
3410
3970
|
if (command.stderr) {
|
|
3411
|
-
body.appendChild(el("div", "process-timeline-section-label", "
|
|
3971
|
+
body.appendChild(el("div", "process-timeline-section-label", "错误输出"));
|
|
3412
3972
|
appendProcessPre(body, command.stderr);
|
|
3413
3973
|
}
|
|
3414
3974
|
if (command.truncated) {
|
|
@@ -3505,39 +4065,7 @@ function renderNonPiProcess(content, body) {
|
|
|
3505
4065
|
ul.appendChild(li);
|
|
3506
4066
|
}
|
|
3507
4067
|
content.appendChild(ul);
|
|
3508
|
-
|
|
3509
|
-
}
|
|
3510
|
-
|
|
3511
|
-
function downloadTextFile(filename, text) {
|
|
3512
|
-
const blob = new Blob([text], { type: "text/html;charset=utf-8" });
|
|
3513
|
-
const url = URL.createObjectURL(blob);
|
|
3514
|
-
const a = document.createElement("a");
|
|
3515
|
-
a.href = url;
|
|
3516
|
-
a.download = filename;
|
|
3517
|
-
document.body.appendChild(a);
|
|
3518
|
-
a.click();
|
|
3519
|
-
a.remove();
|
|
3520
|
-
URL.revokeObjectURL(url);
|
|
3521
|
-
}
|
|
3522
|
-
|
|
3523
|
-
function appendProcessExportButton(content, body, meta) {
|
|
3524
|
-
if (!body || ((body.commands ?? []).length === 0 && (body.steps ?? []).length === 0)) return;
|
|
3525
|
-
const toolbar = el("div", "process-timeline-export-bar");
|
|
3526
|
-
const btn = el("button", "btn btn-secondary", "导出 HTML");
|
|
3527
|
-
btn.type = "button";
|
|
3528
|
-
btn.addEventListener("click", async () => {
|
|
3529
|
-
const cssText = await fetchPageStylesheetText();
|
|
3530
|
-
// 复用页面真实渲染 DOM,保证导出排版与页面一致。
|
|
3531
|
-
const clone = content.cloneNode(true);
|
|
3532
|
-
const bar = clone.querySelector(".process-timeline-export-bar");
|
|
3533
|
-
if (bar) bar.remove();
|
|
3534
|
-
downloadTextFile(
|
|
3535
|
-
`node-process-${meta?.nodeId || "node"}-${Date.now()}.html`,
|
|
3536
|
-
buildPageStyledDoc(clone.innerHTML, meta ?? {}, cssText),
|
|
3537
|
-
);
|
|
3538
|
-
});
|
|
3539
|
-
toolbar.appendChild(btn);
|
|
3540
|
-
content.insertBefore(toolbar, content.firstChild);
|
|
4068
|
+
content.appendChild(createTimelineAutoScrollControl(content));
|
|
3541
4069
|
}
|
|
3542
4070
|
|
|
3543
4071
|
export async function mergeNodeProcess(dagRunId, node) {
|
|
@@ -3588,6 +4116,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3588
4116
|
if (sameNode && cached.kind === "pi-session") {
|
|
3589
4117
|
renderSessionTimeline(content, node.nodeId, {
|
|
3590
4118
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4119
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3591
4120
|
});
|
|
3592
4121
|
return;
|
|
3593
4122
|
}
|
|
@@ -3598,6 +4127,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3598
4127
|
if ((node.executor ?? "pi") === "pi") {
|
|
3599
4128
|
renderSessionTimeline(content, node.nodeId, {
|
|
3600
4129
|
executionKey: effectiveExecutionKey(dagRunId, node),
|
|
4130
|
+
attemptCount: node.executionSummary?.totalAttemptCount,
|
|
3601
4131
|
});
|
|
3602
4132
|
return;
|
|
3603
4133
|
}
|
|
@@ -3619,7 +4149,7 @@ function renderExecutorAwareTimeline(content, dagRunId, node) {
|
|
|
3619
4149
|
});
|
|
3620
4150
|
}
|
|
3621
4151
|
|
|
3622
|
-
function fillDagInspectorContent(content, dagRunId, node) {
|
|
4152
|
+
function fillDagInspectorContent(content, dagRunId, node, run) {
|
|
3623
4153
|
const previousRenderedNode = content.dataset.dagNodeId ?? "";
|
|
3624
4154
|
const previousRenderedRun = content.dataset.dagRunId ?? "";
|
|
3625
4155
|
const previousTab = content.dataset.inspectorTab ?? "";
|
|
@@ -3655,27 +4185,37 @@ function fillDagInspectorContent(content, dagRunId, node) {
|
|
|
3655
4185
|
content.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3656
4186
|
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
3657
4187
|
content.dataset.inspectorTab = activeTab;
|
|
4188
|
+
content.dataset.executionKey = inspectorTabExecutionKey(dagRunId, node, activeTab) ?? "";
|
|
4189
|
+
content._readingState = inspectorReadingState(dagRunId, node.nodeId, activeTab, content.dataset.executionKey, isLatestExecutionSelection(dagRunId, node) && ["running", "pending"].includes(String(node.status ?? "").toLowerCase()));
|
|
4190
|
+
uiState.dagTimelineAutoScrollEnabled = content._readingState.followLatest;
|
|
4191
|
+
uiState.dagTimelineViewportState = { ...content._readingState, dagRunId, nodeId: node.nodeId };
|
|
3658
4192
|
// Drop timeline-only classes when switching tabs so output layout stays clean.
|
|
3659
4193
|
content.className = "dag-inspector-content";
|
|
4194
|
+
if (activeTab !== "prompt-restart") {
|
|
4195
|
+
content.classList.add("is-readable-page");
|
|
4196
|
+
}
|
|
3660
4197
|
ensureInspectorContentScrollCapture(
|
|
3661
4198
|
content,
|
|
3662
4199
|
dagRunId,
|
|
3663
4200
|
node.nodeId,
|
|
3664
4201
|
activeTab,
|
|
3665
4202
|
);
|
|
3666
|
-
if (activeTab === "
|
|
4203
|
+
if (activeTab === "compare") {
|
|
4204
|
+
void renderAttemptComparison(content, dagRunId, node);
|
|
4205
|
+
} else if (activeTab === "overview") {
|
|
3667
4206
|
renderNodeOverview(content, dagRunId, node);
|
|
3668
4207
|
} else if (activeTab === "context") {
|
|
3669
4208
|
void renderEffectiveContext(content, dagRunId, node);
|
|
3670
4209
|
} else if (activeTab === "lineage") {
|
|
3671
4210
|
void renderRunLineage(content, dagRunId, node);
|
|
3672
4211
|
} else if (activeTab === "spec-evidence") {
|
|
4212
|
+
ensureInspectorPageLead(content, "spec-evidence");
|
|
3673
4213
|
void renderSpecEvidence(content, dagRunId, node.nodeId);
|
|
3674
4214
|
} else if (activeTab === "prompt-restart") {
|
|
3675
|
-
void renderPromptRestart(content, dagRunId, node);
|
|
4215
|
+
void renderPromptRestart(content, dagRunId, node, run);
|
|
3676
4216
|
} else if (activeTab === "timeline") {
|
|
3677
|
-
|
|
3678
|
-
|
|
4217
|
+
ensureInspectorPageLead(content, "timeline");
|
|
4218
|
+
|
|
3679
4219
|
renderExecutorAwareTimeline(content, dagRunId, node);
|
|
3680
4220
|
// Initial content is rendered before it is attached. Restore again on the
|
|
3681
4221
|
// connected animation frame so live-follow sees the final scroll height.
|
|
@@ -3684,8 +4224,8 @@ function fillDagInspectorContent(content, dagRunId, node) {
|
|
|
3684
4224
|
void renderDagNodeInput(content, dagRunId, node);
|
|
3685
4225
|
} else {
|
|
3686
4226
|
// output tab: attempt bar + latest preview immediate / historical async body.
|
|
3687
|
-
|
|
3688
|
-
|
|
4227
|
+
ensureInspectorPageLead(content, "output");
|
|
4228
|
+
|
|
3689
4229
|
const key = effectiveExecutionKey(dagRunId, node);
|
|
3690
4230
|
const latest = isLatestExecutionSelection(dagRunId, node);
|
|
3691
4231
|
const rendered = renderCachedOrPreviewBody(
|
|
@@ -3719,46 +4259,49 @@ function inspectorNodePurpose(node) {
|
|
|
3719
4259
|
return describeDagNodePurpose(node?.nodeId, node);
|
|
3720
4260
|
}
|
|
3721
4261
|
|
|
4262
|
+
/** 纯函数:Tab 条两端是否有被裁切内容(驱动渐隐提示)。 */
|
|
4263
|
+
export function inspectorTabOverflowState({ scrollLeft = 0, clientWidth = 0, scrollWidth = 0 } = {}) {
|
|
4264
|
+
const maxScroll = Math.max(0, scrollWidth - clientWidth);
|
|
4265
|
+
return {
|
|
4266
|
+
start: scrollLeft > 1 && maxScroll > 0,
|
|
4267
|
+
end: scrollLeft < maxScroll - 1 && maxScroll > 0,
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
function syncInspectorTabOverflowClasses(tabs) {
|
|
4272
|
+
if (!tabs?.classList) return;
|
|
4273
|
+
const state = inspectorTabOverflowState({
|
|
4274
|
+
scrollLeft: tabs.scrollLeft ?? 0,
|
|
4275
|
+
clientWidth: tabs.clientWidth ?? 0,
|
|
4276
|
+
scrollWidth: tabs.scrollWidth ?? 0,
|
|
4277
|
+
});
|
|
4278
|
+
tabs.classList.toggle("is-overflow-start", state.start);
|
|
4279
|
+
tabs.classList.toggle("is-overflow-end", state.end);
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
/** 选中 Tab 保持可见:只滚动 Tab 条自身,不牵动外层页面。 */
|
|
4283
|
+
function revealInspectorTab(tabs, button) {
|
|
4284
|
+
if (!tabs || !button) return;
|
|
4285
|
+
const left = button.offsetLeft;
|
|
4286
|
+
const right = left + (button.offsetWidth ?? 0);
|
|
4287
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return;
|
|
4288
|
+
if (left < tabs.scrollLeft + 8) tabs.scrollLeft = Math.max(0, left - 8);
|
|
4289
|
+
else if (right > tabs.scrollLeft + tabs.clientWidth - 8) {
|
|
4290
|
+
tabs.scrollLeft = right - tabs.clientWidth + 8;
|
|
4291
|
+
}
|
|
4292
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
3722
4295
|
function appendInspectorPurpose(title, node) {
|
|
3723
4296
|
const purpose = inspectorNodePurpose(node);
|
|
3724
4297
|
if (!purpose) return;
|
|
3725
4298
|
title.appendChild(el("p", "dag-inspector-purpose", purpose));
|
|
3726
4299
|
}
|
|
3727
4300
|
|
|
3728
|
-
function
|
|
3729
|
-
const
|
|
3730
|
-
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
3731
|
-
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3732
|
-
const title = el("div", "dag-inspector-title");
|
|
3733
|
-
title.appendChild(el("h3", null, "节点检查器"));
|
|
3734
|
-
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
3735
|
-
appendInspectorPurpose(title, node);
|
|
3736
|
-
header.appendChild(title);
|
|
3737
|
-
const controls = el("div", "dag-inspector-controls");
|
|
3738
|
-
const status = badge(node.status ?? "unknown");
|
|
3739
|
-
status.classList.add("dag-inspector-status");
|
|
3740
|
-
controls.appendChild(status);
|
|
3741
|
-
const close = document.createElement("button");
|
|
3742
|
-
close.type = "button";
|
|
3743
|
-
close.className = "dag-inspector-close";
|
|
3744
|
-
close.setAttribute("aria-label", "关闭节点检查器");
|
|
3745
|
-
const closeIcon = el("i", "ri-close-line");
|
|
3746
|
-
closeIcon.setAttribute("aria-hidden", "true");
|
|
3747
|
-
close.appendChild(closeIcon);
|
|
3748
|
-
close.addEventListener("click", closeDagInspector);
|
|
3749
|
-
controls.appendChild(close);
|
|
3750
|
-
header.appendChild(controls);
|
|
3751
|
-
|
|
3752
|
-
const tabDefs = DAG_INSPECTOR_READ_ONLY_TABS.map((definition) => [
|
|
3753
|
-
...definition,
|
|
3754
|
-
]);
|
|
3755
|
-
// Export/rerun mutation surface: the server injects a SCOPED inspect token.
|
|
3756
|
-
// The emergency kill switch suppresses the token;
|
|
3757
|
-
// no token → no tab → the inspector stays strictly read-only.
|
|
3758
|
-
if (globalThis.__INSPECT_MUTATION_TOKEN__) {
|
|
3759
|
-
tabDefs.push([...DAG_INSPECTOR_MUTATION_TAB]);
|
|
3760
|
-
}
|
|
4301
|
+
function buildDagInspectorTabs(node) {
|
|
4302
|
+
const tabDefs = inspectorTabDefinitions(node);
|
|
3761
4303
|
const tabs = el("div", "dag-inspector-tabs");
|
|
4304
|
+
tabs.dataset.tabsSignature = tabDefs.map(([tab]) => tab).join(",");
|
|
3762
4305
|
tabs.setAttribute("role", "tablist");
|
|
3763
4306
|
tabs.setAttribute("aria-label", "节点检查器面板");
|
|
3764
4307
|
for (const [tab, label, icon] of tabDefs) {
|
|
@@ -3781,6 +4324,10 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3781
4324
|
renderDagInspectorForCurrent();
|
|
3782
4325
|
const next = document.getElementById(`dag-inspector-tab-${tab}`);
|
|
3783
4326
|
next?.focus();
|
|
4327
|
+
revealInspectorTab(
|
|
4328
|
+
document.querySelector(".dag-inspector-tabs"),
|
|
4329
|
+
next,
|
|
4330
|
+
);
|
|
3784
4331
|
});
|
|
3785
4332
|
button.addEventListener("keydown", (event) => {
|
|
3786
4333
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
@@ -3793,17 +4340,106 @@ function buildDagInspectorHeader(dagRunId, node) {
|
|
|
3793
4340
|
const nextTab = order[(idx + delta + order.length) % order.length];
|
|
3794
4341
|
uiState.dagInspectorTab = nextTab;
|
|
3795
4342
|
renderDagInspectorForCurrent();
|
|
3796
|
-
document.getElementById(`dag-inspector-tab-${nextTab}`)
|
|
4343
|
+
const next = document.getElementById(`dag-inspector-tab-${nextTab}`);
|
|
4344
|
+
next?.focus();
|
|
4345
|
+
revealInspectorTab(document.querySelector(".dag-inspector-tabs"), next);
|
|
3797
4346
|
});
|
|
3798
4347
|
tabs.appendChild(button);
|
|
3799
4348
|
}
|
|
4349
|
+
// 溢出渐隐与选中可见性:挂载后测量(构建时 offsetLeft 不可用)。
|
|
4350
|
+
tabs.addEventListener("scroll", () => syncInspectorTabOverflowClasses(tabs), { passive: true });
|
|
4351
|
+
if (typeof ResizeObserver === "function") {
|
|
4352
|
+
const overflowObserver = new ResizeObserver(() => {
|
|
4353
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4354
|
+
});
|
|
4355
|
+
overflowObserver.observe(tabs);
|
|
4356
|
+
}
|
|
4357
|
+
if (typeof requestAnimationFrame === "function") {
|
|
4358
|
+
requestAnimationFrame(() => {
|
|
4359
|
+
if (!tabs.isConnected) return;
|
|
4360
|
+
syncInspectorTabOverflowClasses(tabs);
|
|
4361
|
+
const selected = tabs.querySelector("[role='tab'][aria-selected='true']");
|
|
4362
|
+
revealInspectorTab(tabs, selected);
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
return tabs;
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
function buildDagInspectorHeader(dagRunId, node) {
|
|
4369
|
+
const header = el("div", "dag-inspector-header");
|
|
4370
|
+
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
4371
|
+
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
4372
|
+
const title = el("div", "dag-inspector-title");
|
|
4373
|
+
title.appendChild(el("h3", null, "节点检查器"));
|
|
4374
|
+
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
4375
|
+
appendInspectorPurpose(title, node);
|
|
4376
|
+
header.appendChild(title);
|
|
4377
|
+
const controls = el("div", "dag-inspector-controls");
|
|
4378
|
+
const status = badge(node.status ?? "unknown");
|
|
4379
|
+
status.classList.add("dag-inspector-status");
|
|
4380
|
+
controls.appendChild(status);
|
|
4381
|
+
const expand = iconButton(uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4382
|
+
expand.type = "button";
|
|
4383
|
+
expand.setAttribute("aria-pressed", String(Boolean(uiState.dagInspectorExpanded)));
|
|
4384
|
+
expand.addEventListener("click", () => {
|
|
4385
|
+
uiState.dagInspectorExpanded = !uiState.dagInspectorExpanded;
|
|
4386
|
+
document.getElementById("dag-inspector")?.classList.toggle("is-expanded", uiState.dagInspectorExpanded);
|
|
4387
|
+
setIconButton(expand, uiState.dagInspectorExpanded ? "还原宽度" : "展开阅读", uiState.dagInspectorExpanded ? "ri-contract-left-right-line" : "ri-expand-left-right-line");
|
|
4388
|
+
expand.setAttribute("aria-pressed", String(uiState.dagInspectorExpanded));
|
|
4389
|
+
});
|
|
4390
|
+
controls.appendChild(expand);
|
|
4391
|
+
const share = iconButton("复制位置", "ri-link");
|
|
4392
|
+
share.type = "button";
|
|
4393
|
+
share.addEventListener("click", async () => {
|
|
4394
|
+
const selection = inspectorSelection;
|
|
4395
|
+
const query = new URLSearchParams({ node: selection.node.nodeId, tab: activeInspectorTab() });
|
|
4396
|
+
const key = effectiveExecutionKey(selection.dagRunId, selection.node);
|
|
4397
|
+
if (key) query.set("attempt", key);
|
|
4398
|
+
const href = inspectPathToCanonicalHref(`/dag/${encodeURIComponent(selection.dagRunId)}?${query}`);
|
|
4399
|
+
const copied = await copyText(new URL(href, location.href).href);
|
|
4400
|
+
setIconButton(share, copied ? "已复制位置" : "复制失败", copied ? "ri-check-line" : "ri-error-warning-line");
|
|
4401
|
+
});
|
|
4402
|
+
controls.appendChild(share);
|
|
4403
|
+
const close = document.createElement("button");
|
|
4404
|
+
close.type = "button";
|
|
4405
|
+
close.className = "dag-inspector-close";
|
|
4406
|
+
close.setAttribute("aria-label", "关闭节点检查器");
|
|
4407
|
+
const closeIcon = el("i", "ri-close-line");
|
|
4408
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
4409
|
+
close.appendChild(closeIcon);
|
|
4410
|
+
close.addEventListener("click", closeDagInspector);
|
|
4411
|
+
controls.appendChild(close);
|
|
4412
|
+
header.appendChild(controls);
|
|
4413
|
+
|
|
4414
|
+
const tabs = buildDagInspectorTabs(node);
|
|
3800
4415
|
header.appendChild(tabs);
|
|
4416
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4417
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4418
|
+
header.dataset.attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
3801
4419
|
return header;
|
|
3802
4420
|
}
|
|
3803
4421
|
|
|
3804
4422
|
function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
3805
4423
|
const header = panel.querySelector(".dag-inspector-header");
|
|
3806
4424
|
if (!header) return;
|
|
4425
|
+
const tabs = header.querySelector(".dag-inspector-tabs");
|
|
4426
|
+
const tabsSignature = inspectorTabDefinitions(node).map(([tab]) => tab).join(",");
|
|
4427
|
+
if (tabs && tabs.dataset.tabsSignature !== tabsSignature) {
|
|
4428
|
+
const restoreFocus = document.activeElement?.getAttribute("role") === "tab" && tabs.contains(document.activeElement);
|
|
4429
|
+
const nextTabs = buildDagInspectorTabs(node);
|
|
4430
|
+
const scrollLeft = tabs.scrollLeft;
|
|
4431
|
+
tabs.replaceWith(nextTabs);
|
|
4432
|
+
nextTabs.scrollLeft = scrollLeft;
|
|
4433
|
+
syncInspectorTabOverflowClasses(nextTabs);
|
|
4434
|
+
if (restoreFocus) document.getElementById(`dag-inspector-tab-${activeInspectorTab()}`)?.focus();
|
|
4435
|
+
}
|
|
4436
|
+
const attemptSignature = JSON.stringify([activeInspectorTab(), effectiveExecutionKey(dagRunId, node), node.executionSummary, node.passes]);
|
|
4437
|
+
if (header.dataset.dagNodeId !== String(node.nodeId) || header.dataset.attemptSignature !== attemptSignature) {
|
|
4438
|
+
header.querySelector(".dag-inspector-attempt-bar")?.remove();
|
|
4439
|
+
const attempt = inspectorTabUsesAttempt(activeInspectorTab(), node) ? buildAttemptBar(dagRunId, node) : null;
|
|
4440
|
+
if (attempt) header.appendChild(attempt.bar);
|
|
4441
|
+
header.dataset.attemptSignature = attemptSignature;
|
|
4442
|
+
}
|
|
3807
4443
|
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
3808
4444
|
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
3809
4445
|
const nodeLabel = header.querySelector(".dag-inspector-node");
|
|
@@ -3833,7 +4469,7 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
|
3833
4469
|
|
|
3834
4470
|
function dagInspectorSignature(dagRunId, node) {
|
|
3835
4471
|
const tab = activeInspectorTab();
|
|
3836
|
-
const execKey =
|
|
4472
|
+
const execKey = inspectorTabExecutionKey(dagRunId, node, tab);
|
|
3837
4473
|
const cachedBody = execKey
|
|
3838
4474
|
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, execKey)
|
|
3839
4475
|
: null;
|
|
@@ -3870,11 +4506,11 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
3870
4506
|
// the DOM while selection/history are unchanged; switching attempt or a
|
|
3871
4507
|
// changed attempt count rebuilds the selector.
|
|
3872
4508
|
const summary = node.executionSummary;
|
|
3873
|
-
const attemptRevision = [
|
|
4509
|
+
const attemptRevision = tab === "compare" ? attemptSelectorOptions(node) : inspectorTabUsesAttempt(tab, node) ? [
|
|
3874
4510
|
summary?.totalAttemptCount ?? 0,
|
|
3875
4511
|
(node.passes ?? []).join(","),
|
|
3876
4512
|
execKey ?? "latest",
|
|
3877
|
-
];
|
|
4513
|
+
] : null;
|
|
3878
4514
|
return JSON.stringify([
|
|
3879
4515
|
dagRunId,
|
|
3880
4516
|
node.nodeId,
|
|
@@ -3884,14 +4520,17 @@ function dagInspectorSignature(dagRunId, node) {
|
|
|
3884
4520
|
]);
|
|
3885
4521
|
}
|
|
3886
4522
|
|
|
3887
|
-
export function renderDagInspector(dagRunId, node, initial = true) {
|
|
4523
|
+
export function renderDagInspector(dagRunId, node, initial = true, run = null) {
|
|
3888
4524
|
// Record the current selection before any early-return so tab handlers
|
|
3889
4525
|
// (which reference the mutable inspectorSelection slot) always re-render
|
|
3890
4526
|
// the currently selected node, never a stale build-time closure.
|
|
3891
4527
|
inspectorSelection.dagRunId = dagRunId;
|
|
3892
4528
|
inspectorSelection.node = node;
|
|
4529
|
+
inspectorSelection.run = run;
|
|
3893
4530
|
const panel = document.getElementById("dag-inspector");
|
|
3894
4531
|
if (!panel) return;
|
|
4532
|
+
// 运行状态标记:回到底部按钮由滚动位置自行显隐(终态同样可用),此标记仅供诊断。
|
|
4533
|
+
panel.dataset.runEnded = run && !isDagRunActive(run) ? "1" : "0";
|
|
3895
4534
|
captureDagTimelineViewportState(panel);
|
|
3896
4535
|
if (!uiState.dagInspectorOpen || !node) {
|
|
3897
4536
|
panel.classList.remove("is-open");
|
|
@@ -3899,6 +4538,8 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
3899
4538
|
return;
|
|
3900
4539
|
}
|
|
3901
4540
|
|
|
4541
|
+
uiState.dagInspectorTab = activeInspectorTab();
|
|
4542
|
+
|
|
3902
4543
|
// Defer tearing down the inspector shell while the user is actively
|
|
3903
4544
|
// dragging the resize handle (or the graph) so the pointer-capture target
|
|
3904
4545
|
// is not destroyed mid-gesture; the next poll refreshes the content.
|
|
@@ -3918,6 +4559,7 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
3918
4559
|
if (panel.dataset.contentSignature === nextContentSignature) return;
|
|
3919
4560
|
const previousNode = existingContent.dataset.dagNodeId ?? "";
|
|
3920
4561
|
const previousRun = existingContent.dataset.dagRunId ?? "";
|
|
4562
|
+
const previousExecutionKey = existingContent.dataset.executionKey;
|
|
3921
4563
|
const previousTab =
|
|
3922
4564
|
existingContent.dataset.inspectorTab ?? activeInspectorTab();
|
|
3923
4565
|
const nodeChanged =
|
|
@@ -3939,7 +4581,7 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
3939
4581
|
scrollTop: pre.scrollTop,
|
|
3940
4582
|
}));
|
|
3941
4583
|
clearNode(existingContent);
|
|
3942
|
-
fillDagInspectorContent(existingContent, dagRunId, node);
|
|
4584
|
+
fillDagInspectorContent(existingContent, dagRunId, node, run);
|
|
3943
4585
|
// Async tabs restore themselves after fetch; sync tabs restore here.
|
|
3944
4586
|
if (activeTab === "output" || activeTab === "timeline") {
|
|
3945
4587
|
restoreInspectorContentScroll(
|
|
@@ -3949,7 +4591,7 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
3949
4591
|
activeTab,
|
|
3950
4592
|
);
|
|
3951
4593
|
}
|
|
3952
|
-
if (!nodeChanged && previousTab === activeTab) {
|
|
4594
|
+
if (!nodeChanged && previousTab === activeTab && previousExecutionKey === existingContent.dataset.executionKey) {
|
|
3953
4595
|
for (const [index, pre] of [
|
|
3954
4596
|
...existingContent.querySelectorAll("pre"),
|
|
3955
4597
|
].entries()) {
|
|
@@ -3966,11 +4608,12 @@ export function renderDagInspector(dagRunId, node, initial = true) {
|
|
|
3966
4608
|
clearNode(panel);
|
|
3967
4609
|
panel.classList.add("is-open");
|
|
3968
4610
|
panel.setAttribute("aria-hidden", "false");
|
|
4611
|
+
panel.classList.toggle("is-expanded", Boolean(uiState.dagInspectorExpanded));
|
|
3969
4612
|
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
3970
4613
|
panel.appendChild(buildDagInspectorHeader(dagRunId, node));
|
|
3971
4614
|
|
|
3972
4615
|
const content = el("div", "dag-inspector-content");
|
|
3973
|
-
fillDagInspectorContent(content, dagRunId, node);
|
|
4616
|
+
fillDagInspectorContent(content, dagRunId, node, run);
|
|
3974
4617
|
panel.appendChild(content);
|
|
3975
4618
|
panel.dataset.contentSignature = nextContentSignature;
|
|
3976
4619
|
}
|