myagent-ai 1.47.5 → 1.47.11

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.
@@ -100,6 +100,9 @@ def _start_xvfb(display_num: int = 99) -> Optional[str]:
100
100
  """
101
101
  启动 Xvfb 虚拟显示服务器。
102
102
 
103
+ [v1.47.10] 增强:如果指定 display 号被占用,自动尝试其他 display 号(:100-:109)。
104
+ 僵尸 Xvfb 进程(OOM Kill 后残留锁文件)是 returncode=1 的常见原因。
105
+
103
106
  Returns:
104
107
  DISPLAY 环境变量值(如 :99),失败返回 None
105
108
  """
@@ -127,36 +130,65 @@ def _start_xvfb(display_num: int = 99) -> Optional[str]:
127
130
  logger.warning("Xvfb 未安装,无法启动虚拟显示")
128
131
  return None
129
132
 
130
- display_str = f":{display_num}"
131
- try:
132
- # 先清理可能残留的 X11 锁文件
133
- for lock_file in (f"/tmp/.X{display_num}-lock", f"/tmp/.X11-unix/X{display_num}"):
134
- if os.path.exists(lock_file):
133
+ # [v1.47.10] 尝试多个 display 号,避免锁文件冲突
134
+ # 优先用 :99(和 VNC 一致),失败则尝试 :100-:109
135
+ display_candidates = [display_num] + list(range(100, 110))
136
+
137
+ for dn in display_candidates:
138
+ display_str = f":{dn}"
139
+ try:
140
+ # 先清理可能残留的 X11 锁文件
141
+ for lock_file in (f"/tmp/.X{dn}-lock", f"/tmp/.X11-unix/X{dn}"):
142
+ if os.path.exists(lock_file):
143
+ try:
144
+ os.unlink(lock_file)
145
+ except Exception:
146
+ pass
147
+
148
+ # 检查是否有 Xvfb 进程已经在用这个 display
149
+ try:
150
+ result = subprocess.run(
151
+ ["pgrep", "-f", f"Xvfb :{dn}"],
152
+ capture_output=True, text=True, timeout=3,
153
+ )
154
+ if result.returncode == 0 and result.stdout.strip():
155
+ # 已有进程在用这个 display,复用它
156
+ _xvfb_display = display_str
157
+ os.environ["DISPLAY"] = display_str
158
+ logger.info(f"检测到已有 Xvfb 进程使用 display :{dn},复用")
159
+ return display_str
160
+ except Exception:
161
+ pass
162
+
163
+ _xvfb_process = subprocess.Popen(
164
+ [xvfb_path, display_str, "-screen", "0", "1920x1080x24", "-ac", "-nolisten", "tcp"],
165
+ stdout=subprocess.DEVNULL,
166
+ stderr=subprocess.PIPE,
167
+ )
168
+ # 等待 Xvfb 启动
169
+ time.sleep(0.8)
170
+ if _xvfb_process.poll() is not None:
171
+ stderr = ""
135
172
  try:
136
- os.unlink(lock_file)
173
+ stderr = _xvfb_process.stderr.read().decode("utf-8", errors="replace")[:300]
137
174
  except Exception:
138
175
  pass
139
-
140
- _xvfb_process = subprocess.Popen(
141
- [xvfb_path, display_str, "-screen", "0", "1920x1080x24", "-ac", "-nolisten", "tcp"],
142
- stdout=subprocess.DEVNULL,
143
- stderr=subprocess.DEVNULL,
144
- )
145
- # 等待 Xvfb 启动
146
- time.sleep(0.5)
147
- if _xvfb_process.poll() is not None:
148
- logger.error(f"Xvfb 启动后立即退出 (returncode={_xvfb_process.returncode})")
176
+ logger.debug(f"Xvfb :{dn} 启动失败 (rc={_xvfb_process.returncode}): {stderr}")
177
+ _xvfb_process = None
178
+ # 继续尝试下一个 display
179
+ continue
180
+
181
+ _xvfb_display = display_str
182
+ os.environ["DISPLAY"] = display_str
183
+ logger.info(f"Xvfb 虚拟显示已启动: {display_str}")
184
+ return display_str
185
+ except Exception as e:
186
+ logger.debug(f"启动 Xvfb :{dn} 异常: {e}")
149
187
  _xvfb_process = None
150
- return None
188
+ continue
151
189
 
152
- _xvfb_display = display_str
153
- os.environ["DISPLAY"] = display_str
154
- logger.info(f"Xvfb 虚拟显示已启动: {display_str}")
155
- return display_str
156
- except Exception as e:
157
- logger.error(f"启动 Xvfb 失败: {e}")
158
- _xvfb_process = None
159
- return None
190
+ logger.error("所有 display 号均无法启动 Xvfb")
191
+ return None
160
192
 
161
193
 
162
194
  def _stop_xvfb() -> None:
@@ -182,15 +214,21 @@ def _ensure_display() -> Optional[Dict[str, Any]]:
182
214
  """
183
215
  确保有可用的 X11 显示,供浏览器有头模式使用。
184
216
 
185
- 优先级:
186
- 1. 复用 VNC 远程桌面的 Xvfb 显示(用户可在 VNC 中看到浏览器操作)
187
- 2. 自动启动 VNC(含 Xvfb + x11vnc + websockify)
188
- 3. VNC 不可用时独立启动 Xvfb
217
+ Termux+Ubuntu 环境: 只支持 VNC,无其他 fallback
218
+ Termux 容器环境: VNC > 独立 Xvfb 降级
189
219
 
190
220
  Returns:
191
221
  {"display": ":99", "vnc": True/False, "xvfb_standalone": True/False}
192
222
  失败返回 None
193
223
  """
224
+ # 检测是否为 Termux 环境
225
+ _is_termux = False
226
+ try:
227
+ from core.env_detect import is_termux
228
+ _is_termux = is_termux()
229
+ except ImportError:
230
+ pass
231
+
194
232
  # 1. 尝试复用 VNC 远程桌面
195
233
  try:
196
234
  from core.vnc_manager import get_vnc_manager
@@ -208,8 +246,6 @@ def _ensure_display() -> Optional[Dict[str, Any]]:
208
246
  import asyncio
209
247
  loop = asyncio.get_event_loop()
210
248
  if loop.is_running():
211
- # 在已有事件循环中,用 run_until_complete 不行,
212
- # 创建 task 等待完成
213
249
  import concurrent.futures
214
250
  with concurrent.futures.ThreadPoolExecutor() as pool:
215
251
  result = pool.submit(asyncio.run, vnc.start()).result(timeout=30)
@@ -231,8 +267,16 @@ def _ensure_display() -> Optional[Dict[str, Any]]:
231
267
  except Exception as e:
232
268
  logger.warning(f"VNC 检测异常: {e}")
233
269
 
234
- # 2. VNC 不可用 独立启动 Xvfb
235
- xvfb_display = _start_xvfb()
270
+ # ── Termux+Ubuntu: VNC 是唯一方式,不允许其他 fallback ──
271
+ if _is_termux:
272
+ logger.error(
273
+ "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
274
+ "VNC 启动失败。请检查 VNC 配置或手动启动 VNC 远程桌面。"
275
+ )
276
+ return None
277
+
278
+ # 2. 非 Termux 容器环境 → 独立启动 Xvfb 作为 fallback
279
+ xvfb_display = _start_xvfb(display_num=100)
236
280
  if xvfb_display:
237
281
  return {"display": xvfb_display, "vnc": False, "xvfb_standalone": True}
238
282
 
@@ -527,17 +571,24 @@ class StealthBrowser:
527
571
  logger.info("桌面环境,使用系统 Chrome 原生参数")
528
572
 
529
573
  # ── 无显示环境处理 ──
530
- # 使用 is_desktop() 判断是否为桌面环境(Windows/macOS/Linux有真实显示器)
531
- # 桌面环境直接使用系统 Chrome,不需要 VNC/Xvfb
532
- # 仅在非桌面的 Linux(容器/Termux)环境中才尝试 VNC/Xvfb
574
+ # 桌面环境 (Windows/macOS/Linux有真实显示器): 直接用系统 Chrome
575
+ # Termux+Ubuntu: 仅支持 VNC,不降级到 headless
576
+ # Termux 容器: VNC > Xvfb > headless 降级
577
+ _is_termux_env = False
578
+ try:
579
+ from core.env_detect import is_termux
580
+ _is_termux_env = is_termux()
581
+ except ImportError:
582
+ pass
583
+
533
584
  if not self._headless:
534
585
  try:
535
586
  from core.env_detect import is_desktop
536
587
  if is_desktop():
537
- # 桌面环境(Windows/macOS/Linux有真实显示器): 直接用系统 Chrome
588
+ # 桌面环境: 直接用系统 Chrome
538
589
  logger.info("桌面环境,直接使用系统浏览器,跳过 VNC/Xvfb")
539
590
  else:
540
- # 非桌面 Linux(容器/Termux): 尝试 VNC > Xvfb > headless 降级
591
+ # 非桌面环境 (容器/Termux): 通过 _ensure_display() 获取显示
541
592
  display = _ensure_display()
542
593
  if display:
543
594
  self._vnc_used = display.get("vnc", False)
@@ -545,6 +596,17 @@ class StealthBrowser:
545
596
  if self._vnc_used:
546
597
  logger.info(f"复用 VNC 远程桌面显示 ({display['display']}),可在 VNC 中查看浏览器操作")
547
598
  else:
599
+ # ── Termux+Ubuntu: VNC 失败 → 直接报错,不降级 headless ──
600
+ if _is_termux_env:
601
+ return SkillResult(
602
+ success=False,
603
+ error=(
604
+ "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
605
+ "VNC 启动失败。请先启动 VNC 远程桌面,"
606
+ "或通过 Web 管理面板打开 VNC 后再使用浏览器功能。"
607
+ ),
608
+ )
609
+ # ── 非 Termux 容器: 降级到 headless ──
548
610
  self._headless = True
549
611
  logger.warning(
550
612
  "无显示环境且 VNC/Xvfb 均不可用,自动降级为 headless 模式"
@@ -557,10 +619,39 @@ class StealthBrowser:
557
619
  self._vnc_used = display.get("vnc", False)
558
620
  self._xvfb_started_by_us = display.get("xvfb_standalone", False)
559
621
  else:
622
+ if _is_termux_env:
623
+ return SkillResult(
624
+ success=False,
625
+ error=(
626
+ "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
627
+ "VNC 启动失败。请先启动 VNC 远程桌面后再使用浏览器功能。"
628
+ ),
629
+ )
560
630
  self._headless = True
561
631
  logger.warning(
562
632
  "无 DISPLAY 环境且 VNC/Xvfb 均不可用,自动降级为 headless 模式"
563
633
  )
634
+ else:
635
+ # headless=True 被显式请求,但 Termux 环境下仍强制使用 VNC
636
+ # 因为 headless Chromium 在 Termux 下容易被 OOM Kill
637
+ if _is_termux_env:
638
+ logger.info("Termux+Ubuntu 环境: 忽略 headless 请求,强制使用 VNC 模式")
639
+ self._headless = False
640
+ display = _ensure_display()
641
+ if display:
642
+ self._vnc_used = display.get("vnc", False)
643
+ self._xvfb_started_by_us = display.get("xvfb_standalone", False)
644
+ if self._vnc_used:
645
+ logger.info(f"Termux+Ubuntu: 已通过 VNC 获取显示 ({display['display']})")
646
+ else:
647
+ return SkillResult(
648
+ success=False,
649
+ error=(
650
+ "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
651
+ "VNC 启动失败。headless 模式在此环境下不可用(容易被 OOM Kill)。"
652
+ "请先启动 VNC 远程桌面后再使用浏览器功能。"
653
+ ),
654
+ )
564
655
 
565
656
  # 无头模式(co.headless() 内部设置 --headless=new)
566
657
  if self._headless:
@@ -1664,9 +1755,42 @@ class StealthBrowser:
1664
1755
  except Exception as e:
1665
1756
  logger.debug(f"清理残留 Chrome 进程时出错: {e}")
1666
1757
 
1758
+ @staticmethod
1759
+ def _is_snap_wrapper(path: str) -> bool:
1760
+ """[v1.47.10] 检测浏览器路径是否是 snap 包装器。
1761
+
1762
+ Ubuntu 22.04+ 的 chromium-browser 和 firefox 可能是 snap 包装脚本,
1763
+ 在 proot 环境下不可用(snapd 需要 systemd)。
1764
+ """
1765
+ try:
1766
+ if not os.path.isfile(path):
1767
+ return False
1768
+ # snap 包装器通常是 shell 脚本,包含 "snap" 关键字
1769
+ with open(path, "r", errors="replace") as f:
1770
+ content = f.read(2048)
1771
+ # 常见 snap 包装脚本特征
1772
+ snap_signatures = [
1773
+ "snap", # snap run chromium / snap run firefox
1774
+ "/snap/", # snap 路径
1775
+ "SNAP_NAME", # snap 环境变量
1776
+ "snapd", # snap daemon
1777
+ ]
1778
+ # 只有文本文件(脚本)才检查,二进制不需要
1779
+ if content.startswith("#!") or content.startswith("/*"):
1780
+ for sig in snap_signatures:
1781
+ if sig in content:
1782
+ return True
1783
+ except Exception:
1784
+ pass
1785
+ return False
1786
+
1667
1787
  @staticmethod
1668
1788
  def _detect_browser() -> Optional[str]:
1669
- """自动检测 Chromium/Chrome 浏览器路径"""
1789
+ """自动检测 Chromium/Chrome 浏览器路径
1790
+
1791
+ [v1.47.10] 增加 snap 包装器检测:proot 下 snap 不可用,
1792
+ 跳过 snap 包装脚本,避免浏览器启动后无法连接。
1793
+ """
1670
1794
  # 1. 环境变量
1671
1795
  for key in ("CHROME_PATH", "BROWSER_PATH", "CHROMIUM_PATH"):
1672
1796
  val = os.environ.get(key, "").strip()
@@ -1680,6 +1804,10 @@ class StealthBrowser:
1680
1804
  ):
1681
1805
  found = shutil.which(cmd)
1682
1806
  if found:
1807
+ # [v1.47.10] 跳过 snap 包装器
1808
+ if StealthBrowser._is_snap_wrapper(found):
1809
+ logger.info(f"跳过 {cmd} ({found}) — snap 包装器,proot 下不可用")
1810
+ continue
1683
1811
  return found
1684
1812
 
1685
1813
  # 3. 操作系统特定路径
@@ -1718,6 +1846,10 @@ class StealthBrowser:
1718
1846
  "/usr/bin/microsoft-edge",
1719
1847
  ):
1720
1848
  if os.path.isfile(p) and os.access(p, os.X_OK):
1849
+ # [v1.47.10] 跳过 snap 包装器
1850
+ if StealthBrowser._is_snap_wrapper(p):
1851
+ logger.info(f"跳过 {p} — snap 包装器,proot 下不可用")
1852
+ continue
1721
1853
  return p
1722
1854
 
1723
1855
  # 4. Puppeteer / Playwright 缓存
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.47.5",
3
+ "version": "1.47.11",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {