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