diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs index babb4ba92..2875c99c7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs @@ -668,7 +668,7 @@ pub(super) const AGENT_RUNTIME_AUTONOMOUS_MANIFEST_DAG_WAIT_LIVENESS_ERROR_PREFI pub(super) const AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX: &str = "自主构建专业 Agent 已给出结论但结构化计划仍未完成"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX: &str = - "自主构建完整写入游戏入口(index.html 或 game/index.html)时必须提交闭合且可运行的 HTML"; + "自主构建完整写入 game/index.html 时必须提交闭合且可运行的 HTML"; pub(super) const AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID: &str = "quality-review"; pub(super) const AGENT_RUNTIME_SUPERVISOR_INITIAL_COLLABORATION_LIVENESS_ERROR_PREFIX: &str = "Project Supervisor 首批协作在首个 planning 窗口内未建立"; diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index 06311479f..2a464b0c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -1557,17 +1557,35 @@ pub(crate) fn spawn_mock_llm_tool_plan_then_invalid_final_reply( let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr")); std::thread::spawn(move || { let (mut planning_stream, _) = listener.accept().expect("mock tool plan accept"); - drop(read_mock_http_request(&mut planning_stream)); - let planning_body = serde_json::json!({ - "id": "resp_invalid_final_reply_planning", - "model": "mock-game-model", - "output_text": planning_response, - "status": "completed", - "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } - }) - .to_string(); + let planning_request = read_mock_http_request(&mut planning_stream); + let planning_streaming = planning_request.contains("\"stream\":true"); + let (planning_body, planning_content_type) = if planning_streaming { + ( + format!( + "data: {}\n\ndata: {}\n\n", + serde_json::json!({ + "type": "response.output_text.delta", + "delta": planning_response + }), + serde_json::json!({ "type": "response.completed" }) + ), + "text/event-stream; charset=utf-8", + ) + } else { + ( + serde_json::json!({ + "id": "resp_invalid_final_reply_planning", + "model": "mock-game-model", + "output_text": planning_response, + "status": "completed", + "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } + }) + .to_string(), + "application/json", + ) + }; let planning_response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + "HTTP/1.1 200 OK\r\nContent-Type: {planning_content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", planning_body.len(), planning_body ); @@ -1762,30 +1780,65 @@ fn spawn_mock_llm_server_responses_with_capture( if let Some(sender) = request_sender.as_ref() { let _ = sender.send(request_text.clone()); } - let body = if request_text.contains("POST /responses HTTP/1.1") { - serde_json::json!({ - "id": "resp_game_creator_mock", - "model": "mock-game-model", - "output_text": response_content, - "status": "completed", - "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } - }) + let is_responses = request_text.contains("POST /responses HTTP/1.1"); + let is_stream = request_text.contains("\"stream\":true"); + let (body, content_type) = if is_stream && is_responses { + ( + format!( + "data: {}\n\ndata: {}\n\n", + serde_json::json!({ + "type": "response.output_text.delta", + "delta": response_content + }), + serde_json::json!({ "type": "response.completed" }) + ), + "text/event-stream; charset=utf-8", + ) + } else if is_stream { + ( + format!( + "data: {}\n\ndata: [DONE]\n\n", + serde_json::json!({ + "id": "chatcmpl_game_creator_mock", + "object": "chat.completion.chunk", + "choices": [{ + "index": 0, + "delta": { "role": "assistant", "content": response_content }, + "finish_reason": "stop" + }] + }) + ), + "text/event-stream; charset=utf-8", + ) + } else if is_responses { + ( + serde_json::json!({ + "id": "resp_game_creator_mock", + "model": "mock-game-model", + "output_text": response_content, + "status": "completed", + "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } + }) + .to_string(), + "application/json", + ) } else { - serde_json::json!({ - "id": "chatcmpl_game_creator_mock", - "model": "mock-game-model", - "choices": [ - { + ( + serde_json::json!({ + "id": "chatcmpl_game_creator_mock", + "model": "mock-game-model", + "choices": [{ "message": { "content": response_content }, "finish_reason": "stop" - } - ], - "usage": { "prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33 } - }) - } - .to_string(); + }], + "usage": { "prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33 } + }) + .to_string(), + "application/json", + ) + }; let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body ); @@ -2159,18 +2212,34 @@ fn spawn_mock_llm_tool_plan_then_transient_final_reply( .try_into() .unwrap_or(u64::MAX); let planning_request = read_mock_http_request(&mut planning_stream); - let _ = request_capture_sender.send((planning_accepted_at_ms, planning_request)); + let _ = request_capture_sender.send((planning_accepted_at_ms, planning_request.clone())); let _ = request_notice_sender.send(()); - let planning_body = serde_json::json!({ - "id": "resp_transient_final_reply_planning", - "model": "mock-game-model", - "output_text": planning_response, - "status": "completed", - "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } - }) - .to_string(); + let planning_body = if planning_request.contains("\"stream\":true") { + format!( + "data: {}\n\ndata: {}\n\n", + serde_json::json!({ + "type": "response.output_text.delta", + "delta": planning_response + }), + serde_json::json!({ "type": "response.completed" }) + ) + } else { + serde_json::json!({ + "id": "resp_transient_final_reply_planning", + "model": "mock-game-model", + "output_text": planning_response, + "status": "completed", + "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } + }) + .to_string() + }; + let planning_content_type = if planning_request.contains("\"stream\":true") { + "text/event-stream; charset=utf-8" + } else { + "application/json" + }; let planning_http_response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + "HTTP/1.1 200 OK\r\nContent-Type: {planning_content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", planning_body.len(), planning_body ); @@ -2250,7 +2319,7 @@ fn spawn_mock_llm_tool_plan_then_transient_final_compaction( let (mut planning_stream, _) = listener.accept().expect("mock tool plan accept"); let planning_accepted_at_ms = accepted_at_ms(); let planning_request = read_mock_http_request(&mut planning_stream); - let _ = request_capture_sender.send((planning_accepted_at_ms, planning_request)); + let _ = request_capture_sender.send((planning_accepted_at_ms, planning_request.clone())); replace_test_local_config( &config_path, format!( @@ -2274,16 +2343,32 @@ fn spawn_mock_llm_tool_plan_then_transient_final_compaction( ), ); let _ = request_notice_sender.send(()); - let planning_body = serde_json::json!({ - "id": "resp_final_compaction_planning", - "model": "mock-game-model", - "output_text": planning_response, - "status": "completed", - "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } - }) - .to_string(); + let planning_body = if planning_request.contains("\"stream\":true") { + format!( + "data: {}\n\ndata: {}\n\n", + serde_json::json!({ + "type": "response.output_text.delta", + "delta": planning_response + }), + serde_json::json!({ "type": "response.completed" }) + ) + } else { + serde_json::json!({ + "id": "resp_final_compaction_planning", + "model": "mock-game-model", + "output_text": planning_response, + "status": "completed", + "usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 } + }) + .to_string() + }; + let planning_content_type = if planning_request.contains("\"stream\":true") { + "text/event-stream; charset=utf-8" + } else { + "application/json" + }; let planning_http_response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + "HTTP/1.1 200 OK\r\nContent-Type: {planning_content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", planning_body.len(), planning_body ); @@ -2308,19 +2393,35 @@ fn spawn_mock_llm_tool_plan_then_transient_final_compaction( let recovered_compaction_request = read_mock_http_request(&mut recovered_compaction_stream); let _ = request_capture_sender.send(( recovered_compaction_accepted_at_ms, - recovered_compaction_request, + recovered_compaction_request.clone(), )); let _ = request_notice_sender.send(()); - let compaction_body = serde_json::json!({ - "id": "resp_final_compaction_recovered", - "model": "mock-game-model", - "output_text": compaction_response, - "status": "completed", - "usage": { "input_tokens": 44, "output_tokens": 12, "total_tokens": 56 } - }) - .to_string(); + let compaction_body = if recovered_compaction_request.contains("\"stream\":true") { + format!( + "data: {}\n\ndata: {}\n\n", + serde_json::json!({ + "type": "response.output_text.delta", + "delta": compaction_response + }), + serde_json::json!({ "type": "response.completed" }) + ) + } else { + serde_json::json!({ + "id": "resp_final_compaction_recovered", + "model": "mock-game-model", + "output_text": compaction_response, + "status": "completed", + "usage": { "input_tokens": 44, "output_tokens": 12, "total_tokens": 56 } + }) + .to_string() + }; + let compaction_content_type = if recovered_compaction_request.contains("\"stream\":true") { + "text/event-stream; charset=utf-8" + } else { + "application/json" + }; let compaction_http_response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + "HTTP/1.1 200 OK\r\nContent-Type: {compaction_content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", compaction_body.len(), compaction_body );