@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,472 +1,356 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* - finalize 流式消息为正式 AI 气泡
|
|
8
|
-
* - 折叠 think / environment_details 块
|
|
9
|
-
* - 复制 / 重新回答 / 蒸馏为判断 按钮
|
|
10
|
-
* - 挂载 step-timeline (2026-06-15) 步骤状态条到每条 AI 消息内
|
|
11
|
-
*
|
|
12
|
-
* 状态 (本模块私有):
|
|
13
|
-
* - streamingMessageEl / streamingTextNode / streamingText
|
|
14
|
-
* - lastUserCommand / lastAiContent (去重)
|
|
15
|
-
*
|
|
16
|
-
* 依赖 (import):
|
|
17
|
-
* - 浏览器 API (document, marked, fetch, navigator)
|
|
18
|
-
* - ./step-timeline (4 状态步骤条模块)
|
|
19
|
-
*
|
|
20
|
-
* 不依赖 (零业务 import, 防循环):
|
|
21
|
-
* - 不 import client.js
|
|
22
|
-
* - 不 import 任何业务模块
|
|
23
|
-
*
|
|
24
|
-
* 输入 (从 client.js 调用):
|
|
25
|
-
* - addMessage(content, type, save, container, usedIds, ctx)
|
|
26
|
-
* - handleStreamTokenEvent({ streamType, content }, ctx)
|
|
27
|
-
* - finalizeTimelineAsMessage(ctx)
|
|
28
|
-
* - handleStepEvent({ type: 'step_start'|'step_done'|'step_error', ... }, ctx)
|
|
29
|
-
* - escapeHtml(s)
|
|
30
|
-
* - getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl)
|
|
31
|
-
*
|
|
32
|
-
* 输出 (DOM):
|
|
33
|
-
* - .message / .bubble / .think-container / .env-container
|
|
34
|
-
* - .message-streaming (流式期间, finalize 时移除)
|
|
35
|
-
* - .message-actions (复制/重新回答/蒸馏按钮)
|
|
36
|
-
* - .used-judgments-link (P0.5 反向引用)
|
|
37
|
-
* - .step-timeline (气泡内步骤状态条, 由 ./step-timeline 渲染)
|
|
38
|
-
*/
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// 类型定义
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
import { mountStepTimeline, pushStepToTimeline, migrateStepTimeline, getStepTimeline, } from './step-timeline.js';
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// 模块私有状态
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
1
|
+
import {
|
|
2
|
+
mountStepTimeline,
|
|
3
|
+
pushStepToTimeline,
|
|
4
|
+
migrateStepTimeline,
|
|
5
|
+
getStepTimeline
|
|
6
|
+
} from "./step-timeline.js";
|
|
46
7
|
let streamingMessageEl = null;
|
|
47
8
|
let streamingTextNode = null;
|
|
48
|
-
let streamingText =
|
|
49
|
-
let lastUserCommand =
|
|
50
|
-
let lastAiContent =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* 避免和后续 `done` → finalizeTimelineAsMessage 产生双气泡.
|
|
54
|
-
* 流式过程中 (streamingText > 0) server 推 `ai(content=fullResponse)` 时跳过,
|
|
55
|
-
* 真正渲染走 `done` 触发的 finalizeTimelineAsMessage.
|
|
56
|
-
*/
|
|
57
|
-
export function hasStreamingText() {
|
|
58
|
-
return streamingText.length > 0;
|
|
9
|
+
let streamingText = "";
|
|
10
|
+
let lastUserCommand = "";
|
|
11
|
+
let lastAiContent = "";
|
|
12
|
+
function hasStreamingText() {
|
|
13
|
+
return streamingText.length > 0;
|
|
59
14
|
}
|
|
60
|
-
// 滚动限频 (60ms 16fps, 减 reflow)
|
|
61
15
|
let scrollToBottomTimer = null;
|
|
62
16
|
function scheduleScrollToBottom(container) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
scrollToBottomTimer =
|
|
68
|
-
|
|
69
|
-
scrollToBottomTimer = null;
|
|
70
|
-
}, 60);
|
|
17
|
+
if (!container) return;
|
|
18
|
+
if (scrollToBottomTimer) return;
|
|
19
|
+
scrollToBottomTimer = setTimeout(() => {
|
|
20
|
+
container.scrollTop = container.scrollHeight;
|
|
21
|
+
scrollToBottomTimer = null;
|
|
22
|
+
}, 60);
|
|
71
23
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return messagesContainers.get(currentChannelId) || null;
|
|
78
|
-
}
|
|
79
|
-
return messagesEl;
|
|
24
|
+
function getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl) {
|
|
25
|
+
if (currentChannelId && messagesContainers.get(currentChannelId)) {
|
|
26
|
+
return messagesContainers.get(currentChannelId) || null;
|
|
27
|
+
}
|
|
28
|
+
return messagesEl;
|
|
80
29
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
30
|
+
function escapeHtml(s) {
|
|
31
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
32
|
+
"&": "&",
|
|
33
|
+
"<": "<",
|
|
34
|
+
">": ">",
|
|
35
|
+
'"': """,
|
|
36
|
+
"'": "'"
|
|
37
|
+
})[c]);
|
|
88
38
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const toRemove = msgContainer.children.length - 200;
|
|
100
|
-
for (let i = 0; i < toRemove; i++) {
|
|
101
|
-
const first = msgContainer.firstElementChild;
|
|
102
|
-
if (first)
|
|
103
|
-
msgContainer.removeChild(first);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// 去重 (save=true 时)
|
|
107
|
-
if (save) {
|
|
108
|
-
const lastContent = type === 'user' ? lastUserCommand : lastAiContent;
|
|
109
|
-
if (lastContent && content === lastContent) {
|
|
110
|
-
console.log(`[addMessage] 跳过重复的 ${type} 消息`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (type === 'user')
|
|
114
|
-
lastUserCommand = content;
|
|
115
|
-
else
|
|
116
|
-
lastAiContent = content;
|
|
117
|
-
}
|
|
118
|
-
const div = document.createElement('div');
|
|
119
|
-
div.className = `message message-${type}`;
|
|
120
|
-
// 清理: 移除 tool_call 标记
|
|
121
|
-
let cleanContent = content
|
|
122
|
-
.replace(/\[TOOL_CALL\][\s\S]*?\[\/TOOL_CALL\]/g, '')
|
|
123
|
-
.replace(/TOOL_CALL[\s\S]*?\/TOOL_CALL/g, '')
|
|
124
|
-
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
125
|
-
.replace(/\{\s*"tool":[\s\S]*?\}/g, '')
|
|
126
|
-
.replace(/\{\s*tool\s*=>\s*["'][^"']+["']\s*(?:,\s*args\s*=>\s*\{[\s\S]*?\})?\s*\}/g, '')
|
|
127
|
-
.replace(/\[Function[^\]]*\]\s*/g, '')
|
|
128
|
-
.trim();
|
|
129
|
-
// think 折叠
|
|
130
|
-
const thinkMatch = cleanContent.match(/<think>([\s\S]*?)<\/think>/);
|
|
131
|
-
let mainContent = cleanContent;
|
|
132
|
-
let thinkContainer = null;
|
|
133
|
-
if (thinkMatch) {
|
|
134
|
-
const thinkContent = thinkMatch[1].trim();
|
|
135
|
-
mainContent = cleanContent.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
|
136
|
-
thinkContainer = buildThinkContainer(thinkContent);
|
|
137
|
-
}
|
|
138
|
-
// environment_details 折叠 + 身份头
|
|
139
|
-
const envMatch = mainContent.match(/^(.+?)\n<environment_details>([\s\S]*?)<\/environment_details>\n([\s\S]*)$/);
|
|
140
|
-
if (envMatch) {
|
|
141
|
-
const identity = envMatch[1].trim();
|
|
142
|
-
const envDetails = envMatch[2].trim();
|
|
143
|
-
const messageBody = envMatch[3].trim();
|
|
144
|
-
const header = document.createElement('div');
|
|
145
|
-
header.className = 'message-header';
|
|
146
|
-
header.textContent = identity;
|
|
147
|
-
div.appendChild(header);
|
|
148
|
-
div.appendChild(buildEnvContainer(envDetails));
|
|
149
|
-
if (thinkContainer)
|
|
150
|
-
div.appendChild(thinkContainer);
|
|
151
|
-
if (messageBody)
|
|
152
|
-
div.appendChild(buildBubble(messageBody, type));
|
|
153
|
-
}
|
|
154
|
-
else if (cleanContent) {
|
|
155
|
-
if (thinkContainer)
|
|
156
|
-
div.appendChild(thinkContainer);
|
|
157
|
-
div.appendChild(buildBubble(cleanContent, type));
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
return; // 空内容, 不上屏
|
|
161
|
-
}
|
|
162
|
-
// 纯文本用于复制按钮
|
|
163
|
-
const rawContent = cleanContent
|
|
164
|
-
.replace(/<[^>]+>/g, '')
|
|
165
|
-
.replace(/</g, '<')
|
|
166
|
-
.replace(/>/g, '>')
|
|
167
|
-
.replace(/&/g, '&')
|
|
168
|
-
.replace(/ /g, ' ');
|
|
169
|
-
// 时间
|
|
170
|
-
const time = document.createElement('div');
|
|
171
|
-
time.className = 'time';
|
|
172
|
-
time.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
173
|
-
// AI 消息操作按钮
|
|
174
|
-
if (type === 'ai') {
|
|
175
|
-
div.appendChild(buildMessageActions(div, rawContent, ctx));
|
|
39
|
+
function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
|
|
40
|
+
const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
|
|
41
|
+
const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
|
|
42
|
+
const currentChannelId = ctx.currentChannelId;
|
|
43
|
+
const msgContainer = container || getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
44
|
+
if (!save && msgContainer && msgContainer.children.length > 200) {
|
|
45
|
+
const toRemove = msgContainer.children.length - 200;
|
|
46
|
+
for (let i = 0; i < toRemove; i++) {
|
|
47
|
+
const first = msgContainer.firstElementChild;
|
|
48
|
+
if (first) msgContainer.removeChild(first);
|
|
176
49
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
e.preventDefault();
|
|
184
|
-
if (typeof ctx.openJudgmentsModalWithFilter === 'function') {
|
|
185
|
-
ctx.openJudgmentsModalWithFilter(usedJudgmentIds);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
div.appendChild(link);
|
|
189
|
-
}
|
|
190
|
-
// 2026-06-15: 每条 AI 消息挂一个空 step-timeline 占位 (用户决策)
|
|
191
|
-
// 后续 step_start/step_done 事件会通过 handleStepEvent 推入
|
|
192
|
-
if (type === 'ai' && msgContainer) {
|
|
193
|
-
mountStepTimeline(div, currentChannelId);
|
|
194
|
-
}
|
|
195
|
-
div.appendChild(time);
|
|
196
|
-
if (msgContainer) {
|
|
197
|
-
msgContainer.appendChild(div);
|
|
198
|
-
scheduleScrollToBottom(msgContainer);
|
|
50
|
+
}
|
|
51
|
+
if (save) {
|
|
52
|
+
const lastContent = type === "user" ? lastUserCommand : lastAiContent;
|
|
53
|
+
if (lastContent && content === lastContent) {
|
|
54
|
+
console.log(`[addMessage] \u8DF3\u8FC7\u91CD\u590D\u7684 ${type} \u6D88\u606F`);
|
|
55
|
+
return;
|
|
199
56
|
}
|
|
57
|
+
if (type === "user") lastUserCommand = content;
|
|
58
|
+
else lastAiContent = content;
|
|
59
|
+
}
|
|
60
|
+
const div = document.createElement("div");
|
|
61
|
+
div.className = `message message-${type}`;
|
|
62
|
+
let cleanContent = content.replace(/\[TOOL_CALL\][\s\S]*?\[\/TOOL_CALL\]/g, "").replace(/TOOL_CALL[\s\S]*?\/TOOL_CALL/g, "").replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, "").replace(/\{\s*"tool":[\s\S]*?\}/g, "").replace(/\{\s*tool\s*=>\s*["'][^"']+["']\s*(?:,\s*args\s*=>\s*\{[\s\S]*?\})?\s*\}/g, "").replace(/\[Function[^\]]*\]\s*/g, "").trim();
|
|
63
|
+
const thinkMatch = cleanContent.match(/<think>([\s\S]*?)<\/think>/);
|
|
64
|
+
let mainContent = cleanContent;
|
|
65
|
+
let thinkContainer = null;
|
|
66
|
+
if (thinkMatch) {
|
|
67
|
+
const thinkContent = thinkMatch[1].trim();
|
|
68
|
+
mainContent = cleanContent.replace(/<think>[\s\S]*?<\/think>/, "").trim();
|
|
69
|
+
thinkContainer = buildThinkContainer(thinkContent);
|
|
70
|
+
}
|
|
71
|
+
const envMatch = mainContent.match(/^(.+?)\n<environment_details>([\s\S]*?)<\/environment_details>\n([\s\S]*)$/);
|
|
72
|
+
if (envMatch) {
|
|
73
|
+
const identity = envMatch[1].trim();
|
|
74
|
+
const envDetails = envMatch[2].trim();
|
|
75
|
+
const messageBody = envMatch[3].trim();
|
|
76
|
+
const header = document.createElement("div");
|
|
77
|
+
header.className = "message-header";
|
|
78
|
+
header.textContent = identity;
|
|
79
|
+
div.appendChild(header);
|
|
80
|
+
div.appendChild(buildEnvContainer(envDetails));
|
|
81
|
+
if (thinkContainer) div.appendChild(thinkContainer);
|
|
82
|
+
if (messageBody) div.appendChild(buildBubble(messageBody, type));
|
|
83
|
+
} else if (cleanContent) {
|
|
84
|
+
if (thinkContainer) div.appendChild(thinkContainer);
|
|
85
|
+
div.appendChild(buildBubble(cleanContent, type));
|
|
86
|
+
} else {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const rawContent = cleanContent.replace(/<[^>]+>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/ /g, " ");
|
|
90
|
+
const time = document.createElement("div");
|
|
91
|
+
time.className = "time";
|
|
92
|
+
time.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
|
93
|
+
if (type === "ai") {
|
|
94
|
+
div.appendChild(buildMessageActions(div, rawContent, ctx));
|
|
95
|
+
}
|
|
96
|
+
if (type === "ai" && Array.isArray(usedJudgmentIds) && usedJudgmentIds.length > 0) {
|
|
97
|
+
const link = document.createElement("a");
|
|
98
|
+
link.className = "used-judgments-link";
|
|
99
|
+
link.textContent = `\u{1F4CE} \u53C2\u8003 ${usedJudgmentIds.length} \u6761\u539F\u5219`;
|
|
100
|
+
link.onclick = (e) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
if (typeof ctx.openJudgmentsModalWithFilter === "function") {
|
|
103
|
+
ctx.openJudgmentsModalWithFilter(usedJudgmentIds);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
div.appendChild(link);
|
|
107
|
+
}
|
|
108
|
+
if (type === "ai" && msgContainer) {
|
|
109
|
+
mountStepTimeline(div, currentChannelId);
|
|
110
|
+
}
|
|
111
|
+
div.appendChild(time);
|
|
112
|
+
if (msgContainer) {
|
|
113
|
+
msgContainer.appendChild(div);
|
|
114
|
+
scheduleScrollToBottom(msgContainer);
|
|
115
|
+
}
|
|
200
116
|
}
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
// 私有: think 折叠块
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
117
|
function buildThinkContainer(thinkContent) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
container.appendChild(content);
|
|
230
|
-
return container;
|
|
118
|
+
const container = document.createElement("div");
|
|
119
|
+
container.className = "think-container";
|
|
120
|
+
const toggle = document.createElement("div");
|
|
121
|
+
toggle.className = "think-toggle";
|
|
122
|
+
toggle.innerHTML = '\u{1F4AD} \u601D\u8003\u8FC7\u7A0B <span class="think-arrow">\u25B8</span>';
|
|
123
|
+
toggle.onclick = function() {
|
|
124
|
+
const details = container.querySelector(".think-content");
|
|
125
|
+
const arrow = toggle.querySelector(".think-arrow");
|
|
126
|
+
if (!details || !arrow) return;
|
|
127
|
+
if (details.style.display === "none") {
|
|
128
|
+
details.style.display = "block";
|
|
129
|
+
arrow.textContent = "\u25BE";
|
|
130
|
+
} else {
|
|
131
|
+
details.style.display = "none";
|
|
132
|
+
arrow.textContent = "\u25B8";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const content = document.createElement("div");
|
|
136
|
+
content.className = "think-content";
|
|
137
|
+
content.style.display = "none";
|
|
138
|
+
content.innerHTML = `<pre>${escapeHtml(thinkContent)}</pre>`;
|
|
139
|
+
container.appendChild(toggle);
|
|
140
|
+
container.appendChild(content);
|
|
141
|
+
return container;
|
|
231
142
|
}
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
// 私有: environment_details 折叠块
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
143
|
function buildEnvContainer(envDetails) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
container.appendChild(content);
|
|
261
|
-
return container;
|
|
144
|
+
const container = document.createElement("div");
|
|
145
|
+
container.className = "env-container";
|
|
146
|
+
const toggle = document.createElement("div");
|
|
147
|
+
toggle.className = "env-toggle";
|
|
148
|
+
toggle.innerHTML = '\u2699\uFE0F \u73AF\u5883\u4FE1\u606F <span class="env-arrow">\u25B8</span>';
|
|
149
|
+
toggle.onclick = function() {
|
|
150
|
+
const details = container.querySelector(".environment-details");
|
|
151
|
+
const arrow = toggle.querySelector(".env-arrow");
|
|
152
|
+
if (!details || !arrow) return;
|
|
153
|
+
if (details.style.display === "none") {
|
|
154
|
+
details.style.display = "block";
|
|
155
|
+
arrow.textContent = "\u25BE";
|
|
156
|
+
} else {
|
|
157
|
+
details.style.display = "none";
|
|
158
|
+
arrow.textContent = "\u25B8";
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const content = document.createElement("div");
|
|
162
|
+
content.className = "environment-details";
|
|
163
|
+
content.style.display = "none";
|
|
164
|
+
content.innerHTML = `<pre>${escapeHtml(envDetails)}</pre>`;
|
|
165
|
+
container.appendChild(toggle);
|
|
166
|
+
container.appendChild(content);
|
|
167
|
+
return container;
|
|
262
168
|
}
|
|
263
|
-
// ---------------------------------------------------------------------------
|
|
264
|
-
// 私有: 气泡 (marked.parse 渲染)
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
169
|
function buildBubble(text, type) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
bubble.innerHTML = marked ? marked.parse(text) : escapeHtml(text);
|
|
273
|
-
return bubble;
|
|
170
|
+
const bubble = document.createElement("div");
|
|
171
|
+
bubble.className = `bubble bubble-${type}`;
|
|
172
|
+
const marked = window.marked;
|
|
173
|
+
bubble.innerHTML = marked ? marked.parse(text) : escapeHtml(text);
|
|
174
|
+
return bubble;
|
|
274
175
|
}
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// 私有: 消息操作按钮 (复制 / 重新回答 / 蒸馏为判断)
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
176
|
function buildMessageActions(div, rawContent, ctx) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
regenBtn.
|
|
307
|
-
regenBtn.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
lastUserMsg = bubble.textContent || bubble.innerText || '';
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
177
|
+
const actions = document.createElement("div");
|
|
178
|
+
actions.className = "message-actions";
|
|
179
|
+
const copyBtn = document.createElement("button");
|
|
180
|
+
copyBtn.className = "action-btn copy-btn";
|
|
181
|
+
copyBtn.innerHTML = copyIcon() + " \u590D\u5236";
|
|
182
|
+
copyBtn.title = "\u590D\u5236\u6D88\u606F";
|
|
183
|
+
copyBtn.onclick = () => {
|
|
184
|
+
navigator.clipboard.writeText(rawContent).then(() => {
|
|
185
|
+
copyBtn.innerHTML = checkIcon() + " \u5DF2\u590D\u5236";
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
copyBtn.innerHTML = copyIcon() + " \u590D\u5236";
|
|
188
|
+
}, 2e3);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
actions.appendChild(copyBtn);
|
|
192
|
+
const saveJudgmentBtn = document.createElement("button");
|
|
193
|
+
saveJudgmentBtn.className = "action-btn save-as-judgment";
|
|
194
|
+
saveJudgmentBtn.title = "AI \u84B8\u998F\u4E3A 30-80 \u5B57\u5224\u65AD\u529B + \u81EA\u52A8\u6F14\u5316\u5BF9\u9F50";
|
|
195
|
+
saveJudgmentBtn.setAttribute("data-decision", rawContent.substring(0, 800));
|
|
196
|
+
if (ctx.currentChannelId) saveJudgmentBtn.setAttribute("data-channel-id", ctx.currentChannelId);
|
|
197
|
+
saveJudgmentBtn.innerHTML = shieldIcon() + " \u84B8\u998F\u4E3A\u5224\u65AD";
|
|
198
|
+
actions.appendChild(saveJudgmentBtn);
|
|
199
|
+
const regenBtn = document.createElement("button");
|
|
200
|
+
regenBtn.className = "action-btn regenerate-btn";
|
|
201
|
+
regenBtn.innerHTML = refreshIcon(false) + " \u91CD\u65B0\u56DE\u7B54";
|
|
202
|
+
regenBtn.title = "\u91CD\u65B0\u751F\u6210\u56DE\u590D";
|
|
203
|
+
regenBtn.onclick = () => {
|
|
204
|
+
regenBtn.innerHTML = refreshIcon(true) + " \u751F\u6210\u4E2D...";
|
|
205
|
+
regenBtn.disabled = true;
|
|
206
|
+
const messages = div.parentElement?.querySelectorAll(".message") || [];
|
|
207
|
+
let lastUserMsg = "";
|
|
208
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
209
|
+
const msg = messages[i];
|
|
210
|
+
if (msg.classList.contains("message-user")) {
|
|
211
|
+
const bubble = msg.querySelector(".bubble");
|
|
212
|
+
if (bubble) {
|
|
213
|
+
lastUserMsg = bubble.textContent || bubble.innerText || "";
|
|
214
|
+
break;
|
|
321
215
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
fetch("/regenerate", {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: { "Content-Type": "application/json" },
|
|
221
|
+
body: JSON.stringify({ channelId: ctx.currentChannelId, userMessage: lastUserMsg })
|
|
222
|
+
}).then((res) => {
|
|
223
|
+
if (!res.ok) throw new Error("regenerate failed");
|
|
224
|
+
}).catch((err) => {
|
|
225
|
+
console.error("\u91CD\u65B0\u751F\u6210\u5931\u8D25:", err);
|
|
226
|
+
regenBtn.innerHTML = refreshIcon(false) + " \u5931\u8D25";
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
regenBtn.innerHTML = refreshIcon(false) + " \u91CD\u65B0\u56DE\u7B54";
|
|
229
|
+
regenBtn.disabled = false;
|
|
230
|
+
}, 2e3);
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
actions.appendChild(regenBtn);
|
|
234
|
+
return actions;
|
|
340
235
|
}
|
|
341
|
-
function copyIcon() {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
scheduleScrollToBottom(container);
|
|
236
|
+
function copyIcon() {
|
|
237
|
+
return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
|
238
|
+
}
|
|
239
|
+
function checkIcon() {
|
|
240
|
+
return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
|
241
|
+
}
|
|
242
|
+
function shieldIcon() {
|
|
243
|
+
return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path><path d="M9 12l2 2 4-4"></path></svg>';
|
|
244
|
+
}
|
|
245
|
+
function refreshIcon(spin = false) {
|
|
246
|
+
const cls = spin ? ' class="spin"' : "";
|
|
247
|
+
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"${cls}><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg>`;
|
|
248
|
+
}
|
|
249
|
+
function handleStreamTokenEvent(data, ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
|
|
250
|
+
const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
|
|
251
|
+
const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
|
|
252
|
+
const currentChannelId = ctx.currentChannelId;
|
|
253
|
+
const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
254
|
+
if (!container) return;
|
|
255
|
+
const delta = data.content || "";
|
|
256
|
+
if (!delta) return;
|
|
257
|
+
if (!streamingMessageEl || !streamingMessageEl.isConnected) {
|
|
258
|
+
streamingMessageEl = document.createElement("div");
|
|
259
|
+
streamingMessageEl.className = "message message-ai message-streaming";
|
|
260
|
+
streamingTextNode = document.createTextNode("");
|
|
261
|
+
streamingMessageEl.appendChild(streamingTextNode);
|
|
262
|
+
streamingText = "";
|
|
263
|
+
mountStepTimeline(streamingMessageEl, currentChannelId);
|
|
264
|
+
container.appendChild(streamingMessageEl);
|
|
265
|
+
if (typeof ctx.setTimelineState === "function") {
|
|
266
|
+
ctx.setTimelineState("streaming");
|
|
374
267
|
}
|
|
375
|
-
if (streamingTextNode)
|
|
376
|
-
streamingTextNode.appendData(delta);
|
|
377
|
-
streamingText += delta;
|
|
378
268
|
scheduleScrollToBottom(container);
|
|
269
|
+
}
|
|
270
|
+
if (streamingTextNode) streamingTextNode.appendData(delta);
|
|
271
|
+
streamingText += delta;
|
|
272
|
+
scheduleScrollToBottom(container);
|
|
379
273
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// 2026-06-15: finalize 时把 streaming 内的 step-timeline 整体搬到新建的正式 AI message 内
|
|
390
|
-
// addMessage 会建一个新 timeline 占位, 先记下 streaming 的引用, addMessage 后再迁移
|
|
391
|
-
// 避免节点从 0 重渲 (10+ 步的任务, 重渲闪烁会很厉害)
|
|
392
|
-
const oldStreamingEl = streamingMessageEl;
|
|
393
|
-
if (oldStreamingEl && oldStreamingEl.parentNode) {
|
|
394
|
-
oldStreamingEl.parentNode.removeChild(oldStreamingEl);
|
|
395
|
-
}
|
|
396
|
-
addMessage(streamingText, 'ai', true, container, ctx.lastUsedJudgmentIds || [], ctx);
|
|
397
|
-
if (oldStreamingEl && container) {
|
|
398
|
-
// 找刚 addMessage 创建的最后一条 ai message
|
|
399
|
-
const newAiMsg = container.querySelector('.message-ai:last-of-type');
|
|
400
|
-
if (newAiMsg && newAiMsg !== oldStreamingEl) {
|
|
401
|
-
migrateStepTimeline(oldStreamingEl, newAiMsg);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
274
|
+
function finalizeTimelineAsMessage(ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
|
|
275
|
+
const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
|
|
276
|
+
const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
|
|
277
|
+
const currentChannelId = ctx.currentChannelId;
|
|
278
|
+
const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
279
|
+
if (streamingText.trim().length > 0) {
|
|
280
|
+
const oldStreamingEl = streamingMessageEl;
|
|
281
|
+
if (oldStreamingEl && oldStreamingEl.parentNode) {
|
|
282
|
+
oldStreamingEl.parentNode.removeChild(oldStreamingEl);
|
|
404
283
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
ctx.setTimelineState('done');
|
|
284
|
+
addMessage(streamingText, "ai", true, container, ctx.lastUsedJudgmentIds || [], ctx);
|
|
285
|
+
if (oldStreamingEl && container) {
|
|
286
|
+
const newAiMsg = container.querySelector(".message-ai:last-of-type");
|
|
287
|
+
if (newAiMsg && newAiMsg !== oldStreamingEl) {
|
|
288
|
+
migrateStepTimeline(oldStreamingEl, newAiMsg);
|
|
289
|
+
}
|
|
412
290
|
}
|
|
291
|
+
}
|
|
292
|
+
streamingMessageEl = null;
|
|
293
|
+
streamingTextNode = null;
|
|
294
|
+
streamingText = "";
|
|
295
|
+
if (typeof ctx.setTimelineState === "function") {
|
|
296
|
+
ctx.setTimelineState("done");
|
|
297
|
+
}
|
|
413
298
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (!timeline)
|
|
438
|
-
return;
|
|
439
|
-
pushStepToTimeline(timeline, data.type, {
|
|
440
|
-
tool: data.tool || 'unknown',
|
|
441
|
-
args: data.args,
|
|
442
|
-
success: data.success,
|
|
443
|
-
output: data.output,
|
|
444
|
-
error: data.error,
|
|
445
|
-
});
|
|
299
|
+
function handleStepEvent(data, ctx = { messagesEl: null, messagesContainers: /* @__PURE__ */ new Map(), currentChannelId: null }) {
|
|
300
|
+
const messagesEl = ctx.messagesEl || (typeof document !== "undefined" ? document.getElementById("messages") : null);
|
|
301
|
+
const messagesContainers = ctx.messagesContainers || /* @__PURE__ */ new Map();
|
|
302
|
+
const currentChannelId = ctx.currentChannelId;
|
|
303
|
+
const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
304
|
+
if (!container) return;
|
|
305
|
+
if (!data || !data.type) return;
|
|
306
|
+
let target = streamingMessageEl && streamingMessageEl.isConnected ? streamingMessageEl : null;
|
|
307
|
+
if (!target) {
|
|
308
|
+
const aiMsgs = container.querySelectorAll(".message-ai");
|
|
309
|
+
if (aiMsgs.length === 0) return;
|
|
310
|
+
target = aiMsgs[aiMsgs.length - 1];
|
|
311
|
+
}
|
|
312
|
+
if (!target) return;
|
|
313
|
+
const timeline = getStepTimeline(target);
|
|
314
|
+
if (!timeline) return;
|
|
315
|
+
pushStepToTimeline(timeline, data.type, {
|
|
316
|
+
tool: data.tool || "unknown",
|
|
317
|
+
args: data.args,
|
|
318
|
+
success: data.success,
|
|
319
|
+
output: data.output,
|
|
320
|
+
error: data.error
|
|
321
|
+
});
|
|
446
322
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
clearTimeout(scrollToBottomTimer);
|
|
458
|
-
scrollToBottomTimer = null;
|
|
459
|
-
}
|
|
323
|
+
function resetRendererState() {
|
|
324
|
+
streamingMessageEl = null;
|
|
325
|
+
streamingTextNode = null;
|
|
326
|
+
streamingText = "";
|
|
327
|
+
lastUserCommand = "";
|
|
328
|
+
lastAiContent = "";
|
|
329
|
+
if (scrollToBottomTimer) {
|
|
330
|
+
clearTimeout(scrollToBottomTimer);
|
|
331
|
+
scrollToBottomTimer = null;
|
|
332
|
+
}
|
|
460
333
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
334
|
+
const MessageRenderer = {
|
|
335
|
+
addMessage,
|
|
336
|
+
handleStreamTokenEvent,
|
|
337
|
+
finalizeTimelineAsMessage,
|
|
338
|
+
handleStepEvent,
|
|
339
|
+
escapeHtml,
|
|
340
|
+
getMessagesContainerForCurrent,
|
|
341
|
+
resetRendererState
|
|
469
342
|
};
|
|
470
|
-
if (typeof window !==
|
|
471
|
-
|
|
343
|
+
if (typeof window !== "undefined") {
|
|
344
|
+
window.MR = MessageRenderer;
|
|
472
345
|
}
|
|
346
|
+
export {
|
|
347
|
+
MessageRenderer,
|
|
348
|
+
addMessage,
|
|
349
|
+
escapeHtml,
|
|
350
|
+
finalizeTimelineAsMessage,
|
|
351
|
+
getMessagesContainerForCurrent,
|
|
352
|
+
handleStepEvent,
|
|
353
|
+
handleStreamTokenEvent,
|
|
354
|
+
hasStreamingText,
|
|
355
|
+
resetRendererState
|
|
356
|
+
};
|