myagent-ai 1.47.23 → 1.47.25

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.
@@ -2124,6 +2124,10 @@ class StealthBrowser:
2124
2124
  message=f"Firefox+VNC 截图已保存",
2125
2125
  data={"path": save_path},
2126
2126
  )
2127
+ logger.warning(
2128
+ f"[_firefox_screenshot] import 截图失败 (rc={result.returncode}): "
2129
+ f"{result.stderr.decode(errors='ignore')[:200]}"
2130
+ )
2127
2131
 
2128
2132
  # 方法2: 使用 xdotool + scrot
2129
2133
  scrot_cmd = shutil.which("scrot")
@@ -2140,6 +2144,10 @@ class StealthBrowser:
2140
2144
  message=f"Firefox+VNC 截图已保存 (scrot)",
2141
2145
  data={"path": save_path},
2142
2146
  )
2147
+ logger.warning(
2148
+ f"[_firefox_screenshot] scrot 截图失败 (rc={result.returncode}): "
2149
+ f"{result.stderr.decode(errors='ignore')[:200]}"
2150
+ )
2143
2151
 
2144
2152
  # 方法3: 使用 xwd + convert
2145
2153
  xwd_cmd = shutil.which("xwd")
@@ -2168,21 +2176,93 @@ class StealthBrowser:
2168
2176
  message=f"Firefox+VNC 截图已保存",
2169
2177
  data={"path": save_path},
2170
2178
  )
2179
+ logger.warning(
2180
+ f"[_firefox_screenshot] xwd+convert 截图失败 (rc={result.returncode}): "
2181
+ f"{result.stderr.decode(errors='ignore')[:200]}"
2182
+ )
2183
+
2184
+ # 方法4: 使用 xwd + ffmpeg (proot 兼容)
2185
+ ffmpeg_cmd = shutil.which("ffmpeg")
2186
+ if xwd_cmd and ffmpeg_cmd:
2187
+ xwd_tmp = save_path + ".xwd"
2188
+ result = subprocess.run(
2189
+ [xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
2190
+ capture_output=True, timeout=10,
2191
+ env=env, start_new_session=True,
2192
+ )
2193
+ if result.returncode == 0 and os.path.isfile(xwd_tmp):
2194
+ result2 = subprocess.run(
2195
+ [ffmpeg_cmd, "-y", "-i", xwd_tmp, save_path],
2196
+ capture_output=True, timeout=10,
2197
+ env=env, start_new_session=True,
2198
+ )
2199
+ try:
2200
+ os.unlink(xwd_tmp)
2201
+ except Exception:
2202
+ pass
2203
+ if result2.returncode == 0 and os.path.isfile(save_path):
2204
+ logger.info(f"Firefox 截图已保存 (xwd+ffmpeg): {save_path}")
2205
+ return SkillResult(
2206
+ success=True,
2207
+ message=f"Firefox+VNC 截图已保存",
2208
+ data={"path": save_path},
2209
+ )
2210
+ logger.warning(
2211
+ f"[_firefox_screenshot] xwd+ffmpeg 截图失败: xwd_rc={result.returncode}"
2212
+ )
2213
+
2214
+ # 方法5: 使用 Python Pillow + xwd
2215
+ try:
2216
+ from PIL import Image
2217
+ if xwd_cmd:
2218
+ xwd_tmp = save_path + ".xwd"
2219
+ result = subprocess.run(
2220
+ [xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
2221
+ capture_output=True, timeout=10,
2222
+ env=env, start_new_session=True,
2223
+ )
2224
+ if result.returncode == 0 and os.path.isfile(xwd_tmp):
2225
+ try:
2226
+ img = Image.open(xwd_tmp)
2227
+ img.save(save_path, "PNG")
2228
+ os.unlink(xwd_tmp)
2229
+ logger.info(f"Firefox 截图已保存 (xwd+Pillow): {save_path}")
2230
+ return SkillResult(
2231
+ success=True,
2232
+ message=f"Firefox+VNC 截图已保存",
2233
+ data={"path": save_path},
2234
+ )
2235
+ except Exception as pil_err:
2236
+ logger.warning(f"[_firefox_screenshot] Pillow 转换失败: {pil_err}")
2237
+ try: os.unlink(xwd_tmp)
2238
+ except Exception: pass
2239
+ except ImportError:
2240
+ pass
2241
+
2242
+ _available = []
2243
+ if import_cmd: _available.append("import")
2244
+ if scrot_cmd: _available.append("scrot")
2245
+ if xwd_cmd and convert_cmd: _available.append("xwd+convert")
2246
+ if xwd_cmd and ffmpeg_cmd: _available.append("xwd+ffmpeg")
2247
+ _tried = ", ".join(_available) if _available else "无"
2171
2248
 
2172
2249
  return SkillResult(
2173
2250
  success=False,
2174
- error="Firefox+VNC 截图失败: 没有可用的截图工具 "
2175
- "(需要 ImageMagick import / scrot / xwd+convert)",
2251
+ error=f"Firefox+VNC 截图失败 (尝试过: {_tried}) "
2252
+ f"需要 ImageMagick / scrot / xwd+convert/ffmpeg",
2176
2253
  )
2177
2254
  except Exception as e:
2178
2255
  return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
2179
2256
 
2180
- def _firefox_read_sessionstore(self) -> dict:
2257
+ def _firefox_read_sessionstore(self, max_wait: float = 5.0) -> dict:
2181
2258
  """[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
2182
2259
 
2183
2260
  Firefox 运行时会将当前会话信息写入 sessionstore-backups/recovery.jsonlz4。
2184
2261
  该文件使用 mozLz4 格式(8字节自定义头 + LZ4 压缩数据)。
2185
2262
 
2263
+ Args:
2264
+ max_wait: 最长等待时间(秒),给 Firefox 时间写入 sessionstore
2265
+
2186
2266
  Returns:
2187
2267
  dict: {"url": str, "title": str, "tabs": [{"url": str, "title": str}]}
2188
2268
  """
@@ -2193,30 +2273,56 @@ class StealthBrowser:
2193
2273
  if self._firefox_profile_dir:
2194
2274
  search_dirs.append(self._firefox_profile_dir)
2195
2275
  # 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
2276
+ ff_base = os.path.expanduser("~/.mozilla/firefox")
2196
2277
  for extra in [
2197
- os.path.expanduser("~/.mozilla/firefox/default"),
2198
- os.path.expanduser("~/.mozilla/firefox"),
2278
+ os.path.join(ff_base, "default"),
2279
+ ff_base,
2199
2280
  ]:
2200
2281
  if os.path.isdir(extra) and extra not in search_dirs:
2201
2282
  search_dirs.append(extra)
2283
+ # [v1.47.24] 搜索所有 Firefox profile 子目录(如 xxx.default-release)
2284
+ try:
2285
+ if os.path.isdir(ff_base):
2286
+ for entry in os.listdir(ff_base):
2287
+ entry_path = os.path.join(ff_base, entry)
2288
+ if os.path.isdir(entry_path) and entry_path not in search_dirs:
2289
+ # Firefox profile 目录通常包含 user.js 或 prefs.js
2290
+ if os.path.isfile(os.path.join(entry_path, "prefs.js")) or \
2291
+ os.path.isfile(os.path.join(entry_path, "user.js")):
2292
+ search_dirs.append(entry_path)
2293
+ except Exception:
2294
+ pass
2202
2295
 
2203
- recovery_files = []
2204
- for base_dir in search_dirs:
2205
- ss_dir = os.path.join(base_dir, "sessionstore-backups")
2206
- if os.path.isdir(ss_dir):
2207
- for fname in ("recovery.jsonlz4", "recovery.baklz4",
2208
- "previous.jsonlz4"):
2209
- fpath = os.path.join(ss_dir, fname)
2296
+ # [v1.47.24] 等待 Firefox 写入 sessionstore(刚导航完可能还没写入)
2297
+ _start_time = time.time()
2298
+ while True:
2299
+ recovery_files = []
2300
+ for base_dir in search_dirs:
2301
+ ss_dir = os.path.join(base_dir, "sessionstore-backups")
2302
+ if os.path.isdir(ss_dir):
2303
+ for fname in ("recovery.jsonlz4", "recovery.baklz4",
2304
+ "previous.jsonlz4"):
2305
+ fpath = os.path.join(ss_dir, fname)
2306
+ if os.path.isfile(fpath):
2307
+ recovery_files.append(fpath)
2308
+ # 也检查 base_dir 本身(有些 Firefox 版本)
2309
+ for fname in ("sessionstore.jsonlz4", "sessionstore-backups/recovery.jsonlz4"):
2310
+ fpath = os.path.join(base_dir, fname)
2210
2311
  if os.path.isfile(fpath):
2211
2312
  recovery_files.append(fpath)
2212
- # 也检查 base_dir 本身(有些 Firefox 版本)
2213
- for fname in ("sessionstore.jsonlz4", "sessionstore-backups/recovery.jsonlz4"):
2214
- fpath = os.path.join(base_dir, fname)
2215
- if os.path.isfile(fpath):
2216
- recovery_files.append(fpath)
2313
+
2314
+ # 如果找到了文件,或者超时了,就跳出
2315
+ if recovery_files or (time.time() - _start_time) >= max_wait:
2316
+ break
2317
+
2318
+ # 等 0.5 秒再试
2319
+ time.sleep(0.5)
2217
2320
 
2218
2321
  if not recovery_files:
2219
- logger.debug("[_firefox_read_sessionstore] 未找到 sessionstore 文件")
2322
+ logger.warning(
2323
+ f"[_firefox_read_sessionstore] 未找到 sessionstore 文件 "
2324
+ f"(搜索了 {len(search_dirs)} 个目录: {[d.replace(os.path.expanduser('~'), '~') for d in search_dirs]})"
2325
+ )
2220
2326
  return result_info
2221
2327
 
2222
2328
  # 读取 mozLz4 格式
@@ -2290,9 +2396,19 @@ class StealthBrowser:
2290
2396
  screenshot_path = ""
2291
2397
  if screenshot_result.success and screenshot_result.data:
2292
2398
  screenshot_path = screenshot_result.data.get("path", "")
2399
+ logger.info(f"[_firefox_get_content] 截图成功: {screenshot_path}")
2400
+ else:
2401
+ logger.warning(
2402
+ f"[_firefox_get_content] 截图失败: "
2403
+ f"{screenshot_result.error or '无数据'}"
2404
+ )
2293
2405
 
2294
2406
  # 2. 读取 sessionstore
2295
2407
  session_info = self._firefox_read_sessionstore()
2408
+ logger.info(
2409
+ f"[_firefox_get_content] sessionstore: url={session_info.get('url', '')}, "
2410
+ f"title={session_info.get('title', '')}, tabs={len(session_info.get('tabs', []))}"
2411
+ )
2296
2412
 
2297
2413
  # 3. 组合返回信息
2298
2414
  url = session_info.get("url", "")
@@ -2315,7 +2431,17 @@ class StealthBrowser:
2315
2431
  if screenshot_path:
2316
2432
  content_parts.append(f"截图: {screenshot_path}")
2317
2433
 
2318
- content_text = "\n".join(content_parts) if content_parts else "无法获取页面内容"
2434
+ if not content_parts:
2435
+ # 全部失败 → 至少给截图失败原因
2436
+ fallback_msg = "Firefox+VNC 无法获取页面内容"
2437
+ if not screenshot_result.success:
2438
+ fallback_msg += f" (截图失败: {screenshot_result.error})"
2439
+ if not session_info.get("url"):
2440
+ fallback_msg += " (sessionstore 未读取到 URL)"
2441
+ logger.error(f"[_firefox_get_content] {fallback_msg}")
2442
+ content_text = fallback_msg
2443
+ else:
2444
+ content_text = "\n".join(content_parts)
2319
2445
 
2320
2446
  return SkillResult(
2321
2447
  success=True,
@@ -189,55 +189,27 @@ def _ensure_display() -> Optional[str]:
189
189
  logger.info(f"VNC 远程桌面已在运行,复用显示: {display}")
190
190
  return display
191
191
 
192
- # [v1.47.13] VNC 未运行 → 尝试自动启动
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
- import concurrent.futures
199
-
200
- def _start_vnc_in_thread():
201
- try:
202
- return asyncio.run(vnc.start())
203
- except Exception as e:
204
- return {"success": False, "message": str(e)}
205
-
206
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
207
- future = pool.submit(_start_vnc_in_thread)
208
-
209
- max_wait = 60
210
- check_interval = 2
211
- elapsed = 0
212
- while elapsed < max_wait:
213
- if vnc.is_running:
214
- display = vnc.display
215
- os.environ["DISPLAY"] = display
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: