myagent-ai 1.32.9 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/vnc_manager.py +39 -2
- package/install/setup-termux-vnc.sh +516 -0
- package/package.json +1 -1
- package/web/api_server.py +80 -15
- package/web/ui/admin/admin-sites.js +33 -2
- package/web/ui/chat/chat_main.js +2 -0
- package/web/ui/chat/flow_engine.js +122 -4
- package/worklog.md +21 -0
package/core/vnc_manager.py
CHANGED
|
@@ -228,6 +228,12 @@ class VNCManager:
|
|
|
228
228
|
"port": self.novnc_port,
|
|
229
229
|
"pid": self._websockify_process.pid if ws_alive else None,
|
|
230
230
|
},
|
|
231
|
+
# [v1.33.0] 浏览器和窗口管理器信息
|
|
232
|
+
"browser": {
|
|
233
|
+
"available": bool(shutil.which("google-chrome") or shutil.which("chromium-browser") or shutil.which("chromium")),
|
|
234
|
+
"path": shutil.which("google-chrome") or shutil.which("chromium-browser") or shutil.which("chromium") or None,
|
|
235
|
+
},
|
|
236
|
+
"window_manager": self._wm_name,
|
|
231
237
|
"novnc_url": f"/vnc/vnc.html?autoconnect=true&resize=scale" if running else None,
|
|
232
238
|
}
|
|
233
239
|
|
|
@@ -246,6 +252,8 @@ class VNCManager:
|
|
|
246
252
|
def ensure_dependencies(self) -> tuple:
|
|
247
253
|
"""检测并安装 VNC 所需依赖。
|
|
248
254
|
|
|
255
|
+
[v1.33.0] 增加 Chrome/Chromium 浏览器检测和窗口管理器依赖。
|
|
256
|
+
|
|
249
257
|
Returns:
|
|
250
258
|
(success: bool, message: str)
|
|
251
259
|
"""
|
|
@@ -267,6 +275,22 @@ class VNCManager:
|
|
|
267
275
|
missing.append("websockify")
|
|
268
276
|
install_commands.append("websockify")
|
|
269
277
|
|
|
278
|
+
# [v1.33.0] 4. Chrome/Chromium 浏览器检测
|
|
279
|
+
_has_browser = (
|
|
280
|
+
shutil.which("google-chrome") or
|
|
281
|
+
shutil.which("chromium-browser") or
|
|
282
|
+
shutil.which("chromium")
|
|
283
|
+
)
|
|
284
|
+
if not _has_browser:
|
|
285
|
+
missing.append("Chrome/Chromium")
|
|
286
|
+
# 不加入 apt 自动安装列表(Chrome 安装较复杂,由安装脚本处理)
|
|
287
|
+
|
|
288
|
+
# [v1.33.0] 5. 窗口管理器检测
|
|
289
|
+
_has_wm = any(shutil.which(wm) for wm in ["fluxbox", "openbox", "xfce4-session", "icewm", "jwm", "twm"])
|
|
290
|
+
if not _has_wm:
|
|
291
|
+
missing.append("窗口管理器(fluxbox)")
|
|
292
|
+
install_commands.append("fluxbox")
|
|
293
|
+
|
|
270
294
|
if not missing:
|
|
271
295
|
return True, "所有 VNC 依赖已就绪"
|
|
272
296
|
|
|
@@ -276,7 +300,7 @@ class VNCManager:
|
|
|
276
300
|
sudo = ["sudo"] if os.getuid() != 0 else []
|
|
277
301
|
|
|
278
302
|
# 安装 apt 包
|
|
279
|
-
apt_packages = [p for p in install_commands if p in ("xvfb", "x11vnc")]
|
|
303
|
+
apt_packages = [p for p in install_commands if p in ("xvfb", "x11vnc", "fluxbox")]
|
|
280
304
|
if apt_packages:
|
|
281
305
|
try:
|
|
282
306
|
logger.info(f"apt install: {apt_packages}")
|
|
@@ -327,8 +351,21 @@ class VNCManager:
|
|
|
327
351
|
if not shutil.which(cmd_name):
|
|
328
352
|
all_ok = False
|
|
329
353
|
|
|
354
|
+
# [v1.33.0] 检查浏览器和窗口管理器
|
|
355
|
+
_has_browser = shutil.which("google-chrome") or shutil.which("chromium-browser") or shutil.which("chromium")
|
|
356
|
+
_has_wm = any(shutil.which(wm) for wm in ["fluxbox", "openbox", "xfce4-session", "icewm", "jwm", "twm"])
|
|
357
|
+
|
|
358
|
+
warnings = []
|
|
359
|
+
if not _has_browser:
|
|
360
|
+
warnings.append("Chrome/Chromium 未安装(请运行 install/setup-termux-vnc.sh 安装)")
|
|
361
|
+
if not _has_wm:
|
|
362
|
+
warnings.append("窗口管理器未安装(VNC 将显示黑屏)")
|
|
363
|
+
|
|
330
364
|
if all_ok:
|
|
331
|
-
|
|
365
|
+
msg = "VNC 依赖安装完成"
|
|
366
|
+
if warnings:
|
|
367
|
+
msg += f"(警告: {'; '.join(warnings)})"
|
|
368
|
+
return True, msg
|
|
332
369
|
else:
|
|
333
370
|
still_missing = [c for c in ["Xvfb", "x11vnc", "websockify"] if not shutil.which(c)]
|
|
334
371
|
return False, f"部分依赖安装失败: {still_missing}"
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# ══════════════════════════════════════════════════════════════════
|
|
5
|
+
# MyAgent — Termux + Ubuntu 远程桌面一键安装脚本
|
|
6
|
+
# ══════════════════════════════════════════════════════════════════
|
|
7
|
+
#
|
|
8
|
+
# 功能:
|
|
9
|
+
# 1. 在 Termux proot-distro Ubuntu 中安装完整 GUI 桌面环境
|
|
10
|
+
# 2. 安装 Chrome/Chromium 浏览器(ARM64 / AMD64 自动适配)
|
|
11
|
+
# 3. 安装 VNC 远程桌面所需全部依赖 (Xvfb + x11vnc + websockify)
|
|
12
|
+
# 4. 安装窗口管理器 (fluxbox) + 终端 (xterm)
|
|
13
|
+
# 5. 安装中文字体和输入法支持
|
|
14
|
+
#
|
|
15
|
+
# 使用方法:
|
|
16
|
+
# 在 Termux 中:
|
|
17
|
+
# proot-distro login ubuntu
|
|
18
|
+
# bash /path/to/setup-termux-vnc.sh
|
|
19
|
+
#
|
|
20
|
+
# 或一行命令:
|
|
21
|
+
# curl -fsSL https://raw.githubusercontent.com/ctz168/myagent/main/install/setup-termux-vnc.sh | bash
|
|
22
|
+
#
|
|
23
|
+
# 架构:
|
|
24
|
+
# Android Termux → proot-distro Ubuntu → Xvfb(:99) + x11vnc(5900)
|
|
25
|
+
# + websockify(6080) → noVNC
|
|
26
|
+
# + fluxbox + Chrome
|
|
27
|
+
#
|
|
28
|
+
# ══════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
BOLD='\033[1m'
|
|
31
|
+
ACCENT='\033[36m'
|
|
32
|
+
INFO='\033[90m'
|
|
33
|
+
SUCCESS='\033[32m'
|
|
34
|
+
WARN='\033[33m'
|
|
35
|
+
ERROR='\033[31m'
|
|
36
|
+
NC='\033[0m'
|
|
37
|
+
|
|
38
|
+
info() { echo -e "${INFO}[i]${NC} $*"; }
|
|
39
|
+
success() { echo -e "${SUCCESS}[✓]${NC} $*"; }
|
|
40
|
+
warn() { echo -e "${WARN}[!]${NC} $*"; }
|
|
41
|
+
err() { echo -e "${ERROR}[✗]${NC} $*" >&2; }
|
|
42
|
+
step() { echo -e "${ACCENT}[*]${NC} $*"; }
|
|
43
|
+
banner() { echo -e "\n${BOLD}${ACCENT}$*${NC}\n"; }
|
|
44
|
+
|
|
45
|
+
# ── 检测是否在 Termux/proot 环境中 ──────────────────────────
|
|
46
|
+
detect_environment() {
|
|
47
|
+
banner "检测运行环境..."
|
|
48
|
+
|
|
49
|
+
# 检测是否在 proot 环境中
|
|
50
|
+
IS_PROOT=false
|
|
51
|
+
if grep -qi 'proot' /proc/self/maps 2>/dev/null || \
|
|
52
|
+
grep -qi 'proot' /proc/version 2>/dev/null || \
|
|
53
|
+
[ -n "${PROOT_SERVICE:-}" ]; then
|
|
54
|
+
IS_PROOT=true
|
|
55
|
+
info "检测到 proot 环境 (Termux proot-distro)"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# 检测架构
|
|
59
|
+
ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m)
|
|
60
|
+
info "系统架构: $ARCH"
|
|
61
|
+
|
|
62
|
+
# 检测是否为 root
|
|
63
|
+
IS_ROOT=false
|
|
64
|
+
if [ "$(id -u)" -eq 0 ]; then
|
|
65
|
+
IS_ROOT=true
|
|
66
|
+
info "当前用户: root"
|
|
67
|
+
else
|
|
68
|
+
info "当前用户: $(whoami) (非 root,将使用 sudo)"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# 检测 OS
|
|
72
|
+
if [ ! -f /etc/os-release ]; then
|
|
73
|
+
err "无法检测操作系统版本,此脚本仅支持 Ubuntu/Debian"
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
source /etc/os-release
|
|
77
|
+
info "操作系统: $PRETTY_NAME"
|
|
78
|
+
|
|
79
|
+
if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then
|
|
80
|
+
warn "此脚本主要针对 Ubuntu/Debian,当前系统 $ID 可能不兼容"
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ── apt 封装 ────────────────────────────────────────────────
|
|
85
|
+
apt_install() {
|
|
86
|
+
if $IS_ROOT; then
|
|
87
|
+
apt-get install -y --no-install-recommends "$@"
|
|
88
|
+
else
|
|
89
|
+
sudo apt-get install -y --no-install-recommends "$@"
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
apt_update() {
|
|
94
|
+
if $IS_ROOT; then
|
|
95
|
+
apt-get update -qq 2>/dev/null || true
|
|
96
|
+
else
|
|
97
|
+
sudo apt-get update -qq 2>/dev/null || true
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ── Step 1: 更新系统并安装基础依赖 ──────────────────────────
|
|
102
|
+
install_base_deps() {
|
|
103
|
+
banner "Step 1/6: 安装基础依赖..."
|
|
104
|
+
|
|
105
|
+
step "更新 apt 源..."
|
|
106
|
+
apt_update
|
|
107
|
+
|
|
108
|
+
step "安装基础工具..."
|
|
109
|
+
apt_install \
|
|
110
|
+
wget curl gnupg2 ca-certificates \
|
|
111
|
+
software-properties-common \
|
|
112
|
+
apt-transport-https \
|
|
113
|
+
procps psmisc \
|
|
114
|
+
2>/dev/null || true
|
|
115
|
+
|
|
116
|
+
success "基础依赖安装完成"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ── Step 2: 安装 X11 / VNC 远程桌面依赖 ────────────────────
|
|
120
|
+
install_vnc_deps() {
|
|
121
|
+
banner "Step 2/6: 安装 X11 / VNC 远程桌面依赖..."
|
|
122
|
+
|
|
123
|
+
step "安装 Xvfb (虚拟显示服务器)..."
|
|
124
|
+
apt_install xvfb
|
|
125
|
+
|
|
126
|
+
step "安装 x11vnc (VNC 服务器)..."
|
|
127
|
+
apt_install x11vnc
|
|
128
|
+
|
|
129
|
+
step "安装 x11vnc 依赖库..."
|
|
130
|
+
apt_install \
|
|
131
|
+
python3-numpy libxtst6 libxext6 libxfixes3 \
|
|
132
|
+
libxrandr2 libxrender1 libxinerama1 libfontconfig1 \
|
|
133
|
+
2>/dev/null || true
|
|
134
|
+
|
|
135
|
+
step "安装 X11 工具..."
|
|
136
|
+
apt_install \
|
|
137
|
+
x11-xserver-utils xdotool xsetroot \
|
|
138
|
+
2>/dev/null || true
|
|
139
|
+
|
|
140
|
+
step "安装窗口管理器 (fluxbox) + 终端 (xterm)..."
|
|
141
|
+
apt_install fluxbox xterm 2>/dev/null || {
|
|
142
|
+
warn "fluxbox 安装失败,尝试 openbox..."
|
|
143
|
+
apt_install openbox 2>/dev/null || true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
step "安装截图工具 (ffmpeg)..."
|
|
147
|
+
apt_install ffmpeg 2>/dev/null || {
|
|
148
|
+
warn "ffmpeg 安装失败,尝试 ImageMagick..."
|
|
149
|
+
apt_install imagemagick 2>/dev/null || true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
step "安装 websockify (Python WebSocket 代理)..."
|
|
153
|
+
pip3 install websockify 2>/dev/null || pip install websockify 2>/dev/null || {
|
|
154
|
+
warn "pip 安装 websockify 失败,尝试 apt..."
|
|
155
|
+
apt_install websockify 2>/dev/null || true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# 验证关键组件
|
|
159
|
+
local missing=()
|
|
160
|
+
for cmd in Xvfb x11vnc; do
|
|
161
|
+
if ! command -v "$cmd" &>/dev/null; then
|
|
162
|
+
missing+=("$cmd")
|
|
163
|
+
fi
|
|
164
|
+
done
|
|
165
|
+
if ! command -v websockify &>/dev/null && ! python3 -c "import websockify" 2>/dev/null; then
|
|
166
|
+
missing+=("websockify")
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if [ ${#missing[@]} -gt 0 ]; then
|
|
170
|
+
err "VNC 关键组件安装失败: ${missing[*]}"
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
success "VNC 依赖安装完成"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ── Step 3: 安装 Chrome/Chromium 浏览器 ────────────────────
|
|
178
|
+
install_chrome() {
|
|
179
|
+
banner "Step 3/6: 安装 Chrome/Chromium 浏览器..."
|
|
180
|
+
|
|
181
|
+
# 检查是否已安装
|
|
182
|
+
if command -v google-chrome &>/dev/null; then
|
|
183
|
+
success "Google Chrome 已安装: $(google-chrome --version 2>/dev/null)"
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
if command -v chromium-browser &>/dev/null; then
|
|
187
|
+
success "Chromium 已安装: $(chromium-browser --version 2>/dev/null)"
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
if command -v chromium &>/dev/null; then
|
|
191
|
+
success "Chromium 已安装: $(chromium --version 2>/dev/null)"
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# 安装 Chrome/Chromium 共享库依赖
|
|
196
|
+
step "安装 Chrome/Chromium 共享库依赖..."
|
|
197
|
+
apt_install \
|
|
198
|
+
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
|
199
|
+
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
|
|
200
|
+
libgbm1 libpango-1.0-0 libcairo2 libasound2 libatspi2.0-0 \
|
|
201
|
+
libxshmfence1 libx11-xcb1 libxcb-dri3-0 \
|
|
202
|
+
fonts-liberation xdg-utils \
|
|
203
|
+
2>/dev/null || true
|
|
204
|
+
|
|
205
|
+
# 根据架构选择安装方式
|
|
206
|
+
case "$ARCH" in
|
|
207
|
+
amd64|x86_64)
|
|
208
|
+
step "AMD64 架构: 尝试安装 Google Chrome..."
|
|
209
|
+
install_chrome_amd64
|
|
210
|
+
;;
|
|
211
|
+
arm64|aarch64)
|
|
212
|
+
step "ARM64 架构: 安装 Chromium (Google Chrome 不支持 ARM64)..."
|
|
213
|
+
install_chrome_arm64
|
|
214
|
+
;;
|
|
215
|
+
*)
|
|
216
|
+
warn "未知架构 $ARCH,尝试通用方式安装 Chromium..."
|
|
217
|
+
install_chromium_apt
|
|
218
|
+
;;
|
|
219
|
+
esac
|
|
220
|
+
|
|
221
|
+
# 验证安装
|
|
222
|
+
if command -v google-chrome &>/dev/null; then
|
|
223
|
+
success "Google Chrome 安装成功: $(google-chrome --version 2>/dev/null)"
|
|
224
|
+
elif command -v chromium-browser &>/dev/null; then
|
|
225
|
+
success "Chromium 安装成功: $(chromium-browser --version 2>/dev/null)"
|
|
226
|
+
elif command -v chromium &>/dev/null; then
|
|
227
|
+
success "Chromium 安装成功: $(chromium --version 2>/dev/null)"
|
|
228
|
+
else
|
|
229
|
+
warn "Chrome/Chromium 标准安装失败,尝试 Puppeteer 内置 Chrome..."
|
|
230
|
+
install_chrome_puppeteer
|
|
231
|
+
fi
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
install_chrome_amd64() {
|
|
235
|
+
# 尝试 Google Chrome 官方源
|
|
236
|
+
step "添加 Google Chrome 官方 APT 源..."
|
|
237
|
+
if ! grep -q 'dl.google.com/linux/chrome' /etc/apt/sources.list 2>/dev/null && \
|
|
238
|
+
[ ! -f /etc/apt/sources.list.d/google-chrome.list ]; then
|
|
239
|
+
(
|
|
240
|
+
if $IS_ROOT; then
|
|
241
|
+
wget -qO- https://dl.google.com/linux/linux_signing_key.pub | apt-key add - 2>/dev/null || true
|
|
242
|
+
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" \
|
|
243
|
+
> /etc/apt/sources.list.d/google-chrome.list
|
|
244
|
+
else
|
|
245
|
+
wget -qO- https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 2>/dev/null || true
|
|
246
|
+
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" \
|
|
247
|
+
| sudo tee /etc/apt/sources.list.d/google-chrome.list > /dev/null
|
|
248
|
+
fi
|
|
249
|
+
apt_update
|
|
250
|
+
) || warn "添加 Chrome 源失败"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
step "安装 google-chrome-stable..."
|
|
254
|
+
apt_install google-chrome-stable 2>/dev/null && return 0
|
|
255
|
+
|
|
256
|
+
# Chrome 安装失败,回退到 Chromium
|
|
257
|
+
warn "Google Chrome 安装失败,回退到 Chromium..."
|
|
258
|
+
install_chromium_apt
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
install_chrome_arm64() {
|
|
262
|
+
# ARM64 上 Google Chrome 不支持,使用 Chromium
|
|
263
|
+
step "ARM64: 安装 Chromium 浏览器..."
|
|
264
|
+
|
|
265
|
+
# Ubuntu 22.04+ 使用 snap 安装的 chromium,在 proot 下不工作
|
|
266
|
+
# 尝试使用 PPA 或直接安装 deb 包
|
|
267
|
+
apt_install chromium-browser 2>/dev/null && return 0
|
|
268
|
+
apt_install chromium 2>/dev/null && return 0
|
|
269
|
+
|
|
270
|
+
# 尝试从 Ubuntu ports 安装
|
|
271
|
+
step "尝试通过 apt 直接安装 chromium..."
|
|
272
|
+
apt_update
|
|
273
|
+
apt_install chromium-browser 2>/dev/null && return 0
|
|
274
|
+
|
|
275
|
+
# 尝试下载预编译的 Chromium (chromium-bsu 不行,要浏览器)
|
|
276
|
+
warn "apt 安装 Chromium 失败,尝试下载预编译版本..."
|
|
277
|
+
|
|
278
|
+
# 使用 snap 的 chromium 在 proot 下不工作
|
|
279
|
+
# 最佳方案: 使用 npx puppeteer 下载的 Chrome for Testing
|
|
280
|
+
step "通过 Puppeteer 下载 Chrome for Testing (ARM64)..."
|
|
281
|
+
if command -v npx &>/dev/null; then
|
|
282
|
+
npx puppeteer browsers install chrome@stable 2>/dev/null || true
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# 如果还是失败,尝试安装 firefox 作为备选
|
|
286
|
+
if ! command -v chromium &>/dev/null && ! command -v chromium-browser &>/dev/null; then
|
|
287
|
+
warn "Chromium 安装失败,安装 Firefox 作为备选浏览器..."
|
|
288
|
+
apt_install firefox 2>/dev/null || {
|
|
289
|
+
warn "Firefox 也安装失败"
|
|
290
|
+
}
|
|
291
|
+
fi
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
install_chromium_apt() {
|
|
295
|
+
step "通过 apt 安装 Chromium..."
|
|
296
|
+
apt_install chromium-browser 2>/dev/null && return 0
|
|
297
|
+
apt_install chromium 2>/dev/null && return 0
|
|
298
|
+
return 1
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
install_chrome_puppeteer() {
|
|
302
|
+
step "通过 Puppeteer 安装 Chrome for Testing..."
|
|
303
|
+
if command -v npx &>/dev/null; then
|
|
304
|
+
npx puppeteer browsers install chrome@stable 2>/dev/null || true
|
|
305
|
+
# Puppeteer 安装的 Chrome 通常在 ~/.cache/puppeteer/ 下
|
|
306
|
+
# 创建符号链接使其可被发现
|
|
307
|
+
CHROME_PATH=$(find ~/.cache/puppeteer -name "chrome" -type f 2>/dev/null | head -1)
|
|
308
|
+
if [ -n "$CHROME_PATH" ]; then
|
|
309
|
+
ln -sf "$CHROME_PATH" /usr/local/bin/google-chrome 2>/dev/null || \
|
|
310
|
+
ln -sf "$CHROME_PATH" /usr/bin/google-chrome 2>/dev/null || true
|
|
311
|
+
success "Puppeteer Chrome 已链接: $CHROME_PATH"
|
|
312
|
+
return 0
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
warn "Puppeteer Chrome 安装也失败"
|
|
316
|
+
return 1
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# ── Step 4: 安装中文字体和输入法 ────────────────────────────
|
|
320
|
+
install_fonts_and_locale() {
|
|
321
|
+
banner "Step 4/6: 安装中文字体和本地化支持..."
|
|
322
|
+
|
|
323
|
+
step "安装中文字体..."
|
|
324
|
+
apt_install \
|
|
325
|
+
fonts-noto-cjk \
|
|
326
|
+
fonts-noto-cjk-extra \
|
|
327
|
+
fonts-wqy-zenhei \
|
|
328
|
+
fonts-wqy-microhei \
|
|
329
|
+
2>/dev/null || true
|
|
330
|
+
|
|
331
|
+
step "安装额外字体..."
|
|
332
|
+
apt_install \
|
|
333
|
+
fonts-liberation \
|
|
334
|
+
fonts-dejavu \
|
|
335
|
+
fonts-freefont-ttf \
|
|
336
|
+
2>/dev/null || true
|
|
337
|
+
|
|
338
|
+
step "配置 locale..."
|
|
339
|
+
apt_install locales 2>/dev/null || true
|
|
340
|
+
if $IS_ROOT; then
|
|
341
|
+
locale-gen zh_CN.UTF-8 2>/dev/null || true
|
|
342
|
+
locale-gen en_US.UTF-8 2>/dev/null || true
|
|
343
|
+
update-locale LANG=zh_CN.UTF-8 2>/dev/null || true
|
|
344
|
+
else
|
|
345
|
+
sudo locale-gen zh_CN.UTF-8 2>/dev/null || true
|
|
346
|
+
sudo locale-gen en_US.UTF-8 2>/dev/null || true
|
|
347
|
+
sudo update-locale LANG=zh_CN.UTF-8 2>/dev/null || true
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# 刷新字体缓存
|
|
351
|
+
fc-cache -fv 2>/dev/null || true
|
|
352
|
+
|
|
353
|
+
success "字体和本地化安装完成"
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# ── Step 5: 修复 Termux/proot 特有问题 ─────────────────────
|
|
357
|
+
fix_termux_issues() {
|
|
358
|
+
banner "Step 5/6: 修复 Termux/proot 兼容性问题..."
|
|
359
|
+
|
|
360
|
+
# 1. /dev/shm — Android 限制 SysV IPC
|
|
361
|
+
step "检查 /dev/shm..."
|
|
362
|
+
if [ ! -d /dev/shm ]; then
|
|
363
|
+
mkdir -p /dev/shm 2>/dev/null || warn "无法创建 /dev/shm (proot 限制)"
|
|
364
|
+
fi
|
|
365
|
+
chmod 1777 /dev/shm 2>/dev/null || warn "无法修改 /dev/shm 权限"
|
|
366
|
+
|
|
367
|
+
# 2. /tmp/.X11-unix — X11 socket 目录
|
|
368
|
+
step "确保 X11 socket 目录存在..."
|
|
369
|
+
mkdir -p /tmp/.X11-unix
|
|
370
|
+
chmod 1777 /tmp/.X11-unix 2>/dev/null || true
|
|
371
|
+
|
|
372
|
+
# 3. Chrome 沙箱 — proot 下无法使用 Chrome sandbox
|
|
373
|
+
step "配置 Chrome/Chromium 无沙箱模式 (proot 兼容)..."
|
|
374
|
+
# 创建包装脚本,自动添加 --no-sandbox --disable-gpu
|
|
375
|
+
for browser_name in google-chrome chromium-browser chromium; do
|
|
376
|
+
browser_path=$(command -v "$browser_name" 2>/dev/null || true)
|
|
377
|
+
if [ -n "$browser_path" ] && [ "$browser_path" != "/usr/local/bin/$browser_name" ]; then
|
|
378
|
+
real_path=$(readlink -f "$browser_path" 2>/dev/null || echo "$browser_path")
|
|
379
|
+
cat > "/usr/local/bin/$browser_name" <<SCRIPT
|
|
380
|
+
#!/bin/bash
|
|
381
|
+
exec $real_path --no-sandbox --disable-gpu --disable-dev-shm-usage "\$@"
|
|
382
|
+
SCRIPT
|
|
383
|
+
chmod +x "/usr/local/bin/$browser_name"
|
|
384
|
+
info "已为 $browser_name 添加 --no-sandbox 包装 (实际: $real_path)"
|
|
385
|
+
fi
|
|
386
|
+
done
|
|
387
|
+
|
|
388
|
+
# 4. 创建 Chrome 启动桌面快捷方式
|
|
389
|
+
step "创建桌面快捷方式..."
|
|
390
|
+
mkdir -p /usr/share/applications
|
|
391
|
+
cat > /usr/share/applications/chromium-browser.desktop <<'DESKTOP'
|
|
392
|
+
[Desktop Entry]
|
|
393
|
+
Version=1.0
|
|
394
|
+
Name=Chromium Web Browser
|
|
395
|
+
GenericName=Web Browser
|
|
396
|
+
Comment=Access the Internet
|
|
397
|
+
Exec=chromium-browser --no-sandbox --disable-gpu --disable-dev-shm-usage %U
|
|
398
|
+
Terminal=false
|
|
399
|
+
Type=Application
|
|
400
|
+
Icon=chromium
|
|
401
|
+
Categories=Network;WebBrowser;
|
|
402
|
+
DESKTOP
|
|
403
|
+
|
|
404
|
+
# 5. 确保用户 chrome 配置目录存在
|
|
405
|
+
step "确保 Chrome 配置目录..."
|
|
406
|
+
mkdir -p ~/.config/chromium 2>/dev/null || true
|
|
407
|
+
mkdir -p ~/.config/google-chrome 2>/dev/null || true
|
|
408
|
+
|
|
409
|
+
success "Termux/proot 兼容性修复完成"
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# ── Step 6: 验证安装并输出信息 ─────────────────────────────
|
|
413
|
+
verify_and_summary() {
|
|
414
|
+
banner "Step 6/6: 验证安装..."
|
|
415
|
+
|
|
416
|
+
local all_ok=true
|
|
417
|
+
|
|
418
|
+
# 验证 VNC 组件
|
|
419
|
+
echo ""
|
|
420
|
+
echo -e " ${BOLD}VNC 远程桌面组件:${NC}"
|
|
421
|
+
for cmd in Xvfb x11vnc; do
|
|
422
|
+
if command -v "$cmd" &>/dev/null; then
|
|
423
|
+
success " $cmd: $(command -v "$cmd")"
|
|
424
|
+
else
|
|
425
|
+
err " $cmd: 未安装"
|
|
426
|
+
all_ok=false
|
|
427
|
+
fi
|
|
428
|
+
done
|
|
429
|
+
if command -v websockify &>/dev/null || python3 -c "import websockify" 2>/dev/null; then
|
|
430
|
+
success " websockify: 已安装"
|
|
431
|
+
else
|
|
432
|
+
err " websockify: 未安装"
|
|
433
|
+
all_ok=false
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
# 验证窗口管理器
|
|
437
|
+
echo ""
|
|
438
|
+
echo -e " ${BOLD}窗口管理器:${NC}"
|
|
439
|
+
for wm in fluxbox openbox xfce4-session icewm jwm twm; do
|
|
440
|
+
if command -v "$wm" &>/dev/null; then
|
|
441
|
+
success " $wm: 已安装"
|
|
442
|
+
break
|
|
443
|
+
fi
|
|
444
|
+
done
|
|
445
|
+
|
|
446
|
+
# 验证浏览器
|
|
447
|
+
echo ""
|
|
448
|
+
echo -e " ${BOLD}浏览器:${NC}"
|
|
449
|
+
for browser in google-chrome chromium-browser chromium; do
|
|
450
|
+
if command -v "$browser" &>/dev/null; then
|
|
451
|
+
success " $browser: $(command -v "$browser")"
|
|
452
|
+
break
|
|
453
|
+
fi
|
|
454
|
+
done
|
|
455
|
+
if ! command -v google-chrome &>/dev/null && \
|
|
456
|
+
! command -v chromium-browser &>/dev/null && \
|
|
457
|
+
! command -v chromium &>/dev/null; then
|
|
458
|
+
err " 浏览器: 未安装"
|
|
459
|
+
all_ok=false
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# 验证字体
|
|
463
|
+
echo ""
|
|
464
|
+
echo -e " ${BOLD}中文字体:${NC}"
|
|
465
|
+
if fc-list :lang=zh 2>/dev/null | head -1 | grep -q .; then
|
|
466
|
+
success " 中文字体已安装"
|
|
467
|
+
else
|
|
468
|
+
warn " 中文字体未检测到"
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
# 输出使用说明
|
|
472
|
+
echo ""
|
|
473
|
+
echo "═══════════════════════════════════════════════════════════"
|
|
474
|
+
if $all_ok; then
|
|
475
|
+
echo -e " ${SUCCESS}${BOLD}所有组件安装成功!${NC}"
|
|
476
|
+
else
|
|
477
|
+
echo -e " ${WARN}${BOLD}部分组件安装失败,请查看上方日志${NC}"
|
|
478
|
+
fi
|
|
479
|
+
echo "═══════════════════════════════════════════════════════════"
|
|
480
|
+
echo ""
|
|
481
|
+
echo -e " ${BOLD}使用方法:${NC}"
|
|
482
|
+
echo ""
|
|
483
|
+
echo " 1. 启动 MyAgent Web 服务:"
|
|
484
|
+
echo -e " ${ACCENT}myagent-ai web${NC}"
|
|
485
|
+
echo ""
|
|
486
|
+
echo " 2. 在聊天页面点击 🖥️ 远程桌面按钮启动 VNC"
|
|
487
|
+
echo " 或通过 API: POST /api/vnc/start"
|
|
488
|
+
echo ""
|
|
489
|
+
echo " 3. 在管理页面 → 网站管理 → 点击"登录"按钮"
|
|
490
|
+
echo " 系统会自动启动 VNC 并在远程桌面中打开浏览器"
|
|
491
|
+
echo ""
|
|
492
|
+
echo " 4. VNC 连接信息:"
|
|
493
|
+
echo " noVNC Web: http://<服务器IP>:<服务端口>/vnc/vnc.html"
|
|
494
|
+
echo " VNC 直连: <服务器IP>:5900"
|
|
495
|
+
echo ""
|
|
496
|
+
echo -e " ${WARN}注意:${NC} 在 Termux proot 环境下,Chrome/Chromium"
|
|
497
|
+
echo " 已自动配置 --no-sandbox 模式以兼容 proot 限制。"
|
|
498
|
+
echo ""
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# ── Main ────────────────────────────────────────────────────
|
|
502
|
+
main() {
|
|
503
|
+
echo ""
|
|
504
|
+
echo -e " ${BOLD}${ACCENT}MyAgent${NC} ${BOLD}— Termux + Ubuntu 远程桌面安装${NC}"
|
|
505
|
+
echo ""
|
|
506
|
+
|
|
507
|
+
detect_environment
|
|
508
|
+
install_base_deps
|
|
509
|
+
install_vnc_deps
|
|
510
|
+
install_chrome
|
|
511
|
+
install_fonts_and_locale
|
|
512
|
+
fix_termux_issues
|
|
513
|
+
verify_and_summary
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
main "$@"
|
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -554,16 +554,16 @@ class ApiServer:
|
|
|
554
554
|
# ── 工作流 ──
|
|
555
555
|
r.add_get("/api/workflows", self.handle_list_workflows)
|
|
556
556
|
r.add_get("/api/workflows/stats", self.handle_workflow_stats)
|
|
557
|
-
r.add_get("/api/workflows/{agent:[\d]+}", self.handle_list_agent_workflows)
|
|
558
|
-
r.add_post("/api/workflows/{agent:[\d]+}", self.handle_create_workflow)
|
|
559
|
-
r.add_get("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}", self.handle_get_workflow)
|
|
560
|
-
r.add_put("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}", self.handle_update_workflow)
|
|
561
|
-
r.add_delete("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}", self.handle_delete_workflow)
|
|
562
|
-
r.add_post("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/run", self.handle_run_workflow)
|
|
563
|
-
r.add_post("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/toggle", self.handle_toggle_workflow)
|
|
564
|
-
r.add_post("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/duplicate", self.handle_duplicate_workflow)
|
|
565
|
-
r.add_get("/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/runs", self.handle_list_workflow_runs)
|
|
566
|
-
r.add_get("/api/workflows/{agent:[\d]+}/runs/{rid:[\w\-]+}", self.handle_get_workflow_run)
|
|
557
|
+
r.add_get(r"/api/workflows/{agent:[\d]+}", self.handle_list_agent_workflows)
|
|
558
|
+
r.add_post(r"/api/workflows/{agent:[\d]+}", self.handle_create_workflow)
|
|
559
|
+
r.add_get(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}", self.handle_get_workflow)
|
|
560
|
+
r.add_put(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}", self.handle_update_workflow)
|
|
561
|
+
r.add_delete(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}", self.handle_delete_workflow)
|
|
562
|
+
r.add_post(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/run", self.handle_run_workflow)
|
|
563
|
+
r.add_post(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/toggle", self.handle_toggle_workflow)
|
|
564
|
+
r.add_post(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/duplicate", self.handle_duplicate_workflow)
|
|
565
|
+
r.add_get(r"/api/workflows/{agent:[\d]+}/{wid:[\w\-]+}/runs", self.handle_list_workflow_runs)
|
|
566
|
+
r.add_get(r"/api/workflows/{agent:[\d]+}/runs/{rid:[\w\-]+}", self.handle_get_workflow_run)
|
|
567
567
|
# ── 任务持久化 ──
|
|
568
568
|
r.add_get("/api/tasks", self.handle_list_tasks)
|
|
569
569
|
r.add_post("/api/tasks/{task_id}/retry", self.handle_retry_task)
|
|
@@ -3258,9 +3258,44 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3258
3258
|
if self._agents_initialized:
|
|
3259
3259
|
return
|
|
3260
3260
|
self._ensure_default_agent()
|
|
3261
|
+
# [v1.33.0] 同步 agent ID 序列号:确保 .agent_id_seq 不小于已有 agent 目录的最大 ID
|
|
3262
|
+
self._sync_agent_id_seq()
|
|
3261
3263
|
self._agents_initialized = True
|
|
3262
3264
|
logger.info("系统 Agent 初始化完成(全权Agent + 配置助手)")
|
|
3263
3265
|
|
|
3266
|
+
def _sync_agent_id_seq(self):
|
|
3267
|
+
"""[v1.33.0] 同步 agent ID 序列号,确保 .agent_id_seq >= 已有 agent 目录的最大数字 ID。
|
|
3268
|
+
|
|
3269
|
+
问题:_ensure_default_agent() 用硬编码 aid=1/2 创建目录但不更新序列号,
|
|
3270
|
+
导致 next_agent_id() 从 0 开始计数,新建 agent 时 ID 1/2 冲突。
|
|
3271
|
+
修复:扫描 agents/ 目录中所有数字子目录,取最大值与 .agent_id_seq 对比,
|
|
3272
|
+
取较大者写入 .agent_id_seq,保证后续 next_agent_id() 不会返回已用 ID。
|
|
3273
|
+
"""
|
|
3274
|
+
from core.utils import _AGENT_ID_SEQ
|
|
3275
|
+
try:
|
|
3276
|
+
# 扫描 agents/ 目录中的数字子目录
|
|
3277
|
+
agents_dir = self._agents_dir()
|
|
3278
|
+
max_id = 0
|
|
3279
|
+
if agents_dir.exists():
|
|
3280
|
+
for d in agents_dir.iterdir():
|
|
3281
|
+
if d.is_dir() and d.name.isdigit():
|
|
3282
|
+
max_id = max(max_id, int(d.name))
|
|
3283
|
+
|
|
3284
|
+
# 读取当前序列号
|
|
3285
|
+
try:
|
|
3286
|
+
cur_seq = int(_AGENT_ID_SEQ.read_text().strip())
|
|
3287
|
+
except (FileNotFoundError, ValueError):
|
|
3288
|
+
cur_seq = 0
|
|
3289
|
+
|
|
3290
|
+
# 取较大者
|
|
3291
|
+
new_seq = max(max_id, cur_seq)
|
|
3292
|
+
if new_seq > cur_seq:
|
|
3293
|
+
_AGENT_ID_SEQ.parent.mkdir(parents=True, exist_ok=True)
|
|
3294
|
+
_AGENT_ID_SEQ.write_text(str(new_seq))
|
|
3295
|
+
logger.info(f"Agent ID 序列号已同步: {cur_seq} → {new_seq} (最大目录 ID={max_id})")
|
|
3296
|
+
except Exception as e:
|
|
3297
|
+
logger.warning(f"同步 Agent ID 序列号失败: {e}")
|
|
3298
|
+
|
|
3264
3299
|
def _ensure_default_agent(self):
|
|
3265
3300
|
"""确保默认 agent 存在(ID=1,名为「全权Agent」,目录名=1)
|
|
3266
3301
|
|
|
@@ -3569,8 +3604,15 @@ window.addEventListener('beforeunload', function() {{
|
|
|
3569
3604
|
|
|
3570
3605
|
aid = str(next_agent_id())
|
|
3571
3606
|
ad = self._agent_dir(aid)
|
|
3607
|
+
# [v1.33.0] ID 碰撞时自动重试,而非直接报错(防御序列号不同步的极端情况)
|
|
3608
|
+
_retries = 0
|
|
3609
|
+
while (ad / "config.json").exists() and _retries < 20:
|
|
3610
|
+
logger.warning(f"Agent ID={aid} 目录已存在,跳过并重试 (retry={_retries})")
|
|
3611
|
+
aid = str(next_agent_id())
|
|
3612
|
+
ad = self._agent_dir(aid)
|
|
3613
|
+
_retries += 1
|
|
3572
3614
|
if (ad / "config.json").exists():
|
|
3573
|
-
return web.json_response({"error": f"Agent '{name}' already exists"}, status=409)
|
|
3615
|
+
return web.json_response({"error": f"Agent '{name}' already exists (ID conflict)"}, status=409)
|
|
3574
3616
|
|
|
3575
3617
|
now = _now_iso()
|
|
3576
3618
|
cfg = {
|
|
@@ -5265,17 +5307,36 @@ window.addEventListener('beforeunload', function() {{
|
|
|
5265
5307
|
return web.json_response({"ok": True})
|
|
5266
5308
|
|
|
5267
5309
|
async def handle_open_site_browser(self, request):
|
|
5268
|
-
"""POST /api/sites/{name}/open-browser - 打开浏览器登录网站
|
|
5310
|
+
"""POST /api/sites/{name}/open-browser - 打开浏览器登录网站
|
|
5311
|
+
|
|
5312
|
+
[v1.33.0] 自动启动 VNC 远程桌面(如果尚未运行),确保用户可通过 VNC 看到浏览器界面。
|
|
5313
|
+
在 Termux/proot 环境下,有头浏览器需要 Xvfb 虚拟显示才能运行。
|
|
5314
|
+
"""
|
|
5269
5315
|
name = request.match_info["name"]
|
|
5270
5316
|
reg = self._get_site_registry()
|
|
5271
5317
|
site = reg.get_site(name)
|
|
5272
5318
|
if not site:
|
|
5273
5319
|
return web.json_response({"error": f"网站 '{name}' 不存在"}, status=404)
|
|
5274
5320
|
|
|
5321
|
+
# [v1.33.0] 自动启动 VNC 远程桌面(有头浏览器需要虚拟显示)
|
|
5322
|
+
vnc_mgr = self._get_vnc_manager()
|
|
5323
|
+
vnc_was_started = vnc_mgr.is_running
|
|
5324
|
+
if not vnc_was_started:
|
|
5325
|
+
logger.info(f"网站登录需要 VNC 远程桌面,正在自动启动...")
|
|
5326
|
+
vnc_result = await vnc_mgr.start()
|
|
5327
|
+
if not vnc_result["success"]:
|
|
5328
|
+
return web.json_response({"error": f"启动远程桌面失败(登录浏览器需要远程桌面): {vnc_result['message']}"}, status=500)
|
|
5329
|
+
# VNC 启动后,重建 MCP 客户端以使用有头浏览器
|
|
5330
|
+
try:
|
|
5331
|
+
from aiskills.chromedev_mcp import rebuild_mcp_client
|
|
5332
|
+
await rebuild_mcp_client()
|
|
5333
|
+
except Exception as e:
|
|
5334
|
+
logger.warning(f"VNC 启动后重建 MCP 客户端失败: {e}")
|
|
5335
|
+
|
|
5275
5336
|
# Ensure profile is initialized
|
|
5276
5337
|
from core.browser_profile import get_browser_profile_manager
|
|
5277
|
-
|
|
5278
|
-
profile =
|
|
5338
|
+
bprofile_mgr = get_browser_profile_manager()
|
|
5339
|
+
profile = bprofile_mgr.get_profile(name)
|
|
5279
5340
|
if not profile.is_initialized():
|
|
5280
5341
|
profile.ensure_dirs()
|
|
5281
5342
|
|
|
@@ -5298,7 +5359,11 @@ window.addEventListener('beforeunload', function() {{
|
|
|
5298
5359
|
"ok": True,
|
|
5299
5360
|
"site": name,
|
|
5300
5361
|
"login_url": login_url,
|
|
5301
|
-
"
|
|
5362
|
+
"vnc_started": not vnc_was_started, # [v1.33.0] 是否自动启动了 VNC
|
|
5363
|
+
"vnc_running": True, # VNC 现在肯定是运行中
|
|
5364
|
+
"novnc_url": "/vnc/vnc.html?autoconnect=true&resize=scale",
|
|
5365
|
+
"message": f"浏览器已打开,请通过远程桌面完成 {site.get('display_name', name)} 的登录"
|
|
5366
|
+
+ ("(已自动启动远程桌面)" if not vnc_was_started else ""),
|
|
5302
5367
|
})
|
|
5303
5368
|
except Exception as e:
|
|
5304
5369
|
logger.error(f"打开浏览器失败 ({name}): {e}")
|
|
@@ -329,15 +329,46 @@ async function deleteSiteProfile(name) {
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
// ── Browser Login ──
|
|
332
|
+
// [v1.33.0] 登录按钮自动关联 VNC 远程桌面:后端会自动启动 VNC,前端在登录后引导用户打开远程桌面
|
|
332
333
|
async function openSiteBrowser(name) {
|
|
333
|
-
showToast('
|
|
334
|
+
showToast('正在启动浏览器和远程桌面...', 'info', 30000);
|
|
334
335
|
var r = await api('/api/sites/' + encodeURIComponent(name) + '/open-browser', { method: 'POST' });
|
|
335
336
|
if (r.error) { showToast(r.error, 'danger'); return; }
|
|
336
|
-
|
|
337
|
+
|
|
338
|
+
var msg = r.message || '浏览器已打开';
|
|
339
|
+
showToast(msg, 'success', 8000);
|
|
340
|
+
|
|
341
|
+
// [v1.33.0] 如果 VNC 是自动启动的,提示用户打开远程桌面查看浏览器
|
|
342
|
+
if (r.vnc_started || r.vnc_running) {
|
|
343
|
+
// 提供"查看远程桌面"按钮让用户可以跳转到 VNC
|
|
344
|
+
var vncHint = document.createElement('div');
|
|
345
|
+
vncHint.id = 'vncLoginHint';
|
|
346
|
+
vncHint.style.cssText = 'position:fixed;bottom:80px;right:20px;z-index:10000;background:var(--card);border:1px solid var(--accent);border-radius:8px;padding:12px 16px;box-shadow:0 4px 12px rgba(0,0,0,0.3);max-width:320px;font-size:13px';
|
|
347
|
+
vncHint.innerHTML = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">' +
|
|
348
|
+
'<span style="font-size:16px">🖥️</span>' +
|
|
349
|
+
'<strong style="color:var(--text)">远程桌面已就绪</strong></div>' +
|
|
350
|
+
'<div style="color:var(--text2);margin-bottom:10px">浏览器已在远程桌面中打开,请通过远程桌面完成登录操作</div>' +
|
|
351
|
+
'<div style="display:flex;gap:8px">' +
|
|
352
|
+
'<button onclick="openAdminVNC()" style="padding:4px 12px;border-radius:4px;border:1px solid var(--accent);background:var(--accent);color:#fff;cursor:pointer;font-size:12px">打开远程桌面</button>' +
|
|
353
|
+
'<button onclick="this.closest(\'#vncLoginHint\').remove()" style="padding:4px 12px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--text2);cursor:pointer;font-size:12px">关闭</button>' +
|
|
354
|
+
'</div>';
|
|
355
|
+
document.body.appendChild(vncHint);
|
|
356
|
+
// 15 秒后自动消失
|
|
357
|
+
setTimeout(function() { var h = document.getElementById('vncLoginHint'); if (h) h.remove(); }, 15000);
|
|
358
|
+
}
|
|
359
|
+
|
|
337
360
|
// Refresh to update profile status
|
|
338
361
|
setTimeout(function() { renderSites(); }, 2000);
|
|
339
362
|
}
|
|
340
363
|
|
|
364
|
+
// [v1.33.0] 在管理页面打开 VNC 远程桌面(跳转到聊天页面的 VNC 界面,或直接打开新标签)
|
|
365
|
+
function openAdminVNC() {
|
|
366
|
+
var vncHint = document.getElementById('vncLoginHint');
|
|
367
|
+
if (vncHint) vncHint.remove();
|
|
368
|
+
// 打开 VNC 在新标签页
|
|
369
|
+
window.open('/vnc/vnc.html?autoconnect=true&resize=scale', '_blank');
|
|
370
|
+
}
|
|
371
|
+
|
|
341
372
|
async function closeSiteBrowser(name) {
|
|
342
373
|
var r = await api('/api/sites/' + encodeURIComponent(name) + '/close-browser', { method: 'POST' });
|
|
343
374
|
if (r.error) { showToast(r.error, 'danger'); return; }
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -3526,6 +3526,8 @@ function groupHistoryMessages(messages) {
|
|
|
3526
3526
|
// ── [v1.23.20] 统一消息渲染函数:历史消息和流式消息共享 ──
|
|
3527
3527
|
// 生成单条消息的完整 HTML(message-row 外壳 + 所有内部内容)
|
|
3528
3528
|
// 流式结束后也用此函数重建 DOM,确保历史/流式样式完全一致
|
|
3529
|
+
// [v1.32.9] 流式结束后 _normalizeMessageAfterStreaming() 将 parts 转为 groupHistoryMessages() 格式,
|
|
3530
|
+
// 所以无论数据来源如何,此函数都走同一渲染分支
|
|
3529
3531
|
window.buildMessageHtml = function(msg, idx, agent) {
|
|
3530
3532
|
const isUser = msg.role === 'user';
|
|
3531
3533
|
if (msg.role === 'tool') return '';
|
|
@@ -1373,11 +1373,13 @@ function showToolResultModal(msgIndex, eventId) {
|
|
|
1373
1373
|
if (msg.exec_events) {
|
|
1374
1374
|
evt = msg.exec_events.find(e => String(e.id) === String(eventId));
|
|
1375
1375
|
}
|
|
1376
|
-
// 再从 parts 查找(V2
|
|
1376
|
+
// 再从 parts 查找(V2 格式或规范化后的 exec 格式)
|
|
1377
1377
|
if (!evt && msg.parts) {
|
|
1378
1378
|
for (const part of msg.parts) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1379
|
+
// [v1.32.9] 同时支持 v2_tool 和规范化后的 exec 格式
|
|
1380
|
+
const inner = (part.type === 'v2_tool') ? part.data : (part.type === 'exec') ? part.data : null;
|
|
1381
|
+
if (inner && String(inner.id) === String(eventId)) {
|
|
1382
|
+
evt = inner;
|
|
1381
1383
|
break;
|
|
1382
1384
|
}
|
|
1383
1385
|
}
|
|
@@ -1532,6 +1534,112 @@ function _assembleV2Content(msg, msgParts) {
|
|
|
1532
1534
|
return '(无回复)';
|
|
1533
1535
|
}
|
|
1534
1536
|
|
|
1537
|
+
// ══════════════════════════════════════════════════════
|
|
1538
|
+
// ── [v1.32.9] Post-Streaming Normalization ──
|
|
1539
|
+
// ══════════════════════════════════════════════════════
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* 流式结束后,将 msg.parts 规范化为与 groupHistoryMessages() 一致的格式。
|
|
1543
|
+
* 这确保 buildMessageHtml() 在流式和历史两条路径上走同一个渲染分支,
|
|
1544
|
+
* 避免视觉差异。
|
|
1545
|
+
*
|
|
1546
|
+
* 主要处理:
|
|
1547
|
+
* 1. v2_tool (tool_start + tool_result) → exec (合并卡片,与历史格式对齐)
|
|
1548
|
+
* 2. 仅含1个文本部分且无工具调用时 → 删除 parts(走简单气泡路径,与历史对齐)
|
|
1549
|
+
* 3. v2_ask → 保留原样
|
|
1550
|
+
*/
|
|
1551
|
+
function _normalizeMessageAfterStreaming(msg) {
|
|
1552
|
+
if (!msg || !Array.isArray(msg.parts) || msg.parts.length === 0) return;
|
|
1553
|
+
|
|
1554
|
+
// ── Step 1: 转换 v2_tool 为 exec 格式,并合并 tool_start + tool_result ──
|
|
1555
|
+
var normalizedParts = [];
|
|
1556
|
+
var evtIdCounter = 0;
|
|
1557
|
+
|
|
1558
|
+
for (var i = 0; i < msg.parts.length; i++) {
|
|
1559
|
+
var part = msg.parts[i];
|
|
1560
|
+
|
|
1561
|
+
if (part.type === 'v2_tool') {
|
|
1562
|
+
var inner = part.data || {};
|
|
1563
|
+
// tool_start: 创建 exec 卡片
|
|
1564
|
+
if (inner.type === 'tool_start' || inner.status === 'running') {
|
|
1565
|
+
var execPart = {
|
|
1566
|
+
type: 'exec',
|
|
1567
|
+
data: {
|
|
1568
|
+
id: evtIdCounter,
|
|
1569
|
+
type: 'tool_call',
|
|
1570
|
+
title: inner.title || inner.tool_name || '工具调用',
|
|
1571
|
+
tool_name: inner.tool_name || '',
|
|
1572
|
+
params: inner.params,
|
|
1573
|
+
status: 'done',
|
|
1574
|
+
has_result: false,
|
|
1575
|
+
result_data: null,
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
// 向后搜索同一 tool_name 的 tool_result 进行合并
|
|
1579
|
+
for (var ri = i + 1; ri < msg.parts.length; ri++) {
|
|
1580
|
+
var rp = msg.parts[ri];
|
|
1581
|
+
if (rp.type === 'v2_tool' && rp.data && rp.data.type === 'tool_result') {
|
|
1582
|
+
var rInner = rp.data;
|
|
1583
|
+
if (rInner.tool_name === inner.tool_name || rInner.id === inner.id) {
|
|
1584
|
+
execPart.data.has_result = true;
|
|
1585
|
+
execPart.data.success = rInner.success;
|
|
1586
|
+
execPart.data.summary = rInner.summary ? rInner.summary.substring(0, 500).trim() : undefined;
|
|
1587
|
+
execPart.data.result = rInner.result || { output: rInner.summary || '' };
|
|
1588
|
+
if (rInner.title) execPart.data.title = rInner.title;
|
|
1589
|
+
// 标记已消费
|
|
1590
|
+
rp._consumed = true;
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
// 遇到下一个 tool_start 就停止搜索
|
|
1595
|
+
if (rp.type === 'v2_tool' && rp.data && (rp.data.type === 'tool_start' || rp.data.status === 'running')) break;
|
|
1596
|
+
}
|
|
1597
|
+
evtIdCounter++;
|
|
1598
|
+
normalizedParts.push(execPart);
|
|
1599
|
+
} else if (inner.type === 'tool_result' && !part._consumed) {
|
|
1600
|
+
// 孤立的 tool_result(没有匹配的 tool_start)
|
|
1601
|
+
evtIdCounter++;
|
|
1602
|
+
normalizedParts.push({
|
|
1603
|
+
type: 'exec',
|
|
1604
|
+
data: {
|
|
1605
|
+
id: evtIdCounter,
|
|
1606
|
+
type: 'tool_result',
|
|
1607
|
+
title: inner.title || inner.tool_name || '工具结果',
|
|
1608
|
+
tool_name: inner.tool_name || '',
|
|
1609
|
+
success: inner.success,
|
|
1610
|
+
summary: inner.summary ? inner.summary.substring(0, 500).trim() : undefined,
|
|
1611
|
+
result: inner.result || { output: inner.summary || '' },
|
|
1612
|
+
has_result: true,
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
// 其他 v2_tool 类型(如已完成但非 start/result 的),也转为 exec
|
|
1617
|
+
else if (inner.type !== 'tool_start' && inner.type !== 'tool_result') {
|
|
1618
|
+
evtIdCounter++;
|
|
1619
|
+
normalizedParts.push({
|
|
1620
|
+
type: 'exec',
|
|
1621
|
+
data: Object.assign({}, inner, { id: evtIdCounter })
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
} else {
|
|
1625
|
+
// text / exec / v2_ask 等类型保持不变
|
|
1626
|
+
normalizedParts.push(part);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// ── Step 2: 与 groupHistoryMessages() 对齐——决定是否保留 parts ──
|
|
1631
|
+
// groupHistoryMessages 逻辑: 仅当 hasExecParts || textParts.length > 1 时设置 parts
|
|
1632
|
+
var textParts = normalizedParts.filter(function(p) { return p.type === 'text'; });
|
|
1633
|
+
var hasExecParts = normalizedParts.some(function(p) { return p.type === 'exec'; });
|
|
1634
|
+
|
|
1635
|
+
if (hasExecParts || textParts.length > 1) {
|
|
1636
|
+
msg.parts = normalizedParts;
|
|
1637
|
+
} else {
|
|
1638
|
+
// 仅1段文本且无工具调用 → 删除 parts,走简单气泡渲染路径
|
|
1639
|
+
delete msg.parts;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1535
1643
|
// ══════════════════════════════════════════════════════
|
|
1536
1644
|
// ── Voice Input: User Bubble Replacement ──
|
|
1537
1645
|
// ══════════════════════════════════════════════════════
|
|
@@ -1899,6 +2007,8 @@ async function sendMessage(opts) {
|
|
|
1899
2007
|
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
1900
2008
|
state.messages[msgIdx]._streamingText = '';
|
|
1901
2009
|
if (allExecEvents.length > 0) state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
2010
|
+
// [v1.32.9] 规范化 parts 为 groupHistoryMessages() 格式
|
|
2011
|
+
_normalizeMessageAfterStreaming(state.messages[msgIdx]);
|
|
1902
2012
|
}
|
|
1903
2013
|
// Start new message
|
|
1904
2014
|
state.messages.push({ role: 'user', content: evt.message, time: new Date().toISOString() });
|
|
@@ -2308,6 +2418,8 @@ async function sendMessage(opts) {
|
|
|
2308
2418
|
state.messages[msgIdx].exec_events = allExecEvents;
|
|
2309
2419
|
// Assemble final content: prefer V2 reasoning/ask text over raw XML
|
|
2310
2420
|
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
2421
|
+
// [v1.32.9] 规范化 parts 为 groupHistoryMessages() 格式,确保 buildMessageHtml() 渲染一致
|
|
2422
|
+
_normalizeMessageAfterStreaming(state.messages[msgIdx]);
|
|
2311
2423
|
}
|
|
2312
2424
|
|
|
2313
2425
|
// ── 流式传输完成,清除恢复数据 ──
|
|
@@ -2416,7 +2528,13 @@ async function sendMessage(opts) {
|
|
|
2416
2528
|
state.isGenerating = false;
|
|
2417
2529
|
state.abortController = null;
|
|
2418
2530
|
// 重置所有消息的流式标志
|
|
2419
|
-
state.messages.forEach(m => {
|
|
2531
|
+
state.messages.forEach(m => {
|
|
2532
|
+
if(m.streaming) {
|
|
2533
|
+
m.streaming = false;
|
|
2534
|
+
// [v1.32.9] 规范化 parts,确保异常结束时也和历史格式一致
|
|
2535
|
+
_normalizeMessageAfterStreaming(m);
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2420
2538
|
hideTypingIndicator();
|
|
2421
2539
|
stopExecTimerPolling();
|
|
2422
2540
|
document.getElementById('sendBtn').style.display = '';
|
package/worklog.md
CHANGED
|
@@ -19,3 +19,24 @@ Stage Summary:
|
|
|
19
19
|
- [XSS] admin-core.js - esc() 函数增加单引号转义,与 escHtml 统一
|
|
20
20
|
- [Content-Type] admin-core.js - api() 函数不再自动设置 Content-Type,支持 FormData multipart 上传
|
|
21
21
|
- 注意: GitHub token 已失效,推送失败,代码仅本地提交
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
Task ID: 2
|
|
25
|
+
Agent: main
|
|
26
|
+
Task: 统一聊天页面流式渲染与历史记录渲染格式 (v1.32.10)
|
|
27
|
+
|
|
28
|
+
Work Log:
|
|
29
|
+
- 深入分析 buildMessageHtml()、groupHistoryMessages()、updateStreamingMessage() 三大函数
|
|
30
|
+
- 发现核心差异:流式结束后 msg.parts 格式与 groupHistoryMessages() 输出不一致
|
|
31
|
+
- 流式总是设置 parts(走 timeline 布局),历史仅在有 exec/multiple text 时设置 parts(走简单气泡布局)
|
|
32
|
+
- v2_tool 类型的 parts 与 exec 类型数据结构不同,renderInlineExecEvent() 渲染逻辑不同
|
|
33
|
+
- 添加 _normalizeMessageAfterStreaming() 函数,在流式结束后规范化 msg.parts
|
|
34
|
+
- v2_tool parts 转为 exec 格式,合并 tool_start + tool_result 为单个卡片
|
|
35
|
+
- 仅1段文本且无工具调用时删除 parts,走简单气泡渲染路径
|
|
36
|
+
- 在3处流式结束点调用规范化:正常结束、queue_start、异常 finally
|
|
37
|
+
- 修复 showToolResultModal() 搜索 parts 时同时支持 v2_tool 和 exec 格式
|
|
38
|
+
|
|
39
|
+
Stage Summary:
|
|
40
|
+
- 核心修复:流式结束后规范化消息数据格式,确保 buildMessageHtml() 走同一渲染分支
|
|
41
|
+
- 版本:v1.32.10 已发布到 npm
|
|
42
|
+
- 文件变更:flow_engine.js (+120行), chat_main.js (+2行注释)
|