@bolloon/bolloon-agent 0.1.40 → 0.1.41
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/dist/web/client.js +2876 -3770
- package/dist/web/components/p2p/index.js +226 -264
- package/dist/web/server.js +24 -13
- package/dist/web/ui/message-renderer.js +326 -442
- package/dist/web/ui/step-timeline.js +255 -351
- package/package.json +1 -1
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -657
- package/dist/web/components/p2p/p2p-tools.js +0 -248
|
@@ -1,375 +1,279 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* step-timeline.ts — 气泡内 4 状态步骤条 (2026-06-15)
|
|
3
|
-
*
|
|
4
|
-
* 职责 (单一):
|
|
5
|
-
* - 给每条 AI 消息挂一个可折叠/展开的步骤条
|
|
6
|
-
* - 状态机: idle ○ / active ● / done 🟢 / error 🔴
|
|
7
|
-
* - 摘要条 + 详情区共用同一份 Step[] 数据
|
|
8
|
-
* - 流式期间在 streaming 元素内, finalize 时搬到正式 message
|
|
9
|
-
* - 折叠状态按 channel 持久化 (localStorage)
|
|
10
|
-
*
|
|
11
|
-
* 状态 (模块私有, 每个 message 一个实例):
|
|
12
|
-
* - steps: Step[]
|
|
13
|
-
* - currentIndex: number
|
|
14
|
-
* - expanded: boolean (受 localStorage 影响)
|
|
15
|
-
*
|
|
16
|
-
* 输入 (从 message-renderer 调用):
|
|
17
|
-
* - createEmptyStepTimeline(channelId) -> HTMLElement
|
|
18
|
-
* - mountStepTimeline(messageEl, channelId) // 已有 message 上挂 timeline
|
|
19
|
-
* - pushStepToTimeline(timelineEl, type, data) // 订阅 step_start/done/error
|
|
20
|
-
* - migrateStepTimeline(fromEl, toEl) // finalize 时搬家
|
|
21
|
-
*
|
|
22
|
-
* 输出 (DOM):
|
|
23
|
-
* - .step-timeline / .step-timeline-summary / .step-timeline-body
|
|
24
|
-
* - .step-dot (摘要条圆点) / .step-timeline-node (详情区节点)
|
|
25
|
-
*/
|
|
26
1
|
const MAX_VISIBLE_NODES = 8;
|
|
27
|
-
const STORAGE_PREFIX =
|
|
28
|
-
const stateMap = new WeakMap();
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// 工具: 持久化
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
2
|
+
const STORAGE_PREFIX = "bolloon.stepTimeline.expanded.";
|
|
3
|
+
const stateMap = /* @__PURE__ */ new WeakMap();
|
|
32
4
|
function readExpanded(channelId) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
5
|
+
if (!channelId) return false;
|
|
6
|
+
try {
|
|
7
|
+
return localStorage.getItem(STORAGE_PREFIX + channelId) === "1";
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
41
11
|
}
|
|
42
12
|
function writeExpanded(channelId, expanded) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
catch { /* localStorage 写失败 (隐私模式), 静默 */ }
|
|
13
|
+
if (!channelId) return;
|
|
14
|
+
try {
|
|
15
|
+
localStorage.setItem(STORAGE_PREFIX + channelId, expanded ? "1" : "0");
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
49
18
|
}
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// 工具: 摘要条 title
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
19
|
function computeTitle(steps) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return -1;
|
|
66
|
-
})();
|
|
67
|
-
if (lastErrorIdx >= 0)
|
|
68
|
-
return `✗ 失败 · ${steps[lastErrorIdx].name}`;
|
|
69
|
-
return `✓ 已完成 · ${steps.length} 步`;
|
|
20
|
+
if (steps.length === 0) return "\u6267\u884C\u6B65\u9AA4";
|
|
21
|
+
const activeIdx = steps.findIndex((s) => s.status === "active");
|
|
22
|
+
if (activeIdx >= 0) return `\u25CF \u6267\u884C\u4E2D \xB7 ${steps[activeIdx].name}`;
|
|
23
|
+
const lastErrorIdx = (() => {
|
|
24
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
25
|
+
if (steps[i].status === "error") return i;
|
|
26
|
+
}
|
|
27
|
+
return -1;
|
|
28
|
+
})();
|
|
29
|
+
if (lastErrorIdx >= 0) return `\u2717 \u5931\u8D25 \xB7 ${steps[lastErrorIdx].name}`;
|
|
30
|
+
return `\u2713 \u5DF2\u5B8C\u6210 \xB7 ${steps.length} \u6B65`;
|
|
70
31
|
}
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// 核心: 渲染 (摘要条 + 详情区, 共用 steps 状态)
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
32
|
function render(timelineEl) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
dotsEl.appendChild(dot);
|
|
95
|
-
}
|
|
96
|
-
dot.setAttribute('data-status', step.status);
|
|
97
|
-
dot.setAttribute('title', `${step.name} · ${step.status}`);
|
|
98
|
-
}
|
|
99
|
-
while (dotsEl.children.length > steps.length) {
|
|
100
|
-
dotsEl.removeChild(dotsEl.lastChild);
|
|
101
|
-
}
|
|
33
|
+
const state = stateMap.get(timelineEl);
|
|
34
|
+
if (!state) return;
|
|
35
|
+
const { steps, expanded, showAll } = state;
|
|
36
|
+
const titleEl = timelineEl.querySelector("[data-current-tool]");
|
|
37
|
+
if (titleEl) titleEl.textContent = computeTitle(steps);
|
|
38
|
+
const dotsEl = timelineEl.querySelector("[data-dots]");
|
|
39
|
+
if (dotsEl) {
|
|
40
|
+
const existing = Array.from(dotsEl.children);
|
|
41
|
+
for (let i = 0; i < steps.length; i++) {
|
|
42
|
+
const step = steps[i];
|
|
43
|
+
let dot = existing[i];
|
|
44
|
+
if (!dot) {
|
|
45
|
+
dot = document.createElement("span");
|
|
46
|
+
dot.className = "step-dot";
|
|
47
|
+
dot.setAttribute("data-index", String(i));
|
|
48
|
+
dotsEl.appendChild(dot);
|
|
49
|
+
}
|
|
50
|
+
dot.setAttribute("data-status", step.status);
|
|
51
|
+
dot.setAttribute("title", `${step.name} \xB7 ${step.status}`);
|
|
102
52
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (listEl) {
|
|
106
|
-
const hiddenCount = Math.max(0, steps.length - MAX_VISIBLE_NODES);
|
|
107
|
-
const visibleSteps = showAll ? steps : steps.slice(-MAX_VISIBLE_NODES);
|
|
108
|
-
const existing = Array.from(listEl.children);
|
|
109
|
-
let htmlIdx = 0;
|
|
110
|
-
// "+N 更多" 折叠行 (仅在 showAll=false 且有 hidden 时)
|
|
111
|
-
if (!showAll && hiddenCount > 0) {
|
|
112
|
-
let moreBtn = existing[htmlIdx];
|
|
113
|
-
if (!moreBtn || !moreBtn.classList.contains('step-timeline-more')) {
|
|
114
|
-
moreBtn = document.createElement('li');
|
|
115
|
-
moreBtn.className = 'step-timeline-more';
|
|
116
|
-
moreBtn.setAttribute('role', 'button');
|
|
117
|
-
moreBtn.textContent = `+ ${hiddenCount} 更多`;
|
|
118
|
-
moreBtn.onclick = () => {
|
|
119
|
-
state.showAll = true;
|
|
120
|
-
render(timelineEl);
|
|
121
|
-
};
|
|
122
|
-
if (existing[htmlIdx] && existing[htmlIdx] !== moreBtn) {
|
|
123
|
-
listEl.replaceChild(moreBtn, existing[htmlIdx]);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
listEl.appendChild(moreBtn);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
moreBtn.textContent = `+ ${hiddenCount} 更多`;
|
|
131
|
-
}
|
|
132
|
-
htmlIdx++;
|
|
133
|
-
}
|
|
134
|
-
for (let i = 0; i < visibleSteps.length; i++) {
|
|
135
|
-
const step = visibleSteps[i];
|
|
136
|
-
const realIndex = showAll ? i : (steps.length - visibleSteps.length + i);
|
|
137
|
-
let node = existing[htmlIdx];
|
|
138
|
-
if (!node || !node.classList.contains('step-timeline-node')) {
|
|
139
|
-
node = document.createElement('li');
|
|
140
|
-
node.className = 'step-timeline-node';
|
|
141
|
-
node.setAttribute('data-index', String(realIndex));
|
|
142
|
-
const marker = document.createElement('span');
|
|
143
|
-
marker.className = 'step-timeline-marker';
|
|
144
|
-
const label = document.createElement('span');
|
|
145
|
-
label.className = 'step-timeline-label';
|
|
146
|
-
const args = document.createElement('span');
|
|
147
|
-
args.className = 'step-timeline-args';
|
|
148
|
-
node.appendChild(marker);
|
|
149
|
-
node.appendChild(label);
|
|
150
|
-
node.appendChild(args);
|
|
151
|
-
if (existing[htmlIdx] && existing[htmlIdx] !== node) {
|
|
152
|
-
listEl.replaceChild(node, existing[htmlIdx]);
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
listEl.appendChild(node);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
node.setAttribute('data-index', String(realIndex));
|
|
160
|
-
}
|
|
161
|
-
node.setAttribute('data-status', step.status);
|
|
162
|
-
const label = node.querySelector('.step-timeline-label');
|
|
163
|
-
if (label)
|
|
164
|
-
label.textContent = step.name;
|
|
165
|
-
const argsEl = node.querySelector('.step-timeline-args');
|
|
166
|
-
if (argsEl) {
|
|
167
|
-
const argStr = step.args && Object.keys(step.args).length > 0
|
|
168
|
-
? JSON.stringify(step.args).slice(0, 60)
|
|
169
|
-
: '';
|
|
170
|
-
argsEl.textContent = argStr;
|
|
171
|
-
argsEl.style.display = argStr ? '' : 'none';
|
|
172
|
-
}
|
|
173
|
-
htmlIdx++;
|
|
174
|
-
}
|
|
175
|
-
// 清理多余 li
|
|
176
|
-
while (listEl.children.length > htmlIdx) {
|
|
177
|
-
listEl.removeChild(listEl.lastChild);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
// 折叠态: 空状态显示占位, 有节点就保持
|
|
181
|
-
if (steps.length === 0) {
|
|
182
|
-
timelineEl.setAttribute('data-empty', 'true');
|
|
53
|
+
while (dotsEl.children.length > steps.length) {
|
|
54
|
+
dotsEl.removeChild(dotsEl.lastChild);
|
|
183
55
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
56
|
+
}
|
|
57
|
+
const listEl = timelineEl.querySelector("[data-list]");
|
|
58
|
+
if (listEl) {
|
|
59
|
+
const hiddenCount = Math.max(0, steps.length - MAX_VISIBLE_NODES);
|
|
60
|
+
const visibleSteps = showAll ? steps : steps.slice(-MAX_VISIBLE_NODES);
|
|
61
|
+
const existing = Array.from(listEl.children);
|
|
62
|
+
let htmlIdx = 0;
|
|
63
|
+
if (!showAll && hiddenCount > 0) {
|
|
64
|
+
let moreBtn = existing[htmlIdx];
|
|
65
|
+
if (!moreBtn || !moreBtn.classList.contains("step-timeline-more")) {
|
|
66
|
+
moreBtn = document.createElement("li");
|
|
67
|
+
moreBtn.className = "step-timeline-more";
|
|
68
|
+
moreBtn.setAttribute("role", "button");
|
|
69
|
+
moreBtn.textContent = `+ ${hiddenCount} \u66F4\u591A`;
|
|
70
|
+
moreBtn.onclick = () => {
|
|
71
|
+
state.showAll = true;
|
|
72
|
+
render(timelineEl);
|
|
73
|
+
};
|
|
74
|
+
if (existing[htmlIdx] && existing[htmlIdx] !== moreBtn) {
|
|
75
|
+
listEl.replaceChild(moreBtn, existing[htmlIdx]);
|
|
76
|
+
} else {
|
|
77
|
+
listEl.appendChild(moreBtn);
|
|
197
78
|
}
|
|
198
|
-
|
|
199
|
-
|
|
79
|
+
} else {
|
|
80
|
+
moreBtn.textContent = `+ ${hiddenCount} \u66F4\u591A`;
|
|
81
|
+
}
|
|
82
|
+
htmlIdx++;
|
|
83
|
+
}
|
|
84
|
+
for (let i = 0; i < visibleSteps.length; i++) {
|
|
85
|
+
const step = visibleSteps[i];
|
|
86
|
+
const realIndex = showAll ? i : steps.length - visibleSteps.length + i;
|
|
87
|
+
let node = existing[htmlIdx];
|
|
88
|
+
if (!node || !node.classList.contains("step-timeline-node")) {
|
|
89
|
+
node = document.createElement("li");
|
|
90
|
+
node.className = "step-timeline-node";
|
|
91
|
+
node.setAttribute("data-index", String(realIndex));
|
|
92
|
+
const marker = document.createElement("span");
|
|
93
|
+
marker.className = "step-timeline-marker";
|
|
94
|
+
const label2 = document.createElement("span");
|
|
95
|
+
label2.className = "step-timeline-label";
|
|
96
|
+
const args = document.createElement("span");
|
|
97
|
+
args.className = "step-timeline-args";
|
|
98
|
+
node.appendChild(marker);
|
|
99
|
+
node.appendChild(label2);
|
|
100
|
+
node.appendChild(args);
|
|
101
|
+
if (existing[htmlIdx] && existing[htmlIdx] !== node) {
|
|
102
|
+
listEl.replaceChild(node, existing[htmlIdx]);
|
|
103
|
+
} else {
|
|
104
|
+
listEl.appendChild(node);
|
|
200
105
|
}
|
|
106
|
+
} else {
|
|
107
|
+
node.setAttribute("data-index", String(realIndex));
|
|
108
|
+
}
|
|
109
|
+
node.setAttribute("data-status", step.status);
|
|
110
|
+
const label = node.querySelector(".step-timeline-label");
|
|
111
|
+
if (label) label.textContent = step.name;
|
|
112
|
+
const argsEl = node.querySelector(".step-timeline-args");
|
|
113
|
+
if (argsEl) {
|
|
114
|
+
const argStr = step.args && Object.keys(step.args).length > 0 ? JSON.stringify(step.args).slice(0, 60) : "";
|
|
115
|
+
argsEl.textContent = argStr;
|
|
116
|
+
argsEl.style.display = argStr ? "" : "none";
|
|
117
|
+
}
|
|
118
|
+
htmlIdx++;
|
|
119
|
+
}
|
|
120
|
+
while (listEl.children.length > htmlIdx) {
|
|
121
|
+
listEl.removeChild(listEl.lastChild);
|
|
201
122
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
123
|
+
}
|
|
124
|
+
if (steps.length === 0) {
|
|
125
|
+
timelineEl.setAttribute("data-empty", "true");
|
|
126
|
+
} else {
|
|
127
|
+
timelineEl.removeAttribute("data-empty");
|
|
128
|
+
}
|
|
129
|
+
const body = timelineEl.querySelector("[data-body]");
|
|
130
|
+
if (body) {
|
|
131
|
+
if (expanded) {
|
|
132
|
+
body.style.maxHeight = body.scrollHeight + "px";
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
if (state.expanded) body.style.maxHeight = "";
|
|
135
|
+
}, 300);
|
|
136
|
+
} else {
|
|
137
|
+
body.style.maxHeight = "0";
|
|
208
138
|
}
|
|
139
|
+
}
|
|
140
|
+
const arrow = timelineEl.querySelector(".step-timeline-arrow");
|
|
141
|
+
if (arrow) arrow.style.transform = expanded ? "rotate(180deg)" : "rotate(0deg)";
|
|
142
|
+
if (expanded && listEl) {
|
|
143
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
144
|
+
}
|
|
209
145
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const state = stateMap.get(root);
|
|
239
|
-
if (!state)
|
|
240
|
-
return;
|
|
241
|
-
state.expanded = !state.expanded;
|
|
242
|
-
writeExpanded(state.channelId, state.expanded);
|
|
243
|
-
render(root);
|
|
244
|
-
};
|
|
245
|
-
// 详情区
|
|
246
|
-
const body = document.createElement('div');
|
|
247
|
-
body.className = 'step-timeline-body';
|
|
248
|
-
body.setAttribute('data-body', '');
|
|
249
|
-
const list = document.createElement('ul');
|
|
250
|
-
list.className = 'step-timeline-list';
|
|
251
|
-
list.setAttribute('data-list', '');
|
|
252
|
-
body.appendChild(list);
|
|
253
|
-
root.appendChild(summary);
|
|
254
|
-
root.appendChild(body);
|
|
255
|
-
// 初始折叠态
|
|
256
|
-
const initialExpanded = readExpanded(channelId);
|
|
257
|
-
stateMap.set(root, {
|
|
258
|
-
steps: [],
|
|
259
|
-
currentIndex: -1,
|
|
260
|
-
expanded: initialExpanded,
|
|
261
|
-
channelId: channelId || null,
|
|
262
|
-
showAll: false,
|
|
263
|
-
});
|
|
146
|
+
function createEmptyStepTimeline(channelId = null) {
|
|
147
|
+
const root = document.createElement("div");
|
|
148
|
+
root.className = "step-timeline";
|
|
149
|
+
root.setAttribute("data-step-timeline", "");
|
|
150
|
+
root.setAttribute("data-empty", "true");
|
|
151
|
+
const summary = document.createElement("div");
|
|
152
|
+
summary.className = "step-timeline-summary";
|
|
153
|
+
summary.setAttribute("data-summary", "");
|
|
154
|
+
const titleSpan = document.createElement("span");
|
|
155
|
+
titleSpan.className = "step-timeline-title";
|
|
156
|
+
const titleContent = document.createElement("span");
|
|
157
|
+
titleContent.setAttribute("data-current-tool", "");
|
|
158
|
+
titleContent.textContent = "\u6267\u884C\u6B65\u9AA4";
|
|
159
|
+
titleSpan.appendChild(titleContent);
|
|
160
|
+
const dots = document.createElement("div");
|
|
161
|
+
dots.className = "step-timeline-dots";
|
|
162
|
+
dots.setAttribute("data-dots", "");
|
|
163
|
+
const arrow = document.createElement("span");
|
|
164
|
+
arrow.className = "step-timeline-arrow";
|
|
165
|
+
arrow.textContent = "\u25BE";
|
|
166
|
+
summary.appendChild(titleSpan);
|
|
167
|
+
summary.appendChild(dots);
|
|
168
|
+
summary.appendChild(arrow);
|
|
169
|
+
summary.onclick = () => {
|
|
170
|
+
const state = stateMap.get(root);
|
|
171
|
+
if (!state) return;
|
|
172
|
+
state.expanded = !state.expanded;
|
|
173
|
+
writeExpanded(state.channelId, state.expanded);
|
|
264
174
|
render(root);
|
|
265
|
-
|
|
175
|
+
};
|
|
176
|
+
const body = document.createElement("div");
|
|
177
|
+
body.className = "step-timeline-body";
|
|
178
|
+
body.setAttribute("data-body", "");
|
|
179
|
+
const list = document.createElement("ul");
|
|
180
|
+
list.className = "step-timeline-list";
|
|
181
|
+
list.setAttribute("data-list", "");
|
|
182
|
+
body.appendChild(list);
|
|
183
|
+
root.appendChild(summary);
|
|
184
|
+
root.appendChild(body);
|
|
185
|
+
const initialExpanded = readExpanded(channelId);
|
|
186
|
+
stateMap.set(root, {
|
|
187
|
+
steps: [],
|
|
188
|
+
currentIndex: -1,
|
|
189
|
+
expanded: initialExpanded,
|
|
190
|
+
channelId: channelId || null,
|
|
191
|
+
showAll: false
|
|
192
|
+
});
|
|
193
|
+
render(root);
|
|
194
|
+
return root;
|
|
266
195
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
else if (bubble && bubble.parentNode === messageEl) {
|
|
287
|
-
bubble.parentNode.insertBefore(timeline, bubble.nextSibling);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
messageEl.appendChild(timeline);
|
|
291
|
-
}
|
|
292
|
-
return timeline;
|
|
196
|
+
function mountStepTimeline(messageEl, channelId) {
|
|
197
|
+
const existing = messageEl.querySelector("[data-step-timeline]");
|
|
198
|
+
if (existing) {
|
|
199
|
+
const state = stateMap.get(existing);
|
|
200
|
+
if (state && channelId && !state.channelId) state.channelId = channelId;
|
|
201
|
+
return existing;
|
|
202
|
+
}
|
|
203
|
+
const bubble = messageEl.querySelector(".bubble");
|
|
204
|
+
const actions = messageEl.querySelector(".message-actions");
|
|
205
|
+
const timeline = createEmptyStepTimeline(channelId);
|
|
206
|
+
if (actions && actions.parentNode === messageEl) {
|
|
207
|
+
messageEl.insertBefore(timeline, actions);
|
|
208
|
+
} else if (bubble && bubble.parentNode === messageEl) {
|
|
209
|
+
bubble.parentNode.insertBefore(timeline, bubble.nextSibling);
|
|
210
|
+
} else {
|
|
211
|
+
messageEl.appendChild(timeline);
|
|
212
|
+
}
|
|
213
|
+
return timeline;
|
|
293
214
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// 优先匹配 currentIndex, 其次回退到最后一个 active
|
|
314
|
-
let targetIdx = state.currentIndex;
|
|
315
|
-
if (targetIdx < 0 || !steps[targetIdx] || steps[targetIdx].name !== data.tool) {
|
|
316
|
-
// 兜底: 找最后一个同名 active
|
|
317
|
-
for (let i = steps.length - 1; i >= 0; i--) {
|
|
318
|
-
if (steps[i].name === data.tool && steps[i].status === 'active') {
|
|
319
|
-
targetIdx = i;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (targetIdx < 0) {
|
|
325
|
-
// 没有 active 节点匹配, 当作新 error 节点
|
|
326
|
-
targetIdx = steps.length;
|
|
327
|
-
steps.push({ name: data.tool, status: 'error', eventType });
|
|
328
|
-
}
|
|
329
|
-
if (eventType === 'step_done') {
|
|
330
|
-
steps[targetIdx].status = data.success === false ? 'error' : 'done';
|
|
331
|
-
if (data.output)
|
|
332
|
-
steps[targetIdx].output = data.output;
|
|
333
|
-
if (data.error)
|
|
334
|
-
steps[targetIdx].error = data.error;
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
steps[targetIdx].status = 'error';
|
|
338
|
-
if (data.error)
|
|
339
|
-
steps[targetIdx].error = data.error;
|
|
215
|
+
function pushStepToTimeline(timelineEl, eventType, data) {
|
|
216
|
+
const state = stateMap.get(timelineEl);
|
|
217
|
+
if (!state) return;
|
|
218
|
+
const { steps } = state;
|
|
219
|
+
if (eventType === "step_start") {
|
|
220
|
+
state.currentIndex = steps.length;
|
|
221
|
+
steps.push({
|
|
222
|
+
name: data.tool,
|
|
223
|
+
status: "active",
|
|
224
|
+
args: data.args,
|
|
225
|
+
eventType
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
let targetIdx = state.currentIndex;
|
|
229
|
+
if (targetIdx < 0 || !steps[targetIdx] || steps[targetIdx].name !== data.tool) {
|
|
230
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
231
|
+
if (steps[i].name === data.tool && steps[i].status === "active") {
|
|
232
|
+
targetIdx = i;
|
|
233
|
+
break;
|
|
340
234
|
}
|
|
341
|
-
|
|
342
|
-
state.currentIndex = -1;
|
|
235
|
+
}
|
|
343
236
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// 公共: finalize 时把 timeline 元素从 from 搬到 to (避免重渲)
|
|
348
|
-
// 接受 timelineEl 本身或 fromEl 内部的 timeline
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
export function migrateStepTimeline(fromEl, toEl) {
|
|
351
|
-
const fromTimeline = fromEl.querySelector('[data-step-timeline]');
|
|
352
|
-
const toTimeline = toEl.querySelector('[data-step-timeline]');
|
|
353
|
-
if (!fromTimeline)
|
|
354
|
-
return;
|
|
355
|
-
// 移除 toEl 的占位, 搬 fromEl 的过来
|
|
356
|
-
if (toTimeline && toTimeline !== fromTimeline) {
|
|
357
|
-
toTimeline.remove();
|
|
358
|
-
}
|
|
359
|
-
if (toTimeline !== fromTimeline) {
|
|
360
|
-
toEl.appendChild(fromTimeline);
|
|
237
|
+
if (targetIdx < 0) {
|
|
238
|
+
targetIdx = steps.length;
|
|
239
|
+
steps.push({ name: data.tool, status: "error", eventType });
|
|
361
240
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
241
|
+
if (eventType === "step_done") {
|
|
242
|
+
steps[targetIdx].status = data.success === false ? "error" : "done";
|
|
243
|
+
if (data.output) steps[targetIdx].output = data.output;
|
|
244
|
+
if (data.error) steps[targetIdx].error = data.error;
|
|
245
|
+
} else {
|
|
246
|
+
steps[targetIdx].status = "error";
|
|
247
|
+
if (data.error) steps[targetIdx].error = data.error;
|
|
366
248
|
}
|
|
249
|
+
steps[targetIdx].eventType = eventType;
|
|
250
|
+
state.currentIndex = -1;
|
|
251
|
+
}
|
|
252
|
+
render(timelineEl);
|
|
253
|
+
}
|
|
254
|
+
function migrateStepTimeline(fromEl, toEl) {
|
|
255
|
+
const fromTimeline = fromEl.querySelector("[data-step-timeline]");
|
|
256
|
+
const toTimeline = toEl.querySelector("[data-step-timeline]");
|
|
257
|
+
if (!fromTimeline) return;
|
|
258
|
+
if (toTimeline && toTimeline !== fromTimeline) {
|
|
259
|
+
toTimeline.remove();
|
|
260
|
+
}
|
|
261
|
+
if (toTimeline !== fromTimeline) {
|
|
262
|
+
toEl.appendChild(fromTimeline);
|
|
263
|
+
}
|
|
264
|
+
const state = stateMap.get(fromTimeline);
|
|
265
|
+
if (state) {
|
|
266
|
+
render(fromTimeline);
|
|
267
|
+
}
|
|
367
268
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
export function getStepTimeline(messageEl) {
|
|
372
|
-
if (!messageEl)
|
|
373
|
-
return null;
|
|
374
|
-
return messageEl.querySelector('[data-step-timeline]');
|
|
269
|
+
function getStepTimeline(messageEl) {
|
|
270
|
+
if (!messageEl) return null;
|
|
271
|
+
return messageEl.querySelector("[data-step-timeline]");
|
|
375
272
|
}
|
|
273
|
+
export {
|
|
274
|
+
createEmptyStepTimeline,
|
|
275
|
+
getStepTimeline,
|
|
276
|
+
migrateStepTimeline,
|
|
277
|
+
mountStepTimeline,
|
|
278
|
+
pushStepToTimeline
|
|
279
|
+
};
|