diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs index a05daef41..ca26742f5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/wire.rs @@ -1,6 +1,9 @@ use super::model::{DirectCodexUserContentPart, DirectCodexUserItem}; use super::validation::validate_direct_codex_user_item; -use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path}; +use crate::agent::{ + read_manifest_for_project, sanitize_attachment_local_path, sanitize_attachment_media_type, + sanitize_attachment_name, +}; use serde_json::Value; use std::path::Path; @@ -101,14 +104,15 @@ pub(crate) fn direct_codex_user_item_to_wire_input( summary } DirectCodexUserContentPart::AgcAttachmentReference(reference) => { + let name = sanitize_attachment_name(&reference.name); + let media_type = sanitize_attachment_media_type(&reference.media_type); + let local_path = sanitize_attachment_local_path(&reference.local_path); let mut summary = format!( "[附件:名称={};类型={};大小={} 字节", - reference.name.trim(), - reference.media_type.trim(), - reference.size + name, media_type, reference.size ); - if !reference.local_path.trim().is_empty() { - summary.push_str(&format!(";项目路径={}", reference.local_path.trim())); + if let Some(local_path) = local_path { + summary.push_str(&format!(";项目路径={local_path}")); } summary.push_str(&format!(";状态={}", reference.status.trim())); summary.push(']'); @@ -212,6 +216,36 @@ mod tests { assert!(content[1]["text"].as_str().unwrap().contains("notes.txt")); } + #[test] + fn attachment_metadata_is_sanitized_before_prompt_projection() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试") + .expect("init project"); + let item = json!({ + "type": "message", + "role": "user", + "id": "turn-1:user", + "content": [{ + "type": "agc_attachment_reference", + "name": "C:\\tmp\\notes\nsecret.md", + "mediaType": "text/plain\nBearer secret", + "size": 4, + "localPath": "assets\\.\\notes.txt", + "status": "imported" + }] + }); + let wire = super::direct_codex_user_item_to_wire_input( + root.path(), + &serde_json::from_value(item).expect("deserialize user item"), + ) + .expect("attachment metadata should project"); + let text = wire[0]["text"].as_str().expect("wire text"); + assert!(text.contains("名称=notessecret.md"), "{text}"); + assert!(text.contains("类型=application/octet-stream"), "{text}"); + assert!(text.contains("项目路径=assets/notes.txt"), "{text}"); + assert!(!text.contains("Bearer secret"), "{text}"); + } + #[test] fn whitespace_only_text_parts_survive_validation() { let root = tempfile::tempdir().expect("temp project");