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.
@@ -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
- return True, "VNC 依赖安装完成"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.32.9",
3
+ "version": "1.33.0",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
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
- mgr = get_browser_profile_manager()
5278
- profile = mgr.get_profile(name)
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
- "message": f"浏览器已打开,请在浏览器中完成 {site.get('display_name', name)} 的登录",
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('正在启动浏览器...', 'info');
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
- showToast(r.message || '浏览器已打开', 'success', 5000);
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; }
@@ -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 格式:{type:'v2_tool', data:{id, tool_name, ...}})
1376
+ // 再从 parts 查找(V2 格式或规范化后的 exec 格式)
1377
1377
  if (!evt && msg.parts) {
1378
1378
  for (const part of msg.parts) {
1379
- if (part.type === 'v2_tool' && part.data && String(part.data.id) === String(eventId)) {
1380
- evt = part.data;
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 => { if(m.streaming) m.streaming = false; });
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行注释)