myagent-ai 1.7.2 → 1.7.3
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/package.json +2 -2
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +40 -13
- package/web/ui/chat/chat.css +17 -0
- package/web/ui/chat/chat_main.js +36 -4
- package/web/ui/chat/flow_engine.js +185 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagent-ai",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
|
|
5
5
|
"main": "main.py",
|
|
6
6
|
"bin": {
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"departments/",
|
|
66
66
|
"web/"
|
|
67
67
|
]
|
|
68
|
-
}
|
|
68
|
+
}
|
|
Binary file
|
package/web/api_server.py
CHANGED
|
@@ -746,29 +746,41 @@ class ApiServer:
|
|
|
746
746
|
return ""
|
|
747
747
|
|
|
748
748
|
base_instruction = (
|
|
749
|
-
"你当前处于【执行模式】(Execution Mode)。\n"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
"
|
|
749
|
+
"你当前处于【执行模式】(Execution Mode)。\n\n"
|
|
750
|
+
"## 核心规则\n"
|
|
751
|
+
"1. **任务列表(强制)**:每次回复【必须】包含一个 ```tasklist``` 代码块,内含 JSON 数组格式的任务进度。\n"
|
|
752
|
+
" - 格式:```tasklist\\n[{\"text\": \"步骤描述\", \"status\": \"pending\"}]\\n```\n"
|
|
753
|
+
" - status 可选值:pending(待执行)、running(进行中)、done(已完成)、blocked(受阻)\n"
|
|
754
|
+
" - 首次收到任务时,拆分为多个步骤,全部标记为 pending\n"
|
|
755
|
+
" - 每次执行完一个步骤后,更新对应步骤状态为 done,下一个为 running\n"
|
|
756
|
+
"2. **单步执行(强制)**:每次回复【只能执行一个操作】(一个工具调用、一个代码块或一个技能调用)。\n"
|
|
757
|
+
" - 执行完一个操作后停下来,等待结果反馈后再决定下一步\n"
|
|
758
|
+
" - 不要一次性执行多个操作\n"
|
|
759
|
+
"3. **回复格式**:先写纯文本分析/总结 → 再写 ```tasklist``` 更新进度 → 最后写 ```action``` 执行操作(如有)\n"
|
|
753
760
|
)
|
|
754
761
|
|
|
755
762
|
# 从内存读取当前任务列表
|
|
756
763
|
tasks = self._task_list_store.get(agent_path, [])
|
|
757
764
|
if not tasks:
|
|
758
|
-
return base_instruction + "
|
|
765
|
+
return base_instruction + "\n## 当前状态\n暂无任务计划。请先分析用户需求,拆分为具体步骤,然后用 ```tasklist``` 输出计划。"
|
|
759
766
|
|
|
760
767
|
pending = [f" - ⏳ {t['text']}" for t in tasks if t.get("status") in ("pending", "running", "blocked")]
|
|
761
768
|
done = [f" - ✅ {t['text']}" for t in tasks if t.get("status") == "done"]
|
|
762
769
|
running = [f" - 🔄 {t['text']}" for t in tasks if t.get("status") == "running"]
|
|
763
770
|
|
|
764
|
-
context = base_instruction + "\n
|
|
771
|
+
context = base_instruction + "\n## 当前任务进度\n"
|
|
765
772
|
if done:
|
|
766
773
|
context += "已完成:\n" + "\n".join(done) + "\n"
|
|
767
774
|
if running:
|
|
768
775
|
context += "进行中:\n" + "\n".join(running) + "\n"
|
|
769
776
|
if pending:
|
|
770
777
|
context += "待执行:\n" + "\n".join(pending) + "\n"
|
|
771
|
-
context +=
|
|
778
|
+
context += (
|
|
779
|
+
"\n## 下一步\n"
|
|
780
|
+
"1. 用纯文本简要分析当前进展\n"
|
|
781
|
+
"2. 用 ```tasklist``` 更新任务进度(标记已完成的步骤为 done,标记当前步骤为 running)\n"
|
|
782
|
+
"3. 用 ```action``` 执行下一个待执行步骤(每次只执行一个操作)\n"
|
|
783
|
+
)
|
|
772
784
|
return context
|
|
773
785
|
|
|
774
786
|
async def handle_chat_page(self, request):
|
|
@@ -2613,7 +2625,9 @@ class ApiServer:
|
|
|
2613
2625
|
iteration = 0
|
|
2614
2626
|
# 追踪连续无 action 迭代次数,防止无限重新提示
|
|
2615
2627
|
_consecutive_no_action = 0
|
|
2616
|
-
_MAX_NO_ACTION_RETRIES =
|
|
2628
|
+
_MAX_NO_ACTION_RETRIES = 5 # 提高重试次数,给 LLM 更多机会完成剩余任务
|
|
2629
|
+
# ── 追踪所有流式推送的纯文本(用于刷新后恢复) ──
|
|
2630
|
+
_all_streamed_text_parts = [] # 每轮迭代推送的纯文本片段
|
|
2617
2631
|
|
|
2618
2632
|
while iteration < max_iter:
|
|
2619
2633
|
iteration += 1
|
|
@@ -2682,6 +2696,7 @@ class ApiServer:
|
|
|
2682
2696
|
text_before = remaining[:marker_pos]
|
|
2683
2697
|
if text_before.strip():
|
|
2684
2698
|
await _write_sse({"type": "text_delta", "content": text_before})
|
|
2699
|
+
_all_streamed_text_parts.append(text_before)
|
|
2685
2700
|
# 跳过整个开始标记(```action 或 ```tasklist),不要只跳到 ```
|
|
2686
2701
|
st["processed_pos"] += marker_pos + len(f"```{block_type}")
|
|
2687
2702
|
if block_type == "tasklist":
|
|
@@ -2718,7 +2733,9 @@ class ApiServer:
|
|
|
2718
2733
|
# 没有找到标记,流式推送(保留末尾可能的部分标记)
|
|
2719
2734
|
safe_end = len(remaining) - _MAX_HOLD
|
|
2720
2735
|
if safe_end > 0:
|
|
2721
|
-
|
|
2736
|
+
chunk = remaining[:safe_end]
|
|
2737
|
+
await _write_sse({"type": "text_delta", "content": chunk})
|
|
2738
|
+
_all_streamed_text_parts.append(chunk)
|
|
2722
2739
|
st["processed_pos"] += safe_end
|
|
2723
2740
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
2724
2741
|
else:
|
|
@@ -2790,6 +2807,7 @@ class ApiServer:
|
|
|
2790
2807
|
await _stream_text_chunked(remaining, _write_sse, chunk_size=3, delay=0.01)
|
|
2791
2808
|
else:
|
|
2792
2809
|
await _write_sse({"type": "text_delta", "content": remaining})
|
|
2810
|
+
_all_streamed_text_parts.append(remaining)
|
|
2793
2811
|
st["processed_pos"] = len(full_text)
|
|
2794
2812
|
|
|
2795
2813
|
# Call LLM with streaming — tokens are filtered through _text_delta_callback
|
|
@@ -3054,10 +3072,19 @@ class ApiServer:
|
|
|
3054
3072
|
break
|
|
3055
3073
|
|
|
3056
3074
|
# Save assistant response to memory
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3075
|
+
# ── 优先使用流式累积文本(包含所有迭代的纯文本),回退到 final_response ──
|
|
3076
|
+
saved_response = final_response
|
|
3077
|
+
if not saved_response and _all_streamed_text_parts:
|
|
3078
|
+
saved_response = "\n\n".join(p for p in _all_streamed_text_parts if p.strip())
|
|
3079
|
+
if not saved_response and content:
|
|
3080
|
+
saved_response = content # 兜底:使用最后一轮的完整输出
|
|
3081
|
+
if agent.memory and saved_response:
|
|
3082
|
+
agent.memory.add_short_term(session_id=session_id, role="assistant", content=saved_response)
|
|
3083
|
+
elif agent.memory:
|
|
3084
|
+
# 即使为空也保存一条,防止刷新后消息丢失
|
|
3085
|
+
agent.memory.add_short_term(session_id=session_id, role="assistant", content="(执行完成,无文本回复)")
|
|
3086
|
+
|
|
3087
|
+
return saved_response or final_response or content or ""
|
|
3061
3088
|
|
|
3062
3089
|
async def _execute_actions_streaming(
|
|
3063
3090
|
self, agent, action_data: dict, context, write_sse
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -1569,6 +1569,20 @@ input,textarea,select{font:inherit}
|
|
|
1569
1569
|
.exec-event-result-btn:hover{background:var(--accent-light);color:var(--accent-dark)}
|
|
1570
1570
|
.exec-event-result-btn svg{width:12px;height:12px}
|
|
1571
1571
|
|
|
1572
|
+
/* ── Inline Exec Events (Timeline Interleaved) ── */
|
|
1573
|
+
.msg-timeline{display:flex;flex-direction:column;gap:6px}
|
|
1574
|
+
.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}
|
|
1575
|
+
.inline-exec-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
|
|
1576
|
+
.inline-exec-icon{font-size:14px}
|
|
1577
|
+
.inline-exec-title{font-weight:500;color:var(--text);font-size:12px}
|
|
1578
|
+
.inline-exec-meta{color:var(--text3);font-size:11px;margin-left:auto}
|
|
1579
|
+
.inline-exec-code{background:var(--bg);padding:6px 8px;border-radius:4px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12px;color:var(--text2);margin:4px 0;max-height:100px;overflow:hidden;cursor:pointer;transition:var(--transition);white-space:pre-wrap;word-break:break-all}
|
|
1580
|
+
.inline-exec-code:hover{background:var(--bg3)}
|
|
1581
|
+
.inline-exec-code.expanded{max-height:none}
|
|
1582
|
+
.inline-exec-summary{color:var(--text2);font-size:12px;margin:4px 0}
|
|
1583
|
+
.inline-exec-result-btn{background:none;border:1px solid var(--border);color:var(--text2);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;margin-top:4px;transition:var(--transition)}
|
|
1584
|
+
.inline-exec-result-btn:hover{background:var(--bg2);border-color:var(--accent);color:var(--accent)}
|
|
1585
|
+
|
|
1572
1586
|
/* ── Execution Result Modal ── */
|
|
1573
1587
|
.exec-result-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;animation:fadeIn .15s ease}
|
|
1574
1588
|
.exec-result-modal{background:var(--bg);border:1px solid var(--border);border-radius:12px;width:min(680px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.25);animation:slideUp .2s ease}
|
|
@@ -1605,6 +1619,9 @@ input,textarea,select{font:inherit}
|
|
|
1605
1619
|
[data-theme="dark"] .exec-result-modal{background:var(--bg2);border-color:var(--border)}
|
|
1606
1620
|
[data-theme="dark"] .exec-result-modal-body pre{background:#0a0c10;color:#cdd6f4}
|
|
1607
1621
|
[data-theme="dark"] .exec-result-info-item{background:var(--bg3)}
|
|
1622
|
+
[data-theme="dark"] .inline-exec-event{background:var(--bg3);border-left-color:var(--border)}
|
|
1623
|
+
[data-theme="dark"] .inline-exec-code{background:var(--bg)}
|
|
1624
|
+
[data-theme="dark"] .inline-exec-result-btn:hover{background:var(--bg4)}
|
|
1608
1625
|
|
|
1609
1626
|
.thought-block {
|
|
1610
1627
|
background: rgba(0, 0, 0, 0.03);
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -1680,9 +1680,12 @@ function renderMessages() {
|
|
|
1680
1680
|
</div>` : '';
|
|
1681
1681
|
const ttsIndicator = ttsManager && ttsManager.isPlaying && ttsManager.currentMsgIndex === i ?
|
|
1682
1682
|
' <span class="tts-playing-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>' : '';
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const
|
|
1683
|
+
|
|
1684
|
+
// ── Determine rendering mode and streaming indicator ──
|
|
1685
|
+
const hasParts = Array.isArray(msg.parts) && msg.parts.length > 0;
|
|
1686
|
+
const hasStreamingText = msg._streamingText && msg._streamingText.trim();
|
|
1687
|
+
const anyContent = msg.content || msg._streamingText || hasParts;
|
|
1688
|
+
const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
|
|
1686
1689
|
<div class="streaming-indicator">
|
|
1687
1690
|
<div class="spinner"></div>
|
|
1688
1691
|
<div class="streaming-dots">
|
|
@@ -1690,13 +1693,42 @@ function renderMessages() {
|
|
|
1690
1693
|
</div>
|
|
1691
1694
|
<span style="font-weight:500">Agent 正在思考...</span>
|
|
1692
1695
|
</div>` : '';
|
|
1696
|
+
|
|
1697
|
+
// ── Timeline rendering for interleaved text + exec events ──
|
|
1698
|
+
let timelineHtml = '';
|
|
1699
|
+
if (hasParts || hasStreamingText) {
|
|
1700
|
+
let partsHtml = '';
|
|
1701
|
+
for (const part of (msg.parts || [])) {
|
|
1702
|
+
if (part.type === 'text' && part.content.trim()) {
|
|
1703
|
+
partsHtml += '<div class="message-bubble">' + renderMarkdown(part.content) + '</div>';
|
|
1704
|
+
} else if (part.type === 'exec') {
|
|
1705
|
+
partsHtml += renderInlineExecEvent(part.data, i);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (hasStreamingText) {
|
|
1709
|
+
partsHtml += '<div class="message-bubble">' + renderMarkdown(msg._streamingText) + '</div>';
|
|
1710
|
+
}
|
|
1711
|
+
if (partsHtml) {
|
|
1712
|
+
timelineHtml = '<div class="msg-timeline">' + partsHtml + '</div>';
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// Backward compat: single bubble for messages without parts
|
|
1717
|
+
const singleBubbleHtml = (!hasParts && !hasStreamingText)
|
|
1718
|
+
? ((content || streamingIndicator) ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : '')
|
|
1719
|
+
: '';
|
|
1720
|
+
|
|
1721
|
+
// Exec events panel: only for backward compat (messages without parts loaded from DB)
|
|
1722
|
+
const execEventsHtml = (!isUser && !hasParts && msg.exec_events && msg.exec_events.length > 0)
|
|
1723
|
+
? renderExecEvents(msg.exec_events, i) : '';
|
|
1693
1724
|
html += `
|
|
1694
1725
|
<div class="message-row ${msg.role}">
|
|
1695
1726
|
<div class="message-avatar">${avatar}</div>
|
|
1696
1727
|
<div style="flex:1;min-width:0">
|
|
1697
1728
|
${reasoningHtml}
|
|
1698
1729
|
${thoughtHtml}
|
|
1699
|
-
${
|
|
1730
|
+
${timelineHtml}
|
|
1731
|
+
${singleBubbleHtml}
|
|
1700
1732
|
${streamingIndicator}
|
|
1701
1733
|
${execEventsHtml}
|
|
1702
1734
|
${msg.time ? `<div class="message-time">${formatTime(msg.time)}</div>` : ''}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<string>:27: SyntaxWarning: invalid escape sequence '\s'
|
|
2
1
|
// ══════════════════════════════════════════════════════
|
|
3
2
|
// ── Flow Engine: 文本处理流引擎 ──
|
|
4
3
|
// ── 负责消息发送、SSE 流式处理、大文本检测与分段、
|
|
@@ -351,22 +350,87 @@ function updateStreamingMessage(msgIdx) {
|
|
|
351
350
|
}
|
|
352
351
|
}
|
|
353
352
|
|
|
354
|
-
// Update content bubble
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
353
|
+
// Update content - timeline (interleaved text + exec events) or single bubble (backward compat)
|
|
354
|
+
const hasParts = Array.isArray(msg.parts);
|
|
355
|
+
if (hasParts) {
|
|
356
|
+
// ── Timeline rendering for interleaved text + exec events ──
|
|
357
|
+
let timeline = contentArea.querySelector('.msg-timeline');
|
|
358
|
+
if (!timeline) {
|
|
359
|
+
// Remove old single bubble if exists
|
|
360
|
+
const oldBubble = contentArea.querySelector(':scope > .message-bubble');
|
|
361
|
+
if (oldBubble) oldBubble.remove();
|
|
362
|
+
// Create timeline container
|
|
363
|
+
timeline = document.createElement('div');
|
|
364
|
+
timeline.className = 'msg-timeline';
|
|
365
|
+
// Insert after thought blocks or at beginning
|
|
366
|
+
const allThoughts = contentArea.querySelectorAll(':scope > .thought-block');
|
|
367
|
+
if (allThoughts.length > 0) {
|
|
368
|
+
allThoughts[allThoughts.length - 1].insertAdjacentElement('afterend', timeline);
|
|
369
|
+
} else {
|
|
370
|
+
contentArea.appendChild(timeline);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Cache completed parts rendering (only re-render when parts count changes)
|
|
375
|
+
const partsCount = msg.parts.length;
|
|
376
|
+
if (!msg._renderedPartsHtml || msg._lastPartsCount !== partsCount) {
|
|
377
|
+
let html = '';
|
|
378
|
+
for (const part of msg.parts) {
|
|
379
|
+
if (part.type === 'text' && part.content.trim()) {
|
|
380
|
+
html += '<div class="message-bubble">' + renderMarkdown(part.content) + '</div>';
|
|
381
|
+
} else if (part.type === 'exec') {
|
|
382
|
+
html += renderInlineExecEvent(part.data, msgIdx);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
msg._renderedPartsHtml = html;
|
|
386
|
+
msg._lastPartsCount = partsCount;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Build streaming bubble for current in-progress text
|
|
390
|
+
const streamingText = msg._streamingText || '';
|
|
391
|
+
const streamingBubbleHtml = streamingText.trim()
|
|
392
|
+
? '<div class="message-bubble">' + renderMarkdown(streamingText) + '</div>'
|
|
393
|
+
: '';
|
|
394
|
+
|
|
395
|
+
timeline.innerHTML = msg._renderedPartsHtml + streamingBubbleHtml;
|
|
396
|
+
|
|
397
|
+
// Remove exec events panel if present (events are now inline in timeline)
|
|
398
|
+
const execPanel = contentArea.querySelector('.exec-events-panel');
|
|
399
|
+
if (execPanel) execPanel.remove();
|
|
400
|
+
} else {
|
|
401
|
+
// ── Backward compat: single content bubble + exec events panel ──
|
|
402
|
+
let bubble = contentArea.querySelector('.message-bubble');
|
|
403
|
+
const content = renderMarkdown(msg.content);
|
|
404
|
+
if (content && !bubble) {
|
|
405
|
+
bubble = document.createElement('div');
|
|
406
|
+
bubble.className = 'message-bubble';
|
|
407
|
+
contentArea.appendChild(bubble);
|
|
408
|
+
}
|
|
409
|
+
if (bubble && content) {
|
|
410
|
+
bubble.innerHTML = content;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Exec events panel (only for backward compat messages without parts)
|
|
414
|
+
if (msg.exec_events && msg.exec_events.length > 0) {
|
|
415
|
+
let execPanel = contentArea.querySelector('.exec-events-panel');
|
|
416
|
+
const newExecHtml = renderExecEvents(msg.exec_events, msgIdx);
|
|
417
|
+
if (execPanel) {
|
|
418
|
+
execPanel.outerHTML = newExecHtml;
|
|
419
|
+
} else {
|
|
420
|
+
const timeEl = contentArea.querySelector('.message-time');
|
|
421
|
+
if (timeEl) {
|
|
422
|
+
timeEl.insertAdjacentHTML('beforebegin', newExecHtml);
|
|
423
|
+
} else {
|
|
424
|
+
contentArea.insertAdjacentHTML('beforeend', newExecHtml);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
365
428
|
}
|
|
366
429
|
|
|
367
430
|
// Update streaming indicator
|
|
368
431
|
let indicator = contentArea.querySelector('.streaming-indicator');
|
|
369
|
-
const
|
|
432
|
+
const anyContent = msg.content || msg._streamingText || (msg.parts && msg.parts.length > 0);
|
|
433
|
+
const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
|
|
370
434
|
<div class="streaming-indicator">
|
|
371
435
|
<div class="streaming-dots">
|
|
372
436
|
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
|
@@ -381,23 +445,6 @@ function updateStreamingMessage(msgIdx) {
|
|
|
381
445
|
indicator.remove();
|
|
382
446
|
}
|
|
383
447
|
|
|
384
|
-
// Update exec events panel
|
|
385
|
-
if (msg.exec_events && msg.exec_events.length > 0) {
|
|
386
|
-
let execPanel = contentArea.querySelector('.exec-events-panel');
|
|
387
|
-
const newExecHtml = renderExecEvents(msg.exec_events, msgIdx);
|
|
388
|
-
if (execPanel) {
|
|
389
|
-
execPanel.outerHTML = newExecHtml;
|
|
390
|
-
} else {
|
|
391
|
-
// Insert before time element or at end
|
|
392
|
-
const timeEl = contentArea.querySelector('.message-time');
|
|
393
|
-
if (timeEl) {
|
|
394
|
-
timeEl.insertAdjacentHTML('beforebegin', newExecHtml);
|
|
395
|
-
} else {
|
|
396
|
-
contentArea.insertAdjacentHTML('beforeend', newExecHtml);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
448
|
// Auto-scroll
|
|
402
449
|
scrollToBottom();
|
|
403
450
|
}
|
|
@@ -508,6 +555,52 @@ function toggleExecEventsPanel(header) {
|
|
|
508
555
|
body.classList.toggle('expanded');
|
|
509
556
|
}
|
|
510
557
|
|
|
558
|
+
// ══════════════════════════════════════════════════════
|
|
559
|
+
// ── Inline Exec Event (Timeline Card) ──
|
|
560
|
+
// ══════════════════════════════════════════════════════
|
|
561
|
+
|
|
562
|
+
function renderInlineExecEvent(data, msgIdx) {
|
|
563
|
+
const iconEmoji = getEventIconEmoji(data);
|
|
564
|
+
const title = data.title || (data.tool_name || data.skill_name || '执行事件');
|
|
565
|
+
|
|
566
|
+
// Build meta text
|
|
567
|
+
let metaParts = [];
|
|
568
|
+
if (data.execution_time !== undefined) metaParts.push('耗时 ' + data.execution_time + 's');
|
|
569
|
+
if (data.language) metaParts.push(escapeHtml(data.language));
|
|
570
|
+
if (data.tool_name || data.skill_name) metaParts.push(escapeHtml(data.tool_name || data.skill_name));
|
|
571
|
+
if (data.timed_out) metaParts.push('超时');
|
|
572
|
+
if (data.exit_code !== undefined) metaParts.push('exit: ' + data.exit_code);
|
|
573
|
+
const metaText = metaParts.join(' · ');
|
|
574
|
+
|
|
575
|
+
// Build body content
|
|
576
|
+
let bodyHtml = '';
|
|
577
|
+
// Code preview for code_exec/code_result
|
|
578
|
+
if (data.code_preview && (data.type === 'code_exec' || data.type === 'code_result')) {
|
|
579
|
+
bodyHtml += '<div class="inline-exec-code" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')" title="点击查看完整结果">' + escapeHtml(data.code_preview) + '</div>';
|
|
580
|
+
}
|
|
581
|
+
// Summary for tool_result/skill_result
|
|
582
|
+
if (data.summary && (data.type === 'tool_result' || data.type === 'skill_result')) {
|
|
583
|
+
bodyHtml += '<div class="inline-exec-summary">' + escapeHtml(data.summary) + '</div>';
|
|
584
|
+
}
|
|
585
|
+
// Result button for code_result
|
|
586
|
+
if (data.type === 'code_result' && (data.stdout || data.stderr || data.error)) {
|
|
587
|
+
bodyHtml += '<button class="inline-exec-result-btn" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
|
|
588
|
+
}
|
|
589
|
+
// Result button for tool_result/skill_result
|
|
590
|
+
if ((data.type === 'tool_result' || data.type === 'skill_result') && data.result) {
|
|
591
|
+
bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return '<div class="inline-exec-event">' +
|
|
595
|
+
'<div class="inline-exec-header">' +
|
|
596
|
+
'<span class="inline-exec-icon">' + iconEmoji + '</span>' +
|
|
597
|
+
'<span class="inline-exec-title">' + escapeHtml(title) + '</span>' +
|
|
598
|
+
(metaText ? '<span class="inline-exec-meta">' + metaText + '</span>' : '') +
|
|
599
|
+
'</div>' +
|
|
600
|
+
bodyHtml +
|
|
601
|
+
'</div>';
|
|
602
|
+
}
|
|
603
|
+
|
|
511
604
|
// ══════════════════════════════════════════════════════
|
|
512
605
|
// ── Execution Result Modal (执行结果弹窗) ──
|
|
513
606
|
// ══════════════════════════════════════════════════════
|
|
@@ -703,14 +796,22 @@ async function sendMessage() {
|
|
|
703
796
|
const reader = resp.body.getReader();
|
|
704
797
|
const decoder = new TextDecoder();
|
|
705
798
|
let buffer = '';
|
|
706
|
-
let
|
|
799
|
+
let msgParts = []; // Timeline: [{type:'text', content:'...'}, {type:'exec', data:{...}}]
|
|
800
|
+
let currentText = ''; // Accumulator for current streaming text segment
|
|
801
|
+
let allExecEvents = []; // All exec events (for summary panel at bottom)
|
|
707
802
|
let msgIdx = state.messages.length;
|
|
708
803
|
let sessionIdReceived = sessionId;
|
|
709
|
-
let execEventsReceived = [];
|
|
710
804
|
let fullThought = '';
|
|
711
|
-
|
|
805
|
+
|
|
806
|
+
function flushCurrentText() {
|
|
807
|
+
if (currentText.trim()) {
|
|
808
|
+
msgParts.push({type: 'text', content: currentText});
|
|
809
|
+
}
|
|
810
|
+
currentText = '';
|
|
811
|
+
}
|
|
812
|
+
|
|
712
813
|
// Add placeholder for streaming response
|
|
713
|
-
state.messages.push({ role: 'assistant', content: '', thought: '', time: new Date().toISOString(), streaming: true });
|
|
814
|
+
state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
|
|
714
815
|
renderMessages();
|
|
715
816
|
|
|
716
817
|
while (true) {
|
|
@@ -731,13 +832,22 @@ async function sendMessage() {
|
|
|
731
832
|
// Sync the actual session ID (backend may prefix with agent_path)
|
|
732
833
|
state.activeSessionId = evt.session_id;
|
|
733
834
|
} else if (evt.type === 'text') {
|
|
734
|
-
|
|
835
|
+
// Full text event (non-streaming replacement)
|
|
836
|
+
flushCurrentText();
|
|
837
|
+
msgParts.push({type: 'text', content: evt.content});
|
|
838
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
839
|
+
state.messages[msgIdx]._streamingText = '';
|
|
735
840
|
state.messages[msgIdx].content = evt.content;
|
|
736
841
|
renderMessages();
|
|
737
842
|
} else if (evt.type === 'text_delta') {
|
|
738
843
|
// Incremental streaming token
|
|
739
|
-
|
|
740
|
-
|
|
844
|
+
currentText += evt.content;
|
|
845
|
+
// Build backward-compat content from all parts + streaming text
|
|
846
|
+
const allText = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n')
|
|
847
|
+
+ (currentText.trim() ? '\n\n' + currentText : '');
|
|
848
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
849
|
+
state.messages[msgIdx]._streamingText = currentText;
|
|
850
|
+
state.messages[msgIdx].content = allText;
|
|
741
851
|
throttledStreamUpdate(msgIdx);
|
|
742
852
|
// ── 分段流式 TTS:推送增量文本 ──
|
|
743
853
|
if (ttsManager.enabled && !ttsManager._streamActive) {
|
|
@@ -759,28 +869,39 @@ async function sendMessage() {
|
|
|
759
869
|
state.messages[msgIdx].thought = fullThought;
|
|
760
870
|
throttledStreamUpdate(msgIdx);
|
|
761
871
|
} else if (evt.type === 'queue_start') {
|
|
762
|
-
//
|
|
872
|
+
// Finalize previous message
|
|
873
|
+
flushCurrentText();
|
|
763
874
|
if (state.messages[msgIdx]) {
|
|
764
875
|
state.messages[msgIdx].streaming = false;
|
|
765
|
-
|
|
876
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
877
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
|
|
878
|
+
state.messages[msgIdx]._streamingText = '';
|
|
879
|
+
if (allExecEvents.length > 0) state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
766
880
|
}
|
|
881
|
+
// Start new message
|
|
767
882
|
state.messages.push({ role: 'user', content: evt.message, time: new Date().toISOString() });
|
|
768
883
|
msgIdx = state.messages.length;
|
|
769
|
-
|
|
884
|
+
msgParts = [];
|
|
885
|
+
currentText = '';
|
|
886
|
+
allExecEvents = [];
|
|
770
887
|
fullThought = '';
|
|
771
|
-
|
|
772
|
-
state.messages.push({ role: 'assistant', content: '', thought: '', time: new Date().toISOString(), streaming: true });
|
|
888
|
+
state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
|
|
773
889
|
renderMessages();
|
|
774
890
|
} else if (evt.type === 'clear_text') {
|
|
775
891
|
// Clear intermediate text from previous agent loop iterations
|
|
776
|
-
|
|
777
|
-
state.messages[msgIdx].
|
|
892
|
+
flushCurrentText();
|
|
893
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
894
|
+
state.messages[msgIdx]._streamingText = '';
|
|
895
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '';
|
|
778
896
|
throttledStreamUpdate(msgIdx);
|
|
779
897
|
} else if (evt.type === 'exec_event') {
|
|
780
898
|
// Real-time execution event (tool call, code exec, skill result, etc.)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
899
|
+
flushCurrentText();
|
|
900
|
+
msgParts.push({type: 'exec', data: evt.data});
|
|
901
|
+
allExecEvents.push(evt.data);
|
|
902
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
903
|
+
state.messages[msgIdx]._streamingText = '';
|
|
904
|
+
state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
784
905
|
throttledStreamUpdate(msgIdx);
|
|
785
906
|
} else if (evt.type === 'task_list_update') {
|
|
786
907
|
// 任务列表 JSON 直推更新(exec 模式)
|
|
@@ -803,11 +924,15 @@ async function sendMessage() {
|
|
|
803
924
|
}
|
|
804
925
|
}
|
|
805
926
|
} else if (evt.type === 'done') {
|
|
927
|
+
flushCurrentText();
|
|
806
928
|
// done 事件提供最终事件列表(可能有去重/合并)
|
|
807
929
|
if (evt.exec_events && evt.exec_events.length > 0) {
|
|
808
|
-
|
|
809
|
-
state.messages[msgIdx].exec_events = [...execEventsReceived];
|
|
930
|
+
allExecEvents = evt.exec_events;
|
|
810
931
|
}
|
|
932
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
933
|
+
state.messages[msgIdx]._streamingText = '';
|
|
934
|
+
state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
935
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
|
|
811
936
|
} else if (evt.type === 'reasoning_delta') {
|
|
812
937
|
// 模型推理过程增量文本(OpenAI o1/o3/DeepSeek-R1 等推理模型)
|
|
813
938
|
if (!state.messages[msgIdx].reasoning) state.messages[msgIdx].reasoning = '';
|
|
@@ -818,22 +943,25 @@ async function sendMessage() {
|
|
|
818
943
|
state.messages[msgIdx].reasoning = evt.content;
|
|
819
944
|
throttledStreamUpdate(msgIdx);
|
|
820
945
|
} else if (evt.type === 'error') {
|
|
821
|
-
|
|
822
|
-
|
|
946
|
+
flushCurrentText();
|
|
947
|
+
currentText = '❌ ' + evt.error;
|
|
948
|
+
msgParts.push({type: 'text', content: currentText});
|
|
949
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
950
|
+
state.messages[msgIdx]._streamingText = '';
|
|
951
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n');
|
|
823
952
|
}
|
|
824
953
|
} catch (e) { /* skip malformed */ }
|
|
825
954
|
}
|
|
826
955
|
}
|
|
827
956
|
|
|
828
957
|
// Finalize message
|
|
958
|
+
flushCurrentText();
|
|
829
959
|
if (state.messages[msgIdx]) {
|
|
830
960
|
state.messages[msgIdx].streaming = false;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
state.messages[msgIdx].content = '(无回复)';
|
|
836
|
-
}
|
|
961
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
962
|
+
state.messages[msgIdx]._streamingText = '';
|
|
963
|
+
state.messages[msgIdx].exec_events = allExecEvents;
|
|
964
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
|
|
837
965
|
}
|
|
838
966
|
|
|
839
967
|
// Task list 已通过 SSE task_list_update 事件实时推送,无需再轮询
|