diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs b/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs index d802866df..fa6193cb7 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs @@ -11,6 +11,9 @@ pub(crate) mod package_layout; pub const VERSION: &str = package_layout::CODEX_VERSION; +/// 仅发行构建的协议门禁与共享单测需要它:开发构建按 profile 跳过版本门禁(见 +/// `direct_execution::bind_codex_executor` 与 `execution::validate_approval_version`)。 +#[cfg(any(test, not(debug_assertions)))] pub const CLI_VERSION: &str = package_layout::CODEX_CLI_VERSION; pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA; 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 db9db4abf..ffa82d58c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -1,8 +1,5 @@ use super::*; use sha2::{Digest, Sha256}; -use std::collections::BTreeSet; -use std::io::{Seek, SeekFrom}; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; mod art_manifest; mod claude_code_cli; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_events.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_events.rs index 429c97240..364188718 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_events.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_events.rs @@ -211,16 +211,6 @@ pub(crate) fn start_game_creator_manifest_invalidation_event_sink( Ok(GameCreatorManifestInvalidationEventSink { port, token }) } -#[cfg(test)] -pub(crate) fn configure_game_creator_manifest_invalidation_event_sink( - port: u16, - token: &str, -) -> Result<(), String> { - let sink = validate_game_creator_manifest_invalidation_event_sink(port, token)?; - register_game_creator_manifest_invalidation_event_sink(sink); - Ok(()) -} - pub(crate) fn validate_game_creator_manifest_invalidation_event_sink( port: u16, token: &str, @@ -266,10 +256,6 @@ pub(crate) struct GameCreatorManifestInvalidationEventSinkTestGuard { #[cfg(test)] impl GameCreatorManifestInvalidationEventSinkTestGuard { - pub(crate) fn configure(&self, port: u16, token: &str) -> Result<(), String> { - configure_game_creator_manifest_invalidation_event_sink(port, token) - } - pub(crate) fn configured_sink(&self) -> Option { lock_game_creator_manifest_invalidation_event_sinks() .first() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs index f55d64cee..2c03ef196 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs @@ -2,57 +2,24 @@ use super::*; mod canvas_generation; mod draft_validation; -mod draft_writer; mod external_generation_state; mod loop_orchestration; mod pass_artifacts; mod prompt_context; mod trace; +#[cfg(test)] +pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_test; #[cfg(test)] pub(crate) use canvas_generation::submit_external_generation_request; pub(in crate::agent) use canvas_generation::{ - admit_platform_art_generation_at, commit_prepared_platform_art_asset_at, - commit_prepared_platform_art_asset_strict_slices_at, generate_admitted_platform_art_asset_at, - generate_art_package_asset_with_runtime_options_at, - platform_art_generation_error_result_unknown, recover_persisted_visual_generation_options, - register_existing_platform_art_slices_at, request_platform_art_asset_with_runtime_options_at, + admit_platform_art_generation_at, generate_admitted_platform_art_asset_at, + generate_art_package_asset_with_runtime_options_at, register_existing_platform_art_slices_at, restore_platform_art_asset_bytes_at, retained_platform_art_generation_runtime_spritesheet_identity_at, retained_platform_art_generation_runtime_state_matches_direct_stage_at, validate_platform_art_png_bytes_with_limits, AdmittedPlatformArtGeneration, }; -pub(crate) use canvas_generation::{ - external_canvas_placeholder, external_editor_json_request, external_editor_response_data, - external_generation_poll_after_ms, platform_art_generation_error_needs_reconciliation, - prepare_external_canvas_generation_context, resolve_canvas_resource_download_with_access, - ExternalCanvasGenerationContext, -}; -pub(in crate::agent) use draft_validation::validate_closed_game_script_blocks; -pub(crate) use external_generation_state::{ - classify_platform_art_generation_service_identity, - platform_art_generation_external_service_fingerprint, - platform_art_generation_external_service_origin, PlatformArtGenerationServiceIdentityMatch, - PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME, -}; -pub(in crate::agent) use external_generation_state::{ - game_creator_agent_runtime_external_generation_exists, - platform_art_generation_runtime_context_from_pending, - platform_art_generation_runtime_recovery_at, remove_platform_art_generation_runtime_state_at, - PlatformArtGenerationRuntimeContext, PlatformArtGenerationRuntimeRecovery, - PLATFORM_ART_GENERATION_RUNTIME_SCHEMA_VERSION, -}; -#[cfg(test)] -pub(crate) use external_generation_state::{ - platform_art_generation_legacy_external_configuration_fingerprint, - write_platform_art_generation_runtime_accepted_for_test, - write_platform_art_generation_runtime_prepared_for_test, -}; -pub(in crate::agent) use loop_orchestration::build_game_creator_agent_runtime_llm_client; -pub(in crate::agent) use trace::game_creation_agent_group_id; - -#[cfg(test)] -pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_test; #[allow(unused_imports)] pub(crate) use canvas_generation::{ build_platform_art_asset_prompt, editor_api_key_is_configured, generate_platform_art_asset_at, @@ -64,15 +31,32 @@ pub(crate) use canvas_generation::{ project_canvas_asset_media_types, role_has_canvas_assets, validate_platform_art_icon_prompt, PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS, }; -#[allow(unused_imports)] -pub(crate) use draft_validation::{ - contains_any, evaluate_game_draft, extract_json_payload, fnv1a64, - parse_llm_game_draft_response, render_evaluator_findings, render_planner_spec, - strip_llm_thinking_blocks, validate_canvas_rendering_html, validate_game_html_smoke, - validate_llm_agent_handoffs, validate_llm_game_draft, validate_non_placeholder_game_html, - validate_playable_game_html, validate_safe_game_html_runtime, +pub(crate) use canvas_generation::{ + external_canvas_placeholder, external_editor_json_request, external_editor_response_data, + external_generation_poll_after_ms, platform_art_generation_error_needs_reconciliation, + prepare_external_canvas_generation_context, resolve_canvas_resource_download_with_access, + ExternalCanvasGenerationContext, +}; +pub(crate) use draft_validation::{ + contains_any, fnv1a64, strip_llm_thinking_blocks, validate_game_html_smoke, +}; +pub(crate) use external_generation_state::{ + classify_platform_art_generation_service_identity, + platform_art_generation_external_service_fingerprint, + platform_art_generation_external_service_origin, PlatformArtGenerationServiceIdentityMatch, + PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME, +}; +pub(in crate::agent) use external_generation_state::{ + game_creator_agent_runtime_external_generation_exists, + platform_art_generation_runtime_context_from_pending, + remove_platform_art_generation_runtime_state_at, PlatformArtGenerationRuntimeContext, +}; +#[cfg(test)] +pub(crate) use external_generation_state::{ + platform_art_generation_legacy_external_configuration_fingerprint, + write_platform_art_generation_runtime_accepted_for_test, + write_platform_art_generation_runtime_prepared_for_test, }; -pub(crate) use draft_writer::{ensure_legacy_json_generator_project, write_local_game_draft_at}; #[allow(unused_imports)] pub(crate) use loop_orchestration::{ emit_agent_progress, game_creator_agent_llm_error_is_mud_points_insufficient, @@ -80,14 +64,7 @@ pub(crate) use loop_orchestration::{ game_creator_runtime_error_is_mud_points_insufficient, request_game_creator_llm_text, AgentProgressEmitter, GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND, }; -#[allow(unused_imports)] -pub(crate) use pass_artifacts::{ - agent_role_memory_relative_path, agent_role_memory_relative_path_for_task, - append_agent_success_memories, append_group_brief_steps, read_previous_agent_role_brief, - render_agent_pass_agenda_markdown, render_agent_pass_task_graph_json, - render_carryover_role_brief, write_agent_group_brief, write_agent_pass_agenda, - write_agent_pass_artifacts, write_agent_pass_file, write_agent_role_brief, -}; +pub(crate) use pass_artifacts::agent_role_memory_relative_path_for_task; #[allow(unused_imports)] pub(crate) use prompt_context::{ append_prompt_context, asset_source_kind_label, game_creator_generator_user_prompt, @@ -97,18 +74,8 @@ pub(crate) use prompt_context::{ render_local_conversation_prompt_context_for_session, sanitize_error_context, sanitize_prompt_context, truncate_prompt_context, truncate_prompt_context_preserving_tail, }; -#[allow(unused_imports)] pub(crate) use trace::{ - agent_run_lifecycle_status, agent_run_stop_reason, agent_trace_step, agent_trace_step_owned, - append_agent_run_activity, append_agent_run_jsonl, append_agent_run_output, - append_agent_run_trace_step, append_local_artifact_write_step, append_preview_log, - append_preview_start_trace_step, append_preview_stop_trace_step, - append_static_smoke_manual_trace_step, append_static_smoke_step, - build_agent_run_task_graph_trace, collect_agent_pass_plan_traces, - game_creation_agent_group_from_id, infer_agent_trace_phase, parse_agent_task_id_list, - read_agent_agenda_snapshot, ready_task_ids_for_tasks, record_replaced_preview_stop, - role_brief_completes_task, set_task_status_if_current, should_replace_task_status, status_rank, - task_has_status, task_status_from_agent_step, with_task_context, - write_agent_run_context_bundle, write_agent_run_trace, AgentAgendaSnapshot, - AgentPassTaskGraphSnapshot, + agent_trace_step_owned, append_preview_log, append_preview_start_trace_step, + append_preview_stop_trace_step, append_static_smoke_manual_trace_step, + record_replaced_preview_stop, with_task_context, }; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_validation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_validation.rs index 4c2549ce1..9f178c15a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_validation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_validation.rs @@ -1,82 +1,7 @@ -use super::*; use oxc_allocator::Allocator as JavascriptAllocator; -use oxc_ast::ast::{ - RegExpLiteral as JavascriptRegExpLiteral, StringLiteral as JavascriptStringLiteral, - TemplateElement as JavascriptTemplateElement, -}; -use oxc_ast_visit::Visit as VisitJavascript; use oxc_parser::Parser as JavascriptParser; use oxc_semantic::SemanticBuilder as JavascriptSemanticBuilder; -use oxc_span::{GetSpan as JavascriptGetSpan, SourceType as JavascriptSourceType}; - -pub(crate) fn evaluate_game_draft(prompt: &str, draft: &LlmGameDraft) -> Vec { - let mut issues = Vec::new(); - if let Err(error) = validate_llm_game_draft(prompt, draft) { - issues.push(error); - } - let html = draft.game_html.to_ascii_lowercase(); - if !["keydown", "keyup", "pointer", "mousedown", "touch", "click"] - .iter() - .any(|needle| html.contains(needle)) - { - issues.push("gameHtml 缺少明确的键盘、鼠标或触摸输入监听".to_string()); - } - issues -} - -pub(crate) fn render_planner_spec(prompt: &str, spec: &str) -> String { - format!( - "# Planner Spec\n\n## 用户需求\n\n{}\n\n## 规格\n\n{}\n", - prompt.trim(), - spec.trim() - ) -} - -pub(crate) fn render_evaluator_findings(pass: u8, issues: &[impl AsRef]) -> String { - let issue_values = issues - .iter() - .map(|issue| issue.as_ref().trim()) - .filter(|issue| !issue.is_empty()) - .collect::>(); - let actionable_issues = issue_values - .iter() - .filter(|issue| !issue.contains("暂无上一轮问题")) - .map(|issue| (*issue).to_string()) - .collect::>(); - let mut output = format!( - "# Evaluator Findings\n\n- pass: {pass}\n- status: {}\n\n", - if issue_values.is_empty() { - "passed" - } else { - "needs-revision" - } - ); - if issue_values.is_empty() { - output - .push_str("## 结果\n\n- 本地静态验收通过:HTML 自包含、包含 canvas、主循环和输入。\n"); - } else { - output.push_str("## 问题\n\n"); - for issue in &issue_values { - output.push_str("- "); - output.push_str(issue); - output.push('\n'); - } - } - output.push_str("\n## Repair Routes\n\n```json\n"); - if actionable_issues.is_empty() { - output.push_str("[]"); - } else { - let routes = build_game_creation_seed_task_graph("AI 游戏创作") - .map(|graph| route_game_creation_repair_issues(&graph, &actionable_issues)) - .unwrap_or_default(); - match serde_json::to_string_pretty(&routes) { - Ok(payload) => output.push_str(&payload), - Err(_) => output.push_str("[]"), - } - } - output.push_str("\n```\n"); - output -} +use oxc_span::SourceType as JavascriptSourceType; pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 { let mut hash = 0xcbf29ce484222325_u64; @@ -87,14 +12,6 @@ pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 { hash } -pub(crate) fn parse_llm_game_draft_response(content: &str) -> Result { - let content = strip_llm_thinking_blocks(content); - let payload = extract_json_payload(content.as_str()) - .ok_or_else(|| "LLM 返回不是 JSON 对象".to_string())?; - serde_json::from_str::(payload) - .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) -} - pub(crate) fn strip_llm_thinking_blocks(content: &str) -> String { let mut output = String::new(); let mut rest = content; @@ -113,91 +30,6 @@ pub(crate) fn strip_llm_thinking_blocks(content: &str) -> String { output.trim().to_string() } -pub(crate) fn extract_json_payload(content: &str) -> Option<&str> { - let trimmed = content.trim(); - let without_fence = trimmed - .strip_prefix("```json") - .or_else(|| trimmed.strip_prefix("```")) - .and_then(|value| value.strip_suffix("```")) - .map(str::trim) - .unwrap_or(trimmed); - let start = without_fence.find('{')?; - let mut depth = 0usize; - let mut inside_string = false; - let mut escaped = false; - for (offset, character) in without_fence[start..].char_indices() { - if inside_string { - if escaped { - escaped = false; - } else if character == '\\' { - escaped = true; - } else if character == '"' { - inside_string = false; - } - continue; - } - match character { - '"' => inside_string = true, - '{' => depth += 1, - '}' => { - depth = depth.checked_sub(1)?; - if depth == 0 { - let end = start + offset + character.len_utf8(); - return Some(&without_fence[start..end]); - } - } - _ => {} - } - } - None -} - -pub(crate) fn validate_llm_game_draft(prompt: &str, draft: &LlmGameDraft) -> Result<(), String> { - if draft.title.trim().is_empty() { - return Err("LLM 草案缺少标题".to_string()); - } - if draft.design_markdown.trim().is_empty() { - return Err("LLM 草案缺少设计说明".to_string()); - } - if !draft.balance.is_object() { - return Err("LLM 草案 balance 必须是 JSON object".to_string()); - } - if !draft.art_manifest.is_object() { - return Err("LLM 草案 artManifest 必须是 JSON object".to_string()); - } - if !draft.audio_manifest.is_object() { - return Err("LLM 草案 audioManifest 必须是 JSON object".to_string()); - } - if draft.publish_readme.trim().is_empty() { - return Err("LLM 草案缺少发布说明".to_string()); - } - if draft.handoff_summary.trim().is_empty() { - return Err("LLM 草案缺少多智能体交接摘要".to_string()); - } - validate_llm_agent_handoffs(draft)?; - - let html = draft.game_html.trim(); - let lower_html = html.to_ascii_lowercase(); - if !lower_html.contains("')) && html.contains(prompt) { - return Err("LLM 草案 gameHtml 包含未转义的用户输入".to_string()); - } - validate_playable_game_html(html, "LLM 草案 gameHtml")?; - validate_non_placeholder_game_html(html, "LLM 草案 gameHtml")?; - - Ok(()) -} - pub(crate) fn validate_playable_game_html(html: &str, label: &str) -> Result<(), String> { let lower_html = html.to_ascii_lowercase(); if !contains_any( @@ -381,28 +213,6 @@ pub(crate) fn contains_any(haystack: &str, needles: &[&str]) -> bool { needles.iter().any(|needle| haystack.contains(needle)) } -pub(crate) fn validate_llm_agent_handoffs(draft: &LlmGameDraft) -> Result<(), String> { - const REQUIRED_GROUPS: [&str; 6] = ["design", "balance", "art", "audio", "code", "publishing"]; - for required_group in REQUIRED_GROUPS { - let handoff = draft - .handoffs - .iter() - .find(|handoff| handoff.group.trim() == required_group) - .ok_or_else(|| format!("LLM 草案 handoffs 缺少 {required_group} 专业组"))?; - if handoff.role.trim().is_empty() - || handoff.summary.trim().is_empty() - || handoff.next.trim().is_empty() - || handoff - .outputs - .iter() - .all(|output| output.trim().is_empty()) - { - return Err(format!("LLM 草案 handoffs.{required_group} 交接内容不完整")); - } - } - Ok(()) -} - fn is_html_ascii_whitespace(byte: u8) -> bool { matches!(byte, b'\t' | b'\n' | b'\x0c' | b'\r' | b' ') } @@ -540,82 +350,6 @@ fn validate_executable_inline_javascript_syntax(html: &str) -> Result<(), String Ok(()) } -fn html_tag_like_start(content: &str, tag_start: usize) -> bool { - content - .as_bytes() - .get(tag_start + 1) - .is_some_and(|byte| byte.is_ascii_alphabetic() || matches!(byte, b'/' | b'!' | b'?')) -} - -#[derive(Default)] -struct JavascriptLiteralSpanCollector { - strings: Vec>, - regular_expressions: Vec>, - template_elements: Vec>, -} - -fn javascript_span_range(span: oxc_span::Span) -> std::ops::Range { - span.start as usize..span.end as usize -} - -impl<'a> VisitJavascript<'a> for JavascriptLiteralSpanCollector { - fn visit_string_literal(&mut self, literal: &JavascriptStringLiteral<'a>) { - self.strings.push(javascript_span_range(literal.span)); - } - - fn visit_reg_exp_literal(&mut self, literal: &JavascriptRegExpLiteral<'a>) { - self.regular_expressions - .push(javascript_span_range(literal.span)); - } - - fn visit_template_element(&mut self, element: &JavascriptTemplateElement<'a>) { - self.template_elements - .push(javascript_span_range(element.span)); - } -} - -fn javascript_without_string_literals_or_comments(content: &str) -> String { - let allocator = JavascriptAllocator::default(); - let parsed = JavascriptParser::new( - &allocator, - content, - JavascriptSourceType::default().with_unambiguous(true), - ) - .parse(); - if parsed.panicked || !parsed.diagnostics.is_empty() { - return " ".repeat(content.len()); - } - let mut collector = JavascriptLiteralSpanCollector::default(); - collector.visit_program(&parsed.program); - let mut output = content.as_bytes().to_vec(); - for range in collector.template_elements { - output[range].fill(b' '); - } - for range in collector.strings { - if range.end.saturating_sub(range.start) > 2 { - output[range.start + 1..range.end - 1].fill(b' '); - } - } - for range in collector.regular_expressions { - let raw = &content[range.clone()]; - if let Some(close) = raw[1..].rfind('/') { - let close = range.start + 1 + close; - output[range.start + 1..close].fill(b' '); - if close > range.start + 1 { - output[range.start + 1] = b'a'; - } - } - } - for comment in &parsed.program.comments { - for byte in &mut output[javascript_span_range(comment.span())] { - if !matches!(*byte, b'\r' | b'\n') { - *byte = b' '; - } - } - } - String::from_utf8(output).expect("masking parsed JavaScript literals preserves UTF-8") -} - fn html_script_type_is_executable(script_tag: &str) -> bool { if let Some(script_type) = html_attribute_value(script_tag, "type") { let script_type = script_type.trim(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_writer.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_writer.rs deleted file mode 100644 index ac67a164a..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/draft_writer.rs +++ /dev/null @@ -1,171 +0,0 @@ -use super::*; - -pub(crate) fn write_local_game_draft_at( - root: &Path, - prompt: &str, - draft: &LlmGameDraft, -) -> Result { - let prompt = prompt.trim(); - if prompt.is_empty() { - return Err("创作想法不能为空".to_string()); - } - ensure_legacy_json_generator_project(root)?; - validate_llm_game_draft(prompt, draft)?; - let checkpoint = create_local_project_checkpoint_at(root)?; - let timestamp = unix_timestamp(); - let title = draft.title.trim(); - let handoff_summary = draft.handoff_summary.trim(); - let design_path = root.join("game/game_design.md"); - let balance_path = root.join("game/balance.json"); - let art_manifest_path = root.join("assets/manifest.art.json"); - let audio_manifest_path = root.join("assets/manifest.audio.json"); - let publish_readme_path = root.join("exports/README.md"); - let agent_log_path = root.join(".agent/logs/agent.log"); - let short_memory_path = root.join("memory/session.md"); - let long_memory_path = root.join("memory/project.md"); - let game_index_path = root.join("game/index.html"); - - append_markdown_entry( - &short_memory_path, - "# 短期记忆\n\n", - &format!("- {timestamp}: {prompt}\n"), - "写入短期记忆失败", - )?; - append_markdown_entry( - &long_memory_path, - "# 项目长期记忆\n\n## 当前约束\n\n- Web 小游戏原型\n- 本地 HTTP 预览\n\n## 创作目标记录\n\n", - &format!("- {timestamp}: {prompt}\n"), - "写入长期记忆失败", - )?; - fs::write( - &design_path, - format!( - "# 游戏设计草案\n\n## 原始想法\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n## LLM 生成草案\n\n{}\n", - draft.design_markdown.trim() - ), - ) - .map_err(|error| format!("写入游戏设计失败:{}: {error}", design_path.display()))?; - fs::write( - &balance_path, - serde_json::to_string_pretty(&draft.balance) - .map_err(|error| format!("生成数值配置失败:{error}"))?, - ) - .map_err(|error| format!("写入数值配置失败:{}: {error}", balance_path.display()))?; - fs::write( - &art_manifest_path, - serde_json::to_string_pretty(&draft.art_manifest) - .map_err(|error| format!("生成美术清单失败:{error}"))?, - ) - .map_err(|error| format!("写入美术清单失败:{}: {error}", art_manifest_path.display()))?; - fs::write( - &audio_manifest_path, - serde_json::to_string_pretty(&draft.audio_manifest) - .map_err(|error| format!("生成音乐音效清单失败:{error}"))?, - ) - .map_err(|error| { - format!( - "写入音乐音效清单失败:{}: {error}", - audio_manifest_path.display() - ) - })?; - fs::write( - &publish_readme_path, - format!( - "# 发布包装草案\n\n## 标题\n\n{title}\n\n## 简介\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n{}\n", - draft.publish_readme.trim() - ), - ) - .map_err(|error| { - format!( - "写入发布包装草案失败:{}: {error}", - publish_readme_path.display() - ) - })?; - - fs::write(&game_index_path, draft.game_html.trim()) - .map_err(|error| format!("写入游戏入口失败:{}: {error}", game_index_path.display()))?; - fs::OpenOptions::new() - .create(true) - .append(true) - .open(&agent_log_path) - .and_then(|mut file| { - file.write_all( - format!("{timestamp} game.generate_draft llm\n{handoff_summary}\n").as_bytes(), - ) - }) - .map_err(|error| format!("写入 Agent 日志失败:{}: {error}", agent_log_path.display()))?; - append_agent_db_record( - root, - serde_json::json!({ - "recordType": "game.generate_draft", - "goal": prompt, - "title": title, - "paths": [ - "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", - ], - }), - )?; - let manifest = record_draft_task_progress(root, prompt, timestamp, &agent_log_path)?; - let diff = diff_local_project_checkpoint_at(root, &checkpoint.checkpoint_id)?; - append_agent_db_record( - root, - serde_json::json!({ - "recordType": "project.diff", - "checkpointId": checkpoint.checkpoint_id, - "added": diff.added.len(), - "changed": diff.changed.len(), - "deleted": diff.deleted.len(), - }), - )?; - - Ok(GenerateLocalGameDraftResult { - project_path: root.to_string_lossy().into_owned(), - game_index_path: game_index_path.to_string_lossy().into_owned(), - design_path: design_path.to_string_lossy().into_owned(), - short_memory_path: short_memory_path.to_string_lossy().into_owned(), - long_memory_path: long_memory_path.to_string_lossy().into_owned(), - manifest, - }) -} - -pub(crate) fn ensure_legacy_json_generator_project(root: &Path) -> Result<(), String> { - if root.join("game/package.json").exists() - || root.join("package.json").exists() - || !root.join("game/index.html").is_file() - || !root.join(".agent/manifest.json").is_file() - { - return Err("JSON Generator 仅支持已有的单文件 HTML 项目;新建游戏与 npm / Phaser 4 项目请使用 DirectProject 完成依赖安装、构建和试玩".to_string()); - } - Ok(()) -} - -#[cfg(test)] -mod legacy_generator_tests { - use super::*; - - #[test] - fn legacy_generator_rejects_npm_before_writing() { - let root = tempfile::tempdir_in(std::env::temp_dir().canonicalize().unwrap()).unwrap(); - fs::create_dir_all(root.path().join("game")).unwrap(); - fs::create_dir_all(root.path().join(".agent")).unwrap(); - fs::write(root.path().join("game/index.html"), "legacy source").unwrap(); - fs::write(root.path().join(".agent/manifest.json"), "{}").unwrap(); - assert!(ensure_legacy_json_generator_project(root.path()).is_ok()); - fs::write(root.path().join("game/package.json"), "{}").unwrap(); - assert!(ensure_legacy_json_generator_project(root.path()) - .unwrap_err() - .contains("DirectProject")); - assert_eq!( - fs::read_to_string(root.path().join("game/index.html")).unwrap(), - "legacy source" - ); - assert!(!root.path().join(".agent/spec.md").exists()); - } -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs index 6710b1bba..a055c06cd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs @@ -1,326 +1,5 @@ use super::*; -pub(crate) fn write_agent_pass_agenda( - root: &Path, - pass: u8, - findings_markdown: &str, -) -> Result { - let graph = build_game_creation_seed_task_graph("AI 游戏创作") - .map_err(|error| format!("构建 Agent 编排任务图失败:{error}"))?; - let pass_plan = plan_game_creation_agent_pass(&graph, pass, findings_markdown) - .map_err(|error| format!("规划 Agent 编排任务图失败:{error}"))?; - let repair_routes = pass_plan - .repair_routes - .iter() - .map(|route| GameCreationAgentRepairRouteTrace { - issue: route.issue.clone(), - task_ids: route.task_ids.clone(), - reason: route.reason.clone(), - }) - .collect::>(); - let relative_path = format!(".agent/passes/pass-{pass}/agenda.md"); - let markdown = render_agent_pass_agenda_markdown( - pass, - &pass_plan.mode, - &pass_plan.active_task_ids, - &pass_plan.carried_task_ids, - &pass_plan.dependency_waves, - &pass_plan.repair_focus, - &repair_routes, - ); - write_agent_pass_file(root, &relative_path, &markdown)?; - let task_graph_relative_path = format!(".agent/passes/pass-{pass}/task-graph.json"); - write_agent_pass_file( - root, - &task_graph_relative_path, - &render_agent_pass_task_graph_json( - pass, - &pass_plan.mode, - &pass_plan.summary, - &pass_plan.active_task_ids, - &pass_plan.carried_task_ids, - &pass_plan.dependency_waves, - &pass_plan.repair_focus, - &repair_routes, - )?, - )?; - Ok(AgentPassAgenda { - relative_path, - task_graph_relative_path, - active_task_ids: pass_plan.active_task_ids, - carried_task_ids: pass_plan.carried_task_ids, - dependency_waves: pass_plan.dependency_waves, - repair_focus: pass_plan.repair_focus, - repair_routes, - summary: pass_plan.summary, - }) -} - -pub(crate) fn render_agent_pass_agenda_markdown( - pass: u8, - mode: &str, - active_task_ids: &[String], - carried_task_ids: &[String], - dependency_waves: &[Vec], - issues: &[String], - repair_routes: &[GameCreationAgentRepairRouteTrace], -) -> String { - let mut output = format!( - "# Orchestrator Agenda\n\n- pass: {pass}\n- mode: {}\n- activeTasks: {}\n- carriedTasks: {}\n\n", - mode, - active_task_ids.join(", "), - if carried_task_ids.is_empty() { - "none".to_string() - } else { - carried_task_ids.join(", ") - } - ); - output.push_str("## Repair Focus\n\n"); - if issues.is_empty() { - output.push_str("- 首轮生成,所有组内角色参与。\n"); - } else { - for issue in issues { - output.push_str("- "); - output.push_str(issue); - output.push('\n'); - } - } - output.push_str("\n## Dependency Waves\n\n"); - for (index, wave) in dependency_waves.iter().enumerate() { - output.push_str(&format!( - "- wave {}: {}\n", - index + 1, - if wave.is_empty() { - "none".to_string() - } else { - wave.join(", ") - } - )); - } - output.push_str("\n## Repair Routes\n\n"); - if repair_routes.is_empty() { - output.push_str("- none\n"); - } else { - for route in repair_routes { - output.push_str(&format!( - "- issue: {}\n taskIds: {}\n reason: {}\n", - route.issue, - route.task_ids.join(", "), - route.reason - )); - } - } - output.push_str(prompt_text!("goalContext.agenda_rules")); - output -} - -pub(crate) fn render_agent_pass_task_graph_json( - pass: u8, - mode: &str, - summary: &str, - active_task_ids: &[String], - carried_task_ids: &[String], - dependency_waves: &[Vec], - issues: &[String], - repair_routes: &[GameCreationAgentRepairRouteTrace], -) -> Result { - serde_json::to_string_pretty(&serde_json::json!({ - "schemaVersion": "game-creator-agent-pass-task-graph.v1", - "pass": pass, - "mode": mode, - "summary": summary, - "activeTaskIds": active_task_ids, - "carriedTaskIds": carried_task_ids, - "repairFocus": issues, - "repairRoutes": repair_routes, - "dependencyWaves": dependency_waves, - })) - .map(|payload| format!("{payload}\n")) - .map_err(|error| format!("生成 Agent pass task graph 失败:{error}")) -} - -pub(crate) fn read_previous_agent_role_brief( - root: &Path, - pass: u8, - group_definition: AgentGroupDefinition, - role_definition: AgentRoleDefinition, -) -> Result, String> { - if pass <= 1 { - return Ok(None); - } - let relative_path = format!( - ".agent/passes/pass-{}/groups/{}/{}", - pass - 1, - group_definition.id, - role_definition.brief_path_name - ); - match fs::read_to_string(root.join(&relative_path)) { - Ok(content) => Ok(Some((relative_path, content))), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(error) => Err(format!( - "读取上一轮角色 brief 失败:{relative_path}: {error}" - )), - } -} - -pub(crate) fn render_carryover_role_brief( - group_definition: AgentGroupDefinition, - role_definition: AgentRoleDefinition, - source_path: &str, - source_markdown: &str, -) -> String { - format!( - "本角色判断:沿用上一轮 {} / {} 输出。\n交付物:{}\n下游约束:本轮未命中该任务,Generator 只在必要时读取此约束。\n验收风险:如果 Evaluator 后续命中该组,下一轮必须重新激活。\n\n## 上一轮 brief\n\n{}", - group_definition.label, - role_definition.role, - source_path, - source_markdown.trim() - ) -} - -pub(crate) fn write_agent_pass_artifacts( - root: &Path, - pass: u8, - draft: &LlmGameDraft, -) -> Result { - let relative_dir = format!(".agent/passes/pass-{pass}"); - let pass_dir = root.join(&relative_dir); - fs::create_dir_all(&pass_dir) - .map_err(|error| format!("创建 Agent pass 目录失败:{}: {error}", pass_dir.display()))?; - - let paths = AgentPassArtifactPaths { - draft_json: format!("{relative_dir}/draft.json"), - design_markdown: format!("{relative_dir}/design.md"), - balance_json: format!("{relative_dir}/balance.json"), - art_manifest_json: format!("{relative_dir}/manifest.art.json"), - audio_manifest_json: format!("{relative_dir}/manifest.audio.json"), - publish_readme: format!("{relative_dir}/README.md"), - game_html: format!("{relative_dir}/game.html"), - handoff_markdown: format!("{relative_dir}/handoff.md"), - }; - - write_agent_pass_file( - root, - &paths.draft_json, - &format!( - "{}\n", - serde_json::to_string_pretty(draft) - .map_err(|error| format!("序列化 Agent pass draft 失败:{error}"))? - ), - )?; - write_agent_pass_file( - root, - &paths.design_markdown, - &format!("# 策划组 / Gameplay\n\n{}\n", draft.design_markdown.trim()), - )?; - write_agent_pass_file( - root, - &paths.balance_json, - &format!( - "{}\n", - serde_json::to_string_pretty(&draft.balance) - .map_err(|error| format!("序列化 Agent pass 数值失败:{error}"))? - ), - )?; - write_agent_pass_file( - root, - &paths.art_manifest_json, - &format!( - "{}\n", - serde_json::to_string_pretty(&draft.art_manifest) - .map_err(|error| format!("序列化 Agent pass 美术清单失败:{error}"))? - ), - )?; - write_agent_pass_file( - root, - &paths.audio_manifest_json, - &format!( - "{}\n", - serde_json::to_string_pretty(&draft.audio_manifest) - .map_err(|error| format!("序列化 Agent pass 音乐清单失败:{error}"))? - ), - )?; - write_agent_pass_file( - root, - &paths.publish_readme, - &format!("# 运营组 / Publish\n\n{}\n", draft.publish_readme.trim()), - )?; - write_agent_pass_file(root, &paths.game_html, &draft.game_html)?; - write_agent_pass_file( - root, - &paths.handoff_markdown, - &render_agent_pass_handoff(pass, draft), - )?; - - Ok(paths) -} - -pub(crate) fn write_agent_pass_file( - root: &Path, - relative_path: &str, - content: &str, -) -> Result<(), String> { - let path = root.join(relative_path); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|error| { - format!( - "创建 Agent pass 文件目录失败:{}: {error}", - parent.display() - ) - })?; - } - fs::write(&path, content) - .map_err(|error| format!("写入 Agent pass 文件失败:{}: {error}", path.display())) -} - -pub(crate) fn write_agent_group_brief( - root: &Path, - pass: u8, - definition: AgentGroupDefinition, - markdown: &str, -) -> Result { - let relative_path = format!( - ".agent/passes/pass-{pass}/groups/{}", - definition.brief_path_name - ); - write_agent_pass_file( - root, - &relative_path, - &format!( - "# {} / {}\n\n{}\n", - definition.label, - definition.role, - markdown.trim() - ), - )?; - Ok(relative_path) -} - -pub(crate) fn write_agent_role_brief( - root: &Path, - pass: u8, - group_definition: AgentGroupDefinition, - role_definition: AgentRoleDefinition, - markdown: &str, -) -> Result { - let relative_path = format!( - ".agent/passes/pass-{pass}/groups/{}/{}", - group_definition.id, role_definition.brief_path_name - ); - write_agent_pass_file( - root, - &relative_path, - &format!( - "# {} / {}\n\n- task: {}\n\n{}\n", - group_definition.label, - role_definition.role, - role_definition.task_id, - markdown.trim() - ), - )?; - Ok(relative_path) -} - pub(crate) fn agent_role_memory_relative_path( group_definition: AgentGroupDefinition, role_definition: AgentRoleDefinition, @@ -344,156 +23,3 @@ pub(crate) fn agent_role_memory_relative_path_for_task(task_id: &str) -> Result< } Err(format!("未知 Agent 任务:{task_id}")) } - -pub(crate) fn append_agent_success_memories( - root: &Path, - pass: u8, - draft: &LlmGameDraft, - briefs: &[AgentGroupBrief], -) -> Result<(), String> { - let timestamp = unix_timestamp(); - let title = draft.title.trim(); - let mut blackboard_entry = format!( - "\n## pass {pass} - {title}\n\n- 时间:{timestamp}\n- 稳定原型:game/index.html\n- 设计:game/game_design.md\n- 数值:game/balance.json\n- 美术:assets/manifest.art.json\n- 音乐音效:assets/manifest.audio.json\n- 发布包装:exports/README.md\n\n## 角色共享摘要\n\n" - ); - - for brief in briefs { - for role_brief in &brief.role_briefs { - blackboard_entry.push_str(&format!( - "- {} / {}:{};status={};brief={}\n", - role_brief.group_definition.label, - role_brief.role_definition.role, - role_brief.summary, - role_brief.status, - role_brief.relative_path - )); - let private_entry = format!( - "\n## pass {pass} - {title}\n\n- 时间:{timestamp}\n- task:{}\n- status:{}\n- brief:{}\n- 摘要:{}\n", - role_brief.role_definition.task_id, - role_brief.status, - role_brief.relative_path, - role_brief.summary - ); - append_markdown_entry( - &root.join(&role_brief.memory_relative_path), - &format!( - "# Agent 私有记忆 - {} / {}\n\n", - role_brief.group_definition.label, role_brief.role_definition.role - ), - &private_entry, - "写入 Agent 私有记忆失败", - )?; - } - } - - append_markdown_entry( - &root.join(PROJECT_BLACKBOARD_MEMORY_PATH), - "# 项目黑板\n\n", - &blackboard_entry, - "写入项目黑板失败", - ) -} - -pub(crate) fn append_group_brief_steps( - pass: u8, - agenda_relative_path: &str, - briefs: &[AgentGroupBrief], - steps: &mut Vec, -) { - for brief in briefs { - for role_brief in &brief.role_briefs { - let input_paths = vec![ - "memory/session.md".to_string(), - "memory/project.md".to_string(), - PROJECT_BLACKBOARD_MEMORY_PATH.to_string(), - ".agent/conversations/project.jsonl".to_string(), - ".agent/conversations/agents/".to_string(), - role_brief.memory_relative_path.clone(), - ".agent/manifest.json".to_string(), - ".agent/spec.md".to_string(), - ".agent/findings.md".to_string(), - agenda_relative_path.to_string(), - ]; - let mut output_paths = vec![role_brief.relative_path.clone()]; - if role_brief.status == "completed" || role_brief.status == "carried-over" { - output_paths.push(role_brief.memory_relative_path.clone()); - output_paths.push(PROJECT_BLACKBOARD_MEMORY_PATH.to_string()); - } - let step = with_task_context( - agent_trace_step_owned( - pass, - &format!( - "{} / {}", - role_brief.group_definition.label, role_brief.role_definition.role - ), - &role_brief.status, - input_paths.clone(), - output_paths, - &role_brief.summary, - &role_brief.tool_id, - ), - role_brief.group_definition.id, - role_brief.role_definition.role, - Some(role_brief.role_definition.task_id), - "role-brief", - ); - steps.push(step); - } - let role_paths = brief - .role_briefs - .iter() - .map(|role_brief| role_brief.relative_path.clone()) - .collect::>(); - steps.push(with_task_context( - agent_trace_step_owned( - pass, - &format!("{} / GroupCoordinator", brief.definition.label), - "completed", - role_paths, - vec![brief.relative_path.clone()], - "汇总组内角色 brief,交给 Generator", - &format!("agent.group.aggregate.{}", brief.definition.id), - ), - brief.definition.id, - "GroupCoordinator", - None, - "group-aggregate", - )); - } -} - -pub(crate) fn handoff_group_label(group: &str) -> &'static str { - match group.trim() { - "design" => "策划组", - "balance" => "数值组", - "art" => "美术组", - "audio" => "音乐组", - "code" => "程序组", - "publishing" => "运营组", - _ => "专业组", - } -} - -pub(crate) fn render_agent_pass_handoff(pass: u8, draft: &LlmGameDraft) -> String { - let mut output = format!("# Agent Handoff\n\n- pass: {pass}\n\n"); - for handoff in &draft.handoffs { - output.push_str(&format!( - "## {} / {}\n\n{}\n\n- outputs: {}\n- next: {}\n\n", - handoff_group_label(&handoff.group), - handoff.role.trim(), - handoff.summary.trim(), - handoff - .outputs - .iter() - .map(|output| output.trim()) - .filter(|output| !output.is_empty()) - .collect::>() - .join(", "), - handoff.next.trim() - )); - } - output.push_str("## 总结\n\n"); - output.push_str(draft.handoff_summary.trim()); - output.push('\n'); - output -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/trace.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/trace.rs index 46c41064a..fe70b27ca 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/trace.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/trace.rs @@ -110,81 +110,6 @@ pub(crate) fn game_creation_agent_group_from_id( } } -pub(in crate::agent) fn game_creation_agent_group_id( - group: &GameCreationAppAgentGroup, -) -> &'static str { - match group { - GameCreationAppAgentGroup::Design => "design", - GameCreationAppAgentGroup::Balance => "balance", - GameCreationAppAgentGroup::Art => "art", - GameCreationAppAgentGroup::Audio => "audio", - GameCreationAppAgentGroup::Code => "code", - GameCreationAppAgentGroup::Publishing => "publishing", - } -} - -pub(crate) fn append_static_smoke_step( - root: &Path, - prompt: &str, - loop_result: &mut GameCreatorAgentLoopResult, -) -> Result<(), String> { - match run_limited_local_command_at(root, "game.static_smoke") { - Ok(smoke) => { - loop_result.steps.push(with_task_context( - agent_trace_step( - loop_result.passes, - "Playtest", - "completed", - &["game/index.html"], - &[".agent/logs/command.log", ".agent/manifest.json"], - &smoke.output, - "game.static_smoke", - ), - "code", - "Preview", - Some("preview-readiness"), - "playtest", - )); - write_agent_run_trace( - root, - &loop_result.run_id, - prompt, - "passed", - loop_result.passes, - &loop_result.steps, - None, - ) - } - Err(error) => { - loop_result.steps.push(with_task_context( - agent_trace_step( - loop_result.passes, - "Playtest", - "failed", - &["game/index.html"], - &[".agent/logs/command.log"], - &error, - "game.static_smoke", - ), - "code", - "Preview", - Some("preview-readiness"), - "playtest", - )); - write_agent_run_trace( - root, - &loop_result.run_id, - prompt, - "failed", - loop_result.passes, - &loop_result.steps, - Some(&error), - )?; - Err(format!("生成后自检失败:{error}")) - } - } -} - pub(crate) fn append_static_smoke_manual_trace_step( root: &Path, result: &LimitedLocalCommandResult, @@ -212,46 +137,6 @@ pub(crate) fn append_static_smoke_manual_trace_step( ) } -pub(crate) fn append_local_artifact_write_step( - root: &Path, - prompt: &str, - loop_result: &mut GameCreatorAgentLoopResult, -) -> Result<(), String> { - loop_result.steps.push(agent_trace_step_owned( - loop_result.passes, - "ArtifactWriter", - "completed", - vec![ - format!(".agent/passes/pass-{}/draft.json", loop_result.passes), - format!(".agent/passes/pass-{}/handoff.md", loop_result.passes), - ], - vec![ - "memory/session.md".to_string(), - "memory/project.md".to_string(), - PROJECT_BLACKBOARD_MEMORY_PATH.to_string(), - "memory/agents/".to_string(), - "game/game_design.md".to_string(), - "game/balance.json".to_string(), - "assets/manifest.art.json".to_string(), - "assets/manifest.audio.json".to_string(), - "exports/README.md".to_string(), - "game/index.html".to_string(), - ".agent/manifest.json".to_string(), - ], - "把通过 Evaluator 的草案写入本地项目产物", - "file.write.local_artifacts", - )); - write_agent_run_trace( - root, - &loop_result.run_id, - prompt, - "artifacts-written", - loop_result.passes, - &loop_result.steps, - None, - ) -} - pub(crate) fn append_preview_start_trace_step( root: &Path, preview: &LocalPreviewResult, @@ -670,46 +555,6 @@ pub(crate) fn ready_task_ids_for_tasks(tasks: &[GameCreationAppTaskState]) -> Ve .collect() } -pub(crate) fn write_agent_run_trace( - root: &Path, - run_id: &str, - prompt: &str, - status: &str, - passes: u8, - steps: &[GameCreationAgentRunStep], - error: Option<&str>, -) -> Result<(), String> { - let tool_call_count = count_agent_tool_calls(steps)?; - let trace = GameCreationAgentRunTrace { - schema_version: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION.to_string(), - run_id: run_id.to_string(), - command_id: "game.generate_draft".to_string(), - status: status.to_string(), - lifecycle_status: Some(agent_run_lifecycle_status(status).to_string()), - passes, - max_passes: GAME_CREATOR_AGENT_LOOP_MAX_PASSES, - tool_call_count, - max_tool_calls: GAME_CREATOR_AGENT_TOOL_CALL_MAX, - stop_reason: agent_run_stop_reason(status, error).to_string(), - goal: prompt.trim().to_string(), - coordination: "filesystem".to_string(), - steps: steps.to_vec(), - artifacts: collect_agent_run_artifacts(root)?, - task_graph: build_agent_run_task_graph_trace(root, prompt, passes, steps)?, - pass_plans: collect_agent_pass_plan_traces(root, passes)?, - next_step: match status { - "passed" => "preview-playtest", - "artifacts-written" => "game.static_smoke", - "failed" => "inspect-error", - _ => "generator-revision", - } - .to_string(), - error: error.map(str::to_string), - updated_at: unix_timestamp(), - }; - write_agent_run_trace_payload(root, &trace) -} - pub(crate) fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'static str { match status { "running" => "loop-running", @@ -726,113 +571,6 @@ pub(crate) fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'stat } } -pub(crate) fn agent_run_lifecycle_status(status: &str) -> &'static str { - match status { - "running" | "needs-revision" | "artifacts-written" | "preview-running" => "running", - "waiting" | "preview-stopped" => "waiting", - "pending" => "pending", - "killed" => "killed", - "failed" => "failed", - "passed" => "done", - _ => "scheduled", - } -} - -pub(crate) fn append_agent_run_activity( - root: &Path, - run_id: &str, - event: &str, - message: &str, -) -> Result<(), String> { - append_agent_run_jsonl( - root, - ".agent/activity.jsonl", - &serde_json::json!({ - "timestamp": unix_timestamp(), - "runId": run_id, - "event": event, - "message": message, - }), - ) -} - -pub(crate) fn append_agent_run_output( - root: &Path, - run_id: &str, - event: &str, - message: &str, -) -> Result<(), String> { - append_agent_run_jsonl( - root, - ".agent/output.jsonl", - &serde_json::json!({ - "timestamp": unix_timestamp(), - "runId": run_id, - "event": event, - "content": message, - }), - ) -} - -pub(crate) fn append_agent_run_jsonl( - root: &Path, - relative_path: &str, - value: &serde_json::Value, -) -> Result<(), String> { - let path = root.join(relative_path); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|error| format!("创建 Agent 事件目录失败:{}: {error}", parent.display()))?; - } - let line = - serde_json::to_string(value).map_err(|error| format!("序列化 Agent 事件失败:{error}"))?; - append_jsonl_line(&path, &line, "Agent 事件") -} - -pub(crate) fn write_agent_run_context_bundle( - root: &Path, - trace: &GameCreationAgentRunTrace, -) -> Result<(), String> { - let bundle_path = root.join(".agent/context.bundle.json"); - if let Some(parent) = bundle_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - format!( - "创建 Agent context bundle 目录失败:{}: {error}", - parent.display() - ) - })?; - } - let manifest = read_manifest_for_project(root).ok(); - let payload = serde_json::json!({ - "schemaVersion": "game-creator-context-bundle.v1", - "runId": trace.run_id, - "commandId": trace.command_id, - "goal": trace.goal, - "lifecycleStatus": trace.lifecycle_status.as_deref().unwrap_or_else(|| agent_run_lifecycle_status(&trace.status)), - "status": trace.status, - "nextStep": trace.next_step, - "memory": { - "short": "memory/session.md", - "long": "memory/project.md", - "blackboard": PROJECT_BLACKBOARD_MEMORY_PATH, - "agents": "memory/agents/" - }, - "manifest": manifest, - "trace": ".agent/run.latest.json", - "activity": ".agent/activity.jsonl", - "output": ".agent/output.jsonl", - "updatedAt": unix_timestamp() - }); - let content = serde_json::to_string_pretty(&payload) - .map_err(|error| format!("生成 Agent context bundle 失败:{error}"))?; - fs::write(&bundle_path, content).map_err(|error| { - format!( - "写入 Agent context bundle 失败:{}: {error}", - bundle_path.display() - ) - }) -} - pub(crate) fn count_agent_tool_calls(steps: &[GameCreationAgentRunStep]) -> Result { let count = steps.iter().try_fold(0u16, |current, step| { let step_count = u16::try_from(step.tool_calls.len()) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/redaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/redaction.rs index 31a931eaa..2c7771bc2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/redaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/redaction.rs @@ -25,17 +25,6 @@ pub(crate) fn truncate_agent_runtime_text(value: &str, max_chars: usize) -> Stri output } -pub(crate) fn truncate_agent_runtime_text_preserving_tail(value: &str, max_chars: usize) -> String { - let value = value.trim(); - let count = value.chars().count(); - if count <= max_chars { - return value.to_string(); - } - let mut characters = value.chars().rev().take(max_chars).collect::>(); - characters.reverse(); - format!("…{}", characters.into_iter().collect::()) -} - pub(crate) fn sanitize_agent_runtime_text(value: &str, max_chars: usize) -> String { truncate_agent_runtime_text(sanitize_prompt_context(value).trim(), max_chars) } @@ -133,32 +122,6 @@ pub(crate) fn redact_agent_runtime_project_paths_raw(root: &Path, value: &str) - redacted } -pub(crate) fn redact_agent_runtime_project_paths_preserving_tail( - root: &Path, - value: &str, - max_chars: usize, -) -> String { - let mut redacted = value.to_string(); - let root_display = root.to_string_lossy(); - if !root_display.is_empty() { - redacted = redacted.replace(root_display.as_ref(), "$PROJECT_ROOT"); - #[cfg(windows)] - if let Some(non_verbatim_root) = root_display.strip_prefix(r"\\?\") { - redacted = redacted.replace(non_verbatim_root, "$PROJECT_ROOT"); - } - } - if let Ok(canonical_root) = root.canonicalize() { - let canonical_display = canonical_root.to_string_lossy(); - if !canonical_display.is_empty() && canonical_display != root_display { - redacted = redacted.replace(canonical_display.as_ref(), "$PROJECT_ROOT"); - } - } - truncate_agent_runtime_text_preserving_tail( - sanitize_prompt_context(&redacted).trim(), - max_chars, - ) -} - /// 把一个身份值压成可安全拼进项目内路径的分量:只保留 ASCII 字母数字和 `-`、`_`、`.`, /// 其余折叠成 `-`,超长截断;全空时退回 `fallback`。 pub(crate) fn agent_runtime_confirmation_path_component(value: &str, fallback: &str) -> String { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/task_lock.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/task_lock.rs index 950cc6491..488cc2479 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/task_lock.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/task_lock.rs @@ -10,6 +10,7 @@ use crate::project::{normalize_relative_path, unix_timestamp, validate_project_r use std::fs::{self, File}; use std::io::{Seek, SeekFrom, Write}; use std::path::Path; +#[cfg(unix)] use std::sync::{Mutex, OnceLock}; use std::time::Duration; @@ -333,12 +334,3 @@ pub(crate) fn try_open_game_creator_agent_runtime_task_lock_file( root.join(relative_path).display() )) } - -#[cfg(test)] -pub(crate) fn game_creator_agent_runtime_task_lock_is_available( - root: &Path, - agent_id: &str, -) -> Result { - let relative_path = format!(".agent/runtime/locks/{agent_id}.lock"); - Ok(try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?.is_some()) -}