实现 DirectProject 用户消息预写与回显过滤
在 turn/start 前幂等写入 AGC 规范化 user item 过滤 Codex userMessage 与 raw role=user 回显,保留 assistant/tool/partial 落盘 补充用户消息来源隔离回归测试
This commit is contained in:
@@ -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<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
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,
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user