myagent-ai 1.47.24 → 1.47.26
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 +304 -16
- package/aiskills/chromedev_mcp.py +19 -67
- package/core/output_parser.py +730 -0
- package/core/vnc_manager.py +78 -1111
- package/main.py +1 -19
- package/package.json +3 -3
- package/requirements.txt +2 -0
- package/start.js +1 -5
- package/tray_manager.py +2 -20
- package/web/ui/admin/admin-sites.js +15 -6
- package/web/ui/chat/chat_container.html +2 -2
- package/worklog.md +0 -75
|
@@ -34,6 +34,7 @@ import json
|
|
|
34
34
|
import os
|
|
35
35
|
import shutil
|
|
36
36
|
import subprocess
|
|
37
|
+
import sys
|
|
37
38
|
import threading
|
|
38
39
|
import time
|
|
39
40
|
from pathlib import Path
|
|
@@ -880,12 +881,29 @@ class StealthBrowser:
|
|
|
880
881
|
capture_output=True, text=True, timeout=5,
|
|
881
882
|
)
|
|
882
883
|
if result.returncode == 0 and result.stdout.strip():
|
|
883
|
-
|
|
884
|
+
pid = result.stdout.strip().split()[0]
|
|
885
|
+
logger.info(f"检测到已有 Firefox 运行 (PID: {pid}),复用")
|
|
886
|
+
|
|
887
|
+
# [v1.47.26] 修正 profile 目录:已有 Firefox 可能用的是 VNC manager 启动的
|
|
888
|
+
# 默认 profile (~/.mozilla/firefox/default),而不是 browser_profile_manager 路径
|
|
889
|
+
# 读取 Firefox 的命令行参数来确定实际使用的 profile
|
|
890
|
+
actual_profile = self._detect_firefox_profile(pid)
|
|
891
|
+
if actual_profile:
|
|
892
|
+
logger.info(f"检测到已有 Firefox 使用的 profile: {actual_profile}")
|
|
893
|
+
self._firefox_profile_dir = actual_profile
|
|
894
|
+
else:
|
|
895
|
+
# 从 profiles.ini 推断默认 profile
|
|
896
|
+
default_profile = self._get_firefox_default_profile()
|
|
897
|
+
if default_profile:
|
|
898
|
+
logger.info(f"使用 Firefox 默认 profile: {default_profile}")
|
|
899
|
+
self._firefox_profile_dir = default_profile
|
|
900
|
+
|
|
884
901
|
self._started = True
|
|
885
902
|
return SkillResult(
|
|
886
903
|
success=True,
|
|
887
|
-
message=f"Firefox 已在 VNC 中运行 (Profile: {self.profile_name})",
|
|
888
|
-
data={"profile": self.profile_name, "mode": "firefox_vnc", "reused": True
|
|
904
|
+
message=f"Firefox 已在 VNC 中运行 (Profile: {self.profile_name}, 实际目录: {self._firefox_profile_dir})",
|
|
905
|
+
data={"profile": self.profile_name, "mode": "firefox_vnc", "reused": True,
|
|
906
|
+
"firefox_profile_dir": self._firefox_profile_dir},
|
|
889
907
|
)
|
|
890
908
|
except Exception:
|
|
891
909
|
pass
|
|
@@ -2097,10 +2115,27 @@ class StealthBrowser:
|
|
|
2097
2115
|
)
|
|
2098
2116
|
|
|
2099
2117
|
def _firefox_screenshot(self, save_path: str = "") -> SkillResult:
|
|
2100
|
-
"""Firefox+VNC
|
|
2118
|
+
"""Firefox+VNC 模式下截图。
|
|
2119
|
+
|
|
2120
|
+
截图方法优先级(纯 Python 优先,无需外部工具):
|
|
2121
|
+
1. python-xlib + Pillow — 纯 Python X11 协议截图,proot 下最可靠
|
|
2122
|
+
2. ImageMagick import — 外部命令,需安装 imagemagick
|
|
2123
|
+
3. scrot — 轻量截图工具
|
|
2124
|
+
4. xwd + convert — 需要 xwd + imagemagick
|
|
2125
|
+
5. xwd + ffmpeg — 需要 xwd + ffmpeg
|
|
2126
|
+
6. xwd + Pillow — 需要 xwd,Pillow 转换
|
|
2127
|
+
"""
|
|
2101
2128
|
display = os.environ.get("DISPLAY", ":99")
|
|
2102
2129
|
env = {**os.environ, "DISPLAY": display}
|
|
2103
2130
|
|
|
2131
|
+
# [v1.47.26] 检测 python-xlib 是否可用(用于截图方法和错误报告)
|
|
2132
|
+
_xlib_available = False
|
|
2133
|
+
try:
|
|
2134
|
+
from Xlib import display as _xd # noqa: F401
|
|
2135
|
+
_xlib_available = True
|
|
2136
|
+
except ImportError:
|
|
2137
|
+
pass
|
|
2138
|
+
|
|
2104
2139
|
try:
|
|
2105
2140
|
if not save_path:
|
|
2106
2141
|
from core.browser_profile import get_browser_profile_manager
|
|
@@ -2109,7 +2144,40 @@ class StealthBrowser:
|
|
|
2109
2144
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
2110
2145
|
save_path = str(save_dir / f"firefox_{timestamp}.png")
|
|
2111
2146
|
|
|
2112
|
-
# 方法1:
|
|
2147
|
+
# 方法1: python-xlib + Pillow — 纯 Python X11 协议截图(无需外部工具)
|
|
2148
|
+
# [v1.47.26] 这是 proot 环境下最可靠的方法,不依赖 import/scrot/xwd 等外部命令
|
|
2149
|
+
try:
|
|
2150
|
+
from Xlib import display as xdisplay
|
|
2151
|
+
from PIL import Image as PILImage
|
|
2152
|
+
|
|
2153
|
+
disp = xdisplay.Display(os.environ.get("DISPLAY", ":99"))
|
|
2154
|
+
screen = disp.screen()
|
|
2155
|
+
root = screen.root
|
|
2156
|
+
geom = root.get_geometry()
|
|
2157
|
+
width = geom.width
|
|
2158
|
+
height = geom.height
|
|
2159
|
+
|
|
2160
|
+
# XGetImage 获取原始像素数据
|
|
2161
|
+
raw = root.get_image(0, 0, width, height, xdisplay.X.ZPixmap, 0xffffffff)
|
|
2162
|
+
# raw.data 是 bytes,格式为 BGRX(每像素4字节)
|
|
2163
|
+
img = PILImage.frombytes("RGB", (width, height), raw.data, "raw", "BGRX")
|
|
2164
|
+
img.save(save_path, "PNG")
|
|
2165
|
+
|
|
2166
|
+
if os.path.isfile(save_path):
|
|
2167
|
+
logger.info(f"Firefox 截图已保存 (python-xlib): {save_path}")
|
|
2168
|
+
return SkillResult(
|
|
2169
|
+
success=True,
|
|
2170
|
+
message="Firefox+VNC 截图已保存 (python-xlib)",
|
|
2171
|
+
data={"path": save_path},
|
|
2172
|
+
)
|
|
2173
|
+
else:
|
|
2174
|
+
logger.warning("[_firefox_screenshot] python-xlib 截图保存失败")
|
|
2175
|
+
except ImportError:
|
|
2176
|
+
logger.debug("[_firefox_screenshot] python-xlib 未安装,跳过纯 Python 截图")
|
|
2177
|
+
except Exception as xlib_err:
|
|
2178
|
+
logger.warning(f"[_firefox_screenshot] python-xlib 截图失败: {xlib_err}")
|
|
2179
|
+
|
|
2180
|
+
# 方法2: 使用 ImageMagick import 截取整个屏幕
|
|
2113
2181
|
import_cmd = shutil.which("import")
|
|
2114
2182
|
if import_cmd:
|
|
2115
2183
|
result = subprocess.run(
|
|
@@ -2181,19 +2249,16 @@ class StealthBrowser:
|
|
|
2181
2249
|
f"{result.stderr.decode(errors='ignore')[:200]}"
|
|
2182
2250
|
)
|
|
2183
2251
|
|
|
2184
|
-
# 方法4: 使用
|
|
2185
|
-
xdotool_cmd = shutil.which("xdotool")
|
|
2252
|
+
# 方法4: 使用 xwd + ffmpeg (proot 兼容)
|
|
2186
2253
|
ffmpeg_cmd = shutil.which("ffmpeg")
|
|
2187
|
-
if
|
|
2254
|
+
if xwd_cmd and ffmpeg_cmd:
|
|
2188
2255
|
xwd_tmp = save_path + ".xwd"
|
|
2189
|
-
# 用 xdotool 获取活动窗口并截图
|
|
2190
2256
|
result = subprocess.run(
|
|
2191
|
-
[xwd_cmd
|
|
2257
|
+
[xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
|
|
2192
2258
|
capture_output=True, timeout=10,
|
|
2193
2259
|
env=env, start_new_session=True,
|
|
2194
2260
|
)
|
|
2195
2261
|
if result.returncode == 0 and os.path.isfile(xwd_tmp):
|
|
2196
|
-
# 用 ffmpeg 转换 xwd 到 png
|
|
2197
2262
|
result2 = subprocess.run(
|
|
2198
2263
|
[ffmpeg_cmd, "-y", "-i", xwd_tmp, save_path],
|
|
2199
2264
|
capture_output=True, timeout=10,
|
|
@@ -2210,22 +2275,213 @@ class StealthBrowser:
|
|
|
2210
2275
|
message=f"Firefox+VNC 截图已保存",
|
|
2211
2276
|
data={"path": save_path},
|
|
2212
2277
|
)
|
|
2278
|
+
logger.warning(
|
|
2279
|
+
f"[_firefox_screenshot] xwd+ffmpeg 截图失败: xwd_rc={result.returncode}"
|
|
2280
|
+
)
|
|
2281
|
+
|
|
2282
|
+
# 方法5: 使用 Python Pillow + xwd
|
|
2283
|
+
try:
|
|
2284
|
+
from PIL import Image
|
|
2285
|
+
if xwd_cmd:
|
|
2286
|
+
xwd_tmp = save_path + ".xwd"
|
|
2287
|
+
result = subprocess.run(
|
|
2288
|
+
[xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
|
|
2289
|
+
capture_output=True, timeout=10,
|
|
2290
|
+
env=env, start_new_session=True,
|
|
2291
|
+
)
|
|
2292
|
+
if result.returncode == 0 and os.path.isfile(xwd_tmp):
|
|
2293
|
+
try:
|
|
2294
|
+
img = Image.open(xwd_tmp)
|
|
2295
|
+
img.save(save_path, "PNG")
|
|
2296
|
+
os.unlink(xwd_tmp)
|
|
2297
|
+
logger.info(f"Firefox 截图已保存 (xwd+Pillow): {save_path}")
|
|
2298
|
+
return SkillResult(
|
|
2299
|
+
success=True,
|
|
2300
|
+
message=f"Firefox+VNC 截图已保存",
|
|
2301
|
+
data={"path": save_path},
|
|
2302
|
+
)
|
|
2303
|
+
except Exception as pil_err:
|
|
2304
|
+
logger.warning(f"[_firefox_screenshot] Pillow 转换失败: {pil_err}")
|
|
2305
|
+
try: os.unlink(xwd_tmp)
|
|
2306
|
+
except Exception: pass
|
|
2307
|
+
except ImportError:
|
|
2308
|
+
pass
|
|
2213
2309
|
|
|
2214
2310
|
_available = []
|
|
2311
|
+
_available.append("python-xlib" if _xlib_available else "python-xlib❌")
|
|
2215
2312
|
if import_cmd: _available.append("import")
|
|
2216
2313
|
if scrot_cmd: _available.append("scrot")
|
|
2217
2314
|
if xwd_cmd and convert_cmd: _available.append("xwd+convert")
|
|
2218
|
-
if
|
|
2219
|
-
_tried = ", ".join(_available)
|
|
2315
|
+
if xwd_cmd and ffmpeg_cmd: _available.append("xwd+ffmpeg")
|
|
2316
|
+
_tried = ", ".join(_available)
|
|
2317
|
+
|
|
2318
|
+
# [v1.47.26] 自动安装 python-xlib(如果未安装)
|
|
2319
|
+
if not _xlib_available:
|
|
2320
|
+
logger.info("[_firefox_screenshot] 尝试自动安装 python-xlib ...")
|
|
2321
|
+
try:
|
|
2322
|
+
result = subprocess.run(
|
|
2323
|
+
[sys.executable, "-m", "pip", "install", "python-xlib", "-q"],
|
|
2324
|
+
capture_output=True, timeout=60,
|
|
2325
|
+
)
|
|
2326
|
+
if result.returncode == 0:
|
|
2327
|
+
logger.info("[_firefox_screenshot] python-xlib 安装成功,重试截图")
|
|
2328
|
+
try:
|
|
2329
|
+
from Xlib import display as xdisplay
|
|
2330
|
+
from PIL import Image as PILImage
|
|
2331
|
+
disp = xdisplay.Display(os.environ.get("DISPLAY", ":99"))
|
|
2332
|
+
screen = disp.screen()
|
|
2333
|
+
root = screen.root
|
|
2334
|
+
geom = root.get_geometry()
|
|
2335
|
+
raw = root.get_image(0, 0, geom.width, geom.height, xdisplay.X.ZPixmap, 0xffffffff)
|
|
2336
|
+
img = PILImage.frombytes("RGB", (geom.width, geom.height), raw.data, "raw", "BGRX")
|
|
2337
|
+
img.save(save_path, "PNG")
|
|
2338
|
+
if os.path.isfile(save_path):
|
|
2339
|
+
logger.info(f"Firefox 截图已保存 (python-xlib 安装后): {save_path}")
|
|
2340
|
+
return SkillResult(
|
|
2341
|
+
success=True,
|
|
2342
|
+
message="Firefox+VNC 截图已保存 (python-xlib)",
|
|
2343
|
+
data={"path": save_path},
|
|
2344
|
+
)
|
|
2345
|
+
except Exception as retry_err:
|
|
2346
|
+
logger.warning(f"[_firefox_screenshot] python-xlib 安装后截图仍失败: {retry_err}")
|
|
2347
|
+
else:
|
|
2348
|
+
logger.warning(f"[_firefox_screenshot] python-xlib 安装失败: {result.stderr.decode(errors='ignore')[:200]}")
|
|
2349
|
+
except Exception as pip_err:
|
|
2350
|
+
logger.warning(f"[_firefox_screenshot] pip install python-xlib 异常: {pip_err}")
|
|
2220
2351
|
|
|
2221
2352
|
return SkillResult(
|
|
2222
2353
|
success=False,
|
|
2223
2354
|
error=f"Firefox+VNC 截图失败 (尝试过: {_tried}) "
|
|
2224
|
-
f"— 需要
|
|
2355
|
+
f"— 需要 python-xlib (pip install python-xlib) 或 ImageMagick/scrot",
|
|
2225
2356
|
)
|
|
2226
2357
|
except Exception as e:
|
|
2227
2358
|
return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
|
|
2228
2359
|
|
|
2360
|
+
@staticmethod
|
|
2361
|
+
def _detect_firefox_profile(pid: str) -> Optional[str]:
|
|
2362
|
+
"""[v1.47.26] 从运行中 Firefox 进程的命令行参数检测其 profile 目录。
|
|
2363
|
+
|
|
2364
|
+
读取 /proc/<pid>/cmdline 获取 Firefox 启动参数,查找 --profile 或
|
|
2365
|
+
从 --user-data-dir 推断 profile 位置。
|
|
2366
|
+
|
|
2367
|
+
Args:
|
|
2368
|
+
pid: Firefox 进程 PID
|
|
2369
|
+
|
|
2370
|
+
Returns:
|
|
2371
|
+
Firefox profile 目录路径,检测失败返回 None
|
|
2372
|
+
"""
|
|
2373
|
+
try:
|
|
2374
|
+
cmdline_path = f"/proc/{pid}/cmdline"
|
|
2375
|
+
if not os.path.isfile(cmdline_path):
|
|
2376
|
+
return None
|
|
2377
|
+
|
|
2378
|
+
with open(cmdline_path, "rb") as f:
|
|
2379
|
+
cmdline_data = f.read()
|
|
2380
|
+
|
|
2381
|
+
# /proc/pid/cmdline 用 \0 分隔参数
|
|
2382
|
+
args = cmdline_data.decode("utf-8", errors="replace").split("\0")
|
|
2383
|
+
args = [a for a in args if a.strip()]
|
|
2384
|
+
|
|
2385
|
+
# 查找 --profile 参数
|
|
2386
|
+
for i, arg in enumerate(args):
|
|
2387
|
+
if arg == "--profile" and i + 1 < len(args):
|
|
2388
|
+
profile_dir = args[i + 1]
|
|
2389
|
+
if os.path.isdir(profile_dir):
|
|
2390
|
+
logger.info(f"[_detect_firefox_profile] 从 cmdline 找到 --profile: {profile_dir}")
|
|
2391
|
+
return profile_dir
|
|
2392
|
+
elif arg.startswith("--profile="):
|
|
2393
|
+
profile_dir = arg.split("=", 1)[1]
|
|
2394
|
+
if os.path.isdir(profile_dir):
|
|
2395
|
+
logger.info(f"[_detect_firefox_profile] 从 cmdline 找到 --profile=: {profile_dir}")
|
|
2396
|
+
return profile_dir
|
|
2397
|
+
|
|
2398
|
+
# 没有 --profile 参数 → Firefox 使用 profiles.ini 中的默认 profile
|
|
2399
|
+
return None
|
|
2400
|
+
except Exception as e:
|
|
2401
|
+
logger.debug(f"[_detect_firefox_profile] 检测失败: {e}")
|
|
2402
|
+
return None
|
|
2403
|
+
|
|
2404
|
+
@staticmethod
|
|
2405
|
+
def _get_firefox_default_profile() -> Optional[str]:
|
|
2406
|
+
"""[v1.47.26] 从 profiles.ini 获取 Firefox 默认 profile 目录路径。
|
|
2407
|
+
|
|
2408
|
+
Firefox 的 profiles.ini 格式:
|
|
2409
|
+
[Profile0]
|
|
2410
|
+
Name=default
|
|
2411
|
+
IsRelative=1
|
|
2412
|
+
Path=default
|
|
2413
|
+
Default=1
|
|
2414
|
+
|
|
2415
|
+
[General]
|
|
2416
|
+
StartWithLastProfile=1
|
|
2417
|
+
|
|
2418
|
+
如果 profiles.ini 不存在或解析失败,尝试直接检测
|
|
2419
|
+
~/.mozilla/firefox/default 目录。
|
|
2420
|
+
|
|
2421
|
+
Returns:
|
|
2422
|
+
Firefox 默认 profile 目录的绝对路径,失败返回 None
|
|
2423
|
+
"""
|
|
2424
|
+
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
2425
|
+
profiles_ini = os.path.join(ff_base, "profiles.ini")
|
|
2426
|
+
|
|
2427
|
+
# 方法1: 解析 profiles.ini
|
|
2428
|
+
if os.path.isfile(profiles_ini):
|
|
2429
|
+
try:
|
|
2430
|
+
import configparser
|
|
2431
|
+
config = configparser.ConfigParser()
|
|
2432
|
+
config.read(profiles_ini)
|
|
2433
|
+
|
|
2434
|
+
# 查找标记为 Default=1 的 profile
|
|
2435
|
+
for section in config.sections():
|
|
2436
|
+
if section.startswith("Profile"):
|
|
2437
|
+
is_default = config.get(section, "Default", fallback="")
|
|
2438
|
+
is_relative = config.get(section, "IsRelative", fallback="1")
|
|
2439
|
+
path = config.get(section, "Path", fallback="")
|
|
2440
|
+
|
|
2441
|
+
if is_default == "1" and path:
|
|
2442
|
+
if is_relative == "1":
|
|
2443
|
+
full_path = os.path.join(ff_base, path)
|
|
2444
|
+
else:
|
|
2445
|
+
full_path = path
|
|
2446
|
+
if os.path.isdir(full_path):
|
|
2447
|
+
logger.info(f"[_get_firefox_default_profile] profiles.ini Default profile: {full_path}")
|
|
2448
|
+
return full_path
|
|
2449
|
+
|
|
2450
|
+
# 没有 Default=1,取第一个 Profile section
|
|
2451
|
+
for section in config.sections():
|
|
2452
|
+
if section.startswith("Profile"):
|
|
2453
|
+
path = config.get(section, "Path", fallback="")
|
|
2454
|
+
is_relative = config.get(section, "IsRelative", fallback="1")
|
|
2455
|
+
if path:
|
|
2456
|
+
full_path = os.path.join(ff_base, path) if is_relative == "1" else path
|
|
2457
|
+
if os.path.isdir(full_path):
|
|
2458
|
+
logger.info(f"[_get_firefox_default_profile] profiles.ini 第一个 profile: {full_path}")
|
|
2459
|
+
return full_path
|
|
2460
|
+
except Exception as e:
|
|
2461
|
+
logger.debug(f"[_get_firefox_default_profile] profiles.ini 解析失败: {e}")
|
|
2462
|
+
|
|
2463
|
+
# 方法2: 直接检测 ~/.mozilla/firefox/default 目录
|
|
2464
|
+
default_dir = os.path.join(ff_base, "default")
|
|
2465
|
+
if os.path.isdir(default_dir):
|
|
2466
|
+
logger.info(f"[_get_firefox_default_profile] 直接找到 default 目录: {default_dir}")
|
|
2467
|
+
return default_dir
|
|
2468
|
+
|
|
2469
|
+
# 方法3: 扫描所有 Firefox profile 子目录
|
|
2470
|
+
if os.path.isdir(ff_base):
|
|
2471
|
+
try:
|
|
2472
|
+
for entry in sorted(os.listdir(ff_base)):
|
|
2473
|
+
entry_path = os.path.join(ff_base, entry)
|
|
2474
|
+
if not os.path.isdir(entry_path):
|
|
2475
|
+
continue
|
|
2476
|
+
# Firefox profile 目录特征:包含 prefs.js
|
|
2477
|
+
if os.path.isfile(os.path.join(entry_path, "prefs.js")):
|
|
2478
|
+
logger.info(f"[_get_firefox_default_profile] 扫描找到 profile: {entry_path}")
|
|
2479
|
+
return entry_path
|
|
2480
|
+
except Exception:
|
|
2481
|
+
pass
|
|
2482
|
+
|
|
2483
|
+
return None
|
|
2484
|
+
|
|
2229
2485
|
def _firefox_read_sessionstore(self, max_wait: float = 5.0) -> dict:
|
|
2230
2486
|
"""[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
|
|
2231
2487
|
|
|
@@ -2242,8 +2498,33 @@ class StealthBrowser:
|
|
|
2242
2498
|
|
|
2243
2499
|
# 搜索可能的 sessionstore 路径
|
|
2244
2500
|
search_dirs = []
|
|
2245
|
-
|
|
2501
|
+
|
|
2502
|
+
# [v1.47.26] 优先搜索 Firefox 实际使用的 profile 目录
|
|
2503
|
+
# 先尝试从运行中 Firefox 进程检测实际 profile
|
|
2504
|
+
actual_profile = None
|
|
2505
|
+
try:
|
|
2506
|
+
result = subprocess.run(
|
|
2507
|
+
["pgrep", "-f", "firefox"],
|
|
2508
|
+
capture_output=True, text=True, timeout=3,
|
|
2509
|
+
)
|
|
2510
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
2511
|
+
pid = result.stdout.strip().split()[0]
|
|
2512
|
+
actual_profile = self._detect_firefox_profile(pid)
|
|
2513
|
+
except Exception:
|
|
2514
|
+
pass
|
|
2515
|
+
|
|
2516
|
+
if actual_profile and os.path.isdir(actual_profile) and actual_profile not in search_dirs:
|
|
2517
|
+
search_dirs.append(actual_profile)
|
|
2518
|
+
logger.info(f"[_firefox_read_sessionstore] 搜索实际 Firefox profile: {actual_profile}")
|
|
2519
|
+
|
|
2520
|
+
# 也从 profiles.ini 获取默认 profile
|
|
2521
|
+
default_profile = self._get_firefox_default_profile()
|
|
2522
|
+
if default_profile and os.path.isdir(default_profile) and default_profile not in search_dirs:
|
|
2523
|
+
search_dirs.append(default_profile)
|
|
2524
|
+
|
|
2525
|
+
if self._firefox_profile_dir and os.path.isdir(self._firefox_profile_dir) and self._firefox_profile_dir not in search_dirs:
|
|
2246
2526
|
search_dirs.append(self._firefox_profile_dir)
|
|
2527
|
+
|
|
2247
2528
|
# 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
|
|
2248
2529
|
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
2249
2530
|
for extra in [
|
|
@@ -2252,7 +2533,8 @@ class StealthBrowser:
|
|
|
2252
2533
|
]:
|
|
2253
2534
|
if os.path.isdir(extra) and extra not in search_dirs:
|
|
2254
2535
|
search_dirs.append(extra)
|
|
2255
|
-
|
|
2536
|
+
|
|
2537
|
+
# 搜索所有 Firefox profile 子目录(如 xxx.default-release)
|
|
2256
2538
|
try:
|
|
2257
2539
|
if os.path.isdir(ff_base):
|
|
2258
2540
|
for entry in os.listdir(ff_base):
|
|
@@ -2265,6 +2547,12 @@ class StealthBrowser:
|
|
|
2265
2547
|
except Exception:
|
|
2266
2548
|
pass
|
|
2267
2549
|
|
|
2550
|
+
# [v1.47.26] 详细记录搜索路径
|
|
2551
|
+
logger.info(
|
|
2552
|
+
f"[_firefox_read_sessionstore] 搜索 {len(search_dirs)} 个目录: "
|
|
2553
|
+
f"{[d.replace(os.path.expanduser('~'), '~') for d in search_dirs]}"
|
|
2554
|
+
)
|
|
2555
|
+
|
|
2268
2556
|
# [v1.47.24] 等待 Firefox 写入 sessionstore(刚导航完可能还没写入)
|
|
2269
2557
|
_start_time = time.time()
|
|
2270
2558
|
while True:
|
|
@@ -189,55 +189,27 @@ def _ensure_display() -> Optional[str]:
|
|
|
189
189
|
logger.info(f"VNC 远程桌面已在运行,复用显示: {display}")
|
|
190
190
|
return display
|
|
191
191
|
|
|
192
|
-
#
|
|
193
|
-
# 修复: 不能在 ThreadPoolExecutor + asyncio.run 中直接调用 vnc.start()!
|
|
194
|
-
# 因为 vnc.start() 内部用 asyncio.Lock(),跨 event loop 会导致死锁。
|
|
195
|
-
# 正确做法:在单独线程中用 asyncio.run 启动,然后轮询等待 VNC 就绪。
|
|
192
|
+
# VNC 未运行 → 尝试自动启动
|
|
196
193
|
try:
|
|
197
194
|
import asyncio
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
logger.info(f"VNC 远程桌面已启动,复用显示: {display}")
|
|
217
|
-
return display
|
|
218
|
-
|
|
219
|
-
try:
|
|
220
|
-
result = future.result(timeout=0.1)
|
|
221
|
-
if result.get("success") or vnc.is_running:
|
|
222
|
-
display = vnc.display
|
|
223
|
-
os.environ["DISPLAY"] = display
|
|
224
|
-
logger.info(f"VNC 远程桌面已启动,显示: {display}")
|
|
225
|
-
return display
|
|
226
|
-
else:
|
|
227
|
-
logger.warning(f"VNC 启动失败: {result.get('message', '')}")
|
|
228
|
-
break
|
|
229
|
-
except concurrent.futures.TimeoutError:
|
|
230
|
-
pass
|
|
231
|
-
|
|
232
|
-
import time
|
|
233
|
-
time.sleep(check_interval)
|
|
234
|
-
elapsed += check_interval
|
|
235
|
-
|
|
236
|
-
if vnc.is_running:
|
|
237
|
-
display = vnc.display
|
|
238
|
-
os.environ["DISPLAY"] = display
|
|
239
|
-
return display
|
|
240
|
-
|
|
195
|
+
try:
|
|
196
|
+
loop = asyncio.get_event_loop()
|
|
197
|
+
if loop.is_running():
|
|
198
|
+
import concurrent.futures
|
|
199
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
200
|
+
result = pool.submit(asyncio.run, vnc.start()).result(timeout=30)
|
|
201
|
+
else:
|
|
202
|
+
result = asyncio.run(vnc.start())
|
|
203
|
+
except RuntimeError:
|
|
204
|
+
result = asyncio.run(vnc.start())
|
|
205
|
+
|
|
206
|
+
if result.get("success"):
|
|
207
|
+
display = vnc.display
|
|
208
|
+
os.environ["DISPLAY"] = display
|
|
209
|
+
logger.info(f"VNC 远程桌面已自动启动,显示: {display}")
|
|
210
|
+
return display
|
|
211
|
+
else:
|
|
212
|
+
logger.warning(f"VNC 自动启动失败: {result.get('message', '')}")
|
|
241
213
|
except Exception as e:
|
|
242
214
|
logger.warning(f"VNC 自动启动异常: {e}")
|
|
243
215
|
|
|
@@ -1372,26 +1344,6 @@ class BrowserOpenSkill(Skill):
|
|
|
1372
1344
|
if not url:
|
|
1373
1345
|
return SkillResult(success=False, error="缺少必需参数: url")
|
|
1374
1346
|
|
|
1375
|
-
# [v1.47.20] VNC 模式下:Chromium 不可用时回退到 stealth_browser_navigate
|
|
1376
|
-
try:
|
|
1377
|
-
from core.vnc_manager import get_vnc_manager
|
|
1378
|
-
vnc_mgr = get_vnc_manager()
|
|
1379
|
-
if vnc_mgr.is_running:
|
|
1380
|
-
# 检查是否有可用的 Chromium
|
|
1381
|
-
import shutil
|
|
1382
|
-
has_chrome = bool(shutil.which("chromium-browser") or shutil.which("chromium")
|
|
1383
|
-
or shutil.which("google-chrome"))
|
|
1384
|
-
if not has_chrome:
|
|
1385
|
-
# VNC 模式下无 Chromium,回退到 stealth_browser
|
|
1386
|
-
return SkillResult(
|
|
1387
|
-
success=False,
|
|
1388
|
-
error="VNC 远程桌面模式下没有 Chromium 浏览器,无法使用 browser_open。"
|
|
1389
|
-
"请改用 stealth_browser_start + stealth_browser_navigate 操作 Firefox。"
|
|
1390
|
-
"Firefox 在 VNC 模式下已可用。",
|
|
1391
|
-
)
|
|
1392
|
-
except ImportError:
|
|
1393
|
-
pass # vnc_manager 不可用,跳过检测
|
|
1394
|
-
|
|
1395
1347
|
# 检查依赖
|
|
1396
1348
|
dep_err = await asyncio.get_event_loop().run_in_executor(None, _ensure_node_deps)
|
|
1397
1349
|
if dep_err:
|