myagent-ai 1.23.19 → 1.23.21
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/chatbot/whatsapp_bot.py +0 -0
- package/chatbot/whatsapp_bridge/bridge.mjs +0 -0
- package/chatbot/whatsapp_bridge/package.json +0 -0
- package/core/stt.py +0 -0
- package/core/tool_dispatcher.py +0 -0
- package/core/web_control.py +0 -0
- package/data/novnc/lib/base64.js +0 -0
- package/data/novnc/lib/crypto/aes.js +0 -0
- package/data/novnc/lib/crypto/bigint.js +0 -0
- package/data/novnc/lib/crypto/crypto.js +0 -0
- package/data/novnc/lib/crypto/des.js +0 -0
- package/data/novnc/lib/crypto/dh.js +0 -0
- package/data/novnc/lib/crypto/md5.js +0 -0
- package/data/novnc/lib/crypto/rsa.js +0 -0
- package/data/novnc/lib/decoders/copyrect.js +0 -0
- package/data/novnc/lib/decoders/hextile.js +0 -0
- package/data/novnc/lib/decoders/jpeg.js +0 -0
- package/data/novnc/lib/decoders/raw.js +0 -0
- package/data/novnc/lib/decoders/rre.js +0 -0
- package/data/novnc/lib/decoders/tight.js +0 -0
- package/data/novnc/lib/decoders/tightpng.js +0 -0
- package/data/novnc/lib/decoders/zrle.js +0 -0
- package/data/novnc/lib/deflator.js +0 -0
- package/data/novnc/lib/display.js +0 -0
- package/data/novnc/lib/encodings.js +0 -0
- package/data/novnc/lib/inflator.js +0 -0
- package/data/novnc/lib/input/domkeytable.js +0 -0
- package/data/novnc/lib/input/fixedkeys.js +0 -0
- package/data/novnc/lib/input/gesturehandler.js +0 -0
- package/data/novnc/lib/input/keyboard.js +0 -0
- package/data/novnc/lib/input/keysym.js +0 -0
- package/data/novnc/lib/input/keysymdef.js +0 -0
- package/data/novnc/lib/input/util.js +0 -0
- package/data/novnc/lib/input/vkeys.js +0 -0
- package/data/novnc/lib/input/xtscancodes.js +0 -0
- package/data/novnc/lib/ra2.js +0 -0
- package/data/novnc/lib/rfb.js +0 -0
- package/data/novnc/lib/util/browser.js +0 -0
- package/data/novnc/lib/util/cursor.js +0 -0
- package/data/novnc/lib/util/element.js +0 -0
- package/data/novnc/lib/util/events.js +0 -0
- package/data/novnc/lib/util/eventtarget.js +0 -0
- package/data/novnc/lib/util/int.js +0 -0
- package/data/novnc/lib/util/logging.js +0 -0
- package/data/novnc/lib/util/strings.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/utils/common.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/adler32.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/constants.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/crc32.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/deflate.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/gzheader.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inffast.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inflate.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inftrees.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/messages.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/trees.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/zstream.js +0 -0
- package/data/novnc/lib/websock.js +0 -0
- package/package.json +1 -1
- package/scripts/cli.py +0 -0
- package/skills/agent_tool_skill.py +0 -0
- package/web/ui/admin/admin-agents.js +570 -0
- package/web/ui/admin/admin-core.js +322 -0
- package/web/ui/admin/admin-dashboard.js +153 -0
- package/web/ui/admin/admin-executor.js +67 -0
- package/web/ui/admin/admin-files.js +81 -0
- package/web/ui/admin/admin-llm.js +190 -0
- package/web/ui/admin/admin-logs.js +69 -0
- package/web/ui/admin/admin-memory.js +91 -0
- package/web/ui/admin/admin-org.js +283 -0
- package/web/ui/admin/admin-permissions.js +147 -0
- package/web/ui/admin/admin-platforms.js +221 -0
- package/web/ui/admin/admin-sessions.js +182 -0
- package/web/ui/admin/admin-skills.js +217 -0
- package/web/ui/admin/admin-system.js +154 -0
- package/web/ui/admin/admin-tasks.js +131 -0
- package/web/ui/chat/chat_main.js +292 -304
- package/web/ui/chat/flow_engine.js +96 -41
- package/web/ui/index.html +15 -2776
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>';
|