ForcePilot/backend/package/yuxi/agents/toolkits/buildin/crawl/client.py

113 lines
3.7 KiB
Python
Raw Normal View History

"""Crawl4AI 服务 HTTP 客户端,封装 /crawl 端点调用。
Crawl4AI 服务监听 11235 端口Gunicorn仅内网访问 http://crawl4ai:11235
本模块负责单页抓取与多页递归抓取的 HTTP 调用封装
"""
import httpx
from yuxi.config import config
class Crawl4AIClient:
"""Crawl4AI 服务 HTTP 客户端,封装 /crawl 端点调用。
Crawl4AI 服务监听 11235 端口Gunicorn仅内网访问 http://crawl4ai:11235
"""
def __init__(self):
self._client: httpx.AsyncClient | None = None
async def crawl_single(
self,
url: str,
*,
output_format: str = "markdown",
check_robots_txt: bool = True,
) -> dict:
"""单页抓取,返回 CrawlResult JSON。
Crawl4AI /crawl 接口请求体字段为 urls复数可传字符串或数组
"""
base_url = config.crawl4ai_base_url
timeout = config.crawl4ai_timeout
headers = self._build_headers()
body = {
"urls": url,
"crawler_run_config": {
"output_format": output_format,
"check_robots_txt": check_robots_txt,
"fit_markdown": True, # 启用 fit_markdown 噪声剥离
},
}
client = self._get_client()
response = await client.post(
f"{base_url}/crawl", json=body, headers=headers, timeout=timeout
)
response.raise_for_status()
data = response.json()
# Crawl4AI 返回结构为 {"results": [CrawlResult, ...]}
results = data.get("results", [])
return results[0] if results else {}
async def crawl_many(
self,
urls: list[str],
*,
max_depth: int = 2,
max_pages: int = 20,
url_pattern: str = "",
check_robots_txt: bool = True,
semaphore_count: int = 5,
) -> list[dict]:
"""多页递归抓取,返回 CrawlResult 列表。
使用 Crawl4AI BFSDeepCrawlStrategy广度优先include_external=False 仅抓同域
"""
base_url = config.crawl4ai_base_url
# 多页抓取超时放宽到 5 分钟
timeout = min(300, config.crawl4ai_timeout * 10)
headers = self._build_headers()
crawler_config = {
"deep_crawl_strategy": {
"type": "BFSDeepCrawlStrategy",
"max_depth": max_depth,
"max_pages": max_pages,
"include_external": False,
"url_pattern": url_pattern or None,
},
"crawler_run_config": {
"output_format": "markdown",
"check_robots_txt": check_robots_txt,
"fit_markdown": True,
},
"arun_many_config": {
"semaphore_count": semaphore_count,
},
}
body = {"urls": urls, **crawler_config}
client = self._get_client()
response = await client.post(
f"{base_url}/crawl", json=body, headers=headers, timeout=timeout
)
response.raise_for_status()
data = response.json()
return data.get("results", [])
def _build_headers(self) -> dict:
"""构建请求头,含可选的 Bearer Token 鉴权。"""
headers = {"Content-Type": "application/json"}
token = config.crawl4ai_api_token
if token:
headers["Authorization"] = f"Bearer {token}"
return headers
def _get_client(self) -> httpx.AsyncClient:
"""懒初始化并复用 httpx.AsyncClient 连接池。"""
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient()
return self._client
# 模块级单例
crawl4ai_client = Crawl4AIClient()