فهرست منبع

update: browser-use支持视觉捕获与点击

guantao 3 هفته پیش
والد
کامیت
79593a721b

+ 34 - 2
agent/core/runner.py

@@ -115,6 +115,7 @@ BUILTIN_TOOLS = [
     "browser_read_long_content",
     "browser_get_page_html",
     "browser_get_selector_map",
+    "browser_get_visual_selector_map",
     "browser_evaluate",
     "browser_ensure_login_with_cookies",
     "browser_wait_for_user_action",
@@ -626,17 +627,47 @@ class AgentRunner:
                         }
                     )
 
+                    # --- 支持多模态工具反馈 ---
+                    # execute() 返回 dict{"text","images"} 或 str
+                    if isinstance(tool_result, dict) and tool_result.get("images"):
+                        tool_result_text = tool_result["text"]
+                        # 构建多模态消息格式
+                        tool_content_for_llm = [{"type": "text", "text": tool_result_text}]
+                        for img in tool_result["images"]:
+                            if img.get("type") == "base64" and img.get("data"):
+                                media_type = img.get("media_type", "image/png")
+                                tool_content_for_llm.append({
+                                    "type": "image_url",
+                                    "image_url": {
+                                        "url": f"data:{media_type};base64,{img['data']}"
+                                    }
+                                })
+                        img_count = len(tool_content_for_llm) - 1  # 减去 text 块
+                        print(f"[Runner] 多模态工具反馈: tool={tool_name}, images={img_count}, text_len={len(tool_result_text)}")
+                    else:
+                        tool_result_text = str(tool_result)
+                        tool_content_for_llm = tool_result_text
+
                     tool_msg = Message.create(
                         trace_id=trace_id,
                         role="tool",
                         sequence=sequence,
                         goal_id=current_goal_id,
                         tool_call_id=tc["id"],
-                        content={"tool_name": tool_name, "result": tool_result},
+                        content={"tool_name": tool_name, "result": tool_result_text},
                     )
 
                     if self.trace_store:
                         await self.trace_store.add_message(tool_msg)
+                        # 截图单独存为同名 PNG 文件
+                        if isinstance(tool_result, dict) and tool_result.get("images"):
+                            import base64 as b64mod
+                            for img in tool_result["images"]:
+                                if img.get("data"):
+                                    png_path = self.trace_store._get_messages_dir(trace_id) / f"{tool_msg.message_id}.png"
+                                    png_path.write_bytes(b64mod.b64decode(img["data"]))
+                                    print(f"[Runner] 截图已保存: {png_path.name}")
+                                    break  # 只存第一张
 
                     yield tool_msg
                     sequence += 1
@@ -645,8 +676,9 @@ class AgentRunner:
                         "role": "tool",
                         "tool_call_id": tc["id"],
                         "name": tool_name,
-                        "content": str(tool_result),
+                        "content": tool_content_for_llm, # 这里传入 list 即可触发模型的视觉能力
                     })
+                    # ------------------------------------------
 
                 continue  # 继续循环
 

+ 2 - 2
agent/memory/skills/core.md

@@ -78,7 +78,7 @@ goal(abandon="方案A需要Redis,环境没有")
 
 1. **页面导航**: 使用 `browser_navigate_to_url` 或 `browser_search_web` 到达目标页面
 2. **等待加载**: 页面跳转后调用 `browser_wait(seconds=2)` 等待内容加载
-3. **获取元素索引**: 调用 `browser_get_selector_map` 获取可交互元素的索引映射
+3. **获取元素索引**: 调用 `browser_get_visual_selector_map` 获取可交互元素的索引映射和当前界面的截图
 4. **执行交互**: 使用 `browser_click_element`、`browser_input_text` 等工具操作页面
 5. **提取内容**: 使用 `browser_extract_content`, `browser_read_long_content`, `browser_get_page_html` 获取数据
 
@@ -96,5 +96,5 @@ goal(abandon="方案A需要Redis,环境没有")
 **导航**: browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
-**提取**: browser_extract_content, browser_read_long_content, browser_get_page_html,    browser_get_selector_map
+**提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map
 **高级**: browser_evaluate, browser_ensure_login_with_cookies, browser_wait_for_user_action

+ 10 - 0
agent/tools/builtin/browser/__init__.py

@@ -41,6 +41,7 @@ from agent.tools.builtin.browser.baseClass import (
     browser_read_long_content,
     browser_get_page_html,
     browser_get_selector_map,
+    browser_get_visual_selector_map,
 
     # JavaScript 执行工具
     browser_evaluate,
@@ -51,6 +52,10 @@ from agent.tools.builtin.browser.baseClass import (
 
     # 任务完成
     browser_done,
+
+    # Cookie 持久化
+    browser_export_cookies,
+    browser_load_cookies,
 )
 
 __all__ = [
@@ -90,6 +95,7 @@ __all__ = [
     'browser_read_long_content',
     'browser_get_page_html',
     'browser_get_selector_map',
+    'browser_get_visual_selector_map',
 
     # JavaScript 执行工具
     'browser_evaluate',
@@ -100,4 +106,8 @@ __all__ = [
 
     # 任务完成
     'browser_done',
+
+    # Cookie 持久化
+    'browser_export_cookies',
+    'browser_load_cookies',
 ]

+ 211 - 0
agent/tools/builtin/browser/baseClass.py

@@ -954,7 +954,103 @@ async def browser_find_text(text: str) -> ToolResult:
             long_term_memory=f"查找文本 '{text}' 失败"
         )
 
+@tool()
+async def browser_get_visual_selector_map() -> ToolResult:
+    """
+    获取当前页面的视觉快照和交互元素索引映射。
+    Get visual snapshot and selector map of interactive elements.
+
+    该工具会同时执行两个操作:
+    1. 捕捉当前页面的截图,并用 browser-use 内置方法在截图上标注元素索引号。
+    2. 生成页面所有可交互元素的索引字典(含 href、type 等属性信息)。
+
+    Returns:
+        ToolResult: 包含高亮截图(在 images 中)和元素列表的工具返回对象。
+    """
+    try:
+        browser, _ = await get_browser_session()
+
+        # 1. 构造同时包含 DOM 和 截图 的请求
+        from browser_use.browser.events import BrowserStateRequestEvent
+        from browser_use.browser.python_highlights import create_highlighted_screenshot_async
+
+        event = browser.event_bus.dispatch(
+            BrowserStateRequestEvent(
+                include_dom=True,
+                include_screenshot=True,
+                include_recent_events=False
+            )
+        )
+
+        # 2. 等待浏览器返回完整状态
+        browser_state = await event.event_result(raise_if_none=True, raise_if_any=True)
+
+        # 3. 提取 Selector Map
+        selector_map = browser_state.dom_state.selector_map if browser_state.dom_state else {}
+
+        # 4. 提取截图并生成带索引标注的高亮截图(通过 CDP 获取精确 DPI 和滚动偏移)
+        screenshot_b64 = browser_state.screenshot or ""
+        highlighted_b64 = ""
+        if screenshot_b64 and selector_map:
+            try:
+                cdp_session = await browser.get_or_create_cdp_session()
+                highlighted_b64 = await create_highlighted_screenshot_async(
+                    screenshot_b64, selector_map,
+                    cdp_session=cdp_session,
+                    filter_highlight_ids=False
+                )
+            except Exception:
+                highlighted_b64 = screenshot_b64  # fallback to raw screenshot
+        else:
+            highlighted_b64 = screenshot_b64
 
+        # 5. 构建供 Agent 阅读的完整元素列表,包含丰富的属性信息
+        elements_info = []
+        for index, node in selector_map.items():
+            tag = node.tag_name
+            attrs = node.attributes or {}
+            desc = attrs.get('aria-label') or attrs.get('placeholder') or attrs.get('title') or node.get_all_children_text(max_depth=1) or ""
+            # 收集有用的属性片段
+            extra_parts = []
+            if attrs.get('href'):
+                extra_parts.append(f"href={attrs['href'][:60]}")
+            if attrs.get('type'):
+                extra_parts.append(f"type={attrs['type']}")
+            if attrs.get('role'):
+                extra_parts.append(f"role={attrs['role']}")
+            if attrs.get('name'):
+                extra_parts.append(f"name={attrs['name']}")
+            extra = f" ({', '.join(extra_parts)})" if extra_parts else ""
+            elements_info.append(f"Index {index}: <{tag}> \"{desc[:50]}\"{extra}")
+
+        output = f"页面截图已捕获(含元素索引标注)\n找到 {len(selector_map)} 个交互元素\n\n"
+        output += "元素列表:\n" + "\n".join(elements_info)
+
+        # 6. 将高亮截图存入 images 字段,metadata 保留结构化数据
+        images = []
+        if highlighted_b64:
+            images.append({"type": "base64", "media_type": "image/png", "data": highlighted_b64})
+
+        return ToolResult(
+            title="视觉元素观察",
+            output=output,
+            long_term_memory=f"在页面观察到 {len(selector_map)} 个元素并保存了截图",
+            images=images,
+            metadata={
+                "selector_map": {k: str(v) for k, v in list(selector_map.items())[:100]},
+                "url": browser_state.url,
+                "title": browser_state.title
+            }
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="视觉观察失败",
+            output="",
+            error=f"Failed to get visual selector map: {str(e)}",
+            long_term_memory="获取视觉元素映射失败"
+        )
+    
 @tool()
 async def browser_screenshot() -> ToolResult:
     """
@@ -1787,6 +1883,116 @@ async def browser_done(text: str, success: bool = True,
         )
 
 
+# ============================================================
+# Cookie 持久化工具
+# ============================================================
+
+_COOKIES_DIR = Path(__file__).parent.parent.parent.parent / ".cookies"
+
+@tool()
+async def browser_export_cookies(name: str = "") -> ToolResult:
+    """
+    导出当前浏览器的所有 Cookie 到本地 JSON 文件。
+    Export all browser cookies to a local JSON file.
+
+    登录成功后调用此工具,下次启动时可通过 browser_load_cookies 恢复登录态。
+
+    Args:
+        name: 保存名称(可选,默认用当前域名)
+
+    Returns:
+        ToolResult: 导出结果
+    """
+    try:
+        browser, _ = await get_browser_session()
+        cookies = await browser._cdp_get_cookies()
+
+        if not cookies:
+            return ToolResult(
+                title="Cookie 导出",
+                output="当前浏览器没有 Cookie",
+                long_term_memory="导出 Cookie 失败:无 Cookie"
+            )
+
+        # 确定文件名
+        if not name:
+            url = getattr(browser, 'current_url', '') or ''
+            from urllib.parse import urlparse
+            parsed = urlparse(url)
+            name = parsed.netloc.replace("www.", "") or "default"
+
+        _COOKIES_DIR.mkdir(parents=True, exist_ok=True)
+        cookie_file = _COOKIES_DIR / f"{name}.json"
+
+        cookie_file.write_text(
+            json.dumps(cookies, ensure_ascii=False, indent=2),
+            encoding="utf-8"
+        )
+
+        return ToolResult(
+            title="Cookie 已导出",
+            output=f"已保存 {len(cookies)} 条 Cookie 到 {cookie_file.name}",
+            long_term_memory=f"导出 {len(cookies)} 条 Cookie 到 .cookies/{cookie_file.name}"
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="Cookie 导出失败",
+            output="",
+            error=f"Failed to export cookies: {str(e)}",
+            long_term_memory="导出 Cookie 失败"
+        )
+
+
+@tool()
+async def browser_load_cookies(name: str, url: str = "") -> ToolResult:
+    """
+    从本地 JSON 文件加载 Cookie 到浏览器,恢复登录态。
+    Load cookies from a local JSON file into the browser.
+
+    Args:
+        name: Cookie 文件名(不含 .json 后缀)
+        url: 加载后导航到的 URL(可选)
+
+    Returns:
+        ToolResult: 加载结果
+    """
+    try:
+        browser, tools = await get_browser_session()
+
+        cookie_file = _COOKIES_DIR / f"{name}.json"
+        if not cookie_file.exists():
+            # 列出可用文件
+            available = [f.stem for f in _COOKIES_DIR.glob("*.json")] if _COOKIES_DIR.exists() else []
+            return ToolResult(
+                title="Cookie 文件不存在",
+                output=f"未找到 .cookies/{name}.json,可用: {available}",
+                error=f"Cookie file not found: {name}.json",
+                long_term_memory=f"Cookie 文件 {name}.json 不存在"
+            )
+
+        cookies = json.loads(cookie_file.read_text(encoding="utf-8"))
+        await browser._cdp_set_cookies(cookies)
+
+        if url:
+            await tools.navigate(url=url, browser_session=browser)
+            await tools.wait(seconds=2, browser_session=browser)
+
+        return ToolResult(
+            title="Cookie 已加载",
+            output=f"已注入 {len(cookies)} 条 Cookie(来自 {name}.json)",
+            long_term_memory=f"从 .cookies/{name}.json 加载了 {len(cookies)} 条 Cookie"
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="Cookie 加载失败",
+            output="",
+            error=f"Failed to load cookies: {str(e)}",
+            long_term_memory="加载 Cookie 失败"
+        )
+
+
 # ============================================================
 # 导出所有工具函数(供外部使用)
 # ============================================================
@@ -1828,6 +2034,7 @@ __all__ = [
     'browser_get_page_html',
     'browser_read_long_content',
     'browser_get_selector_map',
+    'browser_get_visual_selector_map',
 
     # JavaScript 执行工具
     'browser_evaluate',
@@ -1838,4 +2045,8 @@ __all__ = [
 
     # 任务完成
     'browser_done',
+
+    # Cookie 持久化
+    'browser_export_cookies',
+    'browser_load_cookies',
 ]

+ 4 - 1
agent/tools/registry.py

@@ -234,13 +234,16 @@ class ToolRegistry:
 			duration = time.time() - start_time
 			stats.total_duration += duration
 
-			# 返回 JSON 字符串或文本
+			# 返回结果:ToolResult 转为可序列化格式
 			if isinstance(result, str):
 				return result
 
 			# 处理 ToolResult 对象
 			from agent.tools.models import ToolResult
 			if isinstance(result, ToolResult):
+				# 有图片时返回 dict 以便 runner 构建多模态消息
+				if result.images:
+					return {"text": result.to_llm_message(), "images": result.images}
 				return result.to_llm_message()
 
 			return json.dumps(result, ensure_ascii=False, indent=2)

+ 2 - 1
examples/research/run.py

@@ -34,12 +34,13 @@ from agent.trace import (
 from agent.llm import create_openrouter_llm_call
 
 # 导入浏览器清理工具
-from agent.tools.builtin.browser.baseClass import kill_browser_session
+from agent.tools.builtin.browser.baseClass import kill_browser_session, init_browser_session
 
 async def main():
     # 路径配置
     base_dir = Path(__file__).parent
     project_root = base_dir.parent.parent
+    trace_dir = project_root / ".trace"
     prompt_path = base_dir / "test.prompt"
     output_dir = base_dir / "output"
     output_dir.mkdir(exist_ok=True)

+ 1 - 1
examples/research/test.prompt

@@ -7,4 +7,4 @@ $system$
 你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
 
 $user$
-使用浏览器帮我做个调研:一张图片中的构图可以如何表示?我希望寻找一些构图特征的表示方法。尝试查阅一些论文pdf, 网页等资料,最后输出一份调研报告。
+使用浏览器帮我做一下调研,打开小红书的官网,自动登录(输入手机号15035599703),点击同意协议,并点击验证,等待我手动输入(可以通过get_visual_selector_map来确定屏幕效果),然后点击登录,并随机查找一个摄影博主的最近发帖的信息