From 06e6cf8523951c1a09588817b5ed686ad2091d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 7 Oct 2026 14:39:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E9=A2=84=E6=A3=80=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E4=B8=8E=E5=AE=BF=E4=B8=BB=E6=95=85=E9=9A=9C=E5=88=86?= =?UTF-8?q?=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verification.rs 拆出按已解析规格运行的入口,项目侧预检错误不再与沙箱/日志故障混成一个字符串 - 陶泥儿与小红书构建把预检失败映射成新的 projectUnavailable 变体(改项目代码可解) - 前端失败表补 projectUnavailable(归类为 code agent 可修),更新生成绑定与测试 --- .../src/export/draft/taonier/build.rs | 21 ++++++++++++++---- .../src/export/draft/taonier/error.rs | 22 +++++++++++++++++++ .../src/export/draft/xhs_minitool/build.rs | 22 +++++++++++++++---- .../src/export/draft/xhs_minitool/error.rs | 22 +++++++++++++++++++ .../src-tauri/src/project/verification.rs | 16 ++++++++++++++ .../export/generated/ProjectUnavailable.ts | 9 ++++++++ .../generated/TaonierExportBuildError.ts | 3 ++- .../generated/TaonierProjectUnavailable.ts | 9 ++++++++ .../generated/XHSMiniToolExportBuildError.ts | 3 ++- .../export/state/taonierFailure.ts | 4 ++++ .../export/state/xhsMinitoolFailure.ts | 4 ++++ .../tests/xhsMinitoolExport.test.tsx | 19 ++++++++++++++++ 12 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/view/project-development/export/generated/ProjectUnavailable.ts create mode 100644 apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierProjectUnavailable.ts diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/build.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/build.rs index 078132dce..d997e8686 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/build.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/taonier/build.rs @@ -14,13 +14,15 @@ use std::path::Path; use sha2::{Digest, Sha256}; -use crate::project::run_project_verification_with_commit_at; +use crate::project::{ + resolve_project_verification_spec_with_cwd_at, run_project_verification_spec_with_commit_at, +}; use super::commands::offload; use super::dto::TaonierExportRunResult; use super::error::{ TaonierArtifactMissing, TaonierCommandDenied, TaonierCommandFailed, TaonierExportBuildError, - TaonierExportUnavailable, + TaonierExportUnavailable, TaonierProjectUnavailable, }; use super::layout::{artifact_path, artifact_relative_path, SCRIPT_NAME}; use super::script::{self, NpmScript}; @@ -74,12 +76,23 @@ pub(crate) async fn run(root: &Path) -> Result) -> Self { + Self { + cause: cause.into(), + } + } +} + impl TaonierExportUnavailable { pub(crate) fn new(cause: impl Into) -> Self { Self { 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 a95d729a0..29ae466ab 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 @@ -8,12 +8,15 @@ use std::path::Path; -use crate::project::run_project_verification_with_commit_at; +use crate::project::{ + resolve_project_verification_spec_with_cwd_at, run_project_verification_spec_with_commit_at, +}; use super::commands::offload; use super::dto::XHSMiniToolExportRunResult; use super::error::{ - ArtifactMissing, CommandDenied, CommandFailed, ExportUnavailable, XHSMiniToolExportBuildError, + ArtifactMissing, CommandDenied, CommandFailed, ExportUnavailable, ProjectUnavailable, + XHSMiniToolExportBuildError, }; use super::layout::{artifact_path, artifact_relative_path, SCRIPT_NAME}; use super::script::{self, NpmScript}; @@ -37,12 +40,23 @@ pub(crate) async fn run( }) .await?; - let result = run_project_verification_with_commit_at( + // 项目侧预检(package.json / 脚本缺失或漂移 / 包管理器不支持等)单独跑:它失败时报 + // `projectUnavailable`(改项目代码可解),不能和沙箱、日志这类宿主故障一起收成 + // `exportUnavailable`,否则用户会被引导去重试一个改项目才能解决的问题。 + let spec = resolve_project_verification_spec_with_cwd_at( &root, SCRIPT_NAME, &script.command, TIMEOUT_SECONDS, - script.cwd_relative, + &script.cwd_relative, + ) + .map_err(|cause| { + XHSMiniToolExportBuildError::ProjectUnavailable(ProjectUnavailable::new(cause)) + })?; + + let result = run_project_verification_spec_with_commit_at( + &root, + spec, // 导出不推进项目 revision,也不改任何正式状态:产物只是 `.export/` 下的一个文件。 || Ok(()), ) diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/error.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/error.rs index 7c86b6869..d168b0aa1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/error.rs @@ -72,6 +72,9 @@ pub(crate) enum XHSMiniToolExportBuildError { CommandFailed(CommandFailed), /// 脚本正常收场但产物不在。 ArtifactMissing(ArtifactMissing), + /// 项目侧预检没过(package.json / 脚本缺失或漂移 / 包管理器不支持等):改项目代码可解, + /// 与宿主环境跑不起来区分开。 + ProjectUnavailable(ProjectUnavailable), /// 脚本无法启动或其它宿主侧事实故障。 ExportUnavailable(ExportUnavailable), } @@ -140,6 +143,25 @@ pub(crate) struct ExportUnavailable { pub(crate) cause: String, } +/// 项目侧预检失败的原文:缺哪个脚本、漂移到什么值、检测到哪种包管理器等。 +/// +/// 它和 [`ExportUnavailable`] 分开,因为「要改的东西在不在项目代码里」是前端分流的唯一判据: +/// 这里的 `cause` 只是引导 agent 的现场原文,前端不解析它。 +#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))] +pub(crate) struct ProjectUnavailable { + pub(crate) cause: String, +} + +impl ProjectUnavailable { + pub(crate) fn new(cause: impl Into) -> Self { + Self { + cause: cause.into(), + } + } +} + impl ExportUnavailable { pub(crate) fn new(cause: impl Into) -> Self { Self { 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 c2cc15b9c..e1824f6a4 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 @@ -711,6 +711,22 @@ where 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( + root: &Path, + spec: ProjectVerificationSpec, + durable_commit: F, +) -> Result +where + F: FnOnce() -> Result<(), String>, +{ let started_at = std::time::Instant::now(); let mut process = match run_project_verification_process(root, &spec, durable_commit).await { Ok(process) => process, diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/ProjectUnavailable.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/ProjectUnavailable.ts new file mode 100644 index 000000000..5b77f769c --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/ProjectUnavailable.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 项目侧预检失败的原文:缺哪个脚本、漂移到什么值、检测到哪种包管理器等。 + * + * 它和 [`ExportUnavailable`] 分开,因为「要改的东西在不在项目代码里」是前端分流的唯一判据: + * 这里的 `cause` 只是引导 agent 的现场原文,前端不解析它。 + */ +export type ProjectUnavailable = { cause: string, }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierExportBuildError.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierExportBuildError.ts index 9ebd3a462..b0e2a6e7d 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierExportBuildError.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierExportBuildError.ts @@ -4,8 +4,9 @@ import type { TaonierBuildScriptMissing } from "./TaonierBuildScriptMissing"; import type { TaonierCommandDenied } from "./TaonierCommandDenied"; import type { TaonierCommandFailed } from "./TaonierCommandFailed"; import type { TaonierExportUnavailable } from "./TaonierExportUnavailable"; +import type { TaonierProjectUnavailable } from "./TaonierProjectUnavailable"; /** * 跑构建可能怎么失败。构建不是表单的消费者,所以这里没有表单类变体。 */ -export type TaonierExportBuildError = { "type": "buildScriptMissing" } & TaonierBuildScriptMissing | { "type": "commandDenied" } & TaonierCommandDenied | { "type": "commandFailed" } & TaonierCommandFailed | { "type": "artifactMissing" } & TaonierArtifactMissing | { "type": "exportUnavailable" } & TaonierExportUnavailable; +export type TaonierExportBuildError = { "type": "buildScriptMissing" } & TaonierBuildScriptMissing | { "type": "commandDenied" } & TaonierCommandDenied | { "type": "commandFailed" } & TaonierCommandFailed | { "type": "artifactMissing" } & TaonierArtifactMissing | { "type": "projectUnavailable" } & TaonierProjectUnavailable | { "type": "exportUnavailable" } & TaonierExportUnavailable; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierProjectUnavailable.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierProjectUnavailable.ts new file mode 100644 index 000000000..8499ae7a3 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/TaonierProjectUnavailable.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 项目侧预检失败的原文:缺哪个脚本、漂移到什么值、检测到哪种包管理器等。 + * + * 它和 [`TaonierExportUnavailable`] 分开,因为「要改的东西在不在项目代码里」是前端分流的唯一判据: + * 这里的 `cause` 只是引导 agent 的现场原文,前端不解析它。 + */ +export type TaonierProjectUnavailable = { cause: string, }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportBuildError.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportBuildError.ts index 8e4ab1228..fc97e82b5 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportBuildError.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportBuildError.ts @@ -4,8 +4,9 @@ import type { BuildScriptMissing } from "./BuildScriptMissing"; import type { CommandDenied } from "./CommandDenied"; import type { CommandFailed } from "./CommandFailed"; import type { ExportUnavailable } from "./ExportUnavailable"; +import type { ProjectUnavailable } from "./ProjectUnavailable"; /** * 跑构建可能怎么失败。构建不是表单的消费者,所以这里没有表单类变体。 */ -export type XHSMiniToolExportBuildError = { "type": "buildScriptMissing" } & BuildScriptMissing | { "type": "commandDenied" } & CommandDenied | { "type": "commandFailed" } & CommandFailed | { "type": "artifactMissing" } & ArtifactMissing | { "type": "exportUnavailable" } & ExportUnavailable; +export type XHSMiniToolExportBuildError = { "type": "buildScriptMissing" } & BuildScriptMissing | { "type": "commandDenied" } & CommandDenied | { "type": "commandFailed" } & CommandFailed | { "type": "artifactMissing" } & ArtifactMissing | { "type": "projectUnavailable" } & ProjectUnavailable | { "type": "exportUnavailable" } & ExportUnavailable; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/taonierFailure.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/taonierFailure.ts index 39e3e0d67..f3bbfbe85 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/taonierFailure.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/taonierFailure.ts @@ -53,6 +53,7 @@ export function classifyTaonierFailure( case 'buildScriptMissing': case 'commandFailed': case 'artifactMissing': + case 'projectUnavailable': return TaonierFailureRepairability.CodeAgentRepairable; case 'commandDenied': case 'exportUnavailable': @@ -80,6 +81,9 @@ export function describeTaonierFailure(error: TaonierCardFailure): string { : '导出中途出错了,没能生成代码包。'; case 'artifactMissing': return '导出跑完了,但没有生成代码包。可以让陶泥儿看看。'; + case 'projectUnavailable': + // 项目侧的预检事实(脚本漂移、包管理器不支持等):要改的东西在项目代码里,不是宿主故障。 + return '这个项目的导出配置对不上,需要先按提示调整项目后再试。'; case 'exportUnavailable': return '这次没能执行导出,请稍后重试。'; default: diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts index 88f694346..5f2f937f4 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFailure.ts @@ -53,6 +53,7 @@ export function classifyXhsMinitoolFailure( case 'buildScriptMissing': case 'commandFailed': case 'artifactMissing': + case 'projectUnavailable': return XhsMinitoolFailureRepairability.CodeAgentRepairable; case 'commandDenied': case 'exportUnavailable': @@ -82,6 +83,9 @@ export function describeXhsMinitoolFailure( : '导出中途出错了,没能生成代码包。'; case 'artifactMissing': return '导出跑完了,但没有生成代码包。可以让陶泥儿看看。'; + case 'projectUnavailable': + // 项目侧的预检事实(脚本漂移、包管理器不支持等):要改的东西在项目代码里,不是宿主故障。 + return '这个项目的导出配置对不上,需要先按提示调整项目后再试。'; case 'exportUnavailable': return '这次没能执行导出,请稍后重试。'; default: diff --git a/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx b/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx index 69e554bc5..d9f58d239 100644 --- a/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx +++ b/apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx @@ -647,6 +647,25 @@ describe('面板左半', () => { fireEvent.click(screen.getByRole('button', { name: '让陶泥儿来修' })); expect(broken.repair).toHaveBeenCalledTimes(1); expect(broken.adapt).not.toHaveBeenCalled(); + + // 项目侧预检没过(脚本漂移等):要改的东西在项目代码里,同样给修复入口。 + cleanup(); + const projectMismatch = fakeMinitool({ + failure: { + type: 'projectUnavailable', + cause: 'package.json 中的 build:xhs-minitool 脚本已变化', + }, + }); + renderPane(projectMismatch); + + expect( + screen.getByText( + '这个项目的导出配置对不上,需要先按提示调整项目后再试。', + ), + ).toBeTruthy(); + fireEvent.click(screen.getByRole('button', { name: '让陶泥儿来修' })); + expect(projectMismatch.repair).toHaveBeenCalledTimes(1); + expect(projectMismatch.adapt).not.toHaveBeenCalled(); }); it('只能人来改的失败,弹窗不给任何「让陶泥儿」入口,只给关闭', () => {