myagent-ai 1.47.19 → 1.47.20

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.
@@ -924,6 +924,24 @@ class MainAgent(BaseAgent):
924
924
 
925
925
  messages.append(Message(role="system", content=_system_content))
926
926
 
927
+ # [v1.47.20] VNC 模式下注入浏览器工具使用提示
928
+ try:
929
+ from core.vnc_manager import get_vnc_manager
930
+ vnc_mgr = get_vnc_manager()
931
+ if vnc_mgr.is_running:
932
+ vnc_hint = (
933
+ "\n\n## VNC 远程桌面模式提示\n"
934
+ "当前运行在 VNC 远程桌面环境,浏览器为 Firefox(不支持 Chromium/CDP)。\n"
935
+ "- **网页浏览**: 优先使用 stealth_browser_start → stealth_browser_navigate → stealth_browser_content\n"
936
+ "- **获取页面内容**: stealth_browser_content(返回截图+标签页信息),不要使用 browser_open\n"
937
+ "- **交互操作**: stealth_browser_click / stealth_browser_fill / stealth_browser_key\n"
938
+ "- **不要使用**: browser_open(需要 Chromium)、web_control(需要前端面板)\n"
939
+ "- **不要关闭 Firefox**: stealth_browser_close 在 VNC 模式下只释放会话,不关闭浏览器"
940
+ )
941
+ messages[0] = Message(role="system", content=messages[0].content + vnc_hint)
942
+ except (ImportError, Exception):
943
+ pass
944
+
927
945
  # 注入对话历史
928
946
  if conversation_history:
929
947
  _history_budget = int(self.context_builder.context_window * 0.25) if self.context_builder else 50000
@@ -956,10 +956,15 @@ class StealthBrowser:
956
956
  """关闭浏览器"""
957
957
  self._started = False
958
958
 
959
- # [v1.47.16] Firefox+VNC 模式
959
+ # [v1.47.20] Firefox+VNC 模式:VNC 桌面的浏览器不能杀,只清理内部状态
960
960
  if self._firefox_mode:
961
961
  try:
962
- if self._firefox_process and self._firefox_process.poll() is None:
962
+ if self._vnc_used:
963
+ # VNC 模式:Firefox 由 vnc_manager 管理,不能杀进程
964
+ # 只清理本实例的内部状态
965
+ logger.info("Firefox+VNC 模式: 不关闭 VNC 浏览器(由 vnc_manager 管理),仅释放内部状态")
966
+ elif self._firefox_process and self._firefox_process.poll() is None:
967
+ # 非 VNC 模式(独立启动的 Firefox):可以关闭
963
968
  self._firefox_process.terminate()
964
969
  try:
965
970
  self._firefox_process.wait(timeout=5)
@@ -970,19 +975,15 @@ class StealthBrowser:
970
975
  pass
971
976
  logger.info("Firefox 已关闭")
972
977
  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
978
+ # 可能是复用的非 VNC Firefox 进程
979
+ logger.info("Firefox 进程非本实例启动,跳过关闭")
979
980
  except Exception as e:
980
981
  logger.error(f"关闭 Firefox 异常: {e}")
981
982
  finally:
982
983
  self._firefox_process = None
983
984
  self._firefox_mode = False
984
985
  self._vnc_used = False
985
- return SkillResult(success=True, message="Firefox 已关闭")
986
+ return SkillResult(success=True, message="浏览器会话已释放(VNC 浏览器保持运行)")
986
987
 
987
988
  try:
988
989
  if self._browser:
@@ -1475,11 +1476,13 @@ class StealthBrowser:
1475
1476
  if not self._ensure_page():
1476
1477
  return SkillResult(success=False, error="浏览器未启动")
1477
1478
 
1478
- # [v1.47.16] Firefox+VNC 模式:无法通过 CDP 执行 JS
1479
+ # [v1.47.20] Firefox+VNC 模式:无法通过 CDP 执行 JS
1479
1480
  if self._firefox_mode:
1480
1481
  return SkillResult(
1481
1482
  success=False,
1482
- error="Firefox+VNC 模式下不支持 JS 执行。请在 VNC 中手动操作,或切换到桌面环境使用 Chromium。",
1483
+ error="Firefox+VNC 模式下不支持 JS 执行。请使用 stealth_browser_navigate "
1484
+ "导航页面,用 stealth_browser_content(截图+标签页信息)获取内容,"
1485
+ "用 xdotool 相关操作(click/fill/key)进行交互。",
1483
1486
  )
1484
1487
 
1485
1488
  try:
@@ -1538,12 +1541,9 @@ class StealthBrowser:
1538
1541
  if not self._ensure_page():
1539
1542
  return SkillResult(success=False, error="浏览器未启动")
1540
1543
 
1541
- # [v1.47.16] Firefox+VNC 模式:无法获取页面内容
1544
+ # [v1.47.20] Firefox+VNC 模式:截图 + sessionstore 读取
1542
1545
  if self._firefox_mode:
1543
- return SkillResult(
1544
- success=False,
1545
- error="Firefox+VNC 模式下不支持获取页面内容。请在 VNC 中手动查看,或切换到桌面环境使用 Chromium。",
1546
- )
1546
+ return self._firefox_get_content()
1547
1547
 
1548
1548
  try:
1549
1549
  # Bug Fix: DrissionPage 没有 page.text 属性
@@ -1583,12 +1583,9 @@ class StealthBrowser:
1583
1583
  if not self._ensure_page():
1584
1584
  return SkillResult(success=False, error="浏览器未启动")
1585
1585
 
1586
- # [v1.47.16] Firefox+VNC 模式:无法获取页面 HTML
1586
+ # [v1.47.20] Firefox+VNC 模式:截图 + sessionstore 替代
1587
1587
  if self._firefox_mode:
1588
- return SkillResult(
1589
- success=False,
1590
- error="Firefox+VNC 模式下不支持获取页面 HTML。请在 VNC 中手动查看,或切换到桌面环境使用 Chromium。",
1591
- )
1588
+ return self._firefox_get_content()
1592
1589
 
1593
1590
  try:
1594
1591
  html = self._page.html or ""
@@ -1614,11 +1611,17 @@ class StealthBrowser:
1614
1611
  if not self._ensure_page():
1615
1612
  return SkillResult(success=False, error="浏览器未启动")
1616
1613
 
1617
- # [v1.47.16] Firefox+VNC 模式:无法等待元素
1614
+ # [v1.47.20] Firefox+VNC 模式:sleep + 截图替代
1618
1615
  if self._firefox_mode:
1616
+ await asyncio.sleep(min(timeout, 5))
1617
+ # 等待后截图,确认页面状态
1618
+ session_info = self._firefox_read_sessionstore()
1619
+ url = session_info.get("url", "")
1620
+ title = session_info.get("title", "")
1619
1621
  return SkillResult(
1620
- success=False,
1621
- error="Firefox+VNC 模式下不支持等待元素。请在 VNC 中手动操作。",
1622
+ success=True,
1623
+ message=f"Firefox+VNC: 已等待 {min(timeout, 5)}秒,当前页面: {title or url}",
1624
+ data={"url": url, "title": title},
1622
1625
  )
1623
1626
 
1624
1627
  try:
@@ -2174,6 +2177,162 @@ class StealthBrowser:
2174
2177
  except Exception as e:
2175
2178
  return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
2176
2179
 
2180
+ def _firefox_read_sessionstore(self) -> dict:
2181
+ """[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
2182
+
2183
+ Firefox 运行时会将当前会话信息写入 sessionstore-backups/recovery.jsonlz4。
2184
+ 该文件使用 mozLz4 格式(8字节自定义头 + LZ4 压缩数据)。
2185
+
2186
+ Returns:
2187
+ dict: {"url": str, "title": str, "tabs": [{"url": str, "title": str}]}
2188
+ """
2189
+ result_info = {"url": "", "title": "", "tabs": []}
2190
+
2191
+ # 搜索可能的 sessionstore 路径
2192
+ search_dirs = []
2193
+ if self._firefox_profile_dir:
2194
+ search_dirs.append(self._firefox_profile_dir)
2195
+ # 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
2196
+ for extra in [
2197
+ os.path.expanduser("~/.mozilla/firefox/default"),
2198
+ os.path.expanduser("~/.mozilla/firefox"),
2199
+ ]:
2200
+ if os.path.isdir(extra) and extra not in search_dirs:
2201
+ search_dirs.append(extra)
2202
+
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)
2210
+ if os.path.isfile(fpath):
2211
+ 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)
2217
+
2218
+ if not recovery_files:
2219
+ logger.debug("[_firefox_read_sessionstore] 未找到 sessionstore 文件")
2220
+ return result_info
2221
+
2222
+ # 读取 mozLz4 格式
2223
+ for fpath in recovery_files:
2224
+ try:
2225
+ import struct
2226
+ with open(fpath, "rb") as f:
2227
+ data = f.read()
2228
+ if len(data) < 8:
2229
+ continue
2230
+ magic = struct.unpack("<I", data[:4])[0]
2231
+ if magic != 0x00080000:
2232
+ continue
2233
+ orig_size = struct.unpack("<I", data[4:8])[0]
2234
+ try:
2235
+ import lz4.block
2236
+ decompressed = lz4.block.decompress(
2237
+ data[8:], uncompressed_size=orig_size
2238
+ )
2239
+ except ImportError:
2240
+ logger.debug("[_firefox_read_sessionstore] lz4 未安装,跳过 mozLz4 解析")
2241
+ continue
2242
+ except Exception as lz4_err:
2243
+ logger.debug(f"[_firefox_read_sessionstore] lz4 解压失败: {lz4_err}")
2244
+ continue
2245
+
2246
+ session = json.loads(decompressed)
2247
+ tabs_info = []
2248
+ for win in session.get("windows", []):
2249
+ for tab in win.get("tabs", []):
2250
+ idx = tab.get("index", 1) - 1
2251
+ entries = tab.get("entries", [])
2252
+ if entries and 0 <= idx < len(entries):
2253
+ entry = entries[idx]
2254
+ tab_info = {
2255
+ "url": entry.get("url", ""),
2256
+ "title": entry.get("title", ""),
2257
+ }
2258
+ tabs_info.append(tab_info)
2259
+
2260
+ result_info["tabs"] = tabs_info
2261
+ if tabs_info:
2262
+ # 取第一个标签页作为当前页面(通常是最活跃的)
2263
+ result_info["url"] = tabs_info[0]["url"]
2264
+ result_info["title"] = tabs_info[0]["title"]
2265
+
2266
+ logger.info(
2267
+ f"[_firefox_read_sessionstore] 读取成功: {len(tabs_info)} 个标签页, "
2268
+ f"当前: {result_info['title'][:50]}"
2269
+ )
2270
+ return result_info
2271
+
2272
+ except Exception as e:
2273
+ logger.debug(f"[_firefox_read_sessionstore] 读取 {fpath} 失败: {e}")
2274
+ continue
2275
+
2276
+ return result_info
2277
+
2278
+ def _firefox_get_content(self) -> SkillResult:
2279
+ """[v1.47.20] Firefox+VNC 模式下获取页面内容。
2280
+
2281
+ 由于无法通过 CDP 获取 Firefox 页面文本,采用以下策略:
2282
+ 1. 截取当前屏幕截图(供 VLM 视觉理解)
2283
+ 2. 读取 Firefox sessionstore 获取 URL/标题
2284
+ 3. 返回截图路径和基本元信息
2285
+
2286
+ Agent 可以使用截图进行视觉理解,或使用 web_search/web_read 获取页面文本。
2287
+ """
2288
+ # 1. 截图
2289
+ screenshot_result = self._firefox_screenshot()
2290
+ screenshot_path = ""
2291
+ if screenshot_result.success and screenshot_result.data:
2292
+ screenshot_path = screenshot_result.data.get("path", "")
2293
+
2294
+ # 2. 读取 sessionstore
2295
+ session_info = self._firefox_read_sessionstore()
2296
+
2297
+ # 3. 组合返回信息
2298
+ url = session_info.get("url", "")
2299
+ title = session_info.get("title", "")
2300
+ tabs = session_info.get("tabs", [])
2301
+ tabs_summary = ""
2302
+ if tabs:
2303
+ tabs_lines = []
2304
+ for i, tab in enumerate(tabs[:10]): # 最多10个标签页
2305
+ tabs_lines.append(f" [{i+1}] {tab.get('title', '?')} - {tab.get('url', '?')}")
2306
+ tabs_summary = "\n".join(tabs_lines)
2307
+
2308
+ content_parts = []
2309
+ if title:
2310
+ content_parts.append(f"标题: {title}")
2311
+ if url:
2312
+ content_parts.append(f"URL: {url}")
2313
+ if tabs_summary:
2314
+ content_parts.append(f"标签页:\n{tabs_summary}")
2315
+ if screenshot_path:
2316
+ content_parts.append(f"截图: {screenshot_path}")
2317
+
2318
+ content_text = "\n".join(content_parts) if content_parts else "无法获取页面内容"
2319
+
2320
+ return SkillResult(
2321
+ success=True,
2322
+ message=f"Firefox+VNC 页面信息: {title or url or '未知'}",
2323
+ data={
2324
+ "url": url,
2325
+ "title": title,
2326
+ "tabs": tabs,
2327
+ "screenshot_path": screenshot_path,
2328
+ "mode": "firefox_vnc",
2329
+ "note": "Firefox+VNC 模式无法直接获取页面文本,已提供截图和标签页信息。"
2330
+ "请使用截图进行视觉理解,或用 web_search/web_read 获取网页文本。",
2331
+ },
2332
+ files=[screenshot_path] if screenshot_path else [],
2333
+ output=content_text,
2334
+ )
2335
+
2177
2336
  def _firefox_get_cookies(self) -> SkillResult:
2178
2337
  """Firefox+VNC 模式下读取 cookies.sqlite。"""
2179
2338
  try:
@@ -3063,8 +3222,25 @@ class StealthBrowserCloseSkill(Skill):
3063
3222
  ]
3064
3223
 
3065
3224
  async def execute(self, profile: str = "", **kw) -> SkillResult:
3066
- # close_stealth_browser 现在是同步函数,直接调用
3225
+ # [v1.47.20] VNC 模式下不关闭 Firefox,只释放会话状态
3226
+ with _browser_lock:
3227
+ is_vnc = False
3228
+ if profile:
3229
+ for key, browser in _browsers.items():
3230
+ if key == profile or key == f"__system__:{profile}":
3231
+ if browser._vnc_used:
3232
+ is_vnc = True
3233
+ break
3234
+ else:
3235
+ is_vnc = any(b._vnc_used for b in _browsers.values())
3236
+
3067
3237
  close_stealth_browser(profile_name=profile)
3238
+
3239
+ if is_vnc:
3240
+ return SkillResult(
3241
+ success=True,
3242
+ message="VNC 模式: 浏览器会话已释放,Firefox 保持运行(VNC 远程桌面需要)",
3243
+ )
3068
3244
  return SkillResult(success=True, message="浏览器已关闭")
3069
3245
 
3070
3246
 
@@ -1372,6 +1372,26 @@ class BrowserOpenSkill(Skill):
1372
1372
  if not url:
1373
1373
  return SkillResult(success=False, error="缺少必需参数: url")
1374
1374
 
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
+
1375
1395
  # 检查依赖
1376
1396
  dep_err = await asyncio.get_event_loop().run_in_executor(None, _ensure_node_deps)
1377
1397
  if dep_err:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.47.19",
3
+ "version": "1.47.20",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {