diff --git a/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs b/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs index ed1d70941..f5210841d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/command_exec.rs @@ -638,45 +638,6 @@ pub(crate) fn resolve_project_command_spec_at( .map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error)) } -/// Resolves the one privileged npm operation used to hydrate a DirectProject. -/// It intentionally bypasses the general command.exec npm allow-list: callers -/// must use the dedicated `project.bootstrap` action, which only accepts the -/// literal `npm install` in the project's `game` directory. -pub(crate) fn resolve_project_bootstrap_spec_at( - root: &Path, - timeout_seconds: u64, -) -> Result { - validate_project_root(root) - .map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?; - let cwd_relative = "game".to_string(); - let cwd = resolve_local_project_path(root, &cwd_relative) - .map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?; - validate_project_command_cwd_components(root, &cwd_relative) - .map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?; - if !(PROJECT_COMMAND_MIN_TIMEOUT_SECONDS..=PROJECT_COMMAND_MAX_TIMEOUT_SECONDS) - .contains(&timeout_seconds) - { - return Err(ProjectCommandError::new( - ProjectCommandErrorStage::Validation, - format!("project.bootstrap timeoutSeconds 必须在 {PROJECT_COMMAND_MIN_TIMEOUT_SECONDS}-{PROJECT_COMMAND_MAX_TIMEOUT_SECONDS} 之间"), - )); - } - let program = "npm".to_string(); - let resolved = resolve_project_command_executable(root, &program) - .map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?; - Ok(ProjectCommandSpec { - program, - executable: resolved.executable, - safe_path: resolved.safe_path, - arguments: vec!["install".to_string()], - cwd_relative, - cwd, - timeout_seconds, - verification_eligible: false, - node_launcher: resolved.node_launcher, - }) -} - fn resolve_project_command_spec_inner( root: &Path, program: &str, @@ -2497,16 +2458,6 @@ pub(crate) async fn run_project_command_with_output_at( .await } -pub(crate) async fn run_project_bootstrap_command_at( - root: &Path, - timeout_seconds: u64, -) -> Result { - let spec = resolve_project_bootstrap_spec_at(root, timeout_seconds)?; - let launch = prepare_project_command_launch_spec(root, &spec)?; - let staged = stage_project_command_launch_spec(&spec, launch)?; - run_prepared_project_command_with_output_at(root, &spec, staged, None, || Ok(())).await -} - pub(crate) async fn run_prepared_project_command_with_output_at( root: &Path, spec: &ProjectCommandSpec, diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/mod.rs index 64298d6f6..a6388478c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/mod.rs @@ -22,4 +22,4 @@ pub(crate) mod registry; mod script; /// 发布链路读固定产物的入口。只读 `.export/taonier.zip`,不接用户路径。 -pub(crate) use build::{read_artifact_package, TaonierArtifactPackage}; +pub(crate) use build::read_artifact_package; diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/registry.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/registry.rs index 42dc30b60..d862541e4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/registry.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/registry.rs @@ -80,17 +80,6 @@ pub(crate) fn save( Ok(RegistrySnapshot { form, content_hash }) } -/// 发布链路只读草稿表单。 -/// -/// 发布不是写草稿的入口,所以这里只取表单、不关心指纹,也不做冲突判定;文件缺失时与读命令 -/// 同一口径(`load_or_create` 补一份空表单),让后续的封面/张数校验给出确定的「请先选择封面」 -/// 提示,而不是把「草稿不存在」误报成宿主故障。 -pub(crate) fn read_form(root: &Path) -> Result { - let loaded = load_or_create(®istry_path(root), &TaonierExportRegistry::default()) - .map_err(|error| error.to_string())?; - Ok(loaded.value.form) -} - #[cfg(test)] mod tests { use super::*; diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/script.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/script.rs index 819803f66..3f75c9a6e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/script.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/script.rs @@ -18,7 +18,8 @@ pub(crate) struct NpmScript { pub(crate) cwd_relative: &'static str, /// `package.json` 里声明的脚本文本。交给运行器做反漂移的 `expectedCommand`。 pub(crate) command: String, - /// 相对项目根的 `package.json` 路径,报错与提示都用它。 + /// 相对项目根的 `package.json` 路径;当前只被用例读取。 + #[cfg(test)] pub(crate) package_json_relative: String, } @@ -59,6 +60,7 @@ pub(crate) fn find(root: &Path) -> Option { return Some(NpmScript { cwd_relative: cwd, command: command.to_string(), + #[cfg(test)] package_json_relative: package_json_relative(cwd), }); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/game_package_upload.rs b/apps/ai-game-creator-shell/src-tauri/src/game_package_upload.rs index 679ed41f5..715c95a3b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/game_package_upload.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/game_package_upload.rs @@ -16,7 +16,7 @@ use serde_json::Value; mod runtime; pub(crate) use runtime::{ game_package_upload_staging_dir, progress_event_payload, upload_staged_game_package, - GamePackageUploadOutcome, GamePackageUploadRequest, GAME_PACKAGE_UPLOAD_PROGRESS_EVENT, + GamePackageUploadOutcome, GamePackageUploadRequest, }; /// 已暂存(归一化后)的发行包:内容寻址,重启后同一包复用同一个文件,续传才有意义。 @@ -70,6 +70,10 @@ pub(crate) fn stage_game_package_bytes( } /// 暂存文件必须落在暂存目录内;渲染进程不能借这条命令读任意路径。 +/// +/// 生产接入已随 exports 退役(master `b04aceb57` 删除 `project/export.rs` 与 `commands/desktop.rs` +/// 的发布链路)删除,当前只有本文件的用例调用它。 +#[cfg(test)] pub(crate) fn ensure_staging_path_in_dir( staging_dir: &Path, staging_path: &str, diff --git a/apps/ai-game-creator-shell/src-tauri/src/game_package_upload/runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/game_package_upload/runtime.rs index ccaf75a95..350a59226 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/game_package_upload/runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/game_package_upload/runtime.rs @@ -8,8 +8,6 @@ use std::{ time::Duration, }; -pub(crate) const GAME_PACKAGE_UPLOAD_PROGRESS_EVENT: &str = "game-package-upload-progress"; - const AGC_CLIENT_MARKER_HEADER: &str = "X-Genarrative-Client"; const AGC_CLIENT_MARKER_VALUE: &str = "agc"; const UPLOAD_OFFSET_HEADER: &str = "x-genarrative-upload-offset"; diff --git a/apps/ai-game-creator-shell/src-tauri/src/project.rs b/apps/ai-game-creator-shell/src-tauri/src/project.rs index ffe1f5005..cb3ebbb35 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -6,7 +6,6 @@ mod agent_db; mod asset_export; #[cfg(not(test))] mod asset_rename; -mod bootstrap; mod checkpoint; mod conversation; mod external_editor_bindings; @@ -27,7 +26,6 @@ pub(crate) use agent_db::*; pub(crate) use asset_export::*; #[cfg(not(test))] pub(crate) use asset_rename::*; -pub(crate) use bootstrap::*; pub(crate) use checkpoint::*; pub(crate) use conversation::*; pub(crate) use external_editor_bindings::*; diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/bootstrap.rs b/apps/ai-game-creator-shell/src-tauri/src/project/bootstrap.rs deleted file mode 100644 index 51ac834a7..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/project/bootstrap.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::*; -use sha2::{Digest, Sha256}; - -const BOOTSTRAP_PACKAGE_MAX_BYTES: u64 = 512 * 1024; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct ProjectBootstrapResult { - pub(crate) status: String, - pub(crate) output: String, - pub(crate) package_sha256: String, - pub(crate) lock_sha256: Option, - pub(crate) log_path: String, - pub(crate) updated_at: u64, -} - -fn read_bootstrap_file(path: &Path, label: &str) -> Result, String> { - let metadata = fs::symlink_metadata(path) - .map_err(|error| format!("读取 {label} 失败:{}: {error}", path.display()))?; - if metadata.file_type().is_symlink() || !metadata.is_file() { - return Err(format!("project.bootstrap 要求 {label} 是普通文件")); - } - if metadata.len() > BOOTSTRAP_PACKAGE_MAX_BYTES { - return Err(format!( - "project.bootstrap {label} 超过 {} 字节上限", - BOOTSTRAP_PACKAGE_MAX_BYTES - )); - } - prepare_game_creator_private_path_for_read(path, false, label)?; - fs::read(path).map_err(|error| format!("读取 {label} 失败:{error}")) -} - -pub(crate) async fn run_project_bootstrap_at( - root: &Path, - timeout_seconds: u64, -) -> Result { - validate_project_root(root)?; - let game = resolve_local_project_path(root, "game")?; - if !game.is_dir() { - return Err("project.bootstrap 只允许项目内 game 目录".to_string()); - } - let package = read_bootstrap_file(&game.join("package.json"), "game/package.json")?; - let package_json: serde_json::Value = serde_json::from_slice(&package) - .map_err(|error| format!("解析 game/package.json 失败:{error}"))?; - if let Some(manager) = package_json - .get("packageManager") - .and_then(serde_json::Value::as_str) - { - if !manager.trim().starts_with("npm@") && manager.trim() != "npm" { - return Err("project.bootstrap 当前只支持 npm packageManager".to_string()); - } - } - for lock_name in ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"] { - if game.join(lock_name).exists() { - return Err(format!("project.bootstrap 检测到非 npm 锁文件 {lock_name}")); - } - } - if !package_json - .get("scripts") - .is_some_and(serde_json::Value::is_object) - { - return Err("project.bootstrap 要求 game/package.json 包含 scripts 对象".to_string()); - } - let lock = match fs::symlink_metadata(game.join("package-lock.json")) { - Ok(_) => Some(read_bootstrap_file( - &game.join("package-lock.json"), - "game/package-lock.json", - )?), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, - Err(error) => return Err(format!("读取 game/package-lock.json 失败:{error}")), - }; - let command = crate::command_exec::run_project_bootstrap_command_at(root, timeout_seconds) - .await - .map_err(|error| error.to_string())?; - let completed = command.exit_code == Some(0) && !command.timed_out; - let status = if completed { "completed" } else { "failed" }; - let updated_at = unix_timestamp(); - let log_path = resolve_local_project_path(root, ".agent/logs/command.log")?; - let package_sha256 = format!("{:x}", Sha256::digest(&package)); - let lock_sha256 = lock - .as_ref() - .map(|bytes| format!("{:x}", Sha256::digest(bytes))); - let output = sanitize_project_verification_output(&command.output); - let line = format!( - "{updated_at} project.bootstrap status={} packageSha256={} lockSha256={} cwd=game\n{}\n", - status, - package_sha256, - lock_sha256.as_deref().unwrap_or("none"), - output - ); - append_game_creator_private_file(&log_path, line.as_bytes(), "命令日志")?; - record_command_run( - root, - GameCreationAppCommandRunState { - command_id: "project.bootstrap".to_string(), - status: if completed { - GameCreationAppCommandRunStatus::Completed - } else { - GameCreationAppCommandRunStatus::Failed - }, - output: output.clone(), - log_path: ".agent/logs/command.log".to_string(), - updated_at, - }, - )?; - Ok(ProjectBootstrapResult { - status: status.to_string(), - output, - package_sha256, - lock_sha256, - log_path: ".agent/logs/command.log".to_string(), - updated_at, - }) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs b/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs index fa8244a98..5259d0166 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/verification.rs @@ -659,30 +659,9 @@ where }) } -pub(crate) async fn run_project_verification_with_commit_at( - root: &Path, - script: &str, - expected_command: &str, - timeout_seconds: u64, - cwd_relative: &str, - durable_commit: F, -) -> Result -where - F: FnOnce() -> Result<(), String>, -{ - let spec = resolve_project_verification_spec_with_cwd_at( - root, - script, - expected_command, - timeout_seconds, - cwd_relative, - )?; - run_project_verification_spec_with_commit_at(root, spec, durable_commit).await -} - /// 跑一份**已解析好**的验证规格。 /// -/// 从 [`run_project_verification_with_commit_at`] 拆出来,是为了让调用方能先把「项目侧预检」 +/// 从「解析 + 运行」合一的入口拆出来,是为了让调用方能先把「项目侧预检」 /// (package.json / 脚本缺失或漂移 / 包管理器不支持等)与「跑起来之后的宿主侧事实」分开处理: /// 预检错误由调用方自己决定怎么表达,不必跟沙箱、日志这类宿主故障混成同一个字符串错误。 pub(crate) async fn run_project_verification_spec_with_commit_at(