#[path = "build_support/frontend_dist_guard.rs"] mod frontend_dist_guard; #[path = "build_support/runtime_prompt_bundle.rs"] mod runtime_prompt_bundle; #[cfg(windows)] use sha2::{Digest, Sha256}; use std::collections::BTreeSet; use std::env; use std::fs; use std::path::PathBuf; #[cfg(windows)] use std::io::{BufReader, Read}; const BUNDLED_CODEX_CLI_VERSION: &str = "codex-cli 0.147.0"; #[cfg(windows)] const BUNDLED_CODEX_FILES: [&str; 6] = [ "bin/codex.exe", "bin/codex-code-mode-host.exe", "codex-path/rg.exe", "codex-resources/codex-command-runner.exe", "codex-resources/codex-windows-sandbox-setup.exe", "codex-package.json", ]; #[cfg(windows)] fn sha256_file(path: &std::path::Path) -> Result { let file = fs::File::open(path)?; let mut reader = BufReader::new(file); let mut hasher = Sha256::new(); let mut buffer = [0_u8; 64 * 1024]; loop { let read = reader.read(&mut buffer)?; if read == 0 { break; } hasher.update(&buffer[..read]); } Ok(format!("{:x}", hasher.finalize())) } fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) { #[cfg(windows)] { let app_root = manifest_dir .parent() .expect("AI 游戏创作 Tauri manifest 必须位于应用目录下"); let repo_root = app_root .parent() .and_then(|apps_dir| apps_dir.parent()) .expect("AI 游戏创作应用必须位于仓库 apps 目录下"); let source_candidates = [ app_root.join( "node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc", ), app_root.join( "node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc", ), repo_root.join( "node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc", ), repo_root.join( "node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc", ), ]; let source = source_candidates .iter() .find(|path| { BUNDLED_CODEX_FILES .iter() .all(|relative| path.join(relative).is_file()) }) .cloned() .unwrap_or_else(|| { panic!( "内置 Codex CLI 缺失;请先在仓库根目录执行 npm ci(已检查:{})", source_candidates .iter() .map(|path| path.display().to_string()) .collect::>() .join(";") ) }); let target_dir = manifest_dir.join("resources/codex/win-x64"); let notice = target_dir.join("NOTICE.md"); if !notice.is_file() { panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display()); } fs::create_dir_all(&target_dir).expect("创建内置 Codex CLI 资源目录失败"); let mut file_hashes = serde_json::Map::new(); for relative in BUNDLED_CODEX_FILES { let source_path = source.join(relative); let target_path = target_dir.join(relative); if let Some(parent) = target_path.parent() { fs::create_dir_all(parent).expect("创建内置 Codex CLI 资源子目录失败"); } let source_sha256 = sha256_file(&source_path).expect("读取内置 Codex CLI 资源失败"); let target_matches_source = target_path.is_file() && sha256_file(&target_path) .map(|target_sha256| target_sha256 == source_sha256) .unwrap_or(false); if !target_matches_source { fs::copy(&source_path, &target_path).expect("复制内置 Codex CLI 资源失败"); } file_hashes.insert( relative.to_string(), serde_json::Value::String(source_sha256), ); } let manifest = serde_json::json!({ "schemaVersion": "genarrative-codex-sidecar.v2", "platform": "win32-x64", "version": BUNDLED_CODEX_CLI_VERSION, "files": file_hashes, }); let manifest_path = target_dir.join("manifest.json"); let manifest_payload = format!( "{}\n", serde_json::to_string_pretty(&manifest).expect("序列化内置 Codex CLI 清单失败") ); if fs::read_to_string(&manifest_path) .map(|current| current != manifest_payload) .unwrap_or(true) { fs::write(&manifest_path, manifest_payload).expect("写入内置 Codex CLI 清单失败"); } for relative in BUNDLED_CODEX_FILES { println!("cargo:rerun-if-changed={}", source.join(relative).display()); } println!("cargo:rerun-if-changed={}", notice.display()); } } fn seed_task_group_id( group: &shared_contracts::game_creation_app::GameCreationAppAgentGroup, ) -> &'static str { use shared_contracts::game_creation_app::GameCreationAppAgentGroup; match group { GameCreationAppAgentGroup::Design => "design", GameCreationAppAgentGroup::Art => "art", GameCreationAppAgentGroup::Code => "code", GameCreationAppAgentGroup::Balance => "balance", GameCreationAppAgentGroup::Audio => "audio", GameCreationAppAgentGroup::Publishing => "publishing", } } fn validate_seed_task_catalog(compiled: &runtime_prompt_bundle::CompiledPromptBundle) { let actual = compiled .specialist_nodes .iter() .map(|node| { ( node.task_id.clone(), node.group_id.clone(), node.role.clone(), ) }) .collect::>(); let expected = shared_contracts::game_creation_app::new_game_creation_app_seed_tasks() .into_iter() .map(|task| { ( task.id, seed_task_group_id(&task.group).to_string(), task.role, ) }) .collect::>(); if actual != expected { panic!( "Prompt Bundle agentCatalog 与正式 seed DAG 的 taskId/group/role 不一致\nactual={actual:#?}\nexpected={expected:#?}" ); } } fn main() { let manifest_dir = PathBuf::from( env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be available"), ); let manifest_path = manifest_dir.join("prompts/runtime/manifest.json"); stage_bundled_codex_cli(&manifest_dir); let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path) .unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}")); validate_seed_task_catalog(&compiled); for dependency in &compiled.dependencies { println!("cargo:rerun-if-changed={}", dependency.display()); } let output_path = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be available")) .join("agent_runtime_prompt_bundle.rs"); fs::write(&output_path, compiled.rust_source) .unwrap_or_else(|error| panic!("写入 Prompt Bundle 生成代码失败:{error}")); if !tauri_build::is_dev() { let frontend_dist = manifest_dir .parent() .expect("AI 游戏创作 Tauri manifest 必须位于应用目录下") .join("dist"); println!("cargo:rerun-if-changed={}", frontend_dist.display()); frontend_dist_guard::validate_frontend_dist(&frontend_dist) .unwrap_or_else(|error| panic!("生产 frontendDist 检查失败:{error}")); } tauri_build::build() }