myagent-ai 1.19.1 → 1.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/vnc_manager.py +4 -7
- package/install.ps1 +32 -12
- package/package.json +2 -2
- package/requirements-optional.txt +10 -0
- package/requirements.txt +5 -12
- package/web/ui/chat/chat.css +26 -3
- package/web/ui/chat/chat_container.html +21 -0
- package/web/ui/chat/chat_main.js +209 -292
- package/web/ui/index.html +7 -7
package/core/vnc_manager.py
CHANGED
|
@@ -726,20 +726,18 @@ class VNCManager:
|
|
|
726
726
|
"-nocursorshape",
|
|
727
727
|
"-deferupdate", "5", # 延迟更新(降低带宽)
|
|
728
728
|
"-scale", "2/3", # 缩小 2/3(降低带宽)
|
|
729
|
-
# [v1.18.10] 抑制 Xvfb 环境下不支持的扩展警告
|
|
730
|
-
"-noxfixes", # 跳过 XFIXES 检查(Xvfb 可能不支持)
|
|
731
729
|
]
|
|
732
730
|
|
|
733
|
-
# [v1.18.5] 不使用 -threads 模式:与 -scale
|
|
731
|
+
# [v1.18.5] 不使用 -threads 模式:与 -scale 组合在 0.9.17 中
|
|
734
732
|
# 会导致父进程 fork 后立即退出,端口延迟监听
|
|
735
733
|
|
|
736
|
-
# 处理 shm-helper:
|
|
734
|
+
# [v1.19.1] 处理 shm-helper: 找到就传绝对路径,找不到就跳过
|
|
735
|
+
# 注意: x11vnc 0.9.17 不支持 -no-shm 参数,会报 unrecognized option
|
|
737
736
|
if shm_helper:
|
|
738
737
|
cmd.extend(["-shm-helper", shm_helper])
|
|
739
738
|
logger.info(f"x11vnc shm-helper: {shm_helper}")
|
|
740
739
|
else:
|
|
741
|
-
|
|
742
|
-
logger.info("x11vnc 未找到 shm-helper,使用 -no-shm 禁用 SHM")
|
|
740
|
+
logger.info("x11vnc 未找到 shm-helper,跳过 SHM 相关参数")
|
|
743
741
|
|
|
744
742
|
env = {**os.environ, "DISPLAY": self.display}
|
|
745
743
|
|
|
@@ -841,7 +839,6 @@ class VNCManager:
|
|
|
841
839
|
"-noxdamage",
|
|
842
840
|
"-nowf",
|
|
843
841
|
"-deferupdate", "5",
|
|
844
|
-
"-no-shm",
|
|
845
842
|
"-nobell",
|
|
846
843
|
]
|
|
847
844
|
env = {**os.environ, "DISPLAY": self.display}
|
package/install.ps1
CHANGED
|
@@ -9,7 +9,6 @@ $VENV_DIR = ".venv"
|
|
|
9
9
|
Write-Host "`n--- MyAgent 一键部署 ---" -ForegroundColor Cyan
|
|
10
10
|
|
|
11
11
|
# 1. 检查 Python
|
|
12
|
-
# 尝试使用 python 或 python3
|
|
13
12
|
$PYTHON_EXE = "python"
|
|
14
13
|
if (-not (Get-Command $PYTHON_EXE -ErrorAction SilentlyContinue)) {
|
|
15
14
|
$PYTHON_EXE = "python3"
|
|
@@ -20,7 +19,6 @@ if (-not (Get-Command $PYTHON_EXE -ErrorAction SilentlyContinue)) {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
try {
|
|
23
|
-
# 更加稳健的版本获取方式
|
|
24
22
|
$py_ver_str = & $PYTHON_EXE -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
|
25
23
|
Write-Host "[i] 找到 Python: $py_ver_str"
|
|
26
24
|
|
|
@@ -50,20 +48,42 @@ if (-not (Test-Path $VENV_DIR)) {
|
|
|
50
48
|
Write-Host "[✓] 虚拟环境已创建。" -ForegroundColor Green
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
Write-Host "[*] 正在安装/升级依赖..." -ForegroundColor Cyan
|
|
55
|
-
& "$VENV_DIR\Scripts\python.exe" -m pip install --upgrade pip --quiet
|
|
56
|
-
& "$VENV_DIR\Scripts\python.exe" -m pip install -r requirements.txt
|
|
51
|
+
$PIP = "$VENV_DIR\Scripts\python.exe -m pip"
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Write-Host "
|
|
53
|
+
# 3. 升级 pip
|
|
54
|
+
Write-Host "[*] 正在升级 pip..." -ForegroundColor Cyan
|
|
55
|
+
& $VENV_DIR\Scripts\python.exe -m pip install --upgrade pip --quiet
|
|
56
|
+
if ($LASTEXITCODE -ne 0) {
|
|
57
|
+
Write-Host "[!] pip 升级失败,继续尝试安装依赖..." -ForegroundColor Yellow
|
|
63
58
|
}
|
|
64
59
|
|
|
60
|
+
# 4. 安装核心依赖 (必需,失败则中止)
|
|
61
|
+
Write-Host "[*] 正在安装核心依赖..." -ForegroundColor Cyan
|
|
62
|
+
& $VENV_DIR\Scripts\python.exe -m pip install -r requirements.txt
|
|
63
|
+
if ($LASTEXITCODE -ne 0) {
|
|
64
|
+
Write-Host "`n[✗] 核心依赖安装失败!" -ForegroundColor Red
|
|
65
|
+
exit 1
|
|
66
|
+
}
|
|
67
|
+
Write-Host "[✓] 核心依赖安装成功。" -ForegroundColor Green
|
|
68
|
+
|
|
69
|
+
# 5. 安装可选依赖 (失败不影响核心功能)
|
|
70
|
+
if (Test-Path "requirements-optional.txt") {
|
|
71
|
+
Write-Host "`n[*] 正在安装可选依赖 (语音识别等)..." -ForegroundColor Cyan
|
|
72
|
+
Write-Host " (如果编译失败,不影响核心功能)" -ForegroundColor DarkGray
|
|
73
|
+
& $VENV_DIR\Scripts\python.exe -m pip install -r requirements-optional.txt
|
|
74
|
+
if ($LASTEXITCODE -eq 0) {
|
|
75
|
+
Write-Host "[✓] 可选依赖安装成功。" -ForegroundColor Green
|
|
76
|
+
} else {
|
|
77
|
+
Write-Host "[!] 可选依赖安装失败(部分功能如语音识别可能不可用)。" -ForegroundColor Yellow
|
|
78
|
+
Write-Host " 提示: funasr 在 Windows + Python 3.14 上需要安装 Microsoft C++ Build Tools" -ForegroundColor DarkGray
|
|
79
|
+
Write-Host " 下载地址: https://visualstudio.microsoft.com/visual-cpp-build-tools/" -ForegroundColor DarkGray
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Write-Host "`n[✓] 部署完成!" -ForegroundColor Green
|
|
84
|
+
Write-Host "提示: 现在可以运行 .\myagent_start.ps1 启动 MyAgent。" -ForegroundColor Yellow
|
|
85
|
+
|
|
65
86
|
Write-Host "`n按回车退出..."
|
|
66
|
-
# 仅在非交互模式下禁用 Read-Host
|
|
67
87
|
if ($Host.Name -ne "ServerRemoteHost") {
|
|
68
88
|
Read-Host
|
|
69
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagent-ai",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.3",
|
|
4
4
|
"description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
|
|
5
5
|
"main": "main.py",
|
|
6
6
|
"bin": {
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"python": ">=3.10",
|
|
44
44
|
"node": ">=18"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
package/requirements.txt
CHANGED
|
@@ -69,18 +69,6 @@ chardet>=5.0.0
|
|
|
69
69
|
# xlrd: 旧版 Excel (.xls) 文件读取
|
|
70
70
|
xlrd>=2.0.0
|
|
71
71
|
|
|
72
|
-
# ============================================================
|
|
73
|
-
# 语音识别 (本地 STT,默认启用)
|
|
74
|
-
# ============================================================
|
|
75
|
-
# [v1.18.8] SenseVoice (funasr) 作为首选引擎,中文识别极佳
|
|
76
|
-
# torch/torchaudio 约 200MB (CPU版),funasr 约 100MB
|
|
77
|
-
# 若仅需 Whisper 备选引擎,可注释下面三行,保留 faster-whisper
|
|
78
|
-
funasr>=1.1.0
|
|
79
|
-
torch>=2.0.0
|
|
80
|
-
torchaudio>=2.0.0
|
|
81
|
-
faster-whisper>=1.0.0
|
|
82
|
-
pydub>=0.25.1
|
|
83
|
-
|
|
84
72
|
# ============================================================
|
|
85
73
|
# Anthropic Claude (可选)
|
|
86
74
|
# ============================================================
|
|
@@ -91,3 +79,8 @@ anthropic>=0.18.0
|
|
|
91
79
|
# ============================================================
|
|
92
80
|
cryptography>=41.0.0
|
|
93
81
|
websockets>=12.0
|
|
82
|
+
|
|
83
|
+
# ============================================================
|
|
84
|
+
# 语音识别 (本地 STT,需要编译)
|
|
85
|
+
# 已移至 requirements-optional.txt,单独安装以避免编译失败阻断核心依赖
|
|
86
|
+
# ============================================================
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -636,10 +636,16 @@ input,textarea,select{font:inherit}
|
|
|
636
636
|
}
|
|
637
637
|
.attachment-thumb:hover .attachment-remove{display:flex}
|
|
638
638
|
|
|
639
|
-
/* [v1.16.12→18] 消息气泡中的附件 */
|
|
639
|
+
/* [v1.16.12→18→19.3] 消息气泡中的附件 */
|
|
640
640
|
.msg-attachments {
|
|
641
641
|
display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px;
|
|
642
642
|
}
|
|
643
|
+
.msg-attachments-images {
|
|
644
|
+
margin-bottom:10px; /* 图片在气泡内容上方,底部留更多空间 */
|
|
645
|
+
}
|
|
646
|
+
.msg-attachments-files {
|
|
647
|
+
margin-top:6px; /* 文件在气泡内容下方 */
|
|
648
|
+
}
|
|
643
649
|
.msg-image-wrapper {
|
|
644
650
|
max-width:300px;border-radius:var(--radius-sm);overflow:hidden;
|
|
645
651
|
border:1px solid var(--bg4);cursor:pointer;
|
|
@@ -652,6 +658,13 @@ input,textarea,select{font:inherit}
|
|
|
652
658
|
transition:max-height .3s ease;
|
|
653
659
|
}
|
|
654
660
|
.msg-image-wrapper:hover .msg-image{max-height:none}
|
|
661
|
+
/* [v1.19.3] Agent 发送的图片样式 */
|
|
662
|
+
.agent-image {
|
|
663
|
+
border-color:var(--accent);
|
|
664
|
+
}
|
|
665
|
+
.agent-image::after {
|
|
666
|
+
content:'点击查看大图 · ⬇ 下载';
|
|
667
|
+
}
|
|
655
668
|
/* [v1.16.18] 图片缩略图 hover 叠加层提示 */
|
|
656
669
|
.msg-image-wrapper::after {
|
|
657
670
|
content:'点击查看大图';
|
|
@@ -666,17 +679,26 @@ input,textarea,select{font:inherit}
|
|
|
666
679
|
.msg-file-item {
|
|
667
680
|
display:flex;align-items:center;gap:8px;padding:6px 10px;
|
|
668
681
|
background:var(--bg3);border-radius:var(--radius-sm);
|
|
669
|
-
font-size:13px;color:var(--text2);max-width:
|
|
682
|
+
font-size:13px;color:var(--text2);max-width:280px;
|
|
670
683
|
cursor:pointer;transition:background .15s,border-color .15s;
|
|
671
684
|
border:1px solid transparent;
|
|
672
685
|
}
|
|
673
686
|
.msg-file-item:hover{background:var(--bg4);border-color:var(--accent)}
|
|
674
687
|
.agent-file{border-color:var(--accent);background:var(--bg2)}
|
|
675
688
|
.msg-file-icon{font-size:18px;flex-shrink:0}
|
|
689
|
+
.msg-file-info{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
|
|
676
690
|
.msg-file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
|
|
677
691
|
.msg-file-size{font-size:11px;color:var(--text3);flex-shrink:0}
|
|
692
|
+
.msg-file-actions{flex-shrink:0;margin-left:4px}
|
|
693
|
+
.msg-file-download{
|
|
694
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
695
|
+
width:28px;height:28px;border-radius:var(--radius-xs);
|
|
696
|
+
font-size:16px;text-decoration:none;color:var(--text3);
|
|
697
|
+
transition:background .15s, color .15s;
|
|
698
|
+
}
|
|
699
|
+
.msg-file-download:hover{background:var(--accent);color:#fff}
|
|
678
700
|
|
|
679
|
-
/* [v1.17] 移动端文件附件适配 */
|
|
701
|
+
/* [v1.17→19.3] 移动端文件附件适配 */
|
|
680
702
|
@media (max-width: 768px) {
|
|
681
703
|
.msg-file-item {
|
|
682
704
|
max-width: 100%;
|
|
@@ -689,6 +711,7 @@ input,textarea,select{font:inherit}
|
|
|
689
711
|
.msg-file-name { font-size: 14px; }
|
|
690
712
|
.msg-attachments { gap: 10px; }
|
|
691
713
|
.msg-image-wrapper { max-width: 100%; }
|
|
714
|
+
.msg-file-download { width: 34px; height: 34px; font-size: 18px; }
|
|
692
715
|
}
|
|
693
716
|
|
|
694
717
|
/* [v1.17] 折叠文件内容样式 */
|
|
@@ -278,6 +278,27 @@
|
|
|
278
278
|
|
|
279
279
|
</div>
|
|
280
280
|
|
|
281
|
+
<!-- [v1.19.2] VNC Remote Desktop Overlay -->
|
|
282
|
+
<div id="vncOverlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;background:#1a1a2e;flex-direction:column">
|
|
283
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 16px;background:#16213e;border-bottom:1px solid #0f3460;flex-shrink:0">
|
|
284
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
285
|
+
<span style="color:#eee;font-size:14px;font-weight:600">远程桌面</span>
|
|
286
|
+
<span id="vncOverlayStatus" style="font-size:12px;color:#888">正在连接...</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div style="display:flex;gap:8px">
|
|
289
|
+
<button onclick="openVNCInNewTab()" style="padding:4px 12px;border-radius:4px;border:1px solid #0f3460;background:#0f3460;color:#eee;cursor:pointer;font-size:12px" title="在新标签页中打开">新标签页</button>
|
|
290
|
+
<button onclick="closeVNCOverlay()" style="padding:4px 12px;border-radius:4px;border:1px solid #e94560;background:transparent;color:#e94560;cursor:pointer;font-size:12px">关闭</button>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
<div style="flex:1;position:relative">
|
|
294
|
+
<iframe id="vncIframe" style="width:100%;height:100%;border:none" allow="clipboard-read; clipboard-write"></iframe>
|
|
295
|
+
<div id="vncLoadingHint" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#888;font-size:14px;text-align:center">
|
|
296
|
+
<div style="margin-bottom:8px;font-size:32px">💻</div>
|
|
297
|
+
<div>正在连接远程桌面...</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
281
302
|
<!-- Mobile Overlay -->
|
|
282
303
|
<div class="mobile-overlay" id="chatMobileOverlay"></div>
|
|
283
304
|
<!-- Toast Container -->
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -975,43 +975,80 @@ async function toggleVNC() {
|
|
|
975
975
|
return;
|
|
976
976
|
}
|
|
977
977
|
|
|
978
|
-
toast('
|
|
978
|
+
toast('正在启动远程桌面,请稍候...', 'info', 60000);
|
|
979
979
|
try {
|
|
980
980
|
var result = await api('/api/vnc/start', { method: 'POST' });
|
|
981
981
|
if (result.success) {
|
|
982
982
|
await refreshVNCStatus();
|
|
983
|
+
toast('远程桌面已启动,正在打开远程桌面窗口...', 'success');
|
|
983
984
|
// 如果是"已在运行"(附加到已有实例),立刻打开窗口
|
|
984
985
|
if (result.message && result.message.indexOf('已在运行') > -1) {
|
|
985
|
-
toast('远程桌面已连接', 'success');
|
|
986
986
|
openVNCWindow();
|
|
987
987
|
} else {
|
|
988
|
-
toast('远程桌面已启动', 'success');
|
|
989
988
|
// 新启动的需要等服务完全就绪
|
|
990
989
|
setTimeout(function() {
|
|
991
990
|
openVNCWindow();
|
|
992
991
|
}, 2000);
|
|
993
992
|
}
|
|
994
993
|
} else {
|
|
995
|
-
toast('
|
|
994
|
+
toast('远程桌面启动失败: ' + (result.message || '未知错误'), 'error', 8000);
|
|
996
995
|
}
|
|
997
996
|
} catch (e) {
|
|
998
|
-
toast('
|
|
997
|
+
toast('远程桌面启动失败: ' + e.message, 'error', 8000);
|
|
999
998
|
}
|
|
1000
999
|
}
|
|
1001
1000
|
|
|
1002
1001
|
function openVNCWindow() {
|
|
1002
|
+
// [v1.19.2] 优先使用页面内覆盖层(兼容 IM WebView 等不允许弹窗的环境)
|
|
1003
|
+
var vncUrl = '/vnc/vnc.html';
|
|
1004
|
+
var overlay = document.getElementById('vncOverlay');
|
|
1005
|
+
var iframe = document.getElementById('vncIframe');
|
|
1006
|
+
if (overlay && iframe) {
|
|
1007
|
+
overlay.style.display = 'flex';
|
|
1008
|
+
iframe.src = vncUrl;
|
|
1009
|
+
// iframe 加载完成后隐藏 loading 提示
|
|
1010
|
+
iframe.onload = function() {
|
|
1011
|
+
var hint = document.getElementById('vncLoadingHint');
|
|
1012
|
+
var status = document.getElementById('vncOverlayStatus');
|
|
1013
|
+
if (hint) hint.style.display = 'none';
|
|
1014
|
+
if (status) status.textContent = '已连接';
|
|
1015
|
+
if (status) status.style.color = '#00ff88';
|
|
1016
|
+
};
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
// 回退: 弹窗方式(仅限非 IM 环境)
|
|
1003
1020
|
if (vncWindow && !vncWindow.closed) {
|
|
1004
1021
|
vncWindow.focus();
|
|
1005
1022
|
return;
|
|
1006
1023
|
}
|
|
1007
|
-
// 在新窗口中打开 noVNC 客户端
|
|
1008
1024
|
var w = Math.min(1200, screen.width - 100);
|
|
1009
1025
|
var h = Math.min(800, screen.height - 100);
|
|
1010
1026
|
var left = Math.round((screen.width - w) / 2);
|
|
1011
1027
|
var top = Math.round((screen.height - h) / 2);
|
|
1012
|
-
vncWindow = window.open(
|
|
1028
|
+
vncWindow = window.open(vncUrl, 'myagent_vnc',
|
|
1013
1029
|
'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top +
|
|
1014
1030
|
',menubar=no,toolbar=no,location=no,status=no,scrollbars=no,resizable=yes');
|
|
1031
|
+
if (!vncWindow || vncWindow.closed) {
|
|
1032
|
+
vncWindow = window.open(vncUrl, '_blank');
|
|
1033
|
+
if (!vncWindow || vncWindow.closed) {
|
|
1034
|
+
toast('浏览器拦截了弹窗,请允许弹窗后重试,或点击菜单远程桌面入口', 'warning', 8000);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function closeVNCOverlay() {
|
|
1040
|
+
var overlay = document.getElementById('vncOverlay');
|
|
1041
|
+
var iframe = document.getElementById('vncIframe');
|
|
1042
|
+
var hint = document.getElementById('vncLoadingHint');
|
|
1043
|
+
var status = document.getElementById('vncOverlayStatus');
|
|
1044
|
+
if (overlay) overlay.style.display = 'none';
|
|
1045
|
+
if (iframe) iframe.src = 'about:blank';
|
|
1046
|
+
if (hint) hint.style.display = 'block';
|
|
1047
|
+
if (status) { status.textContent = '正在连接...'; status.style.color = '#888'; }
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function openVNCInNewTab() {
|
|
1051
|
+
window.open('/vnc/vnc.html', '_blank');
|
|
1015
1052
|
}
|
|
1016
1053
|
|
|
1017
1054
|
async function stopVNC() {
|
|
@@ -1021,6 +1058,8 @@ async function stopVNC() {
|
|
|
1021
1058
|
var result = await api('/api/vnc/stop', { method: 'POST' });
|
|
1022
1059
|
toast(result.message || '远程桌面已停止', 'info');
|
|
1023
1060
|
await refreshVNCStatus();
|
|
1061
|
+
// 关闭 VNC 覆盖层和弹窗
|
|
1062
|
+
closeVNCOverlay();
|
|
1024
1063
|
if (vncWindow && !vncWindow.closed) {
|
|
1025
1064
|
vncWindow.close();
|
|
1026
1065
|
}
|
|
@@ -2317,280 +2356,142 @@ async function clearCurrentChat() {
|
|
|
2317
2356
|
}
|
|
2318
2357
|
|
|
2319
2358
|
// ── Group History Messages ──
|
|
2320
|
-
//
|
|
2321
|
-
// assistant
|
|
2359
|
+
// 将数据库中的扁平消息序列分组为 user/assistant 交替结构,
|
|
2360
|
+
// 每个 assistant 组包含 interleaved 的 text + tool_call + tool_result parts。
|
|
2322
2361
|
//
|
|
2323
|
-
// DB
|
|
2324
|
-
//
|
|
2325
|
-
//
|
|
2326
|
-
//
|
|
2327
|
-
//
|
|
2328
|
-
//
|
|
2329
|
-
// Result: user → [assistant { text → tool_call → tool_result → text → ... }] → user
|
|
2362
|
+
// DB 存储模式:
|
|
2363
|
+
// 用户输入 → role="user", key="user_input"
|
|
2364
|
+
// 助手文本 → role="assistant", key=""
|
|
2365
|
+
// 工具调用 → role="assistant", key="tool_call"
|
|
2366
|
+
// 工具结果 → role="tool", key="tool_result"
|
|
2367
|
+
// 推理思考 → role="assistant", key="reasoning"
|
|
2330
2368
|
|
|
2331
2369
|
// 解析 tool_call 消息内容,兼容新旧格式
|
|
2332
|
-
// 新格式: "beforecalltext\n调用工具: xxx\n参数: yyy"
|
|
2333
|
-
// 旧格式: "调用工具: xxx\n参数: yyy"
|
|
2334
2370
|
function parseToolCallContent(content) {
|
|
2335
2371
|
if (!content) return { title: '调用工具', toolName: '', params: '' };
|
|
2336
2372
|
var lines = content.split('\n');
|
|
2337
|
-
var
|
|
2338
|
-
var toolName = '';
|
|
2339
|
-
var params = '';
|
|
2340
|
-
// 查找 "调用工具:" 行
|
|
2373
|
+
var toolName = '', params = '';
|
|
2341
2374
|
for (var li = 0; li < lines.length; li++) {
|
|
2342
|
-
var
|
|
2343
|
-
|
|
2344
|
-
if (m) {
|
|
2345
|
-
toolName = m[1];
|
|
2346
|
-
break;
|
|
2347
|
-
}
|
|
2375
|
+
var m = lines[li].trim().match(/^调用工具:\s*(\S+)/);
|
|
2376
|
+
if (m) { toolName = m[1]; break; }
|
|
2348
2377
|
}
|
|
2349
|
-
// 查找 "参数:" 行
|
|
2350
2378
|
for (var li2 = 0; li2 < lines.length; li2++) {
|
|
2351
|
-
var
|
|
2352
|
-
|
|
2353
|
-
if (m2) {
|
|
2354
|
-
params = m2[1].trim();
|
|
2355
|
-
break;
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
// 标题: 如果第一行不是"调用工具:",则使用第一行作为 beforecalltext 标题
|
|
2359
|
-
if (lines[0] && !lines[0].trim().startsWith('调用工具:')) {
|
|
2360
|
-
title = lines[0].trim();
|
|
2361
|
-
} else {
|
|
2362
|
-
title = toolName ? ('调用工具: ' + toolName) : content.substring(0, 100);
|
|
2379
|
+
var m2 = lines[li2].trim().match(/^参数:\s*([\s\S]*)/);
|
|
2380
|
+
if (m2) { params = m2[1].trim(); break; }
|
|
2363
2381
|
}
|
|
2382
|
+
var tcIdx = lines.findIndex(function(l) { return l.trim().match(/^调用工具:/); });
|
|
2383
|
+
var title = tcIdx > 0 ? lines.slice(0, tcIdx).join('\n').trim() : '';
|
|
2384
|
+
if (!title) title = toolName ? ('调用工具: ' + toolName) : '调用工具';
|
|
2364
2385
|
return { title: title, toolName: toolName, params: params };
|
|
2365
2386
|
}
|
|
2366
2387
|
|
|
2388
|
+
// 解析 tool_result 消息: "[tool_name] 成功/失败\n{output}"
|
|
2389
|
+
function parseToolResultContent(content) {
|
|
2390
|
+
if (!content) return { title: '执行结果', toolName: '', success: true, body: '' };
|
|
2391
|
+
var idx = content.indexOf('\n');
|
|
2392
|
+
var header = idx > 0 ? content.substring(0, idx) : content.substring(0, 80);
|
|
2393
|
+
var body = idx > 0 ? content.substring(idx + 1) : '';
|
|
2394
|
+
var toolName = (header.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2395
|
+
var isOk = !content.includes('失败');
|
|
2396
|
+
var title = toolName ? (toolName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2397
|
+
return { title: title, toolName: toolName, success: isOk, body: body };
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2367
2400
|
function groupHistoryMessages(messages) {
|
|
2368
2401
|
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
2369
2402
|
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2403
|
+
var grouped = [];
|
|
2404
|
+
var evtId = 0;
|
|
2405
|
+
|
|
2406
|
+
// 辅助: 将单条消息解析为 part(reasoning 除外)
|
|
2407
|
+
function msgToPart(m) {
|
|
2408
|
+
var key = (m.key || '').toLowerCase();
|
|
2409
|
+
if (key === 'reasoning') return null;
|
|
2410
|
+
if (key === 'tool_call') {
|
|
2411
|
+
var tc = m._parsedToolCall || parseToolCallContent(m.content);
|
|
2412
|
+
m._parsedToolCall = tc;
|
|
2413
|
+
return { type: 'exec', data: { id: evtId++, type: 'tool_call', title: tc.title, tool_name: tc.toolName, params: tc.params || undefined, status: 'done' } };
|
|
2414
|
+
}
|
|
2415
|
+
if (m.role === 'tool') {
|
|
2416
|
+
var tr = parseToolResultContent(m.content);
|
|
2417
|
+
return { type: 'exec', data: { id: evtId++, type: 'tool_result', title: tr.title, tool_name: tr.toolName, success: tr.success, summary: tr.body.substring(0, 500).trim(), result: { output: tr.body.substring(0, 2000) } } };
|
|
2418
|
+
}
|
|
2419
|
+
if (m.content && m.content.trim() && m.content !== '(无回复)') {
|
|
2420
|
+
return { type: 'text', content: m.content };
|
|
2421
|
+
}
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2373
2424
|
|
|
2425
|
+
var i = 0;
|
|
2374
2426
|
while (i < messages.length) {
|
|
2375
|
-
|
|
2427
|
+
var msg = messages[i];
|
|
2376
2428
|
|
|
2429
|
+
// ── 用户消息: 直接加入 ──
|
|
2377
2430
|
if (msg.role === 'user') {
|
|
2378
2431
|
var userEntry = { role: 'user', content: msg.content, time: msg.time || '' };
|
|
2379
|
-
// [v1.16.18] 保留附件元数据
|
|
2380
2432
|
if (msg.images) userEntry.images = msg.images;
|
|
2381
2433
|
if (msg.files) userEntry.files = msg.files;
|
|
2382
2434
|
grouped.push(userEntry);
|
|
2383
2435
|
i++;
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
const parts = [];
|
|
2387
|
-
let lastAssistantTime = msg.time || '';
|
|
2388
|
-
let reasoningText = ''; // 推理模型思考过程
|
|
2389
|
-
|
|
2390
|
-
// Process the first assistant message
|
|
2391
|
-
if (msg.key === 'reasoning') {
|
|
2392
|
-
// 推理模型思考过程 — 合并到分组消息的 reasoning 字段
|
|
2393
|
-
reasoningText = msg.content || '';
|
|
2394
|
-
} else if (msg.key === 'tool_call') {
|
|
2395
|
-
// 新格式: beforecalltext\n调用工具: xxx\n参数: yyy
|
|
2396
|
-
// 旧格式: 调用工具: xxx\n参数: yyy
|
|
2397
|
-
var _tcParts = msg._parsedToolCall || parseToolCallContent(msg.content);
|
|
2398
|
-
msg._parsedToolCall = _tcParts; // 缓存
|
|
2399
|
-
parts.push({
|
|
2400
|
-
type: 'exec',
|
|
2401
|
-
data: {
|
|
2402
|
-
id: _evtId++,
|
|
2403
|
-
type: 'tool_call',
|
|
2404
|
-
title: _tcParts.title,
|
|
2405
|
-
tool_name: _tcParts.toolName,
|
|
2406
|
-
params: _tcParts.params || undefined,
|
|
2407
|
-
status: 'done',
|
|
2408
|
-
}
|
|
2409
|
-
});
|
|
2410
|
-
} else if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
|
|
2411
|
-
parts.push({ type: 'text', content: msg.content });
|
|
2412
|
-
}
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2413
2438
|
|
|
2414
|
-
|
|
2439
|
+
// ── 收集连续的非 user 消息为一个 assistant 组 ──
|
|
2440
|
+
var parts = [];
|
|
2441
|
+
var reasoningText = '';
|
|
2442
|
+
var lastTime = msg.time || '';
|
|
2443
|
+
var firstMsg = msg;
|
|
2444
|
+
// [v1.19.3] 收集组内所有消息的文件(agent 通过 file_send 发送的文件存在 metadata.files 中)
|
|
2445
|
+
var allAgentFiles = [];
|
|
2415
2446
|
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
const next = messages[i];
|
|
2421
|
-
|
|
2422
|
-
if (next.role === 'tool') {
|
|
2423
|
-
// Parse tool_result content: format is "[tool_name] 成功/失败\n{output}"
|
|
2424
|
-
const firstNewline = next.content.indexOf('\n');
|
|
2425
|
-
const headerLine = firstNewline > 0 ? next.content.substring(0, firstNewline) : next.content.substring(0, 80);
|
|
2426
|
-
const bodyContent = firstNewline > 0 ? next.content.substring(firstNewline + 1) : '';
|
|
2427
|
-
// Extract tool name from header: [tool_name] 成功 or [tool_name] 失败
|
|
2428
|
-
const toolResultName = (headerLine.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2429
|
-
const isOk = !next.content.includes('失败');
|
|
2430
|
-
const displayTitle = toolResultName ? (toolResultName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2431
|
-
parts.push({
|
|
2432
|
-
type: 'exec',
|
|
2433
|
-
data: {
|
|
2434
|
-
id: _evtId++,
|
|
2435
|
-
type: 'tool_result',
|
|
2436
|
-
title: displayTitle,
|
|
2437
|
-
tool_name: toolResultName,
|
|
2438
|
-
success: isOk,
|
|
2439
|
-
summary: bodyContent.substring(0, 500).trim(),
|
|
2440
|
-
result: { output: bodyContent.substring(0, 2000) },
|
|
2441
|
-
}
|
|
2442
|
-
});
|
|
2443
|
-
i++;
|
|
2444
|
-
} else if (next.role === 'assistant' && next.key === 'tool_call') {
|
|
2445
|
-
// 新格式: beforecalltext\n调用工具: xxx\n参数: yyy
|
|
2446
|
-
var _tcParts2 = next._parsedToolCall || parseToolCallContent(next.content);
|
|
2447
|
-
next._parsedToolCall = _tcParts2;
|
|
2448
|
-
parts.push({
|
|
2449
|
-
type: 'exec',
|
|
2450
|
-
data: {
|
|
2451
|
-
id: _evtId++,
|
|
2452
|
-
type: 'tool_call',
|
|
2453
|
-
title: _tcParts2.title,
|
|
2454
|
-
tool_name: _tcParts2.toolName,
|
|
2455
|
-
params: _tcParts2.params || undefined,
|
|
2456
|
-
status: 'done',
|
|
2457
|
-
}
|
|
2458
|
-
});
|
|
2459
|
-
lastAssistantTime = next.time || lastAssistantTime;
|
|
2460
|
-
i++;
|
|
2461
|
-
} else if (next.role === 'assistant') {
|
|
2462
|
-
if (next.key === 'reasoning') {
|
|
2463
|
-
// 推理模型思考过程 — 追加到 reasoningText
|
|
2464
|
-
reasoningText = reasoningText ? (reasoningText + '\n\n' + (next.content || '')) : (next.content || '');
|
|
2465
|
-
} else if (next.content && next.content.trim() && next.content !== '(无回复)') {
|
|
2466
|
-
parts.push({ type: 'text', content: next.content });
|
|
2467
|
-
}
|
|
2468
|
-
lastAssistantTime = next.time || lastAssistantTime;
|
|
2469
|
-
i++;
|
|
2470
|
-
} else {
|
|
2471
|
-
break;
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2447
|
+
while (i < messages.length && messages[i].role !== 'user') {
|
|
2448
|
+
var m = messages[i];
|
|
2449
|
+
var mkey = (m.key || '').toLowerCase();
|
|
2450
|
+
lastTime = m.time || lastTime;
|
|
2474
2451
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
var _groupedEntry = {
|
|
2481
|
-
role: 'assistant',
|
|
2482
|
-
content: assembledContent,
|
|
2483
|
-
time: lastAssistantTime,
|
|
2484
|
-
reasoning: reasoningText || undefined,
|
|
2485
|
-
parts: parts.length > 0 ? parts : undefined,
|
|
2486
|
-
exec_events: parts.filter(p => p.type === 'exec').map(p => p.data),
|
|
2487
|
-
};
|
|
2488
|
-
// [fix] 保留 _files 字段(agent 生成的文件附件)
|
|
2489
|
-
if (msg._files) _groupedEntry._files = msg._files;
|
|
2490
|
-
grouped.push(_groupedEntry);
|
|
2491
|
-
} else if (msg.role === 'tool') {
|
|
2492
|
-
// Orphan tool message — wrap in an assistant group
|
|
2493
|
-
const parts = [];
|
|
2494
|
-
const isResult = msg.key === 'tool_result';
|
|
2495
|
-
const isCall = msg.key === 'tool_call';
|
|
2496
|
-
|
|
2497
|
-
if (isCall) {
|
|
2498
|
-
var _tcParts3 = msg._parsedToolCall || parseToolCallContent(msg.content);
|
|
2499
|
-
msg._parsedToolCall = _tcParts3;
|
|
2500
|
-
parts.push({
|
|
2501
|
-
type: 'exec',
|
|
2502
|
-
data: {
|
|
2503
|
-
id: _evtId++,
|
|
2504
|
-
type: 'tool_call',
|
|
2505
|
-
title: _tcParts3.title,
|
|
2506
|
-
tool_name: _tcParts3.toolName,
|
|
2507
|
-
params: _tcParts3.params || undefined,
|
|
2508
|
-
status: 'done',
|
|
2509
|
-
}
|
|
2510
|
-
});
|
|
2511
|
-
} else if (isResult) {
|
|
2512
|
-
const firstNewline = msg.content.indexOf('\n');
|
|
2513
|
-
const headerLine = firstNewline > 0 ? msg.content.substring(0, firstNewline) : msg.content.substring(0, 80);
|
|
2514
|
-
const bodyContent = firstNewline > 0 ? msg.content.substring(firstNewline + 1) : '';
|
|
2515
|
-
const toolResultName = (headerLine.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2516
|
-
const isOk = !msg.content.includes('失败');
|
|
2517
|
-
const displayTitle = toolResultName ? (toolResultName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2518
|
-
parts.push({
|
|
2519
|
-
type: 'exec',
|
|
2520
|
-
data: {
|
|
2521
|
-
id: _evtId++,
|
|
2522
|
-
type: 'tool_result',
|
|
2523
|
-
title: displayTitle,
|
|
2524
|
-
tool_name: toolResultName,
|
|
2525
|
-
success: isOk,
|
|
2526
|
-
summary: bodyContent.substring(0, 500).trim(),
|
|
2527
|
-
result: { output: bodyContent.substring(0, 2000) },
|
|
2528
|
-
}
|
|
2529
|
-
});
|
|
2452
|
+
// 收集该消息携带的文件(来自 metadata.files)
|
|
2453
|
+
if (m.files && Array.isArray(m.files) && m.files.length > 0) {
|
|
2454
|
+
for (var fi = 0; fi < m.files.length; fi++) {
|
|
2455
|
+
allAgentFiles.push(m.files[fi]);
|
|
2456
|
+
}
|
|
2530
2457
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
if (next.role === 'assistant' && next.key === 'tool_call') {
|
|
2536
|
-
var _tcParts4 = next._parsedToolCall || parseToolCallContent(next.content);
|
|
2537
|
-
next._parsedToolCall = _tcParts4;
|
|
2538
|
-
parts.push({
|
|
2539
|
-
type: 'exec',
|
|
2540
|
-
data: {
|
|
2541
|
-
id: _evtId++,
|
|
2542
|
-
type: 'tool_call',
|
|
2543
|
-
title: _tcParts4.title,
|
|
2544
|
-
tool_name: _tcParts4.toolName,
|
|
2545
|
-
params: _tcParts4.params || undefined,
|
|
2546
|
-
status: 'done',
|
|
2547
|
-
}
|
|
2548
|
-
});
|
|
2549
|
-
i++;
|
|
2550
|
-
} else if (next.role === 'assistant') {
|
|
2551
|
-
if (next.content && next.content.trim() && next.content !== '(无回复)') {
|
|
2552
|
-
parts.push({ type: 'text', content: next.content });
|
|
2553
|
-
}
|
|
2554
|
-
i++;
|
|
2555
|
-
} else if (next.role === 'tool') {
|
|
2556
|
-
const firstNewline = next.content.indexOf('\n');
|
|
2557
|
-
const headerLine = firstNewline > 0 ? next.content.substring(0, firstNewline) : next.content.substring(0, 80);
|
|
2558
|
-
const bodyContent = firstNewline > 0 ? next.content.substring(firstNewline + 1) : '';
|
|
2559
|
-
const toolResultName = (headerLine.match(/^\[([^\]]+)\]/) || [])[1] || '';
|
|
2560
|
-
const isOk = !next.content.includes('失败');
|
|
2561
|
-
const displayTitle = toolResultName ? (toolResultName + (isOk ? ' ✓' : ' ✗')) : (isOk ? '执行成功' : '执行失败');
|
|
2562
|
-
parts.push({
|
|
2563
|
-
type: 'exec',
|
|
2564
|
-
data: {
|
|
2565
|
-
id: _evtId++,
|
|
2566
|
-
type: 'tool_result',
|
|
2567
|
-
title: displayTitle,
|
|
2568
|
-
tool_name: toolResultName,
|
|
2569
|
-
success: isOk,
|
|
2570
|
-
summary: bodyContent.substring(0, 500).trim(),
|
|
2571
|
-
result: { output: bodyContent.substring(0, 2000) },
|
|
2572
|
-
}
|
|
2573
|
-
});
|
|
2574
|
-
i++;
|
|
2575
|
-
} else {
|
|
2576
|
-
break;
|
|
2458
|
+
// 也检查流式 _files(兼容实时流切换到历史显示的场景)
|
|
2459
|
+
if (m._files && Array.isArray(m._files) && m._files.length > 0) {
|
|
2460
|
+
for (var _fi = 0; _fi < m._files.length; _fi++) {
|
|
2461
|
+
allAgentFiles.push(m._files[_fi]);
|
|
2577
2462
|
}
|
|
2578
2463
|
}
|
|
2579
2464
|
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
exec_events: parts.filter(p => p.type === 'exec').map(p => p.data),
|
|
2587
|
-
};
|
|
2588
|
-
// [fix] 保留 _files 字段(agent 生成的文件附件)
|
|
2589
|
-
if (msg._files) _groupedEntry2._files = msg._files;
|
|
2590
|
-
grouped.push(_groupedEntry2);
|
|
2591
|
-
} else {
|
|
2465
|
+
if (mkey === 'reasoning') {
|
|
2466
|
+
reasoningText = reasoningText ? (reasoningText + '\n\n' + (m.content || '')) : (m.content || '');
|
|
2467
|
+
} else {
|
|
2468
|
+
var part = msgToPart(m);
|
|
2469
|
+
if (part) parts.push(part);
|
|
2470
|
+
}
|
|
2592
2471
|
i++;
|
|
2593
2472
|
}
|
|
2473
|
+
|
|
2474
|
+
// 组装: content 取最后一段文本(用于搜索/纯文本回退),parts 展示完整时间线
|
|
2475
|
+
var textParts = parts.filter(function(p) { return p.type === 'text'; });
|
|
2476
|
+
var hasExecParts = parts.some(function(p) { return p.type === 'exec'; });
|
|
2477
|
+
|
|
2478
|
+
var entry = {
|
|
2479
|
+
role: 'assistant',
|
|
2480
|
+
content: textParts.length > 0 ? textParts[textParts.length - 1].content : '',
|
|
2481
|
+
time: lastTime,
|
|
2482
|
+
};
|
|
2483
|
+
if (reasoningText) entry.reasoning = reasoningText;
|
|
2484
|
+
// 有工具调用或多段文本时设置 parts(启用 timeline 渲染)
|
|
2485
|
+
if (hasExecParts || textParts.length > 1) {
|
|
2486
|
+
entry.parts = parts;
|
|
2487
|
+
}
|
|
2488
|
+
// exec_events 兼容旧渲染路径
|
|
2489
|
+
var execParts = parts.filter(function(p) { return p.type === 'exec'; });
|
|
2490
|
+
if (execParts.length > 0) entry.exec_events = execParts.map(function(p) { return p.data; });
|
|
2491
|
+
// [v1.19.3] 设置收集到的所有 agent 文件
|
|
2492
|
+
if (allAgentFiles.length > 0) entry._files = allAgentFiles;
|
|
2493
|
+
|
|
2494
|
+
grouped.push(entry);
|
|
2594
2495
|
}
|
|
2595
2496
|
|
|
2596
2497
|
return grouped;
|
|
@@ -2679,25 +2580,22 @@ function _renderMessagesInner() {
|
|
|
2679
2580
|
|
|
2680
2581
|
const avatar = isUser ? '<span style="font-size:18px">👤</span>' : avatarHtml({avatar_image: state.currentAgent?.avatar_image, avatar_emoji: botEmoji, avatar_color: state.currentAgent?.avatar_color, name: state.currentAgent?.name}, 32, 'border-radius:8px;');
|
|
2681
2582
|
const content = renderMarkdown(msg.content);
|
|
2682
|
-
// [v1.
|
|
2683
|
-
const
|
|
2583
|
+
// [v1.19.3] 渲染图片和文件附件(支持磁盘持久化 file_id,图片在气泡顶部、文件在底部)
|
|
2584
|
+
const imageAttachmentHtml = (() => {
|
|
2684
2585
|
let parts = [];
|
|
2685
|
-
// User images
|
|
2586
|
+
// User images(用户发送的图片)
|
|
2686
2587
|
if (isUser && msg.images && msg.images.length > 0) {
|
|
2687
2588
|
for (let _imgIdx = 0; _imgIdx < msg.images.length; _imgIdx++) {
|
|
2688
2589
|
const img = msg.images[_imgIdx];
|
|
2689
|
-
// Check if we have a file_id (from server) or raw data
|
|
2690
2590
|
const fileId = img.id;
|
|
2691
2591
|
let src;
|
|
2692
2592
|
let hasFallback = false;
|
|
2693
2593
|
if (fileId) {
|
|
2694
2594
|
src = '/api/file/' + fileId;
|
|
2695
|
-
// [fix] 如果有 base64 回退数据,准备 onerror 处理
|
|
2696
2595
|
if (img._base64) hasFallback = true;
|
|
2697
2596
|
} else {
|
|
2698
2597
|
src = 'data:' + (img.type || 'image/png') + ';base64,' + (img.data || '');
|
|
2699
2598
|
}
|
|
2700
|
-
// [fix] 添加 onerror 回退:file_id 加载失败时使用 base64 数据或显示占位
|
|
2701
2599
|
let onerror = '';
|
|
2702
2600
|
if (hasFallback) {
|
|
2703
2601
|
onerror = ' onerror="this.onerror=null;this.src=\'data:' + escapeHtml(img.type || 'image/png') + ';base64,' + img._base64 + '\'"';
|
|
@@ -2707,34 +2605,58 @@ function _renderMessagesInner() {
|
|
|
2707
2605
|
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>');
|
|
2708
2606
|
}
|
|
2709
2607
|
}
|
|
2710
|
-
//
|
|
2608
|
+
// Agent images(agent 通过 file_send 发送的图片文件,type 为 image/*)
|
|
2609
|
+
const agentFiles = (msg._files || []);
|
|
2610
|
+
if (!isUser && agentFiles.length > 0) {
|
|
2611
|
+
for (const f of agentFiles) {
|
|
2612
|
+
const isImage = f.type && f.type.startsWith('image/');
|
|
2613
|
+
if (isImage && f.id) {
|
|
2614
|
+
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>');
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-images">' + parts.join('') + '</div>' : '';
|
|
2619
|
+
})();
|
|
2620
|
+
const fileAttachmentHtml = (() => {
|
|
2621
|
+
let parts = [];
|
|
2622
|
+
// User files(用户发送的非图片文件)
|
|
2711
2623
|
if (isUser && msg.files && msg.files.length > 0) {
|
|
2712
2624
|
for (const f of msg.files) {
|
|
2713
2625
|
const fileId = f.id;
|
|
2714
2626
|
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2715
2627
|
const icon = _getFileIcon(f.name || f.type || '');
|
|
2716
|
-
parts.push('<div class="msg-file-item"
|
|
2628
|
+
parts.push('<div class="msg-file-item" title="点击预览">' +
|
|
2717
2629
|
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2718
|
-
'<span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2630
|
+
'<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2719
2631
|
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2632
|
+
'</span>' +
|
|
2633
|
+
'<span class="msg-file-actions">' +
|
|
2634
|
+
'<a class="msg-file-download" href="/api/file/' + (fileId || '') + '/download" download="' + escapeHtml(f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
2635
|
+
'</span>' +
|
|
2720
2636
|
'</div>');
|
|
2721
2637
|
}
|
|
2722
2638
|
}
|
|
2723
|
-
// Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files
|
|
2724
|
-
const agentFiles = (msg._files ||
|
|
2725
|
-
if (agentFiles.length > 0) {
|
|
2639
|
+
// Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files(只渲染非图片文件)
|
|
2640
|
+
const agentFiles = (msg._files || []);
|
|
2641
|
+
if (!isUser && agentFiles.length > 0) {
|
|
2726
2642
|
for (const f of agentFiles) {
|
|
2643
|
+
const isImage = f.type && f.type.startsWith('image/');
|
|
2644
|
+
if (isImage) continue; // 图片已在 imageAttachmentHtml 中渲染
|
|
2727
2645
|
const fileId = f.id;
|
|
2728
2646
|
const sizeStr = f.size ? formatFileSize(f.size) : '';
|
|
2729
2647
|
const icon = _getFileIcon(f.name || f.type || '');
|
|
2730
|
-
parts.push('<div class="msg-file-item agent-file"
|
|
2648
|
+
parts.push('<div class="msg-file-item agent-file" title="点击预览">' +
|
|
2731
2649
|
'<span class="msg-file-icon">' + icon + '</span>' +
|
|
2732
|
-
'<span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2650
|
+
'<span class="msg-file-info"><span class="msg-file-name">' + escapeHtml(f.name) + '</span>' +
|
|
2733
2651
|
(sizeStr ? '<span class="msg-file-size">' + sizeStr + '</span>' : '') +
|
|
2652
|
+
'</span>' +
|
|
2653
|
+
'<span class="msg-file-actions">' +
|
|
2654
|
+
'<a class="msg-file-download" href="/api/file/' + (fileId || '') + '/download" download="' + escapeHtml(f.name) + '" title="下载" onclick="event.stopPropagation()">⬇</a>' +
|
|
2655
|
+
'</span>' +
|
|
2734
2656
|
'</div>');
|
|
2735
2657
|
}
|
|
2736
2658
|
}
|
|
2737
|
-
return parts.length > 0 ? '<div class="msg-attachments">' + parts.join('') + '</div>' : '';
|
|
2659
|
+
return parts.length > 0 ? '<div class="msg-attachments msg-attachments-files">' + parts.join('') + '</div>' : '';
|
|
2738
2660
|
})();
|
|
2739
2661
|
const thoughtHtml = msg.thought ? (() => {
|
|
2740
2662
|
const isStreaming = !!msg.streaming;
|
|
@@ -2774,7 +2696,7 @@ function _renderMessagesInner() {
|
|
|
2774
2696
|
const ttsIndicator = _isSpeakingThis ?
|
|
2775
2697
|
' <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>' : '';
|
|
2776
2698
|
|
|
2777
|
-
// ── Determine rendering mode
|
|
2699
|
+
// ── Determine rendering mode ──
|
|
2778
2700
|
const hasParts = Array.isArray(msg.parts) && msg.parts.length > 0;
|
|
2779
2701
|
const hasStreamingText = msg._streamingText && msg._streamingText.trim();
|
|
2780
2702
|
const anyContent = msg.content || msg._streamingText || hasParts;
|
|
@@ -2787,10 +2709,11 @@ function _renderMessagesInner() {
|
|
|
2787
2709
|
<span style="font-weight:500">Agent 正在思考...</span>
|
|
2788
2710
|
</div>` : '';
|
|
2789
2711
|
|
|
2790
|
-
// ──
|
|
2791
|
-
|
|
2712
|
+
// ── Bubble content: timeline (parts) or single text ──
|
|
2713
|
+
var bubbleHtml = '';
|
|
2792
2714
|
if (hasParts || hasStreamingText) {
|
|
2793
|
-
|
|
2715
|
+
// Timeline mode: interleaved text + tool cards
|
|
2716
|
+
var partsInner = '';
|
|
2794
2717
|
for (const part of (msg.parts || [])) {
|
|
2795
2718
|
if (part.type === 'text' && part.content.trim()) {
|
|
2796
2719
|
partsInner += '<div class="timeline-segment">' + renderMarkdown(part.content) + '</div>';
|
|
@@ -2807,37 +2730,32 @@ function _renderMessagesInner() {
|
|
|
2807
2730
|
partsInner += '<div class="timeline-segment">' + renderMarkdown(msg._streamingText) + _cursor + '</div>';
|
|
2808
2731
|
}
|
|
2809
2732
|
if (partsInner) {
|
|
2810
|
-
|
|
2811
|
-
|
|
2733
|
+
bubbleHtml = '<div class="message-bubble msg-bubble-wrapper"><div class="msg-timeline">' + partsInner + '</div></div>';
|
|
2734
|
+
}
|
|
2735
|
+
} else {
|
|
2736
|
+
// Single bubble mode: plain text (possibly collapsed)
|
|
2737
|
+
var renderedContent = content;
|
|
2738
|
+
if (!msg.streaming && !isUser && shouldCollapseContent(msg.content)) {
|
|
2739
|
+
renderedContent = '<details class="file-content-collapse"><summary>📄 文件内容处理结果(点击展开)</summary><div class="collapse-body">' + content + '</div></details>';
|
|
2740
|
+
}
|
|
2741
|
+
if (renderedContent) {
|
|
2742
|
+
bubbleHtml = '<div class="message-bubble msg-bubble-wrapper">' + renderedContent + ttsIndicator + '</div>';
|
|
2812
2743
|
}
|
|
2813
2744
|
}
|
|
2814
2745
|
|
|
2815
|
-
//
|
|
2816
|
-
const needCollapse = !msg.streaming && !isUser && !hasParts && !hasStreamingText && shouldCollapseContent(msg.content);
|
|
2817
|
-
const collapsedContent = needCollapse
|
|
2818
|
-
? '<details class="file-content-collapse"><summary>📄 文件内容处理结果(点击展开)</summary><div class="collapse-body">' + content + '</div></details>'
|
|
2819
|
-
: content;
|
|
2820
|
-
|
|
2821
|
-
// Backward compat: single bubble for messages without parts (full width)
|
|
2822
|
-
const singleBubbleHtml = (!hasParts && !hasStreamingText)
|
|
2823
|
-
? (collapsedContent ? `<div class="message-bubble msg-bubble-wrapper">${collapsedContent}${ttsIndicator}</div>` : '')
|
|
2824
|
-
: '';
|
|
2825
|
-
|
|
2826
|
-
// ── Task Plan (historical view only — hidden during streaming, shown after completion) ──
|
|
2746
|
+
// ── Task Plan & Finish Reason (V2 structured output, historical only) ──
|
|
2827
2747
|
var taskPlanHtml = '';
|
|
2828
2748
|
if (!msg.streaming && msg._v2TaskPlan && msg._v2TaskPlan.trim()) {
|
|
2829
2749
|
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>';
|
|
2830
2750
|
}
|
|
2831
|
-
// ── Finish Reason(finish=true 时显示完成原因) ──
|
|
2832
2751
|
var finishReasonHtml = '';
|
|
2833
2752
|
if (msg._v2FinishReason && msg._v2FinishReason.trim()) {
|
|
2834
2753
|
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>';
|
|
2835
2754
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
// [v1.15.9] reasoning 块提级:渲染在 message-row 之外,作为 messages-inner 的直接子元素
|
|
2839
|
-
// 这样它不受 message-avatar(32px) 挤压,能真正撑满 100% 宽度
|
|
2755
|
+
|
|
2756
|
+
// ── Reasoning block (rendered outside message-row for full width) ──
|
|
2840
2757
|
if (reasoningHtml) html += reasoningHtml;
|
|
2758
|
+
// ── Message row ──
|
|
2841
2759
|
html += `
|
|
2842
2760
|
<div class="message-row ${msg.role}${msg.streaming ? ' streaming' : ''}">
|
|
2843
2761
|
<div class="message-avatar">${avatar}</div>
|
|
@@ -2845,12 +2763,11 @@ function _renderMessagesInner() {
|
|
|
2845
2763
|
${thoughtHtml}
|
|
2846
2764
|
${taskPlanHtml}
|
|
2847
2765
|
${finishReasonHtml}
|
|
2848
|
-
${
|
|
2849
|
-
${
|
|
2850
|
-
${
|
|
2766
|
+
${imageAttachmentHtml}
|
|
2767
|
+
${bubbleHtml}
|
|
2768
|
+
${fileAttachmentHtml}
|
|
2851
2769
|
${streamingIndicator}
|
|
2852
|
-
${
|
|
2853
|
-
${msg.time ? `<div class="message-time">${formatTime(msg.time)}</div>` : ''}
|
|
2770
|
+
${msg.time ? '<div class="message-time">' + formatTime(msg.time) + '</div>' : ''}
|
|
2854
2771
|
${actionBtns}
|
|
2855
2772
|
</div>
|
|
2856
2773
|
</div>`;
|
|
@@ -3486,13 +3403,13 @@ function initScrollToBottomBtn() {
|
|
|
3486
3403
|
}, { passive: true });
|
|
3487
3404
|
}
|
|
3488
3405
|
|
|
3489
|
-
function toast(message, type = 'info') {
|
|
3406
|
+
function toast(message, type = 'info', duration = 3000) {
|
|
3490
3407
|
const container = document.getElementById('toastContainer');
|
|
3491
3408
|
const el = document.createElement('div');
|
|
3492
3409
|
el.className = `toast toast-${type}`;
|
|
3493
3410
|
el.textContent = message;
|
|
3494
3411
|
container.appendChild(el);
|
|
3495
|
-
setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); },
|
|
3412
|
+
setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, duration);
|
|
3496
3413
|
}
|
|
3497
3414
|
|
|
3498
3415
|
// ══════════════════════════════════════════════════════
|
package/web/ui/index.html
CHANGED
|
@@ -259,19 +259,19 @@ tr:hover{background:var(--surface2)}
|
|
|
259
259
|
<div class="nav">
|
|
260
260
|
<div style="flex:1;overflow-y:auto;padding:8px 0;min-height:0">
|
|
261
261
|
<div class="nav-item active" data-tooltip="仪表盘" onclick="showPage('dashboard')"><span class="icon">📊</span><span class="icon-text">仪表盘</span></div>
|
|
262
|
-
<div class="nav-item" data-tooltip="Agent 管理" onclick="showPage('agents')"><span class="icon"
|
|
263
|
-
<div class="nav-item" data-tooltip="聊天平台" onclick="showPage('platforms')"><span class="icon"
|
|
262
|
+
<div class="nav-item" data-tooltip="Agent 管理" onclick="showPage('agents')"><span class="icon">🤖</span><span class="icon-text">Agent 管理</span></div>
|
|
263
|
+
<div class="nav-item" data-tooltip="聊天平台" onclick="showPage('platforms')"><span class="icon">🌐</span><span class="icon-text">聊天平台</span></div>
|
|
264
264
|
<div class="nav-item" data-tooltip="组织管理" onclick="showPage('organization')"><span class="icon">🏢</span><span class="icon-text">组织管理</span></div>
|
|
265
265
|
<div class="nav-item" data-tooltip="部门管理" onclick="showPage('departments')"><span class="icon">🏛</span><span class="icon-text">部门管理</span></div>
|
|
266
|
-
<div class="nav-item" data-tooltip="会话管理" onclick="showPage('sessions')"><span class="icon"
|
|
266
|
+
<div class="nav-item" data-tooltip="会话管理" onclick="showPage('sessions')"><span class="icon">💬</span><span class="icon-text">会话管理</span></div>
|
|
267
267
|
<div class="nav-item" data-tooltip="记忆管理" onclick="showPage('memory')"><span class="icon">🧠</span><span class="icon-text">记忆管理</span></div>
|
|
268
268
|
<div class="nav-item" data-tooltip="权限管理" onclick="showPage('permissions')"><span class="icon">🔑</span><span class="icon-text">权限管理</span></div>
|
|
269
|
-
<div class="nav-item" data-tooltip="大模型设置" onclick="showPage('llm')"><span class="icon"
|
|
270
|
-
<div class="nav-item" data-tooltip="系统配置" onclick="showPage('system')"><span class="icon"
|
|
269
|
+
<div class="nav-item" data-tooltip="大模型设置" onclick="showPage('llm')"><span class="icon">🧬</span><span class="icon-text">大模型设置</span></div>
|
|
270
|
+
<div class="nav-item" data-tooltip="系统配置" onclick="showPage('system')"><span class="icon">⚙️</span><span class="icon-text">系统配置</span></div>
|
|
271
271
|
<div class="nav-item" data-tooltip="执行引擎" onclick="showPage('executor')"><span class="icon">🔧</span><span class="icon-text">执行引擎</span></div>
|
|
272
272
|
<div class="nav-item" data-tooltip="技能管理" onclick="showPage('skills')"><span class="icon">🛠</span><span class="icon-text">技能管理</span></div>
|
|
273
273
|
<div class="nav-item" data-tooltip="工作目录" onclick="showPage('files')"><span class="icon">📁</span><span class="icon-text">工作目录</span></div>
|
|
274
|
-
<div class="nav-item" data-tooltip="查看日志" onclick="showPage('logs')"><span class="icon"
|
|
274
|
+
<div class="nav-item" data-tooltip="查看日志" onclick="showPage('logs')"><span class="icon">📜</span><span class="icon-text">查看日志</span></div>
|
|
275
275
|
<div class="nav-item" data-tooltip="任务记录" onclick="showPage('tasks')"><span class="icon">📌</span><span class="icon-text">任务记录</span></div>
|
|
276
276
|
</div>
|
|
277
277
|
<div style="padding:10px 12px;border-top:1px solid var(--border);margin-top:4px;font-size:12px;color:var(--text2);flex-shrink:0" class="sidebar-footer-text">
|
|
@@ -292,7 +292,7 @@ tr:hover{background:var(--surface2)}
|
|
|
292
292
|
|
|
293
293
|
<script>
|
|
294
294
|
const API='';
|
|
295
|
-
const pages={dashboard:'📊 仪表盘',agents:'
|
|
295
|
+
const pages={dashboard:'📊 仪表盘',agents:'🤖 Agent 管理',platforms:'🌐 聊天平台',organization:'🏢 组织管理',departments:'🏛 部门管理',sessions:'💬 会话管理',memory:'🧠 记忆管理',permissions:'🔑 权限管理',llm:'🧬 大模型设置',system:'⚙️ 系统配置',executor:'🔧 执行引擎',skills:'🛠 技能管理',files:'📁 工作目录',logs:'📜 查看日志',tasks:'📌 任务记录'};
|
|
296
296
|
let currentPage='dashboard';
|
|
297
297
|
let allAgentsCache=[];
|
|
298
298
|
let allModelsCache=[];
|