初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,127 @@
|
||||
"""HTTP 类工具的直接执行器(非沙箱路径)。
|
||||
|
||||
``fetch_url``:抓取指定 URL 的 HTML 并用 html2text 转成 markdown。
|
||||
|
||||
executor 上下文由 ``ToolRuntime._build_exec_context`` 注入,至少包含:
|
||||
- ``http_client``:长生命周期的 ``httpx.AsyncClient``(禁止裸 ``async with httpx.AsyncClient()``)。
|
||||
- ``trace_id``:本次执行的 trace_id(可空)。
|
||||
|
||||
设计约束(见 backend/CLAUDE.md):
|
||||
- 禁止在调用处裸 ``async with httpx.AsyncClient()``,必须复用注入的 client。
|
||||
- HTTP 错误统一返回 ``ToolResult(ok=False, stderr=...)``,不抛异常(让模型自决策)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import html2text
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
from app.modules.tool_runtime.definition import ToolInput, ToolResult
|
||||
|
||||
# 默认参数
|
||||
_DEFAULT_MAX_CHARS = 8000
|
||||
# 合理的浏览器 UA,避免被部分站点拒绝
|
||||
_USER_AGENT = "Mozilla/5.0 (compatible; HunjingBot/1.0; +https://github.com/hunjing/hunjing)"
|
||||
|
||||
# 模块级 html2text 配置(避免每次调用都构造配置对象)
|
||||
_H2T = html2text.HTML2Text()
|
||||
_H2T.ignore_links = False
|
||||
_H2T.ignore_images = True
|
||||
_H2T.body_width = 0 # 不折行,交给上层截断
|
||||
|
||||
|
||||
def _get_http_client(context: dict[str, Any] | None) -> httpx.AsyncClient | None:
|
||||
"""从 context 取注入的 httpx.AsyncClient;缺失返回 None。"""
|
||||
if context is None:
|
||||
return None
|
||||
client = context.get("http_client")
|
||||
return client if isinstance(client, httpx.AsyncClient) else None
|
||||
|
||||
|
||||
def _ok_result(stdout: str, trace_id: str = "") -> ToolResult:
|
||||
return ToolResult(
|
||||
ok=True,
|
||||
exit_code=0,
|
||||
stdout=stdout,
|
||||
stderr="",
|
||||
duration_ms=0,
|
||||
timeout=False,
|
||||
truncated=False,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
|
||||
def _err_result(stderr: str, trace_id: str = "") -> ToolResult:
|
||||
return ToolResult(
|
||||
ok=False,
|
||||
exit_code=1,
|
||||
stdout="",
|
||||
stderr=stderr,
|
||||
duration_ms=0,
|
||||
timeout=False,
|
||||
truncated=False,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_url_executor(input: ToolInput, context: dict[str, Any] | None) -> ToolResult:
|
||||
"""抓取指定 URL 的网页正文并转 markdown。
|
||||
|
||||
input:
|
||||
url (str, required): 要抓取的 URL。
|
||||
max_chars (int, optional): 正文最大字符数,默认 8000。
|
||||
context:
|
||||
http_client (httpx.AsyncClient): 注入的 HTTP 客户端。
|
||||
trace_id (str): 本次执行 trace_id。
|
||||
"""
|
||||
trace_id = (context or {}).get("trace_id", "") or ""
|
||||
url = input.get("url")
|
||||
if not isinstance(url, str) or not url.strip():
|
||||
return _err_result("fetch_url 需要 'url' 字符串输入。", trace_id)
|
||||
|
||||
raw_max = input.get("max_chars", _DEFAULT_MAX_CHARS)
|
||||
try:
|
||||
max_chars = int(raw_max) if raw_max is not None else _DEFAULT_MAX_CHARS
|
||||
except (TypeError, ValueError):
|
||||
max_chars = _DEFAULT_MAX_CHARS
|
||||
if max_chars < 1:
|
||||
max_chars = _DEFAULT_MAX_CHARS
|
||||
|
||||
client = _get_http_client(context)
|
||||
if client is None:
|
||||
return _err_result("fetch_url 缺少注入的 http_client。", trace_id)
|
||||
|
||||
try:
|
||||
resp = await client.get(
|
||||
url,
|
||||
headers={"User-Agent": _USER_AGENT},
|
||||
timeout=settings.tool_web_http_timeout_seconds,
|
||||
follow_redirects=True,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
return _err_result(f"fetch_url 请求失败:{exc}", trace_id)
|
||||
|
||||
if resp.status_code >= 400:
|
||||
return _err_result(f"fetch_url 返回 HTTP {resp.status_code}", trace_id)
|
||||
|
||||
content_type = resp.headers.get("content-type", "").lower()
|
||||
html_text = resp.text
|
||||
# 无论 content-type 如何都尝试转 markdown(部分站点 content-type 不规范)
|
||||
try:
|
||||
markdown = _H2T.handle(html_text)
|
||||
except Exception: # noqa: BLE001 - html2text 解析失败时回退原文
|
||||
markdown = html_text
|
||||
|
||||
# 规范化多余空白并截断
|
||||
cleaned = "\n".join(line.rstrip() for line in markdown.splitlines())
|
||||
cleaned = cleaned.strip()
|
||||
if len(cleaned) > max_chars:
|
||||
cleaned = f"{cleaned[:max_chars]}\n\n[...已截断,共 {len(cleaned)} 字符]"
|
||||
|
||||
if not cleaned:
|
||||
return _err_result(f"fetch_url 未抓取到正文内容(content-type={content_type})。", trace_id)
|
||||
|
||||
return _ok_result(cleaned, trace_id)
|
||||
Reference in new issue
Block a user