myagent-ai 1.10.8 → 1.11.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/main_agent.py +8 -0
- package/main.py +4 -0
- package/package.json +1 -1
- package/web/api_server.py +13 -1
- package/web/ui/chat/chat.css +26 -3
- package/web/ui/chat/chat_main.js +199 -36
- package/web/ui/chat/flow_engine.js +107 -80
package/agents/main_agent.py
CHANGED
|
@@ -1137,6 +1137,10 @@ class MainAgent(BaseAgent):
|
|
|
1137
1137
|
code_lang = params.get("language", "python")
|
|
1138
1138
|
code_text = params.get("code", parms_str)
|
|
1139
1139
|
if self.executor:
|
|
1140
|
+
# 注入权限检查器(V1 路径在 api_server 中设置,V2 路径需要在此设置)
|
|
1141
|
+
self.executor.set_permission_checker(
|
|
1142
|
+
self.check_permission, self.name
|
|
1143
|
+
)
|
|
1140
1144
|
exec_result = await self.executor.execute(
|
|
1141
1145
|
language=code_lang,
|
|
1142
1146
|
code=code_text,
|
|
@@ -1149,6 +1153,10 @@ class MainAgent(BaseAgent):
|
|
|
1149
1153
|
elif tool_name == "command" or tool_name == "command_run":
|
|
1150
1154
|
code_text = params.get("command", parms_str)
|
|
1151
1155
|
if self.executor:
|
|
1156
|
+
# 注入权限检查器(V1 路径在 api_server 中设置,V2 路径需要在此设置)
|
|
1157
|
+
self.executor.set_permission_checker(
|
|
1158
|
+
self.check_permission, self.name
|
|
1159
|
+
)
|
|
1152
1160
|
exec_result = await self.executor.execute(
|
|
1153
1161
|
language="shell",
|
|
1154
1162
|
code=code_text,
|
package/main.py
CHANGED
|
@@ -218,6 +218,10 @@ class MyAgentApp:
|
|
|
218
218
|
max_retries=exe_cfg.max_retries,
|
|
219
219
|
auto_fix=exe_cfg.auto_fix,
|
|
220
220
|
max_output_length=exe_cfg.max_output_length,
|
|
221
|
+
execution_mode=exe_cfg.execution_mode,
|
|
222
|
+
sandbox_image=exe_cfg.sandbox_image,
|
|
223
|
+
sandbox_network=exe_cfg.sandbox_network,
|
|
224
|
+
sandbox_memory=exe_cfg.sandbox_memory,
|
|
221
225
|
)
|
|
222
226
|
self.logger.info(f"执行引擎: timeout={exe_cfg.timeout}s, auto_fix={exe_cfg.auto_fix}")
|
|
223
227
|
|
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -2358,7 +2358,7 @@ class ApiServer:
|
|
|
2358
2358
|
return web.json_response({**agent_info, "sessions": sessions})
|
|
2359
2359
|
|
|
2360
2360
|
# Internal keys that should not appear in chat history UI
|
|
2361
|
-
_HIDDEN_KEYS = {"llm_output"
|
|
2361
|
+
_HIDDEN_KEYS = {"llm_output"}
|
|
2362
2362
|
|
|
2363
2363
|
async def handle_get_messages(self, request):
|
|
2364
2364
|
sid = request.match_info["sid"]
|
|
@@ -3280,6 +3280,15 @@ class ApiServer:
|
|
|
3280
3280
|
context.metadata["agent_override_path"] = agent_path
|
|
3281
3281
|
context.metadata["chat_mode"] = chat_mode
|
|
3282
3282
|
|
|
3283
|
+
# ── 根据 Agent 配置设置执行引擎参数(execution_mode 等)──
|
|
3284
|
+
agent_cfg_for_exec = self._read_agent_config(agent_path)
|
|
3285
|
+
_original_exec_mode = None
|
|
3286
|
+
if agent_cfg_for_exec and agent.executor:
|
|
3287
|
+
_exec_mode = agent_cfg_for_exec.get("execution_mode")
|
|
3288
|
+
if _exec_mode:
|
|
3289
|
+
_original_exec_mode = agent.executor.execution_mode
|
|
3290
|
+
agent.executor.set_execution_mode(_exec_mode)
|
|
3291
|
+
|
|
3283
3292
|
# Clear execution events from previous runs
|
|
3284
3293
|
agent.clear_execution_events()
|
|
3285
3294
|
|
|
@@ -3358,6 +3367,9 @@ class ApiServer:
|
|
|
3358
3367
|
finally:
|
|
3359
3368
|
# 无论成功或异常,都清理 active_contexts
|
|
3360
3369
|
agent.active_contexts.pop(session_id, None)
|
|
3370
|
+
# 恢复执行引擎原始模式(防止影响后续 Agent 请求)
|
|
3371
|
+
if _original_exec_mode is not None and agent.executor:
|
|
3372
|
+
agent.executor.set_execution_mode(_original_exec_mode)
|
|
3361
3373
|
|
|
3362
3374
|
# V2 结束后:如果 task_list_store 中有任务,确保最终推送一次
|
|
3363
3375
|
if chat_mode == "exec" and session_id in self._task_list_store:
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -242,6 +242,9 @@ input,textarea,select{font:inherit}
|
|
|
242
242
|
border-bottom-left-radius:4px;
|
|
243
243
|
max-width:95%;
|
|
244
244
|
}
|
|
245
|
+
/* Unified bubble wrapper for timeline content — always full width */
|
|
246
|
+
.msg-bubble-wrapper{max-width:95%!important;width:100%}
|
|
247
|
+
.msg-bubble-wrapper>.msg-timeline{gap:10px}
|
|
245
248
|
.message-bubble p{margin-bottom:8px}
|
|
246
249
|
.message-bubble p:last-child{margin-bottom:0}
|
|
247
250
|
.message-bubble code{
|
|
@@ -466,7 +469,7 @@ input,textarea,select{font:inherit}
|
|
|
466
469
|
}
|
|
467
470
|
|
|
468
471
|
/* ── Thought Block (Agent Thinking) ── */
|
|
469
|
-
.thought-block{margin:0 0 10px 0;border:1px solid var(--border-light);border-radius:var(--radius-sm);overflow:hidden;background:linear-gradient(135deg,var(--accent-light),var(--bg2));animation:thoughtFadeIn .4s ease-out}
|
|
472
|
+
.thought-block{width:100%;display:block;margin:0 0 10px 0;border:1px solid var(--border-light);border-radius:var(--radius-sm);overflow:hidden;background:linear-gradient(135deg,var(--accent-light),var(--bg2));animation:thoughtFadeIn .4s ease-out}
|
|
470
473
|
.thought-block.streaming{border-color:var(--accent);box-shadow:0 0 12px rgba(99,102,241,.15)}
|
|
471
474
|
@keyframes thoughtFadeIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
|
|
472
475
|
.thought-block summary{display:flex;align-items:center;gap:8px;padding:8px 14px;cursor:pointer;font-size:12px;font-weight:600;color:var(--text2);user-select:none;transition:var(--transition);text-transform:uppercase;letter-spacing:.3px}
|
|
@@ -478,7 +481,12 @@ input,textarea,select{font:inherit}
|
|
|
478
481
|
.thought-block summary .thought-badge{font-size:10px;padding:2px 8px;border-radius:10px;background:var(--accent);color:#fff;font-weight:500;animation:badgePulse 1.2s ease-in-out infinite}
|
|
479
482
|
@keyframes badgePulse{0%,100%{opacity:1}50%{opacity:.6}}
|
|
480
483
|
.thought-block:not(.streaming) summary .thought-badge{background:var(--bg4);color:var(--text3);animation:none}
|
|
481
|
-
.thought-content{padding:10px 14px 14px;font-size:13px;line-height:1.7;color:var(--text2);border-top:1px solid var(--border-light);max-height:300px;overflow-y:auto;overflow-x:hidden;word-break:break-word;overflow-wrap:break-word}
|
|
484
|
+
.thought-content{width:100%;padding:10px 14px 14px;font-size:13px;line-height:1.7;color:var(--text2);border-top:1px solid var(--border-light);max-height:300px;overflow-y:auto;overflow-x:hidden;word-break:break-word;overflow-wrap:break-word}
|
|
485
|
+
/* Reasoning (模型推理过程): limit to ~5 lines, scrollable */
|
|
486
|
+
.thought-block .thought-content.reasoning-content{max-height:calc(1.7em * 5 + 28px);min-height:0}
|
|
487
|
+
.thought-block .thought-content.reasoning-content::-webkit-scrollbar{width:4px}
|
|
488
|
+
.thought-block .thought-content.reasoning-content::-webkit-scrollbar-thumb{background:var(--bg4);border-radius:2px}
|
|
489
|
+
.thought-block .thought-content.reasoning-content::-webkit-scrollbar-track{background:transparent}
|
|
482
490
|
.thought-content p{margin:4px 0}
|
|
483
491
|
.thought-content p:first-child{margin-top:0}
|
|
484
492
|
.thought-content p:last-child{margin-bottom:0}
|
|
@@ -2050,7 +2058,22 @@ input,textarea,select{font:inherit}
|
|
|
2050
2058
|
.exec-event-result-btn svg{width:12px;height:12px}
|
|
2051
2059
|
|
|
2052
2060
|
/* ── Inline Exec Events (Timeline Interleaved) ── */
|
|
2053
|
-
.msg-timeline{display:flex;flex-direction:column;gap:
|
|
2061
|
+
.msg-timeline{display:flex;flex-direction:column;gap:10px;overflow:hidden}
|
|
2062
|
+
/* Text segment inside the unified bubble */
|
|
2063
|
+
.timeline-segment{word-break:break-word;overflow-wrap:break-word}
|
|
2064
|
+
.timeline-segment p{margin-bottom:8px}
|
|
2065
|
+
.timeline-segment p:last-child{margin-bottom:0}
|
|
2066
|
+
.timeline-segment code{background:rgba(0,0,0,.06);padding:2px 6px;border-radius:4px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12.5px}
|
|
2067
|
+
.timeline-segment pre{background:#1e1e2e;color:#cdd6f4;padding:14px 16px;border-radius:var(--radius-sm);overflow-x:auto;margin:8px 0;font-size:12.5px;line-height:1.5;max-width:100%;white-space:pre-wrap;word-break:break-all}
|
|
2068
|
+
.timeline-segment strong{font-weight:600}
|
|
2069
|
+
.timeline-segment em{font-style:italic}
|
|
2070
|
+
.timeline-segment ul,.timeline-segment ol{padding-left:20px;margin:6px 0}
|
|
2071
|
+
.timeline-segment li{margin:3px 0}
|
|
2072
|
+
.timeline-segment blockquote{border-left:3px solid var(--accent);padding-left:12px;color:var(--text2);margin:6px 0}
|
|
2073
|
+
/* Exec events inside a message-bubble need distinct background for contrast */
|
|
2074
|
+
.message-bubble > .msg-timeline > .inline-exec-event{background:var(--bg);border-left-color:var(--accent)}
|
|
2075
|
+
[data-theme="dark"] .message-bubble > .msg-timeline > .inline-exec-event{background:var(--bg)}
|
|
2076
|
+
[data-theme="dark"] .message-bubble > .msg-timeline > .inline-exec-code{background:var(--bg2)}
|
|
2054
2077
|
.inline-exec-event{margin:2px 0;padding:8px 12px;background:var(--bg2);border-left:3px solid var(--border);border-radius:6px;font-size:13px;animation:execEventSlide .3s ease-out}
|
|
2055
2078
|
.inline-exec-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
|
|
2056
2079
|
.inline-exec-icon{font-size:14px}
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -1600,6 +1600,7 @@ async function clearSessionFromMenu(id) {
|
|
|
1600
1600
|
}
|
|
1601
1601
|
|
|
1602
1602
|
function formatSessionName(id) {
|
|
1603
|
+
if (!id) return '新会话';
|
|
1603
1604
|
if (id.startsWith('web_')) return id.replace('web_', '').replace(/_/g, ' ');
|
|
1604
1605
|
if (id.startsWith('cli_')) return 'CLI: ' + id.replace('cli_', '');
|
|
1605
1606
|
// Strip agent prefix if present (e.g., "default_web_2024..." -> "web_2024...")
|
|
@@ -1777,7 +1778,9 @@ async function selectSession(id) {
|
|
|
1777
1778
|
key: m.key || '',
|
|
1778
1779
|
};
|
|
1779
1780
|
});
|
|
1780
|
-
|
|
1781
|
+
// Group consecutive non-user messages into single assistant messages with parts[]
|
|
1782
|
+
// This creates the interleaved speak→tool→speak pattern matching streaming display
|
|
1783
|
+
state.messages = groupHistoryMessages(loaded);
|
|
1781
1784
|
state._msgLoadOffset = loaded.length;
|
|
1782
1785
|
state._msgLoadTotal = loaded.length;
|
|
1783
1786
|
} catch (e) {
|
|
@@ -1838,11 +1841,14 @@ async function loadMoreMessages() {
|
|
|
1838
1841
|
role: m.role || 'assistant',
|
|
1839
1842
|
content: content,
|
|
1840
1843
|
time: m.time || m.created_at || '',
|
|
1844
|
+
key: m.key || '',
|
|
1841
1845
|
};
|
|
1842
1846
|
});
|
|
1843
1847
|
|
|
1848
|
+
// Group consecutive non-user messages for the loaded batch
|
|
1849
|
+
const grouped = groupHistoryMessages(loaded);
|
|
1844
1850
|
// 追加到现有消息前面(保持滚动位置)
|
|
1845
|
-
state.messages =
|
|
1851
|
+
state.messages = grouped.concat(state.messages);
|
|
1846
1852
|
state._msgLoadOffset += loaded.length;
|
|
1847
1853
|
state._msgLoadTotal = state.messages.length;
|
|
1848
1854
|
|
|
@@ -1995,6 +2001,165 @@ async function clearCurrentChat() {
|
|
|
1995
2001
|
}
|
|
1996
2002
|
}
|
|
1997
2003
|
|
|
2004
|
+
// ── Group History Messages ──
|
|
2005
|
+
// Groups consecutive non-user messages (assistant + tool) into single assistant messages
|
|
2006
|
+
// with parts[] for timeline rendering, matching the streaming display format.
|
|
2007
|
+
// This creates: user → [assistant (speak → tool → speak → tool)] → user → ...
|
|
2008
|
+
function groupHistoryMessages(messages) {
|
|
2009
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
2010
|
+
|
|
2011
|
+
const grouped = [];
|
|
2012
|
+
let i = 0;
|
|
2013
|
+
|
|
2014
|
+
while (i < messages.length) {
|
|
2015
|
+
const msg = messages[i];
|
|
2016
|
+
|
|
2017
|
+
if (msg.role === 'user') {
|
|
2018
|
+
// User message: pass through as-is
|
|
2019
|
+
grouped.push({ role: 'user', content: msg.content, time: msg.time || '' });
|
|
2020
|
+
i++;
|
|
2021
|
+
} else if (msg.role === 'assistant') {
|
|
2022
|
+
// Start of a new agent group: collect all consecutive non-user messages
|
|
2023
|
+
const parts = [];
|
|
2024
|
+
let lastAssistantTime = msg.time || '';
|
|
2025
|
+
|
|
2026
|
+
// If assistant has content, add as text part
|
|
2027
|
+
if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
|
|
2028
|
+
parts.push({ type: 'text', content: msg.content });
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
i++; // Move to next message
|
|
2032
|
+
|
|
2033
|
+
// Collect following tool messages
|
|
2034
|
+
while (i < messages.length && messages[i].role === 'tool') {
|
|
2035
|
+
const toolMsg = messages[i];
|
|
2036
|
+
const isResult = toolMsg.key === 'tool_result';
|
|
2037
|
+
const isCall = toolMsg.key === 'tool_call';
|
|
2038
|
+
|
|
2039
|
+
if (isCall) {
|
|
2040
|
+
// Extract tool name from content
|
|
2041
|
+
const toolName = (toolMsg.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
|
|
2042
|
+
parts.push({
|
|
2043
|
+
type: 'exec',
|
|
2044
|
+
data: {
|
|
2045
|
+
id: 'hist_tool_' + i,
|
|
2046
|
+
type: 'tool_call',
|
|
2047
|
+
title: toolMsg.content.substring(0, 100) || ('调用工具: ' + toolName),
|
|
2048
|
+
tool_name: toolName,
|
|
2049
|
+
status: 'done',
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
} else if (isResult) {
|
|
2053
|
+
// Determine success/failure from content
|
|
2054
|
+
const isOk = !toolMsg.content.includes('失败');
|
|
2055
|
+
parts.push({
|
|
2056
|
+
type: 'exec',
|
|
2057
|
+
data: {
|
|
2058
|
+
id: 'hist_tool_' + i,
|
|
2059
|
+
type: 'tool_result',
|
|
2060
|
+
title: (toolMsg.content.substring(0, 80) || '工具执行结果'),
|
|
2061
|
+
success: isOk,
|
|
2062
|
+
summary: toolMsg.content.substring(0, 500),
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
} else {
|
|
2066
|
+
// Generic tool message
|
|
2067
|
+
parts.push({
|
|
2068
|
+
type: 'exec',
|
|
2069
|
+
data: {
|
|
2070
|
+
id: 'hist_tool_' + i,
|
|
2071
|
+
type: 'tool_call',
|
|
2072
|
+
title: toolMsg.content.substring(0, 100) || '工具调用',
|
|
2073
|
+
status: 'done',
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
i++;
|
|
2079
|
+
|
|
2080
|
+
// If next message is an assistant message, add its content as a text part and continue
|
|
2081
|
+
// This handles the pattern: text → tool → text → tool
|
|
2082
|
+
if (i < messages.length && messages[i].role === 'assistant') {
|
|
2083
|
+
const nextAssistant = messages[i];
|
|
2084
|
+
if (nextAssistant.content && nextAssistant.content.trim() && nextAssistant.content !== '(无回复)') {
|
|
2085
|
+
parts.push({ type: 'text', content: nextAssistant.content });
|
|
2086
|
+
lastAssistantTime = nextAssistant.time || lastAssistantTime;
|
|
2087
|
+
}
|
|
2088
|
+
i++;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Create grouped assistant message with parts
|
|
2093
|
+
// Assemble content from text parts for backward compat
|
|
2094
|
+
const textParts = parts.filter(p => p.type === 'text');
|
|
2095
|
+
const assembledContent = textParts.map(p => p.content).join('\n\n');
|
|
2096
|
+
|
|
2097
|
+
grouped.push({
|
|
2098
|
+
role: 'assistant',
|
|
2099
|
+
content: assembledContent || '',
|
|
2100
|
+
time: lastAssistantTime,
|
|
2101
|
+
parts: parts.length > 0 ? parts : undefined,
|
|
2102
|
+
// Also collect exec_events for backward compat display
|
|
2103
|
+
exec_events: parts.filter(p => p.type === 'exec').map(p => p.data),
|
|
2104
|
+
});
|
|
2105
|
+
} else if (msg.role === 'tool') {
|
|
2106
|
+
// Orphan tool message (no preceding assistant) — wrap in an assistant group
|
|
2107
|
+
const parts = [];
|
|
2108
|
+
const isResult = msg.key === 'tool_result';
|
|
2109
|
+
const isCall = msg.key === 'tool_call';
|
|
2110
|
+
|
|
2111
|
+
if (isCall) {
|
|
2112
|
+
const toolName = (msg.content.match(/^调用工具:\s*(\S+)/) || [])[1] || '';
|
|
2113
|
+
parts.push({
|
|
2114
|
+
type: 'exec',
|
|
2115
|
+
data: {
|
|
2116
|
+
id: 'hist_tool_' + i,
|
|
2117
|
+
type: 'tool_call',
|
|
2118
|
+
title: msg.content.substring(0, 100) || ('调用工具: ' + toolName),
|
|
2119
|
+
tool_name: toolName,
|
|
2120
|
+
status: 'done',
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
} else if (isResult) {
|
|
2124
|
+
const isOk = !msg.content.includes('失败');
|
|
2125
|
+
parts.push({
|
|
2126
|
+
type: 'exec',
|
|
2127
|
+
data: {
|
|
2128
|
+
id: 'hist_tool_' + i,
|
|
2129
|
+
type: 'tool_result',
|
|
2130
|
+
title: msg.content.substring(0, 80) || '工具执行结果',
|
|
2131
|
+
success: isOk,
|
|
2132
|
+
summary: msg.content.substring(0, 500),
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
i++;
|
|
2138
|
+
// Check if next is assistant (to include its content)
|
|
2139
|
+
if (i < messages.length && messages[i].role === 'assistant') {
|
|
2140
|
+
const nextAssistant = messages[i];
|
|
2141
|
+
if (nextAssistant.content && nextAssistant.content.trim() && nextAssistant.content !== '(无回复)') {
|
|
2142
|
+
parts.push({ type: 'text', content: nextAssistant.content });
|
|
2143
|
+
}
|
|
2144
|
+
i++;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
grouped.push({
|
|
2148
|
+
role: 'assistant',
|
|
2149
|
+
content: parts.filter(p => p.type === 'text').map(p => p.content).join('\n\n'),
|
|
2150
|
+
time: msg.time || '',
|
|
2151
|
+
parts: parts.length > 0 ? parts : undefined,
|
|
2152
|
+
exec_events: parts.filter(p => p.type === 'exec').map(p => p.data),
|
|
2153
|
+
});
|
|
2154
|
+
} else {
|
|
2155
|
+
// Skip unknown roles
|
|
2156
|
+
i++;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
return grouped;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
1998
2163
|
// ── Messages ──
|
|
1999
2164
|
function renderMessages() {
|
|
2000
2165
|
try {
|
|
@@ -2072,31 +2237,9 @@ function _renderMessagesInner() {
|
|
|
2072
2237
|
for (let i = 0; i < state.messages.length; i++) {
|
|
2073
2238
|
const msg = state.messages[i];
|
|
2074
2239
|
const isUser = msg.role === 'user';
|
|
2075
|
-
const isTool = msg.role === 'tool';
|
|
2076
2240
|
|
|
2077
|
-
//
|
|
2078
|
-
if (
|
|
2079
|
-
const isResult = msg.key === 'tool_result';
|
|
2080
|
-
const isCall = msg.key === 'tool_call';
|
|
2081
|
-
const icon = isResult ? '📋' : (isCall ? '⚙️' : '🔧');
|
|
2082
|
-
const label = isResult ? '工具执行结果' : (isCall ? '工具调用' : '工具调用过程');
|
|
2083
|
-
// 提取工具名称
|
|
2084
|
-
const toolName = msg.content.match(/^调用工具:\s*(\S+)/) ? msg.content.match(/^调用工具:\s*(\S+)/)[1]
|
|
2085
|
-
: msg.content.match(/^\[([^\]]+)\]/) ? msg.content.match(/^\[([^\]]+)\]/)[1] : '';
|
|
2086
|
-
const titleExtra = toolName ? ' — ' + escapeHtml(toolName) : '';
|
|
2087
|
-
// 判断成功/失败
|
|
2088
|
-
const isOk = isResult && !msg.content.includes('失败');
|
|
2089
|
-
const badge = isResult ? `<span class="thought-badge" style="${isOk ? 'background:var(--ok)' : 'background:var(--danger)'}">${isOk ? '成功' : '失败'}</span>` : '';
|
|
2090
|
-
html += `<details class="thought-block">
|
|
2091
|
-
<summary>
|
|
2092
|
-
<span class="thought-icon">${icon}</span>
|
|
2093
|
-
<span class="thought-label">${label}${titleExtra}</span>
|
|
2094
|
-
${badge}
|
|
2095
|
-
</summary>
|
|
2096
|
-
<div class="thought-content"><pre style="white-space:pre-wrap;word-break:break-word;margin:0;font-size:12px;line-height:1.6">${escapeHtml(msg.content)}</pre></div>
|
|
2097
|
-
</details>`;
|
|
2098
|
-
continue;
|
|
2099
|
-
}
|
|
2241
|
+
// Skip standalone tool messages (now grouped into assistant parts via groupHistoryMessages)
|
|
2242
|
+
if (msg.role === 'tool') continue;
|
|
2100
2243
|
|
|
2101
2244
|
const avatar = isUser ? '👤' : botEmoji;
|
|
2102
2245
|
const content = renderMarkdown(msg.content);
|
|
@@ -2119,7 +2262,7 @@ function _renderMessagesInner() {
|
|
|
2119
2262
|
<span class="thought-label">模型推理过程</span>
|
|
2120
2263
|
${isStreaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
|
|
2121
2264
|
</summary>
|
|
2122
|
-
<div class="thought-content">${renderMarkdown(msg.reasoning)}</div>
|
|
2265
|
+
<div class="thought-content reasoning-content">${renderMarkdown(msg.reasoning)}</div>
|
|
2123
2266
|
</details>`;
|
|
2124
2267
|
})() : '';
|
|
2125
2268
|
const actionBtns = (!isUser && msg.content) ? `
|
|
@@ -2147,36 +2290,41 @@ function _renderMessagesInner() {
|
|
|
2147
2290
|
<span style="font-weight:500">Agent 正在思考...</span>
|
|
2148
2291
|
</div>` : '';
|
|
2149
2292
|
|
|
2150
|
-
// ── Timeline rendering
|
|
2293
|
+
// ── Timeline rendering: all parts in ONE unified bubble ──
|
|
2151
2294
|
let timelineHtml = '';
|
|
2152
2295
|
if (hasParts || hasStreamingText) {
|
|
2153
|
-
let
|
|
2296
|
+
let partsInner = '';
|
|
2154
2297
|
for (const part of (msg.parts || [])) {
|
|
2155
2298
|
if (part.type === 'text' && part.content.trim()) {
|
|
2156
|
-
|
|
2299
|
+
partsInner += '<div class="timeline-segment">' + renderMarkdown(part.content) + '</div>';
|
|
2157
2300
|
} else if (part.type === 'exec') {
|
|
2158
|
-
|
|
2301
|
+
partsInner += renderInlineExecEvent(part.data, i);
|
|
2302
|
+
} else if (part.type === 'v2_tool') {
|
|
2303
|
+
partsInner += renderInlineExecEvent(part, i);
|
|
2304
|
+
} else if (part.type === 'v2_ask') {
|
|
2305
|
+
partsInner += '<div class="v2-ask-user"><div class="v2-ask-icon">❓</div><div class="v2-ask-content">' + renderMarkdown(part.data.question) + '</div></div>';
|
|
2159
2306
|
}
|
|
2160
2307
|
}
|
|
2161
2308
|
if (hasStreamingText) {
|
|
2162
2309
|
const _cursor = msg.streaming ? '<span class="streaming-cursor"></span>' : '';
|
|
2163
|
-
|
|
2310
|
+
partsInner += '<div class="timeline-segment">' + renderMarkdown(msg._streamingText) + _cursor + '</div>';
|
|
2164
2311
|
}
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
2312
|
+
if (partsInner) {
|
|
2313
|
+
// All parts (text segments + tool calls) wrapped in ONE message-bubble
|
|
2314
|
+
timelineHtml = '<div class="message-bubble"><div class="msg-timeline">' + partsInner + '</div></div>';
|
|
2167
2315
|
}
|
|
2168
2316
|
}
|
|
2169
2317
|
|
|
2170
2318
|
// Backward compat: single bubble for messages without parts
|
|
2171
2319
|
const singleBubbleHtml = (!hasParts && !hasStreamingText)
|
|
2172
|
-
? (
|
|
2320
|
+
? (content ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : '')
|
|
2173
2321
|
: '';
|
|
2174
2322
|
|
|
2175
2323
|
// Exec events panel: only for backward compat (messages without parts loaded from DB)
|
|
2176
2324
|
const execEventsHtml = (!isUser && !hasParts && msg.exec_events && msg.exec_events.length > 0)
|
|
2177
2325
|
? renderExecEvents(msg.exec_events, i) : '';
|
|
2178
2326
|
html += `
|
|
2179
|
-
<div class="message-row ${msg.role}">
|
|
2327
|
+
<div class="message-row ${msg.role}${msg.streaming ? ' streaming' : ''}">
|
|
2180
2328
|
<div class="message-avatar">${avatar}</div>
|
|
2181
2329
|
<div class="message-content" style="flex:1;min-width:0">
|
|
2182
2330
|
${reasoningHtml}
|
|
@@ -2294,6 +2442,21 @@ function formatTime(timeStr) {
|
|
|
2294
2442
|
function scrollToBottom(force) {
|
|
2295
2443
|
const c = document.getElementById('messagesContainer');
|
|
2296
2444
|
if (!c) return;
|
|
2445
|
+
// During streaming: pin the active assistant message to the top of the chat window
|
|
2446
|
+
// so the user can see the full response content below
|
|
2447
|
+
const isStreaming = state.isGenerating;
|
|
2448
|
+
if (isStreaming && !force) {
|
|
2449
|
+
const activeRow = c.querySelector('.message-row.assistant.streaming, .message-row.assistant:last-of-type');
|
|
2450
|
+
if (activeRow) {
|
|
2451
|
+
requestAnimationFrame(() => {
|
|
2452
|
+
const rowTop = activeRow.offsetTop;
|
|
2453
|
+
// Scroll so the assistant row sits at the very top of the visible area
|
|
2454
|
+
c.scrollTop = rowTop;
|
|
2455
|
+
updateScrollToBottomBtn(c.scrollHeight - c.scrollTop - c.clientHeight);
|
|
2456
|
+
});
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2297
2460
|
requestAnimationFrame(() => {
|
|
2298
2461
|
// Smart scroll: only auto-scroll if user is near bottom (within 120px)
|
|
2299
2462
|
// or if force is true
|
|
@@ -377,7 +377,7 @@ function updateStreamingMessage(msgIdx) {
|
|
|
377
377
|
<span class="thought-label">模型推理过程${reasoningWordCount}</span>
|
|
378
378
|
${msg.streaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
|
|
379
379
|
</summary>
|
|
380
|
-
<div class="thought-content">${renderMarkdown(msg.reasoning)}</div>
|
|
380
|
+
<div class="thought-content reasoning-content">${renderMarkdown(msg.reasoning)}</div>
|
|
381
381
|
</details>`;
|
|
382
382
|
contentArea.insertAdjacentHTML('afterbegin', reasoningHtml);
|
|
383
383
|
// Set initial length tracking
|
|
@@ -494,114 +494,116 @@ function updateStreamingMessage(msgIdx) {
|
|
|
494
494
|
// Update content - timeline (interleaved text + exec events) or single bubble (backward compat)
|
|
495
495
|
const hasParts = Array.isArray(msg.parts);
|
|
496
496
|
if (hasParts) {
|
|
497
|
-
// ── Timeline rendering
|
|
498
|
-
|
|
499
|
-
|
|
497
|
+
// ── Timeline rendering: all parts in ONE unified bubble ──
|
|
498
|
+
// Structure: .message-bubble > .msg-timeline > (.timeline-segment | .inline-exec-event | .streaming-segment)
|
|
499
|
+
let bubbleWrapper = contentArea.querySelector(':scope > .msg-bubble-wrapper');
|
|
500
|
+
let timeline = bubbleWrapper ? bubbleWrapper.querySelector('.msg-timeline') : null;
|
|
501
|
+
|
|
502
|
+
if (!bubbleWrapper || !timeline) {
|
|
500
503
|
// Remove old single bubble if exists
|
|
501
|
-
const oldBubble = contentArea.querySelector(':scope > .message-bubble');
|
|
504
|
+
const oldBubble = contentArea.querySelector(':scope > .message-bubble:not(.msg-bubble-wrapper .message-bubble)');
|
|
502
505
|
if (oldBubble) oldBubble.remove();
|
|
503
|
-
//
|
|
506
|
+
// Remove old standalone timeline if exists
|
|
507
|
+
const oldTimeline = contentArea.querySelector(':scope > .msg-timeline');
|
|
508
|
+
if (oldTimeline) oldTimeline.remove();
|
|
509
|
+
// Create unified bubble wrapper
|
|
510
|
+
bubbleWrapper = document.createElement('div');
|
|
511
|
+
bubbleWrapper.className = 'message-bubble msg-bubble-wrapper';
|
|
504
512
|
timeline = document.createElement('div');
|
|
505
513
|
timeline.className = 'msg-timeline';
|
|
514
|
+
bubbleWrapper.appendChild(timeline);
|
|
506
515
|
// Insert after thought blocks or at beginning
|
|
507
516
|
const allThoughts = contentArea.querySelectorAll(':scope > .thought-block');
|
|
508
517
|
if (allThoughts.length > 0) {
|
|
509
|
-
allThoughts[allThoughts.length - 1].insertAdjacentElement('afterend',
|
|
518
|
+
allThoughts[allThoughts.length - 1].insertAdjacentElement('afterend', bubbleWrapper);
|
|
510
519
|
} else {
|
|
511
|
-
contentArea.appendChild(
|
|
520
|
+
contentArea.appendChild(bubbleWrapper);
|
|
512
521
|
}
|
|
513
522
|
}
|
|
514
523
|
|
|
515
524
|
// Update completed parts only when count changes (avoid rebuilding DOM)
|
|
516
525
|
const partsCount = msg.parts.length;
|
|
517
526
|
if (!msg._lastPartsCount || msg._lastPartsCount !== partsCount) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (existingStreaming) {
|
|
540
|
-
existingStreaming.insertAdjacentHTML('beforebegin', partHtml);
|
|
541
|
-
} else {
|
|
542
|
-
timeline.insertAdjacentHTML('beforeend', partHtml);
|
|
543
|
-
}
|
|
527
|
+
// Only append new parts (don't rebuild existing)
|
|
528
|
+
const prevCount = msg._lastPartsCount || 0;
|
|
529
|
+
for (let pi = prevCount; pi < msg.parts.length; pi++) {
|
|
530
|
+
const part = msg.parts[pi];
|
|
531
|
+
let partHtml = '';
|
|
532
|
+
if (part.type === 'text' && part.content.trim()) {
|
|
533
|
+
partHtml = '<div class="timeline-segment">' + renderMarkdown(part.content) + '</div>';
|
|
534
|
+
} else if (part.type === 'exec') {
|
|
535
|
+
partHtml = renderInlineExecEvent(part.data, msgIdx);
|
|
536
|
+
} else if (part.type === 'v2_tool') {
|
|
537
|
+
partHtml = renderInlineExecEvent(part, msgIdx);
|
|
538
|
+
} else if (part.type === 'v2_ask') {
|
|
539
|
+
partHtml = '<div class="v2-ask-user"><div class="v2-ask-icon">❓</div><div class="v2-ask-content">' + renderMarkdown(part.data.question) + '</div></div>';
|
|
540
|
+
}
|
|
541
|
+
if (partHtml) {
|
|
542
|
+
// Insert before streaming segment if it exists
|
|
543
|
+
const existingStreaming = timeline.querySelector('.streaming-segment');
|
|
544
|
+
if (existingStreaming) {
|
|
545
|
+
existingStreaming.insertAdjacentHTML('beforebegin', partHtml);
|
|
546
|
+
} else {
|
|
547
|
+
timeline.insertAdjacentHTML('beforeend', partHtml);
|
|
544
548
|
}
|
|
545
549
|
}
|
|
546
|
-
msg._lastPartsCount = partsCount;
|
|
547
550
|
}
|
|
551
|
+
msg._lastPartsCount = partsCount;
|
|
548
552
|
}
|
|
549
553
|
|
|
550
|
-
// Incrementally update streaming
|
|
554
|
+
// Incrementally update streaming segment (inside the same bubble)
|
|
551
555
|
const streamingText = msg._streamingText || '';
|
|
552
556
|
const cursorHtml = msg.streaming ? '<span class="streaming-cursor"></span>' : '';
|
|
553
|
-
let
|
|
557
|
+
let streamingSeg = timeline.querySelector('.streaming-segment');
|
|
554
558
|
if (streamingText.trim()) {
|
|
555
|
-
if (!
|
|
556
|
-
// Append streaming
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
timeline.appendChild(
|
|
559
|
+
if (!streamingSeg) {
|
|
560
|
+
// Append streaming segment at the end of timeline (inside the unified bubble)
|
|
561
|
+
streamingSeg = document.createElement('div');
|
|
562
|
+
streamingSeg.className = 'timeline-segment streaming-segment';
|
|
563
|
+
timeline.appendChild(streamingSeg);
|
|
560
564
|
}
|
|
561
565
|
// Lightweight incremental text update during streaming to avoid flicker
|
|
562
566
|
const prevStreamLen = msg._lastStreamRenderedLen || 0;
|
|
563
567
|
const nowMs = performance.now();
|
|
564
568
|
if (msg.streaming && streamingText.length > prevStreamLen && prevStreamLen > 0) {
|
|
565
569
|
const newChars = streamingText.length - prevStreamLen;
|
|
566
|
-
|
|
567
|
-
if (newChars < 200 && streamingBubble._lastFullHtml) {
|
|
570
|
+
if (newChars < 200 && streamingSeg._lastFullHtml) {
|
|
568
571
|
// Fast path: append new text node (smooth, no reflow)
|
|
569
572
|
const textNode = document.createTextNode(streamingText.substring(prevStreamLen));
|
|
570
|
-
const oldCursor =
|
|
573
|
+
const oldCursor = streamingSeg.querySelector('.streaming-cursor');
|
|
571
574
|
if (oldCursor) oldCursor.remove();
|
|
572
|
-
|
|
575
|
+
streamingSeg.appendChild(textNode);
|
|
573
576
|
const cursor = document.createElement('span');
|
|
574
577
|
cursor.className = 'streaming-cursor';
|
|
575
|
-
|
|
578
|
+
streamingSeg.appendChild(cursor);
|
|
576
579
|
msg._lastStreamRenderedLen = streamingText.length;
|
|
577
580
|
} else {
|
|
578
581
|
// Full markdown render — only when newChars >= 200 or every 1.5s
|
|
579
582
|
const timeSinceFullRender = nowMs - (_fullMdTimer.last || 0);
|
|
580
583
|
if (newChars >= 200 || timeSinceFullRender > 1500) {
|
|
581
|
-
|
|
582
|
-
|
|
584
|
+
streamingSeg.innerHTML = renderMarkdown(streamingText) + cursorHtml;
|
|
585
|
+
streamingSeg._lastFullHtml = streamingSeg.innerHTML;
|
|
583
586
|
msg._lastStreamRenderedLen = streamingText.length;
|
|
584
587
|
_fullMdTimer.last = nowMs;
|
|
585
588
|
} else {
|
|
586
|
-
// Still use fast path even for larger chunks if within 2s cooldown
|
|
587
589
|
const textNode = document.createTextNode(streamingText.substring(prevStreamLen));
|
|
588
|
-
const oldCursor =
|
|
590
|
+
const oldCursor = streamingSeg.querySelector('.streaming-cursor');
|
|
589
591
|
if (oldCursor) oldCursor.remove();
|
|
590
|
-
|
|
592
|
+
streamingSeg.appendChild(textNode);
|
|
591
593
|
const cursor = document.createElement('span');
|
|
592
594
|
cursor.className = 'streaming-cursor';
|
|
593
|
-
|
|
595
|
+
streamingSeg.appendChild(cursor);
|
|
594
596
|
msg._lastStreamRenderedLen = streamingText.length;
|
|
595
597
|
}
|
|
596
598
|
}
|
|
597
599
|
} else {
|
|
598
600
|
// Full render
|
|
599
|
-
|
|
600
|
-
|
|
601
|
+
streamingSeg.innerHTML = renderMarkdown(streamingText) + cursorHtml;
|
|
602
|
+
streamingSeg._lastFullHtml = streamingSeg.innerHTML;
|
|
601
603
|
msg._lastStreamRenderedLen = streamingText.length;
|
|
602
604
|
}
|
|
603
|
-
} else if (
|
|
604
|
-
|
|
605
|
+
} else if (streamingSeg) {
|
|
606
|
+
streamingSeg.remove();
|
|
605
607
|
msg._lastStreamRenderedLen = 0;
|
|
606
608
|
}
|
|
607
609
|
|
|
@@ -1064,29 +1066,32 @@ function _stripXmlTags(xml) {
|
|
|
1064
1066
|
// ══════════════════════════════════════════════════════
|
|
1065
1067
|
|
|
1066
1068
|
function _assembleV2Content(msg, msgParts) {
|
|
1067
|
-
// Priority 1:
|
|
1069
|
+
// Priority 1: Text parts from msgParts (now includes flushed V2 reasoning segments)
|
|
1070
|
+
// In the new interleaved model, reasoning text is flushed into text parts at tool boundaries
|
|
1071
|
+
if (msgParts && Array.isArray(msgParts) && msgParts.length > 0) {
|
|
1072
|
+
var textParts = msgParts.filter(function(p) { return p.type === 'text'; });
|
|
1073
|
+
if (textParts.length > 0) {
|
|
1074
|
+
// Return only the LAST text part as content (the final response)
|
|
1075
|
+
// This avoids showing intermediate reasoning text as the message content
|
|
1076
|
+
var lastText = textParts[textParts.length - 1].content;
|
|
1077
|
+
if (lastText && lastText.trim()) return lastText.trim();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// Priority 2: V2 reasoning text (may still be present during streaming before flush)
|
|
1068
1081
|
if (msg._v2Reasoning && msg._v2Reasoning.trim()) {
|
|
1069
1082
|
return msg._v2Reasoning.trim();
|
|
1070
1083
|
}
|
|
1071
|
-
// Priority
|
|
1084
|
+
// Priority 3: V2 ask user text
|
|
1072
1085
|
if (msg._askUser && msg._askUser.trim()) {
|
|
1073
1086
|
return msg._askUser.trim();
|
|
1074
1087
|
}
|
|
1075
|
-
// Priority
|
|
1088
|
+
// Priority 4: V2 raw XML stripped of tags (fallback when v2_reasoning not sent)
|
|
1076
1089
|
if (msg._v2RawXml && msg._v2RawXml.trim()) {
|
|
1077
1090
|
var strippedText = _stripXmlTags(msg._v2RawXml);
|
|
1078
1091
|
if (strippedText && strippedText.trim()) {
|
|
1079
1092
|
return strippedText.trim();
|
|
1080
1093
|
}
|
|
1081
1094
|
}
|
|
1082
|
-
// Priority 4: V1 text parts (backward compat — non-V2 mode)
|
|
1083
|
-
// Guard: msgParts may be undefined after page refresh (only role/content/time persisted)
|
|
1084
|
-
if (msgParts && Array.isArray(msgParts) && msgParts.length > 0) {
|
|
1085
|
-
var textParts = msgParts.filter(function(p) { return p.type === 'text'; });
|
|
1086
|
-
if (textParts.length > 0) {
|
|
1087
|
-
return textParts.map(function(p) { return p.content; }).join('\n\n');
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
1095
|
// Priority 5: raw content from message (server-stored response)
|
|
1091
1096
|
if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
|
|
1092
1097
|
return msg.content.trim();
|
|
@@ -1199,6 +1204,15 @@ async function sendMessage() {
|
|
|
1199
1204
|
}
|
|
1200
1205
|
currentText = '';
|
|
1201
1206
|
}
|
|
1207
|
+
// Flush accumulated V2 reasoning text as a text part into msgParts
|
|
1208
|
+
// This creates the interleaved speak→tool→speak pattern
|
|
1209
|
+
function flushV2Reasoning() {
|
|
1210
|
+
if (_v2ReasoningText && _v2ReasoningText.trim()) {
|
|
1211
|
+
msgParts.push({type: 'text', content: _v2ReasoningText.trim()});
|
|
1212
|
+
_v2ReasoningText = '';
|
|
1213
|
+
state.messages[msgIdx]._v2Reasoning = _v2ReasoningText;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1202
1216
|
|
|
1203
1217
|
// Add placeholder for streaming response
|
|
1204
1218
|
state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
|
|
@@ -1297,6 +1311,8 @@ async function sendMessage() {
|
|
|
1297
1311
|
scrollToBottom(true); // Force scroll for new message
|
|
1298
1312
|
} else if (evt.type === 'clear_text') {
|
|
1299
1313
|
// Clear intermediate text from previous agent loop iterations
|
|
1314
|
+
// Flush V2 reasoning as text part before clearing
|
|
1315
|
+
flushV2Reasoning();
|
|
1300
1316
|
flushCurrentText();
|
|
1301
1317
|
state.messages[msgIdx].parts = [...msgParts];
|
|
1302
1318
|
state.messages[msgIdx]._streamingText = '';
|
|
@@ -1388,10 +1404,13 @@ async function sendMessage() {
|
|
|
1388
1404
|
callback: evt.tool.callback
|
|
1389
1405
|
});
|
|
1390
1406
|
}
|
|
1407
|
+
// Flush reasoning text BEFORE tool call to create speak→tool pattern
|
|
1408
|
+
flushV2Reasoning();
|
|
1391
1409
|
flushCurrentText();
|
|
1392
1410
|
var toolEvent = {
|
|
1393
1411
|
type: 'v2_tool',
|
|
1394
1412
|
data: {
|
|
1413
|
+
id: 'v2tool_' + Date.now() + '_' + allExecEvents.length,
|
|
1395
1414
|
type: 'tool_start',
|
|
1396
1415
|
title: (evt.tool.beforecalltext || '调用工具: ' + (evt.tool.toolname || '')),
|
|
1397
1416
|
tool_name: evt.tool.toolname,
|
|
@@ -1419,24 +1438,29 @@ async function sendMessage() {
|
|
|
1419
1438
|
// evt.result contains: {success, output, error, ...}
|
|
1420
1439
|
// 记录到调试控制台
|
|
1421
1440
|
if (window.addDebugLog) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1441
|
+
var _toolResult = evt.result || {};
|
|
1442
|
+
var _toolInfo = evt.tool || {};
|
|
1443
|
+
window.addDebugLog(_toolResult.success ? 'tool' : 'error',
|
|
1444
|
+
(_toolInfo.toolname || '工具') + ' 执行' + (_toolResult.success ? '成功' : '失败'), {
|
|
1445
|
+
tool: _toolInfo.toolname,
|
|
1446
|
+
success: _toolResult.success,
|
|
1447
|
+
error: _toolResult.error,
|
|
1448
|
+
result: _toolResult.output || _toolResult.error
|
|
1428
1449
|
});
|
|
1429
1450
|
}
|
|
1451
|
+
var _r = evt.result || {};
|
|
1452
|
+
var _t = evt.tool || {};
|
|
1430
1453
|
var resultEvent = {
|
|
1431
1454
|
type: 'v2_tool',
|
|
1432
1455
|
data: {
|
|
1456
|
+
id: 'v2tool_' + Date.now() + '_' + allExecEvents.length,
|
|
1433
1457
|
type: 'tool_result',
|
|
1434
|
-
title: (
|
|
1435
|
-
tool_name:
|
|
1436
|
-
success:
|
|
1437
|
-
summary: (
|
|
1438
|
-
result:
|
|
1439
|
-
callback:
|
|
1458
|
+
title: (_t.toolname || '工具') + ' 执行完成',
|
|
1459
|
+
tool_name: _t.toolname,
|
|
1460
|
+
success: !!_r.success,
|
|
1461
|
+
summary: (_r.output || _r.error || '').substring(0, 500),
|
|
1462
|
+
result: _r,
|
|
1463
|
+
callback: _t.callback
|
|
1440
1464
|
}
|
|
1441
1465
|
};
|
|
1442
1466
|
msgParts.push(resultEvent);
|
|
@@ -1504,6 +1528,8 @@ async function sendMessage() {
|
|
|
1504
1528
|
ttsManager.streamDelta(evt.content);
|
|
1505
1529
|
}
|
|
1506
1530
|
} else if (evt.type === 'done') {
|
|
1531
|
+
// Flush remaining V2 reasoning as final text part
|
|
1532
|
+
flushV2Reasoning();
|
|
1507
1533
|
flushCurrentText();
|
|
1508
1534
|
// 记录到调试控制台
|
|
1509
1535
|
if (window.addDebugLog) {
|
|
@@ -1547,6 +1573,7 @@ async function sendMessage() {
|
|
|
1547
1573
|
}
|
|
1548
1574
|
|
|
1549
1575
|
// Finalize message
|
|
1576
|
+
flushV2Reasoning();
|
|
1550
1577
|
flushCurrentText();
|
|
1551
1578
|
// Stop all running tool timers
|
|
1552
1579
|
for (var tid in _toolTimers) {
|