导出链路按 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 crate::project::run_project_verification_with_commit_at;
|
||||||
|
|
||||||
use super::dto::XHSMiniToolExportRunResult;
|
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::layout::{artifact_path, artifact_relative_path, SCRIPT_NAME};
|
||||||
use super::script;
|
use super::script;
|
||||||
|
|
||||||
@@ -26,8 +26,17 @@ pub(crate) async fn run(root: &Path) -> Result<XHSMiniToolExportRunResult, XHSMi
|
|||||||
let Some(script) = script::find(root) else {
|
let Some(script) = script::find(root) else {
|
||||||
return Err(script::missing_error());
|
return Err(script::missing_error());
|
||||||
};
|
};
|
||||||
crate::project::enforce_project_permission_policy(root, PERMISSION_COMMAND_ID)
|
// 用 typed 拒绝而不是 `enforce_project_permission_policy` 的字符串版本:那个版本把
|
||||||
.map_err(XHSMiniToolExportError::unavailable)?;
|
// 「策略拒绝」与「策略读不出来」拼成同一句人话,调用方就分不出「预期拒绝」与「宿主故障」了。
|
||||||
|
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(
|
let result = run_project_verification_with_commit_at(
|
||||||
root,
|
root,
|
||||||
@@ -55,9 +64,11 @@ fn conclude(
|
|||||||
output: &str,
|
output: &str,
|
||||||
) -> Result<XHSMiniToolExportRunResult, XHSMiniToolExportError> {
|
) -> Result<XHSMiniToolExportRunResult, XHSMiniToolExportError> {
|
||||||
if timed_out || exit_code != Some(0) {
|
if timed_out || exit_code != Some(0) {
|
||||||
|
let (output_tail, omitted_characters) = tail(output);
|
||||||
return Err(XHSMiniToolExportError::CommandFailed(CommandFailed {
|
return Err(XHSMiniToolExportError::CommandFailed(CommandFailed {
|
||||||
exit_code,
|
exit_code,
|
||||||
output_tail: tail(output),
|
output_tail,
|
||||||
|
omitted_characters,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,21 +85,26 @@ fn conclude(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 成功也回输出尾部:`pack.mjs` 会把 zip 的条目数与体积打在这里,正是用户上传前要看的数字。
|
// 成功也回输出尾部:`pack.mjs` 会把 zip 的条目数与体积打在这里,正是用户上传前要看的数字。
|
||||||
|
let (output_tail, omitted_characters) = tail(output);
|
||||||
Ok(XHSMiniToolExportRunResult {
|
Ok(XHSMiniToolExportRunResult {
|
||||||
output_tail: tail(output),
|
output_tail,
|
||||||
|
omitted_characters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 只留尾部若干字符,并注明省略了多少。整份构建日志塞进 IPC 没有意义,还会把界面拖住。
|
/// 只留尾部若干字符:整份构建日志塞进 IPC 没有意义,还会把界面拖住。
|
||||||
fn tail(output: &str) -> String {
|
///
|
||||||
|
/// 返回(尾部原文, 省略了多少个字符)。省略量是**事实**,「省略前 N 个字符」那句话由前端拼——
|
||||||
|
/// 宿主不预拼用户可见文案,否则前端没法按自己的现场排版,也没法判断这段尾巴是否完整。
|
||||||
|
fn tail(output: &str) -> (String, usize) {
|
||||||
let trimmed = output.trim();
|
let trimmed = output.trim();
|
||||||
let characters: Vec<char> = trimmed.chars().collect();
|
let characters: Vec<char> = trimmed.chars().collect();
|
||||||
if characters.len() <= OUTPUT_TAIL_MAX_CHARS {
|
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 skipped = characters.len() - OUTPUT_TAIL_MAX_CHARS;
|
||||||
let tail: String = characters[skipped..].iter().collect();
|
let tail: String = characters[skipped..].iter().collect();
|
||||||
format!("…(省略前 {skipped} 个字符)\n{tail}")
|
(tail, skipped)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -117,16 +133,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keeps_short_output_as_is() {
|
fn keeps_short_output_as_is_and_omits_nothing() {
|
||||||
assert_eq!(tail(" 构建失败\n"), "构建失败");
|
assert_eq!(tail(" 构建失败\n"), ("构建失败".to_string(), 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 output: String = "前".repeat(OUTPUT_TAIL_MAX_CHARS) + "后";
|
||||||
let kept = tail(&output);
|
let (kept, omitted) = tail(&output);
|
||||||
|
// 载荷里只有原文与省略量两个事实:说明那句话由前端拼,宿主不写。
|
||||||
assert!(kept.ends_with('后'));
|
assert!(kept.ends_with('后'));
|
||||||
assert!(kept.contains("省略前 1 个字符"));
|
assert!(!kept.contains("省略"));
|
||||||
|
assert_eq!(omitted, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -152,6 +170,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(failure.exit_code, Some(1));
|
assert_eq!(failure.exit_code, Some(1));
|
||||||
assert_eq!(failure.output_tail, "vite 构建失败\n入口不存在");
|
assert_eq!(failure.output_tail, "vite 构建失败\n入口不存在");
|
||||||
|
assert_eq!(failure.omitted_characters, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -165,6 +184,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(failure.exit_code, None);
|
assert_eq!(failure.exit_code, None);
|
||||||
assert_eq!(failure.output_tail, "已取消");
|
assert_eq!(failure.output_tail, "已取消");
|
||||||
|
assert_eq!(failure.omitted_characters, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -202,6 +222,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("产物在就应当成功");
|
.expect("产物在就应当成功");
|
||||||
assert!(result.output_tail.contains("3 个条目"));
|
assert!(result.output_tail.contains("3 个条目"));
|
||||||
|
assert_eq!(result.omitted_characters, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 真起进程跑一遍宿主链路:解析 npm 包、走 `command.exec` 边界执行项目脚本、再按产物判据收尾。
|
/// 真起进程跑一遍宿主链路:解析 npm 包、走 `command.exec` 边界执行项目脚本、再按产物判据收尾。
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ pub(crate) struct XHSMiniToolExportState {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))]
|
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))]
|
||||||
pub(crate) struct XHSMiniToolExportRunResult {
|
pub(crate) struct XHSMiniToolExportRunResult {
|
||||||
/// 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。
|
/// 构建输出尾部原文(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。
|
||||||
pub(crate) output_tail: String,
|
pub(crate) output_tail: String,
|
||||||
|
/// 尾部之前省略了多少个字符;0 表示没截。说明文字由前端拼。
|
||||||
|
pub(crate) omitted_characters: usize,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub(crate) enum XHSMiniToolExportError {
|
|||||||
BuildScriptMissing(BuildScriptMissing),
|
BuildScriptMissing(BuildScriptMissing),
|
||||||
/// 保存时发现注册表被别人改过,且字段真的不同。绝不覆盖,交给用户逐字段选。
|
/// 保存时发现注册表被别人改过,且字段真的不同。绝不覆盖,交给用户逐字段选。
|
||||||
SaveConflict(SaveConflict),
|
SaveConflict(SaveConflict),
|
||||||
|
/// 项目权限策略拒绝执行 `command.exec`:用户/项目策略说了不让跑,不是缺陷。
|
||||||
|
CommandDenied(CommandDenied),
|
||||||
/// 脚本跑完没有正常收场(非零退出或超时)。
|
/// 脚本跑完没有正常收场(非零退出或超时)。
|
||||||
CommandFailed(CommandFailed),
|
CommandFailed(CommandFailed),
|
||||||
/// 脚本正常收场但产物不在。
|
/// 脚本正常收场但产物不在。
|
||||||
@@ -72,12 +74,25 @@ pub(crate) struct SaveConflict {
|
|||||||
pub(crate) content_hash: String,
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))]
|
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))]
|
||||||
pub(crate) struct CommandFailed {
|
pub(crate) struct CommandFailed {
|
||||||
pub(crate) exit_code: Option<i32>,
|
pub(crate) exit_code: Option<i32>,
|
||||||
|
/// 脚本输出尾部原文。**不含**任何宿主拼的说明:省略了多少单独作为事实回给前端。
|
||||||
pub(crate) output_tail: String,
|
pub(crate) output_tail: String,
|
||||||
|
/// 为了让载荷留在 IPC 尺寸内,尾部之前省略了多少个字符;0 表示没截。
|
||||||
|
pub(crate) omitted_characters: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, TS)]
|
#[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.
|
// 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.
|
// 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 { ArtifactMissing } from "./ArtifactMissing";
|
||||||
import type { BuildScriptMissing } from "./BuildScriptMissing";
|
import type { BuildScriptMissing } from "./BuildScriptMissing";
|
||||||
|
import type { CommandDenied } from "./CommandDenied";
|
||||||
import type { CommandFailed } from "./CommandFailed";
|
import type { CommandFailed } from "./CommandFailed";
|
||||||
import type { ExportUnavailable } from "./ExportUnavailable";
|
import type { ExportUnavailable } from "./ExportUnavailable";
|
||||||
import type { FormInvalid } from "./FormInvalid";
|
import type { FormInvalid } from "./FormInvalid";
|
||||||
import type { RegistryMalformed } from "./RegistryMalformed";
|
import type { RegistryMalformed } from "./RegistryMalformed";
|
||||||
import type { SaveConflict } from "./SaveConflict";
|
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 = {
|
export type XHSMiniToolExportRunResult = {
|
||||||
/**
|
/**
|
||||||
* 构建输出尾部(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。
|
* 构建输出尾部原文(stdout 与 stderr 合并)。打包脚本会把 zip 的条目数与体积打在这里。
|
||||||
*/
|
*/
|
||||||
outputTail: string, };
|
outputTail: string,
|
||||||
|
/**
|
||||||
|
* 尾部之前省略了多少个字符;0 表示没截。说明文字由前端拼。
|
||||||
|
*/
|
||||||
|
omittedCharacters: number, };
|
||||||
|
|||||||
Reference in New Issue
Block a user