Skip to main content

rucora_tools/web/
fetch.rs

1//! 网页获取工具
2//!
3//! 获取网页的 HTML 内容
4
5use async_trait::async_trait;
6use rucora_core::{
7    error::ToolError,
8    tool::{Tool, ToolCategory},
9};
10use serde_json::{Value, json};
11use std::time::Duration;
12
13use super::security::validate_public_http_url;
14
15/// 最大响应体大小(字节)
16const MAX_RESPONSE_SIZE: usize = 5 * 1024 * 1024;
17
18/// 网页获取工具:获取网页内容。
19///
20/// 使用 HTTP 请求获取网页的 HTML 内容,支持超时设置。
21/// 与 HttpRequestTool 不同,这个工具专门用于获取网页内容。
22///
23/// 输入格式:
24/// ```json
25/// {
26///   "url": "https://example.com",
27///   "timeout": 30
28/// }
29/// ```
30pub struct WebFetchTool;
31
32impl WebFetchTool {
33    /// 创建一个新的 WebFetchTool 实例。
34    pub fn new() -> Self {
35        Self
36    }
37}
38
39impl Default for WebFetchTool {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45#[async_trait]
46impl Tool for WebFetchTool {
47    /// 返回工具名称。
48    fn name(&self) -> &str {
49        "web_fetch"
50    }
51
52    /// 返回工具描述。
53    fn description(&self) -> Option<&str> {
54        Some("获取网页的 HTML 内容")
55    }
56
57    /// 返回工具分类。
58    fn categories(&self) -> &'static [ToolCategory] {
59        &[ToolCategory::Network]
60    }
61
62    /// 返回输入参数的 JSON Schema。
63    fn input_schema(&self) -> Value {
64        json!({
65            "type": "object",
66            "properties": {
67                "url": {
68                    "type": "string",
69                    "description": "网页 URL"
70                },
71                "timeout": {
72                    "type": "integer",
73                    "description": "超时时间(秒),默认 30 秒",
74                    "default": 30
75                }
76            },
77            "required": ["url"]
78        })
79    }
80
81    /// 执行网页获取。
82    async fn call(&self, input: Value) -> Result<Value, ToolError> {
83        let url = input
84            .get("url")
85            .and_then(|v| v.as_str())
86            .ok_or_else(|| ToolError::Message("缺少必需的 'url' 字段".to_string()))?;
87
88        let timeout_secs = input.get("timeout").and_then(|v| v.as_u64()).unwrap_or(30);
89
90        validate_public_http_url(url, None, None).await?;
91
92        // 构建客户端
93        let client = reqwest::Client::builder()
94            .timeout(Duration::from_secs(timeout_secs))
95            .redirect(reqwest::redirect::Policy::none())
96            .user_agent("Mozilla/5.0 (compatible; rucora/0.1)")
97            .build()
98            .map_err(|e| ToolError::Message(format!("HTTP 客户端创建失败: {e}")))?;
99
100        // 发送 GET 请求
101        let response = client
102            .get(url)
103            .send()
104            .await
105            .map_err(|e| ToolError::Message(format!("获取网页失败: {e}")))?;
106
107        let status = response.status().as_u16();
108
109        // 获取响应体并限制大小
110        let body_bytes = response
111            .bytes()
112            .await
113            .map_err(|e| ToolError::Message(format!("读取响应体失败: {e}")))?;
114        if body_bytes.len() > MAX_RESPONSE_SIZE {
115            return Err(ToolError::Message(format!(
116                "响应体过大({} 字节),超过限制({} 字节)",
117                body_bytes.len(),
118                MAX_RESPONSE_SIZE
119            )));
120        }
121        let body = String::from_utf8_lossy(&body_bytes).to_string();
122
123        Ok(json!({
124            "url": url,
125            "status": status,
126            "html": body,
127            "success": (200..300).contains(&status)
128        }))
129    }
130}