@tea-agent/loop-agent 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -2
- package/CHANGELOG.md +91 -24
- package/README.md +84 -12
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +38 -2
- package/dist/application/dag/run-dag.js +11 -27
- package/dist/application/dag/validate-dag.js +13 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +44 -16
- package/dist/cli/program.js +40 -23
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +547 -95
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/commands/plan.js +50 -0
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/checks.js +6 -3
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +24 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +488 -0
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +29 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +32 -3
- package/dist/worker/delivery/final-verification.js +47 -11
- package/dist/worker/delivery/package.js +63 -10
- package/dist/worker/feature/run.js +60 -8
- package/dist/worker/loop-agent/loop-agent-client.js +329 -126
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +338 -83
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +165 -21
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/static/api.js +27 -0
- package/dist/worker/observe/static/app.js +120 -2317
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +128 -0
- package/dist/worker/observe/static/router.js +85 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1720 -495
- package/dist/worker/observe/static/views/batch.js +226 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +477 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +442 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +453 -0
- package/dist/worker/observe/static/views/pool.js +347 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +260 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/run-task/run-task.js +22 -12
- package/dist/worker/runner/run-ready.js +76 -12
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +797 -27
- package/dist/workflows/dag/node-execution.js +183 -35
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +77 -17
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/dist/workflows/dag/skill-snapshot.js +529 -0
- package/dist/workflows/dag/types.js +86 -10
- package/dist/workflows/dag/validate.js +73 -12
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +36 -33
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/agent-dag-runner.md +28 -3
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +140 -0
- package/docs/architecture/evolution.md +53 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +45 -17
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +43 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +1 -3
- package/docs/exec-plans/completed/README.md +23 -0
- package/docs/feature-workflow.md +78 -4
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +60 -25
- package/docs/loop-agent-harness.md +28 -4
- package/docs/progress/README.md +50 -1
- package/docs/reports/README.md +90 -18
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +23 -6
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +70 -3
- package/docs/templates/agent-dag.supervised-implementation.json +9 -8
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +276 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +10 -12
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/project-start-checklist.md +2 -2
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +10 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +7 -15
- package/package.json +22 -46
- package/scripts/check-product-line-docs.sh +10 -7
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +51 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +14 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +36 -20
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
|
@@ -1,2322 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1
|
+
/** Observe UI bootstrap: route wiring, listeners, compatibility re-exports. */
|
|
2
|
+
import {
|
|
3
|
+
parseHashRoute,
|
|
4
|
+
parseHashRouteWithFilters,
|
|
5
|
+
buildHashPath,
|
|
6
|
+
parseRoute,
|
|
7
|
+
} from "./router.js";
|
|
8
|
+
import { parseArtifactPreviewResponse } from "./api.js";
|
|
9
|
+
import {
|
|
10
|
+
objectRouteLink,
|
|
11
|
+
findFeatureInSnapshot,
|
|
12
|
+
featureLookupState,
|
|
13
|
+
buildObjectRelations,
|
|
14
|
+
renderObjectRelationBar,
|
|
15
|
+
} from "./relations.js";
|
|
16
|
+
import { copyText, renderRecommendedCommand } from "./copy.js";
|
|
17
|
+
import {
|
|
18
|
+
isPageVisible,
|
|
19
|
+
detailPollDelay,
|
|
20
|
+
updateDagGraphViewportState,
|
|
21
|
+
captureDagGraphViewportState,
|
|
22
|
+
updateDagTimelineViewportState,
|
|
23
|
+
computeFollowLatest,
|
|
24
|
+
updateDagOutputViewportState,
|
|
25
|
+
shouldReuseDagDetailShell,
|
|
26
|
+
shouldPreserveDiagnosticsOpen,
|
|
27
|
+
isInteractiveGestureActive,
|
|
28
|
+
stopTimers,
|
|
29
|
+
renderViewState,
|
|
30
|
+
mountViewState,
|
|
31
|
+
viewStateDescriptor,
|
|
32
|
+
} from "./state.js";
|
|
33
|
+
import { sortPoolTasks, poolStatusCounts } from "./format-pool.js";
|
|
34
|
+
import {
|
|
35
|
+
parseMarkdownBlocks,
|
|
36
|
+
markdownLinkHref,
|
|
37
|
+
badgeClass,
|
|
38
|
+
formatMs,
|
|
39
|
+
displayDurationMs,
|
|
40
|
+
} from "./format.js";
|
|
41
|
+
import {
|
|
42
|
+
dagMetaVisibility,
|
|
43
|
+
isDagRunActive,
|
|
44
|
+
dashboardVisibleFeatures,
|
|
45
|
+
findDagForRun,
|
|
46
|
+
} from "./dag-model.js";
|
|
47
|
+
|
|
48
|
+
import { renderFailures } from "./views/failures.js";
|
|
49
|
+
import { startPoolPolling } from "./views/pool.js";
|
|
50
|
+
import { startDashboardPolling } from "./views/dashboard.js";
|
|
51
|
+
import { renderFeatureDetail } from "./views/feature.js";
|
|
52
|
+
import { renderBatch } from "./views/batch.js";
|
|
53
|
+
import { startTaskPolling } from "./views/task.js";
|
|
54
|
+
import { startRunPolling } from "./views/run.js";
|
|
55
|
+
import { startDagPolling } from "./views/dag.js";
|
|
56
|
+
import { renderDagGraph } from "./views/dag-graph.js";
|
|
57
|
+
import {
|
|
58
|
+
selectDagNode,
|
|
59
|
+
closeDagInspector,
|
|
60
|
+
closeDagInspectorOnOutsideClick,
|
|
61
|
+
renderDagInspector,
|
|
62
|
+
} from "./views/dag-inspector.js";
|
|
63
|
+
import {
|
|
64
|
+
mergeSessionEvents,
|
|
65
|
+
renderSessionTimeline,
|
|
66
|
+
} from "./views/session-timeline.js";
|
|
67
|
+
|
|
68
|
+
// Compatibility re-exports for poll-stable UI helpers (owned by state.js / views):
|
|
69
|
+
// reuse-dag-detail-shell, reuse-dag-graph-wrap, existingGraphWrap?.parentNode === ranksEl,
|
|
70
|
+
// existingDetails?.parentNode === metaEl, let pre = existingPre, computeFollowLatest,
|
|
71
|
+
// updateDagOutputViewportState, isInteractiveGestureActive.
|
|
72
|
+
export {
|
|
73
|
+
detailPollDelay,
|
|
74
|
+
parseMarkdownBlocks,
|
|
75
|
+
markdownLinkHref,
|
|
76
|
+
updateDagGraphViewportState,
|
|
77
|
+
captureDagGraphViewportState,
|
|
78
|
+
updateDagTimelineViewportState,
|
|
79
|
+
computeFollowLatest,
|
|
80
|
+
updateDagOutputViewportState,
|
|
81
|
+
shouldReuseDagDetailShell,
|
|
82
|
+
shouldPreserveDiagnosticsOpen,
|
|
83
|
+
isInteractiveGestureActive,
|
|
84
|
+
parseHashRoute,
|
|
85
|
+
sortPoolTasks,
|
|
86
|
+
poolStatusCounts,
|
|
87
|
+
badgeClass,
|
|
88
|
+
formatMs,
|
|
89
|
+
dagMetaVisibility,
|
|
90
|
+
displayDurationMs,
|
|
91
|
+
objectRouteLink,
|
|
92
|
+
findFeatureInSnapshot,
|
|
93
|
+
featureLookupState,
|
|
94
|
+
buildObjectRelations,
|
|
95
|
+
renderObjectRelationBar,
|
|
96
|
+
copyText,
|
|
97
|
+
renderRecommendedCommand,
|
|
98
|
+
isDagRunActive,
|
|
99
|
+
dashboardVisibleFeatures,
|
|
100
|
+
findDagForRun,
|
|
101
|
+
parseArtifactPreviewResponse,
|
|
102
|
+
renderViewState,
|
|
103
|
+
mountViewState,
|
|
104
|
+
viewStateDescriptor,
|
|
105
|
+
parseHashRouteWithFilters,
|
|
106
|
+
buildHashPath,
|
|
107
|
+
renderDagGraph,
|
|
108
|
+
selectDagNode,
|
|
109
|
+
closeDagInspector,
|
|
110
|
+
renderDagInspector,
|
|
111
|
+
renderSessionTimeline,
|
|
112
|
+
mergeSessionEvents,
|
|
112
113
|
};
|
|
113
114
|
|
|
114
|
-
const STEP_ORDER = [
|
|
115
|
-
"preflight",
|
|
116
|
-
"materialize",
|
|
117
|
-
"dag-run-task",
|
|
118
|
-
"pi-model-override",
|
|
119
|
-
"dag-validate",
|
|
120
|
-
"run-dag",
|
|
121
|
-
"report-decision",
|
|
122
|
-
"dag-doctor",
|
|
123
|
-
"closeout-draft",
|
|
124
|
-
"promote-run",
|
|
125
|
-
"closeout-task",
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
const TERMINAL_RUN_STATUSES = new Set([
|
|
129
|
-
"finished",
|
|
130
|
-
"completed",
|
|
131
|
-
"succeeded",
|
|
132
|
-
"failed",
|
|
133
|
-
"partial_failed",
|
|
134
|
-
"error",
|
|
135
|
-
"cancelled",
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
const ARTIFACT_KEYS = [
|
|
139
|
-
"stdoutPath",
|
|
140
|
-
"stderrPath",
|
|
141
|
-
"resultPath",
|
|
142
|
-
"runRecordPath",
|
|
143
|
-
"reportMarkdown",
|
|
144
|
-
"doctorMarkdown",
|
|
145
|
-
"closeoutDraft",
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
let dashboardTimer = null;
|
|
149
|
-
let runPollTimer = null;
|
|
150
|
-
let dagPollTimer = null;
|
|
151
|
-
let runEventOffset = 0;
|
|
152
|
-
let runEvents = [];
|
|
153
|
-
let currentRunId = null;
|
|
154
|
-
let currentBatchRunId = null;
|
|
155
|
-
let currentDagRunId = null;
|
|
156
|
-
let selectedDagNodeId = null;
|
|
157
|
-
let sessionEventOffset = 0;
|
|
158
|
-
let sessionEvents = [];
|
|
159
|
-
let lastSnapshot = null;
|
|
160
|
-
let dagInspectorOpen = false;
|
|
161
|
-
let dagInspectorTab = "output";
|
|
162
|
-
let dagInspectorWidth = null;
|
|
163
|
-
let dagGraphViewportState = null;
|
|
164
|
-
const dagGraphViewportStates = new Map();
|
|
165
|
-
let dagTimelineViewportState = null;
|
|
166
|
-
let pollingGeneration = 0;
|
|
167
|
-
|
|
168
|
-
function isPageVisible() {
|
|
169
|
-
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function detailPollDelay(status, isVisible) {
|
|
173
|
-
const normalizedStatus = (status ?? "").toLowerCase().replace(/-/g, "_");
|
|
174
|
-
if (TERMINAL_RUN_STATUSES.has(normalizedStatus)) return null;
|
|
175
|
-
return isVisible ? ACTIVE_DETAIL_POLL_MS : HIDDEN_POLL_MS;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function dashboardPollDelay() {
|
|
179
|
-
return isPageVisible() ? DASHBOARD_POLL_MS : HIDDEN_POLL_MS;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function parseMarkdownBlocks(markdown) {
|
|
183
|
-
const lines = String(markdown ?? "").replace(/\r\n?/g, "\n").split("\n");
|
|
184
|
-
const blocks = [];
|
|
185
|
-
let paragraph = [];
|
|
186
|
-
let list = null;
|
|
187
|
-
let code = null;
|
|
188
|
-
|
|
189
|
-
const flushParagraph = () => {
|
|
190
|
-
const text = paragraph.join(" ").trim();
|
|
191
|
-
if (text) blocks.push({ type: "paragraph", text });
|
|
192
|
-
paragraph = [];
|
|
193
|
-
};
|
|
194
|
-
const flushList = () => {
|
|
195
|
-
if (list?.items.length) blocks.push(list);
|
|
196
|
-
list = null;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
200
|
-
const line = lines[index] ?? "";
|
|
201
|
-
if (code) {
|
|
202
|
-
if (/^```\s*$/.test(line)) {
|
|
203
|
-
blocks.push({ type: "code", language: code.language, text: code.lines.join("\n") });
|
|
204
|
-
code = null;
|
|
205
|
-
} else {
|
|
206
|
-
code.lines.push(line);
|
|
207
|
-
}
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const fence = line.match(/^```([^\s]*)\s*$/);
|
|
212
|
-
if (fence) {
|
|
213
|
-
flushParagraph();
|
|
214
|
-
flushList();
|
|
215
|
-
code = { language: fence[1] ?? "", lines: [] };
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (!line.trim()) {
|
|
219
|
-
flushParagraph();
|
|
220
|
-
flushList();
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
|
224
|
-
if (heading) {
|
|
225
|
-
flushParagraph();
|
|
226
|
-
flushList();
|
|
227
|
-
blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() });
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
|
|
231
|
-
flushParagraph();
|
|
232
|
-
flushList();
|
|
233
|
-
blocks.push({ type: "rule" });
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (line.startsWith(">")) {
|
|
237
|
-
flushParagraph();
|
|
238
|
-
flushList();
|
|
239
|
-
blocks.push({ type: "quote", text: line.replace(/^>\s?/, "") });
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
const unordered = line.match(/^[-*+]\s+(.+)$/);
|
|
243
|
-
const ordered = line.match(/^\d+[.)]\s+(.+)$/);
|
|
244
|
-
if (unordered || ordered) {
|
|
245
|
-
flushParagraph();
|
|
246
|
-
const orderedList = Boolean(ordered);
|
|
247
|
-
if (!list || list.ordered !== orderedList) {
|
|
248
|
-
flushList();
|
|
249
|
-
list = { type: "list", ordered: orderedList, items: [] };
|
|
250
|
-
}
|
|
251
|
-
list.items.push((ordered?.[1] ?? unordered?.[1] ?? "").trim());
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (line.includes("|") && /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1] ?? "")) {
|
|
255
|
-
flushParagraph();
|
|
256
|
-
flushList();
|
|
257
|
-
const parseCells = (row) => row.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
|
|
258
|
-
const headers = parseCells(line);
|
|
259
|
-
const rows = [];
|
|
260
|
-
index += 2;
|
|
261
|
-
while (index < lines.length && lines[index].includes("|")) {
|
|
262
|
-
rows.push(parseCells(lines[index]));
|
|
263
|
-
index += 1;
|
|
264
|
-
}
|
|
265
|
-
index -= 1;
|
|
266
|
-
blocks.push({ type: "table", headers, rows });
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
flushList();
|
|
270
|
-
paragraph.push(line.trim());
|
|
271
|
-
}
|
|
272
|
-
if (code) blocks.push({ type: "code", language: code.language, text: code.lines.join("\n") });
|
|
273
|
-
flushParagraph();
|
|
274
|
-
flushList();
|
|
275
|
-
return blocks;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function markdownLinkHref(value) {
|
|
279
|
-
const href = typeof value === "string" ? value.trim() : "";
|
|
280
|
-
if (!href) return null;
|
|
281
|
-
if (href.startsWith("#") || href.startsWith("/")) return href;
|
|
282
|
-
try {
|
|
283
|
-
const parsed = new URL(
|
|
284
|
-
href,
|
|
285
|
-
typeof location === "undefined" ? "http://localhost" : location.href,
|
|
286
|
-
);
|
|
287
|
-
return ["http:", "https:", "mailto:"].includes(parsed.protocol) ? href : null;
|
|
288
|
-
} catch {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function updateDagGraphViewportState(_previous, dagRunId, scrollLeft, scrollTop) {
|
|
294
|
-
return {
|
|
295
|
-
dagRunId,
|
|
296
|
-
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
297
|
-
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export function captureDagGraphViewportState(previous, dagRunId, viewport) {
|
|
302
|
-
if (!viewport || viewport.dataset?.dagRunId !== dagRunId) return previous;
|
|
303
|
-
return updateDagGraphViewportState(
|
|
304
|
-
previous,
|
|
305
|
-
dagRunId,
|
|
306
|
-
viewport.scrollLeft,
|
|
307
|
-
viewport.scrollTop,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function updateDagTimelineViewportState(
|
|
312
|
-
_previous,
|
|
313
|
-
dagRunId,
|
|
314
|
-
nodeId,
|
|
315
|
-
scrollLeft,
|
|
316
|
-
scrollTop,
|
|
317
|
-
followLatest,
|
|
318
|
-
) {
|
|
319
|
-
return {
|
|
320
|
-
dagRunId,
|
|
321
|
-
nodeId,
|
|
322
|
-
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
323
|
-
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
324
|
-
followLatest: Boolean(followLatest),
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function parseHashRoute(hashValue) {
|
|
329
|
-
const hash = hashValue.replace(/^#/, "") || "/";
|
|
330
|
-
const batchMatch = /^\/batch\/([^/]+)$/.exec(hash);
|
|
331
|
-
if (batchMatch) {
|
|
332
|
-
return { view: "batch", batchRunId: decodeURIComponent(batchMatch[1]) };
|
|
333
|
-
}
|
|
334
|
-
const runMatch = /^\/run\/([^/]+)$/.exec(hash);
|
|
335
|
-
if (runMatch) {
|
|
336
|
-
return { view: "run", workerRunId: decodeURIComponent(runMatch[1]) };
|
|
337
|
-
}
|
|
338
|
-
const dagMatch = /^\/dag\/([^/]+)$/.exec(hash);
|
|
339
|
-
if (dagMatch) {
|
|
340
|
-
return { view: "dag", dagRunId: decodeURIComponent(dagMatch[1]) };
|
|
341
|
-
}
|
|
342
|
-
if (hash === "/failures") {
|
|
343
|
-
return { view: "failures" };
|
|
344
|
-
}
|
|
345
|
-
if (hash === "/dags") {
|
|
346
|
-
return { view: "dashboard", scrollTo: "dags" };
|
|
347
|
-
}
|
|
348
|
-
return { view: "dashboard" };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function parseRoute() {
|
|
352
|
-
return parseHashRoute(location.hash);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function navigate(path) {
|
|
356
|
-
location.hash = path;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function fetchJson(url) {
|
|
360
|
-
try {
|
|
361
|
-
const res = await fetch(url);
|
|
362
|
-
if (!res.ok) return null;
|
|
363
|
-
return await res.json();
|
|
364
|
-
} catch {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function clearNode(node) {
|
|
370
|
-
while (node.firstChild) node.removeChild(node.firstChild);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function el(tag, className, text) {
|
|
374
|
-
const node = document.createElement(tag);
|
|
375
|
-
if (className) node.className = className;
|
|
376
|
-
if (text !== undefined) node.textContent = text;
|
|
377
|
-
return node;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function statusLabel(status) {
|
|
381
|
-
if (!status) return STATUS_LABELS.unknown;
|
|
382
|
-
const key = status.toLowerCase().replace(/-/g, "_");
|
|
383
|
-
if (STATUS_LABELS[key]) return STATUS_LABELS[key];
|
|
384
|
-
if (status === "FINISHED") return STATUS_LABELS.finished_upper;
|
|
385
|
-
if (status === "ERROR") return STATUS_LABELS.error_upper;
|
|
386
|
-
if (status === "SKIPPED") return STATUS_LABELS.skipped_upper;
|
|
387
|
-
return status;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function statusSymbol(status) {
|
|
391
|
-
const key = (status ?? "unknown").toLowerCase();
|
|
392
|
-
if (["finished", "completed", "succeeded", "done"].includes(key)) return "✓";
|
|
393
|
-
if (["failed", "error"].includes(key)) return "✕";
|
|
394
|
-
if (key === "skipped") return "↷";
|
|
395
|
-
if (["running", "started"].includes(key)) return "●";
|
|
396
|
-
if (["pending", "queued", "blocked"].includes(key)) return "○";
|
|
397
|
-
return "◇";
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export function badgeClass(status) {
|
|
401
|
-
if (!status) return "unknown";
|
|
402
|
-
const key = status.toLowerCase().replace(/_/g, "-");
|
|
403
|
-
if (["finished", "completed", "succeeded", "done", "superseded"].includes(key)) {
|
|
404
|
-
return "succeeded";
|
|
405
|
-
}
|
|
406
|
-
if (["error", "failed", "interrupted"].includes(key)) return "failed";
|
|
407
|
-
if (key === "partial-failed") return "partial-failed";
|
|
408
|
-
if (key === "skipped" || key === "abandoned") return "skipped";
|
|
409
|
-
if (key === "running" || key === "started" || key === "running-quiet") return "running";
|
|
410
|
-
if (key === "paused") return "blocked";
|
|
411
|
-
if (key === "remote-unknown") return "unknown";
|
|
412
|
-
return key;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function badge(status) {
|
|
416
|
-
const label = statusLabel(status);
|
|
417
|
-
const cls = badgeClass(status);
|
|
418
|
-
return el("span", `badge badge-${cls}`, label);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function formatTs(value) {
|
|
422
|
-
if (!value) return "—";
|
|
423
|
-
const d = new Date(value);
|
|
424
|
-
if (Number.isNaN(d.getTime())) return value;
|
|
425
|
-
return d.toLocaleString("zh-CN", { hour12: false });
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function formatMs(ms) {
|
|
429
|
-
if (ms === undefined || ms === null) return "—";
|
|
430
|
-
if (ms < 1000) return `${ms}ms`;
|
|
431
|
-
if (ms >= 60_000) {
|
|
432
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
433
|
-
return `${Math.floor(totalSeconds / 60)}min ${totalSeconds % 60}s`;
|
|
434
|
-
}
|
|
435
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function dagStatusBadge(status) {
|
|
439
|
-
const value = status ?? "unknown";
|
|
440
|
-
return el(
|
|
441
|
-
"span",
|
|
442
|
-
`badge badge-${badgeClass(value)}`,
|
|
443
|
-
DAG_EFFECTIVE_STATUS_LABELS[value] ?? statusLabel(value),
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function formatDuration(ms) {
|
|
448
|
-
if (ms === undefined || ms === null || Number.isNaN(ms)) return "—";
|
|
449
|
-
if (ms < 0) ms = 0;
|
|
450
|
-
const totalSec = Math.floor(ms / 1000);
|
|
451
|
-
const hours = Math.floor(totalSec / 3600);
|
|
452
|
-
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
453
|
-
const seconds = totalSec % 60;
|
|
454
|
-
if (hours > 0) return `${hours}小时 ${minutes}分 ${seconds}秒`;
|
|
455
|
-
if (minutes > 0) return `${minutes}分 ${seconds}秒`;
|
|
456
|
-
return `${seconds}秒`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function formatAgo(iso, nowMs) {
|
|
460
|
-
if (!iso) return "—";
|
|
461
|
-
const ts = Date.parse(iso);
|
|
462
|
-
if (Number.isNaN(ts)) return "—";
|
|
463
|
-
const base = nowMs ?? Date.now();
|
|
464
|
-
const deltaSec = Math.max(0, Math.floor((base - ts) / 1000));
|
|
465
|
-
if (deltaSec < 10) return "刚刚";
|
|
466
|
-
if (deltaSec < 60) return `${deltaSec} 秒前`;
|
|
467
|
-
if (deltaSec < 3600) return `${Math.floor(deltaSec / 60)} 分钟前`;
|
|
468
|
-
if (deltaSec < 86400) return `${Math.floor(deltaSec / 3600)} 小时前`;
|
|
469
|
-
return `${Math.floor(deltaSec / 86400)} 天前`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function livenessBadge(liveness) {
|
|
473
|
-
if (!liveness) return null;
|
|
474
|
-
return el("span", `badge liveness-${liveness}`, LIVENESS_LABELS[liveness] ?? liveness);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function showView(name) {
|
|
478
|
-
document.body.dataset.view = name;
|
|
479
|
-
if (name !== "dag") closeDagInspector();
|
|
480
|
-
document.querySelectorAll("[data-view]").forEach((section) => {
|
|
481
|
-
section.hidden = section.dataset.view !== name;
|
|
482
|
-
});
|
|
483
|
-
const activeHref = name === "failures" ? "#/failures" : "#/";
|
|
484
|
-
document.querySelectorAll(".nav-link").forEach((link) => {
|
|
485
|
-
link.classList.toggle("nav-link-active", link.getAttribute("href") === activeHref);
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function setBreadcrumb(parts) {
|
|
490
|
-
const nav = document.getElementById("breadcrumb");
|
|
491
|
-
nav.hidden = parts.length <= 1;
|
|
492
|
-
clearNode(nav);
|
|
493
|
-
parts.forEach((part, i) => {
|
|
494
|
-
if (i > 0) nav.appendChild(document.createTextNode(" / "));
|
|
495
|
-
if (part.href) {
|
|
496
|
-
const a = document.createElement("a");
|
|
497
|
-
a.href = part.href;
|
|
498
|
-
a.textContent = part.label;
|
|
499
|
-
a.addEventListener("click", (e) => {
|
|
500
|
-
e.preventDefault();
|
|
501
|
-
navigate(part.href);
|
|
502
|
-
});
|
|
503
|
-
nav.appendChild(a);
|
|
504
|
-
} else {
|
|
505
|
-
nav.appendChild(document.createTextNode(part.label));
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function updateHeaderRefresh(generatedAt) {
|
|
511
|
-
const elRefresh = document.getElementById("header-refresh");
|
|
512
|
-
if (!elRefresh) return;
|
|
513
|
-
if (!generatedAt) {
|
|
514
|
-
elRefresh.textContent = "";
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
elRefresh.textContent = `${formatAgo(generatedAt)} · ${UI_TEXT.nearRealtime}`;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function stopTimers() {
|
|
521
|
-
pollingGeneration += 1;
|
|
522
|
-
if (dashboardTimer) {
|
|
523
|
-
clearTimeout(dashboardTimer);
|
|
524
|
-
dashboardTimer = null;
|
|
525
|
-
}
|
|
526
|
-
if (runPollTimer) {
|
|
527
|
-
clearTimeout(runPollTimer);
|
|
528
|
-
runPollTimer = null;
|
|
529
|
-
}
|
|
530
|
-
if (dagPollTimer) {
|
|
531
|
-
clearTimeout(dagPollTimer);
|
|
532
|
-
dagPollTimer = null;
|
|
533
|
-
}
|
|
534
|
-
currentRunId = null;
|
|
535
|
-
currentBatchRunId = null;
|
|
536
|
-
currentDagRunId = null;
|
|
537
|
-
selectedDagNodeId = null;
|
|
538
|
-
sessionEventOffset = 0;
|
|
539
|
-
sessionEvents = [];
|
|
540
|
-
runEventOffset = 0;
|
|
541
|
-
runEvents = [];
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function metaGrid(entries) {
|
|
545
|
-
const dl = el("dl", "meta-grid");
|
|
546
|
-
for (const [label, value] of entries) {
|
|
547
|
-
const item = document.createElement("div");
|
|
548
|
-
item.className = "meta-item";
|
|
549
|
-
const dt = document.createElement("dt");
|
|
550
|
-
dt.textContent = label;
|
|
551
|
-
const dd = document.createElement("dd");
|
|
552
|
-
if (value instanceof Node) dd.appendChild(value);
|
|
553
|
-
else dd.textContent = value ?? "—";
|
|
554
|
-
item.appendChild(dt);
|
|
555
|
-
item.appendChild(dd);
|
|
556
|
-
dl.appendChild(item);
|
|
557
|
-
}
|
|
558
|
-
return dl;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const TERMINAL_DAG_EFFECTIVE_STATUSES = new Set([
|
|
562
|
-
"finished",
|
|
563
|
-
"failed",
|
|
564
|
-
"superseded",
|
|
565
|
-
"abandoned",
|
|
566
|
-
]);
|
|
567
|
-
|
|
568
|
-
export function dagMetaVisibility(dag) {
|
|
569
|
-
const terminal =
|
|
570
|
-
(dag?.lifecycle ?? "").toLowerCase() === "completed"
|
|
571
|
-
|| TERMINAL_DAG_EFFECTIVE_STATUSES.has(dag?.effectiveStatus);
|
|
572
|
-
return {
|
|
573
|
-
showExecutorStatus: !terminal,
|
|
574
|
-
showConsistencyWarning: dag?.stateConsistent === false,
|
|
575
|
-
showResume: dag?.recoveryEligibility?.canResume === true,
|
|
576
|
-
showReconcile: dag?.recoveryEligibility?.canReconcile === true,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function buildTable(headers, rows) {
|
|
581
|
-
const table = document.createElement("table");
|
|
582
|
-
const thead = document.createElement("thead");
|
|
583
|
-
const headRow = document.createElement("tr");
|
|
584
|
-
for (const h of headers) {
|
|
585
|
-
const th = document.createElement("th");
|
|
586
|
-
th.textContent = h;
|
|
587
|
-
headRow.appendChild(th);
|
|
588
|
-
}
|
|
589
|
-
thead.appendChild(headRow);
|
|
590
|
-
table.appendChild(thead);
|
|
591
|
-
|
|
592
|
-
const tbody = document.createElement("tbody");
|
|
593
|
-
for (const row of rows) {
|
|
594
|
-
const tr = document.createElement("tr");
|
|
595
|
-
if (row.className) tr.className = row.className;
|
|
596
|
-
if (row.onClick) {
|
|
597
|
-
tr.classList.add("clickable");
|
|
598
|
-
tr.addEventListener("click", row.onClick);
|
|
599
|
-
}
|
|
600
|
-
for (const cell of row.cells) {
|
|
601
|
-
const td = document.createElement("td");
|
|
602
|
-
if (cell instanceof Node) td.appendChild(cell);
|
|
603
|
-
else td.textContent = cell ?? "—";
|
|
604
|
-
tr.appendChild(td);
|
|
605
|
-
}
|
|
606
|
-
tbody.appendChild(tr);
|
|
607
|
-
}
|
|
608
|
-
table.appendChild(tbody);
|
|
609
|
-
return table;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function summaryText(summary) {
|
|
613
|
-
if (!summary) return "—";
|
|
614
|
-
return `总计 ${summary.total} · 成功 ${summary.succeeded} · 失败 ${summary.failed} · 复用历史结果 ${summary.reused}`;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function failuresInboxLink(count) {
|
|
618
|
-
const wrap = el("span");
|
|
619
|
-
wrap.appendChild(document.createTextNode(`${count} `));
|
|
620
|
-
const a = el("a", "inbox-link", `${UI_TEXT.failureInbox} →`);
|
|
621
|
-
a.href = "#/failures";
|
|
622
|
-
a.addEventListener("click", (e) => {
|
|
623
|
-
e.preventDefault();
|
|
624
|
-
navigate("/failures");
|
|
625
|
-
});
|
|
626
|
-
wrap.appendChild(a);
|
|
627
|
-
return wrap;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function artifactLink(label, refPath) {
|
|
631
|
-
if (!refPath) return el("span", "muted", "—");
|
|
632
|
-
const a = el("a", "artifact-link", label);
|
|
633
|
-
a.href = `/api/artifacts?path=${encodeURIComponent(refPath)}&tail=200`;
|
|
634
|
-
a.target = "_blank";
|
|
635
|
-
return a;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function isNodeActive(status) {
|
|
639
|
-
if (!status) return false;
|
|
640
|
-
const s = status.toLowerCase();
|
|
641
|
-
return s === "running" || s === "started" || s === "pending" || s === "queued";
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function isNodeFinished(status) {
|
|
645
|
-
if (!status) return false;
|
|
646
|
-
const s = status.toLowerCase();
|
|
647
|
-
return (
|
|
648
|
-
s === "finished" ||
|
|
649
|
-
s === "completed" ||
|
|
650
|
-
s === "succeeded" ||
|
|
651
|
-
s === "done" ||
|
|
652
|
-
s === "error" ||
|
|
653
|
-
s === "failed" ||
|
|
654
|
-
s === "skipped"
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function isNodeFailed(status) {
|
|
659
|
-
if (!status) return false;
|
|
660
|
-
const s = status.toLowerCase();
|
|
661
|
-
return s === "error" || s === "failed" || s === "partial_failed";
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function truncateText(text, maxLen) {
|
|
665
|
-
if (!text) return "—";
|
|
666
|
-
const normalized = String(text).replace(/\s+/g, " ").trim();
|
|
667
|
-
if (normalized.length <= maxLen) return normalized;
|
|
668
|
-
return `${normalized.slice(0, maxLen)}…`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function extractAssistantSummary(event) {
|
|
672
|
-
if (!event || typeof event !== "object") return "—";
|
|
673
|
-
const record = event;
|
|
674
|
-
if (record.message && typeof record.message === "object") {
|
|
675
|
-
const content = record.message.content;
|
|
676
|
-
if (Array.isArray(content)) {
|
|
677
|
-
return (
|
|
678
|
-
content
|
|
679
|
-
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
680
|
-
.map((part) => part.text)
|
|
681
|
-
.join(" ") || "—"
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
if (typeof record.message.content === "string") {
|
|
685
|
-
return record.message.content;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (typeof record.text === "string") return record.text;
|
|
689
|
-
if (typeof record.outputPreview === "string") return record.outputPreview;
|
|
690
|
-
return "—";
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function formatSessionEventLabel(event) {
|
|
694
|
-
if (!event || typeof event !== "object") return "未知类型";
|
|
695
|
-
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
696
|
-
const toolName = event.toolName ?? event.name ?? "—";
|
|
697
|
-
if (type === "agent_start") return "Agent 已启动";
|
|
698
|
-
if (type === "agent_end") return "Agent 已结束";
|
|
699
|
-
if (type === "agent_settled") return "Agent 已稳定";
|
|
700
|
-
if (type === "turn_start") return "开始新一轮处理";
|
|
701
|
-
if (type === "tool_execution_start" || type === "tool_start") {
|
|
702
|
-
return `调用工具 · ${toolName}`;
|
|
703
|
-
}
|
|
704
|
-
if (type === "tool_execution_end" || type === "tool_end") {
|
|
705
|
-
return `工具调用完成 · ${toolName}`;
|
|
706
|
-
}
|
|
707
|
-
if (type === "turn_end" || type === "assistant" || type === "assistant_message") {
|
|
708
|
-
return `AI 回复摘要 · ${truncateText(extractAssistantSummary(event), 120)}`;
|
|
709
|
-
}
|
|
710
|
-
return type;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function sessionEventTimestamp(event) {
|
|
714
|
-
if (!event || typeof event !== "object") return "";
|
|
715
|
-
const ts =
|
|
716
|
-
(typeof event.timestamp === "string" && event.timestamp) ||
|
|
717
|
-
(typeof event.at === "string" && event.at) ||
|
|
718
|
-
(typeof event.recordedAt === "string" && event.recordedAt);
|
|
719
|
-
return ts || "";
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function formatSessionEventTime(event) {
|
|
723
|
-
const ts = sessionEventTimestamp(event);
|
|
724
|
-
if (!ts) return "未记录时间";
|
|
725
|
-
return formatTs(ts).replace(/\s.*/, "") || formatTs(ts);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function sessionEventKind(event) {
|
|
729
|
-
const type = typeof event?.type === "string" ? event.type : "unknown";
|
|
730
|
-
if (type === "tool_execution_start" || type === "tool_start") return "tool-start";
|
|
731
|
-
if (type === "tool_execution_end" || type === "tool_end") return "tool-end";
|
|
732
|
-
if (type === "turn_end" || type === "assistant" || type === "assistant_message") {
|
|
733
|
-
return "assistant";
|
|
734
|
-
}
|
|
735
|
-
if (type.startsWith("agent_") || type === "turn_start") return "lifecycle";
|
|
736
|
-
return "protocol";
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function buildSessionTimelineItems(events) {
|
|
740
|
-
const items = [];
|
|
741
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
742
|
-
const event = events[index];
|
|
743
|
-
const type = typeof event?.type === "string" ? event.type : "unknown";
|
|
744
|
-
if (type === "message_start" || type === "message_end") {
|
|
745
|
-
let count = 1;
|
|
746
|
-
while (index + 1 < events.length) {
|
|
747
|
-
const nextType = events[index + 1]?.type;
|
|
748
|
-
if (nextType !== "message_start" && nextType !== "message_end") break;
|
|
749
|
-
count += 1;
|
|
750
|
-
index += 1;
|
|
751
|
-
}
|
|
752
|
-
items.push({ event, kind: "protocol", protocolCount: count });
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
items.push({ event, kind: sessionEventKind(event) });
|
|
756
|
-
}
|
|
757
|
-
return items;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
async function mergeSessionEvents(dagRunId, nodeId) {
|
|
761
|
-
if (!dagRunId || !nodeId) return;
|
|
762
|
-
const page = await fetchJson(
|
|
763
|
-
`/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/session-events?after=${sessionEventOffset}`,
|
|
764
|
-
);
|
|
765
|
-
if (!page) return;
|
|
766
|
-
if (Array.isArray(page.events) && page.events.length > 0) {
|
|
767
|
-
sessionEvents = sessionEvents.concat(page.events);
|
|
768
|
-
}
|
|
769
|
-
if (typeof page.nextOffset === "number") {
|
|
770
|
-
sessionEventOffset = page.nextOffset;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function selectDagNode(dagRunId, nodeId) {
|
|
775
|
-
const nodeChanged = selectedDagNodeId !== nodeId;
|
|
776
|
-
selectedDagNodeId = nodeId;
|
|
777
|
-
dagInspectorOpen = true;
|
|
778
|
-
if (nodeChanged) {
|
|
779
|
-
sessionEventOffset = 0;
|
|
780
|
-
sessionEvents = [];
|
|
781
|
-
}
|
|
782
|
-
void renderDagDetail(dagRunId, false);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function closeDagInspector() {
|
|
786
|
-
dagInspectorOpen = false;
|
|
787
|
-
const panel = document.getElementById("dag-inspector");
|
|
788
|
-
if (panel) {
|
|
789
|
-
panel.classList.remove("is-open");
|
|
790
|
-
panel.setAttribute("aria-hidden", "true");
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function closeDagInspectorOnOutsideClick(event) {
|
|
795
|
-
if (!dagInspectorOpen || !(event.target instanceof Element)) return;
|
|
796
|
-
if (
|
|
797
|
-
event.target.closest(
|
|
798
|
-
"#dag-inspector, .dag-node-table tbody tr, button, a, input, select, textarea, [role='button'], [role='tab']",
|
|
799
|
-
)
|
|
800
|
-
) {
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
closeDagInspector();
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function dagInspectorMaximumWidth() {
|
|
807
|
-
return Math.max(
|
|
808
|
-
DAG_INSPECTOR_MIN_WIDTH,
|
|
809
|
-
Math.min(
|
|
810
|
-
DAG_INSPECTOR_MAX_WIDTH,
|
|
811
|
-
window.innerWidth - DAG_INSPECTOR_VIEWPORT_MARGIN,
|
|
812
|
-
),
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function setDagInspectorWidth(panel, requestedWidth) {
|
|
817
|
-
const maximum = dagInspectorMaximumWidth();
|
|
818
|
-
dagInspectorWidth = Math.min(
|
|
819
|
-
maximum,
|
|
820
|
-
Math.max(DAG_INSPECTOR_MIN_WIDTH, Math.round(requestedWidth)),
|
|
821
|
-
);
|
|
822
|
-
panel.style.setProperty("--dag-inspector-width", `${dagInspectorWidth}px`);
|
|
823
|
-
return { minimum: DAG_INSPECTOR_MIN_WIDTH, maximum, value: dagInspectorWidth };
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function createDagInspectorResizeHandle(panel) {
|
|
827
|
-
const handle = el("div", "dag-inspector-resize-handle");
|
|
828
|
-
handle.tabIndex = 0;
|
|
829
|
-
handle.setAttribute("role", "separator");
|
|
830
|
-
handle.setAttribute("aria-orientation", "vertical");
|
|
831
|
-
handle.setAttribute("aria-label", "调整节点检查器宽度");
|
|
832
|
-
handle.title = "拖动调整节点检查器宽度";
|
|
833
|
-
|
|
834
|
-
const updateRange = (requestedWidth) => {
|
|
835
|
-
const range = setDagInspectorWidth(
|
|
836
|
-
panel,
|
|
837
|
-
requestedWidth ?? dagInspectorWidth ?? DAG_INSPECTOR_DEFAULT_WIDTH,
|
|
838
|
-
);
|
|
839
|
-
handle.setAttribute("aria-valuemin", String(range.minimum));
|
|
840
|
-
handle.setAttribute("aria-valuemax", String(range.maximum));
|
|
841
|
-
handle.setAttribute("aria-valuenow", String(range.value));
|
|
842
|
-
};
|
|
843
|
-
|
|
844
|
-
updateRange();
|
|
845
|
-
handle.addEventListener("pointerdown", (event) => {
|
|
846
|
-
if (event.button !== 0) return;
|
|
847
|
-
event.preventDefault();
|
|
848
|
-
handle.setPointerCapture(event.pointerId);
|
|
849
|
-
document.body.classList.add("is-resizing-dag-inspector");
|
|
850
|
-
});
|
|
851
|
-
handle.addEventListener("pointermove", (event) => {
|
|
852
|
-
if (!handle.hasPointerCapture(event.pointerId)) return;
|
|
853
|
-
updateRange(window.innerWidth - event.clientX - DAG_INSPECTOR_RIGHT_OFFSET);
|
|
854
|
-
});
|
|
855
|
-
const stopResizing = (event) => {
|
|
856
|
-
if (handle.hasPointerCapture(event.pointerId)) {
|
|
857
|
-
handle.releasePointerCapture(event.pointerId);
|
|
858
|
-
}
|
|
859
|
-
document.body.classList.remove("is-resizing-dag-inspector");
|
|
860
|
-
};
|
|
861
|
-
handle.addEventListener("pointerup", stopResizing);
|
|
862
|
-
handle.addEventListener("pointercancel", stopResizing);
|
|
863
|
-
handle.addEventListener("keydown", (event) => {
|
|
864
|
-
const currentWidth = dagInspectorWidth ?? DAG_INSPECTOR_DEFAULT_WIDTH;
|
|
865
|
-
if (event.key === "ArrowLeft") {
|
|
866
|
-
event.preventDefault();
|
|
867
|
-
updateRange(currentWidth + DAG_INSPECTOR_KEYBOARD_STEP);
|
|
868
|
-
} else if (event.key === "ArrowRight") {
|
|
869
|
-
event.preventDefault();
|
|
870
|
-
updateRange(currentWidth - DAG_INSPECTOR_KEYBOARD_STEP);
|
|
871
|
-
} else if (event.key === "Home") {
|
|
872
|
-
event.preventDefault();
|
|
873
|
-
updateRange(DAG_INSPECTOR_MIN_WIDTH);
|
|
874
|
-
} else if (event.key === "End") {
|
|
875
|
-
event.preventDefault();
|
|
876
|
-
updateRange(dagInspectorMaximumWidth());
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
return handle;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function captureDagTimelineViewportState(panel) {
|
|
883
|
-
const scrollEl = panel.querySelector(".process-timeline-scroll");
|
|
884
|
-
if (scrollEl?.dataset.dagRunId && scrollEl.dataset.nodeId) {
|
|
885
|
-
const followLatest =
|
|
886
|
-
scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 48;
|
|
887
|
-
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
888
|
-
dagTimelineViewportState,
|
|
889
|
-
scrollEl.dataset.dagRunId,
|
|
890
|
-
scrollEl.dataset.nodeId,
|
|
891
|
-
scrollEl.scrollLeft,
|
|
892
|
-
scrollEl.scrollTop,
|
|
893
|
-
followLatest,
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function renderSessionTimeline(content, nodeId) {
|
|
899
|
-
content.dataset.dagRunId = currentDagRunId ?? "";
|
|
900
|
-
content.dataset.nodeId = nodeId ?? "";
|
|
901
|
-
content.classList.add("process-timeline-scroll");
|
|
902
|
-
if (!nodeId || sessionEvents.length === 0) {
|
|
903
|
-
content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const timelineItems = buildSessionTimelineItems(sessionEvents);
|
|
908
|
-
const hasEventTimes = sessionEvents.some((event) => Boolean(sessionEventTimestamp(event)));
|
|
909
|
-
if (!hasEventTimes) {
|
|
910
|
-
content.appendChild(
|
|
911
|
-
el("p", "process-timeline-note", "事件未记录具体时间,以下按采集顺序展示。"),
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
const ul = el("ul", "process-timeline");
|
|
915
|
-
for (let index = 0; index < timelineItems.length; index += 1) {
|
|
916
|
-
const item = timelineItems[index];
|
|
917
|
-
const li = el(
|
|
918
|
-
"li",
|
|
919
|
-
`process-timeline-item process-timeline-${item.kind}${hasEventTimes ? " has-time" : ""}`,
|
|
920
|
-
);
|
|
921
|
-
if (index === 0) li.classList.add("is-first");
|
|
922
|
-
if (index === timelineItems.length - 1) li.classList.add("is-last");
|
|
923
|
-
const rail = el("span", "process-timeline-rail");
|
|
924
|
-
rail.appendChild(el("span", "process-timeline-dot"));
|
|
925
|
-
li.appendChild(rail);
|
|
926
|
-
if (hasEventTimes) {
|
|
927
|
-
li.appendChild(el("span", "process-timeline-time", formatSessionEventTime(item.event)));
|
|
928
|
-
}
|
|
929
|
-
const label =
|
|
930
|
-
item.protocolCount > 1
|
|
931
|
-
? `消息收发 · 已合并 ${item.protocolCount} 条协议事件`
|
|
932
|
-
: formatSessionEventLabel(item.event);
|
|
933
|
-
li.appendChild(el("span", "process-timeline-label", label));
|
|
934
|
-
ul.appendChild(li);
|
|
935
|
-
}
|
|
936
|
-
content.appendChild(ul);
|
|
937
|
-
|
|
938
|
-
const timelineState =
|
|
939
|
-
dagTimelineViewportState?.dagRunId === currentDagRunId &&
|
|
940
|
-
dagTimelineViewportState.nodeId === nodeId
|
|
941
|
-
? dagTimelineViewportState
|
|
942
|
-
: null;
|
|
943
|
-
content.addEventListener(
|
|
944
|
-
"scroll",
|
|
945
|
-
() => {
|
|
946
|
-
const followLatest =
|
|
947
|
-
content.scrollHeight - content.scrollTop - content.clientHeight < 48;
|
|
948
|
-
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
949
|
-
dagTimelineViewportState,
|
|
950
|
-
currentDagRunId,
|
|
951
|
-
nodeId,
|
|
952
|
-
content.scrollLeft,
|
|
953
|
-
content.scrollTop,
|
|
954
|
-
followLatest,
|
|
955
|
-
);
|
|
956
|
-
},
|
|
957
|
-
{ passive: true },
|
|
958
|
-
);
|
|
959
|
-
if (timelineState?.followLatest ?? true) {
|
|
960
|
-
content.scrollLeft = timelineState?.scrollLeft ?? 0;
|
|
961
|
-
content.scrollTop = content.scrollHeight;
|
|
962
|
-
} else {
|
|
963
|
-
content.scrollLeft = timelineState.scrollLeft;
|
|
964
|
-
content.scrollTop = timelineState.scrollTop;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
export function isDagRunActive(dag) {
|
|
969
|
-
if (["running", "running-quiet", "remote-unknown", "pending"].includes(dag.effectiveStatus)) return true;
|
|
970
|
-
// Only "unknown" (and absent) means liveness evidence is insufficient;
|
|
971
|
-
// it must NOT override raw lifecycle/status/node facts. Every other concrete
|
|
972
|
-
// effectiveStatus is authoritative-non-active and short-circuits to false.
|
|
973
|
-
if (dag.effectiveStatus && dag.effectiveStatus !== "unknown") return false;
|
|
974
|
-
const status = (dag.status ?? "").toLowerCase();
|
|
975
|
-
if ((dag.lifecycle ?? "").toLowerCase() === "paused") return false;
|
|
976
|
-
if (["orphaned", "stale"].includes((dag.liveness ?? "").toLowerCase())) return false;
|
|
977
|
-
if (TERMINAL_RUN_STATUSES.has(status)) return false;
|
|
978
|
-
if (["running", "pending", "started"].includes(status)) return true;
|
|
979
|
-
return (dag.nodes ?? []).some((n) => isNodeActive(n.status));
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const DASHBOARD_FEATURE_STATUSES = new Set(["running", "needs_action"]);
|
|
983
|
-
|
|
984
|
-
export function dashboardVisibleFeatures(features) {
|
|
985
|
-
return (features ?? []).filter((feature) =>
|
|
986
|
-
DASHBOARD_FEATURE_STATUSES.has(feature?.status),
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
function dagProgress(dag) {
|
|
991
|
-
const nodes = dag.nodes ?? [];
|
|
992
|
-
const finished = nodes.filter((n) => isNodeFinished(n.status)).length;
|
|
993
|
-
return { finished, total: nodes.length };
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function getActiveNodes(dag) {
|
|
997
|
-
return (dag.nodes ?? []).filter((n) => isNodeActive(n.status));
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function dagSortTime(dag) {
|
|
1001
|
-
return dag.startedAt ?? dag.finishedAt ?? "";
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function findDagForRun(snapshot, workerRunId, task) {
|
|
1005
|
-
const dagRuns = snapshot?.dagRuns ?? [];
|
|
1006
|
-
const byId = dagRuns.find((d) => d.dagRunId === workerRunId);
|
|
1007
|
-
if (byId) return byId;
|
|
1008
|
-
if (task?.dagRunId) {
|
|
1009
|
-
return dagRuns.find((d) => d.dagRunId === task.dagRunId);
|
|
1010
|
-
}
|
|
1011
|
-
return undefined;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
let kpiHelpSeed = 0;
|
|
1015
|
-
function kpiCard(label, value, extraClass, helpText) {
|
|
1016
|
-
const card = el("div", `kpi-card${extraClass ? ` ${extraClass}` : ""}`);
|
|
1017
|
-
card.appendChild(el("div", "kpi-label", label));
|
|
1018
|
-
const valueWrap = el("div", "kpi-value-row");
|
|
1019
|
-
valueWrap.appendChild(el("div", "kpi-value", String(value)));
|
|
1020
|
-
if (helpText) {
|
|
1021
|
-
kpiHelpSeed += 1;
|
|
1022
|
-
const tipId = `kpi-tip-${kpiHelpSeed}`;
|
|
1023
|
-
const helpBtn = el("button", "kpi-help");
|
|
1024
|
-
helpBtn.type = "button";
|
|
1025
|
-
helpBtn.setAttribute("aria-describedby", tipId);
|
|
1026
|
-
helpBtn.setAttribute("aria-label", `${label} 指标说明`);
|
|
1027
|
-
const icon = el("i", "ri-question-line");
|
|
1028
|
-
icon.setAttribute("aria-hidden", "true");
|
|
1029
|
-
helpBtn.appendChild(icon);
|
|
1030
|
-
const tip = el("span", "kpi-tip");
|
|
1031
|
-
tip.id = tipId;
|
|
1032
|
-
tip.setAttribute("role", "tooltip");
|
|
1033
|
-
tip.textContent = helpText;
|
|
1034
|
-
valueWrap.appendChild(helpBtn);
|
|
1035
|
-
valueWrap.appendChild(tip);
|
|
1036
|
-
}
|
|
1037
|
-
card.appendChild(valueWrap);
|
|
1038
|
-
return card;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function repoBaseName(repoRoot) {
|
|
1042
|
-
if (!repoRoot) return "未绑定仓库";
|
|
1043
|
-
const trimmed = String(repoRoot).replace(/[\\/]+$/, "");
|
|
1044
|
-
const parts = trimmed.split(/[\\/]/);
|
|
1045
|
-
return parts[parts.length - 1] || trimmed || "未绑定仓库";
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function renderRepoBanner(repoEl, snapshot) {
|
|
1049
|
-
if (!repoEl) return;
|
|
1050
|
-
clearNode(repoEl);
|
|
1051
|
-
const repoRoot = snapshot.repoRoot ?? "";
|
|
1052
|
-
const icon = el("i", "ri-git-repository-line");
|
|
1053
|
-
icon.setAttribute("aria-hidden", "true");
|
|
1054
|
-
repoEl.appendChild(icon);
|
|
1055
|
-
const text = el("div", "repo-banner-text");
|
|
1056
|
-
text.appendChild(el("span", "repo-banner-name", repoBaseName(repoRoot)));
|
|
1057
|
-
const pathLine = el("span", "repo-banner-path", repoRoot || "路径未知");
|
|
1058
|
-
pathLine.title = repoRoot || "";
|
|
1059
|
-
text.appendChild(pathLine);
|
|
1060
|
-
repoEl.appendChild(text);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
function renderProjectionFault(error) {
|
|
1064
|
-
const banner = el("div", "projection-fault");
|
|
1065
|
-
banner.setAttribute("role", "alert");
|
|
1066
|
-
const icon = el("i", "ri-error-warning-line");
|
|
1067
|
-
icon.setAttribute("aria-hidden", "true");
|
|
1068
|
-
banner.appendChild(icon);
|
|
1069
|
-
const body = el("div", "projection-fault-body");
|
|
1070
|
-
body.appendChild(el("p", "projection-fault-title", "指标暂不可用:数据投影失败。"));
|
|
1071
|
-
body.appendChild(el("p", "projection-fault-message", error.message ?? "投影失败 (projection failed)"));
|
|
1072
|
-
if (error.at) body.appendChild(el("p", "projection-fault-at", `发生时间:${error.at}`));
|
|
1073
|
-
banner.appendChild(body);
|
|
1074
|
-
return banner;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function renderActiveDagCard(dag, nowMs) {
|
|
1078
|
-
const card = el("div", "active-dag-card");
|
|
1079
|
-
if (isNodeActive(dag.status) || getActiveNodes(dag).length > 0) {
|
|
1080
|
-
card.classList.add("active-dag-card-running");
|
|
1081
|
-
}
|
|
1082
|
-
const title = dag.title || dag.dagRunId;
|
|
1083
|
-
const header = el("div", "active-dag-header");
|
|
1084
|
-
header.appendChild(el("span", "active-dag-title", title));
|
|
1085
|
-
header.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
1086
|
-
card.appendChild(header);
|
|
1087
|
-
|
|
1088
|
-
const progress = dagProgress(dag);
|
|
1089
|
-
const activeNodes = getActiveNodes(dag);
|
|
1090
|
-
const currentLabel =
|
|
1091
|
-
activeNodes.length > 0
|
|
1092
|
-
? activeNodes
|
|
1093
|
-
.map((n) => {
|
|
1094
|
-
const parts = [n.nodeId];
|
|
1095
|
-
if (n.executor) parts.push(`(${n.executor})`);
|
|
1096
|
-
if (n.model) parts.push(n.model);
|
|
1097
|
-
return parts.join(" ");
|
|
1098
|
-
})
|
|
1099
|
-
.join(", ")
|
|
1100
|
-
: "—";
|
|
1101
|
-
|
|
1102
|
-
const elapsed =
|
|
1103
|
-
dag.durationMs ??
|
|
1104
|
-
(dag.startedAt
|
|
1105
|
-
? Math.max(0, nowMs - Date.parse(dag.startedAt))
|
|
1106
|
-
: undefined);
|
|
1107
|
-
const lastUpdate = dag.finishedAt ?? dag.startedAt;
|
|
1108
|
-
|
|
1109
|
-
const meta = el("div", "active-dag-meta");
|
|
1110
|
-
meta.appendChild(
|
|
1111
|
-
el(
|
|
1112
|
-
"span",
|
|
1113
|
-
null,
|
|
1114
|
-
`进度 ${progress.finished}/${progress.total} · 当前节点:${currentLabel}`,
|
|
1115
|
-
),
|
|
1116
|
-
);
|
|
1117
|
-
meta.appendChild(
|
|
1118
|
-
el(
|
|
1119
|
-
"span",
|
|
1120
|
-
null,
|
|
1121
|
-
`已用时 ${formatDuration(elapsed)} · 最近活动 ${formatAgo(lastUpdate, nowMs)}`,
|
|
1122
|
-
),
|
|
1123
|
-
);
|
|
1124
|
-
card.appendChild(meta);
|
|
1125
|
-
|
|
1126
|
-
const link = el("a", "active-dag-link", UI_TEXT.enterDetail);
|
|
1127
|
-
link.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
|
|
1128
|
-
link.addEventListener("click", (e) => {
|
|
1129
|
-
e.preventDefault();
|
|
1130
|
-
navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
|
|
1131
|
-
});
|
|
1132
|
-
card.appendChild(link);
|
|
1133
|
-
return card;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function renderRankLanes(dag) {
|
|
1137
|
-
const container = el("div", "rank-lanes");
|
|
1138
|
-
const nodes = dag.nodes ?? [];
|
|
1139
|
-
const ranks = dag.ranks;
|
|
1140
|
-
const rankGroups = new Map();
|
|
1141
|
-
|
|
1142
|
-
if (ranks && ranks.length > 0) {
|
|
1143
|
-
for (let i = 0; i < ranks.length; i++) {
|
|
1144
|
-
rankGroups.set(String(i), ranks[i]);
|
|
1145
|
-
}
|
|
1146
|
-
for (const node of nodes) {
|
|
1147
|
-
const rank = node.rank ?? "—";
|
|
1148
|
-
if (!rankGroups.has(rank)) {
|
|
1149
|
-
rankGroups.set(rank, [node.nodeId]);
|
|
1150
|
-
} else if (!rankGroups.get(rank).includes(node.nodeId)) {
|
|
1151
|
-
rankGroups.get(rank).push(node.nodeId);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
} else {
|
|
1155
|
-
for (const node of nodes) {
|
|
1156
|
-
const rank = node.rank ?? "—";
|
|
1157
|
-
const list = rankGroups.get(rank) ?? [];
|
|
1158
|
-
list.push(node.nodeId);
|
|
1159
|
-
rankGroups.set(rank, list);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
const sortedRanks = [...rankGroups.keys()].sort((a, b) => {
|
|
1164
|
-
const na = Number(a);
|
|
1165
|
-
const nb = Number(b);
|
|
1166
|
-
if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb;
|
|
1167
|
-
return a.localeCompare(b);
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
const nodeById = Object.fromEntries(nodes.map((n) => [n.nodeId, n]));
|
|
1171
|
-
|
|
1172
|
-
for (const rank of sortedRanks) {
|
|
1173
|
-
const lane = el("div", "rank-lane");
|
|
1174
|
-
lane.appendChild(el("div", "rank-lane-label", `第 ${rank} 层`));
|
|
1175
|
-
const chips = el("div", "rank-lane-nodes");
|
|
1176
|
-
for (const nodeId of rankGroups.get(rank) ?? []) {
|
|
1177
|
-
const node = nodeById[nodeId] ?? { nodeId };
|
|
1178
|
-
const chip = el("div", "rank-node-chip");
|
|
1179
|
-
if (isNodeActive(node.status)) chip.classList.add("rank-node-active");
|
|
1180
|
-
if (isNodeFailed(node.status)) chip.classList.add("rank-node-error");
|
|
1181
|
-
chip.appendChild(el("span", "rank-node-id", nodeId));
|
|
1182
|
-
if (node.status) chip.appendChild(badge(node.status));
|
|
1183
|
-
if (node.durationMs !== undefined) {
|
|
1184
|
-
chip.appendChild(el("span", "rank-node-dur", formatMs(node.durationMs)));
|
|
1185
|
-
}
|
|
1186
|
-
chips.appendChild(chip);
|
|
1187
|
-
}
|
|
1188
|
-
lane.appendChild(chips);
|
|
1189
|
-
container.appendChild(lane);
|
|
1190
|
-
}
|
|
1191
|
-
return container;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
function svgEl(tag, attrs = {}) {
|
|
1195
|
-
const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
1196
|
-
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value));
|
|
1197
|
-
return node;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function renderDagGraph(dag, dagRunId) {
|
|
1201
|
-
const nodes = dag.nodes ?? [];
|
|
1202
|
-
const edges = dag.edges ?? [];
|
|
1203
|
-
const layout = layoutDag(nodes, edges, dag.ranks ?? []);
|
|
1204
|
-
const wrap = el("div", "dag-graph-wrap");
|
|
1205
|
-
const toolbar = el("div", "dag-graph-toolbar");
|
|
1206
|
-
toolbar.appendChild(el("span", "dag-graph-summary", `${nodes.length} 节点 · ${layout.edges.length} 条依赖`));
|
|
1207
|
-
const reset = el("button", "dag-graph-reset", "重置视图");
|
|
1208
|
-
toolbar.appendChild(reset);
|
|
1209
|
-
wrap.appendChild(toolbar);
|
|
1210
|
-
if (edges.length === 0) wrap.appendChild(el("p", "dag-graph-fallback", "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。"));
|
|
1211
|
-
const viewport = el("div", "dag-graph-viewport");
|
|
1212
|
-
viewport.dataset.dagRunId = dagRunId;
|
|
1213
|
-
const svg = svgEl("svg", { class: "dag-graph", viewBox: `0 0 ${layout.width} ${layout.height}`, width: layout.width, height: layout.height, role: "img", "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点` });
|
|
1214
|
-
const defs = svgEl("defs");
|
|
1215
|
-
const marker = svgEl("marker", { id: "dag-arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse" });
|
|
1216
|
-
marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
1217
|
-
defs.appendChild(marker); svg.appendChild(defs);
|
|
1218
|
-
const related = new Set([selectedDagNodeId]);
|
|
1219
|
-
for (const edge of layout.edges) if (edge.from === selectedDagNodeId || edge.to === selectedDagNodeId) { related.add(edge.from); related.add(edge.to); }
|
|
1220
|
-
for (const edge of layout.edges) {
|
|
1221
|
-
const path = svgEl("path", { d: edge.path, class: `dag-edge${selectedDagNodeId && (!related.has(edge.from) || !related.has(edge.to)) ? " dag-dimmed" : ""}`, "marker-end": "url(#dag-arrow)" });
|
|
1222
|
-
path.appendChild(svgEl("title")); path.firstChild.textContent = `${edge.from} → ${edge.to}`; svg.appendChild(path);
|
|
1223
|
-
}
|
|
1224
|
-
const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
|
|
1225
|
-
for (const position of layout.nodes) {
|
|
1226
|
-
const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
|
|
1227
|
-
const status = (node.status ?? "unknown").toLowerCase();
|
|
1228
|
-
const group = svgEl("g", { transform: `translate(${position.x} ${position.y})`, class: `dag-graph-node status-${status}${node.nodeId === selectedDagNodeId ? " selected" : ""}${selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`, tabindex: 0, role: "button", "aria-label": `${node.nodeId},状态 ${status}` });
|
|
1229
|
-
group.appendChild(svgEl("rect", { width: position.width, height: position.height, rx: 10 }));
|
|
1230
|
-
const title = svgEl("title"); title.textContent = `${node.nodeId}\n${node.executor ?? "未知执行器"}\n${status}`; group.appendChild(title);
|
|
1231
|
-
const label = svgEl("text", { x: 14, y: 26, class: "dag-node-label" }); label.textContent = truncateText(node.label ?? node.nodeId, 26); group.appendChild(label);
|
|
1232
|
-
const executor = svgEl("text", { x: 14, y: 50, class: "dag-node-meta" }); executor.textContent = `◈ ${node.executor ?? "unknown"}`; group.appendChild(executor);
|
|
1233
|
-
const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" }); state.textContent = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`; group.appendChild(state);
|
|
1234
|
-
const activate = () => selectDagNode(dagRunId, node.nodeId);
|
|
1235
|
-
group.addEventListener("click", activate);
|
|
1236
|
-
group.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } });
|
|
1237
|
-
svg.appendChild(group);
|
|
1238
|
-
}
|
|
1239
|
-
viewport.appendChild(svg); wrap.appendChild(viewport);
|
|
1240
|
-
const graphViewportState =
|
|
1241
|
-
dagGraphViewportStates.get(dagRunId) ??
|
|
1242
|
-
(dagGraphViewportState?.dagRunId === dagRunId
|
|
1243
|
-
? dagGraphViewportState
|
|
1244
|
-
: null);
|
|
1245
|
-
viewport.addEventListener(
|
|
1246
|
-
"scroll",
|
|
1247
|
-
() => {
|
|
1248
|
-
dagGraphViewportState = updateDagGraphViewportState(
|
|
1249
|
-
dagGraphViewportState,
|
|
1250
|
-
dagRunId,
|
|
1251
|
-
viewport.scrollLeft,
|
|
1252
|
-
viewport.scrollTop,
|
|
1253
|
-
);
|
|
1254
|
-
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
1255
|
-
},
|
|
1256
|
-
{ passive: true },
|
|
1257
|
-
);
|
|
1258
|
-
if (graphViewportState) {
|
|
1259
|
-
const restoreViewport = () => {
|
|
1260
|
-
viewport.scrollLeft = graphViewportState.scrollLeft;
|
|
1261
|
-
viewport.scrollTop = graphViewportState.scrollTop;
|
|
1262
|
-
};
|
|
1263
|
-
if (typeof requestAnimationFrame === "function") {
|
|
1264
|
-
requestAnimationFrame(restoreViewport);
|
|
1265
|
-
} else {
|
|
1266
|
-
restoreViewport();
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
reset.addEventListener("click", () => {
|
|
1270
|
-
dagGraphViewportState = updateDagGraphViewportState(
|
|
1271
|
-
dagGraphViewportState,
|
|
1272
|
-
dagRunId,
|
|
1273
|
-
0,
|
|
1274
|
-
0,
|
|
1275
|
-
);
|
|
1276
|
-
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
1277
|
-
viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
|
|
1278
|
-
});
|
|
1279
|
-
return wrap;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
function appendInlineMarkdown(target, text) {
|
|
1283
|
-
const tokenPattern = /(`[^`]*`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g;
|
|
1284
|
-
let cursor = 0;
|
|
1285
|
-
for (const match of text.matchAll(tokenPattern)) {
|
|
1286
|
-
const index = match.index ?? 0;
|
|
1287
|
-
if (index > cursor) target.appendChild(document.createTextNode(text.slice(cursor, index)));
|
|
1288
|
-
const token = match[0];
|
|
1289
|
-
if (token.startsWith("`")) {
|
|
1290
|
-
target.appendChild(el("code", null, token.slice(1, -1)));
|
|
1291
|
-
} else if (token.startsWith("**")) {
|
|
1292
|
-
const strong = document.createElement("strong");
|
|
1293
|
-
strong.textContent = token.slice(2, -2);
|
|
1294
|
-
target.appendChild(strong);
|
|
1295
|
-
} else if (token.startsWith("*")) {
|
|
1296
|
-
const emphasis = document.createElement("em");
|
|
1297
|
-
emphasis.textContent = token.slice(1, -1);
|
|
1298
|
-
target.appendChild(emphasis);
|
|
1299
|
-
} else {
|
|
1300
|
-
const link = token.match(/^\[([^\]]+)\]\((.+)\)$/);
|
|
1301
|
-
const href = markdownLinkHref(link?.[2]);
|
|
1302
|
-
if (link && href) {
|
|
1303
|
-
const anchor = document.createElement("a");
|
|
1304
|
-
anchor.href = href;
|
|
1305
|
-
anchor.target = "_blank";
|
|
1306
|
-
anchor.rel = "noopener noreferrer";
|
|
1307
|
-
anchor.textContent = link[1];
|
|
1308
|
-
target.appendChild(anchor);
|
|
1309
|
-
} else {
|
|
1310
|
-
target.appendChild(document.createTextNode(token));
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
cursor = index + token.length;
|
|
1314
|
-
}
|
|
1315
|
-
if (cursor < text.length) target.appendChild(document.createTextNode(text.slice(cursor)));
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function renderMarkdown(content) {
|
|
1319
|
-
const article = el("article", "markdown-output");
|
|
1320
|
-
for (const block of parseMarkdownBlocks(content)) {
|
|
1321
|
-
switch (block.type) {
|
|
1322
|
-
case "heading": {
|
|
1323
|
-
const heading = document.createElement(`h${block.level}`);
|
|
1324
|
-
appendInlineMarkdown(heading, block.text);
|
|
1325
|
-
article.appendChild(heading);
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
case "paragraph": {
|
|
1329
|
-
const paragraph = document.createElement("p");
|
|
1330
|
-
appendInlineMarkdown(paragraph, block.text);
|
|
1331
|
-
article.appendChild(paragraph);
|
|
1332
|
-
break;
|
|
1333
|
-
}
|
|
1334
|
-
case "quote": {
|
|
1335
|
-
const quote = document.createElement("blockquote");
|
|
1336
|
-
appendInlineMarkdown(quote, block.text);
|
|
1337
|
-
article.appendChild(quote);
|
|
1338
|
-
break;
|
|
1339
|
-
}
|
|
1340
|
-
case "list": {
|
|
1341
|
-
const list = document.createElement(block.ordered ? "ol" : "ul");
|
|
1342
|
-
for (const item of block.items) {
|
|
1343
|
-
const listItem = document.createElement("li");
|
|
1344
|
-
appendInlineMarkdown(listItem, item);
|
|
1345
|
-
list.appendChild(listItem);
|
|
1346
|
-
}
|
|
1347
|
-
article.appendChild(list);
|
|
1348
|
-
break;
|
|
1349
|
-
}
|
|
1350
|
-
case "code": {
|
|
1351
|
-
const pre = document.createElement("pre");
|
|
1352
|
-
const code = document.createElement("code");
|
|
1353
|
-
if (block.language) code.dataset.language = block.language;
|
|
1354
|
-
code.textContent = block.text;
|
|
1355
|
-
pre.appendChild(code);
|
|
1356
|
-
article.appendChild(pre);
|
|
1357
|
-
break;
|
|
1358
|
-
}
|
|
1359
|
-
case "table": {
|
|
1360
|
-
const table = document.createElement("table");
|
|
1361
|
-
const head = document.createElement("thead");
|
|
1362
|
-
const headRow = document.createElement("tr");
|
|
1363
|
-
for (const value of block.headers) {
|
|
1364
|
-
const cell = document.createElement("th");
|
|
1365
|
-
appendInlineMarkdown(cell, value);
|
|
1366
|
-
headRow.appendChild(cell);
|
|
1367
|
-
}
|
|
1368
|
-
head.appendChild(headRow);
|
|
1369
|
-
table.appendChild(head);
|
|
1370
|
-
const body = document.createElement("tbody");
|
|
1371
|
-
for (const row of block.rows) {
|
|
1372
|
-
const tableRow = document.createElement("tr");
|
|
1373
|
-
for (const value of row) {
|
|
1374
|
-
const cell = document.createElement("td");
|
|
1375
|
-
appendInlineMarkdown(cell, value);
|
|
1376
|
-
tableRow.appendChild(cell);
|
|
1377
|
-
}
|
|
1378
|
-
body.appendChild(tableRow);
|
|
1379
|
-
}
|
|
1380
|
-
table.appendChild(body);
|
|
1381
|
-
article.appendChild(table);
|
|
1382
|
-
break;
|
|
1383
|
-
}
|
|
1384
|
-
case "rule":
|
|
1385
|
-
article.appendChild(document.createElement("hr"));
|
|
1386
|
-
break;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
return article;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
function renderDagInspector(dagRunId, node) {
|
|
1393
|
-
const panel = document.getElementById("dag-inspector");
|
|
1394
|
-
if (!panel) return;
|
|
1395
|
-
captureDagTimelineViewportState(panel);
|
|
1396
|
-
if (!dagInspectorOpen || !node) {
|
|
1397
|
-
panel.classList.remove("is-open");
|
|
1398
|
-
panel.setAttribute("aria-hidden", "true");
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
clearNode(panel);
|
|
1403
|
-
panel.classList.add("is-open");
|
|
1404
|
-
panel.setAttribute("aria-hidden", "false");
|
|
1405
|
-
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
1406
|
-
const header = el("div", "dag-inspector-header");
|
|
1407
|
-
const title = el("div", "dag-inspector-title");
|
|
1408
|
-
title.appendChild(el("h3", null, "节点检查器"));
|
|
1409
|
-
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
1410
|
-
header.appendChild(title);
|
|
1411
|
-
const controls = el("div", "dag-inspector-controls");
|
|
1412
|
-
controls.appendChild(badge(node.status ?? "unknown"));
|
|
1413
|
-
const close = document.createElement("button");
|
|
1414
|
-
close.type = "button";
|
|
1415
|
-
close.className = "dag-inspector-close";
|
|
1416
|
-
close.setAttribute("aria-label", "关闭节点检查器");
|
|
1417
|
-
const closeIcon = el("i", "ri-close-line");
|
|
1418
|
-
closeIcon.setAttribute("aria-hidden", "true");
|
|
1419
|
-
close.appendChild(closeIcon);
|
|
1420
|
-
close.addEventListener("click", closeDagInspector);
|
|
1421
|
-
controls.appendChild(close);
|
|
1422
|
-
header.appendChild(controls);
|
|
1423
|
-
|
|
1424
|
-
const tabs = el("div", "dag-inspector-tabs");
|
|
1425
|
-
tabs.setAttribute("role", "tablist");
|
|
1426
|
-
for (const [tab, label, icon] of [["output", "节点输出", "ri-file-text-line"], ["timeline", "执行过程", "ri-route-line"]]) {
|
|
1427
|
-
const button = document.createElement("button");
|
|
1428
|
-
button.type = "button";
|
|
1429
|
-
button.className = "dag-inspector-tab";
|
|
1430
|
-
button.setAttribute("role", "tab");
|
|
1431
|
-
button.setAttribute("aria-selected", String(dagInspectorTab === tab));
|
|
1432
|
-
const tabIcon = el("i", icon);
|
|
1433
|
-
tabIcon.setAttribute("aria-hidden", "true");
|
|
1434
|
-
button.appendChild(tabIcon);
|
|
1435
|
-
button.appendChild(document.createTextNode(` ${label}`));
|
|
1436
|
-
button.addEventListener("click", () => {
|
|
1437
|
-
dagInspectorTab = tab;
|
|
1438
|
-
renderDagInspector(dagRunId, node);
|
|
1439
|
-
});
|
|
1440
|
-
tabs.appendChild(button);
|
|
1441
|
-
}
|
|
1442
|
-
header.appendChild(tabs);
|
|
1443
|
-
panel.appendChild(header);
|
|
1444
|
-
|
|
1445
|
-
const content = el("div", "dag-inspector-content");
|
|
1446
|
-
content.setAttribute("role", "tabpanel");
|
|
1447
|
-
if (dagInspectorTab === "timeline") {
|
|
1448
|
-
renderSessionTimeline(content, node.nodeId);
|
|
1449
|
-
} else if (!node.outputPreview && !node.errorPreview) {
|
|
1450
|
-
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1451
|
-
} else {
|
|
1452
|
-
if (node.outputPreview) content.appendChild(renderMarkdown(node.outputPreview));
|
|
1453
|
-
if (node.errorPreview) {
|
|
1454
|
-
const error = el("section", "markdown-error");
|
|
1455
|
-
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
1456
|
-
error.appendChild(renderMarkdown(node.errorPreview));
|
|
1457
|
-
content.appendChild(error);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
panel.appendChild(content);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
async function renderFailures() {
|
|
1464
|
-
showView("failures");
|
|
1465
|
-
setBreadcrumb([{ label: UI_TEXT.dashboard, href: "#/" }, { label: UI_TEXT.failures }]);
|
|
1466
|
-
|
|
1467
|
-
const listEl = document.getElementById("failures-list");
|
|
1468
|
-
const emptyEl = document.getElementById("failures-empty");
|
|
1469
|
-
clearNode(listEl);
|
|
1470
|
-
|
|
1471
|
-
const snapshot = await fetchJson("/api/snapshot");
|
|
1472
|
-
if (!snapshot) return;
|
|
1473
|
-
lastSnapshot = snapshot;
|
|
1474
|
-
updateHeaderRefresh(snapshot.generatedAt);
|
|
1475
|
-
|
|
1476
|
-
const tasks = (snapshot.tasks ?? []).filter(
|
|
1477
|
-
(task) =>
|
|
1478
|
-
task.status === "failed" ||
|
|
1479
|
-
task.status === "blocked" ||
|
|
1480
|
-
task.status === "stale" ||
|
|
1481
|
-
task.liveness === "stale",
|
|
1482
|
-
);
|
|
1483
|
-
|
|
1484
|
-
if (tasks.length === 0) {
|
|
1485
|
-
emptyEl.hidden = false;
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
emptyEl.hidden = true;
|
|
1489
|
-
|
|
1490
|
-
const refs = (task) => task.artifactRefs ?? {};
|
|
1491
|
-
const rows = tasks.map((task) => ({
|
|
1492
|
-
className: `row-${task.status}`,
|
|
1493
|
-
onClick: task.workerRunId
|
|
1494
|
-
? () => navigate(`/run/${encodeURIComponent(task.workerRunId)}`)
|
|
1495
|
-
: undefined,
|
|
1496
|
-
cells: [
|
|
1497
|
-
task.taskId ?? "—",
|
|
1498
|
-
task.featureId ?? "—",
|
|
1499
|
-
badge(task.status),
|
|
1500
|
-
task.failureCategory ?? "—",
|
|
1501
|
-
task.recommendedFollowUp ?? "—",
|
|
1502
|
-
(() => {
|
|
1503
|
-
const links = el("div", "artifact-links");
|
|
1504
|
-
const r = refs(task);
|
|
1505
|
-
links.appendChild(artifactLink("报告", r.reportMarkdown));
|
|
1506
|
-
links.appendChild(document.createTextNode(" "));
|
|
1507
|
-
links.appendChild(artifactLink("诊断", r.doctorMarkdown));
|
|
1508
|
-
links.appendChild(document.createTextNode(" "));
|
|
1509
|
-
links.appendChild(artifactLink("收尾方案", r.closeoutDraft));
|
|
1510
|
-
return links;
|
|
1511
|
-
})(),
|
|
1512
|
-
],
|
|
1513
|
-
}));
|
|
1514
|
-
listEl.appendChild(
|
|
1515
|
-
buildTable(
|
|
1516
|
-
["Task ID", "功能", "状态", "分类", "建议跟进", "产物"],
|
|
1517
|
-
rows,
|
|
1518
|
-
),
|
|
1519
|
-
);
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
async function renderDashboard(scrollTo) {
|
|
1523
|
-
showView("dashboard");
|
|
1524
|
-
setBreadcrumb([{ label: UI_TEXT.dashboard }]);
|
|
1525
|
-
|
|
1526
|
-
const kpiEl = document.getElementById("dashboard-kpi");
|
|
1527
|
-
const featureEl = document.getElementById("dashboard-features");
|
|
1528
|
-
const activeEl = document.getElementById("dashboard-active-dags");
|
|
1529
|
-
const riskEl = document.getElementById("dashboard-risk");
|
|
1530
|
-
const dagsEl = document.getElementById("dashboard-dags");
|
|
1531
|
-
const batchesEl = document.getElementById("dashboard-batches");
|
|
1532
|
-
const repoEl = document.getElementById("dashboard-repo");
|
|
1533
|
-
|
|
1534
|
-
const snapshot = await fetchJson("/api/snapshot");
|
|
1535
|
-
if (!snapshot) return;
|
|
1536
|
-
lastSnapshot = snapshot;
|
|
1537
|
-
updateHeaderRefresh(snapshot.generatedAt);
|
|
1538
|
-
|
|
1539
|
-
const dagRuns = snapshot.dagRuns ?? [];
|
|
1540
|
-
const activeDags = dagRuns.filter(isDagRunActive);
|
|
1541
|
-
const activeDagIds = new Set(activeDags.map((d) => d.dagRunId));
|
|
1542
|
-
const recentDags = dagRuns
|
|
1543
|
-
.filter((d) => !activeDagIds.has(d.dagRunId))
|
|
1544
|
-
.sort((a, b) => dagSortTime(b).localeCompare(dagSortTime(a)))
|
|
1545
|
-
.slice(0, 10);
|
|
1546
|
-
|
|
1547
|
-
const features = dashboardVisibleFeatures(snapshot.features);
|
|
1548
|
-
featureEl.hidden = features.length === 0;
|
|
1549
|
-
clearNode(featureEl);
|
|
1550
|
-
if (features.length > 0) {
|
|
1551
|
-
featureEl.appendChild(el("h3", "section-heading", "Feature 决策"));
|
|
1552
|
-
featureEl.appendChild(buildTable(
|
|
1553
|
-
["Feature", "状态", "下一步", "原因", "required AC", "证据"],
|
|
1554
|
-
features.map((feature) => ({ cells: [
|
|
1555
|
-
feature.featureId,
|
|
1556
|
-
feature.statusLabel ?? feature.status,
|
|
1557
|
-
feature.nextAction?.command ?? feature.nextAction?.label ?? "—",
|
|
1558
|
-
feature.blockingItems?.[0]?.message ?? `${feature.summary?.tasksSucceeded ?? 0}/${feature.summary?.tasksTotal ?? 0} tasks completed`,
|
|
1559
|
-
`${feature.summary?.requiredAcCovered ?? 0}/${feature.summary?.requiredAcTotal ?? 0}`,
|
|
1560
|
-
artifactLink("查看", feature.evidence?.delivery ?? feature.evidence?.closeout ?? feature.evidence?.morningReport ?? feature.evidence?.observeSnapshot),
|
|
1561
|
-
] })),
|
|
1562
|
-
));
|
|
1563
|
-
}
|
|
1564
|
-
if (features.length > 0 && (snapshot.projectionWarnings ?? []).length > 0) {
|
|
1565
|
-
featureEl.appendChild(el("p", "empty", `投影警告:${snapshot.projectionWarnings.join(";")}`));
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
renderRepoBanner(repoEl, snapshot);
|
|
1569
|
-
|
|
1570
|
-
clearNode(kpiEl);
|
|
1571
|
-
const dagHealth = snapshot.health?.dag;
|
|
1572
|
-
const dagActiveRuns = dagHealth?.activeRuns ?? activeDags.length;
|
|
1573
|
-
const dagRunningNodes = dagHealth?.runningNodes
|
|
1574
|
-
?? activeDags.flatMap((dag) => dag.nodes ?? []).filter((node) => ["running", "started"].includes((node.status ?? "").toLowerCase())).length;
|
|
1575
|
-
const dagPendingNodes = dagHealth?.pendingNodes
|
|
1576
|
-
?? activeDags.flatMap((dag) => dag.nodes ?? []).filter((node) => ["pending", "queued"].includes((node.status ?? "").toLowerCase())).length;
|
|
1577
|
-
const dagPausedRuns = dagHealth?.pausedRuns ?? 0;
|
|
1578
|
-
const dagStaleRuns = dagHealth?.staleRuns ?? 0;
|
|
1579
|
-
const dagInterruptedRuns = dagHealth?.interruptedRuns ?? 0;
|
|
1580
|
-
const dagInconsistentRuns = dagHealth?.inconsistentRuns ?? 0;
|
|
1581
|
-
const dagAttentionRuns = dagHealth?.attentionRuns
|
|
1582
|
-
?? dagRuns.filter((dag) => (dag.lifecycle ?? "").toLowerCase() !== "completed" && (
|
|
1583
|
-
(dag.lifecycle ?? "").toLowerCase() === "paused"
|
|
1584
|
-
|| ["stale", "orphaned"].includes((dag.liveness ?? "").toLowerCase())
|
|
1585
|
-
|| ["interrupted", "remote-unknown"].includes(dag.effectiveStatus)
|
|
1586
|
-
|| dag.stateConsistent === false
|
|
1587
|
-
)).length;
|
|
1588
|
-
|
|
1589
|
-
const quietCount = snapshot.health?.quietCount ?? 0;
|
|
1590
|
-
const staleCount = snapshot.health?.staleCount ?? 0;
|
|
1591
|
-
const timeoutRiskCount = snapshot.health?.timeoutRiskCount ?? 0;
|
|
1592
|
-
const failuresCount = snapshot.health?.failuresCount ?? 0;
|
|
1593
|
-
|
|
1594
|
-
if (snapshot.projectionError) {
|
|
1595
|
-
kpiEl.appendChild(renderProjectionFault(snapshot.projectionError));
|
|
1596
|
-
} else {
|
|
1597
|
-
const dagGroup = el("fieldset", "kpi-group");
|
|
1598
|
-
dagGroup.appendChild(el("legend", "kpi-group-legend", "DAG 运行"));
|
|
1599
|
-
const dagCards = el("div", "kpi-grid");
|
|
1600
|
-
dagCards.appendChild(kpiCard("活跃 DAG", dagActiveRuns, "kpi-highlight",
|
|
1601
|
-
"当前处于运行、静默运行、远端状态未知或等待执行的 DAG 数量(来源:.harness/dag-runs 的有效状态)。"));
|
|
1602
|
-
dagCards.appendChild(kpiCard("执行中节点", dagRunningNodes, "",
|
|
1603
|
-
"活跃 DAG 中状态为 running 或 started、正在执行的节点数量;DAG 内执行单元统一称为节点。"));
|
|
1604
|
-
dagCards.appendChild(kpiCard("等待节点", dagPendingNodes, "",
|
|
1605
|
-
"活跃 DAG 中状态为 pending 或 queued、尚未开始执行的节点数量。"));
|
|
1606
|
-
dagCards.appendChild(kpiCard("暂停 DAG", dagPausedRuns, dagPausedRuns > 0 ? "kpi-muted" : "",
|
|
1607
|
-
"生命周期被暂停的 DAG 运行数量(来源:.harness/dag-runs 下的 lifecycle 目录)。"));
|
|
1608
|
-
dagCards.appendChild(kpiCard("需处理 DAG", dagAttentionRuns, dagAttentionRuns > 0 ? "kpi-risk" : "",
|
|
1609
|
-
"未完成且暂停、失联、中断、远端状态未知或状态不一致的 DAG 数量;同一个 DAG 只计一次。"));
|
|
1610
|
-
dagGroup.appendChild(dagCards);
|
|
1611
|
-
|
|
1612
|
-
const workerGroup = el("fieldset", "kpi-group");
|
|
1613
|
-
workerGroup.appendChild(el("legend", "kpi-group-legend", "Worker 执行"));
|
|
1614
|
-
const workerCards = el("div", "kpi-grid");
|
|
1615
|
-
workerCards.appendChild(kpiCard("活跃 Worker Task", snapshot.health?.activeTasks ?? 0, "",
|
|
1616
|
-
"当前运行或等待执行的 Worker Task Pool 任务数量(来源:.harness/task-pool 的状态、运行记录与事件;不含 .harness/tasks 下的 task.json)。"));
|
|
1617
|
-
workerCards.appendChild(kpiCard("无输出", quietCount, quietCount > 0 ? "kpi-muted" : "",
|
|
1618
|
-
"心跳仍正常,但超过无输出判定时长仍没有新输出的 Worker Task 数量。"));
|
|
1619
|
-
workerCards.appendChild(kpiCard("心跳失联", staleCount, staleCount > 0 ? "kpi-risk" : "",
|
|
1620
|
-
"超过心跳失联判定时长仍没有新心跳的 Worker Task 数量。"));
|
|
1621
|
-
workerCards.appendChild(kpiCard("即将超时", timeoutRiskCount, timeoutRiskCount > 0 ? "kpi-risk" : "",
|
|
1622
|
-
"命令已执行时长超过 80% 超时的 Worker Task 数量。"));
|
|
1623
|
-
workerCards.appendChild(kpiCard("失败", failuresCount, failuresCount > 0 ? "kpi-danger" : "",
|
|
1624
|
-
"状态为 failed 的 Worker Task 数量。"));
|
|
1625
|
-
workerGroup.appendChild(workerCards);
|
|
1626
|
-
kpiEl.appendChild(workerGroup);
|
|
1627
|
-
kpiEl.appendChild(dagGroup);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
const nowMs = Date.now();
|
|
1631
|
-
|
|
1632
|
-
clearNode(activeEl);
|
|
1633
|
-
activeEl.appendChild(el("h3", "section-heading", UI_TEXT.activeDags));
|
|
1634
|
-
if (activeDags.length === 0) {
|
|
1635
|
-
activeEl.appendChild(el("p", "empty", UI_TEXT.noActiveDags));
|
|
1636
|
-
} else {
|
|
1637
|
-
const list = el("div", "active-dag-list");
|
|
1638
|
-
for (const dag of activeDags.sort((a, b) =>
|
|
1639
|
-
dagSortTime(b).localeCompare(dagSortTime(a)),
|
|
1640
|
-
)) {
|
|
1641
|
-
list.appendChild(renderActiveDagCard(dag, nowMs));
|
|
1642
|
-
}
|
|
1643
|
-
activeEl.appendChild(list);
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
clearNode(riskEl);
|
|
1647
|
-
const dagRiskCount = dagAttentionRuns;
|
|
1648
|
-
const workerRiskCount = failuresCount + staleCount + timeoutRiskCount;
|
|
1649
|
-
riskEl.classList.toggle("panel-risk-attention", dagRiskCount > 0 || workerRiskCount > 0);
|
|
1650
|
-
riskEl.appendChild(el("h3", "section-heading", "运行风险"));
|
|
1651
|
-
if (dagRiskCount === 0 && workerRiskCount === 0) {
|
|
1652
|
-
riskEl.appendChild(el("p", "risk-empty", "当前没有需要处理的异常。"));
|
|
1653
|
-
} else {
|
|
1654
|
-
const riskBlocks = el("div", "risk-blocks");
|
|
1655
|
-
if (dagRiskCount > 0) {
|
|
1656
|
-
const dagBlock = el("div", "risk-block risk-block-dag");
|
|
1657
|
-
dagBlock.appendChild(el("div", "risk-block-label", "需处理 DAG"));
|
|
1658
|
-
dagBlock.appendChild(el("div", "risk-total", String(dagRiskCount)));
|
|
1659
|
-
dagBlock.appendChild(el("p", "risk-summary",
|
|
1660
|
-
`去重运行数 ${dagAttentionRuns} · 中断 ${dagInterruptedRuns} · 失联 ${dagStaleRuns} · 状态不一致 ${dagInconsistentRuns} · 暂停 ${dagPausedRuns}`));
|
|
1661
|
-
riskBlocks.appendChild(dagBlock);
|
|
1662
|
-
}
|
|
1663
|
-
if (workerRiskCount > 0) {
|
|
1664
|
-
const workerBlock = el("div", "risk-block risk-block-worker");
|
|
1665
|
-
workerBlock.appendChild(el("div", "risk-block-label", "Worker 风险"));
|
|
1666
|
-
workerBlock.appendChild(el("div", "risk-total", String(workerRiskCount)));
|
|
1667
|
-
workerBlock.appendChild(el("p", "risk-summary",
|
|
1668
|
-
`失败 ${failuresCount} · 心跳失联 ${staleCount} · 即将超时 ${timeoutRiskCount}`));
|
|
1669
|
-
const link = el("a", "risk-link", "查看异常 Task →");
|
|
1670
|
-
link.href = "#/failures";
|
|
1671
|
-
link.addEventListener("click", (event) => {
|
|
1672
|
-
event.preventDefault();
|
|
1673
|
-
navigate("/failures");
|
|
1674
|
-
});
|
|
1675
|
-
workerBlock.appendChild(link);
|
|
1676
|
-
riskBlocks.appendChild(workerBlock);
|
|
1677
|
-
}
|
|
1678
|
-
riskEl.appendChild(riskBlocks);
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
clearNode(dagsEl);
|
|
1682
|
-
dagsEl.id = "dashboard-dags";
|
|
1683
|
-
dagsEl.appendChild(el("h3", "section-heading", UI_TEXT.recentDags));
|
|
1684
|
-
if (recentDags.length === 0 && activeDags.length === 0) {
|
|
1685
|
-
dagsEl.appendChild(el("p", "empty", UI_TEXT.noRecentDags));
|
|
1686
|
-
} else if (recentDags.length === 0) {
|
|
1687
|
-
dagsEl.appendChild(el("p", "empty", "暂无历史 DAG 记录"));
|
|
1688
|
-
} else {
|
|
1689
|
-
const timeline = el("div", "dag-run-timeline");
|
|
1690
|
-
for (const dag of recentDags) {
|
|
1691
|
-
const progress = dagProgress(dag);
|
|
1692
|
-
const item = el("a", "dag-run-timeline-item");
|
|
1693
|
-
item.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
|
|
1694
|
-
item.title = dag.title || dag.dagRunId;
|
|
1695
|
-
item.addEventListener("click", (event) => {
|
|
1696
|
-
event.preventDefault();
|
|
1697
|
-
navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
const rail = el("span", "dag-run-timeline-rail");
|
|
1701
|
-
rail.appendChild(
|
|
1702
|
-
el(
|
|
1703
|
-
"span",
|
|
1704
|
-
`dag-run-timeline-dot status-${badgeClass(dag.effectiveStatus ?? dag.status)}`,
|
|
1705
|
-
),
|
|
1706
|
-
);
|
|
1707
|
-
item.appendChild(rail);
|
|
1708
|
-
|
|
1709
|
-
const content = el("span", "dag-run-timeline-content");
|
|
1710
|
-
content.appendChild(el("span", "dag-run-timeline-title", dag.title || dag.dagRunId));
|
|
1711
|
-
content.appendChild(
|
|
1712
|
-
el(
|
|
1713
|
-
"span",
|
|
1714
|
-
"dag-run-timeline-meta",
|
|
1715
|
-
`${formatTs(dag.startedAt)} · ${progress.finished}/${progress.total} · ${formatDuration(dag.durationMs)}`,
|
|
1716
|
-
),
|
|
1717
|
-
);
|
|
1718
|
-
item.appendChild(content);
|
|
1719
|
-
|
|
1720
|
-
const state = el("span", "dag-run-timeline-status");
|
|
1721
|
-
state.appendChild(dagStatusBadge(dag.effectiveStatus ?? dag.status));
|
|
1722
|
-
item.appendChild(state);
|
|
1723
|
-
timeline.appendChild(item);
|
|
1724
|
-
}
|
|
1725
|
-
dagsEl.appendChild(timeline);
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
clearNode(batchesEl);
|
|
1729
|
-
batchesEl.appendChild(el("h3", "section-heading", UI_TEXT.batches));
|
|
1730
|
-
const batches = snapshot.batches ?? [];
|
|
1731
|
-
if (batches.length === 0) {
|
|
1732
|
-
batchesEl.appendChild(el("p", "empty", UI_TEXT.noBatches));
|
|
1733
|
-
} else {
|
|
1734
|
-
const rows = batches.map((batch) => ({
|
|
1735
|
-
onClick: () => navigate(`/batch/${encodeURIComponent(batch.batchRunId)}`),
|
|
1736
|
-
cells: [
|
|
1737
|
-
batch.batchRunId,
|
|
1738
|
-
batch.featureId ?? "—",
|
|
1739
|
-
badge(batch.status),
|
|
1740
|
-
formatTs(batch.startedAt),
|
|
1741
|
-
summaryText(batch.summary),
|
|
1742
|
-
],
|
|
1743
|
-
}));
|
|
1744
|
-
batchesEl.appendChild(
|
|
1745
|
-
buildTable(["批次 ID", "功能", "状态", "开始时间", "摘要"], rows),
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
if (scrollTo === "dags") {
|
|
1750
|
-
dagsEl.scrollIntoView({ behavior: "smooth" });
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
async function renderBatch(batchRunId) {
|
|
1755
|
-
showView("batch");
|
|
1756
|
-
setBreadcrumb([
|
|
1757
|
-
{ label: UI_TEXT.dashboard, href: "#/" },
|
|
1758
|
-
{ label: batchRunId },
|
|
1759
|
-
]);
|
|
1760
|
-
|
|
1761
|
-
const metaEl = document.getElementById("batch-meta");
|
|
1762
|
-
const tasksEl = document.getElementById("batch-tasks");
|
|
1763
|
-
|
|
1764
|
-
const batch = await fetchJson(`/api/batches/${encodeURIComponent(batchRunId)}`);
|
|
1765
|
-
if (!batch) {
|
|
1766
|
-
clearNode(metaEl);
|
|
1767
|
-
metaEl.appendChild(el("p", "empty", "找不到该批次。"));
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
clearNode(metaEl);
|
|
1772
|
-
metaEl.appendChild(
|
|
1773
|
-
metaGrid([
|
|
1774
|
-
["批次 ID", batch.batchRunId],
|
|
1775
|
-
["功能 ID", batch.featureId ?? "—"],
|
|
1776
|
-
["状态", badge(batch.status)],
|
|
1777
|
-
["开始时间", formatTs(batch.startedAt)],
|
|
1778
|
-
["结束时间", formatTs(batch.finishedAt)],
|
|
1779
|
-
["摘要", summaryText(batch.summary)],
|
|
1780
|
-
]),
|
|
1781
|
-
);
|
|
1782
|
-
|
|
1783
|
-
clearNode(tasksEl);
|
|
1784
|
-
tasksEl.appendChild(el("h3", null, "Task"));
|
|
1785
|
-
|
|
1786
|
-
const tasks = batch.tasks ?? [];
|
|
1787
|
-
if (tasks.length === 0) {
|
|
1788
|
-
tasksEl.appendChild(el("p", "empty", "该批次暂无 Task"));
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
const rows = tasks.map((task) => ({
|
|
1793
|
-
className: task.status === "running" ? "active-task" : "",
|
|
1794
|
-
onClick: task.workerRunId
|
|
1795
|
-
? () => navigate(`/run/${encodeURIComponent(task.workerRunId)}`)
|
|
1796
|
-
: undefined,
|
|
1797
|
-
cells: [
|
|
1798
|
-
task.taskId,
|
|
1799
|
-
badge(task.status),
|
|
1800
|
-
task.workerRunId ?? "—",
|
|
1801
|
-
formatMs(task.durationMs),
|
|
1802
|
-
task.failureCategory ?? "—",
|
|
1803
|
-
],
|
|
1804
|
-
}));
|
|
1805
|
-
tasksEl.appendChild(
|
|
1806
|
-
buildTable(["Task ID", "状态", "Worker 运行 ID", "耗时", "失败"], rows),
|
|
1807
|
-
);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
function stepDisplayLabel(label) {
|
|
1811
|
-
return STEP_LABELS[label] ?? label;
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
function buildStepStates(events) {
|
|
1815
|
-
const states = new Map();
|
|
1816
|
-
for (const ev of events) {
|
|
1817
|
-
if (ev.type === "step.started") {
|
|
1818
|
-
states.set(ev.label, { status: "running", startedAt: ev.at });
|
|
1819
|
-
} else if (ev.type === "step.finished") {
|
|
1820
|
-
states.set(ev.label, {
|
|
1821
|
-
status: ev.status === "failed" ? "failed" : "succeeded",
|
|
1822
|
-
durationMs: ev.durationMs,
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
const ordered = [...STEP_ORDER];
|
|
1828
|
-
for (const label of states.keys()) {
|
|
1829
|
-
if (!ordered.includes(label)) ordered.push(label);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
return ordered.map((label) => {
|
|
1833
|
-
const state = states.get(label);
|
|
1834
|
-
return {
|
|
1835
|
-
label,
|
|
1836
|
-
display: stepDisplayLabel(label),
|
|
1837
|
-
status: state?.status ?? "pending",
|
|
1838
|
-
durationMs: state?.durationMs,
|
|
1839
|
-
};
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function stepMarker(status) {
|
|
1844
|
-
switch (status) {
|
|
1845
|
-
case "succeeded":
|
|
1846
|
-
return "✓";
|
|
1847
|
-
case "running":
|
|
1848
|
-
return "●";
|
|
1849
|
-
case "failed":
|
|
1850
|
-
return "✗";
|
|
1851
|
-
default:
|
|
1852
|
-
return "○";
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
function getCurrentCommand(events) {
|
|
1857
|
-
const active = new Map();
|
|
1858
|
-
for (const ev of events) {
|
|
1859
|
-
if (ev.type === "command.started") {
|
|
1860
|
-
active.set(ev.label, ev);
|
|
1861
|
-
} else if (ev.type === "command.finished") {
|
|
1862
|
-
active.delete(ev.label);
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
let latest = null;
|
|
1866
|
-
for (const cmd of active.values()) {
|
|
1867
|
-
if (!latest || cmd.at > latest.at) latest = cmd;
|
|
1868
|
-
}
|
|
1869
|
-
if (latest?.at) {
|
|
1870
|
-
const elapsed = Date.now() - Date.parse(latest.at);
|
|
1871
|
-
latest = { ...latest, elapsedMs: Number.isFinite(elapsed) ? elapsed : 0 };
|
|
1872
|
-
}
|
|
1873
|
-
return latest;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
function getOutputTail(events, commandLabel) {
|
|
1877
|
-
if (!commandLabel) return "";
|
|
1878
|
-
const chunks = [];
|
|
1879
|
-
for (const ev of events) {
|
|
1880
|
-
if (ev.type === "command.output" && ev.label === commandLabel && ev.outputPreview) {
|
|
1881
|
-
chunks.push(ev.outputPreview);
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
return chunks.join("");
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function collectArtifacts(task, events) {
|
|
1888
|
-
const arts = new Map();
|
|
1889
|
-
const add = (label, artifactPath) => {
|
|
1890
|
-
if (artifactPath && !arts.has(label)) arts.set(label, artifactPath);
|
|
1891
|
-
};
|
|
1892
|
-
|
|
1893
|
-
if (task?.artifactRefs) {
|
|
1894
|
-
for (const key of ARTIFACT_KEYS) {
|
|
1895
|
-
add(ARTIFACT_LABELS[key] ?? key, task.artifactRefs[key]);
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
for (const ev of events) {
|
|
1900
|
-
if (!ev.artifactRefs) continue;
|
|
1901
|
-
for (const [key, artifactPath] of Object.entries(ev.artifactRefs)) {
|
|
1902
|
-
add(ARTIFACT_LABELS[key] ?? key, artifactPath);
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
return arts;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
function artifactUrl(artifactPath) {
|
|
1910
|
-
return `/api/artifacts?path=${encodeURIComponent(artifactPath)}&tail=200`;
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
function renderRunDagProgress(task, snapshot) {
|
|
1914
|
-
const panel = document.getElementById("run-dag-progress");
|
|
1915
|
-
clearNode(panel);
|
|
1916
|
-
const dag = findDagForRun(snapshot, task.workerRunId, task);
|
|
1917
|
-
if (!dag) return;
|
|
1918
|
-
|
|
1919
|
-
const progress = dagProgress(dag);
|
|
1920
|
-
panel.appendChild(el("h3", null, "DAG 节点进度"));
|
|
1921
|
-
panel.appendChild(
|
|
1922
|
-
metaGrid([
|
|
1923
|
-
["DAG", dag.title || dag.dagRunId],
|
|
1924
|
-
["当前状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
1925
|
-
["进度", `${progress.finished}/${progress.total} 个节点`],
|
|
1926
|
-
[
|
|
1927
|
-
"详情",
|
|
1928
|
-
(() => {
|
|
1929
|
-
const a = el("a", "link", "查看 DAG 详情 →");
|
|
1930
|
-
a.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
|
|
1931
|
-
a.addEventListener("click", (e) => {
|
|
1932
|
-
e.preventDefault();
|
|
1933
|
-
navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
|
|
1934
|
-
});
|
|
1935
|
-
return a;
|
|
1936
|
-
})(),
|
|
1937
|
-
],
|
|
1938
|
-
]),
|
|
1939
|
-
);
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
function renderRunPanels(task, events, snapshot) {
|
|
1943
|
-
const metaEl = document.getElementById("run-meta");
|
|
1944
|
-
const stepsEl = document.getElementById("run-steps");
|
|
1945
|
-
const commandEl = document.getElementById("run-command");
|
|
1946
|
-
const outputEl = document.getElementById("run-output");
|
|
1947
|
-
const artifactsEl = document.getElementById("run-artifacts");
|
|
1948
|
-
|
|
1949
|
-
const nowMs = Date.now();
|
|
1950
|
-
const livenessNode = livenessBadge(task.liveness);
|
|
1951
|
-
const statusCell = livenessNode
|
|
1952
|
-
? (() => {
|
|
1953
|
-
const wrap = document.createDocumentFragment();
|
|
1954
|
-
wrap.appendChild(badge(task.status));
|
|
1955
|
-
wrap.appendChild(document.createTextNode(" "));
|
|
1956
|
-
wrap.appendChild(livenessNode);
|
|
1957
|
-
return wrap;
|
|
1958
|
-
})()
|
|
1959
|
-
: badge(task.status);
|
|
1960
|
-
|
|
1961
|
-
clearNode(metaEl);
|
|
1962
|
-
metaEl.appendChild(
|
|
1963
|
-
metaGrid([
|
|
1964
|
-
["Worker 运行 ID", task.workerRunId ?? "—"],
|
|
1965
|
-
["Task ID", task.taskId],
|
|
1966
|
-
["状态", statusCell],
|
|
1967
|
-
["开始时间", formatTs(task.startedAt)],
|
|
1968
|
-
["已用时", formatDuration(task.elapsedMs)],
|
|
1969
|
-
["上次心跳", formatAgo(task.lastHeartbeatAt, nowMs)],
|
|
1970
|
-
["上次输出", formatAgo(task.lastOutputAt, nowMs)],
|
|
1971
|
-
[
|
|
1972
|
-
"超时截止",
|
|
1973
|
-
task.timeoutAt
|
|
1974
|
-
? formatAgo(task.timeoutAt, nowMs)
|
|
1975
|
-
: task.pid !== undefined
|
|
1976
|
-
? `pid ${task.pid}`
|
|
1977
|
-
: "—",
|
|
1978
|
-
],
|
|
1979
|
-
["耗时", formatMs(task.durationMs)],
|
|
1980
|
-
["失败分类", task.failureCategory ?? "—"],
|
|
1981
|
-
]),
|
|
1982
|
-
);
|
|
1983
|
-
|
|
1984
|
-
renderRunDagProgress(task, snapshot);
|
|
1985
|
-
|
|
1986
|
-
const steps = buildStepStates(events);
|
|
1987
|
-
clearNode(stepsEl);
|
|
1988
|
-
const stepsHeading = el("h3", null, "执行步骤");
|
|
1989
|
-
stepsEl.appendChild(stepsHeading);
|
|
1990
|
-
const ul = el("ul", "step-timeline");
|
|
1991
|
-
for (const step of steps) {
|
|
1992
|
-
const li = document.createElement("li");
|
|
1993
|
-
const marker = el("span", `step-marker ${step.status}`, stepMarker(step.status));
|
|
1994
|
-
const name = el("span", "step-name", step.display);
|
|
1995
|
-
const dur = el(
|
|
1996
|
-
"span",
|
|
1997
|
-
"step-duration",
|
|
1998
|
-
step.durationMs !== undefined ? formatMs(step.durationMs) : "",
|
|
1999
|
-
);
|
|
2000
|
-
li.appendChild(marker);
|
|
2001
|
-
if (step.label === "run-dag" && task.workerRunId) {
|
|
2002
|
-
const dag = findDagForRun(snapshot, task.workerRunId, task);
|
|
2003
|
-
const dagId = dag?.dagRunId ?? task.workerRunId;
|
|
2004
|
-
const link = document.createElement("a");
|
|
2005
|
-
link.href = `#/dag/${encodeURIComponent(dagId)}`;
|
|
2006
|
-
link.className = "step-link";
|
|
2007
|
-
link.textContent = "DAG →";
|
|
2008
|
-
link.addEventListener("click", (e) => {
|
|
2009
|
-
e.preventDefault();
|
|
2010
|
-
navigate(`/dag/${encodeURIComponent(dagId)}`);
|
|
2011
|
-
});
|
|
2012
|
-
li.appendChild(name);
|
|
2013
|
-
li.appendChild(link);
|
|
2014
|
-
} else {
|
|
2015
|
-
li.appendChild(name);
|
|
2016
|
-
}
|
|
2017
|
-
li.appendChild(dur);
|
|
2018
|
-
ul.appendChild(li);
|
|
2019
|
-
}
|
|
2020
|
-
stepsEl.appendChild(ul);
|
|
2021
|
-
|
|
2022
|
-
const cmd = getCurrentCommand(events);
|
|
2023
|
-
clearNode(commandEl);
|
|
2024
|
-
commandEl.appendChild(el("h3", null, "当前命令"));
|
|
2025
|
-
if (!cmd) {
|
|
2026
|
-
commandEl.appendChild(el("p", "empty", UI_TEXT.noActiveCommand));
|
|
2027
|
-
} else {
|
|
2028
|
-
commandEl.appendChild(
|
|
2029
|
-
metaGrid([
|
|
2030
|
-
["标签", cmd.label ?? "—"],
|
|
2031
|
-
["命令", (cmd.command ?? []).join(" ") || "—"],
|
|
2032
|
-
["PID", cmd.pid !== undefined ? String(cmd.pid) : "—"],
|
|
2033
|
-
["超时上限", cmd.timeoutMs !== undefined ? formatMs(cmd.timeoutMs) : "—"],
|
|
2034
|
-
["已用时间", cmd.elapsedMs !== undefined ? formatMs(cmd.elapsedMs) : "—"],
|
|
2035
|
-
]),
|
|
2036
|
-
);
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
const outputLabel = cmd?.label;
|
|
2040
|
-
const tail = getOutputTail(events, outputLabel);
|
|
2041
|
-
clearNode(outputEl);
|
|
2042
|
-
outputEl.appendChild(el("h3", null, "最新输出"));
|
|
2043
|
-
const pre = el("pre", "output-block", tail || "暂无输出");
|
|
2044
|
-
outputEl.appendChild(pre);
|
|
2045
|
-
|
|
2046
|
-
const artifacts = collectArtifacts(task, events);
|
|
2047
|
-
clearNode(artifactsEl);
|
|
2048
|
-
artifactsEl.appendChild(el("h3", null, "产物"));
|
|
2049
|
-
if (artifacts.size === 0) {
|
|
2050
|
-
artifactsEl.appendChild(el("p", "empty", UI_TEXT.noArtifacts));
|
|
2051
|
-
} else {
|
|
2052
|
-
const list = el("ul", "artifact-list");
|
|
2053
|
-
for (const [label, artifactPath] of artifacts) {
|
|
2054
|
-
const li = document.createElement("li");
|
|
2055
|
-
const a = document.createElement("a");
|
|
2056
|
-
a.href = artifactUrl(artifactPath);
|
|
2057
|
-
a.textContent = `${label}: ${artifactPath}`;
|
|
2058
|
-
a.target = "_blank";
|
|
2059
|
-
a.rel = "noopener";
|
|
2060
|
-
li.appendChild(a);
|
|
2061
|
-
list.appendChild(li);
|
|
2062
|
-
}
|
|
2063
|
-
artifactsEl.appendChild(list);
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
async function mergeRunEvents(workerRunId) {
|
|
2068
|
-
const page = await fetchJson(
|
|
2069
|
-
`/api/runs/${encodeURIComponent(workerRunId)}/events?after=${runEventOffset}`,
|
|
2070
|
-
);
|
|
2071
|
-
if (!page?.events) return;
|
|
2072
|
-
runEvents = runEvents.concat(page.events);
|
|
2073
|
-
runEventOffset += page.events.length;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
async function renderRun(workerRunId, initial = true) {
|
|
2077
|
-
showView("run");
|
|
2078
|
-
setBreadcrumb([
|
|
2079
|
-
{ label: UI_TEXT.dashboard, href: "#/" },
|
|
2080
|
-
{ label: workerRunId },
|
|
2081
|
-
]);
|
|
2082
|
-
|
|
2083
|
-
if (initial) {
|
|
2084
|
-
runEvents = [];
|
|
2085
|
-
runEventOffset = 0;
|
|
2086
|
-
currentRunId = workerRunId;
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
const [task, snapshot] = await Promise.all([
|
|
2090
|
-
fetchJson(`/api/runs/${encodeURIComponent(workerRunId)}`),
|
|
2091
|
-
fetchJson("/api/snapshot"),
|
|
2092
|
-
]);
|
|
2093
|
-
if (!task) {
|
|
2094
|
-
if (initial) {
|
|
2095
|
-
clearNode(document.getElementById("run-meta"));
|
|
2096
|
-
document
|
|
2097
|
-
.getElementById("run-meta")
|
|
2098
|
-
.appendChild(el("p", "empty", "找不到该运行。"));
|
|
2099
|
-
}
|
|
2100
|
-
return false;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
if (snapshot) {
|
|
2104
|
-
lastSnapshot = snapshot;
|
|
2105
|
-
updateHeaderRefresh(snapshot.generatedAt);
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
currentBatchRunId = task.batchRunId ?? currentBatchRunId;
|
|
2109
|
-
await mergeRunEvents(workerRunId);
|
|
2110
|
-
renderRunPanels(task, runEvents, snapshot ?? lastSnapshot);
|
|
2111
|
-
return !TERMINAL_RUN_STATUSES.has((task.status ?? "").toLowerCase());
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
function startRunPolling(workerRunId, initial = true) {
|
|
2115
|
-
if (runPollTimer) clearTimeout(runPollTimer);
|
|
2116
|
-
const generation = ++pollingGeneration;
|
|
2117
|
-
const poll = async (isInitial) => {
|
|
2118
|
-
const active = await renderRun(workerRunId, isInitial);
|
|
2119
|
-
if (generation !== pollingGeneration || !active) return;
|
|
2120
|
-
const delay = detailPollDelay("running", isPageVisible());
|
|
2121
|
-
runPollTimer = setTimeout(() => {
|
|
2122
|
-
runPollTimer = null;
|
|
2123
|
-
void poll(false);
|
|
2124
|
-
}, delay);
|
|
2125
|
-
};
|
|
2126
|
-
void poll(initial);
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
async function renderDagDetail(dagRunId, initial = true) {
|
|
2130
|
-
showView("dag");
|
|
2131
|
-
setBreadcrumb([
|
|
2132
|
-
{ label: UI_TEXT.dashboard, href: "#/" },
|
|
2133
|
-
{ label: dagRunId },
|
|
2134
|
-
]);
|
|
2135
|
-
|
|
2136
|
-
if (initial || currentDagRunId !== dagRunId) {
|
|
2137
|
-
currentDagRunId = dagRunId;
|
|
2138
|
-
selectedDagNodeId = null;
|
|
2139
|
-
sessionEventOffset = 0;
|
|
2140
|
-
sessionEvents = [];
|
|
2141
|
-
dagInspectorOpen = false;
|
|
2142
|
-
dagInspectorTab = "output";
|
|
2143
|
-
dagGraphViewportState = null;
|
|
2144
|
-
dagTimelineViewportState = null;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
const metaEl = document.getElementById("dag-meta");
|
|
2148
|
-
const ranksEl = document.getElementById("dag-ranks");
|
|
2149
|
-
const nodesEl = document.getElementById("dag-nodes");
|
|
2150
|
-
dagGraphViewportState = captureDagGraphViewportState(
|
|
2151
|
-
dagGraphViewportState,
|
|
2152
|
-
dagRunId,
|
|
2153
|
-
ranksEl.querySelector(".dag-graph-viewport"),
|
|
2154
|
-
);
|
|
2155
|
-
if (dagGraphViewportState?.dagRunId === dagRunId) {
|
|
2156
|
-
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
const dag = await fetchJson(`/api/dag-runs/${encodeURIComponent(dagRunId)}`);
|
|
2160
|
-
if (!dag) {
|
|
2161
|
-
clearNode(metaEl);
|
|
2162
|
-
metaEl.appendChild(el("p", "empty", "找不到该 DAG 运行。"));
|
|
2163
|
-
closeDagInspector();
|
|
2164
|
-
return false;
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
const nodes = dag.nodes ?? [];
|
|
2168
|
-
if (selectedDagNodeId && !nodes.some((n) => n.nodeId === selectedDagNodeId)) {
|
|
2169
|
-
selectedDagNodeId = null;
|
|
2170
|
-
dagInspectorOpen = false;
|
|
2171
|
-
if (initial) {
|
|
2172
|
-
sessionEventOffset = 0;
|
|
2173
|
-
sessionEvents = [];
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
const progress = dagProgress(dag);
|
|
2178
|
-
const failedCount = nodes.filter((n) => isNodeFailed(n.status)).length;
|
|
2179
|
-
const skippedCount = nodes.filter(
|
|
2180
|
-
(n) => (n.status ?? "").toLowerCase() === "skipped",
|
|
2181
|
-
).length;
|
|
2182
|
-
const succeededCount = nodes.filter((n) => isNodeFinished(n.status) && !isNodeFailed(n.status))
|
|
2183
|
-
.length;
|
|
2184
|
-
|
|
2185
|
-
clearNode(metaEl);
|
|
2186
|
-
const visibility = dagMetaVisibility(dag);
|
|
2187
|
-
const livenessValue = LIVENESS_LABELS[dag.liveness] ?? dag.liveness ?? LIVENESS_LABELS.unknown;
|
|
2188
|
-
const primaryEntries = [
|
|
2189
|
-
["标题", dag.title || dag.dagRunId],
|
|
2190
|
-
["DAG 运行 ID", dag.dagRunId],
|
|
2191
|
-
["状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
2192
|
-
];
|
|
2193
|
-
if (visibility.showExecutorStatus) primaryEntries.push(["执行器状态", livenessValue]);
|
|
2194
|
-
if (visibility.showConsistencyWarning) primaryEntries.push(["状态一致性", "不一致,需要处理"]);
|
|
2195
|
-
if (visibility.showResume) primaryEntries.push(["允许恢复", "是"]);
|
|
2196
|
-
if (visibility.showReconcile) primaryEntries.push(["允许收口", "是"]);
|
|
2197
|
-
primaryEntries.push(
|
|
2198
|
-
["开始时间", formatTs(dag.startedAt)],
|
|
2199
|
-
["结束时间", formatTs(dag.finishedAt)],
|
|
2200
|
-
["总耗时", formatDuration(dag.durationMs)],
|
|
2201
|
-
["进度", `${progress.finished}/${progress.total}`],
|
|
2202
|
-
["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
|
|
2203
|
-
["DAG 路径", dag.dagPath ?? "—"],
|
|
2204
|
-
);
|
|
2205
|
-
metaEl.appendChild(metaGrid(primaryEntries));
|
|
2206
|
-
const details = el("details", "meta-diagnostics");
|
|
2207
|
-
details.appendChild(el("summary", null, "诊断信息"));
|
|
2208
|
-
details.appendChild(metaGrid([
|
|
2209
|
-
["当前判定", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
2210
|
-
["原始记录状态", badge(dag.status)],
|
|
2211
|
-
["执行器状态", livenessValue],
|
|
2212
|
-
["生命周期", dag.lifecycle ?? "—"],
|
|
2213
|
-
["状态一致性", visibility.showConsistencyWarning ? "不一致,需要处理" : "一致"],
|
|
2214
|
-
["允许恢复", visibility.showResume ? "是" : "否"],
|
|
2215
|
-
["允许收口", visibility.showReconcile ? "是" : "否"],
|
|
2216
|
-
]));
|
|
2217
|
-
metaEl.appendChild(details);
|
|
2218
|
-
|
|
2219
|
-
clearNode(ranksEl);
|
|
2220
|
-
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
2221
|
-
if (nodes.length === 0) {
|
|
2222
|
-
ranksEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
2223
|
-
} else {
|
|
2224
|
-
ranksEl.appendChild(renderDagGraph(dag, dagRunId));
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
clearNode(nodesEl);
|
|
2228
|
-
const nodeHeading = el("div", "dag-section-heading");
|
|
2229
|
-
nodeHeading.appendChild(el("h3", null, "节点"));
|
|
2230
|
-
nodeHeading.appendChild(el("span", "dag-section-meta", `共 ${nodes.length} 个节点`));
|
|
2231
|
-
nodesEl.appendChild(nodeHeading);
|
|
2232
|
-
|
|
2233
|
-
if (nodes.length === 0) {
|
|
2234
|
-
nodesEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
2235
|
-
} else {
|
|
2236
|
-
const sortedNodes = [...nodes].sort((a, b) => {
|
|
2237
|
-
const ra = Number(a.rank);
|
|
2238
|
-
const rb = Number(b.rank);
|
|
2239
|
-
if (!Number.isNaN(ra) && !Number.isNaN(rb) && ra !== rb) return ra - rb;
|
|
2240
|
-
if (a.rank !== b.rank) return (a.rank ?? "").localeCompare(b.rank ?? "");
|
|
2241
|
-
return a.nodeId.localeCompare(b.nodeId);
|
|
2242
|
-
});
|
|
2243
|
-
|
|
2244
|
-
const rows = sortedNodes.map((node) => {
|
|
2245
|
-
const active = isNodeActive(node.status);
|
|
2246
|
-
const failed = isNodeFailed(node.status);
|
|
2247
|
-
let className = "";
|
|
2248
|
-
if (active) className = "dag-active-node";
|
|
2249
|
-
if (failed) className = "dag-error-node";
|
|
2250
|
-
if (node.nodeId === selectedDagNodeId) {
|
|
2251
|
-
className = className ? `${className} dag-node-selected` : "dag-node-selected";
|
|
2252
|
-
}
|
|
2253
|
-
return {
|
|
2254
|
-
className,
|
|
2255
|
-
onClick: () => selectDagNode(dagRunId, node.nodeId),
|
|
2256
|
-
cells: [
|
|
2257
|
-
node.nodeId,
|
|
2258
|
-
node.rank ?? "—",
|
|
2259
|
-
node.executor ?? "—",
|
|
2260
|
-
node.model ?? "—",
|
|
2261
|
-
node.status ? badge(node.status) : "—",
|
|
2262
|
-
formatMs(node.durationMs),
|
|
2263
|
-
node.errorPreview ?? node.label ?? "—",
|
|
2264
|
-
],
|
|
2265
|
-
};
|
|
2266
|
-
});
|
|
2267
|
-
const nodeTable = buildTable(
|
|
2268
|
-
["节点 ID", "依赖层", "执行方式", "模型", "状态", "耗时", "备注"],
|
|
2269
|
-
rows,
|
|
2270
|
-
);
|
|
2271
|
-
nodeTable.classList.add("dag-node-table");
|
|
2272
|
-
for (const row of nodeTable.tBodies[0]?.rows ?? []) {
|
|
2273
|
-
for (const columnIndex of [0, 3, 6]) {
|
|
2274
|
-
const cell = row.cells[columnIndex];
|
|
2275
|
-
if (cell?.textContent) cell.title = cell.textContent;
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
nodesEl.appendChild(nodeTable);
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
if (dagInspectorOpen && selectedDagNodeId) {
|
|
2282
|
-
await mergeSessionEvents(dagRunId, selectedDagNodeId);
|
|
2283
|
-
}
|
|
2284
|
-
renderDagInspector(
|
|
2285
|
-
dagRunId,
|
|
2286
|
-
nodes.find((node) => node.nodeId === selectedDagNodeId),
|
|
2287
|
-
);
|
|
2288
|
-
return isDagRunActive(dag);
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
function startDagPolling(dagRunId, initial = true) {
|
|
2292
|
-
if (dagPollTimer) clearTimeout(dagPollTimer);
|
|
2293
|
-
const generation = ++pollingGeneration;
|
|
2294
|
-
const poll = async (isInitial) => {
|
|
2295
|
-
const active = await renderDagDetail(dagRunId, isInitial);
|
|
2296
|
-
if (generation !== pollingGeneration || !active) return;
|
|
2297
|
-
const delay = detailPollDelay("running", isPageVisible());
|
|
2298
|
-
dagPollTimer = setTimeout(() => {
|
|
2299
|
-
dagPollTimer = null;
|
|
2300
|
-
void poll(false);
|
|
2301
|
-
}, delay);
|
|
2302
|
-
};
|
|
2303
|
-
void poll(initial);
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
function startDashboardPolling(scrollTo) {
|
|
2307
|
-
if (dashboardTimer) clearTimeout(dashboardTimer);
|
|
2308
|
-
const generation = ++pollingGeneration;
|
|
2309
|
-
const poll = async (initialScrollTo) => {
|
|
2310
|
-
await renderDashboard(initialScrollTo);
|
|
2311
|
-
if (generation !== pollingGeneration) return;
|
|
2312
|
-
dashboardTimer = setTimeout(() => {
|
|
2313
|
-
dashboardTimer = null;
|
|
2314
|
-
void poll();
|
|
2315
|
-
}, dashboardPollDelay());
|
|
2316
|
-
};
|
|
2317
|
-
void poll(scrollTo);
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
115
|
function route() {
|
|
2321
116
|
stopTimers();
|
|
2322
117
|
const r = parseRoute();
|
|
@@ -2330,6 +125,12 @@ function route() {
|
|
|
2330
125
|
startDagPolling(r.dagRunId);
|
|
2331
126
|
} else if (r.view === "failures") {
|
|
2332
127
|
void renderFailures();
|
|
128
|
+
} else if (r.view === "pool") {
|
|
129
|
+
startPoolPolling();
|
|
130
|
+
} else if (r.view === "task") {
|
|
131
|
+
startTaskPolling(r.taskId);
|
|
132
|
+
} else if (r.view === "feature") {
|
|
133
|
+
void renderFeatureDetail(r.featureId);
|
|
2333
134
|
}
|
|
2334
135
|
}
|
|
2335
136
|
|
|
@@ -2343,5 +144,7 @@ if (typeof window !== "undefined") {
|
|
|
2343
144
|
if (r.view === "dashboard") startDashboardPolling();
|
|
2344
145
|
if (r.view === "run") startRunPolling(r.workerRunId, false);
|
|
2345
146
|
if (r.view === "dag") startDagPolling(r.dagRunId, false);
|
|
147
|
+
if (r.view === "pool") startPoolPolling();
|
|
148
|
+
if (r.view === "task") startTaskPolling(r.taskId);
|
|
2346
149
|
});
|
|
2347
150
|
}
|