Skip to main content

matrixcode_core/tools/
webfetch.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4use std::time::Duration;
5
6use super::{Tool, ToolDefinition};
7
8const DEFAULT_TIMEOUT_SECS: u64 = 30;
9const MAX_TIMEOUT_SECS: u64 = 120;
10
11pub struct WebFetchTool;
12
13#[async_trait]
14impl Tool for WebFetchTool {
15    fn definition(&self) -> ToolDefinition {
16        ToolDefinition {
17            name: "webfetch".to_string(),
18            description: "从 URL 获取内容并返回为文本。支持自定义超时时间。".to_string(),
19            parameters: json!({
20                "type": "object",
21                "properties": {
22                    "url": {
23                        "type": "string",
24                        "description": "要获取的 URL"
25                    },
26                    "max_length": {
27                        "type": "integer",
28                        "description": "最大响应长度(字符数,默认 10000)"
29                    },
30                    "timeout_secs": {
31                        "type": "integer",
32                        "description": format!("超时时间(秒,默认 {},最大 {})", DEFAULT_TIMEOUT_SECS, MAX_TIMEOUT_SECS)
33                    }
34                },
35                "required": ["url"]
36            }),
37            ..Default::default()
38        }
39    }
40
41    async fn execute(&self, params: Value) -> Result<String> {
42        let url = params["url"]
43            .as_str()
44            .ok_or_else(|| anyhow::anyhow!("missing 'url'"))?;
45        let max_length = params["max_length"].as_u64().unwrap_or(10000) as usize;
46        let timeout_secs = params["timeout_secs"]
47            .as_u64()
48            .unwrap_or(DEFAULT_TIMEOUT_SECS)
49            .min(MAX_TIMEOUT_SECS);
50
51        // Create client with timeout
52        let client = reqwest::Client::builder()
53            .timeout(Duration::from_secs(timeout_secs))
54            .connect_timeout(Duration::from_secs(10))
55            .build()?;
56
57        let response = tokio::time::timeout(
58            Duration::from_secs(timeout_secs + 5), // Extra buffer for response processing
59            client.get(url).send()
60        ).await
61            .map_err(|_| anyhow::anyhow!("request timed out after {}s", timeout_secs))?
62            .map_err(|e| anyhow::anyhow!("request failed: {}", e))?;
63
64        let status = response.status();
65
66        if !status.is_success() {
67            anyhow::bail!("HTTP {} for {}", status, url);
68        }
69
70        let body = response.text().await?;
71
72        let truncated = if body.len() > max_length {
73            let end = body.floor_char_boundary(max_length);
74            format!(
75                "{}...\n\n(truncated, {} total bytes)",
76                &body[..end],
77                body.len()
78            )
79        } else {
80            body
81        };
82
83        Ok(truncated)
84    }
85}