修复 cc 反馈回复的 DirectProject 条目冲突
Project CI / AI game creator shell Rust crates (push) Successful in 1m41s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m23s
Project CI / Backend tests (push) Successful in 4m50s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m9s
Project CI / Frontend tests (push) Successful in 2m25s
Project CI / AI game creator shell web tests (push) Successful in 2m29s
Project CI / Native shell tests (push) Successful in 5m59s
Project CI / Repository checks (push) Successful in 3m0s
Project CI / AI game creator shell Rust crates (push) Successful in 1m41s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m23s
Project CI / Backend tests (push) Successful in 4m50s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m9s
Project CI / Frontend tests (push) Successful in 2m25s
Project CI / AI game creator shell web tests (push) Successful in 2m29s
Project CI / Native shell tests (push) Successful in 5m59s
Project CI / Repository checks (push) Successful in 3m0s
同一 client turn 的反馈回复冲突时生成独立 assistant item ID 保持相同回复幂等并补充回归测试与排障记录
This commit is contained in:
@@ -984,21 +984,15 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
// 项目对话历史是这条对话的单一事实源:回复不落盘,UI 就看不到本轮结果。codex 路径由
|
||||
// app-server 的 collect-history 负责写 assistant 条目,cc 没有那一步——只补终态会让
|
||||
// 用户看到"回合结束但没有回复"。落盘失败按回合失败收口,不吞。
|
||||
let item_id = match client_turn_id {
|
||||
Some(client_turn_id) => format!("direct-codex:{client_turn_id}:assistant"),
|
||||
None => format!("direct-codex:{}:assistant", uuid::Uuid::new_v4()),
|
||||
let item_id = match persist_direct_claude_assistant_reply_at(root, client_turn_id, text)
|
||||
{
|
||||
Ok(item_id) => item_id,
|
||||
Err(error) => {
|
||||
eprintln!("[agc-cc-direct] persist assistant failed: {error}");
|
||||
direct_turn_trace("claude-parse-error");
|
||||
return Err(format!("写入本项目对话历史失败:{error}"));
|
||||
}
|
||||
};
|
||||
let item = serde_json::json!({
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"id": item_id.clone(),
|
||||
"content": [{ "type": "output_text", "text": text }],
|
||||
});
|
||||
if let Err(error) = append_direct_project_history_item_at(root, &item) {
|
||||
eprintln!("[agc-cc-direct] persist assistant failed: {error}");
|
||||
direct_turn_trace("claude-parse-error");
|
||||
return Err(format!("写入本项目对话历史失败:{error}"));
|
||||
}
|
||||
// 聊天区是按 `item.completed` 事件流投影的(codex 路径在 rawResponseItem/completed
|
||||
// 时下发同款事件),只落盘历史不会让本轮回复出现在界面上——重进项目才看得到。
|
||||
// 条目身份与落盘的历史条目保持同一个,重进项目按 id 去重。
|
||||
@@ -1026,6 +1020,38 @@ pub(crate) async fn direct_game_creator_claude_code_chat_at(
|
||||
parsed
|
||||
}
|
||||
|
||||
fn persist_direct_claude_assistant_reply_at(
|
||||
root: &Path,
|
||||
client_turn_id: Option<&str>,
|
||||
text: &str,
|
||||
) -> Result<String, String> {
|
||||
let stable_item_id = match client_turn_id {
|
||||
Some(client_turn_id) => format!("direct-codex:{client_turn_id}:assistant"),
|
||||
None => format!("direct-codex:{}:assistant", uuid::Uuid::new_v4()),
|
||||
};
|
||||
let mut item = serde_json::json!({
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"id": stable_item_id.clone(),
|
||||
"content": [{ "type": "output_text", "text": text }],
|
||||
});
|
||||
match append_direct_project_history_item_at(root, &item) {
|
||||
Ok(()) => Ok(stable_item_id),
|
||||
Err(error) if error.starts_with("DirectProject 历史 item id 冲突:") => {
|
||||
// 同一 client turn 的验收反馈会再次进入 cc 执行器。首个回复保留稳定身份,
|
||||
// 后续不同回复必须另用 item id;否则历史层会把反馈收尾误判成失败。
|
||||
let feedback_item_id = format!("{stable_item_id}:{}", uuid::Uuid::new_v4());
|
||||
item["id"] = serde_json::Value::String(feedback_item_id.clone());
|
||||
append_direct_project_history_item_at(root, &item)
|
||||
.map(|()| feedback_item_id)
|
||||
.map_err(|retry_error| {
|
||||
format!("反馈回复历史追加失败:{retry_error}(首个条目冲突:{error})")
|
||||
})
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_direct_stream_result(
|
||||
stdout: &[u8],
|
||||
mut observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>,
|
||||
@@ -1160,6 +1186,28 @@ mod tests {
|
||||
assert!(error.contains("Authentication failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_claude_feedback_reply_does_not_fail_on_a_reused_client_turn_id() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
crate::init_local_game_project_at(root.path(), "cc-feedback", "cc feedback")
|
||||
.expect("init project");
|
||||
let turn_id = Some("client-turn-feedback-0001");
|
||||
|
||||
let first = persist_direct_claude_assistant_reply_at(root.path(), turn_id, "首轮回复")
|
||||
.expect("persist first reply");
|
||||
let identical = persist_direct_claude_assistant_reply_at(root.path(), turn_id, "首轮回复")
|
||||
.expect("an identical retry remains idempotent");
|
||||
let second = persist_direct_claude_assistant_reply_at(root.path(), turn_id, "反馈后回复")
|
||||
.expect("a host feedback retry must persist a second reply");
|
||||
|
||||
assert_eq!(first, identical);
|
||||
assert_ne!(first, second);
|
||||
assert!(second.starts_with("direct-codex:client-turn-feedback-0001:assistant:"));
|
||||
let items =
|
||||
crate::agent::read_direct_project_history_items_at(root.path()).expect("read history");
|
||||
assert_eq!(items.len(), 2);
|
||||
}
|
||||
|
||||
fn test_platform_session() -> crate::platform_session::PlatformSessionSnapshot {
|
||||
crate::platform_session::PlatformSessionSnapshot {
|
||||
user_id: "user-1".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user