myagent-ai 1.47.15 → 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.
- package/aiskills/browser_stealth.py +543 -14
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
|