diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index ad1c292f6..9afbfff26 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -14,7 +14,6 @@ const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同(仅说明项 const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png"; const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png"; const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png"; -const MAX_DIRECT_VISIBLE_REPLY_CHARS: usize = 16 * 1024; const PLATFORM_GENERATION_SOURCE_PRESERVED_NO_RETRY_PREFIX: &str = "platform-generation-source-preserved-no-retry:"; const DIRECT_TAONIER_LOCAL_RECONCILIATION_PREFIX: &str = @@ -3661,24 +3660,15 @@ fn sync_direct_codex_project_outputs_at( sync_direct_codex_project_file_projection_at(root, previous_output_fingerprint) } -/// Project Codex text into the only form that may cross the DirectProject UI -/// boundary. The app-server stream can contain reasoning blocks, URLs, -/// credentials, or host paths before the final reply is known; those values -/// must never be emitted as an intermediate chat message or persisted as the -/// user-visible assistant turn. -fn project_direct_codex_visible_text(root: &Path, value: &str) -> Option { +/// Project Codex text for the user-visible DirectProject stream and reply. +/// Reasoning wrappers are still removed because they are not reply text, but +/// the user owns the project and the resulting reply is not redacted here. +fn project_direct_codex_visible_text(value: &str) -> Option { let stripped = strip_incomplete_direct_thinking_marker(&strip_llm_thinking_blocks(value)); if stripped.trim().is_empty() { return None; } - let redacted = redact_agent_runtime_error(root, &stripped, MAX_DIRECT_VISIBLE_REPLY_CHARS) - .replace("", "(链接已隐藏)") - .replace("$PROJECT_ROOT", "(项目路径已隐藏)") - .replace("", "(路径已隐藏)") - .replace("[redacted-secret]", "(敏感信息已隐藏)") - .replace("[redacted-sensitive-field]", "(敏感字段已隐藏)") - .replace("[redacted sensitive context]", "(内部信息已隐藏)"); - let visible = redacted.trim().to_string(); + let visible = stripped.trim().to_string(); (!visible.is_empty()).then_some(visible) } @@ -3695,14 +3685,13 @@ fn strip_incomplete_direct_thinking_marker(value: &str) -> String { } fn project_direct_codex_accumulated_text( - root: &Path, stream_enabled: bool, accumulated_text: &str, ) -> Option { if !stream_enabled { return None; } - project_direct_codex_visible_text(root, accumulated_text) + project_direct_codex_visible_text(accumulated_text) } pub(crate) fn build_direct_codex_system_prompt(root: &Path) -> Result { @@ -3915,7 +3904,7 @@ async fn run_direct_game_creator_turn_inner( let mut observer = move |observation: DirectCodexTurnObservation| match observation { DirectCodexTurnObservation::AccumulatedText(accumulated_text) => { let visible_text = - project_direct_codex_accumulated_text(root, stream_enabled, &accumulated_text); + project_direct_codex_accumulated_text(stream_enabled, &accumulated_text); if visible_text.is_none() { return; } @@ -3926,7 +3915,7 @@ async fn run_direct_game_creator_turn_inner( } DirectCodexTurnObservation::IntermediateText(intermediate_text) => { let visible_text = if stream_enabled { - project_direct_codex_visible_text(root, &intermediate_text) + project_direct_codex_visible_text(&intermediate_text) } else { None }; @@ -3963,7 +3952,7 @@ async fn run_direct_game_creator_turn_inner( .await } .map_err(|error| DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error))?; - let visible_reply = project_direct_codex_visible_text(root, &reply).ok_or_else(|| { + let visible_reply = project_direct_codex_visible_text(&reply).ok_or_else(|| { DirectCodexTurnFailure::new( DirectCodexFailureStage::CodeGeneration, "陶泥儿未返回可展示的回复".to_string(), @@ -4662,58 +4651,48 @@ mod tests { } #[test] - fn direct_visible_stream_projection_hides_internal_content() { - let root = tempfile::tempdir().expect("direct stream root"); - let project_file = root.path().join("game/index.html"); + fn direct_visible_stream_projection_keeps_user_project_reply_content() { + let project_file = std::path::Path::new("game/index.html"); let raw = format!( "先说一句\n内部推理不应显示\n来源 https://example.test/a\n路径 {}\nauthorization: Bearer secret-value-123", project_file.display() ); - let visible = - project_direct_codex_visible_text(root.path(), &raw).expect("safe visible stream text"); + let visible = project_direct_codex_visible_text(&raw).expect("visible stream text"); assert!(visible.contains("先说一句"), "{visible}"); assert!(!visible.contains("内部推理"), "{visible}"); - assert!(!visible.contains("https://example.test"), "{visible}"); + assert!(visible.contains("https://example.test"), "{visible}"); assert!( - !visible.contains(project_file.to_string_lossy().as_ref()), - "{visible}" - ); - assert!(!visible.contains("secret-value-123"), "{visible}"); - assert!(visible.contains("链接已隐藏"), "{visible}"); - assert!( - visible.contains("项目路径已隐藏") || visible.contains("路径已隐藏"), + visible.contains(project_file.to_string_lossy().as_ref()), "{visible}" ); + assert!(visible.contains("secret-value-123"), "{visible}"); } #[test] fn direct_visible_stream_projection_drops_unclosed_thinking_only_delta() { - let root = tempfile::tempdir().expect("direct stream root"); assert_eq!( - project_direct_codex_visible_text(root.path(), "secret reasoning"), + project_direct_codex_visible_text("secret reasoning"), None ); } #[test] fn direct_visible_stream_projection_hides_partial_thinking_tag() { - let root = tempfile::tempdir().expect("direct stream root"); assert_eq!( - project_direct_codex_visible_text(root.path(), "已公开内容\n