From c5537f63b0c73565af3391af0ce275c4495d522a Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sun, 8 Mar 2026 17:46:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81gemini=20cli=20=E7=9A=84=20Ag?= =?UTF-8?q?ent=20=E8=A7=A3=E6=9E=90=E7=94=A8=E6=88=B7=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=87=8C=E7=9A=84=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/parsers/gemini.rs | 151 +++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/parsers/gemini.rs b/src-tauri/src/parsers/gemini.rs index 27a0c35..f37aa40 100644 --- a/src-tauri/src/parsers/gemini.rs +++ b/src-tauri/src/parsers/gemini.rs @@ -154,6 +154,109 @@ impl GeminiParser { .or_else(|| message.get("message").and_then(Self::extract_text)) } + fn parse_data_uri_image(raw: &str) -> Option<(String, String)> { + let trimmed = raw.trim(); + let without_prefix = trimmed.strip_prefix("data:")?; + let marker = ";base64,"; + let marker_idx = without_prefix.find(marker)?; + let mime_type = without_prefix.get(..marker_idx)?.trim(); + if !mime_type.starts_with("image/") { + return None; + } + let data = without_prefix.get(marker_idx + marker.len()..)?.trim(); + if data.is_empty() { + return None; + } + Some((mime_type.to_string(), data.to_string())) + } + + fn parse_user_image_part(part: &serde_json::Value) -> Option { + let inline = part + .get("inlineData") + .or_else(|| part.get("inline_data")) + .unwrap_or(part); + let data = inline + .get("data") + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|v| !v.is_empty())?; + + if let Some((mime_type, data)) = Self::parse_data_uri_image(data) { + return Some(ContentBlock::Image { + data, + mime_type, + uri: None, + }); + } + + let mime_type = inline + .get("mimeType") + .or_else(|| inline.get("mime_type")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|m| !m.is_empty() && m.starts_with("image/"))?; + let uri = inline + .get("fileUri") + .or_else(|| inline.get("uri")) + .and_then(|u| u.as_str()) + .map(|s| s.to_string()); + + Some(ContentBlock::Image { + data: data.to_string(), + mime_type: mime_type.to_string(), + uri, + }) + } + + fn parse_user_blocks(message: &serde_json::Value) -> Vec { + let mut blocks = Vec::new(); + let content = match message.get("content") { + Some(c) => c, + None => { + if let Some(text) = message.get("message").and_then(Self::extract_text) { + blocks.push(ContentBlock::Text { text }); + } + return blocks; + } + }; + + if let Some(text) = content + .as_str() + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + { + blocks.push(ContentBlock::Text { text }); + return blocks; + } + + if let Some(parts) = content.as_array() { + for part in parts { + if let Some(text) = part.get("text").and_then(Self::extract_text) { + blocks.push(ContentBlock::Text { text }); + } else if let Some(text) = Self::extract_text(part) { + blocks.push(ContentBlock::Text { text }); + } + + if let Some(image) = Self::parse_user_image_part(part) { + blocks.push(image); + } + } + return blocks; + } + + if let Some(image) = Self::parse_user_image_part(content) { + blocks.push(image); + return blocks; + } + + if let Some(text) = Self::extract_text(content) { + blocks.push(ContentBlock::Text { text }); + } + + blocks + } + fn parse_summary_from_value( &self, path: &Path, @@ -382,13 +485,14 @@ impl GeminiParser { match msg_type.as_str() { "user" => { - let Some(text) = Self::extract_message_text(&raw) else { + let blocks = Self::parse_user_blocks(&raw); + if blocks.is_empty() { continue; - }; + } messages.push(UnifiedMessage { id: msg_id, role: MessageRole::User, - content: vec![ContentBlock::Text { text }], + content: blocks, timestamp, usage: None, duration_ms: None, @@ -609,6 +713,7 @@ fn group_into_turns(messages: Vec) -> Vec { mod tests { use super::resolve_gemini_base_dir_from; use super::GeminiParser; + use crate::models::ContentBlock; use crate::parsers::AgentParser; use std::env; use std::fs; @@ -706,4 +811,44 @@ mod tests { let resolved = resolve_gemini_base_dir_from(None, Some(PathBuf::from("/Users/default"))); assert_eq!(resolved, PathBuf::from("/Users/default/.gemini")); } + + #[test] + fn parses_user_inline_image_block() { + let message = serde_json::json!({ + "content": [ + {"text": "这是什么"}, + {"inlineData": {"mimeType": "image/jpeg", "data": "QUJD"}} + ] + }); + + let blocks = GeminiParser::parse_user_blocks(&message); + assert_eq!(blocks.len(), 2); + assert!(matches!(&blocks[0], ContentBlock::Text { text } if text == "这是什么")); + assert!(matches!( + &blocks[1], + ContentBlock::Image { data, mime_type, uri } + if data == "QUJD" && mime_type == "image/jpeg" && uri.is_none() + )); + } + + #[test] + fn parses_user_data_uri_image_block() { + let message = serde_json::json!({ + "content": [ + { + "inlineData": { + "data": "data:image/png;base64,QUJD" + } + } + ] + }); + + let blocks = GeminiParser::parse_user_blocks(&message); + assert_eq!(blocks.len(), 1); + assert!(matches!( + &blocks[0], + ContentBlock::Image { data, mime_type, uri } + if data == "QUJD" && mime_type == "image/png" && uri.is_none() + )); + } }