From 928fc6ffbc9947640a7f08dfd4ef5f9872dfba79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 7 Sep 2026 15:41:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20DirectProject=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=B6=88=E6=81=AF=E9=A2=84=E5=86=99=E4=B8=8E=E5=9B=9E?= =?UTF-8?q?=E6=98=BE=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 turn/start 前幂等写入 AGC 规范化 user item 过滤 Codex userMessage 与 raw role=user 回显,保留 assistant/tool/partial 落盘 补充用户消息来源隔离回归测试 --- .../src-tauri/src/agent/codex_app_server.rs | 33 +++++++-- .../src/agent/direct_project_history.rs | 72 ++++++++++++++++++- .../src-tauri/src/agent/direct_runtime.rs | 2 +- 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index a93dd519d..3cc1860d3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, OnceLock, Weak}; use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::sync::{mpsc, oneshot, Mutex, Notify}; +use uuid::Uuid; const GAME_CREATOR_CODEX_APP_SERVER_PROVIDER_ID: &str = "genarrative_agc"; const GAME_CREATOR_CODEX_APP_SERVER_API_KEY_ENV: &str = "GENARRATIVE_AGC_CODEX_API_KEY"; @@ -2129,15 +2130,12 @@ impl CodexAppServerConnection { llm: &GameCreatorLlmConfig, request: LlmRunRequest, direct_history_root: Option<&std::path::Path>, - _direct_client_turn_id: Option<&str>, + direct_client_turn_id: Option<&str>, mut on_agent_message_delta: Option<&mut (dyn FnMut(&platform_llm::LlmStreamDelta) + Send)>, mut direct_observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>, mut audit: Option<&mut DirectCodexTurnAudit>, ) -> Result { let _turn_guard = self.inner.turn_gate.lock().await; - let (thread_lease, thread_created) = self.thread_for(snapshot, &request, llm).await?; - self.wait_for_initial_client_mcp_startup().await; - let thread_id = thread_lease.thread_id.clone(); let mut request = request; let history_root = direct_history_root.unwrap_or(&self.inner.workspace_path); if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { @@ -2147,6 +2145,21 @@ impl CodexAppServerConnection { "DirectProject 用户消息不能为空".to_string(), )); } + if let Some(client_turn_id) = direct_client_turn_id { + let user_item = direct_project_local_message_item( + "user", + current_prompt, + Some(&format!("direct-codex:{client_turn_id}:user")), + ) + .map_err(platform_llm::LlmError::InvalidRequest)?; + append_direct_project_user_message_at(history_root, &user_item) + .map_err(platform_llm::LlmError::InvalidRequest)?; + } + } + let (thread_lease, thread_created) = self.thread_for(snapshot, &request, llm).await?; + self.wait_for_initial_client_mcp_startup().await; + let thread_id = thread_lease.thread_id.clone(); + if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { if thread_created { let items = match read_direct_project_history_items_at(history_root) { Ok(items) => items, @@ -2203,7 +2216,7 @@ impl CodexAppServerConnection { model, &self.inner.workspace_path, self.inner.workspace_mode, - _direct_client_turn_id, + direct_client_turn_id, ); apply_game_creator_codex_app_server_reasoning_effort(&mut params, &request); if let Some(schema) = game_creator_codex_cli_tool_output_schema(&request) { @@ -3288,13 +3301,21 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer( .with_model(config.llm.model.clone()) .with_request_timeout_ms(config.llm.request_timeout_ms) .with_max_output_tokens(16_000); + let generated_client_turn_id; + let effective_client_turn_id = match client_turn_id { + Some(client_turn_id) => Some(client_turn_id), + None => { + generated_client_turn_id = format!("direct-cli-{}", Uuid::new_v4()); + Some(generated_client_turn_id.as_str()) + } + }; connection .run_turn_with_direct_observer_and_history( &snapshot, &config.llm, request, Some(&codex_root), - client_turn_id, + effective_client_turn_id, None, observer, audit, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs index 6919a89e3..42777dcfa 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs @@ -178,11 +178,32 @@ fn direct_project_history_item_from_line( pub(crate) fn append_direct_project_history_item_at( root: &Path, item: &Value, +) -> Result<(), String> { + append_direct_project_history_item_at_with_user_policy(root, item, false) +} + +/// Appends a user message authored by AGC itself. Codex response items use +/// the default path above, which deliberately ignores echoed user messages; +/// this explicit entry point keeps the two sources distinct. +pub(crate) fn append_direct_project_user_message_at( + root: &Path, + item: &Value, +) -> Result<(), String> { + append_direct_project_history_item_at_with_user_policy(root, item, true) +} + +fn append_direct_project_history_item_at_with_user_policy( + root: &Path, + item: &Value, + allow_user_item: bool, ) -> Result<(), String> { enforce_project_permission_policy(root, "conversation.write")?; if is_direct_project_internal_context_item(item) { return Ok(()); } + if !allow_user_item && is_direct_project_codex_user_item(item) { + return Ok(()); + } let _project_lock = crate::project::acquire_project_write_lock(root, "conversation.write")?; let path = history_path(root); let history_exists = @@ -203,6 +224,19 @@ pub(crate) fn append_direct_project_history_item_at( append_jsonl_line_unlocked(&path, &line, "DirectProject 历史") } +fn is_direct_project_codex_user_item(item: &Value) -> bool { + if item.get("type").and_then(Value::as_str) == Some("userMessage") { + return true; + } + if item.get("role").and_then(Value::as_str) != Some("user") { + return false; + } + !item + .get("id") + .and_then(Value::as_str) + .is_some_and(|id| id.starts_with("direct-codex:") && id.ends_with(":user")) +} + pub(crate) fn direct_project_local_message_item( role: &str, content: &str, @@ -321,7 +355,7 @@ pub(crate) fn read_direct_project_chat_history_at( #[cfg(test)] mod tests { use super::{ - append_direct_project_history_item_at, history_path, + append_direct_project_history_item_at, append_direct_project_user_message_at, history_path, is_direct_project_internal_context_item, read_direct_project_history_items_at, }; use serde_json::json; @@ -394,4 +428,40 @@ mod tests { read_direct_project_history_items_at(root.path()).expect("read repaired history"); assert_eq!(items, vec![item]); } + + #[test] + fn codex_user_echo_is_filtered_but_agc_user_message_is_persisted() { + let root = tempfile::tempdir().expect("temp project"); + crate::init_local_game_project_at(root.path(), "user-echo", "用户回显过滤") + .expect("init project"); + let user = serde_json::json!({ + "type": "message", + "role": "user", + "id": "direct-codex:turn-0001:user", + "content": [{"type": "input_text", "text": "请创建菜单"}] + }); + append_direct_project_user_message_at(root.path(), &user).expect("persist AGC user"); + append_direct_project_history_item_at( + root.path(), + &serde_json::json!({ + "type": "userMessage", + "id": "codex-user-item-1", + "clientId": "turn-0001", + "content": [{"type": "text", "text": "请创建菜单"}] + }), + ) + .expect("ignore Codex echo"); + append_direct_project_history_item_at( + root.path(), + &serde_json::json!({ + "type": "message", + "role": "user", + "id": "codex-raw-user-item-1", + "content": [{"type": "input_text", "text": "请创建菜单"}] + }), + ) + .expect("ignore raw Codex user echo"); + let items = read_direct_project_history_items_at(root.path()).expect("read history"); + assert_eq!(items, vec![user]); + } } 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 679908d4c..2dd661a36 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 @@ -4241,7 +4241,7 @@ fn persist_direct_codex_user_prompt_at( prompt, Some(&format!("direct-codex:{client_turn_id}:user")), )?; - append_direct_project_history_item_at(root, &item) + append_direct_project_user_message_at(root, &item) } #[cfg(test)]