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 f4c19615a..31cf94aba 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 @@ -72,10 +72,41 @@ pub(crate) fn append_direct_project_history_item_at( let path = history_path(root); let lock = project_append_lock_for(&path)?; let _append_guard = lock.lock("DirectProject 历史追加写")?; + if let Some(item_id) = item.get("id").and_then(Value::as_str) { + if read_direct_project_history_items_at(root)?.iter().any(|existing| { + existing.get("id").and_then(Value::as_str) == Some(item_id) + }) { + return Ok(()); + } + } let line = record(item)?; append_jsonl_line_unlocked(&path, &line, "DirectProject 历史") } +pub(crate) fn direct_project_local_message_item( + role: &str, + content: &str, + message_id: Option<&str>, +) -> Result { + let role = role.trim(); + let content = content.trim(); + if content.is_empty() || !matches!(role, "user" | "assistant") { + return Err("DirectProject 只接受非空 user/assistant 历史消息".to_string()); + } + let mut item = serde_json::json!({ + "type": "message", + "role": role, + "content": [{ + "type": if role == "user" { "input_text" } else { "output_text" }, + "text": content, + }], + }); + if let Some(message_id) = message_id.map(str::trim).filter(|value| !value.is_empty()) { + item["id"] = Value::String(message_id.to_string()); + } + Ok(item) +} + pub(crate) fn direct_project_user_message_item(prompt: &str) -> Value { serde_json::json!({ "type": "message", @@ -133,3 +164,47 @@ pub(crate) fn read_direct_project_history_items_at( } Ok(items) } + +pub(crate) fn read_direct_project_chat_history_at( + root: &Path, +) -> Result { + let path = history_path(root); + let items = read_direct_project_history_items_at(root)?; + let messages = items + .into_iter() + .filter_map(|item| { + let role = item.get("role").and_then(Value::as_str)?; + if !matches!(role, "user" | "assistant") { + return None; + } + let content = item + .get("content") + .and_then(Value::as_array) + .map(|parts| { + parts + .iter() + .filter_map(|part| part.get("text").and_then(Value::as_str)) + .collect::>() + .join("") + }) + .unwrap_or_default(); + (!content.is_empty()).then(|| LocalConversationMessageRecord { + schema_version: "agc-direct-project-context.v1".to_string(), + role: role.to_string(), + content, + agent_id: None, + message_id: item + .get("id") + .and_then(Value::as_str) + .map(str::to_string), + updated_at: 0, + }) + }) + .collect(); + Ok(LocalConversationResult { + path: path.to_string_lossy().into_owned(), + agent_id: None, + session_id: None, + messages, + }) +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 99eeb1fa9..aef229a17 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -1,4 +1,5 @@ use super::*; +use crate::agent::read_direct_project_chat_history_at; use crate::ui_editor::resource::font::FontAsset; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, HashSet}; @@ -4707,6 +4708,9 @@ pub(crate) fn read_local_conversation( ) -> Result { let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "conversation.read")?; + if agent_id.is_none() && session_id.is_none() { + return read_direct_project_chat_history_at(root); + } read_local_conversation_for_session_at(root, agent_id.as_deref(), session_id.as_deref()) } @@ -4720,6 +4724,15 @@ pub(crate) fn append_local_conversation_message( ) -> Result { let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "conversation.write")?; + if agent_id.is_none() && session_id.is_none() { + let item = direct_project_local_message_item( + &message.role, + &message.content, + message_id.as_deref(), + )?; + append_direct_project_history_item_at(root, &item)?; + return read_direct_project_chat_history_at(root); + } let _lock = acquire_project_write_lock(root, "conversation.write")?; match message_id.as_deref() { Some(message_id) => append_local_conversation_message_for_session_idempotent_at(