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 be8d993ba..cd3b553ee 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 @@ -11,7 +11,7 @@ use std::path::Path; use crate::project::run_project_verification_with_commit_at; use super::dto::XHSMiniToolExportRunResult; -use super::error::{ArtifactMissing, CommandFailed, XHSMiniToolExportError}; +use super::error::{ArtifactMissing, CommandDenied, CommandFailed, XHSMiniToolExportError}; use super::layout::{artifact_path, artifact_relative_path, SCRIPT_NAME}; use super::script; @@ -26,8 +26,17 @@ pub(crate) async fn run(root: &Path) -> Result { + XHSMiniToolExportError::CommandDenied(CommandDenied { command_id }) + } + crate::agent::tool::error::ProjectPermissionRejection::PolicyUnavailable { cause } => { + XHSMiniToolExportError::unavailable(cause) + } + })?; let result = run_project_verification_with_commit_at( root, @@ -55,9 +64,11 @@ fn conclude( output: &str, ) -> Result { if timed_out || exit_code != Some(0) { + let (output_tail, omitted_characters) = tail(output); return Err(XHSMiniToolExportError::CommandFailed(CommandFailed { exit_code, - output_tail: tail(output), + output_tail, + omitted_characters, })); } @@ -74,21 +85,26 @@ fn conclude( } // 成功也回输出尾部:`pack.mjs` 会把 zip 的条目数与体积打在这里,正是用户上传前要看的数字。 + let (output_tail, omitted_characters) = tail(output); Ok(XHSMiniToolExportRunResult { - output_tail: tail(output), + output_tail, + omitted_characters, }) } -/// 只留尾部若干字符,并注明省略了多少。整份构建日志塞进 IPC 没有意义,还会把界面拖住。 -fn tail(output: &str) -> String { +/// 只留尾部若干字符:整份构建日志塞进 IPC 没有意义,还会把界面拖住。 +/// +/// 返回(尾部原文, 省略了多少个字符)。省略量是**事实**,「省略前 N 个字符」那句话由前端拼—— +/// 宿主不预拼用户可见文案,否则前端没法按自己的现场排版,也没法判断这段尾巴是否完整。 +fn tail(output: &str) -> (String, usize) { let trimmed = output.trim(); let characters: Vec = trimmed.chars().collect(); if characters.len() <= OUTPUT_TAIL_MAX_CHARS { - return trimmed.to_string(); + return (trimmed.to_string(), 0); } let skipped = characters.len() - OUTPUT_TAIL_MAX_CHARS; let tail: String = characters[skipped..].iter().collect(); - format!("…(省略前 {skipped} 个字符)\n{tail}") + (tail, skipped) } #[cfg(test)] @@ -117,16 +133,18 @@ mod tests { } #[test] - fn keeps_short_output_as_is() { - assert_eq!(tail(" 构建失败\n"), "构建失败"); + fn keeps_short_output_as_is_and_omits_nothing() { + assert_eq!(tail(" 构建失败\n"), ("构建失败".to_string(), 0)); } #[test] - fn keeps_only_the_tail_of_long_output() { + fn keeps_only_the_tail_of_long_output_and_reports_the_fact() { let output: String = "前".repeat(OUTPUT_TAIL_MAX_CHARS) + "后"; - let kept = tail(&output); + let (kept, omitted) = tail(&output); + // 载荷里只有原文与省略量两个事实:说明那句话由前端拼,宿主不写。 assert!(kept.ends_with('后')); - assert!(kept.contains("省略前 1 个字符")); + assert!(!kept.contains("省略")); + assert_eq!(omitted, 1); } #[test] @@ -152,6 +170,7 @@ mod tests { }; assert_eq!(failure.exit_code, Some(1)); assert_eq!(failure.output_tail, "vite 构建失败\n入口不存在"); + assert_eq!(failure.omitted_characters, 0); } #[test] @@ -165,6 +184,7 @@ mod tests { }; assert_eq!(failure.exit_code, None); assert_eq!(failure.output_tail, "已取消"); + assert_eq!(failure.omitted_characters, 0); } #[test] @@ -202,6 +222,7 @@ mod tests { ) .expect("产物在就应当成功"); assert!(result.output_tail.contains("3 个条目")); + assert_eq!(result.omitted_characters, 0); } /// 真起进程跑一遍宿主链路:解析 npm 包、走 `command.exec` 边界执行项目脚本、再按产物判据收尾。 diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs index 107bf0bd3..050b76968 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs @@ -74,6 +74,8 @@ pub(crate) struct XHSMiniToolExportState { #[serde(rename_all = "camelCase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))] pub(crate) struct XHSMiniToolExportRunResult { - /// 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。 + /// 构建输出尾部原文(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。 pub(crate) output_tail: String, + /// 尾部之前省略了多少个字符;0 表示没截。说明文字由前端拼。 + pub(crate) omitted_characters: usize, } 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 1505e8f16..0a4cf3117 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 @@ -28,6 +28,8 @@ pub(crate) enum XHSMiniToolExportError { BuildScriptMissing(BuildScriptMissing), /// 保存时发现注册表被别人改过,且字段真的不同。绝不覆盖,交给用户逐字段选。 SaveConflict(SaveConflict), + /// 项目权限策略拒绝执行 `command.exec`:用户/项目策略说了不让跑,不是缺陷。 + CommandDenied(CommandDenied), /// 脚本跑完没有正常收场(非零退出或超时)。 CommandFailed(CommandFailed), /// 脚本正常收场但产物不在。 @@ -72,12 +74,25 @@ pub(crate) struct SaveConflict { pub(crate) content_hash: String, } +/// 策略拒绝与宿主故障分开:前者是预期拒绝(面板给固定话术,不入错误池),后者是宿主侧事实故障 +/// (面板给现场,并原样抛出进错误池)。ADR 要求顶层只放调用方要分流的类别,这里就是那个分流点。 +#[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 CommandDenied { + /// 被拒的命令点位(`command.exec`)。句子由前端拼,宿主只给事实。 + pub(crate) command_id: String, +} + #[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 CommandFailed { pub(crate) exit_code: Option, + /// 脚本输出尾部原文。**不含**任何宿主拼的说明:省略了多少单独作为事实回给前端。 pub(crate) output_tail: String, + /// 为了让载荷留在 IPC 尺寸内,尾部之前省略了多少个字符;0 表示没截。 + pub(crate) omitted_characters: usize, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)] diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandDenied.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandDenied.ts new file mode 100644 index 000000000..6c1456800 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandDenied.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * 策略拒绝与宿主故障分开:前者是预期拒绝(面板给固定话术,不入错误池),后者是宿主侧事实故障 + * (面板给现场,并原样抛出进错误池)。ADR 要求顶层只放调用方要分流的类别,这里就是那个分流点。 + */ +export type CommandDenied = { +/** + * 被拒的命令点位(`command.exec`)。句子由前端拼,宿主只给事实。 + */ +commandId: string, }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandFailed.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandFailed.ts index f8f5a8f25..e857e6f27 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandFailed.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/CommandFailed.ts @@ -1,3 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CommandFailed = { exitCode: number | null, outputTail: string, }; +export type CommandFailed = { exitCode: number | null, +/** + * 脚本输出尾部原文。**不含**任何宿主拼的说明:省略了多少单独作为事实回给前端。 + */ +outputTail: string, +/** + * 为了让载荷留在 IPC 尺寸内,尾部之前省略了多少个字符;0 表示没截。 + */ +omittedCharacters: number, }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportError.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportError.ts index 4011afcbb..e90224f01 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportError.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportError.ts @@ -1,10 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ArtifactMissing } from "./ArtifactMissing"; import type { BuildScriptMissing } from "./BuildScriptMissing"; +import type { CommandDenied } from "./CommandDenied"; import type { CommandFailed } from "./CommandFailed"; import type { ExportUnavailable } from "./ExportUnavailable"; import type { FormInvalid } from "./FormInvalid"; import type { RegistryMalformed } from "./RegistryMalformed"; import type { SaveConflict } from "./SaveConflict"; -export type XHSMiniToolExportError = { "type": "registryMalformed" } & RegistryMalformed | { "type": "formInvalid" } & FormInvalid | { "type": "buildScriptMissing" } & BuildScriptMissing | { "type": "saveConflict" } & SaveConflict | { "type": "commandFailed" } & CommandFailed | { "type": "artifactMissing" } & ArtifactMissing | { "type": "exportUnavailable" } & ExportUnavailable; +export type XHSMiniToolExportError = { "type": "registryMalformed" } & RegistryMalformed | { "type": "formInvalid" } & FormInvalid | { "type": "buildScriptMissing" } & BuildScriptMissing | { "type": "saveConflict" } & SaveConflict | { "type": "commandDenied" } & CommandDenied | { "type": "commandFailed" } & CommandFailed | { "type": "artifactMissing" } & ArtifactMissing | { "type": "exportUnavailable" } & ExportUnavailable; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts index ec9a4b976..799c8a375 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRunResult.ts @@ -8,6 +8,10 @@ */ export type XHSMiniToolExportRunResult = { /** - * 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。 + * 构建输出尾部原文(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。 */ -outputTail: string, }; +outputTail: string, +/** + * 尾部之前省略了多少个字符;0 表示没截。说明文字由前端拼。 + */ +omittedCharacters: number, };