@tea-agent/loop-agent 0.29.3 → 0.31.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/CHANGELOG.md +60 -0
- package/dist/executors/dag-pi-executor.js +18 -6
- package/dist/worker/observability/dag-execution-trajectory.js +591 -0
- package/dist/worker/observability/read-model.js +258 -31
- package/dist/worker/observe/dag-node-execution-output.js +180 -0
- package/dist/worker/observe/routes.js +53 -6
- package/dist/worker/observe/static/dag-edge-routing.js +368 -0
- package/dist/worker/observe/static/dag-history-labels.js +95 -0
- package/dist/worker/observe/static/dag-layout.d.ts +12 -7
- package/dist/worker/observe/static/dag-layout.js +101 -21
- package/dist/worker/observe/static/favicon.svg +37 -0
- package/dist/worker/observe/static/format.js +31 -1
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/state.js +102 -0
- package/dist/worker/observe/static/styles.css +267 -7
- package/dist/worker/observe/static/views/dag-graph.js +414 -154
- package/dist/worker/observe/static/views/dag-inspector.js +478 -27
- package/dist/worker/observe/static/views/dag-trajectory.js +313 -0
- package/dist/worker/observe/static/views/dag.js +20 -3
- package/dist/workflows/dag/backend-test-writer-completeness.js +398 -26
- package/dist/workflows/dag/dynamic-runtime/map.js +10 -4
- package/dist/workflows/dag/init-hybrid.js +228 -229
- package/dist/workflows/dag/node-execution.js +13 -2
- package/dist/workflows/dag/types.js +21 -7
- package/docs/architecture/README.md +4 -0
- package/docs/architecture/dag-execution.md +1 -1
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/governance/README.md +3 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/backend-test-dag.json +664 -460
- package/docs/templates/frontend-test-dag.json +1 -1
- package/harness.json +2 -2
- package/package.json +3 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +1 -1
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
/** DAG dependency graph rendering (Observe UI R5). */
|
|
2
2
|
import { layoutDag } from "../dag-layout.js";
|
|
3
|
+
import {
|
|
4
|
+
assignControlLanes,
|
|
5
|
+
controlLaneMetrics,
|
|
6
|
+
edgeCollidesNonEndpointNodes,
|
|
7
|
+
routeControlEdge,
|
|
8
|
+
} from "../dag-edge-routing.js";
|
|
3
9
|
import { el } from "../dom.js";
|
|
4
10
|
import {
|
|
5
11
|
statusLabel,
|
|
6
12
|
statusSymbol,
|
|
7
13
|
formatMs,
|
|
8
|
-
|
|
14
|
+
truncateDisplayText,
|
|
9
15
|
} from "../format.js";
|
|
10
16
|
import {
|
|
11
17
|
uiState,
|
|
12
18
|
dagGraphViewportStates,
|
|
19
|
+
dagTrajectoryViewportStates,
|
|
13
20
|
updateDagGraphViewportState,
|
|
21
|
+
updateDagTrajectoryViewportState,
|
|
22
|
+
setDagGraphViewMode,
|
|
23
|
+
currentDagGraphViewMode,
|
|
14
24
|
} from "../state.js";
|
|
15
25
|
import { selectDagNode } from "./dag-inspector.js";
|
|
26
|
+
import { renderTrajectoryStoryboard } from "./dag-trajectory.js";
|
|
16
27
|
|
|
17
28
|
export function svgEl(tag, attrs = {}) {
|
|
18
29
|
const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
@@ -21,10 +32,110 @@ export function svgEl(tag, attrs = {}) {
|
|
|
21
32
|
return node;
|
|
22
33
|
}
|
|
23
34
|
|
|
35
|
+
/** Card geometry: compact 3-line card vs 4-line history card. */
|
|
36
|
+
const NODE_HEIGHT_BASE = 92;
|
|
37
|
+
const NODE_HEIGHT_WITH_HISTORY = 108;
|
|
38
|
+
const NODE_TEXT = {
|
|
39
|
+
labelY: 24,
|
|
40
|
+
executorY: 44,
|
|
41
|
+
statusY: 66,
|
|
42
|
+
historyY: 88,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const GRAPH_MODE_TABS = [
|
|
46
|
+
{ mode: "topology", label: "逻辑拓扑" },
|
|
47
|
+
{ mode: "trajectory", label: "执行轨迹" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function hasHistoryLabel(node) {
|
|
51
|
+
return (
|
|
52
|
+
typeof node?.executionSummary?.historyLabel === "string" &&
|
|
53
|
+
Boolean(node.executionSummary.historyLabel.trim())
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Visible primary control-loop count (Chinese label uses 修复回环 ×N). */
|
|
58
|
+
function controlLoopCount(dag) {
|
|
59
|
+
const conv = dag?.convergence;
|
|
60
|
+
if (conv?.enabled && Number.isFinite(Number(conv.currentPass))) {
|
|
61
|
+
return Math.max(1, Number(conv.currentPass) - 1);
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(conv?.passHistory) && conv.passHistory.length > 0) {
|
|
64
|
+
return Math.max(1, conv.passHistory.length);
|
|
65
|
+
}
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Raw pass range for title/diagnostics only (not primary label). */
|
|
70
|
+
function controlPassRangeLabel(dag) {
|
|
71
|
+
const conv = dag?.convergence;
|
|
72
|
+
if (!conv?.enabled) return "";
|
|
73
|
+
const current = Number(conv.currentPass);
|
|
74
|
+
if (!Number.isFinite(current) || current < 2) return "";
|
|
75
|
+
return `P${current - 1} → P${current}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 双视图 segmented control(AC-001):role=tab + aria-selected + roving
|
|
80
|
+
* tabIndex;ArrowLeft/ArrowRight 循环、Enter/Space 激活。切换后写入
|
|
81
|
+
* dagGraphViewModes(dagRunId) 并就地重绘图区域(不依赖 fetch)。
|
|
82
|
+
*/
|
|
83
|
+
function buildGraphModeSwitch(dag, dagRunId, wrap, mode) {
|
|
84
|
+
const switchEl = el("div", "dag-graph-mode-switch");
|
|
85
|
+
switchEl.setAttribute("role", "tablist");
|
|
86
|
+
switchEl.setAttribute("aria-label", "图视图模式");
|
|
87
|
+
const activate = (nextMode) => {
|
|
88
|
+
if (nextMode === mode) return;
|
|
89
|
+
setDagGraphViewMode(dagRunId, nextMode);
|
|
90
|
+
renderDagGraph(dag, dagRunId, wrap);
|
|
91
|
+
// Restore keyboard focus to the newly selected tab after rebuild.
|
|
92
|
+
wrap.querySelector?.('.dag-graph-mode-tab[aria-selected="true"]')?.focus?.();
|
|
93
|
+
};
|
|
94
|
+
GRAPH_MODE_TABS.forEach((tab, index) => {
|
|
95
|
+
const selected = tab.mode === mode;
|
|
96
|
+
const button = document.createElement("button");
|
|
97
|
+
button.type = "button";
|
|
98
|
+
button.className = "dag-graph-mode-tab";
|
|
99
|
+
button.setAttribute("role", "tab");
|
|
100
|
+
button.setAttribute("aria-selected", String(selected));
|
|
101
|
+
button.setAttribute("tabindex", selected ? "0" : "-1");
|
|
102
|
+
button.textContent = tab.label;
|
|
103
|
+
button.addEventListener("click", () => activate(tab.mode));
|
|
104
|
+
button.addEventListener("keydown", (event) => {
|
|
105
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
const delta = event.key === "ArrowRight" ? 1 : -1;
|
|
108
|
+
const next = GRAPH_MODE_TABS[
|
|
109
|
+
(index + delta + GRAPH_MODE_TABS.length) % GRAPH_MODE_TABS.length
|
|
110
|
+
];
|
|
111
|
+
activate(next.mode);
|
|
112
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
activate(tab.mode);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
switchEl.appendChild(button);
|
|
118
|
+
});
|
|
119
|
+
return switchEl;
|
|
120
|
+
}
|
|
121
|
+
|
|
24
122
|
export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
25
123
|
const nodes = dag.nodes ?? [];
|
|
26
124
|
const edges = dag.edges ?? [];
|
|
27
|
-
const
|
|
125
|
+
const mode = currentDagGraphViewMode(dagRunId);
|
|
126
|
+
const nodeHeights = Object.fromEntries(
|
|
127
|
+
nodes.map((node) => [
|
|
128
|
+
node.nodeId,
|
|
129
|
+
hasHistoryLabel(node) ? NODE_HEIGHT_WITH_HISTORY : NODE_HEIGHT_BASE,
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
// controlEdges never enter topology; only reserve presentation lanes.
|
|
133
|
+
const controlEdgeList = Array.isArray(dag.controlEdges) ? dag.controlEdges : [];
|
|
134
|
+
const controlLaneCount = controlEdgeList.length;
|
|
135
|
+
const layout = layoutDag(nodes, edges, dag.ranks ?? [], {
|
|
136
|
+
nodeHeights,
|
|
137
|
+
controlLaneCount,
|
|
138
|
+
});
|
|
28
139
|
const wrap = existingWrap ?? el("div", "dag-graph-wrap");
|
|
29
140
|
// Preserve .dag-graph-viewport DOM identity across poll refreshes so the
|
|
30
141
|
// user's active scroll/pointer target is not destroyed mid-gesture.
|
|
@@ -44,15 +155,19 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
44
155
|
}
|
|
45
156
|
}
|
|
46
157
|
const toolbar = el("div", "dag-graph-toolbar");
|
|
47
|
-
|
|
158
|
+
const toolbarLeft = el("div", "dag-graph-toolbar-left");
|
|
159
|
+
toolbarLeft.appendChild(buildGraphModeSwitch(dag, dagRunId, wrap, mode));
|
|
160
|
+
let summaryText = `${nodes.length} 节点 · ${edges.length} 条依赖`;
|
|
48
161
|
const convergence = dag.convergence;
|
|
49
162
|
if (convergence?.enabled) {
|
|
50
|
-
|
|
163
|
+
// AC-003:工具栏摘要使用中文「修复轮次 x/y」,不以裸 P1/P2 为主文案。
|
|
164
|
+
summaryText += ` · 修复轮次 ${convergence.currentPass}/${convergence.maxPasses}`;
|
|
51
165
|
if (convergence.terminalReason) {
|
|
52
166
|
summaryText += ` · ${convergence.terminalReason}`;
|
|
53
167
|
}
|
|
54
168
|
}
|
|
55
|
-
|
|
169
|
+
toolbarLeft.appendChild(el("span", "dag-graph-summary", summaryText));
|
|
170
|
+
toolbar.appendChild(toolbarLeft);
|
|
56
171
|
const reset = el("button", "dag-graph-reset", "重置视图");
|
|
57
172
|
toolbar.appendChild(reset);
|
|
58
173
|
const appendBeforeViewport = (child) => {
|
|
@@ -60,143 +175,237 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
60
175
|
else wrap.appendChild(child);
|
|
61
176
|
};
|
|
62
177
|
appendBeforeViewport(toolbar);
|
|
63
|
-
if (edges.length === 0) {
|
|
64
|
-
appendBeforeViewport(
|
|
65
|
-
el(
|
|
66
|
-
"p",
|
|
67
|
-
"dag-graph-fallback",
|
|
68
|
-
"该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。",
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
178
|
if (!viewport) {
|
|
73
179
|
viewport = el("div", "dag-graph-viewport");
|
|
74
180
|
}
|
|
75
181
|
viewport.dataset.dagRunId = dagRunId;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
markerHeight: 7,
|
|
92
|
-
orient: "auto-start-reverse",
|
|
93
|
-
});
|
|
94
|
-
marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
95
|
-
defs.appendChild(marker);
|
|
96
|
-
const controlMarker = svgEl("marker", {
|
|
97
|
-
id: "dag-arrow-control",
|
|
98
|
-
viewBox: "0 0 10 10",
|
|
99
|
-
refX: 9,
|
|
100
|
-
refY: 5,
|
|
101
|
-
markerWidth: 7,
|
|
102
|
-
markerHeight: 7,
|
|
103
|
-
orient: "auto-start-reverse",
|
|
104
|
-
});
|
|
105
|
-
controlMarker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
106
|
-
defs.appendChild(controlMarker);
|
|
107
|
-
svg.appendChild(defs);
|
|
108
|
-
const related = new Set([uiState.selectedDagNodeId]);
|
|
109
|
-
for (const edge of layout.edges)
|
|
110
|
-
if (
|
|
111
|
-
edge.from === uiState.selectedDagNodeId ||
|
|
112
|
-
edge.to === uiState.selectedDagNodeId
|
|
113
|
-
) {
|
|
114
|
-
related.add(edge.from);
|
|
115
|
-
related.add(edge.to);
|
|
182
|
+
|
|
183
|
+
if (mode === "trajectory") {
|
|
184
|
+
// 执行轨迹:lane storyboard / 空态 / 警示横幅 / 纵向降级列表。
|
|
185
|
+
renderTrajectoryStoryboard(dag, dagRunId, viewport, () =>
|
|
186
|
+
renderDagGraph(dag, dagRunId, wrap),
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
if (edges.length === 0) {
|
|
190
|
+
appendBeforeViewport(
|
|
191
|
+
el(
|
|
192
|
+
"p",
|
|
193
|
+
"dag-graph-fallback",
|
|
194
|
+
"该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。",
|
|
195
|
+
),
|
|
196
|
+
);
|
|
116
197
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
198
|
+
const svg = svgEl("svg", {
|
|
199
|
+
class: "dag-graph",
|
|
200
|
+
viewBox: `0 0 ${layout.width} ${layout.height}`,
|
|
201
|
+
width: layout.width,
|
|
202
|
+
height: layout.height,
|
|
203
|
+
role: "img",
|
|
204
|
+
"aria-label": `DAG 依赖图,共 ${nodes.length} 个节点`,
|
|
122
205
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const to = layoutByNode.get(ctrl.to);
|
|
133
|
-
if (!from || !to) continue;
|
|
134
|
-
const x1 = from.x;
|
|
135
|
-
const y1 = from.y + from.height / 2;
|
|
136
|
-
const x2 = to.x + to.width;
|
|
137
|
-
const y2 = to.y + to.height / 2;
|
|
138
|
-
const lift = Math.max(48, (from.x - to.x) / 2);
|
|
139
|
-
const rise = Math.max(40, Math.abs(from.y - to.y) / 2 + 16);
|
|
140
|
-
const path = svgEl("path", {
|
|
141
|
-
d: `M ${x1} ${y1} C ${x1 - lift} ${y1 - rise}, ${x2 + lift} ${y2 - rise}, ${x2} ${y2}`,
|
|
142
|
-
class: "dag-edge dag-edge-control",
|
|
143
|
-
"marker-end": "url(#dag-arrow-control)",
|
|
206
|
+
const defs = svgEl("defs");
|
|
207
|
+
const marker = svgEl("marker", {
|
|
208
|
+
id: "dag-arrow",
|
|
209
|
+
viewBox: "0 0 10 10",
|
|
210
|
+
refX: 9,
|
|
211
|
+
refY: 5,
|
|
212
|
+
markerWidth: 7,
|
|
213
|
+
markerHeight: 7,
|
|
214
|
+
orient: "auto-start-reverse",
|
|
144
215
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class: `dag-graph-node status-${status}${node.nodeId === uiState.selectedDagNodeId ? " selected" : ""}${uiState.selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`,
|
|
156
|
-
tabindex: 0,
|
|
157
|
-
role: "button",
|
|
158
|
-
"aria-label": `${node.nodeId},状态 ${status}`,
|
|
216
|
+
marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
217
|
+
defs.appendChild(marker);
|
|
218
|
+
const controlMarker = svgEl("marker", {
|
|
219
|
+
id: "dag-arrow-control",
|
|
220
|
+
viewBox: "0 0 10 10",
|
|
221
|
+
refX: 9,
|
|
222
|
+
refY: 5,
|
|
223
|
+
markerWidth: 7,
|
|
224
|
+
markerHeight: 7,
|
|
225
|
+
orient: "auto-start-reverse",
|
|
159
226
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
227
|
+
controlMarker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
228
|
+
defs.appendChild(controlMarker);
|
|
229
|
+
svg.appendChild(defs);
|
|
230
|
+
const related = new Set([uiState.selectedDagNodeId]);
|
|
231
|
+
for (const edge of layout.edges)
|
|
232
|
+
if (
|
|
233
|
+
edge.from === uiState.selectedDagNodeId ||
|
|
234
|
+
edge.to === uiState.selectedDagNodeId
|
|
235
|
+
) {
|
|
236
|
+
related.add(edge.from);
|
|
237
|
+
related.add(edge.to);
|
|
238
|
+
}
|
|
239
|
+
for (const edge of layout.edges) {
|
|
240
|
+
const path = svgEl("path", {
|
|
241
|
+
d: edge.path,
|
|
242
|
+
class: `dag-edge${uiState.selectedDagNodeId && (!related.has(edge.from) || !related.has(edge.to)) ? " dag-dimmed" : ""}`,
|
|
243
|
+
"marker-end": "url(#dag-arrow)",
|
|
244
|
+
});
|
|
245
|
+
path.appendChild(svgEl("title"));
|
|
246
|
+
path.firstChild.textContent = `${edge.from} → ${edge.to}`;
|
|
247
|
+
svg.appendChild(path);
|
|
176
248
|
}
|
|
177
|
-
|
|
178
|
-
|
|
249
|
+
// 控制回环:仅由 read model 派生(dag.controlEdges),不进拓扑。
|
|
250
|
+
// 独立顶部 lane + rounded-orthogonal;标签锚定顶部水平段中部。
|
|
251
|
+
const layoutByNode = new Map(layout.nodes.map((n) => [n.nodeId, n]));
|
|
252
|
+
const assignedControl = assignControlLanes(controlEdgeList, layoutByNode);
|
|
253
|
+
const laneMetrics = controlLaneMetrics(assignedControl.length);
|
|
254
|
+
let minX = 0;
|
|
255
|
+
let maxX = layout.width;
|
|
256
|
+
let maxY = layout.height;
|
|
257
|
+
const controlDraw = [];
|
|
258
|
+
for (const ctrl of assignedControl) {
|
|
259
|
+
const from = layoutByNode.get(ctrl.from);
|
|
260
|
+
const to = layoutByNode.get(ctrl.to);
|
|
261
|
+
if (!from || !to) continue;
|
|
262
|
+
const routed = routeControlEdge(from, to, ctrl.laneIndex, {
|
|
263
|
+
controlLaneHeight: laneMetrics.laneHeight,
|
|
264
|
+
controlLaneTop: laneMetrics.laneTop,
|
|
265
|
+
});
|
|
266
|
+
const collides = edgeCollidesNonEndpointNodes(
|
|
267
|
+
routed.points,
|
|
268
|
+
layout.nodes,
|
|
269
|
+
ctrl.from,
|
|
270
|
+
ctrl.to,
|
|
271
|
+
);
|
|
272
|
+
for (const p of routed.points) {
|
|
273
|
+
minX = Math.min(minX, p.x);
|
|
274
|
+
maxX = Math.max(maxX, p.x + 24);
|
|
275
|
+
maxY = Math.max(maxY, p.y + 24);
|
|
276
|
+
}
|
|
277
|
+
const passRange = controlPassRangeLabel(dag);
|
|
278
|
+
const titleText = `${ctrl.from} → ${ctrl.to} (control loop)${passRange ? ` ${passRange}` : ""}${collides ? " [routing-collision]" : ""}`;
|
|
279
|
+
// Prefer router-provided top-lane midpoint so the label is not on a corner.
|
|
280
|
+
const labelX =
|
|
281
|
+
Number.isFinite(routed.labelX)
|
|
282
|
+
? routed.labelX
|
|
283
|
+
: (from.x + to.x + to.width) / 2;
|
|
284
|
+
const labelY =
|
|
285
|
+
Number.isFinite(routed.labelY) ? routed.labelY : routed.laneY - 8;
|
|
286
|
+
controlDraw.push({
|
|
287
|
+
path: routed.path,
|
|
288
|
+
titleText,
|
|
289
|
+
labelX,
|
|
290
|
+
labelY,
|
|
291
|
+
labelText: `修复回环 ×${controlLoopCount(dag)}`,
|
|
292
|
+
});
|
|
179
293
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
294
|
+
const originX = minX < 0 ? minX - 16 : 0;
|
|
295
|
+
const viewWidth = Math.max(layout.width, maxX) - originX;
|
|
296
|
+
const viewHeight = Math.max(layout.height, maxY);
|
|
297
|
+
svg.setAttribute(
|
|
298
|
+
"viewBox",
|
|
299
|
+
`${originX} 0 ${viewWidth} ${viewHeight}`,
|
|
300
|
+
);
|
|
301
|
+
svg.setAttribute("width", String(viewWidth));
|
|
302
|
+
svg.setAttribute("height", String(viewHeight));
|
|
303
|
+
for (const item of controlDraw) {
|
|
304
|
+
const path = svgEl("path", {
|
|
305
|
+
d: item.path,
|
|
306
|
+
class: "dag-edge dag-edge-control",
|
|
307
|
+
"marker-end": "url(#dag-arrow-control)",
|
|
308
|
+
});
|
|
309
|
+
path.appendChild(svgEl("title"));
|
|
310
|
+
path.firstChild.textContent = item.titleText;
|
|
311
|
+
svg.appendChild(path);
|
|
312
|
+
const text = svgEl("text", {
|
|
313
|
+
x: item.labelX,
|
|
314
|
+
y: item.labelY,
|
|
315
|
+
class: "dag-edge-control-label",
|
|
316
|
+
"text-anchor": "middle",
|
|
317
|
+
});
|
|
318
|
+
text.textContent = item.labelText;
|
|
319
|
+
const title = svgEl("title");
|
|
320
|
+
title.textContent = item.titleText;
|
|
321
|
+
text.appendChild(title);
|
|
322
|
+
svg.appendChild(text);
|
|
323
|
+
}
|
|
324
|
+
const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
|
|
325
|
+
for (const position of layout.nodes) {
|
|
326
|
+
const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
|
|
327
|
+
const status = (node.status ?? "unknown").toLowerCase();
|
|
328
|
+
// 仅有叙事价值的历史才上卡(多 attempt / 多 pass);"1 次" / 单 P1 不展示。
|
|
329
|
+
const historyLabel =
|
|
330
|
+
typeof node.executionSummary?.historyLabel === "string" &&
|
|
331
|
+
node.executionSummary.historyLabel.trim()
|
|
332
|
+
? node.executionSummary.historyLabel.trim()
|
|
333
|
+
: "";
|
|
334
|
+
// 状态行只保留「状态 · 耗时」;P*/attempt 细节放 history 行或 title,避免溢出节点宽。
|
|
335
|
+
const statusLine = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`;
|
|
336
|
+
// 节点宽 216、左右 padding≈14:按显示列宽截断(CJK 双宽)。
|
|
337
|
+
const maxDisplayWidth = Math.max(10, Math.floor((position.width - 28) / 7));
|
|
338
|
+
const group = svgEl("g", {
|
|
339
|
+
transform: `translate(${position.x} ${position.y})`,
|
|
340
|
+
class: `dag-graph-node status-${status}${node.nodeId === uiState.selectedDagNodeId ? " selected" : ""}${uiState.selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`,
|
|
341
|
+
tabindex: 0,
|
|
342
|
+
role: "button",
|
|
343
|
+
"aria-label": `${node.nodeId},状态 ${status}${historyLabel ? `,执行历史 ${historyLabel}` : ""}`,
|
|
344
|
+
});
|
|
345
|
+
group.appendChild(
|
|
346
|
+
svgEl("rect", { width: position.width, height: position.height, rx: 10 }),
|
|
347
|
+
);
|
|
348
|
+
const title = svgEl("title");
|
|
349
|
+
const passHint =
|
|
350
|
+
Array.isArray(node.passes) && node.passes.length > 0
|
|
351
|
+
? `\nconvergence pass: P${node.passes.join(",")}`
|
|
352
|
+
: "";
|
|
353
|
+
const attemptHint =
|
|
354
|
+
node.attemptCount > 0 ? `\nattempts: ${node.attemptCount}` : "";
|
|
355
|
+
title.textContent = `${node.nodeId}\n${node.executor ?? "未知执行器"}\n${status}${historyLabel ? `\n执行历史:${historyLabel}` : ""}${passHint}${attemptHint}`;
|
|
356
|
+
group.appendChild(title);
|
|
357
|
+
const label = svgEl("text", {
|
|
358
|
+
x: 14,
|
|
359
|
+
y: NODE_TEXT.labelY,
|
|
360
|
+
class: "dag-node-label",
|
|
361
|
+
});
|
|
362
|
+
label.textContent = truncateDisplayText(
|
|
363
|
+
node.label ?? node.nodeId,
|
|
364
|
+
maxDisplayWidth,
|
|
365
|
+
);
|
|
366
|
+
group.appendChild(label);
|
|
367
|
+
const executor = svgEl("text", {
|
|
368
|
+
x: 14,
|
|
369
|
+
y: NODE_TEXT.executorY,
|
|
370
|
+
class: "dag-node-meta",
|
|
371
|
+
});
|
|
372
|
+
executor.textContent = truncateDisplayText(
|
|
373
|
+
`◈ ${node.executor ?? "unknown"}`,
|
|
374
|
+
maxDisplayWidth,
|
|
375
|
+
);
|
|
376
|
+
group.appendChild(executor);
|
|
377
|
+
const state = svgEl("text", {
|
|
378
|
+
x: 14,
|
|
379
|
+
y: NODE_TEXT.statusY,
|
|
380
|
+
class: "dag-node-status",
|
|
381
|
+
});
|
|
382
|
+
state.textContent = truncateDisplayText(statusLine, maxDisplayWidth);
|
|
383
|
+
group.appendChild(state);
|
|
384
|
+
if (historyLabel) {
|
|
385
|
+
const history = svgEl("text", {
|
|
386
|
+
x: 14,
|
|
387
|
+
y: NODE_TEXT.historyY,
|
|
388
|
+
class: "dag-node-history",
|
|
389
|
+
});
|
|
390
|
+
history.textContent = truncateDisplayText(historyLabel, maxDisplayWidth);
|
|
391
|
+
group.appendChild(history);
|
|
188
392
|
}
|
|
189
|
-
|
|
190
|
-
|
|
393
|
+
const activate = () => selectDagNode(dagRunId, node.nodeId);
|
|
394
|
+
group.addEventListener("click", activate);
|
|
395
|
+
group.addEventListener("keydown", (event) => {
|
|
396
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
activate();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
svg.appendChild(group);
|
|
402
|
+
}
|
|
403
|
+
viewport.appendChild(svg);
|
|
191
404
|
}
|
|
192
|
-
viewport.appendChild(svg);
|
|
193
405
|
if (viewport.parentNode !== wrap) wrap.appendChild(viewport);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
? uiState.dagGraphViewportState
|
|
198
|
-
: null);
|
|
199
|
-
// Bind scroll capture once per viewport instance so reuse does not stack listeners.
|
|
406
|
+
// Bind scroll capture once per viewport instance so reuse does not stack
|
|
407
|
+
// listeners. Capture routes to the mode-specific viewport state map so the
|
|
408
|
+
// two views keep independent positions (AC-001 / 交互 reset-current-mode).
|
|
200
409
|
if (viewport.dataset.scrollBound !== "1") {
|
|
201
410
|
viewport.dataset.scrollBound = "1";
|
|
202
411
|
viewport.addEventListener(
|
|
@@ -204,39 +413,90 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
204
413
|
() => {
|
|
205
414
|
const id = viewport.dataset.dagRunId;
|
|
206
415
|
if (!id) return;
|
|
207
|
-
|
|
208
|
-
uiState.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
416
|
+
if (currentDagGraphViewMode(id) === "trajectory") {
|
|
417
|
+
uiState.dagTrajectoryViewportState = updateDagTrajectoryViewportState(
|
|
418
|
+
uiState.dagTrajectoryViewportState,
|
|
419
|
+
id,
|
|
420
|
+
viewport.scrollLeft,
|
|
421
|
+
viewport.scrollTop,
|
|
422
|
+
);
|
|
423
|
+
dagTrajectoryViewportStates.set(
|
|
424
|
+
id,
|
|
425
|
+
uiState.dagTrajectoryViewportState,
|
|
426
|
+
);
|
|
427
|
+
} else {
|
|
428
|
+
uiState.dagGraphViewportState = updateDagGraphViewportState(
|
|
429
|
+
uiState.dagGraphViewportState,
|
|
430
|
+
id,
|
|
431
|
+
viewport.scrollLeft,
|
|
432
|
+
viewport.scrollTop,
|
|
433
|
+
);
|
|
434
|
+
dagGraphViewportStates.set(id, uiState.dagGraphViewportState);
|
|
435
|
+
}
|
|
214
436
|
},
|
|
215
437
|
{ passive: true },
|
|
216
438
|
);
|
|
217
439
|
}
|
|
218
|
-
if (
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
440
|
+
if (mode === "trajectory") {
|
|
441
|
+
const trajectoryViewportState =
|
|
442
|
+
dagTrajectoryViewportStates.get(dagRunId) ??
|
|
443
|
+
(uiState.dagTrajectoryViewportState?.dagRunId === dagRunId
|
|
444
|
+
? uiState.dagTrajectoryViewportState
|
|
445
|
+
: null);
|
|
446
|
+
if (trajectoryViewportState) {
|
|
447
|
+
const restoreViewport = () => {
|
|
448
|
+
if (viewport.dataset.dagRunId !== dagRunId) return;
|
|
449
|
+
viewport.scrollLeft = trajectoryViewportState.scrollLeft;
|
|
450
|
+
viewport.scrollTop = trajectoryViewportState.scrollTop;
|
|
451
|
+
};
|
|
452
|
+
restoreViewport();
|
|
453
|
+
if (typeof requestAnimationFrame === "function") {
|
|
454
|
+
requestAnimationFrame(restoreViewport);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
const graphViewportState =
|
|
459
|
+
dagGraphViewportStates.get(dagRunId) ??
|
|
460
|
+
(uiState.dagGraphViewportState?.dagRunId === dagRunId
|
|
461
|
+
? uiState.dagGraphViewportState
|
|
462
|
+
: null);
|
|
463
|
+
if (graphViewportState) {
|
|
464
|
+
const restoreViewport = () => {
|
|
465
|
+
if (viewport.dataset.dagRunId !== dagRunId) return;
|
|
466
|
+
viewport.scrollLeft = graphViewportState.scrollLeft;
|
|
467
|
+
viewport.scrollTop = graphViewportState.scrollTop;
|
|
468
|
+
};
|
|
469
|
+
// Apply immediately so identity reuse keeps position even before paint,
|
|
470
|
+
// then again on rAF after SVG layout settles.
|
|
471
|
+
restoreViewport();
|
|
472
|
+
if (typeof requestAnimationFrame === "function") {
|
|
473
|
+
requestAnimationFrame(restoreViewport);
|
|
474
|
+
}
|
|
229
475
|
}
|
|
230
476
|
}
|
|
231
477
|
// Toolbar is rebuilt each paint; bind reset to the current viewport/dagRunId.
|
|
478
|
+
// 重置视图只重置当前模式的 viewport:不切换模式、不重置另一模式、不关闭 Inspector。
|
|
232
479
|
reset.addEventListener("click", () => {
|
|
233
|
-
|
|
234
|
-
uiState.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
480
|
+
if (mode === "trajectory") {
|
|
481
|
+
uiState.dagTrajectoryViewportState = updateDagTrajectoryViewportState(
|
|
482
|
+
uiState.dagTrajectoryViewportState,
|
|
483
|
+
dagRunId,
|
|
484
|
+
0,
|
|
485
|
+
0,
|
|
486
|
+
);
|
|
487
|
+
dagTrajectoryViewportStates.set(
|
|
488
|
+
dagRunId,
|
|
489
|
+
uiState.dagTrajectoryViewportState,
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
uiState.dagGraphViewportState = updateDagGraphViewportState(
|
|
493
|
+
uiState.dagGraphViewportState,
|
|
494
|
+
dagRunId,
|
|
495
|
+
0,
|
|
496
|
+
0,
|
|
497
|
+
);
|
|
498
|
+
dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
|
|
499
|
+
}
|
|
240
500
|
if (typeof viewport.scrollTo === "function") {
|
|
241
501
|
viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
|
|
242
502
|
} else {
|