From 1b4bca1ab316e241203715ccc42517bca2cabfdd Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Thu, 25 Jun 2026 20:55:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20AI=20=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E5=88=9B=E4=BD=9C=E7=9C=9F=E5=AE=9E=E7=BD=91=E5=85=B3=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整 AI 游戏创作 App 的 LLM 默认超时和输出上限 将组内角色 brief 改为本地编排生成,保留 6 组多角色 trace,减少真实 provider 调用 允许画板资源占位引用通过自检,继续拒绝固定模板和未完成实现 更新本地 provider smoke 和 Agent trace 断言 --- .../smoke-agent-run-local-provider.mjs | 35 +- .../src-tauri/src/main.rs | 318 +++++++++--------- .../tests/agentTraceSummary.test.ts | 2 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 4 files changed, 162 insertions(+), 195 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs index a44f71820..a031cedd5 100644 --- a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs +++ b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs @@ -23,33 +23,6 @@ const smokeAssetBytes = Buffer.concat([ const smokeAudioAssetPath = 'assets/uploads/smoke-bounce.mp3'; const smokeAudioAssetBytes = 'SMOKE_LOCAL_AUDIO:bounce'; -const groupRoles = [ - ['策划组', 'Director', 'design', 'director'], - ['策划组', 'Gameplay', 'design', 'gameplay'], - ['数值组', 'Director', 'balance', 'director'], - ['数值组', 'Difficulty', 'balance', 'difficulty'], - ['美术组', 'Director', 'art', 'director'], - ['美术组', 'Asset', 'art', 'asset'], - ['美术组', 'Polish', 'art', 'polish'], - ['音乐组', 'Director', 'audio', 'director'], - ['音乐组', 'SFX', 'audio', 'sfx'], - ['程序组', 'Director', 'code', 'director'], - ['程序组', 'Code', 'code', 'code'], - ['程序组', 'Preview', 'code', 'preview'], - ['程序组', 'Playtest', 'code', 'playtest'], - ['运营组', 'Director', 'publishing', 'director'], - ['运营组', 'Publish', 'publishing', 'publish'], -]; - -function roleBrief(pass, [label, role, group, id]) { - return [ - `本角色判断:pass ${pass} ${label} ${role} 负责 ${group}.${id}`, - `交付物:${group}/${id}`, - '下游约束:必须围绕反弹弹幕厨房,不能输出固定模板。', - '验收风险:缺输入、缺主循环或缺胜负状态都要返工。', - ].join('\n'); -} - function handoffs() { return [ [ @@ -203,11 +176,7 @@ function draft({ withInput }) { const responses = [ '## 核心循环\n\n反弹弹幕点亮三口锅。\n\n## Evaluator 验收\n\n必须有输入监听、主循环、胜负状态和重开路径。', - ...groupRoles.map((role) => roleBrief(1, role)), JSON.stringify(draft({ withInput: false })), - ...groupRoles - .filter(([, , group]) => group === 'code' || group === 'publishing') - .map((role) => roleBrief(2, role)), JSON.stringify(draft({ withInput: true })), ]; @@ -498,7 +467,7 @@ try { (step) => step.pass === 2 && step.agent === '程序组 / Director' && - step.toolCalls?.[0]?.toolId === 'llm.chat.group.code.director', + step.toolCalls?.[0]?.toolId === 'agent.role.brief.code.director', ), 'trace missing code repair role', ); @@ -507,7 +476,7 @@ try { (step) => step.pass === 2 && step.agent === '运营组 / Publish' && - step.toolCalls?.[0]?.toolId === 'llm.chat.group.publishing.publish', + step.toolCalls?.[0]?.toolId === 'agent.role.brief.publishing.publish', ), 'trace missing downstream publishing repair role', ); 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 2897da85f..87c49778f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -14,8 +14,7 @@ use platform_agent::{ route_game_creation_repair_issues, }; use platform_llm::{ - LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmTextRequest, DEFAULT_REQUEST_TIMEOUT_MS, - DEFAULT_RETRY_BACKOFF_MS, + LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmTextRequest, DEFAULT_RETRY_BACKOFF_MS, }; use reqwest::header; use serde::{Deserialize, Serialize}; @@ -343,7 +342,9 @@ const DEFAULT_GAME_INDEX_HTML: &str = r#" "#; const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000"; -const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 7000; +const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 5000; +const GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS: u32 = 900; +const GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 180_000; const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3; const GAME_CREATOR_AGENT_TOOL_CALL_MAX: u16 = GAME_CREATION_AGENT_TOOL_CALL_MAX; const GAME_CREATOR_AGENT_DB_SCHEMA_VERSION: &str = "game-creator-agent-db.v1"; @@ -389,14 +390,14 @@ static DESIGN_AGENT_ROLES: [AgentRoleDefinition; 2] = [ id: "director", role: "Director", task_id: "design-director", - tool_id: "llm.chat.group.design.director", + tool_id: "agent.role.brief.design.director", brief_path_name: "director.md", }, AgentRoleDefinition { id: "gameplay", role: "Gameplay", task_id: "design-foundation", - tool_id: "llm.chat.group.design.gameplay", + tool_id: "agent.role.brief.design.gameplay", brief_path_name: "gameplay.md", }, ]; @@ -406,14 +407,14 @@ static BALANCE_AGENT_ROLES: [AgentRoleDefinition; 2] = [ id: "director", role: "Director", task_id: "balance-director", - tool_id: "llm.chat.group.balance.director", + tool_id: "agent.role.brief.balance.director", brief_path_name: "director.md", }, AgentRoleDefinition { id: "difficulty", role: "Difficulty", task_id: "balance-seed", - tool_id: "llm.chat.group.balance.difficulty", + tool_id: "agent.role.brief.balance.difficulty", brief_path_name: "difficulty.md", }, ]; @@ -423,21 +424,21 @@ static ART_AGENT_ROLES: [AgentRoleDefinition; 3] = [ id: "director", role: "Director", task_id: "art-director", - tool_id: "llm.chat.group.art.director", + tool_id: "agent.role.brief.art.director", brief_path_name: "director.md", }, AgentRoleDefinition { id: "asset", role: "Asset", task_id: "art-asset-plan", - tool_id: "llm.chat.group.art.asset", + tool_id: "agent.role.brief.art.asset", brief_path_name: "asset.md", }, AgentRoleDefinition { id: "polish", role: "Polish", task_id: "art-polish", - tool_id: "llm.chat.group.art.polish", + tool_id: "agent.role.brief.art.polish", brief_path_name: "polish.md", }, ]; @@ -447,14 +448,14 @@ static AUDIO_AGENT_ROLES: [AgentRoleDefinition; 2] = [ id: "director", role: "Director", task_id: "audio-director", - tool_id: "llm.chat.group.audio.director", + tool_id: "agent.role.brief.audio.director", brief_path_name: "director.md", }, AgentRoleDefinition { id: "sfx", role: "SFX", task_id: "audio-asset-plan", - tool_id: "llm.chat.group.audio.sfx", + tool_id: "agent.role.brief.audio.sfx", brief_path_name: "sfx.md", }, ]; @@ -464,28 +465,28 @@ static CODE_AGENT_ROLES: [AgentRoleDefinition; 4] = [ id: "director", role: "Director", task_id: "code-director", - tool_id: "llm.chat.group.code.director", + tool_id: "agent.role.brief.code.director", brief_path_name: "director.md", }, AgentRoleDefinition { id: "code", role: "Code", task_id: "code-prototype", - tool_id: "llm.chat.group.code.code", + tool_id: "agent.role.brief.code.code", brief_path_name: "code.md", }, AgentRoleDefinition { id: "preview", role: "Preview", task_id: "preview-readiness", - tool_id: "llm.chat.group.code.preview", + tool_id: "agent.role.brief.code.preview", brief_path_name: "preview.md", }, AgentRoleDefinition { id: "playtest", role: "Playtest", task_id: "preview-playtest", - tool_id: "llm.chat.group.code.playtest", + tool_id: "agent.role.brief.code.playtest", brief_path_name: "playtest.md", }, ]; @@ -495,14 +496,14 @@ static PUBLISHING_AGENT_ROLES: [AgentRoleDefinition; 2] = [ id: "director", role: "Director", task_id: "publish-strategy", - tool_id: "llm.chat.group.publishing.director", + tool_id: "agent.role.brief.publishing.director", brief_path_name: "director.md", }, AgentRoleDefinition { id: "publish", role: "Publish", task_id: "publish-package", - tool_id: "llm.chat.group.publishing.publish", + tool_id: "agent.role.brief.publishing.publish", brief_path_name: "publish.md", }, ]; @@ -1229,7 +1230,7 @@ fn build_game_creator_llm_client_from_env() -> Result { "GENARRATIVE_LLM_REQUEST_TIMEOUT_MS", "LLM_REQUEST_TIMEOUT_MS", ], - DEFAULT_REQUEST_TIMEOUT_MS, + GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS, )?; let retry_backoff_ms = read_u64_env( &[ @@ -1323,7 +1324,7 @@ fn check_game_creator_llm_config_values( base_url.to_string(), api_key.to_string(), model.to_string(), - DEFAULT_REQUEST_TIMEOUT_MS, + GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS, 0, DEFAULT_RETRY_BACKOFF_MS, ) @@ -1467,7 +1468,7 @@ async fn run_game_creator_agent_loop_at( &[".agent/spec.md", ".agent/findings.md"], &[], &issues[0], - "llm.chat.group", + "agent.role.brief", )); latest_findings = render_evaluator_findings(pass, &issues); fs::write(&findings_path, &latest_findings).map_err(|write_error| { @@ -1634,15 +1635,15 @@ async fn request_planner_spec_with_client( long_memory, )), ]) - .with_max_tokens(1800); + .with_max_tokens(GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS); let response = request_game_creator_llm_text(client, request) .await .map_err(|error| format!("Planner 生成失败:{error}"))?; - let spec = response.content.trim(); + let spec = strip_llm_thinking_blocks(response.content.as_str()); if spec.is_empty() { Err("Planner 未返回规格".to_string()) } else { - Ok(spec.to_string()) + Ok(spec) } } @@ -1672,7 +1673,8 @@ async fn request_generator_game_draft_with_client( let response = request_game_creator_llm_text(client, request) .await .map_err(|error| format!("LLM 生成失败:{error}"))?; - parse_llm_game_draft_response(response.content.as_str()) + let content = strip_llm_thinking_blocks(response.content.as_str()); + parse_llm_game_draft_response(content.as_str()) } async fn request_game_creator_llm_text( @@ -1702,7 +1704,7 @@ fn game_creator_llm_stream_enabled() -> bool { async fn request_agent_group_briefs_with_client( root: &Path, - client: &LlmClient, + _client: &LlmClient, prompt: &str, short_memory: &str, long_memory: &str, @@ -1763,55 +1765,32 @@ async fn request_agent_group_briefs_with_client( if !should_run { continue; } - let request = LlmTextRequest::new(vec![ - LlmMessage::system(game_creator_role_system_prompt( - definition, - *role_definition, - )), - LlmMessage::user(game_creator_role_user_prompt( - definition, - *role_definition, - prompt, - short_memory, - long_memory, - spec_markdown, - findings_markdown, - &agenda_markdown, - &completed_group_context, - &completed_role_context, - )), - ]) - .with_max_tokens(900); - let response = request_game_creator_llm_text(client, request) - .await - .map_err(|error| { - format!( - "{} / {} 生成失败:{error}", - definition.label, role_definition.role - ) - })?; - let markdown = response.content.trim(); - if markdown.is_empty() { - return Err(format!( - "{} / {} 未返回 brief", - definition.label, role_definition.role - )); - } + let markdown = render_local_agent_role_brief( + definition, + *role_definition, + prompt, + short_memory, + long_memory, + spec_markdown, + findings_markdown, + &agenda_markdown, + &completed_group_context, + &completed_role_context, + pass, + ); let relative_path = - write_agent_role_brief(root, pass, definition, *role_definition, markdown)?; + write_agent_role_brief(root, pass, definition, *role_definition, &markdown)?; let role_brief = AgentRoleBrief { group_definition: definition, role_definition: *role_definition, - markdown: markdown.to_string(), + markdown, relative_path, status: "completed".to_string(), tool_id: role_definition.tool_id.to_string(), - summary: markdown - .lines() - .map(str::trim) - .find(|line| !line.is_empty()) - .unwrap_or("完成组内角色 brief") - .to_string(), + summary: format!( + "本地编排生成 {} / {} brief", + definition.label, role_definition.role + ), }; completed_role_context.push_str(&render_agent_role_brief_context(&role_brief)); role_briefs.push(role_brief); @@ -1834,6 +1813,75 @@ async fn request_agent_group_briefs_with_client( Ok(briefs) } +fn render_local_agent_role_brief( + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, + prompt: &str, + short_memory: &str, + long_memory: &str, + spec_markdown: &str, + findings_markdown: &str, + agenda_markdown: &str, + completed_group_context: &str, + completed_role_context: &str, + pass: u8, +) -> String { + format!( + "本角色判断:pass {pass},{} / {} 负责 {},围绕用户需求“{}”给 Generator 提供约束。\n交付物:{}\n下游约束:{}\n验收风险:{}\n\n## Planner 摘要\n\n{}\n\n## Evaluator 反馈\n\n{}\n\n## 本轮 agenda\n\n{}\n\n## 记忆摘要\n\n短期:{}\n长期:{}\n\n## 已完成上下文\n\n{}\n{}", + group_definition.label, + role_definition.role, + role_definition.task_id, + truncate_inline(prompt, 80), + role_definition.brief_path_name, + local_role_downstream_constraint(group_definition, role_definition), + local_role_acceptance_risk(group_definition, role_definition), + truncate_prompt_context(spec_markdown), + truncate_prompt_context(findings_markdown), + truncate_prompt_context(agenda_markdown), + truncate_inline(short_memory, 120), + truncate_inline(long_memory, 120), + truncate_prompt_context(completed_group_context), + truncate_prompt_context(completed_role_context) + ) +} + +fn local_role_downstream_constraint( + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, +) -> &'static str { + match (group_definition.id, role_definition.id) { + ("design", _) => "Generator 必须保留核心循环、输入、目标、失败、胜利和重开路径。", + ("balance", _) => "Generator 必须输出可被 game/balance.json 表达的速度、生命、难度和节奏参数。", + ("art", "asset") => "Generator 必须保留画板资源占位引用,并给出本地 fallback 视觉。", + ("art", _) => "Generator 必须保持像素风厨房主题和首版可读性。", + ("audio", _) => "Generator 必须给出 BGM 与核心交互音效清单,缺素材时使用静音 fallback。", + ("code", _) => "Generator 必须生成单文件 canvas HTML、输入监听、requestAnimationFrame 主循环和静态自检可通过的代码。", + ("publishing", _) => "Generator 必须输出标题、简介、标签、封面需求和试玩验收说明。", + _ => "Generator 必须响应本角色交付物并保持可试玩原型闭环。", + } +} + +fn local_role_acceptance_risk( + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, +) -> &'static str { + match (group_definition.id, role_definition.id) { + ("code", _) => "缺 canvas 绘制、输入监听、胜负状态、重开或使用远程资源都会触发 Evaluator 返工。", + ("art", "asset") | ("audio", "sfx") => "未记录画板或本地资产占位会影响资产回流验收。", + ("publishing", _) => "缺发布包装会影响最终 exports/README.md 与运营组 handoff。", + _ => "输出空泛或偏离用户需求会增加 Generator 返工概率。", + } +} + +fn truncate_inline(value: &str, max_chars: usize) -> String { + let trimmed = value.split_whitespace().collect::>().join(" "); + let mut output = trimmed.chars().take(max_chars).collect::(); + if trimmed.chars().count() > max_chars { + output.push_str("..."); + } + output +} + fn evaluate_game_draft(prompt: &str, draft: &LlmGameDraft) -> Vec { let mut issues = Vec::new(); if let Err(error) = validate_llm_game_draft(prompt, draft) { @@ -2013,7 +2061,7 @@ fn render_agent_pass_agenda_markdown( } } output.push_str( - "\n## Rule\n\n- activeTasks 调用对应角色 LLM。\n- carriedTasks 沿用上一轮 brief,避免无关角色重复返工。\n- dependencyWaves 是按任务依赖排序后的执行层级,Generator 必须优先服从较早 wave 的约束。\n", + "\n## Rule\n\n- activeTasks 产出对应角色 brief。\n- carriedTasks 沿用上一轮 brief,避免无关角色重复返工。\n- dependencyWaves 是按任务依赖排序后的执行层级,Generator 必须优先服从较早 wave 的约束。\n", ); output } @@ -2722,7 +2770,7 @@ fn infer_agent_trace_phase(tool_id: &str) -> &'static str { "planning" } else if tool_id.starts_with("agent.task_graph.") { "orchestration" - } else if tool_id.starts_with("llm.chat.group.") { + } else if tool_id.starts_with("agent.role.brief.") { "role-brief" } else if tool_id.starts_with("agent.group.aggregate.") { "group-aggregate" @@ -3518,12 +3566,31 @@ fn fnv1a64(bytes: &[u8]) -> u64 { } fn parse_llm_game_draft_response(content: &str) -> Result { + let content = strip_llm_thinking_blocks(content); let payload = - extract_json_payload(content).ok_or_else(|| "LLM 返回不是 JSON 对象".to_string())?; + extract_json_payload(content.as_str()).ok_or_else(|| "LLM 返回不是 JSON 对象".to_string())?; serde_json::from_str::(payload) .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) } +fn strip_llm_thinking_blocks(content: &str) -> String { + let mut output = String::new(); + let mut rest = content; + loop { + let Some(start) = rest.to_ascii_lowercase().find("") else { + output.push_str(rest); + break; + }; + output.push_str(&rest[..start]); + let after_start = &rest[start + "".len()..]; + let Some(end) = after_start.to_ascii_lowercase().find("") else { + break; + }; + rest = &after_start[end + "".len()..]; + } + output.trim().to_string() +} + fn extract_json_payload(content: &str) -> Option<&str> { let trimmed = content.trim(); let without_fence = trimmed @@ -3702,14 +3769,15 @@ fn validate_canvas_rendering_html(html: &str, label: &str) -> Result<(), String> fn validate_non_placeholder_game_html(html: &str, label: &str) -> Result<(), String> { let lower_html = html.to_ascii_lowercase(); for forbidden in [ - "占位", - "placeholder", "星核传送门", "点击按钮加分", "点击按钮得分", + "todo:", + "待实现", + "这里省略", ] { if lower_html.contains(forbidden) { - return Err(format!("{label} 不能是占位或固定模板:{forbidden}")); + return Err(format!("{label} 不能是固定模板或未完成实现:{forbidden}")); } } let compact = lower_html @@ -3785,20 +3853,6 @@ fn game_creator_planner_system_prompt() -> &'static str { r#"你是 Genarrative 的 AI 游戏创作 Planner。输出一份给 Generator 使用的 Markdown 规格,不要生成代码。规格必须包含:核心循环、输入方式、胜负条件、首版关卡、6 个专业组分工、组内角色任务矩阵、Evaluator 验收标准。不要写客套说明。"# } -fn game_creator_role_system_prompt( - group_definition: AgentGroupDefinition, - role_definition: AgentRoleDefinition, -) -> String { - format!( - "你是 Genarrative AI 游戏创作平台的{} / {} agent,group id 为 {},role id 为 {},task id 为 {}。只输出 Markdown brief,不输出 JSON,不写客套话。brief 必须包含:本角色判断、交付物、给下游角色或下游组的约束、验收风险。", - group_definition.label, - role_definition.role, - group_definition.id, - role_definition.id, - role_definition.task_id - ) -} - fn game_creator_planner_user_prompt(prompt: &str, short_memory: &str, long_memory: &str) -> String { format!( "用户需求:\n{}\n\n短期记忆:\n{}\n\n长期记忆:\n{}\n\n请输出 Planner 规格 Markdown。", @@ -3808,33 +3862,6 @@ fn game_creator_planner_user_prompt(prompt: &str, short_memory: &str, long_memor ) } -fn game_creator_role_user_prompt( - group_definition: AgentGroupDefinition, - role_definition: AgentRoleDefinition, - prompt: &str, - short_memory: &str, - long_memory: &str, - spec_markdown: &str, - findings_markdown: &str, - agenda_markdown: &str, - completed_group_context: &str, - completed_role_context: &str, -) -> String { - format!( - "用户需求:\n{}\n\n短期记忆:\n{}\n\n长期记忆:\n{}\n\nPlanner 规格:\n{}\n\nEvaluator 反馈:\n{}\n\n本轮 Orchestrator agenda:\n{}\n\n已完成上游专业组 brief:\n{}\n\n本组已完成角色 brief:\n{}\n\n请以 {} / {} 的职责给出本轮 brief。只写本角色该交付什么、约束什么、提醒下游角色和 Generator 什么。", - prompt.trim(), - truncate_prompt_context(short_memory), - truncate_prompt_context(long_memory), - truncate_prompt_context(spec_markdown), - truncate_prompt_context(findings_markdown), - truncate_prompt_context(agenda_markdown), - truncate_prompt_context(completed_group_context), - truncate_prompt_context(completed_role_context), - group_definition.label, - role_definition.role - ) -} - fn game_creator_generator_user_prompt( prompt: &str, short_memory: &str, @@ -6239,35 +6266,6 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true base_url } - fn fake_group_brief_responses(pass: u8) -> Vec { - GAME_CREATOR_AGENT_GROUP_DEFINITIONS - .iter() - .flat_map(|definition| { - definition.roles.iter().map(move |role| { - format!( - "本角色判断:pass {pass} {} {} brief\n交付物:{}\n下游约束:必须响应月光厨房玩法\n验收风险:不能空转", - definition.label, role.role, role.brief_path_name - ) - }) - }) - .collect() - } - - fn fake_group_brief_responses_for_group(pass: u8, group_id: &str) -> Vec { - GAME_CREATOR_AGENT_GROUP_DEFINITIONS - .iter() - .filter(|definition| definition.id == group_id) - .flat_map(|definition| { - definition.roles.iter().map(move |role| { - format!( - "本角色判断:pass {pass} {} {} 修复 brief\n交付物:{}\n下游约束:必须修复 Evaluator 命中的问题\n验收风险:不能空转", - definition.label, role.role, role.brief_path_name - ) - }) - }) - .collect() - } - #[tokio::test] async fn request_llm_game_draft_uses_openai_compatible_provider_output() { let response_content = @@ -6278,7 +6276,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true base_url, "test-key".to_string(), "mock-game-model".to_string(), - DEFAULT_REQUEST_TIMEOUT_MS, + GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS, 0, DEFAULT_RETRY_BACKOFF_MS, ) @@ -6316,7 +6314,6 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true "## 核心循环\n\n用上传角色图做主角。\n\n## Evaluator 验收\n\n必须使用本地资产。" .to_string(), ]; - responses.extend(fake_group_brief_responses(1)); responses.push(serde_json::to_string(&fake_llm_game_draft()).expect("draft json")); let (sender, receiver) = mpsc::channel(); let base_url = spawn_mock_llm_server_responses_with_capture(responses, Some(sender)); @@ -6389,10 +6386,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true "## 核心循环\n\n反弹月光弹幕点亮三口锅。\n\n## Evaluator 验收\n\n必须有输入监听。" .to_string(), ]; - responses.extend(fake_group_brief_responses(1)); responses.push(serde_json::to_string(&first_draft).expect("first draft json")); - responses.extend(fake_group_brief_responses_for_group(2, "code")); - responses.extend(fake_group_brief_responses_for_group(2, "publishing")); responses.push(serde_json::to_string(&fixed_draft).expect("fixed draft json")); let base_url = spawn_mock_llm_server_responses(responses); let config = LlmConfig::new( @@ -6400,7 +6394,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true base_url, "test-key".to_string(), "mock-game-model".to_string(), - DEFAULT_REQUEST_TIMEOUT_MS, + GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS, 0, DEFAULT_RETRY_BACKOFF_MS, ) @@ -6459,7 +6453,8 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true fs::read_to_string(root.join(".agent/passes/pass-1/groups/design/director.md")) .expect("design director brief"); assert!(first_design_director.contains("策划组 / Director")); - assert!(first_design_director.contains("pass 1 策划组 Director brief")); + assert!(first_design_director.contains("本角色判断:pass 1")); + assert!(first_design_director.contains("策划组 / Director")); let first_design_brief = fs::read_to_string(root.join(".agent/passes/pass-1/groups/design.md")) .expect("design brief"); @@ -6649,7 +6644,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true && step["taskId"] == "design-director" && step["group"] == "design" && step["role"] == "Director" - && step["toolCalls"][0]["toolId"] == "llm.chat.group.design.director" + && step["toolCalls"][0]["toolId"] == "agent.role.brief.design.director" && step["inputPaths"] .as_array() .unwrap() @@ -6665,11 +6660,11 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert!(steps.iter().any(|step| step["agent"] == "程序组 / Director" && step["pass"].as_u64() == Some(2) && step["status"] == "completed" - && step["toolCalls"][0]["toolId"] == "llm.chat.group.code.director")); + && step["toolCalls"][0]["toolId"] == "agent.role.brief.code.director")); assert!(steps.iter().any(|step| step["agent"] == "运营组 / Publish" && step["pass"].as_u64() == Some(2) && step["status"] == "completed" - && step["toolCalls"][0]["toolId"] == "llm.chat.group.publishing.publish")); + && step["toolCalls"][0]["toolId"] == "agent.role.brief.publishing.publish")); assert!(steps .iter() .any(|step| step["agent"] == "策划组 / GroupCoordinator" @@ -6832,13 +6827,8 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true .to_string(); let invalid_draft_json = serde_json::to_string(&invalid_draft).expect("invalid draft json"); let mut responses = vec!["## 核心循环\n\n点亮厨房,但必须通过 Evaluator。".to_string()]; - responses.extend(fake_group_brief_responses(1)); responses.push(invalid_draft_json.clone()); - responses.extend(fake_group_brief_responses_for_group(2, "code")); - responses.extend(fake_group_brief_responses_for_group(2, "publishing")); responses.push(invalid_draft_json.clone()); - responses.extend(fake_group_brief_responses_for_group(3, "code")); - responses.extend(fake_group_brief_responses_for_group(3, "publishing")); responses.push(invalid_draft_json); let base_url = spawn_mock_llm_server_responses(responses); let previous_api_key = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY").ok(); @@ -7148,6 +7138,14 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert!(error.contains("handoffs 缺少 publishing 专业组")); } + #[test] + fn strip_llm_thinking_blocks_removes_reasoning_wrappers() { + assert_eq!( + strip_llm_thinking_blocks("内部推理\n{\"ok\":true}"), + "{\"ok\":true}" + ); + } + #[test] fn validate_llm_game_draft_requires_playable_states() { let mut draft = fake_llm_game_draft(); @@ -7229,7 +7227,7 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true let error = validate_llm_game_draft("像素厨房弹幕", &draft) .expect_err("fixed placeholder template should fail"); - assert!(error.contains("占位或固定模板")); + assert!(error.contains("固定模板或未完成实现")); } #[test] diff --git a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts index 2e1eb4bca..674cb88ae 100644 --- a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts +++ b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts @@ -52,7 +52,7 @@ describe('AI 游戏创作 Agent loop 摘要', () => { summary: '需要回流画板角色素材', toolCalls: [ { - toolId: 'llm.chat.group.art.asset', + toolId: 'agent.role.brief.art.asset', status: 'completed', inputPaths: ['.agent/manifest.json'], outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 1ec90722a..095e1c1c2 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -115,7 +115,7 @@ game-project/ - 通过 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 依赖。 - `.agent/run.latest.json` 的 schema 固定为共享契约 `GAME_CREATION_AGENT_RUN_SCHEMA_VERSION = game-creator-agent-run.v1`;TS 与 Rust 都从共享契约读取 run trace 类型,避免开发窗口和 Tauri 写入结构漂移。 -- `game.generate_draft` 写入最终产物后会复用白名单受限命令 `game.static_smoke` 做一次生成后自检,至少检查 `game/index.html` 包含 canvas、canvas 渲染上下文、绘制调用、主循环、非空输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,也不得包含占位 / placeholder / 固定星核传送门模板词,并把该工具调用写入 `.agent/run.latest.json` 与 `.agent/logs/command.log`;自检失败则本次命令失败,不继续启动预览。 +- `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`。