|
|
@@ -0,0 +1,334 @@
|
|
|
+# 浏览器自动化技术文档
|
|
|
+
|
|
|
+> agent 框架的浏览器操作模块:会话管理、工具体系、内容提取、Cookie 管理。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+1. [整体架构](#整体架构)
|
|
|
+2. [浏览器会话管理](#浏览器会话管理)
|
|
|
+3. [工具体系](#工具体系)
|
|
|
+4. [结果转换机制](#结果转换机制)
|
|
|
+5. [URL 清洗](#url-清洗)
|
|
|
+6. [文件存储](#文件存储)
|
|
|
+7. [Skill 集成](#skill-集成)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 整体架构
|
|
|
+
|
|
|
+浏览器操作的核心实现位于 `agent/tools/builtin/browser/`,采用适配器模式将第三方库 [browser-use](https://github.com/browser-use/browser-use) 的能力封装为 agent 框架的标准工具。
|
|
|
+
|
|
|
+```
|
|
|
+agent/tools/builtin/browser/
|
|
|
+├── __init__.py # 统一导出所有浏览器工具
|
|
|
+├── baseClass.py # 核心实现(~2200行):会话管理 + 27个工具函数
|
|
|
+└── sync_mysql_help.py # 同步 MySQL 辅助类(Cookie 查询)
|
|
|
+
|
|
|
+agent/skill/skills/
|
|
|
+└── browser.md # 浏览器工具使用指南(Skill 注入到 LLM system prompt)
|
|
|
+```
|
|
|
+
|
|
|
+关键依赖关系:
|
|
|
+
|
|
|
+```
|
|
|
+agent 框架 (@tool 装饰器, ToolResult)
|
|
|
+ ↓ 适配
|
|
|
+browser-use (BrowserSession, Tools, ActionResult)
|
|
|
+ ↓ 底层
|
|
|
+Chrome DevTools Protocol (CDP)
|
|
|
+```
|
|
|
+
|
|
|
+不直接依赖 Playwright,完全基于 CDP 协议与浏览器通信。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 浏览器会话管理
|
|
|
+
|
|
|
+### 全局单例模式
|
|
|
+
|
|
|
+使用模块级全局变量维护唯一的浏览器会话,避免重复创建/销毁浏览器实例:
|
|
|
+
|
|
|
+```python
|
|
|
+# baseClass.py:98-101
|
|
|
+_browser_session: Optional[BrowserSession] = None
|
|
|
+_browser_tools: Optional[Tools] = None
|
|
|
+_file_system: Optional[FileSystem] = None
|
|
|
+_last_browser_type: str = "local"
|
|
|
+_last_headless: bool = True
|
|
|
+_live_url: Optional[str] = None
|
|
|
+```
|
|
|
+
|
|
|
+### 三种浏览器模式
|
|
|
+
|
|
|
+通过 `init_browser_session(browser_type=...)` 初始化,支持三种运行模式:
|
|
|
+
|
|
|
+| 模式 | browser_type | 底层实现 | 适用场景 |
|
|
|
+|------|-------------|---------|---------|
|
|
|
+| 本地浏览器 | `"local"` | 启动本地 Chrome,通过 `user_data_dir` 持久化 profile | 开发调试,速度最快 |
|
|
|
+| 云浏览器 | `"cloud"` | 连接 browser-use 云服务,通过 `cdp_url` 远程控制 | 生产环境,不占本地资源 |
|
|
|
+| 容器浏览器 | `"container"` | 调用远程 API 创建 Docker 容器,内含 Chrome,通过 CDP 连接 | 隔离性好,支持预配置账户 |
|
|
|
+
|
|
|
+初始化流程(`baseClass.py:230-322`):
|
|
|
+
|
|
|
+```
|
|
|
+init_browser_session()
|
|
|
+ ├─ local: 检测 macOS Chrome 路径 → 创建 user_data_dir → BrowserSession(is_local=True)
|
|
|
+ ├─ cloud: BrowserSession(use_cloud=True) → 解析 cdp_url 生成 live_url
|
|
|
+ └─ container: create_container() API → 等待浏览器启动 → BrowserSession(cdp_url=...)
|
|
|
+```
|
|
|
+
|
|
|
+### 容器创建流程
|
|
|
+
|
|
|
+容器模式通过 HTTP API 与远程容器管理服务交互(`baseClass.py:105-228`):
|
|
|
+
|
|
|
+```
|
|
|
+步骤 1.1: POST /api/v1/container/create
|
|
|
+ → 返回 container_id, vnc, cdp 地址
|
|
|
+ → 等待 5 秒让容器内浏览器启动
|
|
|
+
|
|
|
+步骤 1.2: POST /api/v1/browser/page/create
|
|
|
+ → 传入 container_id, url, account_name
|
|
|
+ → 返回 connection_id
|
|
|
+ → 内置重试机制(最多 3 次)
|
|
|
+```
|
|
|
+
|
|
|
+### 会话健康检查与自动恢复
|
|
|
+
|
|
|
+`get_browser_session()`(`baseClass.py:330-372`)在每次工具调用前检查 CDP 连接是否存活:
|
|
|
+
|
|
|
+```python
|
|
|
+# 通过 CDP 执行 Runtime.evaluate('1+1') 探测连接
|
|
|
+cdp_session = await _browser_session.get_or_create_cdp_session()
|
|
|
+await asyncio.wait_for(
|
|
|
+ cdp_session.cdp_client.send.Runtime.evaluate(
|
|
|
+ params={'expression': '1+1'},
|
|
|
+ session_id=cdp_session.session_id
|
|
|
+ ),
|
|
|
+ timeout=3.0,
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+如果连接断开(WebSocket 超时等),自动 cleanup 并重新初始化。
|
|
|
+
|
|
|
+### 会话生命周期
|
|
|
+
|
|
|
+```
|
|
|
+init_browser_session() → 创建会话(幂等,已存在则直接返回)
|
|
|
+get_browser_session() → 获取会话(自动健康检查 + 重连)
|
|
|
+cleanup_browser_session() → 优雅停止(session.stop())
|
|
|
+kill_browser_session() → 强制终止(session.kill())
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 工具体系
|
|
|
+
|
|
|
+所有工具通过 `@tool()` 装饰器注册到 agent 框架的 `ToolRegistry`,LLM 可直接调用。共 27 个工具,分为 8 类。
|
|
|
+
|
|
|
+### 导航类(Navigation)
|
|
|
+
|
|
|
+| 工具 | 功能 | 底层调用 |
|
|
|
+|------|------|---------|
|
|
|
+| `browser_navigate_to_url(url, new_tab)` | 导航到 URL | `tools.navigate()` |
|
|
|
+| `browser_search_web(query, engine)` | 搜索引擎搜索(支持 bing/google/duckduckgo) | `tools.search()` |
|
|
|
+| `browser_go_back()` | 浏览器后退 | `tools.go_back()` |
|
|
|
+| `browser_wait(seconds)` | 等待指定秒数 | `tools.wait()` |
|
|
|
+| `browser_get_live_url()` | 获取云浏览器实时画面链接 | 读取全局 `_live_url` |
|
|
|
+
|
|
|
+### 元素交互类(Interaction)
|
|
|
+
|
|
|
+| 工具 | 功能 | 特殊处理 |
|
|
|
+|------|------|---------|
|
|
|
+| `browser_click_element(index)` | 点击元素 | 挂载日志监听器自动捕获下载链接 |
|
|
|
+| `browser_input_text(index, text, clear)` | 输入文本 | 支持清除已有内容 |
|
|
|
+| `browser_send_keys(keys)` | 发送键盘按键/快捷键 | 支持组合键如 `Control+A` |
|
|
|
+| `browser_upload_file(index, path)` | 上传文件 | 需要绝对路径 |
|
|
|
+
|
|
|
+`browser_click_element` 的下载链接捕获机制(`baseClass.py:711-846`):
|
|
|
+
|
|
|
+```python
|
|
|
+# 1. 挂载自定义 logging.Handler 到 browser_use 命名空间
|
|
|
+capture_handler = DownloadLinkCaptureHandler()
|
|
|
+logger = logging.getLogger("browser_use")
|
|
|
+logger.addHandler(capture_handler)
|
|
|
+
|
|
|
+# 2. 执行点击
|
|
|
+result = await tools.click(index=index, browser_session=browser)
|
|
|
+
|
|
|
+# 3. 检查是否捕获到下载链接(通过正则匹配日志中的 URL)
|
|
|
+if capture_handler.captured_url:
|
|
|
+ # 将链接注入到 ToolResult.output,LLM 可以看到并决定下一步
|
|
|
+```
|
|
|
+
|
|
|
+### 滚动与视图类(Scroll & View)
|
|
|
+
|
|
|
+| 工具 | 功能 | 特殊处理 |
|
|
|
+|------|------|---------|
|
|
|
+| `browser_scroll_page(down, pages, index)` | 滚动页面 | 通过 CDP 检测 scrollY 变化,判断是否到达边界;限制单次最大 10 页 |
|
|
|
+| `browser_find_text(text)` | 查找文本并滚动到位 | `tools.find_text()` |
|
|
|
+| `browser_screenshot()` | 截图 | `tools.screenshot()` |
|
|
|
+| `browser_get_visual_selector_map()` | 获取带元素索引标注的截图 + 交互元素列表 | 使用 `create_highlighted_screenshot_async` 生成标注图 |
|
|
|
+| `browser_get_selector_map()` | 获取交互元素索引映射(纯文本) | 通过 `BrowserStateRequestEvent` 触发 DOM 更新 |
|
|
|
+
|
|
|
+`browser_get_visual_selector_map`(`baseClass.py:1066-1161`)是最核心的观察工具:
|
|
|
+
|
|
|
+```
|
|
|
+1. 触发 BrowserStateRequestEvent(include_dom=True, include_screenshot=True)
|
|
|
+2. 等待浏览器返回完整状态(DOM 树 + 截图)
|
|
|
+3. 从 DOM 状态提取 selector_map(所有可交互元素的索引)
|
|
|
+4. 调用 create_highlighted_screenshot_async 在截图上标注元素索引号
|
|
|
+5. 构建元素列表(tag, aria-label, href, type, role 等属性)
|
|
|
+6. 返回 ToolResult(images 字段含标注截图,output 含元素列表)
|
|
|
+```
|
|
|
+
|
|
|
+### 标签页管理类(Tab)
|
|
|
+
|
|
|
+| 工具 | 功能 |
|
|
|
+|------|------|
|
|
|
+| `browser_switch_tab(tab_id)` | 切换标签页(4 字符 ID) |
|
|
|
+| `browser_close_tab(tab_id)` | 关闭标签页 |
|
|
|
+
|
|
|
+### 下拉框类(Dropdown)
|
|
|
+
|
|
|
+| 工具 | 功能 |
|
|
|
+|------|------|
|
|
|
+| `browser_get_dropdown_options(index)` | 获取下拉框选项 |
|
|
|
+| `browser_select_dropdown_option(index, text)` | 选择下拉框选项 |
|
|
|
+
|
|
|
+### 内容提取类(Content Extraction)
|
|
|
+
|
|
|
+| 工具 | 功能 | 底层 |
|
|
|
+|------|------|------|
|
|
|
+| `browser_extract_content(query, extract_links)` | LLM 驱动的结构化数据提取 | `tools.extract()` + Qwen LLM |
|
|
|
+| `browser_read_long_content(goal, source)` | 智能长内容读取(自动检测 PDF) | `tools.read_long_content()` |
|
|
|
+| `browser_get_page_html()` | 获取完整 HTML | CDP `Runtime.evaluate` |
|
|
|
+| `browser_download_direct_url(url, save_name)` | HTTP 直链下载 | `httpx.AsyncClient` 流式下载 |
|
|
|
+
|
|
|
+#### 内容提取的 LLM 适配
|
|
|
+
|
|
|
+`extraction_adapter`(`baseClass.py:1387-1409`)将 browser-use 的 LangChain Runnable 接口适配为 Qwen LLM 调用:
|
|
|
+
|
|
|
+```python
|
|
|
+async def extraction_adapter(input_data):
|
|
|
+ response = await qwen_llm_call(messages=[{"role": "user", "content": prompt}])
|
|
|
+ content = response["content"]
|
|
|
+ # 自动清洗搜索引擎重定向 URL(Bing Base64 解码、Google url 参数提取)
|
|
|
+ urls = re.findall(r'https?://[^\s<>"\']+', content)
|
|
|
+ for original_url in urls:
|
|
|
+ clean_url = scrub_search_redirect_url(original_url)
|
|
|
+ if clean_url != original_url:
|
|
|
+ content = content.replace(original_url, clean_url)
|
|
|
+ return Namespace(completion=content)
|
|
|
+```
|
|
|
+
|
|
|
+#### PDF 自动检测与下载
|
|
|
+
|
|
|
+`_detect_and_download_pdf_via_cdp`(`baseClass.py:1458-1550`):
|
|
|
+
|
|
|
+```
|
|
|
+1. 检查 URL 是否以 .pdf 结尾
|
|
|
+2. 如果不明显,通过 CDP 检查 document.contentType
|
|
|
+3. 确认是 PDF 后,通过浏览器内 fetch API 下载(自动携带 cookies/session)
|
|
|
+4. 将 data URL 中的 base64 解码为 PDF 文件保存到本地
|
|
|
+5. 将 source 参数改为本地文件路径,交给 pypdf 解析
|
|
|
+```
|
|
|
+
|
|
|
+### Cookie 管理类
|
|
|
+
|
|
|
+| 工具 | 功能 |
|
|
|
+|------|------|
|
|
|
+| `browser_export_cookies(name, account)` | 导出当前域名 Cookie 到 `.cache/.cookies/` |
|
|
|
+| `browser_load_cookies(url, name)` | 自动匹配 Cookie 文件并注入浏览器 |
|
|
|
+| `browser_ensure_login_with_cookies(cookie_type, url)` | 检查登录状态,需要时从 MySQL 查询 Cookie 注入 |
|
|
|
+
|
|
|
+#### Cookie 加载匹配策略
|
|
|
+
|
|
|
+`browser_load_cookies`(`baseClass.py:2046-2177`):
|
|
|
+
|
|
|
+```
|
|
|
+1. 精确匹配:{domain}.json(如 xiaohongshu.com.json)
|
|
|
+2. 前缀匹配:{domain}*.json
|
|
|
+3. 模糊匹配:{主域名}*.json(如 xiaohongshu*.json)
|
|
|
+4. 未找到时:根据 auto_navigate 参数决定是否直接导航到目标页面
|
|
|
+```
|
|
|
+
|
|
|
+#### 从 MySQL 加载 Cookie
|
|
|
+
|
|
|
+`browser_ensure_login_with_cookies` 流程:
|
|
|
+
|
|
|
+```
|
|
|
+1. 导航到目标 URL
|
|
|
+2. 执行 JS 检测登录状态(查找登录按钮/用户头像)
|
|
|
+3. 如果需要登录:
|
|
|
+ a. 从 agent_channel_cookies 表查询 Cookie
|
|
|
+ b. 解析 Cookie(支持 JSON 数组、JSON 对象、分号分隔字符串)
|
|
|
+ c. 通过 CDP _cdp_set_cookies 注入
|
|
|
+ d. 刷新页面
|
|
|
+```
|
|
|
+
|
|
|
+### 控制流类
|
|
|
+
|
|
|
+| 工具 | 功能 |
|
|
|
+|------|------|
|
|
|
+| `browser_evaluate(code)` | 在页面执行任意 JavaScript |
|
|
|
+| `browser_wait_for_user_action(message, timeout)` | 暂停等待用户手动操作(如验证码) |
|
|
|
+| `browser_done(text, success)` | 标记任务完成 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 结果转换机制
|
|
|
+
|
|
|
+所有工具的返回值统一为 agent 框架的 `ToolResult`,通过 `action_result_to_tool_result()` 将 browser-use 的 `ActionResult` 转换:
|
|
|
+
|
|
|
+```python
|
|
|
+# baseClass.py:407-431
|
|
|
+def action_result_to_tool_result(result: ActionResult, title: str = None) -> ToolResult:
|
|
|
+ if result.error:
|
|
|
+ return ToolResult(title=..., output="", error=result.error,
|
|
|
+ long_term_memory=result.long_term_memory or result.error)
|
|
|
+ return ToolResult(title=..., output=result.extracted_content or "",
|
|
|
+ long_term_memory=..., metadata=result.metadata or {})
|
|
|
+```
|
|
|
+
|
|
|
+`ToolResult` 支持双层记忆管理(`agent/tools/models.py`):
|
|
|
+
|
|
|
+- `output`:完整内容,可配置 `include_output_only_once=True` 只给 LLM 看一次
|
|
|
+- `long_term_memory`:简短摘要,永久保留在对话历史中
|
|
|
+- `images`:截图等图片数据(base64)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## URL 清洗
|
|
|
+
|
|
|
+`scrub_search_redirect_url()`(`baseClass.py:1347-1385`)自动解析搜索引擎重定向链接:
|
|
|
+
|
|
|
+| 引擎 | 处理方式 |
|
|
|
+|------|---------|
|
|
|
+| Bing | 提取 `u` 参数,去掉 `a1` 前缀,Base64 解码 |
|
|
|
+| Google | 提取 `url` 参数,URL 解码 |
|
|
|
+| 通用 | 检查 `target`/`dest`/`destination`/`link` 参数 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件存储
|
|
|
+
|
|
|
+所有浏览器产生的文件统一存储在工作目录下:
|
|
|
+
|
|
|
+```
|
|
|
+.cache/
|
|
|
+├── .browser_use_files/ # 浏览器下载、截图、PDF 等临时文件
|
|
|
+└── .cookies/ # Cookie 持久化文件({domain}_{account}.json)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Skill 集成
|
|
|
+
|
|
|
+`agent/skill/skills/browser.md` 作为 Skill 注入到 LLM 的 system prompt,指导 LLM 正确使用浏览器工具。核心规则:
|
|
|
+
|
|
|
+1. 操作前必须先通过 `browser_get_visual_selector_map` 获取元素索引
|
|
|
+2. 任何触发页面变化的操作后都要 `browser_wait`
|
|
|
+3. 登录优先用 `browser_load_cookies`,首次登录需请求人类协助
|
|
|
+4. 优先使用高级提取工具(`extract_content`/`read_long_content`)而非手动解析 DOM
|