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 87c49778f..6f38796f0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -31,6 +31,7 @@ use shared_contracts::game_creation_app::{ GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, }; +use tauri::Emitter; use tauri_plugin_opener::OpenerExt; #[derive(Debug, Eq, PartialEq, Serialize)] @@ -69,6 +70,14 @@ struct GenerateLocalGameDraftResult { manifest: GameCreationAppManifest, } +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct GameCreatorAgentProgressEvent { + project_path: String, + stage: String, + message: String, +} + #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorLlmConfigStatus { @@ -460,7 +469,7 @@ static AUDIO_AGENT_ROLES: [AgentRoleDefinition; 2] = [ }, ]; -static CODE_AGENT_ROLES: [AgentRoleDefinition; 4] = [ +static CODE_AGENT_ROLES: [AgentRoleDefinition; 5] = [ AgentRoleDefinition { id: "director", role: "Director", @@ -475,6 +484,13 @@ static CODE_AGENT_ROLES: [AgentRoleDefinition; 4] = [ tool_id: "agent.role.brief.code.code", brief_path_name: "code.md", }, + AgentRoleDefinition { + id: "review", + role: "Review", + task_id: "quality-review", + tool_id: "agent.role.brief.code.review", + brief_path_name: "review.md", + }, AgentRoleDefinition { id: "preview", role: "Preview", @@ -540,7 +556,7 @@ const GAME_CREATOR_AGENT_GROUP_DEFINITIONS: [AgentGroupDefinition; 6] = [ AgentGroupDefinition { id: "code", label: "程序组", - role: "Director + Code + Preview + Playtest", + role: "Director + Code + Review + Preview + Playtest", brief_path_name: "code.md", roles: &CODE_AGENT_ROLES, }, @@ -740,10 +756,16 @@ fn get_local_game_manifest(project_path: String) -> Result Result { - generate_local_game_draft_at(Path::new(project_path.trim()), prompt.trim()).await + generate_local_game_draft_at( + Path::new(project_path.trim()), + prompt.trim(), + Some(&AgentProgressEmitter::new(&app, project_path.trim())), + ) + .await } #[tauri::command] @@ -1049,6 +1071,7 @@ fn append_agent_db_record(root: &Path, mut record: serde_json::Value) -> Result< async fn generate_local_game_draft_at( root: &Path, prompt: &str, + progress: Option<&AgentProgressEmitter<'_>>, ) -> Result { let prompt = prompt.trim(); if prompt.is_empty() { @@ -1060,14 +1083,41 @@ async fn generate_local_game_draft_at( let long_memory = read_optional_text(&root.join("memory/project.md"))?; let asset_context = render_local_asset_prompt_context(root)?; let long_memory = append_prompt_context(&asset_context, &long_memory); + emit_agent_progress( + progress, + "llm.planner", + "Planner 正在调用 LLM 整理规格和专业组分工", + ); let client = build_game_creator_llm_client_from_env()?; - let loop_result = - run_game_creator_agent_loop_at(root, &client, prompt, &short_memory, &long_memory).await?; + let loop_result = run_game_creator_agent_loop_at( + root, + &client, + prompt, + &short_memory, + &long_memory, + progress, + ) + .await?; let mut loop_result = loop_result; + emit_agent_progress( + progress, + "artifact.write", + "ArtifactWriter 正在写入本地代码、数值、美术、音乐和发布草案", + ); let mut result = write_local_game_draft_at(root, prompt, &loop_result.draft)?; append_local_artifact_write_step(root, prompt, &mut loop_result)?; + emit_agent_progress( + progress, + "playtest.static_smoke", + "Playtest 正在运行 game.static_smoke 自检", + ); append_static_smoke_step(root, prompt, &mut loop_result)?; append_agent_loop_log(root, &loop_result)?; + emit_agent_progress( + progress, + "agent.complete", + "Agent loop 已通过 Evaluator 和静态自检,正在启动本地预览", + ); result.manifest = read_manifest_for_project(root)?; Ok(result) } @@ -1342,6 +1392,37 @@ fn check_game_creator_llm_config_values( } } +struct AgentProgressEmitter<'a> { + app: &'a tauri::AppHandle, + project_path: String, +} + +impl<'a> AgentProgressEmitter<'a> { + fn new(app: &'a tauri::AppHandle, project_path: &str) -> Self { + Self { + app, + project_path: project_path.to_string(), + } + } + + fn emit(&self, stage: &str, message: &str) { + let _ = self.app.emit( + "game-creator-agent-progress", + GameCreatorAgentProgressEvent { + project_path: self.project_path.clone(), + stage: stage.to_string(), + message: message.to_string(), + }, + ); + } +} + +fn emit_agent_progress(progress: Option<&AgentProgressEmitter<'_>>, stage: &str, message: &str) { + if let Some(progress) = progress { + progress.emit(stage, message); + } +} + #[cfg(test)] async fn request_llm_game_draft_with_client( client: &LlmClient, @@ -1368,6 +1449,7 @@ async fn run_game_creator_agent_loop_at( prompt: &str, short_memory: &str, long_memory: &str, + progress: Option<&AgentProgressEmitter<'_>>, ) -> Result { let spec_path = root.join(".agent/spec.md"); let findings_path = root.join(".agent/findings.md"); @@ -1378,6 +1460,7 @@ async fn run_game_creator_agent_loop_at( .map(|spec| render_planner_spec(prompt, &spec))?; fs::write(&spec_path, &planner_spec) .map_err(|error| format!("写入 Planner 规格失败:{}: {error}", spec_path.display()))?; + emit_agent_progress(progress, "llm.planner.done", "Planner 规格已生成"); steps.push(with_task_context( agent_trace_step( 0, @@ -1419,6 +1502,11 @@ async fn run_game_creator_agent_loop_at( let mut last_error = "Evaluator 未产出可用结果".to_string(); for pass in 1..=GAME_CREATOR_AGENT_LOOP_MAX_PASSES { + emit_agent_progress( + progress, + "agent.orchestrator", + &format!("Orchestrator 正在规划第 {pass} 轮任务图"), + ); let spec_markdown = read_optional_text(&spec_path)?; let findings_markdown = read_optional_text(&findings_path)?; let agenda = write_agent_pass_agenda(root, pass, &findings_markdown)?; @@ -1477,22 +1565,38 @@ async fn run_game_creator_agent_loop_at( findings_path.display() ) })?; - steps.push(agent_trace_step( - pass, - "Evaluator", - "needs-revision", - &[], - &[".agent/findings.md"], - "记录专业组协作失败原因", - "file.write.findings", + steps.push(with_task_context( + agent_trace_step( + pass, + "Evaluator", + "needs-revision", + &[], + &[".agent/findings.md"], + "记录专业组协作失败原因", + "file.write.findings", + ), + "code", + "Review", + Some("quality-review"), + "evaluation", )); last_error = issues.join(";"); write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?; continue; } }; + emit_agent_progress( + progress, + "agent.role_briefs", + &format!("6 组角色 brief 已完成,第 {pass} 轮交给 Generator"), + ); append_group_brief_steps(root, pass, &agenda.relative_path, &group_briefs, &mut steps); let group_briefs_markdown = render_agent_group_briefs_context(&group_briefs); + emit_agent_progress( + progress, + "llm.generator", + &format!("Generator 正在调用 LLM 生成第 {pass} 轮可运行草案"), + ); match request_generator_game_draft_with_client( client, prompt, @@ -1506,6 +1610,11 @@ async fn run_game_creator_agent_loop_at( .await { Ok(draft) => { + emit_agent_progress( + progress, + "llm.generator.done", + &format!("Generator 第 {pass} 轮草案已返回,Evaluator 开始质量评审"), + ); let pass_artifacts = write_agent_pass_artifacts(root, pass, &draft)?; let mut generator_input_paths = vec![ "memory/session.md".to_string(), @@ -1536,30 +1645,42 @@ async fn run_game_creator_agent_loop_at( findings_path.display() ) })?; - steps.push(agent_trace_step_owned( - pass, - "Evaluator", - if issues.is_empty() { - "passed" - } else { - "needs-revision" - }, - vec![ - pass_artifacts.game_html.clone(), - pass_artifacts.design_markdown.clone(), - pass_artifacts.balance_json.clone(), - pass_artifacts.art_manifest_json.clone(), - pass_artifacts.audio_manifest_json.clone(), - ], - vec![".agent/findings.md".to_string()], - if issues.is_empty() { - "静态验收通过" - } else { - "发现问题,要求下一轮 Generator 修复" - }, - "evaluator.static_html_check", + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "Evaluator", + if issues.is_empty() { + "passed" + } else { + "needs-revision" + }, + vec![ + pass_artifacts.game_html.clone(), + pass_artifacts.design_markdown.clone(), + pass_artifacts.balance_json.clone(), + pass_artifacts.art_manifest_json.clone(), + pass_artifacts.audio_manifest_json.clone(), + pass_artifacts.publish_readme.clone(), + ], + vec![".agent/findings.md".to_string()], + if issues.is_empty() { + "质量评审通过:玩法、资产、数值、程序和发布包装可进入预览试玩" + } else { + "质量评审发现问题,要求下一轮 Generator 修复" + }, + "evaluator.quality_review", + ), + "code", + "Review", + Some("quality-review"), + "evaluation", )); if issues.is_empty() { + emit_agent_progress( + progress, + "evaluator.passed", + &format!("Evaluator 第 {pass} 轮质量评审通过"), + ); write_agent_run_trace(root, &run_id, prompt, "passed", pass, &steps, None)?; return Ok(GameCreatorAgentLoopResult { run_id, @@ -1571,6 +1692,11 @@ async fn run_game_creator_agent_loop_at( }); } last_error = issues.join(";"); + emit_agent_progress( + progress, + "evaluator.needs_revision", + &format!("Evaluator 第 {pass} 轮要求返工:{last_error}"), + ); write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?; } Err(error) => { @@ -1591,14 +1717,20 @@ async fn run_game_creator_agent_loop_at( findings_path.display() ) })?; - steps.push(agent_trace_step( - pass, - "Evaluator", - "needs-revision", - &[], - &[".agent/findings.md"], - "记录 Generator 失败原因", - "file.write.findings", + steps.push(with_task_context( + agent_trace_step( + pass, + "Evaluator", + "needs-revision", + &[], + &[".agent/findings.md"], + "记录 Generator 失败原因", + "file.write.findings", + ), + "code", + "Review", + Some("quality-review"), + "evaluation", )); last_error = issues.join(";"); write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?; @@ -3119,11 +3251,13 @@ fn build_agent_run_task_graph_trace( &tasks, "preview-readiness", GameCreationAppTaskStatus::Completed, - ) && !steps.iter().any(|step| { - step.task_id.as_deref() == Some("preview-playtest") - && step.phase == "preview" - && step.status == "running" - }) { + ) + && !steps.iter().any(|step| { + step.task_id.as_deref() == Some("preview-playtest") + && step.phase == "preview" + && step.status == "running" + }) + { set_task_status_if_current( &mut tasks, "preview-playtest", @@ -3271,7 +3405,9 @@ fn task_status_from_agent_step( } match step.phase.as_str() { - "planning" | "handoff" | "playtest" => Some(GameCreationAppTaskStatus::Completed), + "planning" | "handoff" | "playtest" | "evaluation" => { + Some(GameCreationAppTaskStatus::Completed) + } "preview" => Some(GameCreationAppTaskStatus::Running), "role-brief" if role_brief_completes_task(task_id) => { Some(GameCreationAppTaskStatus::Completed) @@ -3284,12 +3420,12 @@ fn task_status_from_agent_step( fn role_brief_completes_task(task_id: &str) -> bool { matches!( task_id, - "balance-director" - | "art-director" - | "art-polish" - | "audio-director" - | "code-director" - | "publish-strategy" + "balance-director" + | "art-director" + | "art-polish" + | "audio-director" + | "code-director" + | "publish-strategy" ) } @@ -5300,6 +5436,7 @@ fn record_draft_task_progress( "audio-asset-plan", "code-director", "code-prototype", + "quality-review", ] { set_task_status( &mut manifest, @@ -5734,7 +5871,8 @@ fn run_cli_command(command: CliCommand) -> Result<(), String> { .enable_all() .build() .map_err(|error| format!("创建 CLI runtime 失败:{error}"))?; - let result = runtime.block_on(generate_local_game_draft_at(&project_path, &prompt))?; + let result = + runtime.block_on(generate_local_game_draft_at(&project_path, &prompt, None))?; let (preview, stop) = start_local_game_preview_for_project(&project_path)?; record_preview_state( &project_path, @@ -6324,7 +6462,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url); std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model"); - let result = generate_local_game_draft_at(&root, "用上传角色图做主角").await; + let result = generate_local_game_draft_at(&root, "用上传角色图做主角", None).await; restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", previous_api_key); restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", previous_base_url); @@ -6407,6 +6545,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true "做一个月光厨房弹幕游戏", "", "# 项目长期记忆\n", + None, ) .await .expect("agent loop"); @@ -6471,7 +6610,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert!(first_agenda.contains("mode: initial")); assert!(first_agenda.contains("activeTasks: design-director")); assert!(first_agenda.contains("wave 1: design-director")); - assert!(first_agenda.contains("wave 11: publish-package")); + assert!(first_agenda.contains("wave 12: publish-package")); let first_task_graph: Value = serde_json::from_str( &fs::read_to_string(root.join(".agent/passes/pass-1/task-graph.json")) .expect("task graph 1"), @@ -6620,7 +6759,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true .iter() .any(|task_id| task_id == "code-prototype"))); let steps = trace["steps"].as_array().unwrap(); - assert_eq!(steps.len(), 64); + assert_eq!(steps.len(), 66); assert_eq!(steps[0]["agent"], "Planner"); assert_eq!(steps[0]["phase"], "planning"); assert_eq!(steps[0]["taskId"], "design-director"); @@ -6793,7 +6932,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert_eq!(run_history["runId"], trace["runId"]); assert_eq!(run_history["status"], "passed"); assert_eq!(run_history["stopReason"], "evaluator-passed"); - assert_eq!(run_history["steps"].as_array().unwrap().len(), 64); + assert_eq!(run_history["steps"].as_array().unwrap().len(), 66); let manifest: Value = serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) .expect("manifest json"); @@ -6838,7 +6977,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url); std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model"); - let error = generate_local_game_draft_at(&root, "做一个会失败三轮的厨房游戏") + let error = generate_local_game_draft_at(&root, "做一个会失败三轮的厨房游戏", None) .await .expect_err("max-pass failure should bubble out"); @@ -6990,7 +7129,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert_eq!(manifest["projectId"], "project-1"); assert_eq!(manifest["name"], "像素动作原型"); assert_eq!(manifest["assets"].as_array().unwrap().len(), 0); - assert_eq!(manifest["tasks"].as_array().unwrap().len(), 15); + assert_eq!(manifest["tasks"].as_array().unwrap().len(), 16); assert_eq!(manifest["tasks"][0]["id"], "design-director"); fs::remove_dir_all(root).ok(); @@ -7058,6 +7197,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert_task_status(&manifest, "audio-asset-plan", "completed"); assert_task_status(&manifest, "code-director", "completed"); assert_task_status(&manifest, "code-prototype", "completed"); + assert_task_status(&manifest, "quality-review", "completed"); assert_task_status(&manifest, "preview-readiness", "waiting-for-confirmation"); assert_task_status(&manifest, "preview-playtest", "pending"); assert_task_status(&manifest, "publish-strategy", "pending"); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 144b00680..80a41abda 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -1,4 +1,4 @@ -import { type ChangeEvent, type FormEvent, useState } from 'react'; +import { type ChangeEvent, type FormEvent, useEffect, useState } from 'react'; import { createGameCreationAppManifest, @@ -133,6 +133,12 @@ interface ChatMessage { text: string; } +interface AgentProgressEvent { + projectPath: string; + stage: string; + message: string; +} + export type PendingCommand = | { id: 'game.generate_draft'; @@ -767,7 +773,24 @@ function summarizeSuggestedToolCalls(trace: GameCreationAgentRunTrace) { .join('\n'); } +function summarizeLlmConversation(trace: GameCreationAgentRunTrace) { + return trace.steps + .filter((step) => + step.toolCalls.some((toolCall) => toolCall.toolId.startsWith('llm.')), + ) + .slice(-6) + .map((step) => { + const toolIds = step.toolCalls + .filter((toolCall) => toolCall.toolId.startsWith('llm.')) + .map((toolCall) => toolCall.toolId) + .join(', '); + return `- ${step.agent} #${step.pass} · ${step.status} · ${step.phase} · ${toolIds}`; + }) + .join('\n'); +} + export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) { + const llmConversation = summarizeLlmConversation(trace); const recentSteps = trace.steps .slice(-5) .map( @@ -833,6 +856,7 @@ export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) { `状态:${trace.status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`, `工具调用:${trace.toolCallCount}/${trace.maxToolCalls}`, `下一步:${trace.nextStep}`, + llmConversation ? `LLM 对话:\n${llmConversation}` : null, taskSummary ? `任务:${taskSummary}` : null, `active 任务:${formatTraceTaskIds( trace.taskGraph.activeTaskIds, @@ -855,43 +879,18 @@ export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) { .join('\n'); } -export function summarizeAgentRunCompletion(trace: GameCreationAgentRunTrace) { - const repair = - trace.taskGraph.repairFocus.length > 0 - ? `\n返工焦点:${trace.taskGraph.repairFocus.join(';')}` - : ''; - const artifacts = trace.artifacts - .filter((artifact) => - [ - 'game/index.html', - 'game/game_design.md', - 'game/balance.json', - 'assets/manifest.art.json', - 'assets/manifest.audio.json', - 'exports/README.md', - ].includes(artifact.path), - ) - .map((artifact) => artifact.path) - .join(','); - const suggestedTools = summarizeSuggestedToolCalls(trace); +function summarizeAgentRunCompletionForChat( + trace: GameCreationAgentRunTrace, +) { + return `${summarizeAgentRunTrace(trace)}\n完整 trace:/trace`; +} +function gameDraftStartedMessage() { return [ - `Agent loop:${trace.status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`, - `工具调用:${trace.toolCallCount}/${trace.maxToolCalls}`, - `active 任务:${formatTraceTaskIds( - trace.taskGraph.activeTaskIds, - trace.taskGraph.tasks, - )}`, - `carry-over 任务:${formatTraceTaskIds( - trace.taskGraph.carriedTaskIds, - trace.taskGraph.tasks, - )}${repair}`, - suggestedTools ? `建议命令:\n${suggestedTools}` : null, - artifacts ? `本地产物:${artifacts}` : null, - '完整 trace:/trace', - ] - .filter(Boolean) - .join('\n'); + '开始调用 LLM:Planner 正在整理规格。', + '随后 Orchestrator 编排 6 组角色 brief,Generator 生成代码和资产清单,Evaluator 做质量评审。', + '完成后会把 run trace 和本地产物摘要发回这里。', + ].join('\n'); } function pendingCommandTitle(command: PendingCommand) { @@ -942,7 +941,7 @@ export function pendingCommandDetail( return `运行自检并启动 ${projectPath}/game/`; } if (command.id === 'game.generate_draft') { - return `调用 LLM,写入 ${projectPath}/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器`; + return `调用 LLM Planner / Generator,编排 6 组角色 brief,写入 ${projectPath}/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并交给外部浏览器`; } if (command.id === 'project.create') { return `创建 ${command.projectPath}`; @@ -1028,6 +1027,37 @@ export function App() { `${GAME_CREATION_APP_COMMANDS.length} 个命令已登记权限。`, ]); + useEffect(() => { + const listen = window.__TAURI__?.event?.listen; + if (!listen) { + return; + } + let cleanup: (() => void) | null = null; + let disposed = false; + void listen('game-creator-agent-progress', (event) => { + if ( + localProject && + event.payload.projectPath !== localProject.projectPath + ) { + return; + } + setMessages((current) => [ + ...current, + { role: 'assistant', text: event.payload.message }, + ]); + }).then((unlisten) => { + if (disposed) { + unlisten(); + return; + } + cleanup = unlisten; + }); + return () => { + disposed = true; + cleanup?.(); + }; + }, [localProject?.projectPath]); + function appendLocalPermissionLog( projectPath: string | null, event: 'permission.pending' | 'permission.confirm' | 'permission.cancel', @@ -1534,6 +1564,11 @@ export function App() { try { setCommandLog((current) => [...current, 'game.generate_draft']); + setProjectStatus('正在调用 LLM'); + setMessages((current) => [ + ...current, + { role: 'assistant', text: gameDraftStartedMessage() }, + ]); const result = await invoke( 'generate_local_game_draft', { projectPath: nextProjectPath, prompt }, @@ -1544,7 +1579,7 @@ export function App() { manifest: result.manifest, }); setManifest(result.manifest); - setProjectStatus('已生成草案'); + setProjectStatus('已生成可试玩原型'); setCommandLog((current) => [ ...current, 'memory.write', @@ -3253,7 +3288,7 @@ export function App() { `${trace.status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`, ); setCommandLog((current) => [...current, `file.read ${relativePath}`]); - return summarizeAgentRunCompletion(trace); + return summarizeAgentRunCompletionForChat(trace); } catch (error) { setAgentRunTrace(null); setAgentRunStatus(error instanceof Error ? error.message : String(error)); diff --git a/apps/ai-game-creator-shell/src/vite-env.d.ts b/apps/ai-game-creator-shell/src/vite-env.d.ts index bd873e32b..01e84f53b 100644 --- a/apps/ai-game-creator-shell/src/vite-env.d.ts +++ b/apps/ai-game-creator-shell/src/vite-env.d.ts @@ -8,5 +8,11 @@ interface Window { args?: Record, ) => Promise; }; + event?: { + listen?: ( + event: string, + handler: (event: { payload: Payload }) => void, + ) => Promise<() => void>; + }; }; } diff --git a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts index 674cb88ae..b70a33af4 100644 --- a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts +++ b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts @@ -79,13 +79,18 @@ describe('AI 游戏创作 Agent loop 摘要', () => { taskGraph: { goal: '做一个弹幕厨房游戏', readyTaskIds: ['code-director'], - activeTaskIds: ['code-director', 'publish-package'], + activeTaskIds: ['code-director', 'quality-review', 'publish-package'], carriedTaskIds: ['design-director'], repairFocus: ['gameHtml 缺少 canvas'], repairRoutes: [ { issue: 'gameHtml 缺少 canvas', - taskIds: ['code-director', 'preview-readiness', 'publish-package'], + taskIds: [ + 'code-director', + 'quality-review', + 'preview-readiness', + 'publish-package', + ], reason: 'code-repair+dependency-impact', }, ], @@ -106,14 +111,23 @@ describe('AI 游戏创作 Agent loop 摘要', () => { pass: 2, mode: 'repair', summary: '第 2 轮重跑程序链路及下游发布包装', - activeTaskIds: ['code-director', 'publish-package'], + activeTaskIds: ['code-director', 'quality-review', 'publish-package'], carriedTaskIds: ['design-director'], - dependencyWaves: [['code-director'], ['publish-package']], + dependencyWaves: [ + ['code-director'], + ['quality-review'], + ['publish-package'], + ], repairFocus: ['gameHtml 缺少 canvas'], repairRoutes: [ { issue: 'gameHtml 缺少 canvas', - taskIds: ['code-director', 'preview-readiness', 'publish-package'], + taskIds: [ + 'code-director', + 'quality-review', + 'preview-readiness', + 'publish-package', + ], reason: 'code-repair+dependency-impact', }, ], @@ -130,21 +144,21 @@ describe('AI 游戏创作 Agent loop 摘要', () => { expect(summary).toContain('工具调用:12/128'); expect(summary).toContain('任务:已完成 2'); expect(summary).toContain( - 'active 任务:程序组 / Director 拆解程序实现(code-director), 运营组 / Publish 整理发布包装(publish-package)', + 'active 任务:程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package)', ); expect(summary).toContain( 'carry-over 任务:策划组 / Director 拆解创作方向(design-director)', ); expect(summary).toContain('返工焦点:gameHtml 缺少 canvas'); expect(summary).toContain( - '返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', + '返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', ); expect(summary).toContain('建议命令:'); expect(summary).toContain('agent.tool.suggest.canvas.project_sync'); expect(summary).toContain('/sync-canvas-project <画板项目ID>'); expect(summary).toContain('编排轮次:'); expect(summary).toContain( - 'pass 2 · repair · active 2 · carry 1 · waves 程序组 / Director 拆解程序实现(code-director) / 运营组 / Publish 整理发布包装(publish-package)', + 'pass 2 · repair · active 3 · carry 1 · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package)', ); expect(summary).toContain('.agent/passes/pass-2/task-graph.json'); expect(summary).toContain('Orchestrator #2 · completed · plan'); diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index c179e2780..795fe419b 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1,5 +1,5 @@ /** @vitest-environment jsdom */ -import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import React from 'react'; @@ -232,9 +232,9 @@ describe('AI 游戏创作 App 界面边界', () => { pass: 2, agent: 'Evaluator', phase: 'evaluate', - taskId: 'preview-readiness', + taskId: 'quality-review', group: 'code', - role: 'Preview', + role: 'Review', status: 'passed', inputPaths: ['.agent/passes/pass-2/draft.json'], outputPaths: ['.agent/findings.md'], @@ -296,7 +296,7 @@ describe('AI 游戏创作 App 界面边界', () => { repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-prototype', 'preview-readiness'], + taskIds: ['code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], @@ -317,14 +317,18 @@ describe('AI 游戏创作 App 界面边界', () => { pass: 2, mode: 'repair', summary: '第 2 轮返工', - activeTaskIds: ['code-prototype', 'preview-readiness'], + activeTaskIds: ['code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director'], - dependencyWaves: [['code-prototype'], ['preview-readiness']], + dependencyWaves: [ + ['code-prototype'], + ['quality-review'], + ['preview-readiness'], + ], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-prototype', 'preview-readiness'], + taskIds: ['code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], @@ -394,7 +398,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect(screen.getByText(/能力\/命令契约:通过/)).not.toBeNull(); expect(screen.getByText(/6 组任务配置:通过/)).not.toBeNull(); expect(screen.getByText(/6 组协作证据:通过/)).not.toBeNull(); - expect(screen.getByText(/角色任务 15 个/)).not.toBeNull(); + expect(screen.getByText(/角色任务 16 个/)).not.toBeNull(); expect( screen.getByText(/Loop trace:通过 · run run-chat-audit/), ).not.toBeNull(); @@ -646,9 +650,9 @@ describe('AI 游戏创作 App 界面边界', () => { pass: 3, agent: 'Evaluator', phase: 'evaluate', - taskId: 'preview-readiness', + taskId: 'quality-review', group: 'code', - role: 'Preview', + role: 'Review', status: 'needs-revision', inputPaths: ['.agent/passes/pass-3/draft.json'], outputPaths: ['.agent/findings.md'], @@ -679,13 +683,13 @@ describe('AI 游戏创作 App 界面边界', () => { taskGraph: { goal: '做一个三轮仍失败的厨房弹幕游戏', readyTaskIds: [], - activeTaskIds: ['code-prototype', 'preview-readiness'], + activeTaskIds: ['code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director'], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-prototype', 'preview-readiness'], + taskIds: ['code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], @@ -696,14 +700,18 @@ describe('AI 游戏创作 App 界面边界', () => { pass: 3, mode: 'repair', summary: '第 3 轮返工仍未通过', - activeTaskIds: ['code-prototype', 'preview-readiness'], + activeTaskIds: ['code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director'], - dependencyWaves: [['code-prototype'], ['preview-readiness']], + dependencyWaves: [ + ['code-prototype'], + ['quality-review'], + ['preview-readiness'], + ], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-prototype', 'preview-readiness'], + taskIds: ['code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], @@ -1294,7 +1302,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull(); expect(screen.getByText(/目录:\/tmp\/authorized-game/)).not.toBeNull(); - expect(screen.getByText(/任务:已完成 1,待处理 14/)).not.toBeNull(); + expect(screen.getByText(/任务:已完成 1,待处理 15/)).not.toBeNull(); expect(screen.getByText(/资产:1 个/)).not.toBeNull(); expect( screen.getByText(/预览:运行中 http:\/\/127\.0\.0\.1:3210\//), @@ -1420,11 +1428,77 @@ describe('AI 游戏创作 App 界面边界', () => { expect(screen.getByText('game.generate_draft')).not.toBeNull(); expect( screen.getByText( - '调用 LLM,写入 /tmp/authorized-game/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器', + '调用 LLM Planner / Generator,编排 6 组角色 brief,写入 /tmp/authorized-game/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并交给外部浏览器', ), ).not.toBeNull(); }); + it('streams agent generation progress into the user chat', async () => { + const manifest = createGameCreationAppManifest( + 'local-project-draft', + '未命名游戏原型', + ); + let progressHandler: + | ((event: { + payload: { projectPath: string; stage: string; message: string }; + }) => void) + | null = null; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'append_local_permission_log') { + return {}; + } + if (command !== 'init_local_game_project') { + throw new Error(`unexpected invoke ${command}`); + } + const projectPath = String(args?.projectPath ?? ''); + return { + projectPath, + manifestPath: `${projectPath}/.agent/manifest.json`, + manifest, + }; + }, + ); + const listen = vi.fn(async (_event: string, handler: typeof progressHandler) => { + progressHandler = handler; + return vi.fn(); + }); + window.__TAURI__ = { core: { invoke }, event: { listen } }; + renderAppAt('/'); + + submitChat('/project /tmp/authorized-game'); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText('已设置本地项目:/tmp/authorized-game'), + ).not.toBeNull(); + + await act(async () => { + progressHandler?.({ + payload: { + projectPath: '/tmp/other-game', + stage: 'llm.planner', + message: '不应该显示', + }, + }); + progressHandler?.({ + payload: { + projectPath: '/tmp/authorized-game', + stage: 'llm.planner', + message: 'Planner 正在调用 LLM 整理规格和专业组分工', + }, + }); + }); + + expect(listen).toHaveBeenCalledWith( + 'game-creator-agent-progress', + expect.any(Function), + ); + expect(screen.queryByText('不应该显示')).toBeNull(); + expect( + screen.getByText('Planner 正在调用 LLM 整理规格和专业组分工'), + ).not.toBeNull(); + }); + it('shows agent loop evidence in chat after generation completes', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', @@ -1449,6 +1523,77 @@ describe('AI 游戏创作 App 界面边界', () => { goal: '做一个反弹弹幕厨房游戏', coordination: 'filesystem', steps: [ + { + pass: 0, + agent: 'Planner', + phase: 'planning', + taskId: 'design-director', + group: 'design', + role: 'Director', + status: 'completed', + inputPaths: [ + 'memory/session.md', + 'memory/project.md', + '.agent/manifest.json', + ], + outputPaths: ['.agent/spec.md'], + summary: '完成玩法规格和专业组分工', + toolCalls: [ + { + toolId: 'llm.chat.planner', + status: 'completed', + inputPaths: ['memory/session.md', 'memory/project.md'], + outputPaths: ['.agent/spec.md'], + summary: 'Planner 规格生成', + }, + ], + }, + { + pass: 2, + agent: 'Generator', + phase: 'generate', + taskId: 'code-prototype', + group: 'code', + role: 'Code', + status: 'completed', + inputPaths: [ + '.agent/spec.md', + '.agent/findings.md', + '.agent/passes/pass-2/agenda.md', + ], + outputPaths: ['.agent/passes/pass-2/draft.json'], + summary: '生成结构化游戏草案', + toolCalls: [ + { + toolId: 'llm.chat.generator', + status: 'completed', + inputPaths: ['.agent/spec.md', '.agent/findings.md'], + outputPaths: ['.agent/passes/pass-2/draft.json'], + summary: 'Generator 草案生成', + }, + ], + }, + { + pass: 2, + agent: 'Evaluator', + phase: 'evaluation', + taskId: 'quality-review', + group: 'code', + role: 'Review', + status: 'passed', + inputPaths: ['.agent/passes/pass-2/draft.json'], + outputPaths: ['.agent/findings.md'], + summary: '质量评审通过', + toolCalls: [ + { + toolId: 'evaluator.quality_review', + status: 'completed', + inputPaths: ['.agent/passes/pass-2/draft.json'], + outputPaths: ['.agent/findings.md'], + summary: 'Evaluator 质量评审', + }, + ], + }, { pass: 2, agent: '美术组 / Asset', @@ -1492,13 +1637,13 @@ describe('AI 游戏创作 App 界面边界', () => { taskGraph: { goal: '做一个反弹弹幕厨房游戏', readyTaskIds: ['preview-playtest'], - activeTaskIds: ['code-director', 'preview-readiness'], + activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director', 'design-foundation'], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-director', 'preview-readiness'], + taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], @@ -1509,14 +1654,19 @@ describe('AI 游戏创作 App 界面边界', () => { pass: 2, mode: 'repair', summary: '第 2 轮按 Evaluator 反馈返工', - activeTaskIds: ['code-director', 'preview-readiness'], + activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director', 'design-foundation'], - dependencyWaves: [['code-director'], ['preview-readiness']], + dependencyWaves: [ + ['code-director'], + ['code-prototype'], + ['quality-review'], + ['preview-readiness'], + ], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-director', 'preview-readiness'], + taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], @@ -1609,17 +1759,23 @@ describe('AI 游戏创作 App 界面边界', () => { submitChat('做一个反弹弹幕厨房游戏'); fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect(screen.getByText(/开始调用 LLM:Planner 正在整理规格。/)).not.toBeNull(); + expect(screen.getByText(/Generator 生成代码和资产清单/)).not.toBeNull(); expect( await screen.findByText(/已保存并启动本地预览:http:\/\/127\.0\.0\.1:3210\//), ).not.toBeNull(); expect(screen.getByText(/已交给外部浏览器打开。/)).not.toBeNull(); + expect(screen.getByText(/Run:run-chat-generate/)).not.toBeNull(); expect( - screen.getByText(/Agent loop:passed · 2\/3 轮 · evaluator-passed/), + screen.getByText(/状态:passed · 2\/3 轮 · evaluator-passed/), ).not.toBeNull(); expect(screen.getByText(/工具调用:36\/128/)).not.toBeNull(); + expect(screen.getByText(/LLM 对话:/)).not.toBeNull(); + expect(screen.getByText(/Planner #0 · completed · planning · llm\.chat\.planner/)).not.toBeNull(); + expect(screen.getByText(/Generator #2 · completed · generate · llm\.chat\.generator/)).not.toBeNull(); expect( screen.getByText( - /active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/, + /active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/, ), ).not.toBeNull(); expect(screen.getByText(/carry-over 任务:策划组/)).not.toBeNull(); @@ -1630,11 +1786,11 @@ describe('AI 游戏创作 App 界面边界', () => { expect( screen.getByText(/\/sync-canvas-project <画板项目ID>/), ).not.toBeNull(); - expect( - screen.getByText( - /本地产物:game\/index\.html,game\/game_design\.md,assets\/manifest\.art\.json/, - ), - ).not.toBeNull(); + expect(screen.getByText(/编排轮次:/)).not.toBeNull(); + expect(screen.getByText(/产物快照:/)).not.toBeNull(); + expect(screen.getByText(/- game\/index\.html · fnv1a64:game/)).not.toBeNull(); + expect(screen.getByText(/最近步骤:/)).not.toBeNull(); + expect(screen.getByText(/Evaluator #2 · passed · evaluation/)).not.toBeNull(); expect(screen.getByText(/完整 trace:\/trace/)).not.toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(screen.queryByText('编排 Trace')).toBeNull(); @@ -1896,13 +2052,13 @@ describe('AI 游戏创作 App 界面边界', () => { taskGraph: { goal: '做一个反弹弹幕厨房游戏', readyTaskIds: ['code-director'], - activeTaskIds: ['code-director', 'preview-readiness'], + activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director'], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-director', 'preview-readiness'], + taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-repair', }, ], @@ -1913,14 +2069,19 @@ describe('AI 游戏创作 App 界面边界', () => { pass: 2, mode: 'repair', summary: '第 2 轮按 Evaluator 反馈返工', - activeTaskIds: ['code-director', 'preview-readiness'], + activeTaskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], carriedTaskIds: ['design-director'], - dependencyWaves: [['code-director'], ['preview-readiness']], + dependencyWaves: [ + ['code-director'], + ['code-prototype'], + ['quality-review'], + ['preview-readiness'], + ], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', - taskIds: ['code-director', 'preview-readiness'], + taskIds: ['code-director', 'code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-repair', }, ], @@ -1970,7 +2131,7 @@ describe('AI 游戏创作 App 界面边界', () => { ).not.toBeNull(); expect( screen.getByText( - /active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/, + /active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/, ), ).not.toBeNull(); expect(screen.getByText(/返工路线:code-repair/)).not.toBeNull(); diff --git a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts index 4564f2e99..4217f7e3b 100644 --- a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts +++ b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts @@ -95,7 +95,7 @@ describe('AI 游戏创作聊天记忆命令', () => { '/tmp/game', ), ).toBe( - '调用 LLM,写入 /tmp/game/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器', + '调用 LLM Planner / Generator,编排 6 组角色 brief,写入 /tmp/game/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并交给外部浏览器', ); }); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 2e73ff813..dfe407a5f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,22 @@ --- +## 2026-06-26 AI 游戏创作 App 生成过程必须在聊天可见 + +- 背景:普通用户窗口只保留聊天入口,但如果生成确认后只显示“已生成草案”和本地产物路径,真实 LLM / Agent loop 会被误解成固定模板落盘。 +- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照,`/trace` 继续读取同一份完整证据。 +- 影响范围:`apps/ai-game-creator-shell/src/App.tsx`、`apps/ai-game-creator-shell/src-tauri/src/main.rs`、AI 游戏创作 App 聊天体验和实施计划文档。 +- 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + +## 2026-06-26 AI 游戏创作 App 增加显式质量评审 Gate + +- 背景:AI 游戏创作 App 已有 Evaluator loop 和静态 smoke,但任务图、能力清单和 trace 中没有单独的质检 / 评审任务,用户无法从 `/tasks`、`/trace` 或 `/audit` 看出质量评审是明确环节。 +- 决策:保持策划、美术、程序、数值、音乐、运营 6 个专业组不变,在程序组内新增 `quality-review` / `Review` 角色任务;Evaluator 的评审 step 绑定到该任务,依赖顺序为 `code-prototype -> quality-review -> preview-readiness -> preview-playtest -> publish-strategy -> publish-package`。`game.static_smoke` 只完成 `preview-readiness`,不代替质量评审。 +- 影响范围:AI 游戏创作 App 任务图、共享契约、Tauri trace / manifest 状态推导、聊天 `/capabilities` `/tasks` `/trace` `/audit` 摘要和实施计划文档。 +- 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + ## 2026-06-25 AI 游戏创作 App 真实 LLM 联调用流式请求 - 背景:AI 游戏创作 App 的真实 OpenAI-compatible provider 验收中,小请求可返回,但 Planner 等稍长非流式请求会在上游响应前被网关空闲连接切断,表现为 TLS record 解密失败;本地无密钥 provider smoke 不能覆盖该真实网关行为。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 095e1c1c2..a0ba19ab5 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -86,7 +86,7 @@ game-project/ ## v1 验收证据矩阵 -- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后自动读取 `.agent/run.latest.json` 并在普通聊天消息里展示 loop 轮次 / 工具调用 / active 任务 / carry-over / 返工焦点 / 画板同步建议命令 / 本地产物、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 +- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 - `npm run check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 窗口边界、正式用户 App 不嵌入游戏预览 iframe、用户侧预览命令交给外部浏览器和 Tauri release `--no-bundle` 构建 smoke;用于证明正式用户窗口只登记 `main` 聊天窗口,开发面板只在 debug/dev 路径打开,独立壳能完成 release 编译。 - `npm run check:encoding` 与 `git diff --check`:覆盖中文文档、中文命令文案和补丁空白;用于避免乱码、尾随空白和无关格式漂移。 - `npm run ai-game-creator-shell:llm-status`:只检查 LLM 环境变量是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取仓库根目录或 `apps/ai-game-creator-shell/` 下 gitignored 的 `.env.secrets.local`,再检查当前进程环境。 @@ -100,17 +100,17 @@ game-project/ - 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。 - 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 环境变量是否就绪;CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取 gitignored 的 `.env.secrets.local`,不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。 - 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-agent` 编排测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke。 -- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner、组内角色和 Generator,避免长请求非流式空闲断连。 +- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。 - 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、provider prompt 收到图片与音频资产上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。 - `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`,Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。 -- `.agent/manifest.json` 会保存 6 个专业组下 15 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Preview`、`Playtest`、`Publish`;开发窗口的专业组面板读取 manifest,而不是前端硬编码。 +- `.agent/manifest.json` 会保存 6 个专业组下 16 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Review`、`Preview`、`Playtest`、`Publish`;程序组内显式包含 `quality-review` 质量评审 gate,由 Evaluator trace 标记完成;开发窗口的专业组面板读取 manifest,而不是前端硬编码。 - 共享契约和 `platform-agent` 会按任务依赖与 `completed` 状态计算当前可执行任务,作为 v1 的最小编排选择器;每轮 `Orchestrator` 的 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 由 `platform-agent` 纯编排内核产出,`apps/ai-game-creator-shell` 只负责写入 `.agent/passes/pass-N/` 和执行本地工具;`Evaluator` 会在 `.agent/findings.md` 写出 `## Repair Routes` JSON,下一轮编排优先采用该结构化 taskIds,解析不到时才退回关键词路由;返工路由会按任务图自动扩展下游影响任务,例如美术资产变化会继续触发程序预览和运营包装重算。 -- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner、组内角色和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。 +- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY`、`GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL`、`GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL`;`GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner 和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。 - 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不会显示或保存 API Key。 - `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。 -- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 15 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups//*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做本地静态验收并写 `.agent/findings.md`。 +- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups//*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。 - loop 最多执行 3 轮;Evaluator 发现 HTML 非自包含、缺少 `canvas`、缺少 `requestAnimationFrame`、缺少输入监听或用户输入未转义时,把问题写入 `.agent/findings.md` 并让下一轮 Generator 修复。3 轮仍失败则 `game.generate_draft` 失败,不写最终游戏产物。 -- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/.json`,记录 `Planner` / `Orchestrator` agenda / 15 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/manifest.json` 和 agenda 等上下文来源;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 列出和载入历史 run,普通用户窗口不展示。 +- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/.json`,记录 `Planner` / `Orchestrator` agenda / 16 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` 质量评审 / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/manifest.json` 和 agenda 等上下文来源;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 列出和载入历史 run,普通用户窗口不展示。 - `.agent/run.latest.json` 的 `artifacts` 使用结构化记录,包含相对路径、字节数和 `fnv1a64:` checksum;除最终本地产物外,也会收集 `.agent/passes/pass-N/` 快照,便于确认返工前后的产物差异。 - 通过 Evaluator 和 `game.static_smoke` 后,Agent loop 会把本次 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md` 与 `memory/project.md`;下一次 Planner、组内角色和 Generator 会通过记忆输入自然读取上一轮稳定原型状态,而不只依赖开发窗口 trace。 - `.agent/agent.db` 当前作为最小本地索引文件使用 JSONL:初始化写入 `project.init`,每次 `game.generate_draft` 追加目标、标题和本地产物路径,上传 / 登记 / 画板导入资产时追加 `asset.register` 或 `asset.update`;v1 不引入 SQLite 依赖。 @@ -118,7 +118,7 @@ game-project/ - `game.generate_draft` 写入最终产物后会复用白名单受限命令 `game.static_smoke` 做一次生成后自检,至少检查 `game/index.html` 包含 canvas、canvas 渲染上下文、绘制调用、主循环、非空输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,也不得包含固定星核传送门模板词、纯按钮计分模板或 `TODO` / `待实现` / `这里省略` 等未完成实现;画板资源占位引用允许出现在 asset id 或说明中,并把该工具调用写入 `.agent/run.latest.json` 与 `.agent/logs/command.log`;自检失败则本次命令失败,不继续启动预览。 - `ArtifactWriter` step 使用 `file.write.local_artifacts` 工具调用记录最终写入的 `memory/`、`game/`、`assets/`、`exports/` 和 `.agent/manifest.json` 路径;写入完成后 `nextStep` 指向 `game.static_smoke`。 - `preview.start` / `preview.stop` 会追加 `.agent/logs/preview.log`,并在 `.agent/run.latest.json` 已存在时追加 `Preview` step 和 `preview.*` toolCall,记录本地 HTTP 预览 URL 与停止事件;单全局本地预览被新项目替换时,会 best-effort 把旧项目 manifest、preview log 和 trace 记录为 stopped,避免旧项目残留 running;本地 HTTP server 的 `/` 映射到 `game/index.html`,只允许读取 canonical 后仍位于项目真实 `game/` 或真实 `assets/` 下的文件,拒绝 `memory/`、`.agent/`、`exports/`、`..`、一级 `game` / `assets` 符号链接目录和内部符号链接越界,并为常见图片、音频、视频和 Web 资源返回对应 MIME;静态 `HEAD` 返回真实 `Content-Length` 但不返回 body,确保浏览器和媒体资源探测可用;上传和画板回流资产可被生成游戏引用但不会暴露记忆或 trace;没有 run trace 的手动预览启动不阻断。 -- 聊天输入会生成待确认的 `game.generate_draft` 内置命令;用户确认后才把 LLM 返回的结构化草案写入短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、设计草案 `game/game_design.md`、数值配置 `game/balance.json`、美术清单 `assets/manifest.art.json`、音乐音效清单 `assets/manifest.audio.json`、发布包装草案 `exports/README.md` 和可运行 `game/index.html`。生成完成后,普通聊天消息会自动展示最近一次 Agent loop 的轮次、工具调用、active / carry-over 任务、返工焦点、画板同步建议命令和本地产物摘要;完整证据仍由 `/trace` 读取同一份 `.agent/run.latest.json`。 +- 聊天输入会生成待确认的 `game.generate_draft` 内置命令;用户确认后,正式用户聊天会实时展示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator 质量评审、ArtifactWriter 和 `game.static_smoke` 的进度,再把 LLM 返回的结构化草案写入短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、设计草案 `game/game_design.md`、数值配置 `game/balance.json`、美术清单 `assets/manifest.art.json`、音乐音效清单 `assets/manifest.audio.json`、发布包装草案 `exports/README.md` 和可运行 `game/index.html`。生成完成后,普通聊天消息会自动展示最近一次 Agent loop 的 Run、LLM 对话、轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照;完整证据仍由 `/trace` 读取同一份 `.agent/run.latest.json`。 - `game.generate_draft` 的 `game/index.html` 必须是可试玩原型,至少包含输入、主循环、目标、失败或胜利状态和重开路径;不能只输出按钮计分或纯展示页。 - `game.generate_draft` 会校验 LLM 输出:`balance`、美术清单和音乐清单必须是 JSON object,`gameHtml` 必须是自包含 HTML、包含 `canvas` 与 `requestAnimationFrame`,不得加载远程脚本或资源,不得使用 `eval` / `new Function` / `localStorage` / `fetch` / `WebSocket` / `ServiceWorker`,不得把包含 `<` / `>` 的用户输入原样写入 HTML。 - 同一项目内多次 `game.generate_draft` 不覆盖记忆文件,而是继续追加短期对话记录和长期创作目标记录,保留用户迭代历史。 diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index f5569172f..745aa5e8f 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -87,6 +87,7 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [ area: 'agent-runtime', title: '组内角色协作', }, + { id: 'quality-review', area: 'agent-runtime', title: '质量评审' }, { id: 'repair-loop-carryover', area: 'agent-runtime', @@ -257,13 +258,23 @@ export const GAME_CREATION_APP_SEED_TASKS = [ artifacts: ['game/'], acceptanceCriteria: ['本地 Web 游戏项目可以通过 HTTP server 打开'], }, + { + id: 'quality-review', + title: '执行质量评审', + group: 'code', + role: 'Review', + status: 'pending', + dependencies: ['code-prototype'], + artifacts: ['.agent/findings.md', '.agent/run.latest.json'], + acceptanceCriteria: ['玩法、资产、数值、程序和发布包装通过跨专业组质量评审'], + }, { id: 'preview-readiness', title: '执行静态自检', group: 'code', role: 'Preview', status: 'pending', - dependencies: ['code-prototype'], + dependencies: ['quality-review'], artifacts: ['.agent/logs/command.log', '.agent/run.latest.json'], acceptanceCriteria: ['HTML 自包含且通过本地静态 smoke'], }, diff --git a/server-rs/crates/platform-agent/src/game_creation.rs b/server-rs/crates/platform-agent/src/game_creation.rs index 2c24608d6..a4adc044f 100644 --- a/server-rs/crates/platform-agent/src/game_creation.rs +++ b/server-rs/crates/platform-agent/src/game_creation.rs @@ -170,12 +170,21 @@ pub fn build_game_creation_seed_task_graph( ["game/"], ["本地 Web 游戏项目可以通过 HTTP server 打开"], ), + task( + "quality-review", + "执行质量评审", + GameCreationAgentGroup::Code, + "Review", + ["code-prototype"], + [".agent/findings.md", ".agent/run.latest.json"], + ["玩法、资产、数值、程序和发布包装通过跨专业组质量评审"], + ), task( "preview-readiness", "执行静态自检", GameCreationAgentGroup::Code, "Preview", - ["code-prototype"], + ["quality-review"], [".agent/logs/command.log", ".agent/run.latest.json"], ["HTML 自包含且通过本地静态 smoke"], ), @@ -858,7 +867,7 @@ mod tests { ); assert_eq!(plan.mode, "initial"); - assert_eq!(plan.active_task_ids.len(), 15); + assert_eq!(plan.active_task_ids.len(), 16); assert!(plan.carried_task_ids.is_empty()); assert_eq!(plan.repair_routes, Vec::new()); assert_eq!(plan.dependency_waves[0], vec!["design-director"]); @@ -879,6 +888,7 @@ mod tests { vec![ "code-director", "code-prototype", + "quality-review", "preview-readiness", "preview-playtest", "publish-strategy", @@ -926,6 +936,7 @@ mod tests { plan.active_task_ids, vec![ "code-prototype", + "quality-review", "preview-readiness", "preview-playtest", "publish-strategy", @@ -936,6 +947,7 @@ mod tests { plan.repair_routes[0].task_ids, vec![ "code-prototype", + "quality-review", "preview-readiness", "preview-playtest", "publish-strategy", @@ -981,6 +993,7 @@ mod tests { "art-polish", "code-director", "code-prototype", + "quality-review", "preview-readiness", "preview-playtest", "publish-strategy", @@ -1014,7 +1027,7 @@ mod tests { ); assert_eq!(plan.mode, "repair"); - assert_eq!(plan.active_task_ids.len(), 15); + assert_eq!(plan.active_task_ids.len(), 16); assert!(plan.carried_task_ids.is_empty()); assert_eq!(plan.repair_routes[0].reason, "cross-group-handoff"); } diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index 99b3b47b7..410dd9d4e 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -68,7 +68,7 @@ pub struct GameCreationAgentCapabilityDescriptor { pub title: &'static str, } -pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 21] = [ +pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 22] = [ capability("chat", "user", "聊天入口"), capability("file-upload", "user", "上传文件"), capability("built-in-commands", "agent-runtime", "内置命令调用"), @@ -89,6 +89,7 @@ pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescript capability("tool-call-budget", "agent-runtime", "工具调用预算"), capability("multi-agent-collaboration", "agent-runtime", "多智能体协作"), capability("role-level-collaboration", "agent-runtime", "组内角色协作"), + capability("quality-review", "agent-runtime", "质量评审"), capability( "repair-loop-carryover", "agent-runtime", @@ -269,12 +270,21 @@ pub fn new_game_creation_app_seed_tasks() -> Vec { ["game/"], ["本地 Web 游戏项目可以通过 HTTP server 打开"], ), + task( + "quality-review", + "执行质量评审", + GameCreationAppAgentGroup::Code, + "Review", + ["code-prototype"], + [".agent/findings.md", ".agent/run.latest.json"], + ["玩法、资产、数值、程序和发布包装通过跨专业组质量评审"], + ), task( "preview-readiness", "执行静态自检", GameCreationAppAgentGroup::Code, "Preview", - ["code-prototype"], + ["quality-review"], [".agent/logs/command.log", ".agent/run.latest.json"], ["HTML 自包含且通过本地静态 smoke"], ), @@ -869,6 +879,7 @@ mod tests { "audio-asset-plan", "code-director", "code-prototype", + "quality-review", "preview-readiness", "preview-playtest", "publish-strategy",