myagent-ai 1.23.19 → 1.23.20
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 +1 -1
- package/web/ui/chat/chat_main.js +292 -304
- package/web/ui/chat/flow_engine.js +96 -41
package/package.json
CHANGED
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2729,6 +2729,295 @@ function groupHistoryMessages(messages) {
|
|
|
2729
2729
|
}
|
|
2730
2730
|
|
|
2731
2731
|
// ── Messages ──
|
|
2732
|
+
// ── [v1.23.20] 统一消息渲染函数:历史消息和流式消息共享 ──
|
|
2733
|
+
// 生成单条消息的完整 HTML(message-row 外壳 + 所有内部内容)
|
|
2734
|
+
// 流式结束后也用此函数重建 DOM,确保历史/流式样式完全一致
|
|
2735
|
+
window.buildMessageHtml = function(msg, idx, agent) {
|
|
2736
|
+
const isUser = msg.role === 'user';
|
|
2737
|
+
if (msg.role === 'tool') return '';
|
|
2738
|
+
|
|
2739
|
+
const avatar = isUser ? '<span style="font-size:18px">👤</span>' : avatarHtml(agent, 32, 'border-radius:8px;');
|
|
2740
|
+
const content = renderMarkdown(msg.content);
|
|
2741
|
+
|
|
2742
|
+
// ── 图片/音视频附件 ──
|
|
2743
|
+
const imageAttachmentHtml = (() => {
|
|
2744
|
+
let parts = [];
|
|
2745
|
+
if (isUser && msg.images && msg.images.length > 0) {
|
|
2746
|
+
for (let _imgIdx = 0; _imgIdx < msg.images.length; _imgIdx++) {
|
|
2747
|
+
const img = msg.images[_imgIdx];
|
|
2748
|
+
const fileId = img.id;
|
|
2749
|
+
let src;
|
|
2750
|
+
let hasFallback = false;
|
|
2751
|
+
if (fileId) {
|
|
2752
|
+
src = '/api/file/' + fileId;
|
|
2753
|
+
if (img._base64) hasFallback = true;
|
|
2754
|
+
} else {
|
|
2755
|
+
src = 'data:' + (img.type || 'image/png') + ';base64,' + (img.data || '');
|
|
2756
|
+
}
|
|
2757
|
+
let onerror = '';
|
|
2758
|
+
if (hasFallback) {
|
|
2759
|
+
onerror = " onerror=\"this.onerror=null;this.src='data:" + escapeHtml(img.type || 'image/png') + ";base64," + img._base64 + "'\"";
|
|
2760
|
+
} else if (fileId) {
|
|
2761
|
+
onerror = " onerror=\"this.onerror=null;this.style.background='var(--bg3)';this.style.minHeight='60px';this.alt='[图片加载失败]'\"";
|
|
2762
|
+
}
|
|
2763
|
+
parts.push('<div class="msg-image-wrapper"><img src="' + src + '" class="msg-image" loading="lazy" alt="' + escapeHtml(img.name || 'image') + '"' + onerror + " onclick=\"openFileViewer('" + (fileId || '') + "', this.src, '" + escapeHtml(img.name || 'image') + "')\" /></div>");
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
const agentFiles = (msg._files || []);
|
|
2767
|
+
if (!isUser && agentFiles.length > 0) {
|
|
2768
|
+
for (const f of agentFiles) {
|
|
2769
|
+
const isImage = f.type && f.type.startsWith('image/');
|
|
2770
|
+
const isAudio = f.type && f.type.startsWith('audio/');
|
|
2771
|
+
const isVideo = f.type && f.type.startsWith('video/');
|
|
2772
|
+
if (isImage && f.id) {
|
|
2773
|
+
parts.push('<div class="msg-image-wrapper agent-image"><img src="/api/file/' + f.id + '" class="msg-image" loading="lazy" alt="' + escapeHtml(f.name || 'image') + '" onerror="this.onerror=null;this.style.background=\'var(--bg3)\';this.style.minHeight=\'60px\';this.alt=\'[图片加载失败]\'" onclick="openFileViewer(\'' + f.id + '\', this.src, \'' + escapeHtml(f.name || 'image') + '\')" /></div>');
|
|
2774
|
+
}
|
|
2775
|
+
if (isAudio && f.id) {
|
|
2776
|
+
parts.push('<div class="msg-media-player"><audio controls src="/api/file/' + f.id + '" style="width:100%;max-width:480px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></audio><div class="msg-media-title">' + escapeHtml(f.name || '音频') + '</div></div>');
|
|
2777
|
+
}
|
|
2778
|
+
if (isVideo && f.id) {
|
|
2779
|
+
parts.push('<div class="msg-media-player"><video controls src="/api/file/' + f.id + '" style="width:100%;max-width:640px;border-radius:8px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></video><div class="msg-media-title">' + escapeHtml(f.name || '视频') + '</div></div>');
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-images">' + parts.join('') + '</div>' : '';
|
|
2784
|
+
})();
|
|
2785
|
+
|
|
2786
|
+
// ── 在线媒体嵌入(YouTube/B站/网易云等) ──
|
|
2787
|
+
const mediaEmbedHtml = (() => {
|
|
2788
|
+
const mediaList = (msg._media || []);
|
|
2789
|
+
if (!mediaList || mediaList.length === 0) return '';
|
|
2790
|
+
let parts = [];
|
|
2791
|
+
for (const m of mediaList) {
|
|
2792
|
+
const isAudio = m.media_type === 'audio';
|
|
2793
|
+
let embedUrl = m.embed_url || '';
|
|
2794
|
+
const title = m.title || (isAudio ? '在线音乐' : '在线视频');
|
|
2795
|
+
const origUrl = m.original_url || embedUrl;
|
|
2796
|
+
if (!embedUrl && !origUrl) continue;
|
|
2797
|
+
if (!embedUrl) {
|
|
2798
|
+
const icon = isAudio ? '🎵' : '🎬';
|
|
2799
|
+
parts.push('<div class="msg-media-embed msg-media-link-card" style="padding:10px 14px;border-radius:8px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);cursor:pointer" onclick="window.open(\'' + escapeHtml(origUrl) + '\',\'_blank\')">' +
|
|
2800
|
+
'<div class="msg-media-header"><span class="msg-media-icon">' + icon + '</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
|
|
2801
|
+
'<span style="margin-left:auto;font-size:12px;opacity:0.6">点击在新窗口打开 ↗</span></div>' +
|
|
2802
|
+
'<div style="font-size:12px;opacity:0.5;margin-top:4px;word-break:break-all">' + escapeHtml(origUrl) + '</div>' +
|
|
2803
|
+
'</div>');
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
// URL → embed URL 转换
|
|
2807
|
+
if (embedUrl && !embedUrl.includes('/embed/') && !embedUrl.includes('/player') && !embedUrl.includes('/outchain/')) {
|
|
2808
|
+
const ytMatch = embedUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
|
|
2809
|
+
if (ytMatch) { embedUrl = 'https://www.youtube.com/embed/' + ytMatch[1]; }
|
|
2810
|
+
const biliMatch = embedUrl.match(/bilibili\.com\/video\/(BV[\w]+)/);
|
|
2811
|
+
if (biliMatch) { embedUrl = 'https://player.bilibili.com/player.html?bvid=' + biliMatch[1] + '&autoplay=0'; }
|
|
2812
|
+
const neteaseMatch = embedUrl.match(/music\.163\.com.*[?&]id=(\d+)/);
|
|
2813
|
+
if (neteaseMatch) { embedUrl = 'https://music.163.com/outchain/player?type=2&id=' + neteaseMatch[1] + '&auto=0&height=66'; }
|
|
2814
|
+
const ymMatch = embedUrl.match(/music\.youtube\.com.*list=([\w-]+)/);
|
|
2815
|
+
if (ymMatch) { embedUrl = 'https://music.youtube.com/embed?list=' + ymMatch[1] + '&layout=full'; }
|
|
2816
|
+
}
|
|
2817
|
+
if (isAudio) {
|
|
2818
|
+
var _knownAudioEmbed = embedUrl && (
|
|
2819
|
+
embedUrl.indexOf('music.163.com/outchain') >= 0 ||
|
|
2820
|
+
embedUrl.indexOf('music.youtube.com/embed') >= 0 ||
|
|
2821
|
+
embedUrl.indexOf('player.bilibili.com') >= 0
|
|
2822
|
+
);
|
|
2823
|
+
var _audioPlayerHtml;
|
|
2824
|
+
if (_knownAudioEmbed) {
|
|
2825
|
+
_audioPlayerHtml = '<iframe src="' + escapeHtml(embedUrl) + '" style="width:100%;max-width:480px;height:80px;border:none;border-radius:8px" loading="lazy" allow="autoplay" sandbox="allow-scripts allow-same-origin allow-presentation"></iframe>';
|
|
2826
|
+
} else {
|
|
2827
|
+
_audioPlayerHtml = '<audio controls src="' + escapeHtml(origUrl) + '" style="width:100%;max-width:480px;border-radius:8px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></audio>' +
|
|
2828
|
+
'<div style="font-size:11px;color:var(--text3);margin-top:4px">部分平台可能因跨域限制无法直接播放,可点击右上角 ↗ 在新窗口打开</div>';
|
|
2829
|
+
}
|
|
2830
|
+
parts.push('<div class="msg-media-embed msg-media-audio">' +
|
|
2831
|
+
'<div class="msg-media-header"><span class="msg-media-icon">🎵</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
|
|
2832
|
+
'<a class="msg-media-link" href="' + escapeHtml(origUrl) + '" target="_blank" rel="noopener" title="在新窗口打开">↗</a></div>' +
|
|
2833
|
+
_audioPlayerHtml +
|
|
2834
|
+
'</div>');
|
|
2835
|
+
} else {
|
|
2836
|
+
parts.push('<div class="msg-media-embed msg-media-video">' +
|
|
2837
|
+
'<div class="msg-media-header"><span class="msg-media-icon">🎬</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
|
|
2838
|
+
'<a class="msg-media-link" href="' + escapeHtml(origUrl) + '" target="_blank" rel="noopener" title="在新窗口打开">↗</a></div>' +
|
|
2839
|
+
'<iframe src="' + escapeHtml(embedUrl) + '" style="width:100%;max-width:640px;height:360px;border:none;border-radius:8px" loading="lazy" allow="autoplay;encrypted-media;picture-in-picture" allowfullscreen></iframe>' +
|
|
2840
|
+
'</div>');
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-media">' + parts.join('') + '</div>' : '';
|
|
2844
|
+
})();
|
|
2845
|
+
|
|
2846
|
+
// ── Web Control ──
|
|
2847
|
+
const wcHtml = (() => {
|
|
2848
|
+
const wcEvents = (msg._webControl || []);
|
|
2849
|
+
if (!wcEvents || wcEvents.length === 0) return '';
|
|
2850
|
+
let lastEvent = wcEvents[wcEvents.length - 1];
|
|
2851
|
+
let wcAction = lastEvent.action || 'open';
|
|
2852
|
+
let wcSessionId = lastEvent.session_id || '';
|
|
2853
|
+
let wcUrl = lastEvent.url || '';
|
|
2854
|
+
let wcTitle = 'Web Control';
|
|
2855
|
+
let wcIcon = '🌐';
|
|
2856
|
+
let wcDesc = '';
|
|
2857
|
+
if (wcAction === 'open') {
|
|
2858
|
+
wcDesc = wcUrl ? '打开网页: ' + wcUrl : '已打开网页控制面板';
|
|
2859
|
+
} else if (wcAction === 'navigate') {
|
|
2860
|
+
wcDesc = '导航到: ' + wcUrl;
|
|
2861
|
+
} else if (wcAction === 'close') {
|
|
2862
|
+
wcDesc = '已关闭控制面板';
|
|
2863
|
+
wcIcon = '⏹';
|
|
2864
|
+
}
|
|
2865
|
+
return '<div class="msg-wc-card" onclick="if(typeof openWCOverlay===\'function\')openWCOverlay(\'' + escapeHtml(wcSessionId) + '\',\'' + escapeHtml(wcUrl) + '\')" title="点击重新打开">' +
|
|
2866
|
+
'<span class="msg-wc-icon">' + wcIcon + '</span>' +
|
|
2867
|
+
'<span class="msg-wc-info"><span class="msg-wc-title">' + wcTitle + '</span><span class="msg-wc-desc">' + escapeHtml(wcDesc) + '</span></span>' +
|
|
2868
|
+
'<span class="msg-wc-arrow">↗</span>' +
|
|
2869
|
+
'</div>';
|
|
2870
|
+
})();
|
|
2871
|
+
|
|
2872
|
+
// ── 文件附件(非图片/音视频) ──
|
|
2873
|
+
const fileAttachmentHtml = (() => {
|
|
2874
|
+
let parts = [];
|
|
2875
|
+
if (isUser && msg.files && msg.files.length > 0) {
|
|
2876
|
+
for (const f of msg.files) {
|
|
2877
|
+
const fileId = f.id;
|
|
2878
|
+
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2879
|
+
const icon = _getFileIcon(f.name || f.type || '');
|
|
2880
|
+
parts.push('<div class="msg-file-item" title="点击预览">' +
|
|
2881
|
+
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2882
|
+
'<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2883
|
+
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2884
|
+
'</span>' +
|
|
2885
|
+
'<span class="msg-file-actions">' +
|
|
2886
|
+
'<a class="msg-file-download" href="/api/file/' + (fileId || '') + '?name=' + encodeURIComponent(f.name || 'file') + '" download="' + escapeHtml(f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
2887
|
+
'</span>' +
|
|
2888
|
+
'</div>');
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
const agentFiles = (msg._files || []);
|
|
2892
|
+
if (!isUser && agentFiles.length > 0) {
|
|
2893
|
+
for (const f of agentFiles) {
|
|
2894
|
+
const isImage = f.type && f.type.startsWith('image/');
|
|
2895
|
+
const isAudio = f.type && f.type.startsWith('audio/');
|
|
2896
|
+
const isVideo = f.type && f.type.startsWith('video/');
|
|
2897
|
+
if (isImage || isAudio || isVideo) continue;
|
|
2898
|
+
const fileId = f.id;
|
|
2899
|
+
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2900
|
+
const icon = _getFileIcon(f.name || f.type || '');
|
|
2901
|
+
parts.push('<div class="msg-file-item agent-file" title="点击预览">' +
|
|
2902
|
+
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2903
|
+
'<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2904
|
+
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2905
|
+
'</span>' +
|
|
2906
|
+
'<span class="msg-file-actions">' +
|
|
2907
|
+
'<a class="msg-file-download" href="/api/file/' + (fileId || '') + '?name=' + encodeURIComponent(f.name || 'file') + '" download="' + escapeHtml(f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
2908
|
+
'</span>' +
|
|
2909
|
+
'</div>');
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-files">' + parts.join('') + '</div>' : '';
|
|
2913
|
+
})();
|
|
2914
|
+
|
|
2915
|
+
// ── Thought / Reasoning ──
|
|
2916
|
+
const thoughtHtml = msg.thought ? (() => {
|
|
2917
|
+
const isStreaming = !!msg.streaming;
|
|
2918
|
+
return '<details class="thought-block ' + (isStreaming ? 'streaming' : '') + '"' + (isStreaming ? ' open' : '') + '>' +
|
|
2919
|
+
'<summary>' +
|
|
2920
|
+
'<span class="thought-icon">🧠</span>' +
|
|
2921
|
+
'<span class="thought-label">Agent 思考过程</span>' +
|
|
2922
|
+
(isStreaming ? '<span class="thought-badge">思考中...</span>' : '<span class="thought-badge">已完成</span>') +
|
|
2923
|
+
'</summary>' +
|
|
2924
|
+
'<div class="thought-content">' + renderMarkdown(msg.thought) + '</div>' +
|
|
2925
|
+
'</details>';
|
|
2926
|
+
})() : '';
|
|
2927
|
+
const reasoningHtml = msg.reasoning ? (() => {
|
|
2928
|
+
const isStreaming = !!msg.streaming;
|
|
2929
|
+
return '<details class="thought-block ' + (isStreaming ? 'streaming' : '') + '"' + (isStreaming ? ' open' : '') + '>' +
|
|
2930
|
+
'<summary>' +
|
|
2931
|
+
'<span class="thought-icon">💡</span>' +
|
|
2932
|
+
'<span class="thought-label">模型推理过程</span>' +
|
|
2933
|
+
(isStreaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>') +
|
|
2934
|
+
'</summary>' +
|
|
2935
|
+
'<div class="thought-content reasoning-content">' + renderMarkdown(msg.reasoning) + '</div>' +
|
|
2936
|
+
'</details>';
|
|
2937
|
+
})() : '';
|
|
2938
|
+
|
|
2939
|
+
const _isSpeakingThis = ttsManager && ttsManager.isPlaying && ttsManager.currentMsgIndex === idx;
|
|
2940
|
+
const actionBtns = msg.content ? '<div class="msg-actions">' +
|
|
2941
|
+
'<button class="msg-action-btn" onclick="copyMessage(' + idx + ')" title="复制">' +
|
|
2942
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>' +
|
|
2943
|
+
'</button>' + (!isUser ?
|
|
2944
|
+
'<button class="msg-action-btn' + (_isSpeakingThis ? ' tts-speaking' : '') + '" onclick="speakMessage(' + idx + ')" title="' + (_isSpeakingThis ? '停止朗读' : '朗读') + '">' +
|
|
2945
|
+
(_isSpeakingThis ?
|
|
2946
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>' :
|
|
2947
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" 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="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>') +
|
|
2948
|
+
'</button>' : '') +
|
|
2949
|
+
'</div>' : '';
|
|
2950
|
+
const ttsIndicator = _isSpeakingThis ?
|
|
2951
|
+
' <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>' : '';
|
|
2952
|
+
|
|
2953
|
+
// ── Bubble content: timeline (parts) or single text ──
|
|
2954
|
+
var bubbleHtml = '';
|
|
2955
|
+
const hasParts = Array.isArray(msg.parts) && msg.parts.length > 0;
|
|
2956
|
+
const hasStreamingText = msg._streamingText && msg._streamingText.trim();
|
|
2957
|
+
const anyContent = msg.content || msg._streamingText || hasParts;
|
|
2958
|
+
const streamingIndicator = msg.streaming && !anyContent && !msg.thought ?
|
|
2959
|
+
'<div class="streaming-indicator"><div class="spinner"></div><div class="streaming-dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div><span style="font-weight:500">Agent 正在思考...</span></div>' : '';
|
|
2960
|
+
|
|
2961
|
+
if (hasParts || hasStreamingText) {
|
|
2962
|
+
var partsInner = '';
|
|
2963
|
+
for (const part of (msg.parts || [])) {
|
|
2964
|
+
if (part.type === 'text' && part.content.trim()) {
|
|
2965
|
+
partsInner += '<div class="timeline-segment">' + renderMarkdown(part.content) + '</div>';
|
|
2966
|
+
} else if (part.type === 'exec') {
|
|
2967
|
+
partsInner += renderInlineExecEvent(part.data, idx);
|
|
2968
|
+
} else if (part.type === 'v2_tool') {
|
|
2969
|
+
partsInner += renderInlineExecEvent(part, idx);
|
|
2970
|
+
} else if (part.type === 'v2_ask') {
|
|
2971
|
+
partsInner += '<div class="v2-ask-user"><div class="v2-ask-icon">❓</div><div class="v2-ask-content">' + renderMarkdown(part.data.question) + '</div></div>';
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (hasStreamingText) {
|
|
2975
|
+
const _cursor = msg.streaming ? '<span class="streaming-cursor"></span>' : '';
|
|
2976
|
+
partsInner += '<div class="timeline-segment">' + renderMarkdown(msg._streamingText) + _cursor + '</div>';
|
|
2977
|
+
}
|
|
2978
|
+
if (partsInner) {
|
|
2979
|
+
bubbleHtml = '<div class="message-bubble msg-bubble-wrapper"><div class="msg-timeline">' + partsInner + '</div></div>';
|
|
2980
|
+
}
|
|
2981
|
+
} else {
|
|
2982
|
+
var renderedContent = content;
|
|
2983
|
+
if (!msg.streaming && !isUser && shouldCollapseContent(msg.content)) {
|
|
2984
|
+
renderedContent = '<details class="file-content-collapse"><summary>📄 文件内容处理结果(点击展开)</summary><div class="collapse-body">' + content + '</div></details>';
|
|
2985
|
+
}
|
|
2986
|
+
if (renderedContent) {
|
|
2987
|
+
bubbleHtml = '<div class="message-bubble msg-bubble-wrapper">' + renderedContent + ttsIndicator + '</div>';
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// ── Task Plan & Finish Reason ──
|
|
2992
|
+
var taskPlanHtml = '';
|
|
2993
|
+
if (!msg.streaming && msg._v2TaskPlan && msg._v2TaskPlan.trim()) {
|
|
2994
|
+
taskPlanHtml = '<div class="v2-task-plan" style="margin-bottom:8px"><div class="v2-task-plan-header" style="font-size:12px;font-weight:600;color:var(--text3);margin-bottom:4px">📋 任务计划</div><div class="v2-task-plan-body">' + renderMarkdown(msg._v2TaskPlan) + '</div></div>';
|
|
2995
|
+
}
|
|
2996
|
+
var finishReasonHtml = '';
|
|
2997
|
+
if (msg._v2FinishReason && msg._v2FinishReason.trim()) {
|
|
2998
|
+
finishReasonHtml = '<div class="v2-finish-reason" style="margin-bottom:8px;padding:8px 12px;border-radius:var(--radius-xs);background:rgba(16,185,129,.08);border-left:3px solid #10b981;font-size:13px;color:var(--text2)"><span style="font-weight:600;color:#10b981;margin-right:6px">✅ 完成原因:</span>' + escapeHtml(msg._v2FinishReason.trim()) + '</div>';
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// ── Assemble full message-row ──
|
|
3002
|
+
return (reasoningHtml ? reasoningHtml : '') +
|
|
3003
|
+
'<div class="message-row ' + msg.role + (msg.streaming ? ' streaming' : '') + '">' +
|
|
3004
|
+
'<div class="message-avatar">' + avatar + '</div>' +
|
|
3005
|
+
'<div class="message-content" style="flex:1;min-width:0;width:100%">' +
|
|
3006
|
+
thoughtHtml +
|
|
3007
|
+
taskPlanHtml +
|
|
3008
|
+
finishReasonHtml +
|
|
3009
|
+
imageAttachmentHtml +
|
|
3010
|
+
mediaEmbedHtml +
|
|
3011
|
+
wcHtml +
|
|
3012
|
+
bubbleHtml +
|
|
3013
|
+
fileAttachmentHtml +
|
|
3014
|
+
streamingIndicator +
|
|
3015
|
+
(msg.time ? '<div class="message-time">' + formatTime(msg.time) + '</div>' : '') +
|
|
3016
|
+
actionBtns +
|
|
3017
|
+
'</div>' +
|
|
3018
|
+
'</div>';
|
|
3019
|
+
};
|
|
3020
|
+
|
|
2732
3021
|
function renderMessages() {
|
|
2733
3022
|
try {
|
|
2734
3023
|
_renderMessagesInner();
|
|
@@ -2802,313 +3091,12 @@ function _renderMessagesInner() {
|
|
|
2802
3091
|
'<button id="loadMoreBtn" class="btn btn-ghost" onclick="loadMoreMessages()" style="font-size:13px;">' +
|
|
2803
3092
|
'📜 加载更多历史消息</button></div>';
|
|
2804
3093
|
}
|
|
3094
|
+
// ── [v1.23.20] 使用统一渲染函数 ──
|
|
2805
3095
|
for (let i = 0; i < state.messages.length; i++) {
|
|
2806
|
-
|
|
2807
|
-
const isUser = msg.role === 'user';
|
|
2808
|
-
|
|
2809
|
-
// Skip standalone tool messages (now grouped into assistant parts via groupHistoryMessages)
|
|
2810
|
-
if (msg.role === 'tool') continue;
|
|
2811
|
-
|
|
2812
|
-
const avatar = isUser ? '<span style="font-size:18px">👤</span>' : avatarHtml(agent, 32, 'border-radius:8px;');
|
|
2813
|
-
const content = renderMarkdown(msg.content);
|
|
2814
|
-
// [v1.19.3] 渲染图片和文件附件(支持磁盘持久化 file_id,图片在气泡顶部、文件在底部)
|
|
2815
|
-
const imageAttachmentHtml = (() => {
|
|
2816
|
-
let parts = [];
|
|
2817
|
-
// User images(用户发送的图片)
|
|
2818
|
-
if (isUser && msg.images && msg.images.length > 0) {
|
|
2819
|
-
for (let _imgIdx = 0; _imgIdx < msg.images.length; _imgIdx++) {
|
|
2820
|
-
const img = msg.images[_imgIdx];
|
|
2821
|
-
const fileId = img.id;
|
|
2822
|
-
let src;
|
|
2823
|
-
let hasFallback = false;
|
|
2824
|
-
if (fileId) {
|
|
2825
|
-
src = '/api/file/' + fileId;
|
|
2826
|
-
if (img._base64) hasFallback = true;
|
|
2827
|
-
} else {
|
|
2828
|
-
src = 'data:' + (img.type || 'image/png') + ';base64,' + (img.data || '');
|
|
2829
|
-
}
|
|
2830
|
-
let onerror = '';
|
|
2831
|
-
if (hasFallback) {
|
|
2832
|
-
onerror = ' onerror="this.onerror=null;this.src=\'data:' + escapeHtml(img.type || 'image/png') + ';base64,' + img._base64 + '\'"';
|
|
2833
|
-
} else if (fileId) {
|
|
2834
|
-
onerror = ' onerror="this.onerror=null;this.style.background=\'var(--bg3)\';this.style.minHeight=\'60px\';this.alt=\'[图片加载失败]\'"';
|
|
2835
|
-
}
|
|
2836
|
-
parts.push('<div class="msg-image-wrapper"><img src="' + src + '" class="msg-image" loading="lazy" alt="' + escapeHtml(img.name || 'image') + '"' + onerror + ' onclick="openFileViewer(\'' + (fileId || '') + '\', this.src, \'' + escapeHtml(img.name || 'image') + '\')" /></div>');
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
// Agent images(agent 通过 file_send 发送的图片文件,type 为 image/*)
|
|
2840
|
-
const agentFiles = (msg._files || []);
|
|
2841
|
-
if (!isUser && agentFiles.length > 0) {
|
|
2842
|
-
for (const f of agentFiles) {
|
|
2843
|
-
const isImage = f.type && f.type.startsWith('image/');
|
|
2844
|
-
const isAudio = f.type && f.type.startsWith('audio/');
|
|
2845
|
-
const isVideo = f.type && f.type.startsWith('video/');
|
|
2846
|
-
if (isImage && f.id) {
|
|
2847
|
-
parts.push('<div class="msg-image-wrapper agent-image"><img src="/api/file/' + f.id + '" class="msg-image" loading="lazy" alt="' + escapeHtml(f.name || 'image') + '" onerror="this.onerror=null;this.style.background=\'var(--bg3)\';this.style.minHeight=\'60px\';this.alt=\'[图片加载失败]\'" onclick="openFileViewer(\'' + f.id + '\', this.src, \'' + escapeHtml(f.name) + '\')" /></div>');
|
|
2848
|
-
}
|
|
2849
|
-
// [v1.20.3] 音频文件渲染为内嵌播放器
|
|
2850
|
-
if (isAudio && f.id) {
|
|
2851
|
-
parts.push('<div class="msg-media-player"><audio controls src="/api/file/' + f.id + '" style="width:100%;max-width:480px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></audio><div class="msg-media-title">' + escapeHtml(f.name || '音频') + '</div></div>');
|
|
2852
|
-
}
|
|
2853
|
-
// [v1.20.3] 视频文件渲染为内嵌播放器
|
|
2854
|
-
if (isVideo && f.id) {
|
|
2855
|
-
parts.push('<div class="msg-media-player"><video controls src="/api/file/' + f.id + '" style="width:100%;max-width:640px;border-radius:8px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></video><div class="msg-media-title">' + escapeHtml(f.name || '视频') + '</div></div>');
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-images">' + parts.join('') + '</div>' : '';
|
|
2860
|
-
})();
|
|
2861
|
-
// [v1.20.3] 在线媒体嵌入播放器(YouTube/B站/抖音/QQ音乐/网易云等)
|
|
2862
|
-
const mediaEmbedHtml = (() => {
|
|
2863
|
-
const mediaList = (msg._media || []);
|
|
2864
|
-
if (!mediaList || mediaList.length === 0) return '';
|
|
2865
|
-
let parts = [];
|
|
2866
|
-
for (const m of mediaList) {
|
|
2867
|
-
const isAudio = m.media_type === 'audio';
|
|
2868
|
-
let embedUrl = m.embed_url || '';
|
|
2869
|
-
const title = m.title || (isAudio ? '在线音乐' : '在线视频');
|
|
2870
|
-
const origUrl = m.original_url || embedUrl;
|
|
2871
|
-
if (!embedUrl && !origUrl) continue;
|
|
2872
|
-
// [v1.20.10] 如果 embed_url 为空,渲染链接卡片而非 iframe
|
|
2873
|
-
if (!embedUrl) {
|
|
2874
|
-
const icon = isAudio ? '🎵' : '🎬';
|
|
2875
|
-
parts.push('<div class="msg-media-embed msg-media-link-card" style="padding:10px 14px;border-radius:8px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);cursor:pointer" onclick="window.open(\'' + escapeHtml(origUrl) + '\',\'_blank\')">' +
|
|
2876
|
-
'<div class="msg-media-header"><span class="msg-media-icon">' + icon + '</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
|
|
2877
|
-
'<span style="margin-left:auto;font-size:12px;opacity:0.6">点击在新窗口打开 ↗</span></div>' +
|
|
2878
|
-
'<div style="font-size:12px;opacity:0.5;margin-top:4px;word-break:break-all">' + escapeHtml(origUrl) + '</div>' +
|
|
2879
|
-
'</div>');
|
|
2880
|
-
continue;
|
|
2881
|
-
}
|
|
2882
|
-
// 前端 URL → 嵌入 URL 转换(历史回放时 embed_url 可能是原始 URL)
|
|
2883
|
-
if (embedUrl && !embedUrl.includes('/embed/') && !embedUrl.includes('/player') && !embedUrl.includes('/outchain/')) {
|
|
2884
|
-
const ytMatch = embedUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
|
|
2885
|
-
if (ytMatch) { embedUrl = 'https://www.youtube.com/embed/' + ytMatch[1]; }
|
|
2886
|
-
const biliMatch = embedUrl.match(/bilibili\.com\/video\/(BV[\w]+)/);
|
|
2887
|
-
if (biliMatch) { embedUrl = 'https://player.bilibili.com/player.html?bvid=' + biliMatch[1] + '&autoplay=0'; }
|
|
2888
|
-
const neteaseMatch = embedUrl.match(/music\.163\.com.*[?&]id=(\d+)/);
|
|
2889
|
-
if (neteaseMatch) { embedUrl = 'https://music.163.com/outchain/player?type=2&id=' + neteaseMatch[1] + '&auto=0&height=66'; }
|
|
2890
|
-
// [v1.20.10] YouTube Music 播放列表 → embed
|
|
2891
|
-
const ymMatch = embedUrl.match(/music\.youtube\.com.*list=([\w-]+)/);
|
|
2892
|
-
if (ymMatch) { embedUrl = 'https://music.youtube.com/embed?list=' + ymMatch[1] + '&layout=full'; }
|
|
2893
|
-
}
|
|
2894
|
-
if (isAudio) {
|
|
2895
|
-
// [v1.20.11] 音频播放器:优先使用已知可嵌入平台的 iframe,否则使用原生 <audio> 标签
|
|
2896
|
-
// 避免 iframe 加载失败时显示断裂图片图标
|
|
2897
|
-
var _knownAudioEmbed = embedUrl && (
|
|
2898
|
-
embedUrl.indexOf('music.163.com/outchain') >= 0 ||
|
|
2899
|
-
embedUrl.indexOf('music.youtube.com/embed') >= 0 ||
|
|
2900
|
-
embedUrl.indexOf('player.bilibili.com') >= 0
|
|
2901
|
-
);
|
|
2902
|
-
var _audioPlayerHtml;
|
|
2903
|
-
if (_knownAudioEmbed) {
|
|
2904
|
-
_audioPlayerHtml = '<iframe src="' + escapeHtml(embedUrl) + '" style="width:100%;max-width:480px;height:80px;border:none;border-radius:8px" loading="lazy" allow="autoplay" sandbox="allow-scripts allow-same-origin allow-presentation"></iframe>';
|
|
2905
|
-
} else {
|
|
2906
|
-
// 不可嵌入的音频链接:使用原生 audio 播放器,避免 iframe 断裂图标
|
|
2907
|
-
_audioPlayerHtml = '<audio controls src="' + escapeHtml(origUrl) + '" style="width:100%;max-width:480px;border-radius:8px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></audio>' +
|
|
2908
|
-
'<div style="font-size:11px;color:var(--text3);margin-top:4px">部分平台可能因跨域限制无法直接播放,可点击右上角 ↗ 在新窗口打开</div>';
|
|
2909
|
-
}
|
|
2910
|
-
parts.push('<div class="msg-media-embed msg-media-audio">' +
|
|
2911
|
-
'<div class="msg-media-header"><span class="msg-media-icon">🎵</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
|
|
2912
|
-
'<a class="msg-media-link" href="' + escapeHtml(origUrl) + '" target="_blank" rel="noopener" title="在新窗口打开">↗</a></div>' +
|
|
2913
|
-
_audioPlayerHtml +
|
|
2914
|
-
'</div>');
|
|
2915
|
-
} else {
|
|
2916
|
-
parts.push('<div class="msg-media-embed msg-media-video">' +
|
|
2917
|
-
'<div class="msg-media-header"><span class="msg-media-icon">🎬</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
|
|
2918
|
-
'<a class="msg-media-link" href="' + escapeHtml(origUrl) + '" target="_blank" rel="noopener" title="在新窗口打开">↗</a></div>' +
|
|
2919
|
-
'<iframe src="' + escapeHtml(embedUrl) + '" style="width:100%;max-width:640px;height:360px;border:none;border-radius:8px" loading="lazy" allow="autoplay;encrypted-media;picture-in-picture" allowfullscreen></iframe>' +
|
|
2920
|
-
'</div>');
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-media">' + parts.join('') + '</div>' : '';
|
|
2924
|
-
})();
|
|
2925
|
-
|
|
2926
|
-
// [v1.21.0] Web Control 渲染(显示面板状态卡片)
|
|
2927
|
-
const wcHtml = (() => {
|
|
2928
|
-
const wcEvents = (msg._webControl || []);
|
|
2929
|
-
if (!wcEvents || wcEvents.length === 0) return '';
|
|
2930
|
-
let lastEvent = wcEvents[wcEvents.length - 1];
|
|
2931
|
-
let wcAction = lastEvent.action || 'open';
|
|
2932
|
-
let wcSessionId = lastEvent.session_id || '';
|
|
2933
|
-
let wcUrl = lastEvent.url || '';
|
|
2934
|
-
let wcTitle = 'Web Control';
|
|
2935
|
-
let wcIcon = '🌐';
|
|
2936
|
-
let wcDesc = '';
|
|
2937
|
-
if (wcAction === 'open') {
|
|
2938
|
-
wcDesc = wcUrl ? '打开网页: ' + wcUrl : '已打开网页控制面板';
|
|
2939
|
-
} else if (wcAction === 'navigate') {
|
|
2940
|
-
wcDesc = '导航到: ' + wcUrl;
|
|
2941
|
-
} else if (wcAction === 'close') {
|
|
2942
|
-
wcDesc = '已关闭控制面板';
|
|
2943
|
-
wcIcon = '⏹';
|
|
2944
|
-
}
|
|
2945
|
-
return '<div class="msg-wc-card" onclick="if(typeof openWCOverlay===\'function\')openWCOverlay(\'' + escapeHtml(wcSessionId) + '\',\'' + escapeHtml(wcUrl) + '\')" title="点击重新打开">' +
|
|
2946
|
-
'<span class="msg-wc-icon">' + wcIcon + '</span>' +
|
|
2947
|
-
'<span class="msg-wc-info"><span class="msg-wc-title">' + wcTitle + '</span><span class="msg-wc-desc">' + escapeHtml(wcDesc) + '</span></span>' +
|
|
2948
|
-
'<span class="msg-wc-arrow">↗</span>' +
|
|
2949
|
-
'</div>';
|
|
2950
|
-
})();
|
|
2951
|
-
|
|
2952
|
-
const fileAttachmentHtml = (() => {
|
|
2953
|
-
let parts = [];
|
|
2954
|
-
// User files(用户发送的非图片文件)
|
|
2955
|
-
if (isUser && msg.files && msg.files.length > 0) {
|
|
2956
|
-
for (const f of msg.files) {
|
|
2957
|
-
const fileId = f.id;
|
|
2958
|
-
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2959
|
-
const icon = _getFileIcon(f.name || f.type || '');
|
|
2960
|
-
parts.push('<div class="msg-file-item" title="点击预览">' +
|
|
2961
|
-
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2962
|
-
'<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2963
|
-
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2964
|
-
'</span>' +
|
|
2965
|
-
'<span class="msg-file-actions">' +
|
|
2966
|
-
'<a class="msg-file-download" href="/api/file/' + (fileId || '') + '?name=' + encodeURIComponent(f.name || 'file') + '" download="' + escapeHtml(f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
2967
|
-
'</span>' +
|
|
2968
|
-
'</div>');
|
|
2969
|
-
}
|
|
2970
|
-
}
|
|
2971
|
-
// Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files(只渲染非图片/音频/视频文件)
|
|
2972
|
-
const agentFiles = (msg._files || []);
|
|
2973
|
-
if (!isUser && agentFiles.length > 0) {
|
|
2974
|
-
for (const f of agentFiles) {
|
|
2975
|
-
const isImage = f.type && f.type.startsWith('image/');
|
|
2976
|
-
const isAudio = f.type && f.type.startsWith('audio/');
|
|
2977
|
-
const isVideo = f.type && f.type.startsWith('video/');
|
|
2978
|
-
if (isImage || isAudio || isVideo) continue; // 图片/音视频已在上方渲染为内嵌播放器
|
|
2979
|
-
const fileId = f.id;
|
|
2980
|
-
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2981
|
-
const icon = _getFileIcon(f.name || f.type || '');
|
|
2982
|
-
parts.push('<div class="msg-file-item agent-file" title="点击预览">' +
|
|
2983
|
-
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2984
|
-
'<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2985
|
-
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2986
|
-
'</span>' +
|
|
2987
|
-
'<span class="msg-file-actions">' +
|
|
2988
|
-
'<a class="msg-file-download" href="/api/file/' + (fileId || '') + '?name=' + encodeURIComponent(f.name || 'file') + '" download="' + escapeHtml(f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
2989
|
-
'</span>' +
|
|
2990
|
-
'</div>');
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-files">' + parts.join('') + '</div>' : '';
|
|
2994
|
-
})();
|
|
2995
|
-
const thoughtHtml = msg.thought ? (() => {
|
|
2996
|
-
const isStreaming = !!msg.streaming;
|
|
2997
|
-
return `<details class="thought-block ${isStreaming ? 'streaming' : ''}" ${isStreaming ? 'open' : ''}>
|
|
2998
|
-
<summary>
|
|
2999
|
-
<span class="thought-icon">🧠</span>
|
|
3000
|
-
<span class="thought-label">Agent 思考过程</span>
|
|
3001
|
-
${isStreaming ? '<span class="thought-badge">思考中...</span>' : '<span class="thought-badge">已完成</span>'}
|
|
3002
|
-
</summary>
|
|
3003
|
-
<div class="thought-content">${renderMarkdown(msg.thought)}</div>
|
|
3004
|
-
</details>`;
|
|
3005
|
-
})() : '';
|
|
3006
|
-
const reasoningHtml = msg.reasoning ? (() => {
|
|
3007
|
-
const isStreaming = !!msg.streaming;
|
|
3008
|
-
return `<details class="thought-block ${isStreaming ? 'streaming' : ''}" ${isStreaming ? 'open' : ''}>
|
|
3009
|
-
<summary>
|
|
3010
|
-
<span class="thought-icon">💡</span>
|
|
3011
|
-
<span class="thought-label">模型推理过程</span>
|
|
3012
|
-
${isStreaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
|
|
3013
|
-
</summary>
|
|
3014
|
-
<div class="thought-content reasoning-content">${renderMarkdown(msg.reasoning)}</div>
|
|
3015
|
-
</details>`;
|
|
3016
|
-
})() : '';
|
|
3017
|
-
const _isSpeakingThis = ttsManager && ttsManager.isPlaying && ttsManager.currentMsgIndex === i;
|
|
3018
|
-
const actionBtns = msg.content ? `
|
|
3019
|
-
<div class="msg-actions">
|
|
3020
|
-
<button class="msg-action-btn" onclick="copyMessage(${i})" title="复制">
|
|
3021
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
3022
|
-
</button>${!isUser ? `
|
|
3023
|
-
<button class="msg-action-btn${_isSpeakingThis ? ' tts-speaking' : ''}" onclick="speakMessage(${i})" title="${_isSpeakingThis ? '停止朗读' : '朗读'}">
|
|
3024
|
-
${_isSpeakingThis
|
|
3025
|
-
? '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>'
|
|
3026
|
-
: '<svg viewBox="0 0 24 24" width="14" height="14" 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="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>'
|
|
3027
|
-
}
|
|
3028
|
-
</button>` : ''}
|
|
3029
|
-
</div>` : '';
|
|
3030
|
-
const ttsIndicator = _isSpeakingThis ?
|
|
3031
|
-
' <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>' : '';
|
|
3032
|
-
|
|
3033
|
-
// ── Determine rendering mode ──
|
|
3034
|
-
const hasParts = Array.isArray(msg.parts) && msg.parts.length > 0;
|
|
3035
|
-
const hasStreamingText = msg._streamingText && msg._streamingText.trim();
|
|
3036
|
-
const anyContent = msg.content || msg._streamingText || hasParts;
|
|
3037
|
-
const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
|
|
3038
|
-
<div class="streaming-indicator">
|
|
3039
|
-
<div class="spinner"></div>
|
|
3040
|
-
<div class="streaming-dots">
|
|
3041
|
-
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
|
3042
|
-
</div>
|
|
3043
|
-
<span style="font-weight:500">Agent 正在思考...</span>
|
|
3044
|
-
</div>` : '';
|
|
3045
|
-
|
|
3046
|
-
// ── Bubble content: timeline (parts) or single text ──
|
|
3047
|
-
var bubbleHtml = '';
|
|
3048
|
-
if (hasParts || hasStreamingText) {
|
|
3049
|
-
// Timeline mode: interleaved text + tool cards
|
|
3050
|
-
var partsInner = '';
|
|
3051
|
-
for (const part of (msg.parts || [])) {
|
|
3052
|
-
if (part.type === 'text' && part.content.trim()) {
|
|
3053
|
-
partsInner += '<div class="timeline-segment">' + renderMarkdown(part.content) + '</div>';
|
|
3054
|
-
} else if (part.type === 'exec') {
|
|
3055
|
-
partsInner += renderInlineExecEvent(part.data, i);
|
|
3056
|
-
} else if (part.type === 'v2_tool') {
|
|
3057
|
-
partsInner += renderInlineExecEvent(part, i);
|
|
3058
|
-
} else if (part.type === 'v2_ask') {
|
|
3059
|
-
partsInner += '<div class="v2-ask-user"><div class="v2-ask-icon">❓</div><div class="v2-ask-content">' + renderMarkdown(part.data.question) + '</div></div>';
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
if (hasStreamingText) {
|
|
3063
|
-
const _cursor = msg.streaming ? '<span class="streaming-cursor"></span>' : '';
|
|
3064
|
-
partsInner += '<div class="timeline-segment">' + renderMarkdown(msg._streamingText) + _cursor + '</div>';
|
|
3065
|
-
}
|
|
3066
|
-
if (partsInner) {
|
|
3067
|
-
bubbleHtml = '<div class="message-bubble msg-bubble-wrapper"><div class="msg-timeline">' + partsInner + '</div></div>';
|
|
3068
|
-
}
|
|
3069
|
-
} else {
|
|
3070
|
-
// Single bubble mode: plain text (possibly collapsed)
|
|
3071
|
-
var renderedContent = content;
|
|
3072
|
-
if (!msg.streaming && !isUser && shouldCollapseContent(msg.content)) {
|
|
3073
|
-
renderedContent = '<details class="file-content-collapse"><summary>📄 文件内容处理结果(点击展开)</summary><div class="collapse-body">' + content + '</div></details>';
|
|
3074
|
-
}
|
|
3075
|
-
if (renderedContent) {
|
|
3076
|
-
bubbleHtml = '<div class="message-bubble msg-bubble-wrapper">' + renderedContent + ttsIndicator + '</div>';
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
// ── Task Plan & Finish Reason (V2 structured output, historical only) ──
|
|
3081
|
-
var taskPlanHtml = '';
|
|
3082
|
-
if (!msg.streaming && msg._v2TaskPlan && msg._v2TaskPlan.trim()) {
|
|
3083
|
-
taskPlanHtml = '<div class="v2-task-plan" style="margin-bottom:8px"><div class="v2-task-plan-header" style="font-size:12px;font-weight:600;color:var(--text3);margin-bottom:4px">📋 任务计划</div><div class="v2-task-plan-body">' + renderMarkdown(msg._v2TaskPlan) + '</div></div>';
|
|
3084
|
-
}
|
|
3085
|
-
var finishReasonHtml = '';
|
|
3086
|
-
if (msg._v2FinishReason && msg._v2FinishReason.trim()) {
|
|
3087
|
-
finishReasonHtml = '<div class="v2-finish-reason" style="margin-bottom:8px;padding:8px 12px;border-radius:var(--radius-xs);background:rgba(16,185,129,.08);border-left:3px solid #10b981;font-size:13px;color:var(--text2)"><span style="font-weight:600;color:#10b981;margin-right:6px">✅ 完成原因:</span>' + escapeHtml(msg._v2FinishReason.trim()) + '</div>';
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
// ── Reasoning block (rendered outside message-row for full width) ──
|
|
3091
|
-
if (reasoningHtml) html += reasoningHtml;
|
|
3092
|
-
// ── Message row ──
|
|
3093
|
-
html += `
|
|
3094
|
-
<div class="message-row ${msg.role}${msg.streaming ? ' streaming' : ''}">
|
|
3095
|
-
<div class="message-avatar">${avatar}</div>
|
|
3096
|
-
<div class="message-content" style="flex:1;min-width:0;width:100%">
|
|
3097
|
-
${thoughtHtml}
|
|
3098
|
-
${taskPlanHtml}
|
|
3099
|
-
${finishReasonHtml}
|
|
3100
|
-
${imageAttachmentHtml}
|
|
3101
|
-
${mediaEmbedHtml}
|
|
3102
|
-
${wcHtml}
|
|
3103
|
-
${bubbleHtml}
|
|
3104
|
-
${fileAttachmentHtml}
|
|
3105
|
-
${streamingIndicator}
|
|
3106
|
-
${msg.time ? '<div class="message-time">' + formatTime(msg.time) + '</div>' : ''}
|
|
3107
|
-
${actionBtns}
|
|
3108
|
-
</div>
|
|
3109
|
-
</div>`;
|
|
3096
|
+
html += buildMessageHtml(state.messages[i], i, agent) || '';
|
|
3110
3097
|
}
|
|
3111
3098
|
|
|
3099
|
+
// Keep welcome card in DOM but hidden
|
|
3112
3100
|
// Keep welcome card in DOM but hidden
|
|
3113
3101
|
container.innerHTML = html +
|
|
3114
3102
|
'<div class="welcome-card" id="welcomeCard" style="display:none"></div>';
|
|
@@ -607,51 +607,72 @@ function updateStreamingMessage(msgIdx) {
|
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
// ── [v1.23.15] 增量渲染文件卡片(v2_file 事件到达时立即显示) ──
|
|
610
|
+
// [v1.23.20] DOM 结构完全对齐 buildMessageHtml:
|
|
611
|
+
// 图片/音视频 → .msg-attachments.msg-attachments-images(在 bubble 之前)
|
|
612
|
+
// 非媒体文件 → .msg-attachments.msg-attachments-files(在 bubble 之后)
|
|
610
613
|
if (msg._files && msg._files.length > 0) {
|
|
611
|
-
|
|
614
|
+
// ── 图片/音视频容器(和 buildMessageHtml 的 imageAttachmentHtml 一致) ──
|
|
615
|
+
var imageContainer = contentArea.querySelector(':scope > .msg-attachments-images');
|
|
616
|
+
// ── 非媒体文件容器(和 buildMessageHtml 的 fileAttachmentHtml 一致) ──
|
|
617
|
+
var existingFiles = contentArea.querySelectorAll(':scope > .msg-attachments-files');
|
|
612
618
|
var fileContainer = existingFiles.length > 0 ? existingFiles[0] : null;
|
|
613
|
-
|
|
614
|
-
fileContainer = document.createElement('div');
|
|
615
|
-
fileContainer.className = 'msg-attachments msg-attachments-files';
|
|
616
|
-
if (bubbleWrapper) {
|
|
617
|
-
bubbleWrapper.appendChild(fileContainer);
|
|
618
|
-
} else {
|
|
619
|
-
contentArea.appendChild(fileContainer);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
619
|
+
|
|
622
620
|
// 只添加未渲染的新文件
|
|
623
|
-
var renderedIds = fileContainer._renderedFileIds || [];
|
|
621
|
+
var renderedIds = (fileContainer ? fileContainer._renderedFileIds : []) || [];
|
|
622
|
+
var renderedImageIds = (imageContainer ? imageContainer._renderedImageIds : []) || [];
|
|
624
623
|
for (var _fIdx = 0; _fIdx < msg._files.length; _fIdx++) {
|
|
625
624
|
var _f = msg._files[_fIdx];
|
|
626
625
|
var _fId = _f.id || _f.file_id || '';
|
|
627
|
-
|
|
626
|
+
// 同时检查两个容器的已渲染列表
|
|
627
|
+
if (renderedIds.indexOf(_fId) >= 0 || renderedImageIds.indexOf(_fId) >= 0) continue;
|
|
628
628
|
var _isImg = _f.type && _f.type.indexOf('image/') === 0;
|
|
629
629
|
var _isAud = _f.type && _f.type.indexOf('audio/') === 0;
|
|
630
630
|
var _isVid = _f.type && _f.type.indexOf('video/') === 0;
|
|
631
|
-
|
|
632
|
-
if (_isImg
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
631
|
+
|
|
632
|
+
if (_isImg || _isAud || _isVid) {
|
|
633
|
+
// 图片/音频/视频 → imageContainer(和 buildMessageHtml 的 imageAttachmentHtml 一致)
|
|
634
|
+
if (!imageContainer) {
|
|
635
|
+
imageContainer = document.createElement('div');
|
|
636
|
+
imageContainer.className = 'msg-attachments msg-attachments-images';
|
|
637
|
+
imageContainer._renderedImageIds = [];
|
|
638
|
+
// 插入到 bubbleWrapper 之前(和 buildMessageHtml 顺序一致:images 先,media 后,bubble 后)
|
|
639
|
+
if (bubbleWrapper && bubbleWrapper.parentNode) {
|
|
640
|
+
bubbleWrapper.parentNode.insertBefore(imageContainer, bubbleWrapper);
|
|
641
|
+
} else {
|
|
642
|
+
contentArea.appendChild(imageContainer);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (_isImg && _fId) {
|
|
646
|
+
var _imgDiv = document.createElement('div');
|
|
647
|
+
_imgDiv.className = 'msg-image-wrapper agent-image';
|
|
648
|
+
_imgDiv.innerHTML = '<img src="/api/file/' + _fId + '" class="msg-image" loading="lazy" alt="' + escapeHtml(_f.name || 'image') + '" onerror="this.onerror=null;this.style.background=\'var(--bg3)\';this.style.minHeight=\'60px\';this.alt=\'[图片加载失败]\'" onclick="openFileViewer(\'' + _fId + '\', this.src, \'' + escapeHtml(_f.name) + '\')" />';
|
|
649
|
+
imageContainer.appendChild(_imgDiv);
|
|
650
|
+
} else if (_isAud && _fId) {
|
|
651
|
+
var _audDiv = document.createElement('div');
|
|
652
|
+
_audDiv.className = 'msg-media-player';
|
|
653
|
+
_audDiv.innerHTML = '<audio controls src="/api/file/' + _fId + '" style="width:100%;max-width:480px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></audio><div class="msg-media-title">' + escapeHtml(_f.name || '音频') + '</div>';
|
|
654
|
+
imageContainer.appendChild(_audDiv);
|
|
655
|
+
} else if (_isVid && _fId) {
|
|
656
|
+
var _vidDiv = document.createElement('div');
|
|
657
|
+
_vidDiv.className = 'msg-media-player';
|
|
658
|
+
_vidDiv.innerHTML = '<video controls src="/api/file/' + _fId + '" style="width:100%;max-width:640px;border-radius:8px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></video><div class="msg-media-title">' + escapeHtml(_f.name || '视频') + '</div>';
|
|
659
|
+
imageContainer.appendChild(_vidDiv);
|
|
660
|
+
}
|
|
661
|
+
imageContainer._renderedImageIds.push(_fId);
|
|
646
662
|
continue;
|
|
647
663
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
fileContainer.
|
|
653
|
-
|
|
654
|
-
|
|
664
|
+
|
|
665
|
+
// 非媒体文件 → fileContainer
|
|
666
|
+
if (!fileContainer) {
|
|
667
|
+
fileContainer = document.createElement('div');
|
|
668
|
+
fileContainer.className = 'msg-attachments msg-attachments-files';
|
|
669
|
+
fileContainer._renderedFileIds = [];
|
|
670
|
+
// 插入到 bubbleWrapper 之后(和 buildMessageHtml 顺序一致)
|
|
671
|
+
if (bubbleWrapper && bubbleWrapper.parentNode) {
|
|
672
|
+
bubbleWrapper.parentNode.insertBefore(fileContainer, bubbleWrapper.nextSibling);
|
|
673
|
+
} else {
|
|
674
|
+
contentArea.appendChild(fileContainer);
|
|
675
|
+
}
|
|
655
676
|
}
|
|
656
677
|
var _icon = _getFileIcon(_f.name || _f.type || '');
|
|
657
678
|
var _sizeStr = _f.size ? formatFileSize(_f.size) : '';
|
|
@@ -666,20 +687,21 @@ function updateStreamingMessage(msgIdx) {
|
|
|
666
687
|
'<a class="msg-file-download" href="/api/file/' + (_fId || '') + '?name=' + encodeURIComponent(_f.name || 'file') + '" download="' + escapeHtml(_f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
667
688
|
'</span>';
|
|
668
689
|
fileContainer.appendChild(_fDiv);
|
|
669
|
-
|
|
690
|
+
fileContainer._renderedFileIds.push(_fId);
|
|
670
691
|
}
|
|
671
|
-
fileContainer._renderedFileIds = renderedIds;
|
|
672
692
|
}
|
|
673
693
|
|
|
674
694
|
// ── [v1.23.19] 增量渲染在线媒体嵌入播放器(v2_media 事件) ──
|
|
695
|
+
// [v1.23.20] 媒体容器放在 bubbleWrapper 外部(和 buildMessageHtml 结构一致)
|
|
675
696
|
if (msg._media && msg._media.length > 0) {
|
|
676
|
-
var existingMedia =
|
|
697
|
+
var existingMedia = contentArea.querySelectorAll(':scope > .msg-attachments-media');
|
|
677
698
|
var mediaContainer = existingMedia.length > 0 ? existingMedia[0] : null;
|
|
678
699
|
if (!mediaContainer) {
|
|
679
700
|
mediaContainer = document.createElement('div');
|
|
680
701
|
mediaContainer.className = 'msg-attachments msg-attachments-media';
|
|
681
|
-
|
|
682
|
-
|
|
702
|
+
// 插入到 bubbleWrapper 之前(和 buildMessageHtml 顺序一致:media 先,bubble 后)
|
|
703
|
+
if (bubbleWrapper && bubbleWrapper.parentNode) {
|
|
704
|
+
bubbleWrapper.parentNode.insertBefore(mediaContainer, bubbleWrapper);
|
|
683
705
|
} else {
|
|
684
706
|
contentArea.appendChild(mediaContainer);
|
|
685
707
|
}
|
|
@@ -2116,8 +2138,41 @@ async function sendMessage(opts) {
|
|
|
2116
2138
|
// Assemble final content: prefer V2 reasoning/ask text over raw XML
|
|
2117
2139
|
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
2118
2140
|
}
|
|
2119
|
-
//
|
|
2120
|
-
renderMessages()
|
|
2141
|
+
// ── [v1.23.20] 用统一渲染函数重建最后一条消息 DOM(和历史渲染完全一致) ──
|
|
2142
|
+
// 不再调 renderMessages() 全量重建(会丢失流式增量渲染的文件卡片等)
|
|
2143
|
+
var agent = findAgentByPath(state.activeAgent);
|
|
2144
|
+
var lastMsgRow = document.querySelector('#messagesInner .message-row:last-child');
|
|
2145
|
+
if (lastMsgRow && agent && state.messages[msgIdx]) {
|
|
2146
|
+
var newHtml = buildMessageHtml(state.messages[msgIdx], msgIdx, agent);
|
|
2147
|
+
if (newHtml) {
|
|
2148
|
+
// buildMessageHtml 可能返回 reasoningHtml + message-row(两个顶层元素)
|
|
2149
|
+
var wrapper = document.createElement('div');
|
|
2150
|
+
wrapper.innerHTML = newHtml;
|
|
2151
|
+
var children = Array.from(wrapper.children);
|
|
2152
|
+
// 移除旧的 reasoning block(container 直接子元素,在 message-row 之前)
|
|
2153
|
+
var prevSibling = lastMsgRow.previousElementSibling;
|
|
2154
|
+
if (prevSibling && prevSibling.classList && prevSibling.classList.contains('thought-block')) {
|
|
2155
|
+
var label = prevSibling.querySelector('.thought-label');
|
|
2156
|
+
if (label && label.textContent.includes('模型推理过程')) {
|
|
2157
|
+
prevSibling.remove();
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
// 替换 message-row
|
|
2161
|
+
if (children.length >= 2) {
|
|
2162
|
+
// 有 reasoning block + message-row
|
|
2163
|
+
lastMsgRow.replaceWith(children[children.length - 1]);
|
|
2164
|
+
// reasoning block 插入到 message-row 之前
|
|
2165
|
+
for (var ci = 0; ci < children.length - 1; ci++) {
|
|
2166
|
+
lastMsgRow.parentNode.insertBefore(children[ci], lastMsgRow);
|
|
2167
|
+
}
|
|
2168
|
+
} else if (children.length === 1) {
|
|
2169
|
+
lastMsgRow.replaceWith(children[0]);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
} else {
|
|
2173
|
+
// Fallback: full re-render (should not normally happen)
|
|
2174
|
+
renderMessages();
|
|
2175
|
+
}
|
|
2121
2176
|
// Auto-collapse thought blocks after streaming completes (smooth UX)
|
|
2122
2177
|
setTimeout(function() {
|
|
2123
2178
|
const thoughtBlocks = document.querySelectorAll('.thought-block:not(.streaming)');
|