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.
- package/aiskills/browser_stealth.py +720 -96
- 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()
|
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
|