清洗附件摘要元数据

wire 投影复用附件名称与媒体类型的安全清洗。

附件项目路径输出归一化结果,避免原始控制字符进入模型提示。
This commit is contained in:
2026-09-18 14:00:00 +08:00
parent 2e78cbf152
commit 70fa160819
@@ -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");