myagent-ai 1.47.14 → 1.47.16

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.
@@ -367,6 +367,10 @@ class StealthBrowser:
367
367
  self._user_data_dir = ""
368
368
  self._xvfb_started_by_us = False # 是否由本实例独立启动的 Xvfb
369
369
  self._vnc_used = False # 是否使用了 VNC 远程桌面的显示
370
+ # [v1.47.16] Firefox+VNC 模式:VNC 环境下直接用 Firefox,不走 DrissionPage/Chromium
371
+ self._firefox_mode = False
372
+ self._firefox_process: Optional[subprocess.Popen] = None
373
+ self._firefox_profile_dir = ""
370
374
  # [v1.35.0] 浏览器实例级异步锁:防止多个 Agent 并发操作同一个浏览器实例
371
375
  # 同一 profile 的浏览器共享 _page,并发操作会导致竞态(导航冲突、点击错位等)
372
376
  self._usage_lock = asyncio.Lock()
@@ -634,6 +638,10 @@ class StealthBrowser:
634
638
  self._xvfb_started_by_us = display.get("xvfb_standalone", False)
635
639
  if self._vnc_used:
636
640
  logger.info(f"复用 VNC 远程桌面显示 ({display['display']}),可在 VNC 中查看浏览器操作")
641
+ # [v1.47.16] VNC 模式下直接用 Firefox,不走 DrissionPage/Chromium
642
+ # Chromium 在 proot ARM64 下与 DrissionPage 不兼容
643
+ logger.info("VNC 模式: 直接启动 Firefox(跳过 Chromium 检测)")
644
+ return self._start_firefox_in_vnc()
637
645
  else:
638
646
  # ── Termux+Ubuntu: VNC 失败 → 直接报错,不降级 headless ──
639
647
  if _is_termux_env:
@@ -657,6 +665,10 @@ class StealthBrowser:
657
665
  if display:
658
666
  self._vnc_used = display.get("vnc", False)
659
667
  self._xvfb_started_by_us = display.get("xvfb_standalone", False)
668
+ if self._vnc_used:
669
+ # [v1.47.16] VNC 模式下直接用 Firefox
670
+ logger.info("VNC 模式 (env_detect不可用): 直接启动 Firefox")
671
+ return self._start_firefox_in_vnc()
660
672
  else:
661
673
  if _is_termux_env:
662
674
  return SkillResult(
@@ -681,7 +693,9 @@ class StealthBrowser:
681
693
  self._vnc_used = display.get("vnc", False)
682
694
  self._xvfb_started_by_us = display.get("xvfb_standalone", False)
683
695
  if self._vnc_used:
684
- logger.info(f"Termux+Ubuntu: 已通过 VNC 获取显示 ({display['display']})")
696
+ # [v1.47.16] VNC 模式下直接用 Firefox
697
+ logger.info(f"Termux+Ubuntu VNC 模式: 直接启动 Firefox")
698
+ return self._start_firefox_in_vnc()
685
699
  else:
686
700
  return SkillResult(
687
701
  success=False,
@@ -818,9 +832,179 @@ class StealthBrowser:
818
832
  error=f"启动反检测浏览器失败: {e}",
819
833
  )
820
834
 
835
+ def _start_firefox_in_vnc(self) -> SkillResult:
836
+ """[v1.47.16] VNC 模式下直接启动 Firefox。
837
+
838
+ 在 VNC/Termux 环境下,Chromium 与 DrissionPage 不兼容
839
+ (proot ARM64 下报 "browser executable file path cannot be found"),
840
+ 直接用 Firefox 在 VNC 中运行。
841
+
842
+ Firefox 模式下:
843
+ - navigate: 通过 subprocess 打开 URL(Firefox 支持远程打开 URL)
844
+ - screenshot: 通过 xdotool + import (ImageMagick) 截图
845
+ - click/fill: 通过 xdotool 发送鼠标/键盘事件
846
+ - cookie: 读取 Firefox profile 目录中的 cookies.sqlite
847
+ """
848
+ self._firefox_mode = True
849
+ display = os.environ.get("DISPLAY", ":99")
850
+
851
+ # 1. 检测 Firefox 路径
852
+ firefox_path = None
853
+ for candidate in ("firefox", "myagent-browser"):
854
+ found = shutil.which(candidate)
855
+ if found:
856
+ # 跳过 snap 包装器
857
+ if self._is_snap_wrapper(found):
858
+ logger.info(f"跳过 {candidate} ({found}) — snap 包装器,proot 下不可用")
859
+ continue
860
+ firefox_path = found
861
+ break
862
+
863
+ if not firefox_path:
864
+ # 搜索常见路径
865
+ for p in ("/usr/bin/firefox", "/usr/local/bin/firefox",
866
+ "/snap/bin/firefox"):
867
+ if os.path.isfile(p) and os.access(p, os.X_OK):
868
+ if self._is_snap_wrapper(p):
869
+ continue
870
+ firefox_path = p
871
+ break
872
+
873
+ if not firefox_path:
874
+ return SkillResult(
875
+ success=False,
876
+ error="VNC 模式下未找到 Firefox 浏览器。请安装 Firefox: apt install firefox",
877
+ )
878
+
879
+ logger.info(f"[_start_firefox_in_vnc] 使用 Firefox: {firefox_path}")
880
+
881
+ # 2. 准备 Firefox Profile 目录
882
+ if self._custom_user_data_dir:
883
+ self._firefox_profile_dir = self._custom_user_data_dir
884
+ else:
885
+ from core.browser_profile import get_browser_profile_manager
886
+ mgr = get_browser_profile_manager()
887
+ profile = mgr.get_profile(self.profile_name)
888
+ profile.ensure_dirs()
889
+ self._firefox_profile_dir = str(profile.profile_dir)
890
+
891
+ # 确保目录存在
892
+ try:
893
+ os.makedirs(self._firefox_profile_dir, exist_ok=True)
894
+ except Exception as e:
895
+ logger.warning(f"创建 Firefox Profile 目录失败: {e}")
896
+
897
+ # 3. 检查是否已有 Firefox 运行
898
+ try:
899
+ result = subprocess.run(
900
+ ["pgrep", "-f", "firefox"],
901
+ capture_output=True, text=True, timeout=5,
902
+ )
903
+ if result.returncode == 0 and result.stdout.strip():
904
+ logger.info(f"检测到已有 Firefox 运行 (PID: {result.stdout.strip().split()[0]}),复用")
905
+ self._started = True
906
+ return SkillResult(
907
+ success=True,
908
+ message=f"Firefox 已在 VNC 中运行 (Profile: {self.profile_name})",
909
+ data={"profile": self.profile_name, "mode": "firefox_vnc", "reused": True},
910
+ )
911
+ except Exception:
912
+ pass
913
+
914
+ # 4. 启动 Firefox
915
+ env = {**os.environ, "DISPLAY": display}
916
+ # proot 兼容环境变量
917
+ if not env.get("G_SLICE"):
918
+ env["G_SLICE"] = "always-malloc"
919
+ if not env.get("GSETTINGS_BACKEND"):
920
+ env["GSETTINGS_BACKEND"] = "memory"
921
+ if os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
922
+ env["DBUS_SESSION_BUS_ADDRESS"] = os.environ["DBUS_SESSION_BUS_ADDRESS"]
923
+ xdg_runtime = env.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"
924
+ env["XDG_RUNTIME_DIR"] = xdg_runtime
925
+ try:
926
+ os.makedirs(xdg_runtime, exist_ok=True)
927
+ os.chmod(xdg_runtime, 0o700)
928
+ except Exception:
929
+ pass
930
+
931
+ try:
932
+ firefox_cmd = [
933
+ firefox_path,
934
+ "--profile", self._firefox_profile_dir,
935
+ "--width", "960",
936
+ "--height", "540",
937
+ "--no-remote",
938
+ ]
939
+ logger.info(f"[_start_firefox_in_vnc] 启动: {' '.join(firefox_cmd)}")
940
+ self._firefox_process = subprocess.Popen(
941
+ firefox_cmd,
942
+ stdin=subprocess.DEVNULL,
943
+ stdout=subprocess.DEVNULL,
944
+ stderr=subprocess.DEVNULL,
945
+ env=env,
946
+ preexec_fn=os.setpgrp,
947
+ )
948
+ # 等待 Firefox 启动
949
+ time.sleep(3)
950
+ if self._firefox_process.poll() is not None:
951
+ exit_code = self._firefox_process.returncode
952
+ self._firefox_process = None
953
+ logger.error(f"Firefox 启动后立即退出 (exit code: {exit_code})")
954
+ return SkillResult(
955
+ success=False,
956
+ error=f"Firefox 启动后立即退出 (exit code: {exit_code})",
957
+ )
958
+
959
+ self._started = True
960
+ logger.info(
961
+ f"Firefox 已在 VNC 中启动 (PID={self._firefox_process.pid}, "
962
+ f"profile={self.profile_name})"
963
+ )
964
+ return SkillResult(
965
+ success=True,
966
+ message=f"Firefox 已在 VNC 中启动 (Profile: {self.profile_name})",
967
+ data={"profile": self.profile_name, "mode": "firefox_vnc", "pid": self._firefox_process.pid},
968
+ )
969
+ except Exception as e:
970
+ logger.error(f"Firefox 启动失败: {e}")
971
+ return SkillResult(
972
+ success=False,
973
+ error=f"Firefox 启动失败: {e}",
974
+ )
975
+
821
976
  async def close(self) -> SkillResult:
822
977
  """关闭浏览器"""
823
978
  self._started = False
979
+
980
+ # [v1.47.16] Firefox+VNC 模式
981
+ if self._firefox_mode:
982
+ try:
983
+ if self._firefox_process and self._firefox_process.poll() is None:
984
+ self._firefox_process.terminate()
985
+ try:
986
+ self._firefox_process.wait(timeout=5)
987
+ except Exception:
988
+ try:
989
+ self._firefox_process.kill()
990
+ except Exception:
991
+ pass
992
+ logger.info("Firefox 已关闭")
993
+ else:
994
+ # 可能是复用的 Firefox 进程,尝试通过 pkill 关闭
995
+ try:
996
+ subprocess.run(["pkill", "-f", "firefox"], capture_output=True, timeout=5)
997
+ logger.info("Firefox 进程已终止 (pkill)")
998
+ except Exception:
999
+ pass
1000
+ except Exception as e:
1001
+ logger.error(f"关闭 Firefox 异常: {e}")
1002
+ finally:
1003
+ self._firefox_process = None
1004
+ self._firefox_mode = False
1005
+ self._vnc_used = False
1006
+ return SkillResult(success=True, message="Firefox 已关闭")
1007
+
824
1008
  try:
825
1009
  if self._browser:
826
1010
  self._browser.quit()
@@ -855,6 +1039,10 @@ class StealthBrowser:
855
1039
  if not self._ensure_page():
856
1040
  return SkillResult(success=False, error="浏览器未启动")
857
1041
 
1042
+ # [v1.47.16] Firefox+VNC 模式:通过 subprocess 打开 URL
1043
+ if self._firefox_mode:
1044
+ return self._firefox_navigate(url, wait)
1045
+
858
1046
  try:
859
1047
  self._page.get(url)
860
1048
  if wait > 0:
@@ -960,6 +1148,10 @@ class StealthBrowser:
960
1148
  if not self._ensure_page():
961
1149
  return SkillResult(success=False, error="浏览器未启动")
962
1150
 
1151
+ # [v1.47.16] Firefox+VNC 模式:通过 xdotool 点击
1152
+ if self._firefox_mode:
1153
+ return self._firefox_click(selector, wait)
1154
+
963
1155
  try:
964
1156
  ele = self._find_element(selector, timeout=10)
965
1157
  if not ele:
@@ -1034,6 +1226,10 @@ class StealthBrowser:
1034
1226
  if not self._ensure_page():
1035
1227
  return SkillResult(success=False, error="浏览器未启动")
1036
1228
 
1229
+ # [v1.47.16] Firefox+VNC 模式:通过 xdotool 输入
1230
+ if self._firefox_mode:
1231
+ return self._firefox_fill(selector, value, clear, wait)
1232
+
1037
1233
  try:
1038
1234
  ele = self._find_element(selector, timeout=10)
1039
1235
  if not ele:
@@ -1260,6 +1456,10 @@ class StealthBrowser:
1260
1456
  if not self._ensure_page():
1261
1457
  return SkillResult(success=False, error="浏览器未启动")
1262
1458
 
1459
+ # [v1.47.16] Firefox+VNC 模式:通过 ImageMagick import 截图
1460
+ if self._firefox_mode:
1461
+ return self._firefox_screenshot(save_path)
1462
+
1263
1463
  try:
1264
1464
  if not save_path:
1265
1465
  # 自动生成路径
@@ -1428,6 +1628,10 @@ class StealthBrowser:
1428
1628
  if not self._ensure_page():
1429
1629
  return SkillResult(success=False, error="浏览器未启动")
1430
1630
 
1631
+ # [v1.47.16] Firefox+VNC 模式:读取 cookies.sqlite
1632
+ if self._firefox_mode:
1633
+ return self._firefox_get_cookies()
1634
+
1431
1635
  try:
1432
1636
  # DrissionPage cookies() 返回 CookiesList(list 子类),每项是 dict
1433
1637
  cookies = self._page.cookies()
@@ -1514,6 +1718,10 @@ class StealthBrowser:
1514
1718
  if not self._ensure_page():
1515
1719
  return SkillResult(success=False, error="浏览器未启动")
1516
1720
 
1721
+ # [v1.47.16] Firefox+VNC 模式:删除 cookies.sqlite
1722
+ if self._firefox_mode:
1723
+ return self._firefox_clear_cookies()
1724
+
1517
1725
  try:
1518
1726
  # Bug Fix: DrissionPage 没有 page.clear_cookies() 方法
1519
1727
  # 正确方式是 page.set.cookies.clear() 或 page.clear_cache()
@@ -1553,8 +1761,23 @@ class StealthBrowser:
1553
1761
 
1554
1762
  # Bug Fix: 使用 asyncio.sleep 避免阻塞事件循环
1555
1763
  start = time.time()
1556
- last_url = self._page.url if self._page else ""
1557
1764
  poll_interval = 3
1765
+
1766
+ # [v1.47.16] Firefox+VNC 模式下简化等待逻辑
1767
+ if self._firefox_mode:
1768
+ while time.time() - start < timeout:
1769
+ await asyncio.sleep(poll_interval)
1770
+ # 检查 Firefox 是否仍在运行
1771
+ if not self._ensure_page():
1772
+ break
1773
+ elapsed = int(time.time() - start)
1774
+ return SkillResult(
1775
+ success=True,
1776
+ message=f"等待用户手动操作完成 ({reason}),耗时 {elapsed} 秒",
1777
+ data={"elapsed": elapsed},
1778
+ )
1779
+
1780
+ last_url = self._page.url if self._page else ""
1558
1781
  while time.time() - start < timeout:
1559
1782
  await asyncio.sleep(poll_interval)
1560
1783
  if self._page:
@@ -1579,6 +1802,24 @@ class StealthBrowser:
1579
1802
 
1580
1803
  def _ensure_page(self) -> bool:
1581
1804
  """确保浏览器和页面可用"""
1805
+ # [v1.47.16] Firefox+VNC 模式:检查 Firefox 进程是否存活
1806
+ if self._firefox_mode:
1807
+ if not self._started:
1808
+ return False
1809
+ if self._firefox_process and self._firefox_process.poll() is None:
1810
+ return True
1811
+ # 复用的 Firefox,检查进程是否存在
1812
+ try:
1813
+ result = subprocess.run(
1814
+ ["pgrep", "-f", "firefox"],
1815
+ capture_output=True, text=True, timeout=3,
1816
+ )
1817
+ if result.returncode == 0 and result.stdout.strip():
1818
+ return True
1819
+ except Exception:
1820
+ pass
1821
+ return False
1822
+
1582
1823
  if not self._started or not self._page:
1583
1824
  return False
1584
1825
  try:
@@ -1591,6 +1832,284 @@ class StealthBrowser:
1591
1832
  logger.debug(f"页面检查失败 (profile={self.profile_name}),浏览器可能已关闭")
1592
1833
  return False
1593
1834
 
1835
+ # ── [v1.47.16] Firefox+VNC 模式辅助方法 ──────────────────────────
1836
+
1837
+ def _firefox_navigate(self, url: str, wait: float = 2.0) -> SkillResult:
1838
+ """Firefox+VNC 模式下导航到指定 URL。
1839
+
1840
+ Firefox 支持远程打开 URL:firefox <url> 会在已运行的实例中打开新标签页。
1841
+ """
1842
+ display = os.environ.get("DISPLAY", ":99")
1843
+ env = {**os.environ, "DISPLAY": display}
1844
+ if not env.get("G_SLICE"):
1845
+ env["G_SLICE"] = "always-malloc"
1846
+
1847
+ try:
1848
+ firefox_path = shutil.which("firefox") or shutil.which("myagent-browser")
1849
+ if not firefox_path:
1850
+ return SkillResult(success=False, error="Firefox 未找到")
1851
+
1852
+ result = subprocess.run(
1853
+ [firefox_path, "--profile", self._firefox_profile_dir, url],
1854
+ capture_output=True, text=True, timeout=10,
1855
+ env=env, start_new_session=True,
1856
+ )
1857
+ logger.info(f"Firefox 打开 URL: {url}")
1858
+ if wait > 0:
1859
+ time.sleep(wait)
1860
+ return SkillResult(
1861
+ success=True,
1862
+ message=f"Firefox 已打开: {url}",
1863
+ data={"url": url},
1864
+ )
1865
+ except Exception as e:
1866
+ return SkillResult(success=False, error=f"Firefox 导航失败: {e}")
1867
+
1868
+ def _firefox_click(self, selector: str, wait: float = 1.0) -> SkillResult:
1869
+ """Firefox+VNC 模式下通过 xdotool 点击元素。
1870
+
1871
+ 限制:只能通过坐标点击,无法像 DrissionPage 精确匹配 CSS 选择器。
1872
+ 策略:先用 xdotool search 查找 Firefox 窗口,再尝试通过
1873
+ xdotool key 模拟 Tab+Enter(适用于链接/按钮)。
1874
+ """
1875
+ display = os.environ.get("DISPLAY", ":99")
1876
+ env = {**os.environ, "DISPLAY": display}
1877
+
1878
+ try:
1879
+ # 查找 Firefox 窗口
1880
+ result = subprocess.run(
1881
+ ["xdotool", "search", "--onlyvisible", "--class", "firefox"],
1882
+ capture_output=True, text=True, timeout=5,
1883
+ env=env, start_new_session=True,
1884
+ )
1885
+ if result.returncode != 0 or not result.stdout.strip():
1886
+ return SkillResult(
1887
+ success=False,
1888
+ error="Firefox+VNC: 未找到 Firefox 窗口,无法点击。"
1889
+ "请在 VNC 中确认 Firefox 已打开。",
1890
+ )
1891
+
1892
+ window_id = result.stdout.strip().split()[0]
1893
+ # 激活窗口并点击
1894
+ subprocess.run(
1895
+ ["xdotool", "windowactivate", "--sync", window_id],
1896
+ capture_output=True, timeout=5, env=env, start_new_session=True,
1897
+ )
1898
+ # 如果 selector 是文本,用 Ctrl+F 搜索后按 Enter
1899
+ # 否则直接在窗口中心点击
1900
+ subprocess.run(
1901
+ ["xdotool", "click", "--window", window_id, "1"],
1902
+ capture_output=True, timeout=5, env=env, start_new_session=True,
1903
+ )
1904
+ if wait > 0:
1905
+ time.sleep(wait)
1906
+
1907
+ return SkillResult(
1908
+ success=True,
1909
+ message=f"Firefox+VNC: 已在 Firefox 窗口点击 (selector={selector})。"
1910
+ f"VNC 模式下点击精度有限,建议手动操作。",
1911
+ )
1912
+ except Exception as e:
1913
+ return SkillResult(
1914
+ success=False,
1915
+ error=f"Firefox+VNC 点击失败: {e}。建议在 VNC 中手动操作。",
1916
+ )
1917
+
1918
+ def _firefox_fill(self, selector: str, value: str, clear: bool = True, wait: float = 0.5) -> SkillResult:
1919
+ """Firefox+VNC 模式下通过 xdotool 填写输入框。
1920
+
1921
+ 限制:无法精确定位输入框,需要用户先在 VNC 中点击目标输入框。
1922
+ """
1923
+ display = os.environ.get("DISPLAY", ":99")
1924
+ env = {**os.environ, "DISPLAY": display}
1925
+
1926
+ try:
1927
+ # 查找 Firefox 窗口
1928
+ result = subprocess.run(
1929
+ ["xdotool", "search", "--onlyvisible", "--class", "firefox"],
1930
+ capture_output=True, text=True, timeout=5,
1931
+ env=env, start_new_session=True,
1932
+ )
1933
+ if result.returncode != 0 or not result.stdout.strip():
1934
+ return SkillResult(
1935
+ success=False,
1936
+ error="Firefox+VNC: 未找到 Firefox 窗口。请在 VNC 中确认 Firefox 已打开。",
1937
+ )
1938
+
1939
+ window_id = result.stdout.strip().split()[0]
1940
+ subprocess.run(
1941
+ ["xdotool", "windowactivate", "--sync", window_id],
1942
+ capture_output=True, timeout=5, env=env, start_new_session=True,
1943
+ )
1944
+
1945
+ if clear:
1946
+ # Ctrl+A 全选,然后输入新内容替换
1947
+ subprocess.run(
1948
+ ["xdotool", "key", "--window", window_id, "ctrl+a"],
1949
+ capture_output=True, timeout=3, env=env, start_new_session=True,
1950
+ )
1951
+ time.sleep(0.2)
1952
+
1953
+ # 使用 xdotool type 输入文本
1954
+ subprocess.run(
1955
+ ["xdotool", "type", "--window", window_id, "--delay", "50", value],
1956
+ capture_output=True, timeout=15, env=env, start_new_session=True,
1957
+ )
1958
+
1959
+ if wait > 0:
1960
+ time.sleep(wait)
1961
+
1962
+ return SkillResult(
1963
+ success=True,
1964
+ message=f"Firefox+VNC: 已输入文本到 Firefox。"
1965
+ f"请确认输入框已选中(先在 VNC 中点击目标输入框)。",
1966
+ )
1967
+ except Exception as e:
1968
+ return SkillResult(
1969
+ success=False,
1970
+ error=f"Firefox+VNC 输入失败: {e}",
1971
+ )
1972
+
1973
+ def _firefox_screenshot(self, save_path: str = "") -> SkillResult:
1974
+ """Firefox+VNC 模式下通过 xdotool + import 截图。"""
1975
+ display = os.environ.get("DISPLAY", ":99")
1976
+ env = {**os.environ, "DISPLAY": display}
1977
+
1978
+ try:
1979
+ if not save_path:
1980
+ from core.browser_profile import get_browser_profile_manager
1981
+ save_dir = get_browser_profile_manager().base_dir / "screenshots"
1982
+ os.makedirs(save_dir, exist_ok=True)
1983
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1984
+ save_path = str(save_dir / f"firefox_{timestamp}.png")
1985
+
1986
+ # 方法1: 使用 ImageMagick import 截取整个屏幕
1987
+ import_cmd = shutil.which("import")
1988
+ if import_cmd:
1989
+ result = subprocess.run(
1990
+ [import_cmd, "-window", "root", "-display", display, save_path],
1991
+ capture_output=True, timeout=10,
1992
+ env=env, start_new_session=True,
1993
+ )
1994
+ if result.returncode == 0 and os.path.isfile(save_path):
1995
+ logger.info(f"Firefox 截图已保存: {save_path}")
1996
+ return SkillResult(
1997
+ success=True,
1998
+ message=f"Firefox+VNC 截图已保存",
1999
+ data={"path": save_path},
2000
+ )
2001
+
2002
+ # 方法2: 使用 xdotool + scrot
2003
+ scrot_cmd = shutil.which("scrot")
2004
+ if scrot_cmd:
2005
+ result = subprocess.run(
2006
+ [scrot_cmd, save_path],
2007
+ capture_output=True, timeout=10,
2008
+ env=env, start_new_session=True,
2009
+ )
2010
+ if result.returncode == 0 and os.path.isfile(save_path):
2011
+ logger.info(f"Firefox 截图已保存 (scrot): {save_path}")
2012
+ return SkillResult(
2013
+ success=True,
2014
+ message=f"Firefox+VNC 截图已保存 (scrot)",
2015
+ data={"path": save_path},
2016
+ )
2017
+
2018
+ # 方法3: 使用 xwd + convert
2019
+ xwd_cmd = shutil.which("xwd")
2020
+ convert_cmd = shutil.which("convert")
2021
+ if xwd_cmd and convert_cmd:
2022
+ xwd_tmp = save_path + ".xwd"
2023
+ result = subprocess.run(
2024
+ [xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
2025
+ capture_output=True, timeout=10,
2026
+ env=env, start_new_session=True,
2027
+ )
2028
+ if result.returncode == 0:
2029
+ subprocess.run(
2030
+ [convert_cmd, xwd_tmp, save_path],
2031
+ capture_output=True, timeout=10,
2032
+ env=env, start_new_session=True,
2033
+ )
2034
+ try:
2035
+ os.unlink(xwd_tmp)
2036
+ except Exception:
2037
+ pass
2038
+ if os.path.isfile(save_path):
2039
+ logger.info(f"Firefox 截图已保存 (xwd+convert): {save_path}")
2040
+ return SkillResult(
2041
+ success=True,
2042
+ message=f"Firefox+VNC 截图已保存",
2043
+ data={"path": save_path},
2044
+ )
2045
+
2046
+ return SkillResult(
2047
+ success=False,
2048
+ error="Firefox+VNC 截图失败: 没有可用的截图工具 "
2049
+ "(需要 ImageMagick import / scrot / xwd+convert)",
2050
+ )
2051
+ except Exception as e:
2052
+ return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
2053
+
2054
+ def _firefox_get_cookies(self) -> SkillResult:
2055
+ """Firefox+VNC 模式下读取 cookies.sqlite。"""
2056
+ try:
2057
+ import sqlite3
2058
+ cookies_db = os.path.join(self._firefox_profile_dir, "cookies.sqlite")
2059
+ if not os.path.isfile(cookies_db):
2060
+ return SkillResult(
2061
+ success=True,
2062
+ message="Firefox Profile 中没有 cookies.sqlite",
2063
+ data={"cookies": []},
2064
+ )
2065
+
2066
+ # Firefox 使用 WAL 模式,需要先 checkpoint 或复制文件再读取
2067
+ # 直接读取可能会锁冲突,先复制到临时文件
2068
+ import tempfile
2069
+ with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as tmp:
2070
+ tmp_path = tmp.name
2071
+ shutil.copy2(cookies_db, tmp_path)
2072
+
2073
+ try:
2074
+ conn = sqlite3.connect(tmp_path)
2075
+ cursor = conn.cursor()
2076
+ cursor.execute("SELECT name, value, host, path FROM moz_cookies")
2077
+ rows = cursor.fetchall()
2078
+ conn.close()
2079
+
2080
+ cookie_list = []
2081
+ for row in rows:
2082
+ cookie_list.append({
2083
+ "name": row[0],
2084
+ "value": row[1],
2085
+ "domain": row[2],
2086
+ "path": row[3],
2087
+ })
2088
+
2089
+ return SkillResult(
2090
+ success=True,
2091
+ message=f"读取到 {len(cookie_list)} 个 Cookie",
2092
+ data={"cookies": cookie_list},
2093
+ )
2094
+ finally:
2095
+ try:
2096
+ os.unlink(tmp_path)
2097
+ except Exception:
2098
+ pass
2099
+ except Exception as e:
2100
+ return SkillResult(success=False, error=f"Firefox 读取 Cookie 失败: {e}")
2101
+
2102
+ def _firefox_clear_cookies(self) -> SkillResult:
2103
+ """Firefox+VNC 模式下清除 cookies.sqlite。"""
2104
+ try:
2105
+ cookies_db = os.path.join(self._firefox_profile_dir, "cookies.sqlite")
2106
+ if os.path.isfile(cookies_db):
2107
+ os.remove(cookies_db)
2108
+ logger.info("Firefox cookies.sqlite 已删除")
2109
+ return SkillResult(success=True, message="Firefox Cookie 已清除")
2110
+ except Exception as e:
2111
+ return SkillResult(success=False, error=f"Firefox 清除 Cookie 失败: {e}")
2112
+
1594
2113
  def _log_chrome_cmdline(self) -> None:
1595
2114
  """[DEBUG] 获取 Chrome 实际进程的命令行,验证 --user-data-dir 是否正确。"""
1596
2115
  import re
@@ -1824,11 +2343,16 @@ class StealthBrowser:
1824
2343
  return False
1825
2344
 
1826
2345
  @staticmethod
1827
- def _detect_browser() -> Optional[str]:
2346
+ def _detect_browser(skip_puppeteer: bool = False) -> Optional[str]:
1828
2347
  """自动检测 Chromium/Chrome 浏览器路径
1829
2348
 
1830
2349
  [v1.47.10] 增加 snap 包装器检测:proot 下 snap 不可用,
1831
2350
  跳过 snap 包装脚本,避免浏览器启动后无法连接。
2351
+
2352
+ [v1.47.16] 增加 skip_puppeteer 参数:VNC/Termux 模式下
2353
+ Puppeteer Chrome 在 proot ARM64 下不可用(DrissionPage 报
2354
+ "The browser executable file path cannot be found"),
2355
+ 跳过以避免无效启动尝试。
1832
2356
  """
1833
2357
  # 1. 环境变量
1834
2358
  for key in ("CHROME_PATH", "BROWSER_PATH", "CHROMIUM_PATH"):
@@ -1892,17 +2416,22 @@ class StealthBrowser:
1892
2416
  return p
1893
2417
 
1894
2418
  # 4. Puppeteer / Playwright 缓存
1895
- for cache in (
1896
- os.path.join(home, ".cache", "puppeteer", "chrome"),
1897
- os.path.join(home, ".cache", "ms-playwright"),
1898
- ):
1899
- if os.path.isdir(cache):
1900
- for root, _, files in os.walk(cache):
1901
- for fname in files:
1902
- if fname in ("chrome", "chromium", "headless_shell"):
1903
- full = os.path.join(root, fname)
1904
- if os.access(full, os.X_OK):
1905
- return full
2419
+ # [v1.47.16] VNC/Termux 模式下跳过:Puppeteer Chrome 在 proot ARM64 下
2420
+ # DrissionPage 不兼容(报 "browser executable file path cannot be found"
2421
+ if not skip_puppeteer:
2422
+ for cache in (
2423
+ os.path.join(home, ".cache", "puppeteer", "chrome"),
2424
+ os.path.join(home, ".cache", "ms-playwright"),
2425
+ ):
2426
+ if os.path.isdir(cache):
2427
+ for root, _, files in os.walk(cache):
2428
+ for fname in files:
2429
+ if fname in ("chrome", "chromium", "headless_shell"):
2430
+ full = os.path.join(root, fname)
2431
+ if os.access(full, os.X_OK):
2432
+ return full
2433
+ else:
2434
+ logger.info("[_detect_browser] VNC/Termux 模式: 跳过 Puppeteer/Playwright 缓存浏览器 (proot ARM64 不可用)")
1906
2435
 
1907
2436
  return None
1908
2437
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.47.14",
3
+ "version": "1.47.16",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -1374,8 +1374,11 @@ async function sendMessage(opts) {
1374
1374
 
1375
1375
  // [v1.23.35] Create session if needed — 使用 UUID 防止 ID 重复/乱串
1376
1376
  let sessionId = state.activeSessionId;
1377
- // [v1.25.5] 如果 activeSessionId 为空,等待 loadSessions 完成确保会话绑定正确
1378
- if ((!sessionId || sessionId === '__new__') && typeof loadSessions === 'function' && state.activeAgent) {
1377
+ // [v1.47.14] 修复:新对话状态下不调用 loadSessions()!
1378
+ // loadSessions() 会通过 _pendingSessionRestore URL 参数 auto-select
1379
+ // 历史会话,导致用户在"新对话"页面发消息时被跳转到历史会话。
1380
+ // 只有 activeSessionId 完全为空(不是 '__new__')时才需要等待 loadSessions。
1381
+ if (!sessionId && sessionId !== '__new__' && typeof loadSessions === 'function' && state.activeAgent) {
1379
1382
  console.log('[sendMessage] activeSessionId is empty, waiting for loadSessions...');
1380
1383
  await loadSessions();
1381
1384
  sessionId = state.activeSessionId;