导出链路按 ADR 收口:策略拒绝与宿主故障分开,输出尾部只回事实
- 权限点位改用 typed 的 enforce_project_permission_policy_rejection:Denied 映射成新变体 commandDenied,PolicyUnavailable 仍映射 exportUnavailable,调用方才有「该不该上报」的分流依据 - CommandFailed 与 XHSMiniToolExportRunResult 增加 omittedCharacters;tail() 只回尾部原文,不再由宿主拼「省略前 N 个字符」这句用户可见文案 - 同步 ts-rs 绑定:新增 CommandDenied.ts,生成文件 117 → 118
This commit is contained in:
@@ -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<XHSMiniToolExportRunResult, XHSMi
|
||||
let Some(script) = script::find(root) else {
|
||||
return Err(script::missing_error());
|
||||
};
|
||||
crate::project::enforce_project_permission_policy(root, PERMISSION_COMMAND_ID)
|
||||
.map_err(XHSMiniToolExportError::unavailable)?;
|
||||
// 用 typed 拒绝而不是 `enforce_project_permission_policy` 的字符串版本:那个版本把
|
||||
// 「策略拒绝」与「策略读不出来」拼成同一句人话,调用方就分不出「预期拒绝」与「宿主故障」了。
|
||||
crate::project::enforce_project_permission_policy_rejection(root, PERMISSION_COMMAND_ID)
|
||||
.map_err(|rejection| match rejection {
|
||||
crate::agent::tool::error::ProjectPermissionRejection::Denied { command_id } => {
|
||||
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<XHSMiniToolExportRunResult, XHSMiniToolExportError> {
|
||||
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<char> = 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` 边界执行项目脚本、再按产物判据收尾。
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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<i32>,
|
||||
/// 脚本输出尾部原文。**不含**任何宿主拼的说明:省略了多少单独作为事实回给前端。
|
||||
pub(crate) output_tail: String,
|
||||
/// 为了让载荷留在 IPC 尺寸内,尾部之前省略了多少个字符;0 表示没截。
|
||||
pub(crate) omitted_characters: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||
|
||||
+11
@@ -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, };
|
||||
+9
-1
@@ -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, };
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+6
-2
@@ -8,6 +8,10 @@
|
||||
*/
|
||||
export type XHSMiniToolExportRunResult = {
|
||||
/**
|
||||
* 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。
|
||||
* 构建输出尾部原文(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。
|
||||
*/
|
||||
outputTail: string, };
|
||||
outputTail: string,
|
||||
/**
|
||||
* 尾部之前省略了多少个字符;0 表示没截。说明文字由前端拼。
|
||||
*/
|
||||
omittedCharacters: number, };
|
||||
|
||||
Reference in New Issue
Block a user