use futures::future::BoxFuture;
use serde_json::Value;
use super::security::{ResourceLimits, SecurityConfig, create_safe_http_client, validate_url};
use crate::error::{Result, ToolError};
use crate::tools::{Tool, ToolParameters, ToolResult};
pub struct ImageAnalysisTool;
impl Tool for ImageAnalysisTool {
fn name(&self) -> &str {
"analyze_image"
}
fn description(&self) -> &str {
"分析图片内容,描述图片中的信息。支持从文件路径、URL 或 base64 数据读取图片。返回图片的详细描述和分析结果。"
}
fn parameters(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "图片来源类型:'file'(文件路径)、'url'(网络地址)或 'base64'(编码数据)"
},
"data": {
"type": "string",
"description": "图片数据:文件路径、URL 或 base64 编码的图片数据"
},
"prompt": {
"type": "string",
"description": "分析提示词,告诉 LLM 你希望从图片中获取什么信息(可选)"
}
},
"required": ["source", "data"]
})
}
fn execute(&self, parameters: ToolParameters) -> BoxFuture<'_, Result<ToolResult>> {
Box::pin(async move {
let source = parameters
.get("source")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::MissingParameter("source".to_string()))?;
let data = parameters
.get("data")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::MissingParameter("data".to_string()))?;
let prompt = parameters.get("prompt").and_then(|v| v.as_str()).unwrap_or(
"请详细描述这张图片的内容,包括主要元素、颜色、布局和任何可见的文字信息。",
);
let security = SecurityConfig::global();
let (base64_data, mime_type) = match source {
"file" => read_image_from_file(data, &security)?,
"url" => fetch_image_from_url(data, &security.limits).await?,
"base64" => {
let mime_type = detect_mime_type_from_base64(data);
validate_base64_size(data, &security.limits)?;
(data.to_string(), mime_type)
}
_ => {
return Err(ToolError::InvalidParameter {
name: "source".to_string(),
message: format!(
"不支持的来源类型: '{}', 请使用 'file', 'url' 或 'base64'",
source
),
}
.into());
}
};
let size_kb = base64_data.len() as f64 / 1024.0;
let estimated_raw_kb = (base64_data.len() as f64 * 3.0 / 4.0) / 1024.0;
Ok(ToolResult::success(format!(
"图片已成功加载。\n- MIME 类型: {}\n- Base64 长度: {:.1} KB\n- 估算原始大小: {:.1} KB\n- 分析提示: {}\n\n提示:图片数据已加载但无法直接以文本形式展示给 LLM。如需分析图片内容,请考虑使用支持多模态的 LLM 模型,或使用浏览器工具查看图片 URL。",
mime_type, size_kb, estimated_raw_kb, prompt,
)))
})
}
}
fn read_image_from_file(path: &str, security: &SecurityConfig) -> Result<(String, String)> {
use std::fs;
let path_obj = security.validate_file(path)?;
let mime_type = detect_image_mime_type(&path_obj);
let bytes = fs::read(&path_obj).map_err(|e| ToolError::ExecutionFailed {
tool: "analyze_image".to_string(),
message: format!("读取文件失败: {}", e),
})?;
let base64_data = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
Ok((base64_data, mime_type))
}
async fn fetch_image_from_url(url: &str, limits: &ResourceLimits) -> Result<(String, String)> {
validate_url(url)?;
let client = create_safe_http_client(limits)?;
let response = client
.get(url)
.send()
.await
.map_err(|e| ToolError::ExecutionFailed {
tool: "analyze_image".to_string(),
message: format!("请求图片失败: {}", e),
})?;
if !response.status().is_success() {
return Err(ToolError::ExecutionFailed {
tool: "analyze_image".to_string(),
message: format!("HTTP 请求失败: {}", response.status()),
}
.into());
}
if let Some(content_length) = response.headers().get("content-length")
&& let Ok(len_str) = content_length.to_str()
&& let Ok(len) = len_str.parse::<u64>()
&& len > limits.http_max_size
{
return Err(ToolError::FileTooLarge {
size: len,
max: limits.http_max_size,
}
.into());
}
let mime_type = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.and_then(|ct| {
ct.split(';').next()
})
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "image/png".to_string());
let bytes = response
.bytes()
.await
.map_err(|e| ToolError::ExecutionFailed {
tool: "analyze_image".to_string(),
message: format!("读取图片数据失败: {}", e),
})?;
if bytes.len() as u64 > limits.http_max_size {
return Err(ToolError::FileTooLarge {
size: bytes.len() as u64,
max: limits.http_max_size,
}
.into());
}
let base64_data = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
Ok((base64_data, mime_type))
}
fn detect_image_mime_type(path: &std::path::Path) -> String {
use std::fs::File;
use std::io::Read;
if let Ok(mut file) = File::open(path) {
let mut buf = [0u8; 16];
if let Ok(n) = file.read(&mut buf) {
let header = &buf[..n];
if header.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return "image/png".to_string();
}
if header.starts_with(&[0xFF, 0xD8, 0xFF]) {
return "image/jpeg".to_string();
}
if header.starts_with(b"GIF8") {
return "image/gif".to_string();
}
if header.len() >= 12 && &header[0..4] == b"RIFF" && &header[8..12] == b"WEBP" {
return "image/webp".to_string();
}
if header.starts_with(b"BM") {
return "image/bmp".to_string();
}
}
}
match path.extension().and_then(|e| e.to_str()) {
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("gif") => "image/gif",
Some("webp") => "image/webp",
Some("bmp") => "image/bmp",
_ => "image/png",
}
.to_string()
}
fn detect_mime_type_from_base64(data: &str) -> String {
if data.starts_with("iVBORw0KGgo") {
"image/png"
} else if data.starts_with("/9j/") {
"image/jpeg"
} else if data.starts_with("R0lGOD") {
"image/gif"
} else if data.starts_with("UklGR") {
"image/webp"
} else {
"image/png" }
.to_string()
}
fn validate_base64_size(data: &str, limits: &ResourceLimits) -> Result<()> {
let estimated_size = (data.len() as u64 * 3) / 4;
if estimated_size > limits.max_file_size {
return Err(ToolError::FileTooLarge {
size: estimated_size,
max: limits.max_file_size,
}
.into());
}
Ok(())
}