codei-tools 0.0.11

终端优先的 AI 编程 Agent,用自然语言在本地仓库中读代码、改代码、跑命令、调试问题。
Documentation
use async_trait::async_trait;
use reqwest::header::CONTENT_TYPE;
use serde_json::{json, Value};

use super::web_common::{
    build_http_client, http_error, truncate_bytes, validate_http_url, validate_response_host,
};
use crate::{Tool, ToolContext, ToolError, ToolResult};

pub struct WebFetchTool {
    timeout_secs: u64,
    max_bytes: usize,
    ssrf_protection: bool,
}

impl WebFetchTool {
    pub fn new(timeout_secs: u64, max_bytes: usize, ssrf_protection: bool) -> Self {
        Self {
            timeout_secs,
            max_bytes,
            ssrf_protection,
        }
    }
}

#[async_trait]
impl Tool for WebFetchTool {
    fn name(&self) -> &str {
        "web_fetch"
    }

    fn description(&self) -> &str {
        "Fetch a public HTTP(S) URL and return the response body as text."
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "HTTP or HTTPS URL to fetch"
                }
            },
            "required": ["url"]
        })
    }

    async fn execute(&self, _ctx: &ToolContext, args: Value) -> Result<ToolResult, ToolError> {
        let url = args
            .get("url")
            .and_then(|v| v.as_str())
            .ok_or_else(|| ToolError::InvalidArgs("missing url".into()))?;

        let parsed = validate_http_url(url, self.ssrf_protection)?;
        let client = build_http_client(self.timeout_secs)?;

        let response = client
            .get(parsed.clone())
            .send()
            .await
            .map_err(|err| http_error("web_fetch", err))?;

        let status = response.status();
        let final_url = response.url().clone();
        validate_response_host(&final_url, self.ssrf_protection)?;

        let content_type = response
            .headers()
            .get(CONTENT_TYPE)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("unknown")
            .to_string();

        let bytes = response
            .bytes()
            .await
            .map_err(|err| http_error("web_fetch", err))?;
        let (body, truncated) = truncate_bytes(&bytes, self.max_bytes);

        let mut content =
            format!("url: {final_url}\nstatus: {status}\ncontent-type: {content_type}\n\n{body}");
        if truncated {
            content.push_str(&format!(
                "\n\n...(response truncated to {} bytes)",
                self.max_bytes
            ));
        }

        Ok(ToolResult {
            content,
            is_error: !status.is_success(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::super::web_common::validate_http_url;

    #[test]
    fn accepts_public_https_url() {
        assert!(validate_http_url("https://example.com/docs", true).is_ok());
    }

    #[test]
    fn rejects_non_http_schemes() {
        assert!(validate_http_url("file:///etc/passwd", true).is_err());
    }
}