myagent-ai 1.47.15 → 1.47.17

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()
@@ -540,6 +544,73 @@ class StealthBrowser:
540
544
 
541
545
  async def start(self) -> SkillResult:
542
546
  """启动反检测浏览器"""
547
+ # [v1.47.16] VNC/Termux 模式下直接使用 Firefox,不走 DrissionPage/Chromium
548
+ # Chromium 在 proot ARM64 下与 DrissionPage 不兼容,
549
+ # 所以 VNC 模式下先检测环境,直接走 Firefox 路径,不需要 import DrissionPage
550
+ _is_vnc_mode = False
551
+ _is_termux_env = False
552
+ try:
553
+ from core.env_detect import is_termux
554
+ _is_termux_env = is_termux()
555
+ except ImportError:
556
+ pass
557
+ try:
558
+ from core.env_detect import is_desktop
559
+ if not is_desktop():
560
+ _is_vnc_mode = True
561
+ except ImportError:
562
+ if not _has_display():
563
+ _is_vnc_mode = True
564
+
565
+ # VNC 模式:跳过所有 Chrome/DrissionPage 逻辑,直接用 Firefox
566
+ if _is_vnc_mode:
567
+ logger.info("VNC/非桌面环境: 直接启动 Firefox(跳过 Chromium 检测)")
568
+ if not self._headless:
569
+ display = _ensure_display()
570
+ if display:
571
+ self._vnc_used = display.get("vnc", False)
572
+ self._xvfb_started_by_us = display.get("xvfb_standalone", False)
573
+ if self._vnc_used:
574
+ return self._start_firefox_in_vnc()
575
+ # VNC/Xvfb 不可用
576
+ if _is_termux_env:
577
+ return SkillResult(
578
+ success=False,
579
+ error=(
580
+ "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
581
+ "VNC 启动失败。请先启动 VNC 远程桌面后再使用浏览器功能。"
582
+ ),
583
+ )
584
+ # 非 Termux:尝试 Xvfb
585
+ if display and display.get("xvfb_standalone"):
586
+ # 有 Xvfb 但没有 VNC → 用 Xvfb + Chromium(走下面的正常流程)
587
+ pass
588
+ else:
589
+ # 没有 Xvfb 也没有 VNC → 降级 headless 或报错
590
+ logger.warning("无显示环境且 VNC/Xvfb 均不可用,尝试 Firefox headless 模式")
591
+ return self._start_firefox_in_vnc()
592
+ else:
593
+ # headless=True 被显式请求
594
+ if _is_termux_env:
595
+ logger.info("Termux+Ubuntu 环境: 忽略 headless 请求,强制使用 VNC 模式")
596
+ self._headless = False
597
+ display = _ensure_display()
598
+ if display:
599
+ self._vnc_used = display.get("vnc", False)
600
+ self._xvfb_started_by_us = display.get("xvfb_standalone", False)
601
+ if self._vnc_used:
602
+ return self._start_firefox_in_vnc()
603
+ return SkillResult(
604
+ success=False,
605
+ error=(
606
+ "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
607
+ "VNC 启动失败。headless 模式在此环境下不可用。"
608
+ ),
609
+ )
610
+ # 非 Termux headless → 仍然尝试 Firefox
611
+ return self._start_firefox_in_vnc()
612
+
613
+ # ── 桌面环境:使用 DrissionPage + Chromium ──
543
614
  try:
544
615
  from DrissionPage import Chromium, ChromiumOptions
545
616
  except ImportError:
@@ -609,88 +680,10 @@ class StealthBrowser:
609
680
  else:
610
681
  logger.info("桌面环境,使用系统 Chrome 原生参数")
611
682
 
612
- # ── 无显示环境处理 ──
613
- # 桌面环境 (Windows/macOS/Linux有真实显示器): 直接用系统 Chrome
614
- # Termux+Ubuntu: 仅支持 VNC,不降级到 headless
615
- # 非 Termux 容器: VNC > Xvfb > headless 降级
616
- _is_termux_env = False
617
- try:
618
- from core.env_detect import is_termux
619
- _is_termux_env = is_termux()
620
- except ImportError:
621
- pass
622
-
683
+ # ── 显示环境处理(仅桌面环境到达此代码路径)──
684
+ # VNC/Termux 模式已在方法开头走 Firefox 分支,这里只处理桌面环境
623
685
  if not self._headless:
624
- try:
625
- from core.env_detect import is_desktop
626
- if is_desktop():
627
- # 桌面环境: 直接用系统 Chrome
628
- logger.info("桌面环境,直接使用系统浏览器,跳过 VNC/Xvfb")
629
- else:
630
- # 非桌面环境 (容器/Termux): 通过 _ensure_display() 获取显示
631
- display = _ensure_display()
632
- if display:
633
- self._vnc_used = display.get("vnc", False)
634
- self._xvfb_started_by_us = display.get("xvfb_standalone", False)
635
- if self._vnc_used:
636
- logger.info(f"复用 VNC 远程桌面显示 ({display['display']}),可在 VNC 中查看浏览器操作")
637
- else:
638
- # ── Termux+Ubuntu: VNC 失败 → 直接报错,不降级 headless ──
639
- if _is_termux_env:
640
- return SkillResult(
641
- success=False,
642
- error=(
643
- "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
644
- "VNC 启动失败。请先启动 VNC 远程桌面,"
645
- "或通过 Web 管理面板打开 VNC 后再使用浏览器功能。"
646
- ),
647
- )
648
- # ── 非 Termux 容器: 降级到 headless ──
649
- self._headless = True
650
- logger.warning(
651
- "无显示环境且 VNC/Xvfb 均不可用,自动降级为 headless 模式"
652
- )
653
- except ImportError:
654
- # env_detect 不可用时,降级为原有 X11 检测逻辑
655
- if not _has_display():
656
- display = _ensure_display()
657
- if display:
658
- self._vnc_used = display.get("vnc", False)
659
- self._xvfb_started_by_us = display.get("xvfb_standalone", False)
660
- else:
661
- if _is_termux_env:
662
- return SkillResult(
663
- success=False,
664
- error=(
665
- "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
666
- "VNC 启动失败。请先启动 VNC 远程桌面后再使用浏览器功能。"
667
- ),
668
- )
669
- self._headless = True
670
- logger.warning(
671
- "无 DISPLAY 环境且 VNC/Xvfb 均不可用,自动降级为 headless 模式"
672
- )
673
- else:
674
- # headless=True 被显式请求,但 Termux 环境下仍强制使用 VNC
675
- # 因为 headless Chromium 在 Termux 下容易被 OOM Kill
676
- if _is_termux_env:
677
- logger.info("Termux+Ubuntu 环境: 忽略 headless 请求,强制使用 VNC 模式")
678
- self._headless = False
679
- display = _ensure_display()
680
- if display:
681
- self._vnc_used = display.get("vnc", False)
682
- self._xvfb_started_by_us = display.get("xvfb_standalone", False)
683
- if self._vnc_used:
684
- logger.info(f"Termux+Ubuntu: 已通过 VNC 获取显示 ({display['display']})")
685
- else:
686
- return SkillResult(
687
- success=False,
688
- error=(
689
- "Termux+Ubuntu 环境仅支持通过 VNC 启动浏览器,"
690
- "VNC 启动失败。headless 模式在此环境下不可用(容易被 OOM Kill)。"
691
- "请先启动 VNC 远程桌面后再使用浏览器功能。"
692
- ),
693
- )
686
+ logger.info("桌面环境,直接使用系统浏览器,跳过 VNC/Xvfb")
694
687
 
695
688
  # 无头模式(co.headless() 内部设置 --headless=new)
696
689
  if self._headless:
@@ -818,9 +811,179 @@ class StealthBrowser:
818
811
  error=f"启动反检测浏览器失败: {e}",
819
812
  )
820
813
 
814
+ def _start_firefox_in_vnc(self) -> SkillResult:
815
+ """[v1.47.16] VNC 模式下直接启动 Firefox。
816
+
817
+ 在 VNC/Termux 环境下,Chromium 与 DrissionPage 不兼容
818
+ (proot ARM64 下报 "browser executable file path cannot be found"),
819
+ 直接用 Firefox 在 VNC 中运行。
820
+
821
+ Firefox 模式下:
822
+ - navigate: 通过 subprocess 打开 URL(Firefox 支持远程打开 URL)
823
+ - screenshot: 通过 xdotool + import (ImageMagick) 截图
824
+ - click/fill: 通过 xdotool 发送鼠标/键盘事件
825
+ - cookie: 读取 Firefox profile 目录中的 cookies.sqlite
826
+ """
827
+ self._firefox_mode = True
828
+ display = os.environ.get("DISPLAY", ":99")
829
+
830
+ # 1. 检测 Firefox 路径
831
+ firefox_path = None
832
+ for candidate in ("firefox", "myagent-browser"):
833
+ found = shutil.which(candidate)
834
+ if found:
835
+ # 跳过 snap 包装器
836
+ if self._is_snap_wrapper(found):
837
+ logger.info(f"跳过 {candidate} ({found}) — snap 包装器,proot 下不可用")
838
+ continue
839
+ firefox_path = found
840
+ break
841
+
842
+ if not firefox_path:
843
+ # 搜索常见路径
844
+ for p in ("/usr/bin/firefox", "/usr/local/bin/firefox",
845
+ "/snap/bin/firefox"):
846
+ if os.path.isfile(p) and os.access(p, os.X_OK):
847
+ if self._is_snap_wrapper(p):
848
+ continue
849
+ firefox_path = p
850
+ break
851
+
852
+ if not firefox_path:
853
+ return SkillResult(
854
+ success=False,
855
+ error="VNC 模式下未找到 Firefox 浏览器。请安装 Firefox: apt install firefox",
856
+ )
857
+
858
+ logger.info(f"[_start_firefox_in_vnc] 使用 Firefox: {firefox_path}")
859
+
860
+ # 2. 准备 Firefox Profile 目录
861
+ if self._custom_user_data_dir:
862
+ self._firefox_profile_dir = self._custom_user_data_dir
863
+ else:
864
+ from core.browser_profile import get_browser_profile_manager
865
+ mgr = get_browser_profile_manager()
866
+ profile = mgr.get_profile(self.profile_name)
867
+ profile.ensure_dirs()
868
+ self._firefox_profile_dir = str(profile.profile_dir)
869
+
870
+ # 确保目录存在
871
+ try:
872
+ os.makedirs(self._firefox_profile_dir, exist_ok=True)
873
+ except Exception as e:
874
+ logger.warning(f"创建 Firefox Profile 目录失败: {e}")
875
+
876
+ # 3. 检查是否已有 Firefox 运行
877
+ try:
878
+ result = subprocess.run(
879
+ ["pgrep", "-f", "firefox"],
880
+ capture_output=True, text=True, timeout=5,
881
+ )
882
+ if result.returncode == 0 and result.stdout.strip():
883
+ logger.info(f"检测到已有 Firefox 运行 (PID: {result.stdout.strip().split()[0]}),复用")
884
+ self._started = True
885
+ return SkillResult(
886
+ success=True,
887
+ message=f"Firefox 已在 VNC 中运行 (Profile: {self.profile_name})",
888
+ data={"profile": self.profile_name, "mode": "firefox_vnc", "reused": True},
889
+ )
890
+ except Exception:
891
+ pass
892
+
893
+ # 4. 启动 Firefox
894
+ env = {**os.environ, "DISPLAY": display}
895
+ # proot 兼容环境变量
896
+ if not env.get("G_SLICE"):
897
+ env["G_SLICE"] = "always-malloc"
898
+ if not env.get("GSETTINGS_BACKEND"):
899
+ env["GSETTINGS_BACKEND"] = "memory"
900
+ if os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
901
+ env["DBUS_SESSION_BUS_ADDRESS"] = os.environ["DBUS_SESSION_BUS_ADDRESS"]
902
+ xdg_runtime = env.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"
903
+ env["XDG_RUNTIME_DIR"] = xdg_runtime
904
+ try:
905
+ os.makedirs(xdg_runtime, exist_ok=True)
906
+ os.chmod(xdg_runtime, 0o700)
907
+ except Exception:
908
+ pass
909
+
910
+ try:
911
+ firefox_cmd = [
912
+ firefox_path,
913
+ "--profile", self._firefox_profile_dir,
914
+ "--width", "960",
915
+ "--height", "540",
916
+ "--no-remote",
917
+ ]
918
+ logger.info(f"[_start_firefox_in_vnc] 启动: {' '.join(firefox_cmd)}")
919
+ self._firefox_process = subprocess.Popen(
920
+ firefox_cmd,
921
+ stdin=subprocess.DEVNULL,
922
+ stdout=subprocess.DEVNULL,
923
+ stderr=subprocess.DEVNULL,
924
+ env=env,
925
+ preexec_fn=os.setpgrp,
926
+ )
927
+ # 等待 Firefox 启动
928
+ time.sleep(3)
929
+ if self._firefox_process.poll() is not None:
930
+ exit_code = self._firefox_process.returncode
931
+ self._firefox_process = None
932
+ logger.error(f"Firefox 启动后立即退出 (exit code: {exit_code})")
933
+ return SkillResult(
934
+ success=False,
935
+ error=f"Firefox 启动后立即退出 (exit code: {exit_code})",
936
+ )
937
+
938
+ self._started = True
939
+ logger.info(
940
+ f"Firefox 已在 VNC 中启动 (PID={self._firefox_process.pid}, "
941
+ f"profile={self.profile_name})"
942
+ )
943
+ return SkillResult(
944
+ success=True,
945
+ message=f"Firefox 已在 VNC 中启动 (Profile: {self.profile_name})",
946
+ data={"profile": self.profile_name, "mode": "firefox_vnc", "pid": self._firefox_process.pid},
947
+ )
948
+ except Exception as e:
949
+ logger.error(f"Firefox 启动失败: {e}")
950
+ return SkillResult(
951
+ success=False,
952
+ error=f"Firefox 启动失败: {e}",
953
+ )
954
+
821
955
  async def close(self) -> SkillResult:
822
956
  """关闭浏览器"""
823
957
  self._started = False
958
+
959
+ # [v1.47.16] Firefox+VNC 模式
960
+ if self._firefox_mode:
961
+ try:
962
+ if self._firefox_process and self._firefox_process.poll() is None:
963
+ self._firefox_process.terminate()
964
+ try:
965
+ self._firefox_process.wait(timeout=5)
966
+ except Exception:
967
+ try:
968
+ self._firefox_process.kill()
969
+ except Exception:
970
+ pass
971
+ logger.info("Firefox 已关闭")
972
+ else:
973
+ # 可能是复用的 Firefox 进程,尝试通过 pkill 关闭
974
+ try:
975
+ subprocess.run(["pkill", "-f", "firefox"], capture_output=True, timeout=5)
976
+ logger.info("Firefox 进程已终止 (pkill)")
977
+ except Exception:
978
+ pass
979
+ except Exception as e:
980
+ logger.error(f"关闭 Firefox 异常: {e}")
981
+ finally:
982
+ self._firefox_process = None
983
+ self._firefox_mode = False
984
+ self._vnc_used = False
985
+ return SkillResult(success=True, message="Firefox 已关闭")
986
+
824
987
  try:
825
988
  if self._browser:
826
989
  self._browser.quit()
@@ -855,6 +1018,10 @@ class StealthBrowser:
855
1018
  if not self._ensure_page():
856
1019
  return SkillResult(success=False, error="浏览器未启动")
857
1020
 
1021
+ # [v1.47.16] Firefox+VNC 模式:通过 subprocess 打开 URL
1022
+ if self._firefox_mode:
1023
+ return self._firefox_navigate(url, wait)
1024
+
858
1025
  try:
859
1026
  self._page.get(url)
860
1027
  if wait > 0:
@@ -960,6 +1127,10 @@ class StealthBrowser:
960
1127
  if not self._ensure_page():
961
1128
  return SkillResult(success=False, error="浏览器未启动")
962
1129
 
1130
+ # [v1.47.16] Firefox+VNC 模式:通过 xdotool 点击
1131
+ if self._firefox_mode:
1132
+ return self._firefox_click(selector, wait)
1133
+
963
1134
  try:
964
1135
  ele = self._find_element(selector, timeout=10)
965
1136
  if not ele:
@@ -1034,6 +1205,10 @@ class StealthBrowser:
1034
1205
  if not self._ensure_page():
1035
1206
  return SkillResult(success=False, error="浏览器未启动")
1036
1207
 
1208
+ # [v1.47.16] Firefox+VNC 模式:通过 xdotool 输入
1209
+ if self._firefox_mode:
1210
+ return self._firefox_fill(selector, value, clear, wait)
1211
+
1037
1212
  try:
1038
1213
  ele = self._find_element(selector, timeout=10)
1039
1214
  if not ele:
@@ -1077,8 +1252,11 @@ class StealthBrowser:
1077
1252
  if not self._ensure_page():
1078
1253
  return SkillResult(success=False, error="浏览器未启动")
1079
1254
 
1255
+ # [v1.47.16] Firefox+VNC 模式:通过 xdotool type 输入
1256
+ if self._firefox_mode:
1257
+ return self._firefox_fill(selector or "", text, clear, wait)
1258
+
1080
1259
  try:
1081
- # 如果提供了 selector,先点击聚焦
1082
1260
  if selector:
1083
1261
  ele = self._find_element(selector, timeout=10)
1084
1262
  if not ele:
@@ -1170,8 +1348,11 @@ class StealthBrowser:
1170
1348
  if not self._ensure_page():
1171
1349
  return SkillResult(success=False, error="浏览器未启动")
1172
1350
 
1351
+ # [v1.47.16] Firefox+VNC 模式:通过 xdotool key 按键
1352
+ if self._firefox_mode:
1353
+ return self._firefox_press_key(key, selector, wait)
1354
+
1173
1355
  try:
1174
- # 如果提供了 selector,先聚焦
1175
1356
  if selector:
1176
1357
  ele = self._find_element(selector, timeout=10)
1177
1358
  if ele:
@@ -1260,6 +1441,10 @@ class StealthBrowser:
1260
1441
  if not self._ensure_page():
1261
1442
  return SkillResult(success=False, error="浏览器未启动")
1262
1443
 
1444
+ # [v1.47.16] Firefox+VNC 模式:通过 ImageMagick import 截图
1445
+ if self._firefox_mode:
1446
+ return self._firefox_screenshot(save_path)
1447
+
1263
1448
  try:
1264
1449
  if not save_path:
1265
1450
  # 自动生成路径
@@ -1290,6 +1475,13 @@ class StealthBrowser:
1290
1475
  if not self._ensure_page():
1291
1476
  return SkillResult(success=False, error="浏览器未启动")
1292
1477
 
1478
+ # [v1.47.16] Firefox+VNC 模式:无法通过 CDP 执行 JS
1479
+ if self._firefox_mode:
1480
+ return SkillResult(
1481
+ success=False,
1482
+ error="Firefox+VNC 模式下不支持 JS 执行。请在 VNC 中手动操作,或切换到桌面环境使用 Chromium。",
1483
+ )
1484
+
1293
1485
  try:
1294
1486
  _script = script.strip()
1295
1487
  # 检测是否包含 return 语句
@@ -1346,6 +1538,13 @@ class StealthBrowser:
1346
1538
  if not self._ensure_page():
1347
1539
  return SkillResult(success=False, error="浏览器未启动")
1348
1540
 
1541
+ # [v1.47.16] Firefox+VNC 模式:无法获取页面内容
1542
+ if self._firefox_mode:
1543
+ return SkillResult(
1544
+ success=False,
1545
+ error="Firefox+VNC 模式下不支持获取页面内容。请在 VNC 中手动查看,或切换到桌面环境使用 Chromium。",
1546
+ )
1547
+
1349
1548
  try:
1350
1549
  # Bug Fix: DrissionPage 没有 page.text 属性
1351
1550
  # 需要通过 page.ele('tag:html').text 获取页面文本
@@ -1384,6 +1583,13 @@ class StealthBrowser:
1384
1583
  if not self._ensure_page():
1385
1584
  return SkillResult(success=False, error="浏览器未启动")
1386
1585
 
1586
+ # [v1.47.16] Firefox+VNC 模式:无法获取页面 HTML
1587
+ if self._firefox_mode:
1588
+ return SkillResult(
1589
+ success=False,
1590
+ error="Firefox+VNC 模式下不支持获取页面 HTML。请在 VNC 中手动查看,或切换到桌面环境使用 Chromium。",
1591
+ )
1592
+
1387
1593
  try:
1388
1594
  html = self._page.html or ""
1389
1595
  output_html = html[:50000] if len(html) > 50000 else html
@@ -1408,6 +1614,13 @@ class StealthBrowser:
1408
1614
  if not self._ensure_page():
1409
1615
  return SkillResult(success=False, error="浏览器未启动")
1410
1616
 
1617
+ # [v1.47.16] Firefox+VNC 模式:无法等待元素
1618
+ if self._firefox_mode:
1619
+ return SkillResult(
1620
+ success=False,
1621
+ error="Firefox+VNC 模式下不支持等待元素。请在 VNC 中手动操作。",
1622
+ )
1623
+
1411
1624
  try:
1412
1625
  ele = self._find_element(selector, timeout=timeout)
1413
1626
  if ele:
@@ -1428,6 +1641,10 @@ class StealthBrowser:
1428
1641
  if not self._ensure_page():
1429
1642
  return SkillResult(success=False, error="浏览器未启动")
1430
1643
 
1644
+ # [v1.47.16] Firefox+VNC 模式:读取 cookies.sqlite
1645
+ if self._firefox_mode:
1646
+ return self._firefox_get_cookies()
1647
+
1431
1648
  try:
1432
1649
  # DrissionPage cookies() 返回 CookiesList(list 子类),每项是 dict
1433
1650
  cookies = self._page.cookies()
@@ -1458,6 +1675,13 @@ class StealthBrowser:
1458
1675
  if not self._ensure_page():
1459
1676
  return SkillResult(success=False, error="浏览器未启动")
1460
1677
 
1678
+ # [v1.47.16] Firefox+VNC 模式:Cookie 已由 Firefox 自动保存到 profile 目录
1679
+ if self._firefox_mode:
1680
+ return SkillResult(
1681
+ success=True,
1682
+ message="Firefox+VNC 模式: Cookie 由 Firefox 自动保存到 profile 目录",
1683
+ )
1684
+
1461
1685
  try:
1462
1686
  cookies = self._page.cookies()
1463
1687
  cookie_list = []
@@ -1487,6 +1711,13 @@ class StealthBrowser:
1487
1711
  if not self._ensure_page():
1488
1712
  return SkillResult(success=False, error="浏览器未启动")
1489
1713
 
1714
+ # [v1.47.16] Firefox+VNC 模式:Cookie 由 Firefox 自动从 profile 目录加载
1715
+ if self._firefox_mode:
1716
+ return SkillResult(
1717
+ success=True,
1718
+ message="Firefox+VNC 模式: Cookie 由 Firefox 自动从 profile 目录加载",
1719
+ )
1720
+
1490
1721
  try:
1491
1722
  from core.browser_profile import get_browser_profile_manager
1492
1723
  mgr = get_browser_profile_manager()
@@ -1514,6 +1745,10 @@ class StealthBrowser:
1514
1745
  if not self._ensure_page():
1515
1746
  return SkillResult(success=False, error="浏览器未启动")
1516
1747
 
1748
+ # [v1.47.16] Firefox+VNC 模式:删除 cookies.sqlite
1749
+ if self._firefox_mode:
1750
+ return self._firefox_clear_cookies()
1751
+
1517
1752
  try:
1518
1753
  # Bug Fix: DrissionPage 没有 page.clear_cookies() 方法
1519
1754
  # 正确方式是 page.set.cookies.clear() 或 page.clear_cache()
@@ -1553,8 +1788,23 @@ class StealthBrowser:
1553
1788
 
1554
1789
  # Bug Fix: 使用 asyncio.sleep 避免阻塞事件循环
1555
1790
  start = time.time()
1556
- last_url = self._page.url if self._page else ""
1557
1791
  poll_interval = 3
1792
+
1793
+ # [v1.47.16] Firefox+VNC 模式下简化等待逻辑
1794
+ if self._firefox_mode:
1795
+ while time.time() - start < timeout:
1796
+ await asyncio.sleep(poll_interval)
1797
+ # 检查 Firefox 是否仍在运行
1798
+ if not self._ensure_page():
1799
+ break
1800
+ elapsed = int(time.time() - start)
1801
+ return SkillResult(
1802
+ success=True,
1803
+ message=f"等待用户手动操作完成 ({reason}),耗时 {elapsed} 秒",
1804
+ data={"elapsed": elapsed},
1805
+ )
1806
+
1807
+ last_url = self._page.url if self._page else ""
1558
1808
  while time.time() - start < timeout:
1559
1809
  await asyncio.sleep(poll_interval)
1560
1810
  if self._page:
@@ -1579,6 +1829,24 @@ class StealthBrowser:
1579
1829
 
1580
1830
  def _ensure_page(self) -> bool:
1581
1831
  """确保浏览器和页面可用"""
1832
+ # [v1.47.16] Firefox+VNC 模式:检查 Firefox 进程是否存活
1833
+ if self._firefox_mode:
1834
+ if not self._started:
1835
+ return False
1836
+ if self._firefox_process and self._firefox_process.poll() is None:
1837
+ return True
1838
+ # 复用的 Firefox,检查进程是否存在
1839
+ try:
1840
+ result = subprocess.run(
1841
+ ["pgrep", "-f", "firefox"],
1842
+ capture_output=True, text=True, timeout=3,
1843
+ )
1844
+ if result.returncode == 0 and result.stdout.strip():
1845
+ return True
1846
+ except Exception:
1847
+ pass
1848
+ return False
1849
+
1582
1850
  if not self._started or not self._page:
1583
1851
  return False
1584
1852
  try:
@@ -1591,6 +1859,352 @@ class StealthBrowser:
1591
1859
  logger.debug(f"页面检查失败 (profile={self.profile_name}),浏览器可能已关闭")
1592
1860
  return False
1593
1861
 
1862
+ # ── [v1.47.16] Firefox+VNC 模式辅助方法 ──────────────────────────
1863
+
1864
+ def _firefox_navigate(self, url: str, wait: float = 2.0) -> SkillResult:
1865
+ """Firefox+VNC 模式下导航到指定 URL。
1866
+
1867
+ Firefox 支持远程打开 URL:firefox <url> 会在已运行的实例中打开新标签页。
1868
+ """
1869
+ display = os.environ.get("DISPLAY", ":99")
1870
+ env = {**os.environ, "DISPLAY": display}
1871
+ if not env.get("G_SLICE"):
1872
+ env["G_SLICE"] = "always-malloc"
1873
+
1874
+ try:
1875
+ firefox_path = shutil.which("firefox") or shutil.which("myagent-browser")
1876
+ if not firefox_path:
1877
+ return SkillResult(success=False, error="Firefox 未找到")
1878
+
1879
+ result = subprocess.run(
1880
+ [firefox_path, "--profile", self._firefox_profile_dir, url],
1881
+ capture_output=True, text=True, timeout=10,
1882
+ env=env, start_new_session=True,
1883
+ )
1884
+ logger.info(f"Firefox 打开 URL: {url}")
1885
+ if wait > 0:
1886
+ time.sleep(wait)
1887
+ return SkillResult(
1888
+ success=True,
1889
+ message=f"Firefox 已打开: {url}",
1890
+ data={"url": url},
1891
+ )
1892
+ except Exception as e:
1893
+ return SkillResult(success=False, error=f"Firefox 导航失败: {e}")
1894
+
1895
+ def _firefox_click(self, selector: str, wait: float = 1.0) -> SkillResult:
1896
+ """Firefox+VNC 模式下通过 xdotool 点击元素。
1897
+
1898
+ 限制:只能通过坐标点击,无法像 DrissionPage 精确匹配 CSS 选择器。
1899
+ 策略:先用 xdotool search 查找 Firefox 窗口,再尝试通过
1900
+ xdotool key 模拟 Tab+Enter(适用于链接/按钮)。
1901
+ """
1902
+ display = os.environ.get("DISPLAY", ":99")
1903
+ env = {**os.environ, "DISPLAY": display}
1904
+
1905
+ try:
1906
+ # 查找 Firefox 窗口
1907
+ result = subprocess.run(
1908
+ ["xdotool", "search", "--onlyvisible", "--class", "firefox"],
1909
+ capture_output=True, text=True, timeout=5,
1910
+ env=env, start_new_session=True,
1911
+ )
1912
+ if result.returncode != 0 or not result.stdout.strip():
1913
+ return SkillResult(
1914
+ success=False,
1915
+ error="Firefox+VNC: 未找到 Firefox 窗口,无法点击。"
1916
+ "请在 VNC 中确认 Firefox 已打开。",
1917
+ )
1918
+
1919
+ window_id = result.stdout.strip().split()[0]
1920
+ # 激活窗口并点击
1921
+ subprocess.run(
1922
+ ["xdotool", "windowactivate", "--sync", window_id],
1923
+ capture_output=True, timeout=5, env=env, start_new_session=True,
1924
+ )
1925
+ # 如果 selector 是文本,用 Ctrl+F 搜索后按 Enter
1926
+ # 否则直接在窗口中心点击
1927
+ subprocess.run(
1928
+ ["xdotool", "click", "--window", window_id, "1"],
1929
+ capture_output=True, timeout=5, env=env, start_new_session=True,
1930
+ )
1931
+ if wait > 0:
1932
+ time.sleep(wait)
1933
+
1934
+ return SkillResult(
1935
+ success=True,
1936
+ message=f"Firefox+VNC: 已在 Firefox 窗口点击 (selector={selector})。"
1937
+ f"VNC 模式下点击精度有限,建议手动操作。",
1938
+ )
1939
+ except Exception as e:
1940
+ return SkillResult(
1941
+ success=False,
1942
+ error=f"Firefox+VNC 点击失败: {e}。建议在 VNC 中手动操作。",
1943
+ )
1944
+
1945
+ def _firefox_fill(self, selector: str, value: str, clear: bool = True, wait: float = 0.5) -> SkillResult:
1946
+ """Firefox+VNC 模式下通过 xdotool 填写输入框。
1947
+
1948
+ 限制:无法精确定位输入框,需要用户先在 VNC 中点击目标输入框。
1949
+ """
1950
+ display = os.environ.get("DISPLAY", ":99")
1951
+ env = {**os.environ, "DISPLAY": display}
1952
+
1953
+ try:
1954
+ # 查找 Firefox 窗口
1955
+ result = subprocess.run(
1956
+ ["xdotool", "search", "--onlyvisible", "--class", "firefox"],
1957
+ capture_output=True, text=True, timeout=5,
1958
+ env=env, start_new_session=True,
1959
+ )
1960
+ if result.returncode != 0 or not result.stdout.strip():
1961
+ return SkillResult(
1962
+ success=False,
1963
+ error="Firefox+VNC: 未找到 Firefox 窗口。请在 VNC 中确认 Firefox 已打开。",
1964
+ )
1965
+
1966
+ window_id = result.stdout.strip().split()[0]
1967
+ subprocess.run(
1968
+ ["xdotool", "windowactivate", "--sync", window_id],
1969
+ capture_output=True, timeout=5, env=env, start_new_session=True,
1970
+ )
1971
+
1972
+ if clear:
1973
+ # Ctrl+A 全选,然后输入新内容替换
1974
+ subprocess.run(
1975
+ ["xdotool", "key", "--window", window_id, "ctrl+a"],
1976
+ capture_output=True, timeout=3, env=env, start_new_session=True,
1977
+ )
1978
+ time.sleep(0.2)
1979
+
1980
+ # 使用 xdotool type 输入文本
1981
+ subprocess.run(
1982
+ ["xdotool", "type", "--window", window_id, "--delay", "50", value],
1983
+ capture_output=True, timeout=15, env=env, start_new_session=True,
1984
+ )
1985
+
1986
+ if wait > 0:
1987
+ time.sleep(wait)
1988
+
1989
+ return SkillResult(
1990
+ success=True,
1991
+ message=f"Firefox+VNC: 已输入文本到 Firefox。"
1992
+ f"请确认输入框已选中(先在 VNC 中点击目标输入框)。",
1993
+ )
1994
+ except Exception as e:
1995
+ return SkillResult(
1996
+ success=False,
1997
+ error=f"Firefox+VNC 输入失败: {e}",
1998
+ )
1999
+
2000
+ def _firefox_press_key(self, key: str, selector: str = "", wait: float = 0.5) -> SkillResult:
2001
+ """[v1.47.16] Firefox+VNC 模式下通过 xdotool key 按键。"""
2002
+ display = os.environ.get("DISPLAY", ":99")
2003
+ env = {**os.environ, "DISPLAY": display}
2004
+
2005
+ try:
2006
+ # 查找 Firefox 窗口
2007
+ result = subprocess.run(
2008
+ ["xdotool", "search", "--onlyvisible", "--class", "firefox"],
2009
+ capture_output=True, text=True, timeout=5,
2010
+ env=env, start_new_session=True,
2011
+ )
2012
+ if result.returncode != 0 or not result.stdout.strip():
2013
+ return SkillResult(
2014
+ success=False,
2015
+ error="Firefox+VNC: 未找到 Firefox 窗口。请在 VNC 中确认 Firefox 已打开。",
2016
+ )
2017
+
2018
+ window_id = result.stdout.strip().split()[0]
2019
+ subprocess.run(
2020
+ ["xdotool", "windowactivate", "--sync", window_id],
2021
+ capture_output=True, timeout=5, env=env, start_new_session=True,
2022
+ )
2023
+
2024
+ # 将 JS 按键名称映射为 xdotool 按键名称
2025
+ xdotool_key_map = {
2026
+ 'enter': 'Return', 'tab': 'Tab', 'escape': 'Escape', 'esc': 'Escape',
2027
+ 'backspace': 'BackSpace', 'delete': 'Delete', 'del': 'Delete',
2028
+ 'arrowup': 'Up', 'arrowdown': 'Down', 'arrowleft': 'Left', 'arrowright': 'Right',
2029
+ 'up': 'Up', 'down': 'Down', 'left': 'Left', 'right': 'Right',
2030
+ 'home': 'Home', 'end': 'End',
2031
+ 'pageup': 'Page_Up', 'pagedown': 'Page_Down',
2032
+ 'space': 'space', ' ': 'space',
2033
+ 'ctrl': 'ctrl', 'shift': 'shift', 'alt': 'alt', 'meta': 'super',
2034
+ 'cmd': 'super', 'command': 'super',
2035
+ }
2036
+ # F1-F12
2037
+ for i in range(1, 13):
2038
+ xdotool_key_map[f'f{i}'] = f'F{i}'
2039
+
2040
+ # 解析组合键
2041
+ parts = key.strip().split('+')
2042
+ xdotool_keys = []
2043
+ for part in parts:
2044
+ p = part.strip()
2045
+ mapped = xdotool_key_map.get(p.lower(), p)
2046
+ xdotool_keys.append(mapped)
2047
+
2048
+ # 构建 xdotool key 参数
2049
+ xdotool_key_str = '+'.join(xdotool_keys)
2050
+ subprocess.run(
2051
+ ["xdotool", "key", "--window", window_id, xdotool_key_str],
2052
+ capture_output=True, timeout=5, env=env, start_new_session=True,
2053
+ )
2054
+
2055
+ if wait > 0:
2056
+ time.sleep(wait)
2057
+
2058
+ return SkillResult(
2059
+ success=True,
2060
+ message=f"Firefox+VNC: 已按键: {key}",
2061
+ )
2062
+ except Exception as e:
2063
+ return SkillResult(
2064
+ success=False,
2065
+ error=f"Firefox+VNC 按键失败: {e}",
2066
+ )
2067
+
2068
+ def _firefox_screenshot(self, save_path: str = "") -> SkillResult:
2069
+ """Firefox+VNC 模式下通过 xdotool + import 截图。"""
2070
+ display = os.environ.get("DISPLAY", ":99")
2071
+ env = {**os.environ, "DISPLAY": display}
2072
+
2073
+ try:
2074
+ if not save_path:
2075
+ from core.browser_profile import get_browser_profile_manager
2076
+ save_dir = get_browser_profile_manager().base_dir / "screenshots"
2077
+ os.makedirs(save_dir, exist_ok=True)
2078
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
2079
+ save_path = str(save_dir / f"firefox_{timestamp}.png")
2080
+
2081
+ # 方法1: 使用 ImageMagick import 截取整个屏幕
2082
+ import_cmd = shutil.which("import")
2083
+ if import_cmd:
2084
+ result = subprocess.run(
2085
+ [import_cmd, "-window", "root", "-display", display, save_path],
2086
+ capture_output=True, timeout=10,
2087
+ env=env, start_new_session=True,
2088
+ )
2089
+ if result.returncode == 0 and os.path.isfile(save_path):
2090
+ logger.info(f"Firefox 截图已保存: {save_path}")
2091
+ return SkillResult(
2092
+ success=True,
2093
+ message=f"Firefox+VNC 截图已保存",
2094
+ data={"path": save_path},
2095
+ )
2096
+
2097
+ # 方法2: 使用 xdotool + scrot
2098
+ scrot_cmd = shutil.which("scrot")
2099
+ if scrot_cmd:
2100
+ result = subprocess.run(
2101
+ [scrot_cmd, save_path],
2102
+ capture_output=True, timeout=10,
2103
+ env=env, start_new_session=True,
2104
+ )
2105
+ if result.returncode == 0 and os.path.isfile(save_path):
2106
+ logger.info(f"Firefox 截图已保存 (scrot): {save_path}")
2107
+ return SkillResult(
2108
+ success=True,
2109
+ message=f"Firefox+VNC 截图已保存 (scrot)",
2110
+ data={"path": save_path},
2111
+ )
2112
+
2113
+ # 方法3: 使用 xwd + convert
2114
+ xwd_cmd = shutil.which("xwd")
2115
+ convert_cmd = shutil.which("convert")
2116
+ if xwd_cmd and convert_cmd:
2117
+ xwd_tmp = save_path + ".xwd"
2118
+ result = subprocess.run(
2119
+ [xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
2120
+ capture_output=True, timeout=10,
2121
+ env=env, start_new_session=True,
2122
+ )
2123
+ if result.returncode == 0:
2124
+ subprocess.run(
2125
+ [convert_cmd, xwd_tmp, save_path],
2126
+ capture_output=True, timeout=10,
2127
+ env=env, start_new_session=True,
2128
+ )
2129
+ try:
2130
+ os.unlink(xwd_tmp)
2131
+ except Exception:
2132
+ pass
2133
+ if os.path.isfile(save_path):
2134
+ logger.info(f"Firefox 截图已保存 (xwd+convert): {save_path}")
2135
+ return SkillResult(
2136
+ success=True,
2137
+ message=f"Firefox+VNC 截图已保存",
2138
+ data={"path": save_path},
2139
+ )
2140
+
2141
+ return SkillResult(
2142
+ success=False,
2143
+ error="Firefox+VNC 截图失败: 没有可用的截图工具 "
2144
+ "(需要 ImageMagick import / scrot / xwd+convert)",
2145
+ )
2146
+ except Exception as e:
2147
+ return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
2148
+
2149
+ def _firefox_get_cookies(self) -> SkillResult:
2150
+ """Firefox+VNC 模式下读取 cookies.sqlite。"""
2151
+ try:
2152
+ import sqlite3
2153
+ cookies_db = os.path.join(self._firefox_profile_dir, "cookies.sqlite")
2154
+ if not os.path.isfile(cookies_db):
2155
+ return SkillResult(
2156
+ success=True,
2157
+ message="Firefox Profile 中没有 cookies.sqlite",
2158
+ data={"cookies": []},
2159
+ )
2160
+
2161
+ # Firefox 使用 WAL 模式,需要先 checkpoint 或复制文件再读取
2162
+ # 直接读取可能会锁冲突,先复制到临时文件
2163
+ import tempfile
2164
+ with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as tmp:
2165
+ tmp_path = tmp.name
2166
+ shutil.copy2(cookies_db, tmp_path)
2167
+
2168
+ try:
2169
+ conn = sqlite3.connect(tmp_path)
2170
+ cursor = conn.cursor()
2171
+ cursor.execute("SELECT name, value, host, path FROM moz_cookies")
2172
+ rows = cursor.fetchall()
2173
+ conn.close()
2174
+
2175
+ cookie_list = []
2176
+ for row in rows:
2177
+ cookie_list.append({
2178
+ "name": row[0],
2179
+ "value": row[1],
2180
+ "domain": row[2],
2181
+ "path": row[3],
2182
+ })
2183
+
2184
+ return SkillResult(
2185
+ success=True,
2186
+ message=f"读取到 {len(cookie_list)} 个 Cookie",
2187
+ data={"cookies": cookie_list},
2188
+ )
2189
+ finally:
2190
+ try:
2191
+ os.unlink(tmp_path)
2192
+ except Exception:
2193
+ pass
2194
+ except Exception as e:
2195
+ return SkillResult(success=False, error=f"Firefox 读取 Cookie 失败: {e}")
2196
+
2197
+ def _firefox_clear_cookies(self) -> SkillResult:
2198
+ """Firefox+VNC 模式下清除 cookies.sqlite。"""
2199
+ try:
2200
+ cookies_db = os.path.join(self._firefox_profile_dir, "cookies.sqlite")
2201
+ if os.path.isfile(cookies_db):
2202
+ os.remove(cookies_db)
2203
+ logger.info("Firefox cookies.sqlite 已删除")
2204
+ return SkillResult(success=True, message="Firefox Cookie 已清除")
2205
+ except Exception as e:
2206
+ return SkillResult(success=False, error=f"Firefox 清除 Cookie 失败: {e}")
2207
+
1594
2208
  def _log_chrome_cmdline(self) -> None:
1595
2209
  """[DEBUG] 获取 Chrome 实际进程的命令行,验证 --user-data-dir 是否正确。"""
1596
2210
  import re
@@ -1824,11 +2438,16 @@ class StealthBrowser:
1824
2438
  return False
1825
2439
 
1826
2440
  @staticmethod
1827
- def _detect_browser() -> Optional[str]:
2441
+ def _detect_browser(skip_puppeteer: bool = False) -> Optional[str]:
1828
2442
  """自动检测 Chromium/Chrome 浏览器路径
1829
2443
 
1830
2444
  [v1.47.10] 增加 snap 包装器检测:proot 下 snap 不可用,
1831
2445
  跳过 snap 包装脚本,避免浏览器启动后无法连接。
2446
+
2447
+ [v1.47.16] 增加 skip_puppeteer 参数:VNC/Termux 模式下
2448
+ Puppeteer Chrome 在 proot ARM64 下不可用(DrissionPage 报
2449
+ "The browser executable file path cannot be found"),
2450
+ 跳过以避免无效启动尝试。
1832
2451
  """
1833
2452
  # 1. 环境变量
1834
2453
  for key in ("CHROME_PATH", "BROWSER_PATH", "CHROMIUM_PATH"):
@@ -1892,17 +2511,22 @@ class StealthBrowser:
1892
2511
  return p
1893
2512
 
1894
2513
  # 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
2514
+ # [v1.47.16] VNC/Termux 模式下跳过:Puppeteer Chrome 在 proot ARM64 下
2515
+ # DrissionPage 不兼容(报 "browser executable file path cannot be found"
2516
+ if not skip_puppeteer:
2517
+ for cache in (
2518
+ os.path.join(home, ".cache", "puppeteer", "chrome"),
2519
+ os.path.join(home, ".cache", "ms-playwright"),
2520
+ ):
2521
+ if os.path.isdir(cache):
2522
+ for root, _, files in os.walk(cache):
2523
+ for fname in files:
2524
+ if fname in ("chrome", "chromium", "headless_shell"):
2525
+ full = os.path.join(root, fname)
2526
+ if os.access(full, os.X_OK):
2527
+ return full
2528
+ else:
2529
+ logger.info("[_detect_browser] VNC/Termux 模式: 跳过 Puppeteer/Playwright 缓存浏览器 (proot ARM64 不可用)")
1906
2530
 
1907
2531
  return None
1908
2532
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.47.15",
3
+ "version": "1.47.17",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {