myagent-ai 1.47.25 → 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.
@@ -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
- logger.info(f"检测到已有 Firefox 运行 (PID: {result.stdout.strip().split()[0]}),复用")
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 模式下通过 xdotool + import 截图。"""
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: 使用 ImageMagick import 截取整个屏幕
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(
@@ -2240,20 +2308,180 @@ class StealthBrowser:
2240
2308
  pass
2241
2309
 
2242
2310
  _available = []
2311
+ _available.append("python-xlib" if _xlib_available else "python-xlib❌")
2243
2312
  if import_cmd: _available.append("import")
2244
2313
  if scrot_cmd: _available.append("scrot")
2245
2314
  if xwd_cmd and convert_cmd: _available.append("xwd+convert")
2246
2315
  if xwd_cmd and ffmpeg_cmd: _available.append("xwd+ffmpeg")
2247
- _tried = ", ".join(_available) if _available else "无"
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}")
2248
2351
 
2249
2352
  return SkillResult(
2250
2353
  success=False,
2251
2354
  error=f"Firefox+VNC 截图失败 (尝试过: {_tried}) "
2252
- f"— 需要 ImageMagick / scrot / xwd+convert/ffmpeg",
2355
+ f"— 需要 python-xlib (pip install python-xlib) 或 ImageMagick/scrot",
2253
2356
  )
2254
2357
  except Exception as e:
2255
2358
  return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
2256
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
+
2257
2485
  def _firefox_read_sessionstore(self, max_wait: float = 5.0) -> dict:
2258
2486
  """[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
2259
2487
 
@@ -2270,8 +2498,33 @@ class StealthBrowser:
2270
2498
 
2271
2499
  # 搜索可能的 sessionstore 路径
2272
2500
  search_dirs = []
2273
- if self._firefox_profile_dir:
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:
2274
2526
  search_dirs.append(self._firefox_profile_dir)
2527
+
2275
2528
  # 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
2276
2529
  ff_base = os.path.expanduser("~/.mozilla/firefox")
2277
2530
  for extra in [
@@ -2280,7 +2533,8 @@ class StealthBrowser:
2280
2533
  ]:
2281
2534
  if os.path.isdir(extra) and extra not in search_dirs:
2282
2535
  search_dirs.append(extra)
2283
- # [v1.47.24] 搜索所有 Firefox profile 子目录(如 xxx.default-release)
2536
+
2537
+ # 搜索所有 Firefox profile 子目录(如 xxx.default-release)
2284
2538
  try:
2285
2539
  if os.path.isdir(ff_base):
2286
2540
  for entry in os.listdir(ff_base):
@@ -2293,6 +2547,12 @@ class StealthBrowser:
2293
2547
  except Exception:
2294
2548
  pass
2295
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
+
2296
2556
  # [v1.47.24] 等待 Firefox 写入 sessionstore(刚导航完可能还没写入)
2297
2557
  _start_time = time.time()
2298
2558
  while True:
@@ -452,6 +452,7 @@ class VNCManager:
452
452
 
453
453
  # [v1.47.25] 截图工具 — Firefox+VNC 模式必需,用于获取页面内容
454
454
  # imagemagick 提供 `import` 命令(X11 截图),scrot 作为备选
455
+ # [v1.47.26] python-xlib 是纯 Python X11 协议截图方案(无需外部工具),最可靠
455
456
  if not shutil.which("import") and not shutil.which("scrot"):
456
457
  packages_to_install.append("imagemagick")
457
458
 
@@ -522,6 +523,9 @@ class VNCManager:
522
523
  if not shutil.which("websockify"):
523
524
  self._install_websockify_quick()
524
525
 
526
+ # [v1.47.26] python-xlib pip 安装 — Firefox+VNC 截图依赖
527
+ self._install_python_xlib_quick()
528
+
525
529
  msg = "VNC 依赖安装完成(后台)"
526
530
  return True, msg
527
531
 
@@ -592,6 +596,29 @@ class VNCManager:
592
596
  except Exception as e:
593
597
  logger.warning(f"websockify 安装异常: {e}")
594
598
 
599
+ def _install_python_xlib_quick(self) -> None:
600
+ """[v1.47.26] 快速安装 python-xlib(pip)— Firefox+VNC 截图必需"""
601
+ try:
602
+ from Xlib import display # noqa: F401
603
+ return # 已安装
604
+ except ImportError:
605
+ pass
606
+ try:
607
+ logger.info("pip install python-xlib — Firefox+VNC 截图依赖")
608
+ pip_cmd = shutil.which("pip3") or shutil.which("pip")
609
+ if pip_cmd:
610
+ result = subprocess.run(
611
+ [pip_cmd, "install", "python-xlib", "-q"],
612
+ capture_output=True, text=True, timeout=60,
613
+ start_new_session=True,
614
+ )
615
+ if result.returncode == 0:
616
+ logger.info("python-xlib 安装成功")
617
+ else:
618
+ logger.warning(f"python-xlib 安装失败: {result.stderr[-200:]}")
619
+ except Exception as e:
620
+ logger.warning(f"python-xlib 安装异常: {e}")
621
+
595
622
  def _install_packages_one_by_one(self, sudo: list, packages: list) -> None:
596
623
  """[v1.34.0] 逐个安装包 — 批量安装失败时的回退策略。
597
624
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.47.25",
3
+ "version": "1.47.26",
4
4
  "description": "\u672c\u5730\u684c\u9762\u7aef\u6267\u884c\u578bAI\u52a9\u624b - Open Interpreter \u98ce\u683c | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/requirements.txt CHANGED
@@ -52,6 +52,7 @@ mss>=9.0.0
52
52
  # pystray>=0.19.5
53
53
  Pillow>=10.0.0
54
54
  lz4>=4.0.0
55
+ python-xlib>=0.33 # [v1.47.26] Firefox+VNC 模式截图(纯 Python X11 协议,无需外部截图工具)
55
56
 
56
57
  # ============================================================
57
58
  # 聊天平台 (按需使用,默认安装)