支持gemini cli 的 Agent 解析用户消息里的图片
This commit is contained in:
@@ -154,6 +154,109 @@ impl GeminiParser {
|
|||||||
.or_else(|| message.get("message").and_then(Self::extract_text))
|
.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<ContentBlock> {
|
||||||
|
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<ContentBlock> {
|
||||||
|
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(
|
fn parse_summary_from_value(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
@@ -382,13 +485,14 @@ impl GeminiParser {
|
|||||||
|
|
||||||
match msg_type.as_str() {
|
match msg_type.as_str() {
|
||||||
"user" => {
|
"user" => {
|
||||||
let Some(text) = Self::extract_message_text(&raw) else {
|
let blocks = Self::parse_user_blocks(&raw);
|
||||||
|
if blocks.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
messages.push(UnifiedMessage {
|
messages.push(UnifiedMessage {
|
||||||
id: msg_id,
|
id: msg_id,
|
||||||
role: MessageRole::User,
|
role: MessageRole::User,
|
||||||
content: vec![ContentBlock::Text { text }],
|
content: blocks,
|
||||||
timestamp,
|
timestamp,
|
||||||
usage: None,
|
usage: None,
|
||||||
duration_ms: None,
|
duration_ms: None,
|
||||||
@@ -609,6 +713,7 @@ fn group_into_turns(messages: Vec<UnifiedMessage>) -> Vec<MessageTurn> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::resolve_gemini_base_dir_from;
|
use super::resolve_gemini_base_dir_from;
|
||||||
use super::GeminiParser;
|
use super::GeminiParser;
|
||||||
|
use crate::models::ContentBlock;
|
||||||
use crate::parsers::AgentParser;
|
use crate::parsers::AgentParser;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -706,4 +811,44 @@ mod tests {
|
|||||||
let resolved = resolve_gemini_base_dir_from(None, Some(PathBuf::from("/Users/default")));
|
let resolved = resolve_gemini_base_dir_from(None, Some(PathBuf::from("/Users/default")));
|
||||||
assert_eq!(resolved, PathBuf::from("/Users/default/.gemini"));
|
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()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user