|
@@ -43,35 +43,29 @@ Native Browser-Use Tools Adapter
|
|
|
这些工具功能更完善,支持diff预览、智能匹配、分页读取等
|
|
这些工具功能更完善,支持diff预览、智能匹配、分页读取等
|
|
|
"""
|
|
"""
|
|
|
import logging
|
|
import logging
|
|
|
-import sys
|
|
|
|
|
-import os
|
|
|
|
|
import json
|
|
import json
|
|
|
-import httpx
|
|
|
|
|
import asyncio
|
|
import asyncio
|
|
|
-import aiohttp
|
|
|
|
|
import re
|
|
import re
|
|
|
import base64
|
|
import base64
|
|
|
from urllib.parse import urlparse, parse_qs, unquote
|
|
from urllib.parse import urlparse, parse_qs, unquote
|
|
|
from typing import Literal, Optional, List, Dict, Any, Tuple, Union
|
|
from typing import Literal, Optional, List, Dict, Any, Tuple, Union
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from langchain_core.runnables import RunnableLambda
|
|
from langchain_core.runnables import RunnableLambda
|
|
|
-from argparse import Namespace # 使用 Namespace 快速构造带属性的对象
|
|
|
|
|
-from langchain_core.messages import AIMessage
|
|
|
|
|
|
|
+from argparse import Namespace
|
|
|
from ....llm.qwen import qwen_llm_call
|
|
from ....llm.qwen import qwen_llm_call
|
|
|
-
|
|
|
|
|
-# 将项目根目录添加到 Python 路径
|
|
|
|
|
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
-
|
|
|
|
|
-# 配置日志
|
|
|
|
|
-logger = logging.getLogger(__name__)
|
|
|
|
|
-
|
|
|
|
|
-# 导入框架的工具装饰器和结果类
|
|
|
|
|
from agent.tools import tool, ToolResult
|
|
from agent.tools import tool, ToolResult
|
|
|
-from agent.tools.builtin.browser.sync_mysql_help import mysql
|
|
|
|
|
|
|
+from agent.tools.builtin.browser.downloads import download_direct_url
|
|
|
|
|
+from agent.tools.builtin.browser.providers import (
|
|
|
|
|
+ get_browser_container_provider,
|
|
|
|
|
+ get_browser_cookie_provider,
|
|
|
|
|
+)
|
|
|
|
|
|
|
|
# 导入 browser-use 的核心类
|
|
# 导入 browser-use 的核心类
|
|
|
from browser_use import BrowserSession, BrowserProfile
|
|
from browser_use import BrowserSession, BrowserProfile
|
|
|
|
|
+from browser_use.agent.views import ActionResult
|
|
|
|
|
+from browser_use.filesystem.file_system import FileSystem
|
|
|
from browser_use.tools.service import Tools
|
|
from browser_use.tools.service import Tools
|
|
|
|
|
+
|
|
|
try:
|
|
try:
|
|
|
from browser_use.tools.views import ReadContentAction # type: ignore
|
|
from browser_use.tools.views import ReadContentAction # type: ignore
|
|
|
except Exception:
|
|
except Exception:
|
|
@@ -81,8 +75,9 @@ except Exception:
|
|
|
goal: str
|
|
goal: str
|
|
|
source: str = "page"
|
|
source: str = "page"
|
|
|
context: str = ""
|
|
context: str = ""
|
|
|
-from browser_use.agent.views import ActionResult
|
|
|
|
|
-from browser_use.filesystem.file_system import FileSystem
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
# ============================================================
|
|
@@ -103,129 +98,9 @@ _last_headless: bool = True
|
|
|
_live_url: Optional[str] = None
|
|
_live_url: Optional[str] = None
|
|
|
|
|
|
|
|
async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
|
|
async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
|
|
|
- """
|
|
|
|
|
- 创建浏览器容器并导航到指定URL
|
|
|
|
|
-
|
|
|
|
|
- 按照 test.md 的要求:
|
|
|
|
|
- 1.1 调用接口创建容器
|
|
|
|
|
- 1.2 调用接口创建窗口并导航到URL
|
|
|
|
|
|
|
+ """Create a container through the Host-configurable provider."""
|
|
|
|
|
|
|
|
- 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
|
|
|
|
|
|
|
+ return await get_browser_container_provider().create(url, account_name)
|
|
|
|
|
|
|
|
async def init_browser_session(
|
|
async def init_browser_session(
|
|
|
browser_type: str = "local",
|
|
browser_type: str = "local",
|
|
@@ -260,8 +135,9 @@ async def init_browser_session(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if browser_type == "container":
|
|
if browser_type == "container":
|
|
|
- print("🐳 使用容器浏览器模式")
|
|
|
|
|
- if not url: url = "about:blank"
|
|
|
|
|
|
|
+ logger.info("Initializing container browser session")
|
|
|
|
|
+ if not url:
|
|
|
|
|
+ url = "about:blank"
|
|
|
container_info = await create_container(url=url, account_name=profile_name)
|
|
container_info = await create_container(url=url, account_name=profile_name)
|
|
|
if not container_info["success"]:
|
|
if not container_info["success"]:
|
|
|
raise RuntimeError(f"容器创建失败: {container_info['error']}")
|
|
raise RuntimeError(f"容器创建失败: {container_info['error']}")
|
|
@@ -269,13 +145,13 @@ async def init_browser_session(
|
|
|
await asyncio.sleep(3)
|
|
await asyncio.sleep(3)
|
|
|
|
|
|
|
|
elif browser_type == "cloud":
|
|
elif browser_type == "cloud":
|
|
|
- print("🌐 使用云浏览器模式")
|
|
|
|
|
|
|
+ logger.info("Initializing cloud browser session")
|
|
|
session_params["use_cloud"] = True
|
|
session_params["use_cloud"] = True
|
|
|
if profile_name and profile_name != "default":
|
|
if profile_name and profile_name != "default":
|
|
|
session_params["cloud_profile_id"] = profile_name
|
|
session_params["cloud_profile_id"] = profile_name
|
|
|
|
|
|
|
|
else: # local
|
|
else: # local
|
|
|
- print("💻 使用本地浏览器模式")
|
|
|
|
|
|
|
+ logger.info("Initializing local browser session")
|
|
|
session_params["is_local"] = True
|
|
session_params["is_local"] = True
|
|
|
if user_data_dir is None and profile_name:
|
|
if user_data_dir is None and profile_name:
|
|
|
user_data_dir = str(Path.home() / ".browser_use" / "profiles" / profile_name)
|
|
user_data_dir = str(Path.home() / ".browser_use" / "profiles" / profile_name)
|
|
@@ -303,7 +179,7 @@ async def init_browser_session(
|
|
|
_browser_tools = Tools()
|
|
_browser_tools = Tools()
|
|
|
_file_system = FileSystem(base_dir=str(save_dir))
|
|
_file_system = FileSystem(base_dir=str(save_dir))
|
|
|
|
|
|
|
|
- print(f"✅ 浏览器会话初始化成功 | 默认下载路径: {save_dir}")
|
|
|
|
|
|
|
+ logger.info("Browser session initialized: downloads=%s", save_dir)
|
|
|
|
|
|
|
|
# 云浏览器:捕获 live URL
|
|
# 云浏览器:捕获 live URL
|
|
|
if browser_type == "cloud":
|
|
if browser_type == "cloud":
|
|
@@ -314,7 +190,7 @@ async def init_browser_session(
|
|
|
parsed = urllib.parse.urlparse(cdp_url)
|
|
parsed = urllib.parse.urlparse(cdp_url)
|
|
|
host_url = f"https://{parsed.hostname}"
|
|
host_url = f"https://{parsed.hostname}"
|
|
|
_live_url = f"https://live.browser-use.com?wss={urllib.parse.quote(host_url)}"
|
|
_live_url = f"https://live.browser-use.com?wss={urllib.parse.quote(host_url)}"
|
|
|
- print(f"📡 实时画面链接: {_live_url}")
|
|
|
|
|
|
|
+ logger.info("Browser live view available: %s", _live_url)
|
|
|
|
|
|
|
|
if browser_type in ["local", "cloud"] and url:
|
|
if browser_type in ["local", "cloud"] and url:
|
|
|
await _browser_tools.navigate(url=url, browser_session=_browser_session)
|
|
await _browser_tools.navigate(url=url, browser_session=_browser_session)
|
|
@@ -355,10 +231,10 @@ async def get_browser_session() -> tuple[BrowserSession, Tools]:
|
|
|
)
|
|
)
|
|
|
alive = True
|
|
alive = True
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- pass
|
|
|
|
|
|
|
+ logger.debug("Browser CDP health check failed", exc_info=True)
|
|
|
|
|
|
|
|
if not alive:
|
|
if not alive:
|
|
|
- print("⚠️ 浏览器会话连接已断开,正在重新初始化...")
|
|
|
|
|
|
|
+ logger.warning("Browser session disconnected; reinitializing")
|
|
|
try:
|
|
try:
|
|
|
await cleanup_browser_session()
|
|
await cleanup_browser_session()
|
|
|
except Exception:
|
|
except Exception:
|
|
@@ -432,10 +308,8 @@ def action_result_to_tool_result(result: ActionResult, title: str = None) -> Too
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cookie_domain_for_type(cookie_type: str, url: str) -> Tuple[str, str]:
|
|
def _cookie_domain_for_type(cookie_type: str, url: str) -> Tuple[str, str]:
|
|
|
- if cookie_type:
|
|
|
|
|
- key = cookie_type.lower()
|
|
|
|
|
- if key in {"xiaohongshu", "xhs"}:
|
|
|
|
|
- return ".xiaohongshu.com", "https://www.xiaohongshu.com"
|
|
|
|
|
|
|
+ # ``cookie_type`` remains in the compatibility signature; scope is derived
|
|
|
|
|
+ # from the requested URL so the framework does not encode platform names.
|
|
|
parsed = urlparse(url or "")
|
|
parsed = urlparse(url or "")
|
|
|
domain = parsed.netloc or ""
|
|
domain = parsed.netloc or ""
|
|
|
domain = domain.replace("www.", "")
|
|
domain = domain.replace("www.", "")
|
|
@@ -516,27 +390,9 @@ def _fetch_cookie_row(cookie_type: str) -> Optional[Dict[str, Any]]:
|
|
|
if not cookie_type:
|
|
if not cookie_type:
|
|
|
return None
|
|
return None
|
|
|
try:
|
|
try:
|
|
|
- return mysql.fetchone(
|
|
|
|
|
- "select * from agent_channel_cookies where type=%s limit 1",
|
|
|
|
|
- (cookie_type,)
|
|
|
|
|
- )
|
|
|
|
|
- except Exception:
|
|
|
|
|
- return None
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _fetch_profile_id(cookie_type: str) -> Optional[str]:
|
|
|
|
|
- """从数据库获取 cloud_profile_id"""
|
|
|
|
|
- if not cookie_type:
|
|
|
|
|
- return None
|
|
|
|
|
- try:
|
|
|
|
|
- row = mysql.fetchone(
|
|
|
|
|
- "select profileId from agent_channel_cookies where type=%s limit 1",
|
|
|
|
|
- (cookie_type,)
|
|
|
|
|
- )
|
|
|
|
|
- if row and "profileId" in row:
|
|
|
|
|
- return row["profileId"]
|
|
|
|
|
- return None
|
|
|
|
|
- except Exception:
|
|
|
|
|
|
|
+ return get_browser_cookie_provider().get_cookie_row(cookie_type)
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ logger.warning("Browser cookie lookup failed for %s: %s", cookie_type, exc)
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@@ -725,71 +581,11 @@ class DownloadLinkCaptureHandler(logging.Handler):
|
|
|
# 再次过滤:如果发现提取出的 URL 确实包含三个点,说明依然抓到了截断版,跳过
|
|
# 再次过滤:如果发现提取出的 URL 确实包含三个点,说明依然抓到了截断版,跳过
|
|
|
if "..." not in url:
|
|
if "..." not in url:
|
|
|
self.captured_url = url
|
|
self.captured_url = url
|
|
|
- # print(f"🎯 成功锁定完整直链: {url[:50]}...") # 调试用
|
|
|
|
|
|
|
|
|
|
async def browser_download_direct_url(url: str, save_name: str = "book.epub") -> ToolResult:
|
|
async def browser_download_direct_url(url: str, save_name: str = "book.epub") -> ToolResult:
|
|
|
- save_dir = Path.cwd() / ".cache/.browser_use_files"
|
|
|
|
|
- save_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
-
|
|
|
|
|
- # 提取域名作为 Referer,这能骗过 90% 的防盗链校验
|
|
|
|
|
- from urllib.parse import urlparse
|
|
|
|
|
- parsed_url = urlparse(url)
|
|
|
|
|
- base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
|
|
|
|
-
|
|
|
|
|
- # 如果没传 save_name,自动从 URL 获取
|
|
|
|
|
- if not save_name:
|
|
|
|
|
- import unquote
|
|
|
|
|
- # 尝试从 URL 路径获取文件名并解码(处理中文)
|
|
|
|
|
- save_name = Path(urlparse(url).path).name or f"download_{int(time.time())}"
|
|
|
|
|
- save_name = unquote(save_name)
|
|
|
|
|
-
|
|
|
|
|
- target_path = save_dir / save_name
|
|
|
|
|
-
|
|
|
|
|
- headers = {
|
|
|
|
|
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
|
|
|
- "Accept": "*/*",
|
|
|
|
|
- "Referer": base_url, # 动态设置 Referer
|
|
|
|
|
- "Range": "bytes=0-", # 有时对大文件下载有奇效
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ """Compatibility wrapper for the isolated download implementation."""
|
|
|
|
|
|
|
|
- try:
|
|
|
|
|
- print(f"🚀 开始下载: {url[:60]}...")
|
|
|
|
|
-
|
|
|
|
|
- # 使用 follow_redirects=True 处理链接中的 redirection
|
|
|
|
|
- async with httpx.AsyncClient(headers=headers, follow_redirects=True, timeout=60.0) as client:
|
|
|
|
|
- async with client.stream("GET", url) as response:
|
|
|
|
|
- if response.status_code != 200:
|
|
|
|
|
- print(f"❌ 下载失败,HTTP 状态码: {response.status_code}")
|
|
|
|
|
- return
|
|
|
|
|
-
|
|
|
|
|
- # 获取实际文件名(如果服务器提供了)
|
|
|
|
|
- # 这里会优先使用你指定的 save_name
|
|
|
|
|
-
|
|
|
|
|
- with open(target_path, "wb") as f:
|
|
|
|
|
- downloaded_bytes = 0
|
|
|
|
|
- async for chunk in response.aiter_bytes():
|
|
|
|
|
- f.write(chunk)
|
|
|
|
|
- downloaded_bytes += len(chunk)
|
|
|
|
|
- if downloaded_bytes % (1024 * 1024) == 0: # 每下载 1MB 打印一次
|
|
|
|
|
- print(f"📥 已下载: {downloaded_bytes // (1024 * 1024)} MB")
|
|
|
|
|
-
|
|
|
|
|
- print(f"✅ 下载完成!文件已存至: {target_path}")
|
|
|
|
|
- success_msg = f"✅ 下载完成!文件已存至: {target_path}"
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="直链下载成功",
|
|
|
|
|
- output=success_msg,
|
|
|
|
|
- long_term_memory=success_msg,
|
|
|
|
|
- metadata={"path": str(target_path)}
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- # 异常捕获返回
|
|
|
|
|
- return ToolResult(
|
|
|
|
|
- title="下载异常",
|
|
|
|
|
- output="",
|
|
|
|
|
- error=f"💥 发生错误: {str(e)}",
|
|
|
|
|
- long_term_memory=f"下载任务由于异常中断: {str(e)}"
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ return await download_direct_url(url, save_name)
|
|
|
|
|
|
|
|
async def browser_click_element(index: int) -> ToolResult:
|
|
async def browser_click_element(index: int) -> ToolResult:
|
|
|
"""
|
|
"""
|
|
@@ -873,7 +669,7 @@ async def browser_input_text(index: int, text: str, clear: bool = True) -> ToolR
|
|
|
title="输入失败",
|
|
title="输入失败",
|
|
|
output="",
|
|
output="",
|
|
|
error=f"Failed to input text into element {index}: {str(e)}",
|
|
error=f"Failed to input text into element {index}: {str(e)}",
|
|
|
- long_term_memory=f"输入文本失败"
|
|
|
|
|
|
|
+ long_term_memory="输入文本失败"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1362,7 +1158,7 @@ def scrub_search_redirect_url(url: str) -> str:
|
|
|
return unquote(found)
|
|
return unquote(found)
|
|
|
|
|
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- pass # 解析失败则返回原链接
|
|
|
|
|
|
|
+ logger.debug("Search redirect URL parsing failed", exc_info=True)
|
|
|
|
|
|
|
|
return url
|
|
return url
|
|
|
|
|
|
|
@@ -1387,7 +1183,6 @@ async def extraction_adapter(input_data):
|
|
|
if clean_url != original_url:
|
|
if clean_url != original_url:
|
|
|
content = content.replace(original_url, clean_url)
|
|
content = content.replace(original_url, clean_url)
|
|
|
|
|
|
|
|
- from argparse import Namespace
|
|
|
|
|
return Namespace(completion=content)
|
|
return Namespace(completion=content)
|
|
|
|
|
|
|
|
async def browser_extract_content(query: str, extract_links: bool = False,
|
|
async def browser_extract_content(query: str, extract_links: bool = False,
|
|
@@ -1461,7 +1256,7 @@ async def _detect_and_download_pdf_via_cdp(browser) -> Optional[str]:
|
|
|
content_type = ct_result.get('result', {}).get('value', '')
|
|
content_type = ct_result.get('result', {}).get('value', '')
|
|
|
is_pdf = 'pdf' in content_type.lower()
|
|
is_pdf = 'pdf' in content_type.lower()
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- pass
|
|
|
|
|
|
|
+ logger.debug("Browser PDF content-type probe failed", exc_info=True)
|
|
|
|
|
|
|
|
if not is_pdf:
|
|
if not is_pdf:
|
|
|
return None
|
|
return None
|
|
@@ -1497,12 +1292,12 @@ async def _detect_and_download_pdf_via_cdp(browser) -> Optional[str]:
|
|
|
|
|
|
|
|
value = result.get('result', {}).get('value', '')
|
|
value = result.get('result', {}).get('value', '')
|
|
|
if not value:
|
|
if not value:
|
|
|
- print("⚠️ CDP fetch PDF: 无返回值")
|
|
|
|
|
|
|
+ logger.warning("CDP PDF fetch returned no value")
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
data = json.loads(value)
|
|
data = json.loads(value)
|
|
|
if 'error' in data:
|
|
if 'error' in data:
|
|
|
- print(f"⚠️ CDP fetch PDF 失败: {data['error']}")
|
|
|
|
|
|
|
+ logger.warning("CDP PDF fetch failed: %s", data["error"])
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
# 从 data URL 中提取 base64 并解码
|
|
# 从 data URL 中提取 base64 并解码
|
|
@@ -1523,11 +1318,11 @@ async def _detect_and_download_pdf_via_cdp(browser) -> Optional[str]:
|
|
|
with open(save_path, 'wb') as f:
|
|
with open(save_path, 'wb') as f:
|
|
|
f.write(pdf_bytes)
|
|
f.write(pdf_bytes)
|
|
|
|
|
|
|
|
- print(f"📄 PDF 已通过 CDP 下载到: {save_path} ({len(pdf_bytes)} bytes)")
|
|
|
|
|
|
|
+ logger.info("CDP PDF saved: path=%s bytes=%d", save_path, len(pdf_bytes))
|
|
|
return save_path
|
|
return save_path
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- print(f"⚠️ PDF 检测/下载异常: {e}")
|
|
|
|
|
|
|
+ logger.warning("CDP PDF detection/download failed: %s", e)
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1885,12 +1680,12 @@ async def browser_wait_for_user_action(message: str = "Please complete the actio
|
|
|
import asyncio
|
|
import asyncio
|
|
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"\n{'='*60}")
|
|
|
- print(f"⏸️ WAITING FOR USER ACTION")
|
|
|
|
|
|
|
+ print("⏸️ WAITING FOR USER ACTION")
|
|
|
print(f"{'='*60}")
|
|
print(f"{'='*60}")
|
|
|
print(f"📝 {message}")
|
|
print(f"📝 {message}")
|
|
|
print(f"⏱️ Timeout: {timeout} seconds")
|
|
print(f"⏱️ Timeout: {timeout} seconds")
|
|
|
- print(f"\n👉 Please complete the action in the browser window")
|
|
|
|
|
- print(f"👉 Press ENTER when done, or wait for timeout")
|
|
|
|
|
|
|
+ print("\n👉 Please complete the action in the browser window")
|
|
|
|
|
+ print("👉 Press ENTER when done, or wait for timeout")
|
|
|
print(f"{'='*60}\n")
|
|
print(f"{'='*60}\n")
|
|
|
|
|
|
|
|
# Wait for user input or timeout
|
|
# Wait for user input or timeout
|
|
@@ -2150,7 +1945,7 @@ async def browser_load_cookies(url: str, name: str = "", auto_navigate: bool = T
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
-# 新版统一入口(13 个 @tool,替代原来 28 个)
|
|
|
|
|
|
|
+# 对外统一的浏览器工具入口
|
|
|
# ============================================================
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2461,7 +2256,7 @@ async def browser_download(url: str, save_name: str = "") -> ToolResult:
|
|
|
url: 文件 URL
|
|
url: 文件 URL
|
|
|
save_name: 保存文件名(可选,默认自动推断)
|
|
save_name: 保存文件名(可选,默认自动推断)
|
|
|
"""
|
|
"""
|
|
|
- return await browser_download_direct_url(url=url, save_name=save_name or "download")
|
|
|
|
|
|
|
+ return await browser_download_direct_url(url=url, save_name=save_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
# ============================================================
|
|
@@ -2476,7 +2271,7 @@ __all__ = [
|
|
|
'cleanup_browser_session',
|
|
'cleanup_browser_session',
|
|
|
'kill_browser_session',
|
|
'kill_browser_session',
|
|
|
|
|
|
|
|
- # 13 个 @tool 入口
|
|
|
|
|
|
|
+ # 14 个 @tool 入口
|
|
|
'browser_navigate',
|
|
'browser_navigate',
|
|
|
'browser_search',
|
|
'browser_search',
|
|
|
'browser_back',
|
|
'browser_back',
|