|
@@ -10,9 +10,29 @@ Native Browser-Use Tools Adapter
|
|
|
2. 状态自动保持 - 登录状态、Cookie、LocalStorage 等
|
|
2. 状态自动保持 - 登录状态、Cookie、LocalStorage 等
|
|
|
3. 完整的底层访问 - 可以直接使用 CDP 协议
|
|
3. 完整的底层访问 - 可以直接使用 CDP 协议
|
|
|
4. 性能优异 - 避免频繁创建/销毁浏览器实例
|
|
4. 性能优异 - 避免频繁创建/销毁浏览器实例
|
|
|
|
|
+5. 多种浏览器类型 - 支持 local、cloud、container 三种模式
|
|
|
|
|
+
|
|
|
|
|
+支持的浏览器类型:
|
|
|
|
|
+1. Local (本地浏览器):
|
|
|
|
|
+ - 在本地运行 Chrome
|
|
|
|
|
+ - 支持可视化调试
|
|
|
|
|
+ - 速度最快
|
|
|
|
|
+ - 示例: init_browser_session(browser_type="local")
|
|
|
|
|
+
|
|
|
|
|
+2. Cloud (云浏览器):
|
|
|
|
|
+ - 在云端运行
|
|
|
|
|
+ - 不占用本地资源
|
|
|
|
|
+ - 适合生产环境
|
|
|
|
|
+ - 示例: init_browser_session(browser_type="cloud")
|
|
|
|
|
+
|
|
|
|
|
+3. Container (容器浏览器):
|
|
|
|
|
+ - 在独立容器中运行
|
|
|
|
|
+ - 隔离性好
|
|
|
|
|
+ - 支持预配置账户
|
|
|
|
|
+ - 示例: init_browser_session(browser_type="container", container_url="https://example.com")
|
|
|
|
|
|
|
|
使用方法:
|
|
使用方法:
|
|
|
-1. 在 Agent 初始化时调用 init_browser_session()
|
|
|
|
|
|
|
+1. 在 Agent 初始化时调用 init_browser_session() 并指定 browser_type
|
|
|
2. 使用各个工具函数执行浏览器操作
|
|
2. 使用各个工具函数执行浏览器操作
|
|
|
3. 任务结束时调用 cleanup_browser_session()
|
|
3. 任务结束时调用 cleanup_browser_session()
|
|
|
|
|
|
|
@@ -27,6 +47,7 @@ import sys
|
|
|
import os
|
|
import os
|
|
|
import json
|
|
import json
|
|
|
import asyncio
|
|
import asyncio
|
|
|
|
|
+import aiohttp
|
|
|
from typing import Optional, List, Dict, Any, Tuple
|
|
from typing import Optional, List, Dict, Any, Tuple
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from urllib.parse import urlparse
|
|
from urllib.parse import urlparse
|
|
@@ -44,6 +65,12 @@ from browser_use.tools.service import Tools
|
|
|
from browser_use.agent.views import ActionResult
|
|
from browser_use.agent.views import ActionResult
|
|
|
from browser_use.filesystem.file_system import FileSystem
|
|
from browser_use.filesystem.file_system import FileSystem
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+# ============================================================
|
|
|
|
|
+# 无需注册的内部辅助函数
|
|
|
|
|
+# ============================================================
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
# 全局浏览器会话管理
|
|
# 全局浏览器会话管理
|
|
|
# ============================================================
|
|
# ============================================================
|
|
@@ -53,52 +80,248 @@ _browser_session: Optional[BrowserSession] = None
|
|
|
_browser_tools: Optional[Tools] = None
|
|
_browser_tools: Optional[Tools] = None
|
|
|
_file_system: Optional[FileSystem] = None
|
|
_file_system: Optional[FileSystem] = None
|
|
|
|
|
|
|
|
|
|
+async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 创建浏览器容器并导航到指定URL
|
|
|
|
|
+
|
|
|
|
|
+ 按照 test.md 的要求:
|
|
|
|
|
+ 1.1 调用接口创建容器
|
|
|
|
|
+ 1.2 调用接口创建窗口并导航到URL
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ url: 要导航的URL地址
|
|
|
|
|
+ account_name: 账户名称
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 包含容器信息的字典:
|
|
|
|
|
+ - success: 是否成功
|
|
|
|
|
+ - container_id: 容器ID
|
|
|
|
|
+ - vnc: VNC访问URL
|
|
|
|
|
+ - cdp: CDP协议URL(用于浏览器连接)
|
|
|
|
|
+ - connection_id: 窗口连接ID
|
|
|
|
|
+ - error: 错误信息(如果失败)
|
|
|
|
|
+ """
|
|
|
|
|
+ result = {
|
|
|
|
|
+ "success": False,
|
|
|
|
|
+ "container_id": None,
|
|
|
|
|
+ "vnc": None,
|
|
|
|
|
+ "cdp": None,
|
|
|
|
|
+ "connection_id": None,
|
|
|
|
|
+ "error": None
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with aiohttp.ClientSession() as session:
|
|
|
|
|
+ # 步骤1.1: 创建容器
|
|
|
|
|
+ print("📦 步骤1.1: 创建容器...")
|
|
|
|
|
+ create_url = "http://47.84.182.56:8200/api/v1/container/create"
|
|
|
|
|
+ create_payload = {
|
|
|
|
|
+ "auto_remove": True,
|
|
|
|
|
+ "need_port_binding": True,
|
|
|
|
|
+ "max_lifetime_seconds": 900
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async with session.post(create_url, json=create_payload) as resp:
|
|
|
|
|
+ if resp.status != 200:
|
|
|
|
|
+ raise RuntimeError(f"创建容器失败: HTTP {resp.status}")
|
|
|
|
|
+
|
|
|
|
|
+ create_result = await resp.json()
|
|
|
|
|
+ if create_result.get("code") != 0:
|
|
|
|
|
+ raise RuntimeError(f"创建容器失败: {create_result.get('msg')}")
|
|
|
|
|
+
|
|
|
|
|
+ data = create_result.get("data", {})
|
|
|
|
|
+ result["container_id"] = data.get("container_id")
|
|
|
|
|
+ result["vnc"] = data.get("vnc")
|
|
|
|
|
+ result["cdp"] = data.get("cdp")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"✅ 容器创建成功")
|
|
|
|
|
+ print(f" Container ID: {result['container_id']}")
|
|
|
|
|
+ print(f" VNC: {result['vnc']}")
|
|
|
|
|
+ print(f" CDP: {result['cdp']}")
|
|
|
|
|
+
|
|
|
|
|
+ # 等待容器内的浏览器启动
|
|
|
|
|
+ print(f"\n⏳ 等待容器内浏览器启动...")
|
|
|
|
|
+ await asyncio.sleep(5)
|
|
|
|
|
+
|
|
|
|
|
+ # 步骤1.2: 创建页面并导航
|
|
|
|
|
+ print(f"\n📱 步骤1.2: 创建页面并导航到 {url}...")
|
|
|
|
|
+
|
|
|
|
|
+ page_create_url = "http://47.84.182.56:8200/api/v1/browser/page/create"
|
|
|
|
|
+ page_payload = {
|
|
|
|
|
+ "container_id": result["container_id"],
|
|
|
|
|
+ "url": url,
|
|
|
|
|
+ "account_name": account_name,
|
|
|
|
|
+ "need_wait": True,
|
|
|
|
|
+ "timeout": 30
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # 重试机制:最多尝试3次
|
|
|
|
|
+ max_retries = 3
|
|
|
|
|
+ page_created = False
|
|
|
|
|
+ last_error = None
|
|
|
|
|
+
|
|
|
|
|
+ for attempt in range(max_retries):
|
|
|
|
|
+ try:
|
|
|
|
|
+ if attempt > 0:
|
|
|
|
|
+ print(f" 重试 {attempt + 1}/{max_retries}...")
|
|
|
|
|
+ await asyncio.sleep(3) # 重试前等待
|
|
|
|
|
+
|
|
|
|
|
+ async with session.post(page_create_url, json=page_payload, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
|
|
|
|
+ if resp.status != 200:
|
|
|
|
|
+ response_text = await resp.text()
|
|
|
|
|
+ last_error = f"HTTP {resp.status}: {response_text[:200]}"
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ page_result = await resp.json()
|
|
|
|
|
+ if page_result.get("code") != 0:
|
|
|
|
|
+ last_error = f"{page_result.get('msg')}"
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ page_data = page_result.get("data", {})
|
|
|
|
|
+ result["connection_id"] = page_data.get("connection_id")
|
|
|
|
|
+ result["success"] = True
|
|
|
|
|
+ page_created = True
|
|
|
|
|
+
|
|
|
|
|
+ print(f"✅ 页面创建成功")
|
|
|
|
|
+ print(f" Connection ID: {result['connection_id']}")
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ except asyncio.TimeoutError:
|
|
|
|
|
+ last_error = "请求超时"
|
|
|
|
|
+ continue
|
|
|
|
|
+ except aiohttp.ClientError as e:
|
|
|
|
|
+ last_error = f"网络错误: {str(e)}"
|
|
|
|
|
+ continue
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ last_error = f"未知错误: {str(e)}"
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if not page_created:
|
|
|
|
|
+ raise RuntimeError(f"创建页面失败(尝试{max_retries}次后): {last_error}")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ result["error"] = str(e)
|
|
|
|
|
+ print(f"❌ 错误: {str(e)}")
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
|
|
|
async def init_browser_session(
|
|
async def init_browser_session(
|
|
|
|
|
+ browser_type: str = "local",
|
|
|
headless: bool = False,
|
|
headless: bool = False,
|
|
|
- user_data_dir: Optional[str] = None,
|
|
|
|
|
|
|
+ url: Optional[str] = None,
|
|
|
profile_name: str = "default",
|
|
profile_name: str = "default",
|
|
|
|
|
+ user_data_dir: Optional[str] = None,
|
|
|
browser_profile: Optional[BrowserProfile] = None,
|
|
browser_profile: Optional[BrowserProfile] = None,
|
|
|
- use_cloud: bool = False,
|
|
|
|
|
**kwargs
|
|
**kwargs
|
|
|
) -> tuple[BrowserSession, Tools]:
|
|
) -> tuple[BrowserSession, Tools]:
|
|
|
"""
|
|
"""
|
|
|
- 初始化全局浏览器会话
|
|
|
|
|
|
|
+ 初始化全局浏览器会话 - 支持三种浏览器类型
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
|
|
+ browser_type: 浏览器类型 ("local", "cloud", "container")
|
|
|
headless: 是否无头模式
|
|
headless: 是否无头模式
|
|
|
- user_data_dir: 用户数据目录(用于保存登录状态)
|
|
|
|
|
- profile_name: 配置文件名称
|
|
|
|
|
- browser_profile: BrowserProfile 对象(用于预设 cookies 等)
|
|
|
|
|
- use_cloud: 是否使用云浏览器(默认 False,使用本地浏览器)
|
|
|
|
|
|
|
+ url: 初始访问URL(可选)
|
|
|
|
|
+ - local/cloud: 初始化后会自动导航到此URL
|
|
|
|
|
+ - container: 必需,容器启动时访问的URL
|
|
|
|
|
+ profile_name: 配置文件/账户名称(默认 "default")
|
|
|
|
|
+ - local: 用于创建用户数据目录路径
|
|
|
|
|
+ - cloud: 云浏览器配置ID
|
|
|
|
|
+ - container: 容器账户名称
|
|
|
|
|
+ user_data_dir: 用户数据目录(仅 local 模式,高级用法)
|
|
|
|
|
+ 如果提供则覆盖 profile_name 生成的路径
|
|
|
|
|
+ browser_profile: BrowserProfile 对象(通用,高级用法)
|
|
|
|
|
+ 用于预设 cookies 等
|
|
|
**kwargs: 其他 BrowserSession 参数
|
|
**kwargs: 其他 BrowserSession 参数
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
(BrowserSession, Tools) 元组
|
|
(BrowserSession, Tools) 元组
|
|
|
|
|
+
|
|
|
|
|
+ Examples:
|
|
|
|
|
+ # 本地浏览器
|
|
|
|
|
+ browser, tools = await init_browser_session(
|
|
|
|
|
+ browser_type="local",
|
|
|
|
|
+ url="https://www.baidu.com" # 可选
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 云浏览器
|
|
|
|
|
+ browser, tools = await init_browser_session(
|
|
|
|
|
+ browser_type="cloud",
|
|
|
|
|
+ profile_name="my_cloud_profile" # 可选
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 容器浏览器
|
|
|
|
|
+ browser, tools = await init_browser_session(
|
|
|
|
|
+ browser_type="container",
|
|
|
|
|
+ url="https://www.xiaohongshu.com", # 必需
|
|
|
|
|
+ profile_name="my_account" # 可选
|
|
|
|
|
+ )
|
|
|
"""
|
|
"""
|
|
|
global _browser_session, _browser_tools, _file_system
|
|
global _browser_session, _browser_tools, _file_system
|
|
|
|
|
|
|
|
if _browser_session is not None:
|
|
if _browser_session is not None:
|
|
|
return _browser_session, _browser_tools
|
|
return _browser_session, _browser_tools
|
|
|
|
|
|
|
|
- # 设置用户数据目录(持久化登录状态)
|
|
|
|
|
- if user_data_dir is None and profile_name and not use_cloud:
|
|
|
|
|
- user_data_dir = str(Path.home() / ".browser_use" / "profiles" / profile_name)
|
|
|
|
|
- Path(user_data_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
+ # 验证 browser_type
|
|
|
|
|
+ valid_types = ["local", "cloud", "container"]
|
|
|
|
|
+ if browser_type not in valid_types:
|
|
|
|
|
+ raise ValueError(f"无效的 browser_type: {browser_type},必须是 {valid_types} 之一")
|
|
|
|
|
|
|
|
- # 创建浏览器会话
|
|
|
|
|
|
|
+ # 创建浏览器会话参数
|
|
|
session_params = {
|
|
session_params = {
|
|
|
"headless": headless,
|
|
"headless": headless,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if use_cloud:
|
|
|
|
|
- # 云浏览器模式
|
|
|
|
|
- session_params["use_cloud"] = True
|
|
|
|
|
|
|
+ # === Container 模式 ===
|
|
|
|
|
+ if browser_type == "container":
|
|
|
|
|
+ print("🐳 使用容器浏览器模式")
|
|
|
|
|
+
|
|
|
|
|
+ # container 模式必须提供 URL
|
|
|
|
|
+ if not url:
|
|
|
|
|
+ url = "about:blank" # 使用默认空白页
|
|
|
|
|
+ print("⚠️ 未提供 url 参数,使用默认空白页")
|
|
|
|
|
+
|
|
|
|
|
+ # 创建容器并获取 CDP URL
|
|
|
|
|
+ print(f"📦 正在创建容器...")
|
|
|
|
|
+ container_info = await create_container(
|
|
|
|
|
+ url=url,
|
|
|
|
|
+ account_name=profile_name
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if not container_info["success"]:
|
|
|
|
|
+ raise RuntimeError(f"容器创建失败: {container_info['error']}")
|
|
|
|
|
+
|
|
|
|
|
+ cdp_url = container_info["cdp"]
|
|
|
|
|
+ print(f"✅ 容器创建成功")
|
|
|
|
|
+ print(f" CDP URL: {cdp_url}")
|
|
|
|
|
+ print(f" Container ID: {container_info['container_id']}")
|
|
|
|
|
+ print(f" Connection ID: {container_info.get('connection_id')}")
|
|
|
|
|
+
|
|
|
|
|
+ # 使用容器的 CDP URL 连接
|
|
|
|
|
+ session_params["cdp_url"] = cdp_url
|
|
|
|
|
+
|
|
|
|
|
+ # 等待容器完全启动
|
|
|
|
|
+ print("⏳ 等待容器浏览器启动...")
|
|
|
|
|
+ await asyncio.sleep(3)
|
|
|
|
|
+
|
|
|
|
|
+ # === Cloud 模式 ===
|
|
|
|
|
+ elif browser_type == "cloud":
|
|
|
print("🌐 使用云浏览器模式")
|
|
print("🌐 使用云浏览器模式")
|
|
|
- else:
|
|
|
|
|
- # 本地浏览器模式
|
|
|
|
|
|
|
+ session_params["use_cloud"] = True
|
|
|
|
|
+
|
|
|
|
|
+ # profile_name 作为云配置ID
|
|
|
|
|
+ if profile_name and profile_name != "default":
|
|
|
|
|
+ session_params["cloud_profile_id"] = profile_name
|
|
|
|
|
+
|
|
|
|
|
+ # === Local 模式 ===
|
|
|
|
|
+ else: # local
|
|
|
|
|
+ print("💻 使用本地浏览器模式")
|
|
|
session_params["is_local"] = True
|
|
session_params["is_local"] = True
|
|
|
|
|
|
|
|
|
|
+ # 设置用户数据目录(持久化登录状态)
|
|
|
|
|
+ if user_data_dir is None and profile_name:
|
|
|
|
|
+ user_data_dir = str(Path.home() / ".browser_use" / "profiles" / profile_name)
|
|
|
|
|
+ Path(user_data_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+
|
|
|
# macOS 上显式指定 Chrome 路径
|
|
# macOS 上显式指定 Chrome 路径
|
|
|
import platform
|
|
import platform
|
|
|
if platform.system() == "Darwin": # macOS
|
|
if platform.system() == "Darwin": # macOS
|
|
@@ -110,13 +333,14 @@ async def init_browser_session(
|
|
|
if user_data_dir:
|
|
if user_data_dir:
|
|
|
session_params["user_data_dir"] = user_data_dir
|
|
session_params["user_data_dir"] = user_data_dir
|
|
|
|
|
|
|
|
- # 只在有值时才添加 browser_profile
|
|
|
|
|
|
|
+ # 只在有值时才添加 browser_profile (适用于所有模式)
|
|
|
if browser_profile:
|
|
if browser_profile:
|
|
|
session_params["browser_profile"] = browser_profile
|
|
session_params["browser_profile"] = browser_profile
|
|
|
|
|
|
|
|
# 合并其他参数
|
|
# 合并其他参数
|
|
|
session_params.update(kwargs)
|
|
session_params.update(kwargs)
|
|
|
|
|
|
|
|
|
|
+ # 创建浏览器会话
|
|
|
_browser_session = BrowserSession(**session_params)
|
|
_browser_session = BrowserSession(**session_params)
|
|
|
|
|
|
|
|
# 启动浏览器
|
|
# 启动浏览器
|
|
@@ -132,6 +356,13 @@ async def init_browser_session(
|
|
|
base_dir.mkdir(parents=True, exist_ok=True)
|
|
base_dir.mkdir(parents=True, exist_ok=True)
|
|
|
_file_system = FileSystem(base_dir=str(base_dir))
|
|
_file_system = FileSystem(base_dir=str(base_dir))
|
|
|
|
|
|
|
|
|
|
+ print("✅ 浏览器会话初始化成功")
|
|
|
|
|
+
|
|
|
|
|
+ # 如果是 local 或 cloud 模式且提供了 URL,导航到该 URL
|
|
|
|
|
+ if browser_type in ["local", "cloud"] and url:
|
|
|
|
|
+ print(f"🔗 导航到: {url}")
|
|
|
|
|
+ await _browser_tools.navigate(url=url, browser_session=_browser_session)
|
|
|
|
|
+
|
|
|
return _browser_session, _browser_tools
|
|
return _browser_session, _browser_tools
|
|
|
|
|
|
|
|
|
|
|
|
@@ -318,12 +549,16 @@ def _fetch_profile_id(cookie_type: str) -> Optional[str]:
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ============================================================
|
|
|
|
|
+# 需要注册的工具
|
|
|
|
|
+# ============================================================
|
|
|
|
|
+
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
# 导航类工具 (Navigation Tools)
|
|
# 导航类工具 (Navigation Tools)
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
导航到指定的 URL
|
|
导航到指定的 URL
|
|
|
Navigate to a specific URL
|
|
Navigate to a specific URL
|
|
@@ -363,7 +598,7 @@ async def navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def search_web(query: str, engine: str = "google") -> ToolResult:
|
|
|
|
|
|
|
+async def browser_search_web(query: str, engine: str = "google") -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
使用搜索引擎搜索
|
|
使用搜索引擎搜索
|
|
|
Search the web using a search engine
|
|
Search the web using a search engine
|
|
@@ -400,7 +635,7 @@ async def search_web(query: str, engine: str = "google") -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def go_back() -> ToolResult:
|
|
|
|
|
|
|
+async def browser_go_back() -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
返回到上一个页面
|
|
返回到上一个页面
|
|
|
Go back to the previous page
|
|
Go back to the previous page
|
|
@@ -427,7 +662,7 @@ async def go_back() -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def wait(seconds: int = 3) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_wait(seconds: int = 3) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
等待指定的秒数
|
|
等待指定的秒数
|
|
|
Wait for a specified number of seconds
|
|
Wait for a specified number of seconds
|
|
@@ -464,7 +699,7 @@ async def wait(seconds: int = 3) -> ToolResult:
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def click_element(index: int) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_click_element(index: int) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
通过索引点击页面元素
|
|
通过索引点击页面元素
|
|
|
Click an element by index
|
|
Click an element by index
|
|
@@ -501,7 +736,7 @@ async def click_element(index: int) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def input_text(index: int, text: str, clear: bool = True) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_input_text(index: int, text: str, clear: bool = True) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
在指定元素中输入文本
|
|
在指定元素中输入文本
|
|
|
Input text into an element
|
|
Input text into an element
|
|
@@ -539,7 +774,7 @@ async def input_text(index: int, text: str, clear: bool = True) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def send_keys(keys: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_send_keys(keys: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
发送键盘按键或快捷键
|
|
发送键盘按键或快捷键
|
|
|
Send keyboard keys or shortcuts
|
|
Send keyboard keys or shortcuts
|
|
@@ -579,7 +814,7 @@ async def send_keys(keys: str) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def upload_file(index: int, path: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_upload_file(index: int, path: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
上传文件到文件输入元素
|
|
上传文件到文件输入元素
|
|
|
Upload a file to a file input element
|
|
Upload a file to a file input element
|
|
@@ -624,7 +859,7 @@ async def upload_file(index: int, path: str) -> ToolResult:
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def scroll_page(down: bool = True, pages: float = 1.0,
|
|
|
|
|
|
|
+async def browser_scroll_page(down: bool = True, pages: float = 1.0,
|
|
|
index: Optional[int] = None) -> ToolResult:
|
|
index: Optional[int] = None) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
滚动页面或元素
|
|
滚动页面或元素
|
|
@@ -665,7 +900,7 @@ async def scroll_page(down: bool = True, pages: float = 1.0,
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def find_text(text: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_find_text(text: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
查找页面中的文本并滚动到该位置
|
|
查找页面中的文本并滚动到该位置
|
|
|
Find text on the page and scroll to it
|
|
Find text on the page and scroll to it
|
|
@@ -701,7 +936,7 @@ async def find_text(text: str) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def screenshot() -> ToolResult:
|
|
|
|
|
|
|
+async def browser_screenshot() -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
请求在下次观察中包含页面截图
|
|
请求在下次观察中包含页面截图
|
|
|
Request a screenshot to be included in the next observation
|
|
Request a screenshot to be included in the next observation
|
|
@@ -738,7 +973,7 @@ async def screenshot() -> ToolResult:
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def switch_tab(tab_id: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_switch_tab(tab_id: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
切换到指定标签页
|
|
切换到指定标签页
|
|
|
Switch to a different browser tab
|
|
Switch to a different browser tab
|
|
@@ -773,7 +1008,7 @@ async def switch_tab(tab_id: str) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def close_tab(tab_id: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_close_tab(tab_id: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
关闭指定标签页
|
|
关闭指定标签页
|
|
|
Close a browser tab
|
|
Close a browser tab
|
|
@@ -812,7 +1047,7 @@ async def close_tab(tab_id: str) -> ToolResult:
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def get_dropdown_options(index: int) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_get_dropdown_options(index: int) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
获取下拉框的所有选项
|
|
获取下拉框的所有选项
|
|
|
Get options from a dropdown element
|
|
Get options from a dropdown element
|
|
@@ -846,7 +1081,7 @@ async def get_dropdown_options(index: int) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def select_dropdown_option(index: int, text: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_select_dropdown_option(index: int, text: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
选择下拉框选项
|
|
选择下拉框选项
|
|
|
Select an option from a dropdown
|
|
Select an option from a dropdown
|
|
@@ -886,7 +1121,7 @@ async def select_dropdown_option(index: int, text: str) -> ToolResult:
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def extract_content(query: str, extract_links: bool = False,
|
|
|
|
|
|
|
+async def browser_extract_content(query: str, extract_links: bool = False,
|
|
|
start_from_char: int = 0) -> ToolResult:
|
|
start_from_char: int = 0) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
使用 LLM 从页面提取结构化数据
|
|
使用 LLM 从页面提取结构化数据
|
|
@@ -934,7 +1169,7 @@ async def extract_content(query: str, extract_links: bool = False,
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def get_page_html() -> ToolResult:
|
|
|
|
|
|
|
+async def browser_get_page_html() -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
获取当前页面的完整 HTML
|
|
获取当前页面的完整 HTML
|
|
|
Get the full HTML of the current page
|
|
Get the full HTML of the current page
|
|
@@ -996,7 +1231,7 @@ async def get_page_html() -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def get_selector_map() -> ToolResult:
|
|
|
|
|
|
|
+async def browser_get_selector_map() -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
获取当前页面的元素索引映射
|
|
获取当前页面的元素索引映射
|
|
|
Get the selector map of interactive elements on the current page
|
|
Get the selector map of interactive elements on the current page
|
|
@@ -1052,7 +1287,7 @@ async def get_selector_map() -> ToolResult:
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def evaluate(code: str) -> ToolResult:
|
|
|
|
|
|
|
+async def browser_evaluate(code: str) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
在页面中执行 JavaScript 代码
|
|
在页面中执行 JavaScript 代码
|
|
|
Execute JavaScript code in the page context
|
|
Execute JavaScript code in the page context
|
|
@@ -1094,7 +1329,7 @@ async def evaluate(code: str) -> ToolResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xiaohongshu.com") -> ToolResult:
|
|
|
|
|
|
|
+async def browser_ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xiaohongshu.com") -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
检查登录状态并在需要时注入 cookies
|
|
检查登录状态并在需要时注入 cookies
|
|
|
"""
|
|
"""
|
|
@@ -1190,7 +1425,7 @@ async def ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xi
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def wait_for_user_action(message: str = "Please complete the action in browser",
|
|
|
|
|
|
|
+async def browser_wait_for_user_action(message: str = "Please complete the action in browser",
|
|
|
timeout: int = 300) -> ToolResult:
|
|
timeout: int = 300) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
等待用户在浏览器中完成操作(如登录)
|
|
等待用户在浏览器中完成操作(如登录)
|
|
@@ -1262,7 +1497,7 @@ async def wait_for_user_action(message: str = "Please complete the action in bro
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
@tool()
|
|
@tool()
|
|
|
-async def done(text: str, success: bool = True,
|
|
|
|
|
|
|
+async def browser_done(text: str, success: bool = True,
|
|
|
files_to_display: Optional[List[str]] = None) -> ToolResult:
|
|
files_to_display: Optional[List[str]] = None) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
|
标记任务完成并返回最终消息
|
|
标记任务完成并返回最终消息
|
|
@@ -1300,138 +1535,6 @@ async def done(text: str, success: bool = True,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-# ============================================================
|
|
|
|
|
-# 容器管理工具 (Container Management Tools)
|
|
|
|
|
-# ============================================================
|
|
|
|
|
-
|
|
|
|
|
-import aiohttp
|
|
|
|
|
-
|
|
|
|
|
-async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
|
|
|
|
|
- """
|
|
|
|
|
- 创建浏览器容器并导航到指定URL
|
|
|
|
|
-
|
|
|
|
|
- 按照 test.md 的要求:
|
|
|
|
|
- 1.1 调用接口创建容器
|
|
|
|
|
- 1.2 调用接口创建窗口并导航到URL
|
|
|
|
|
-
|
|
|
|
|
- Args:
|
|
|
|
|
- url: 要导航的URL地址
|
|
|
|
|
- account_name: 账户名称
|
|
|
|
|
-
|
|
|
|
|
- Returns:
|
|
|
|
|
- 包含容器信息的字典:
|
|
|
|
|
- - success: 是否成功
|
|
|
|
|
- - container_id: 容器ID
|
|
|
|
|
- - vnc: VNC访问URL
|
|
|
|
|
- - cdp: CDP协议URL(用于浏览器连接)
|
|
|
|
|
- - connection_id: 窗口连接ID
|
|
|
|
|
- - error: 错误信息(如果失败)
|
|
|
|
|
- """
|
|
|
|
|
- result = {
|
|
|
|
|
- "success": False,
|
|
|
|
|
- "container_id": None,
|
|
|
|
|
- "vnc": None,
|
|
|
|
|
- "cdp": None,
|
|
|
|
|
- "connection_id": None,
|
|
|
|
|
- "error": None
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try:
|
|
|
|
|
- async with aiohttp.ClientSession() as session:
|
|
|
|
|
- # 步骤1.1: 创建容器
|
|
|
|
|
- print("📦 步骤1.1: 创建容器...")
|
|
|
|
|
- create_url = "http://47.84.182.56:8200/api/v1/container/create"
|
|
|
|
|
- create_payload = {
|
|
|
|
|
- "auto_remove": True,
|
|
|
|
|
- "need_port_binding": True,
|
|
|
|
|
- "max_lifetime_seconds": 900
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async with session.post(create_url, json=create_payload) as resp:
|
|
|
|
|
- if resp.status != 200:
|
|
|
|
|
- raise RuntimeError(f"创建容器失败: HTTP {resp.status}")
|
|
|
|
|
-
|
|
|
|
|
- create_result = await resp.json()
|
|
|
|
|
- if create_result.get("code") != 0:
|
|
|
|
|
- raise RuntimeError(f"创建容器失败: {create_result.get('msg')}")
|
|
|
|
|
-
|
|
|
|
|
- data = create_result.get("data", {})
|
|
|
|
|
- result["container_id"] = data.get("container_id")
|
|
|
|
|
- result["vnc"] = data.get("vnc")
|
|
|
|
|
- result["cdp"] = data.get("cdp")
|
|
|
|
|
-
|
|
|
|
|
- print(f"✅ 容器创建成功")
|
|
|
|
|
- print(f" Container ID: {result['container_id']}")
|
|
|
|
|
- print(f" VNC: {result['vnc']}")
|
|
|
|
|
- print(f" CDP: {result['cdp']}")
|
|
|
|
|
-
|
|
|
|
|
- # 等待容器内的浏览器启动
|
|
|
|
|
- print(f"\n⏳ 等待容器内浏览器启动...")
|
|
|
|
|
- await asyncio.sleep(5)
|
|
|
|
|
-
|
|
|
|
|
- # 步骤1.2: 创建页面并导航
|
|
|
|
|
- print(f"\n📱 步骤1.2: 创建页面并导航到 {url}...")
|
|
|
|
|
-
|
|
|
|
|
- page_create_url = "http://47.84.182.56:8200/api/v1/browser/page/create"
|
|
|
|
|
- page_payload = {
|
|
|
|
|
- "container_id": result["container_id"],
|
|
|
|
|
- "url": url,
|
|
|
|
|
- "account_name": account_name,
|
|
|
|
|
- "need_wait": True,
|
|
|
|
|
- "timeout": 30
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- # 重试机制:最多尝试3次
|
|
|
|
|
- max_retries = 3
|
|
|
|
|
- page_created = False
|
|
|
|
|
- last_error = None
|
|
|
|
|
-
|
|
|
|
|
- for attempt in range(max_retries):
|
|
|
|
|
- try:
|
|
|
|
|
- if attempt > 0:
|
|
|
|
|
- print(f" 重试 {attempt + 1}/{max_retries}...")
|
|
|
|
|
- await asyncio.sleep(3) # 重试前等待
|
|
|
|
|
-
|
|
|
|
|
- async with session.post(page_create_url, json=page_payload, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
|
|
|
|
- if resp.status != 200:
|
|
|
|
|
- response_text = await resp.text()
|
|
|
|
|
- last_error = f"HTTP {resp.status}: {response_text[:200]}"
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- page_result = await resp.json()
|
|
|
|
|
- if page_result.get("code") != 0:
|
|
|
|
|
- last_error = f"{page_result.get('msg')}"
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- page_data = page_result.get("data", {})
|
|
|
|
|
- result["connection_id"] = page_data.get("connection_id")
|
|
|
|
|
- result["success"] = True
|
|
|
|
|
- page_created = True
|
|
|
|
|
-
|
|
|
|
|
- print(f"✅ 页面创建成功")
|
|
|
|
|
- print(f" Connection ID: {result['connection_id']}")
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
- except asyncio.TimeoutError:
|
|
|
|
|
- last_error = "请求超时"
|
|
|
|
|
- continue
|
|
|
|
|
- except aiohttp.ClientError as e:
|
|
|
|
|
- last_error = f"网络错误: {str(e)}"
|
|
|
|
|
- continue
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- last_error = f"未知错误: {str(e)}"
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- if not page_created:
|
|
|
|
|
- raise RuntimeError(f"创建页面失败(尝试{max_retries}次后): {last_error}")
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- result["error"] = str(e)
|
|
|
|
|
- print(f"❌ 错误: {str(e)}")
|
|
|
|
|
-
|
|
|
|
|
- return result
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
# 导出所有工具函数(供外部使用)
|
|
# 导出所有工具函数(供外部使用)
|
|
|
# ============================================================
|
|
# ============================================================
|
|
@@ -1444,45 +1547,42 @@ __all__ = [
|
|
|
'kill_browser_session',
|
|
'kill_browser_session',
|
|
|
|
|
|
|
|
# 导航类工具
|
|
# 导航类工具
|
|
|
- 'navigate_to_url',
|
|
|
|
|
- 'search_web',
|
|
|
|
|
- 'go_back',
|
|
|
|
|
- 'wait',
|
|
|
|
|
|
|
+ 'browser_navigate_to_url',
|
|
|
|
|
+ 'browser_search_web',
|
|
|
|
|
+ 'browser_go_back',
|
|
|
|
|
+ 'browser_wait',
|
|
|
|
|
|
|
|
# 元素交互工具
|
|
# 元素交互工具
|
|
|
- 'click_element',
|
|
|
|
|
- 'input_text',
|
|
|
|
|
- 'send_keys',
|
|
|
|
|
- 'upload_file',
|
|
|
|
|
|
|
+ 'browser_click_element',
|
|
|
|
|
+ 'browser_input_text',
|
|
|
|
|
+ 'browser_send_keys',
|
|
|
|
|
+ 'browser_upload_file',
|
|
|
|
|
|
|
|
# 滚动和视图工具
|
|
# 滚动和视图工具
|
|
|
- 'scroll_page',
|
|
|
|
|
- 'find_text',
|
|
|
|
|
- 'screenshot',
|
|
|
|
|
|
|
+ 'browser_scroll_page',
|
|
|
|
|
+ 'browser_find_text',
|
|
|
|
|
+ 'browser_screenshot',
|
|
|
|
|
|
|
|
# 标签页管理工具
|
|
# 标签页管理工具
|
|
|
- 'switch_tab',
|
|
|
|
|
- 'close_tab',
|
|
|
|
|
|
|
+ 'browser_switch_tab',
|
|
|
|
|
+ 'browser_close_tab',
|
|
|
|
|
|
|
|
# 下拉框工具
|
|
# 下拉框工具
|
|
|
- 'get_dropdown_options',
|
|
|
|
|
- 'select_dropdown_option',
|
|
|
|
|
|
|
+ 'browser_get_dropdown_options',
|
|
|
|
|
+ 'browser_select_dropdown_option',
|
|
|
|
|
|
|
|
# 内容提取工具
|
|
# 内容提取工具
|
|
|
- 'extract_content',
|
|
|
|
|
- 'get_page_html',
|
|
|
|
|
- 'get_selector_map',
|
|
|
|
|
|
|
+ 'browser_extract_content',
|
|
|
|
|
+ 'browser_get_page_html',
|
|
|
|
|
+ 'browser_get_selector_map',
|
|
|
|
|
|
|
|
# JavaScript 执行工具
|
|
# JavaScript 执行工具
|
|
|
- 'evaluate',
|
|
|
|
|
- 'ensure_login_with_cookies',
|
|
|
|
|
|
|
+ 'browser_evaluate',
|
|
|
|
|
+ 'browser_ensure_login_with_cookies',
|
|
|
|
|
|
|
|
# 等待用户操作
|
|
# 等待用户操作
|
|
|
- 'wait_for_user_action',
|
|
|
|
|
|
|
+ 'browser_wait_for_user_action',
|
|
|
|
|
|
|
|
# 任务完成
|
|
# 任务完成
|
|
|
- 'done',
|
|
|
|
|
-
|
|
|
|
|
- # 容器管理
|
|
|
|
|
- 'create_container',
|
|
|
|
|
|
|
+ 'browser_done',
|
|
|
]
|
|
]
|