From 9a844f69a0233649b14b202784ad738aaeebc924 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 7 Oct 2026 19:22:19 +0800 Subject: [PATCH] =?UTF-8?q?revert(agc):=20=E6=92=A4=E5=9B=9E=E4=B8=BA=20We?= =?UTF-8?q?rror=20=E6=9C=8D=E5=8A=A1=E3=80=81=E4=B8=94=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=E4=BA=86=E4=B8=8A=E6=B8=B8=E4=BB=A3=E7=A0=81=E7=9A=84=E4=B8=A4?= =?UTF-8?q?=E5=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按决策「不要替上游作者修」:这两处改动的唯一目的是让自己的 `deny(warnings)` 不把上游新代码的存量 告警判死,撤销 Werror 后不再需要,且它们动了 master 的文件,故整体撤回。 - revert 1a86675f4:把 master 本轮新代码带来的 14 处告警(`b04aceb57` 等的 exports/bootstrap 退役 留下的无消费者实现)恢复原样——`project/bootstrap.rs`(113 行)、`project.rs` 的 `mod bootstrap;` 与 glob 再导出、`project/verification.rs` 的薄包装、`command_exec.rs` 的两个 bootstrap 函数、 `game_package_upload{,/runtime}.rs`、`export/draft/taonier/{mod,registry,script}.rs` 全部回到 master 状态。 - revert 694b54506:撤掉上面那批里 `resolve_project_bootstrap_spec_at` 的 `#[cfg(all(target_os = "linux", test))]` 收窄;两个 revert 相抵后该函数就是 **master 原文**, `command_exec.rs` 里 Linux 用例仍能编译(此时它只是 Windows 侧的一条告警,不是错误)。 注:CI 修复提交 `ad33d42bc`(process_tree 夹具的硬编码测试路径)**保留**——那是收敛重命名造成的 真实故障(Linux 测试会失败),与 Werror 无关。 这 14 条告警已逐条入册 `scripts/warning-baseline.json` 的 `agc-windows` / `agc-linux` profile, 归属与风险分级见 `local://commit-1a86675f4-inventory.md`。 --- .../src-tauri/src/command_exec.rs | 17 +-- .../src-tauri/src/export/draft/taonier/mod.rs | 2 +- .../src/export/draft/taonier/registry.rs | 11 ++ .../src/export/draft/taonier/script.rs | 4 +- .../src-tauri/src/game_package_upload.rs | 6 +- .../src/game_package_upload/runtime.rs | 2 + .../src-tauri/src/project.rs | 2 + .../src-tauri/src/project/bootstrap.rs | 113 ++++++++++++++++++ .../src-tauri/src/project/verification.rs | 23 +++- 9 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/src/project/bootstrap.rs 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 d88613688..ed1d70941 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 @@ -642,13 +642,6 @@ pub(crate) fn resolve_project_command_spec_at( /// 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. -/// -/// 生产调用方(`project/export.rs`)已随 master 的 exports 退役删除,现存唯一引用是本文件的 -/// `npm_command_targets_node_plus_npm_cli_on_linux` 用例;因此按**真实使用面**收窄到 -/// `all(target_os = "linux", test)`——与同文件 `project_command_actual_target` 的 -/// `cfg(target_os = "linux")` 相比更窄,因为那个函数 Linux 生产仍在调用,这个已不在。 -/// 引用侧的有效 cfg(`#[cfg(test)] mod tests` + `#[cfg(target_os = "linux")]` 用例)与定义侧一致。 -#[cfg(all(target_os = "linux", test))] pub(crate) fn resolve_project_bootstrap_spec_at( root: &Path, timeout_seconds: u64, @@ -2504,6 +2497,16 @@ 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 a6388478c..64298d6f6 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; +pub(crate) use build::{read_artifact_package, TaonierArtifactPackage}; 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 d862541e4..42dc30b60 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,6 +80,17 @@ 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 3f75c9a6e..819803f66 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,8 +18,7 @@ pub(crate) struct NpmScript { pub(crate) cwd_relative: &'static str, /// `package.json` 里声明的脚本文本。交给运行器做反漂移的 `expectedCommand`。 pub(crate) command: String, - /// 相对项目根的 `package.json` 路径;当前只被用例读取。 - #[cfg(test)] + /// 相对项目根的 `package.json` 路径,报错与提示都用它。 pub(crate) package_json_relative: String, } @@ -60,7 +59,6 @@ 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 715c95a3b..679ed41f5 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, + GamePackageUploadOutcome, GamePackageUploadRequest, GAME_PACKAGE_UPLOAD_PROGRESS_EVENT, }; /// 已暂存(归一化后)的发行包:内容寻址,重启后同一包复用同一个文件,续传才有意义。 @@ -70,10 +70,6 @@ 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 350a59226..ccaf75a95 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,6 +8,8 @@ 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 cb3ebbb35..ffe1f5005 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -6,6 +6,7 @@ mod agent_db; mod asset_export; #[cfg(not(test))] mod asset_rename; +mod bootstrap; mod checkpoint; mod conversation; mod external_editor_bindings; @@ -26,6 +27,7 @@ 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 new file mode 100644 index 000000000..51ac834a7 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/project/bootstrap.rs @@ -0,0 +1,113 @@ +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 5259d0166..fa8244a98 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,9 +659,30 @@ 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(