From 70fa1608194b98914990b7f31aa08a1bca5997cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 18 Sep 2026 14:00:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E6=B4=97=E9=99=84=E4=BB=B6=E6=91=98?= =?UTF-8?q?=E8=A6=81=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wire 投影复用附件名称与媒体类型的安全清洗。 附件项目路径输出归一化结果,避免原始控制字符进入模型提示。 --- .../src/agent/direct_codex_user_item/wire.rs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) 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");