diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs index 060af0d67..011748199 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/build.rs @@ -10,12 +10,13 @@ use std::path::Path; use crate::project::run_project_verification_with_commit_at; +use super::commands::offload; use super::dto::XHSMiniToolExportRunResult; use super::error::{ ArtifactMissing, CommandDenied, CommandFailed, ExportUnavailable, XHSMiniToolExportBuildError, }; use super::layout::{artifact_path, artifact_relative_path, SCRIPT_NAME}; -use super::script; +use super::script::{self, NpmScript}; /// 权限点位复用既有命令口径:这就是在跑项目命令,不新增 `GAME_CREATION_APP_COMMANDS` 条目。 const PERMISSION_COMMAND_ID: &str = "command.exec"; @@ -27,6 +28,45 @@ const OUTPUT_TAIL_MAX_CHARS: usize = 4_000; pub(crate) async fn run( root: &Path, ) -> Result { + // 找脚本、读权限策略、清上一次产物都是同步文件 IO:与读/存命令同一口径挪到阻塞线程池, + // 不占 async worker。 + let root = root.to_path_buf(); + let script = offload({ + let root = root.clone(); + move || prepare(&root) + }) + .await?; + + let result = run_project_verification_with_commit_at( + &root, + SCRIPT_NAME, + &script.command, + TIMEOUT_SECONDS, + script.cwd_relative, + // 导出不推进项目 revision,也不改任何正式状态:产物只是 `.export/` 下的一个文件。 + || Ok(()), + ) + .await + .map_err(ExportUnavailable::new)?; + + // 收尾判产物同样是同步 IO(symlink_metadata),一样搬去阻塞线程池。 + offload(move || { + conclude( + &root, + result.exit_code, + result.timed_out, + &result.output, + ) + }) + .await +} + +/// 构建前置(同步):找到脚本、确认权限点位没被拒、清掉上一次的产物。 +/// +/// 产物判据只看「文件非空」,所以先清掉上一次的 zip:脚本这次什么都没写时,残留的旧 zip 会 +/// 让 no-op 脚本被判成功,用户上传的其实是上一轮的产物。删不掉(权限等)就 fail closed—— +/// 留着旧产物继续判成功,比这次构建失败更糟。 +fn prepare(root: &Path) -> Result { let Some(script) = script::find(root) else { return Err(script::missing_error()); }; @@ -42,9 +82,6 @@ pub(crate) async fn run( } })?; - // 产物判据只看「文件非空」,所以先清掉上一次的 zip:脚本这次什么都没写时,残留的旧 zip 会 - // 让 no-op 脚本被判成功,用户上传的其实是上一轮的产物。删不掉(权限等)就 fail closed—— - // 留着旧产物继续判成功,比这次构建失败更糟。 let artifact = artifact_path(root); match std::fs::remove_file(&artifact) { Ok(()) => {} @@ -59,19 +96,7 @@ pub(crate) async fn run( } } - let result = run_project_verification_with_commit_at( - root, - SCRIPT_NAME, - &script.command, - TIMEOUT_SECONDS, - script.cwd_relative, - // 导出不推进项目 revision,也不改任何正式状态:产物只是 `.export/` 下的一个文件。 - || Ok(()), - ) - .await - .map_err(ExportUnavailable::new)?; - - conclude(root, result.exit_code, result.timed_out, &result.output) + Ok(script) } /// 把宿主的构建结果收成导出结果:先判成功,再确认产物真的在。 diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/commands.rs index 920d93f7e..c3daf01df 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/commands.rs @@ -128,7 +128,9 @@ fn project_root(project_path: &str) -> Result { /// 读命令每 2 秒被轮询一次,注册表 + `package.json` 探测虽然小,但直接在 `async fn` 里做同步 /// 文件 IO 会占着 worker 线程,拖慢同一 runtime 上的其它异步任务(构建子进程、SpacetimeDB /// 客户端)。任务 panic 时 `JoinError` 收口成 `ExportUnavailable`,走各命令既有的失败表。 -async fn offload(job: F) -> Result +/// +/// 构建命令(`build::run`)的前后置同步 IO 也复用这里,所以对同级的 `build` 模块可见。 +pub(super) async fn offload(job: F) -> Result where T: Send + 'static, E: From + Send + 'static,