diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index e8d41fca3..3da206001 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -122,6 +122,49 @@ pub(crate) async fn chat_with_game_creator_role_agent_at( agent_id: &str, prompt: &str, ) -> Result { + let (llm, config_path, request) = + build_game_creator_role_agent_chat_request(root, agent_id, prompt)?; + let client = build_game_creator_llm_client_from_llm_config(&llm, &config_path)?; + let response = request_game_creator_llm_text(&client, &llm, request) + .await + .map_err(|error| format!("{config_path} 单 Agent 聊天调用 LLM 失败:{error}"))?; + let reply_text = strip_llm_thinking_blocks(response.text.as_str()); + if reply_text.is_empty() { + return Err(format!("{config_path} 单 Agent 聊天未返回内容")); + } + + Ok(GameCreatorChatAgentReply { reply_text }) +} + +pub(crate) async fn chat_with_game_creator_role_agent_stream_at( + root: &Path, + agent_id: &str, + prompt: &str, + mut on_delta: F, +) -> Result +where + F: FnMut(&platform_llm::LlmStreamDelta), +{ + let (llm, config_path, request) = + build_game_creator_role_agent_chat_request(root, agent_id, prompt)?; + let client = build_game_creator_llm_client_from_llm_config(&llm, &config_path)?; + let response = client + .stream_run(request, |delta| on_delta(delta)) + .await + .map_err(|error| format!("{config_path} 单 Agent 流式聊天调用 LLM 失败:{error}"))?; + let reply_text = strip_llm_thinking_blocks(response.text.as_str()); + if reply_text.is_empty() { + return Err(format!("{config_path} 单 Agent 流式聊天未返回内容")); + } + + Ok(GameCreatorChatAgentReply { reply_text }) +} + +pub(crate) fn build_game_creator_role_agent_chat_request( + root: &Path, + agent_id: &str, + prompt: &str, +) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> { let agent_id = agent_id.trim(); let prompt = prompt.trim(); if agent_id.is_empty() { @@ -168,7 +211,6 @@ pub(crate) async fn chat_with_game_creator_role_agent_at( let app_config = load_game_creator_app_config()?; let llm = resolve_game_creator_llm_config_for_agent(&app_config, agent_id); let config_path = format!("agentLlm.{agent_id}"); - let client = build_game_creator_llm_client_from_llm_config(&llm, &config_path)?; let user_prompt = if context.trim().is_empty() { format!("用户这轮输入:\n{prompt}") } else { @@ -180,15 +222,7 @@ pub(crate) async fn chat_with_game_creator_role_agent_at( ]) .with_api_kind(parse_game_creator_llm_api_kind(&llm.api_kind)?) .with_max_output_tokens(GAME_CREATOR_CHAT_AGENT_MAX_OUTPUT_TOKENS); - let response = request_game_creator_llm_text(&client, &llm, request) - .await - .map_err(|error| format!("{config_path} 单 Agent 聊天调用 LLM 失败:{error}"))?; - let reply_text = strip_llm_thinking_blocks(response.text.as_str()); - if reply_text.is_empty() { - return Err(format!("{config_path} 单 Agent 聊天未返回内容")); - } - - Ok(GameCreatorChatAgentReply { reply_text }) + Ok((llm, config_path, request)) } pub(crate) fn game_creator_chat_agent_system_prompt() -> &'static str { 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 2364f2f57..30aff269b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -248,6 +248,89 @@ pub(crate) async fn chat_with_game_creator_role_agent( chat_with_game_creator_role_agent_at(root, agent_id.trim(), prompt.trim()).await } +#[tauri::command] +pub(crate) async fn chat_with_game_creator_role_agent_stream( + app: tauri::AppHandle, + project_path: String, + agent_id: String, + prompt: String, + run_id: String, +) -> Result { + let project_path = project_path.trim().to_string(); + let agent_id = agent_id.trim().to_string(); + let run_id = run_id.trim().to_string(); + let root = Path::new(project_path.as_str()); + enforce_project_permission_policy(root, "conversation.read")?; + let emit_app = app.clone(); + let event_project_path = project_path.clone(); + let event_agent_id = agent_id.clone(); + let event_run_id = run_id.clone(); + let _ = app.emit( + "game-creator-role-agent-chat-stream", + GameCreatorRoleAgentChatStreamEvent { + project_path: event_project_path.clone(), + agent_id: event_agent_id.clone(), + run_id: event_run_id.clone(), + status: "started".to_string(), + delta_text: String::new(), + accumulated_text: String::new(), + finish_reason: None, + }, + ); + let result = chat_with_game_creator_role_agent_stream_at( + root, + agent_id.as_str(), + prompt.trim(), + |delta| { + let _ = emit_app.emit( + "game-creator-role-agent-chat-stream", + GameCreatorRoleAgentChatStreamEvent { + project_path: event_project_path.clone(), + agent_id: event_agent_id.clone(), + run_id: event_run_id.clone(), + status: "delta".to_string(), + delta_text: delta.delta_text.clone(), + accumulated_text: delta.accumulated_text.clone(), + finish_reason: delta.finish_reason.clone(), + }, + ); + }, + ) + .await; + match result { + Ok(reply) => { + let _ = app.emit( + "game-creator-role-agent-chat-stream", + GameCreatorRoleAgentChatStreamEvent { + project_path, + agent_id, + run_id, + status: "completed".to_string(), + delta_text: String::new(), + accumulated_text: reply.reply_text.clone(), + finish_reason: None, + }, + ); + Ok(reply) + } + Err(error) => { + let _ = app.emit( + "game-creator-role-agent-chat-stream", + GameCreatorRoleAgentChatStreamEvent { + project_path, + agent_id, + run_id, + status: "failed".to_string(), + delta_text: String::new(), + accumulated_text: String::new(), + finish_reason: None, + }, + ); + Err(error) + } + } +} + #[tauri::command] pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus { check_game_creator_llm_config_from_config() diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 08757cd68..28920a780 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -115,6 +115,18 @@ struct GameCreatorChatAgentReply { reply_text: String, } +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct GameCreatorRoleAgentChatStreamEvent { + project_path: String, + agent_id: String, + run_id: String, + status: String, + delta_text: String, + accumulated_text: String, + finish_reason: Option, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAgentProgressEvent { @@ -928,6 +940,7 @@ fn main() { generate_local_game_draft, chat_with_game_creator_agent, chat_with_game_creator_role_agent, + chat_with_game_creator_role_agent_stream, check_game_creator_llm_config, read_game_creator_app_config, write_game_creator_app_config, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 2a1ee9c99..bf021b1e5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -589,6 +589,31 @@ fn spawn_mock_llm_server_responses_with_capture( base_url } +fn spawn_mock_llm_stream_server_with_capture( + response_body: String, + request_sender: Option>, +) -> String { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("mock stream llm bind"); + let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr")); + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("mock stream llm accept"); + let mut request_buffer = [0_u8; 8192]; + let read_len = stream.read(&mut request_buffer).unwrap_or(0); + if let Some(sender) = request_sender.as_ref() { + let _ = sender.send(String::from_utf8_lossy(&request_buffer[..read_len]).into_owned()); + } + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream; charset=utf-8\r\nContent-Length: {}\r\nx-request-id: req_role_agent_stream\r\nConnection: close\r\n\r\n{}", + response_body.len(), + response_body + ); + stream + .write_all(response.as_bytes()) + .expect("mock stream llm response"); + }); + base_url +} + fn spawn_barrier_mock_llm_server( response_content: String, barrier: Arc<(StdMutex, Condvar)>, @@ -1051,6 +1076,80 @@ async fn chat_with_game_creator_role_agent_uses_agent_context_and_route() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn chat_with_game_creator_role_agent_stream_emits_deltas() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + write_local_agent_memory_at(&root, "art-director", "私有记忆:先做角色规范图") + .expect("write art memory"); + + let (sender, receiver) = mpsc::channel(); + let base_url = spawn_mock_llm_stream_server_with_capture( + concat!( + "data: {\"type\":\"response.output_text.delta\",\"delta\":\"先\"}\n\n", + "data: {\"type\":\"response.output_text.delta\",\"delta\":\"生成规范图\"}\n\n", + "data: {\"type\":\"response.completed\"}\n\n" + ) + .to_string(), + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "llm": {{ + "apiKey": "global-key", + "baseUrl": "https://global.example.test/v1", + "model": "global-model", + "apiKind": "openai_responses" + }}, + "agentLlm": {{ + "art-director": {{ + "apiKey": "art-key", + "baseUrl": {base_url:?}, + "model": "art-chat-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + + let mut deltas = Vec::new(); + let reply = chat_with_game_creator_role_agent_stream_at( + &root, + "art-director", + "我要一个主角设定", + |delta| { + deltas.push(( + delta.delta_text.clone(), + delta.accumulated_text.clone(), + delta.finish_reason.clone(), + )); + }, + ) + .await + .expect("role stream chat reply"); + + assert_eq!(reply.reply_text, "先生成规范图"); + assert_eq!( + deltas, + vec![ + ("先".to_string(), "先".to_string(), None), + ("生成规范图".to_string(), "先生成规范图".to_string(), None), + ] + ); + let request = receiver + .recv_timeout(Duration::from_secs(1)) + .expect("captured role stream chat llm request"); + assert!(request.contains("POST /responses HTTP/1.1")); + assert!(request.contains("\"stream\":true")); + assert!(request.contains("art-chat-model")); + assert!(request.contains("私有记忆:先做角色规范图")); + assert!(request.contains("我要一个主角设定")); + assert!(!request.contains("global-model")); + assert!(!request.contains("global-key")); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn agent_loop_uses_per_agent_llm_overrides() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 16355c7e0..e42f5e595 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -212,6 +212,16 @@ interface GameCreatorChatAgentReply { replyText: string; } +interface GameCreatorRoleAgentChatStreamEvent { + projectPath: string; + agentId: string; + runId: string; + status: 'started' | 'delta' | 'completed' | 'failed'; + deltaText: string; + accumulatedText: string; + finishReason: string | null; +} + interface GameCreatorLlmConfigStatus { configured: boolean; apiKeyPresent: boolean; @@ -402,6 +412,22 @@ interface LocalConversationResult { messages: LocalConversationMessageRecord[]; } +function createLocalConversationDraftMessage( + content: string, +): LocalConversationMessageRecord { + return { + schemaVersion: 'game-creator-conversation.v1', + role: 'assistant', + content, + agentId: null, + updatedAt: Date.now(), + }; +} + +function createAgentChatRunId(prefix: string) { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +} + interface ProjectPermissionPolicy { deniedCommands: string[]; confirmCommands: string[]; @@ -2944,8 +2970,10 @@ export function WorkspaceLauncher({ agentChatLoadVersionRef.current = saveVersion; setAgentChatBusy(true); setAgentChatInput(''); - setAgentChatStatus('正在保存'); + setAgentChatStatus('正在保存用户消息'); let savedUserResult: LocalConversationResult | null = null; + let stopStreamListen: (() => void) | null = null; + let streamListenDisposed = false; try { savedUserResult = await invoke( 'append_local_conversation_message', @@ -2962,19 +2990,84 @@ export function WorkspaceLauncher({ if (agentChatLoadVersionRef.current !== saveVersion) { return; } - setAgentChatMessages(savedUserResult.messages); - setAgentChatStatus('Agent 正在思考'); - const reply = await invoke( - 'chat_with_game_creator_role_agent', - { - projectPath: projectPathForChat, - agentId: agent.id, - prompt: content, - }, - ); + const savedUserMessages = savedUserResult.messages; + setAgentChatMessages(savedUserMessages); + setAgentChatStatus('正在连接 Agent LLM'); + const streamRunId = createAgentChatRunId('launcher-agent-chat'); + const listen = window.__TAURI__?.event?.listen; + if (listen) { + stopStreamListen = + await listen( + 'game-creator-role-agent-chat-stream', + (event) => { + const payload = event.payload; + if ( + payload.projectPath !== projectPathForChat || + payload.agentId !== agent.id || + payload.runId !== streamRunId || + agentChatLoadVersionRef.current !== saveVersion + ) { + return; + } + if (payload.status === 'started') { + setAgentChatStatus('Agent 已连接,正在等待回复'); + return; + } + if (payload.status === 'delta') { + const draftText = payload.accumulatedText || payload.deltaText; + if (draftText) { + setAgentChatMessages([ + ...savedUserMessages, + createLocalConversationDraftMessage(draftText), + ]); + } + setAgentChatStatus( + payload.finishReason + ? `Agent 回复结束:${payload.finishReason}` + : '正在接收 Agent 回复', + ); + return; + } + if (payload.status === 'completed') { + setAgentChatStatus('Agent 回复完成,正在保存'); + return; + } + if (payload.status === 'failed') { + setAgentChatStatus('Agent 流式回复失败,正在记录错误'); + } + }, + ); + if (streamListenDisposed) { + stopStreamListen(); + stopStreamListen = null; + } + } + const reply = listen + ? await invoke( + 'chat_with_game_creator_role_agent_stream', + { + projectPath: projectPathForChat, + agentId: agent.id, + prompt: content, + runId: streamRunId, + }, + ) + : await invoke( + 'chat_with_game_creator_role_agent', + { + projectPath: projectPathForChat, + agentId: agent.id, + prompt: content, + }, + ); if (agentChatLoadVersionRef.current !== saveVersion) { return; } + setAgentChatStatus('正在保存 Agent 回复'); + setAgentChatMessages([ + ...savedUserMessages, + createLocalConversationDraftMessage(reply.replyText), + ]); const assistantResult = await invoke( 'append_local_conversation_message', { @@ -3037,6 +3130,8 @@ export function WorkspaceLauncher({ setAgentChatStatus(error instanceof Error ? error.message : String(error)); } } finally { + streamListenDisposed = true; + stopStreamListen?.(); if (agentChatLoadVersionRef.current === saveVersion) { setAgentChatBusy(false); } @@ -12283,8 +12378,10 @@ export function App() { agentConversationSavingRef.current = true; setAgentConversationSaving(true); setAgentConversationInput(''); - setAgentConversationStatus('正在保存'); + setAgentConversationStatus('正在保存用户消息'); let savedUserResult: LocalConversationResult | null = null; + let stopStreamListen: (() => void) | null = null; + let streamListenDisposed = false; try { savedUserResult = await invoke( 'append_local_conversation_message', @@ -12301,19 +12398,84 @@ export function App() { if (agentConversationLoadVersionRef.current !== saveVersion) { return; } - setAgentConversationMessages(savedUserResult.messages); - setAgentConversationStatus('Agent 正在思考'); - const reply = await invoke( - 'chat_with_game_creator_role_agent', - { - projectPath: nextProjectPath, - agentId: agent.id, - prompt: content, - }, - ); + const savedUserMessages = savedUserResult.messages; + setAgentConversationMessages(savedUserMessages); + setAgentConversationStatus('正在连接 Agent LLM'); + const streamRunId = createAgentChatRunId('agent-conversation'); + const listen = window.__TAURI__?.event?.listen; + if (listen) { + stopStreamListen = + await listen( + 'game-creator-role-agent-chat-stream', + (event) => { + const payload = event.payload; + if ( + payload.projectPath !== nextProjectPath || + payload.agentId !== agent.id || + payload.runId !== streamRunId || + agentConversationLoadVersionRef.current !== saveVersion + ) { + return; + } + if (payload.status === 'started') { + setAgentConversationStatus('Agent 已连接,正在等待回复'); + return; + } + if (payload.status === 'delta') { + const draftText = payload.accumulatedText || payload.deltaText; + if (draftText) { + setAgentConversationMessages([ + ...savedUserMessages, + createLocalConversationDraftMessage(draftText), + ]); + } + setAgentConversationStatus( + payload.finishReason + ? `Agent 回复结束:${payload.finishReason}` + : '正在接收 Agent 回复', + ); + return; + } + if (payload.status === 'completed') { + setAgentConversationStatus('Agent 回复完成,正在保存'); + return; + } + if (payload.status === 'failed') { + setAgentConversationStatus('Agent 流式回复失败,正在记录错误'); + } + }, + ); + if (streamListenDisposed) { + stopStreamListen(); + stopStreamListen = null; + } + } + const reply = listen + ? await invoke( + 'chat_with_game_creator_role_agent_stream', + { + projectPath: nextProjectPath, + agentId: agent.id, + prompt: content, + runId: streamRunId, + }, + ) + : await invoke( + 'chat_with_game_creator_role_agent', + { + projectPath: nextProjectPath, + agentId: agent.id, + prompt: content, + }, + ); if (agentConversationLoadVersionRef.current !== saveVersion) { return; } + setAgentConversationStatus('正在保存 Agent 回复'); + setAgentConversationMessages([ + ...savedUserMessages, + createLocalConversationDraftMessage(reply.replyText), + ]); const assistantResult = await invoke( 'append_local_conversation_message', { @@ -12379,6 +12541,8 @@ export function App() { ); } } finally { + streamListenDisposed = true; + stopStreamListen?.(); agentConversationSavingRef.current = false; setAgentConversationSaving(false); } @@ -19450,6 +19614,7 @@ export function App() { {selectedAgentLlmStatus ? (

{selectedAgentLlmStatus}

) : null} +

{agentConversationStatus}

{selectedAgent.summary}

@@ -19617,7 +19782,6 @@ export function App() { 记入记忆 -

{agentConversationStatus}

) : null} diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 168c0be06..15f10de1a 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1060,6 +1060,180 @@ describe('AI 游戏创作 App 界面边界', () => { ); }); + it('streams developer agent chat status and draft reply before persisting', async () => { + const persistedMessages: Array<{ + role: 'user' | 'assistant'; + content: string; + agentId: string | null; + }> = []; + let releaseStream: (() => void) | null = null; + let streamHandler: ((event: { payload: Record }) => void) | null = + null; + const listen = vi.fn( + async ( + eventName: string, + handler: (event: { payload: Record }) => void, + ) => { + expect(eventName).toBe('game-creator-role-agent-chat-stream'); + streamHandler = handler; + return () => { + if (streamHandler === handler) { + streamHandler = null; + } + }; + }, + ); + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'check_game_creator_llm_config') { + return { + configured: true, + apiKeyPresent: true, + baseUrl: 'https://llm.example.test/v1', + model: 'gpt-5.5', + apiKind: 'openai_chat', + stream: true, + error: null, + agents: [ + { + agentId: 'design-director', + label: '拆解创作方向', + configured: true, + apiKeyPresent: true, + baseUrl: 'https://llm.example.test/v1', + model: 'gpt-5.5', + apiKind: 'openai_chat', + stream: true, + error: null, + }, + ], + }; + } + if (command === 'read_local_conversation') { + return { + path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', + agentId: args?.agentId, + messages: persistedMessages.map((message, index) => ({ + schemaVersion: '1', + ...message, + updatedAt: 1000 + index, + })), + }; + } + if (command === 'chat_with_game_creator_role_agent_stream') { + const payloadBase = { + projectPath: args?.projectPath, + agentId: args?.agentId, + runId: args?.runId, + finishReason: null, + }; + streamHandler?.({ + payload: { + ...payloadBase, + status: 'started', + deltaText: '', + accumulatedText: '', + }, + }); + streamHandler?.({ + payload: { + ...payloadBase, + status: 'delta', + deltaText: '专业', + accumulatedText: '专业', + }, + }); + streamHandler?.({ + payload: { + ...payloadBase, + status: 'delta', + deltaText: ' Agent', + accumulatedText: '专业 Agent', + }, + }); + await new Promise((resolve) => { + releaseStream = resolve; + }); + streamHandler?.({ + payload: { + ...payloadBase, + status: 'completed', + deltaText: '', + accumulatedText: '专业 Agent 已流式完成。', + }, + }); + return { + replyText: '专业 Agent 已流式完成。', + }; + } + if (command === 'append_local_conversation_message') { + const message = args?.message as { + role: 'user' | 'assistant'; + content: string; + agentId: string | null; + }; + persistedMessages.push(message); + return { + path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', + agentId: args?.agentId, + messages: persistedMessages.map((record, index) => ({ + schemaVersion: '1', + ...record, + updatedAt: 2000 + index, + })), + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke }, event: { listen } }; + renderLauncherAgentChatAt('/?agent-chat'); + + fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { + target: { value: '/tmp/authorized-game' }, + }); + fireEvent.click(screen.getByRole('button', { name: '读取历史' })); + expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); + + fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { + target: { value: '请流式回答' }, + }); + fireEvent.click(screen.getByRole('button', { name: '发送' })); + + expect(await screen.findByText('专业 Agent')).not.toBeNull(); + expect(screen.getByText('正在接收 Agent 回复')).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith( + 'chat_with_game_creator_role_agent_stream', + expect.objectContaining({ + projectPath: '/tmp/authorized-game', + agentId: 'design-director', + prompt: '请流式回答', + }), + ); + expect( + invoke, + ).not.toHaveBeenCalledWith('chat_with_game_creator_role_agent', expect.anything()); + + await act(async () => { + releaseStream?.(); + }); + + expect(await screen.findByText('专业 Agent 已流式完成。')).not.toBeNull(); + expect(await screen.findByText(/已保存 2 条/)).not.toBeNull(); + expect(persistedMessages).toEqual([ + { + role: 'user', + content: '请流式回答', + agentId: null, + }, + { + role: 'assistant', + content: '专业 Agent 已流式完成。', + agentId: null, + }, + ]); + }); + it('persists developer agent chat reply failures after saving the user message', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant'; @@ -12026,7 +12200,7 @@ describe('AI 游戏创作 App 界面边界', () => { const form = input.closest('form') as HTMLFormElement; fireEvent.change(input, { target: { value: '只保存一次' } }); fireEvent.submit(form); - await screen.findByText('正在保存'); + await screen.findByText('正在保存用户消息'); fireEvent.submit(form); @@ -12764,7 +12938,7 @@ describe('AI 游戏创作 App 界面边界', () => { const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '旧 Agent 保存回包' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); - await screen.findByText('正在保存'); + await screen.findByText('正在保存用户消息'); fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ })); expect(await screen.findByText('新 Agent 留存消息')).not.toBeNull();