From 593e03e2b9b4881c139feebacb5b5850d5dd8444 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sun, 8 Mar 2026 17:52:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81opencode=20=E7=9A=84=20Agent?= =?UTF-8?q?=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/opencode.rs | 111 +++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/parsers/opencode.rs b/src-tauri/src/parsers/opencode.rs index d1e1af2..a1ea90c 100644 --- a/src-tauri/src/parsers/opencode.rs +++ b/src-tauri/src/parsers/opencode.rs @@ -389,7 +389,9 @@ impl OpenCodeParser { }); } "file" => { - if let Some(file_ref) = extract_file_reference(&value) { + if let Some(image_block) = extract_opencode_file_image(&value) { + blocks.push(image_block); + } else if let Some(file_ref) = extract_file_reference(&value) { blocks.push(ContentBlock::Text { text: format!("@{}", file_ref), }); @@ -502,6 +504,81 @@ fn extract_file_reference(value: &serde_json::Value) -> Option { .map(|s| s.to_string()) } +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 extract_opencode_file_image(value: &serde_json::Value) -> Option { + let mime = value + .get("mime") + .or_else(|| value.get("mimeType")) + .or_else(|| value.get("mime_type")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|m| !m.is_empty() && m.starts_with("image/")) + .map(|s| s.to_string()); + + let url = value + .get("url") + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()); + + if let Some(raw_url) = url { + if let Some((mime_type, data)) = parse_data_uri_image(raw_url) { + let uri = value + .get("filename") + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + return Some(ContentBlock::Image { + data, + mime_type, + uri, + }); + } + } + + let mime_type = mime?; + let data = value + .get("data") + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string())?; + let uri = value + .get("filename") + .and_then(|v| v.as_str()) + .or_else(|| { + value + .get("source") + .and_then(|s| s.get("path")) + .and_then(|v| v.as_str()) + }) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + + Some(ContentBlock::Image { + data, + mime_type, + uri, + }) +} + fn is_error_status(status: &str) -> bool { matches!( status.to_ascii_lowercase().as_str(), @@ -620,7 +697,8 @@ fn group_into_turns(messages: Vec) -> Vec { #[cfg(test)] mod tests { - use super::resolve_xdg_data_home; + use super::{extract_opencode_file_image, resolve_xdg_data_home}; + use crate::models::ContentBlock; use std::path::PathBuf; #[test] @@ -637,4 +715,33 @@ mod tests { let resolved = resolve_xdg_data_home(None, Some(PathBuf::from("/Users/default"))); assert_eq!(resolved, Some(PathBuf::from("/Users/default/.local/share"))); } + + #[test] + fn parses_opencode_user_image_file_part_from_data_uri() { + let value = serde_json::json!({ + "type": "file", + "mime": "image/jpeg", + "filename": "avatar.jpg", + "url": "data:image/jpeg;base64,QUJD" + }); + + let block = extract_opencode_file_image(&value); + assert!(matches!( + block, + Some(ContentBlock::Image { data, mime_type, uri }) + if data == "QUJD" && mime_type == "image/jpeg" && uri.as_deref() == Some("avatar.jpg") + )); + } + + #[test] + fn ignores_non_image_file_part_for_image_parsing() { + let value = serde_json::json!({ + "type": "file", + "mime": "text/plain", + "filename": "notes.txt", + "url": "file:///tmp/notes.txt" + }); + + assert!(extract_opencode_file_image(&value).is_none()); + } }