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 8106613e5..c0caad70e 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 @@ -192,7 +192,9 @@ macro_rules! impl_from_registry_error { fn from(error: crate::export::RegistryError) -> Self { match error { crate::export::RegistryError::Malformed { cause, .. } => { - Self::RegistryMalformed(RegistryMalformed { cause }) + Self::RegistryMalformed(RegistryMalformed { + cause: cause.to_string(), + }) } other => Self::ExportUnavailable(ExportUnavailable::new(other.to_string())), } diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/registry.rs b/apps/ai-game-creator-shell/src-tauri/src/export/registry.rs index 2a777f71a..96460f7d7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/registry.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/registry.rs @@ -24,15 +24,103 @@ pub(crate) struct Loaded { pub(crate) created: bool, } +/// 注册表失败的底层现场。 +/// +/// 能拿到底层错误类型时(`io` / `serde`)就把类型带出来,这样 Rust 侧调用方可以按 +/// `std::io::ErrorKind` 或 serde 错误分流,日志也能沿 `source()` 追到根因;只有本模块自己拼 +/// 出来的一句话(例如「注册表路径缺少父目录」)才退化成 [`RegistryErrorCause::Message`]。 +/// `context` 是给原始错误补的中文说明,只影响展示,不影响类型。 +#[derive(Debug)] +pub(crate) enum RegistryErrorCause { + Io { + context: Option<&'static str>, + error: std::io::Error, + }, + Serde { + context: Option<&'static str>, + error: serde_json::Error, + }, + Message(String), +} + +impl RegistryErrorCause { + fn message(message: impl Into) -> Self { + Self::Message(message.into()) + } + + fn io(error: std::io::Error) -> Self { + Self::Io { + context: None, + error, + } + } + + fn io_with_context(context: &'static str, error: std::io::Error) -> Self { + Self::Io { + context: Some(context), + error, + } + } + + fn serde(error: serde_json::Error) -> Self { + Self::Serde { + context: None, + error, + } + } +} + +impl fmt::Display for RegistryErrorCause { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io { context, error } => match context { + Some(context) => write!(formatter, "{context}:{error}"), + None => write!(formatter, "{error}"), + }, + Self::Serde { context, error } => match context { + Some(context) => write!(formatter, "{context}:{error}"), + None => write!(formatter, "{error}"), + }, + Self::Message(message) => formatter.write_str(message), + } + } +} + +impl std::error::Error for RegistryErrorCause { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io { error, .. } => Some(error), + Self::Serde { error, .. } => Some(error), + Self::Message(_) => None, + } + } +} + /// 注册表文件级失败。目标模块负责把它映射成自己的 typed error。 #[derive(Debug)] pub(crate) enum RegistryError { /// 路径不可读写(含父目录创建失败)。 - Io { path: PathBuf, cause: String }, + Io { + path: PathBuf, + cause: RegistryErrorCause, + }, /// 文件内容不是本目标期望的形状。fail closed:不猜测、不修补。 - Malformed { path: PathBuf, cause: String }, + Malformed { + path: PathBuf, + cause: RegistryErrorCause, + }, /// 宿主自己的 DTO 序列化失败,属于编程错误。 - Encode { cause: String }, + Encode { cause: RegistryErrorCause }, +} + +impl std::error::Error for RegistryError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io { cause, .. } | Self::Malformed { cause, .. } | Self::Encode { cause } => { + Some(cause) + } + } + } } impl fmt::Display for RegistryError { @@ -83,13 +171,13 @@ where // 被报成「路径 / 权限故障」,把用户和 agent 往修权限的方向引。 return Err(RegistryError::Malformed { path: path.to_path_buf(), - cause: format!("注册表不是有效 UTF-8:{error}"), + cause: RegistryErrorCause::io_with_context("注册表不是有效 UTF-8", error), }); } Err(error) => { return Err(RegistryError::Io { path: path.to_path_buf(), - cause: error.to_string(), + cause: RegistryErrorCause::io(error), }); } }; @@ -111,7 +199,7 @@ pub(crate) fn save(path: &Path, value: &T) -> Result(value: &T) -> Result { let mut text = serde_json::to_string_pretty(value).map_err(|error| RegistryError::Encode { - cause: error.to_string(), + cause: RegistryErrorCause::serde(error), })?; text.push('\n'); Ok(text) @@ -120,7 +208,7 @@ fn encode(value: &T) -> Result { fn decode(path: &Path, text: &str) -> Result { serde_json::from_str(text).map_err(|error| RegistryError::Malformed { path: path.to_path_buf(), - cause: error.to_string(), + cause: RegistryErrorCause::serde(error), }) } @@ -131,11 +219,11 @@ fn decode(path: &Path, text: &str) -> Result Result<(), RegistryError> { let parent = path.parent().ok_or_else(|| RegistryError::Io { path: path.to_path_buf(), - cause: "注册表路径缺少父目录".to_string(), + cause: RegistryErrorCause::message("注册表路径缺少父目录"), })?; fs::create_dir_all(parent).map_err(|error| RegistryError::Io { path: path.to_path_buf(), - cause: format!("创建导出目录失败:{error}"), + cause: RegistryErrorCause::io_with_context("创建导出目录失败", error), })?; let temp = temp_path(path); let write = || -> std::io::Result<()> { @@ -153,14 +241,14 @@ fn write_atomically(path: &Path, text: &str) -> Result<(), RegistryError> { let _ = fs::remove_file(&temp); return Err(RegistryError::Io { path: path.to_path_buf(), - cause: error.to_string(), + cause: RegistryErrorCause::io(error), }); } fs::rename(&temp, path).map_err(|error| { let _ = fs::remove_file(&temp); RegistryError::Io { path: path.to_path_buf(), - cause: error.to_string(), + cause: RegistryErrorCause::io(error), } })?; // rename 只改目录条目:不 fsync 父目录的话,崩溃后「哪个文件才是新的」这件事可能没落盘, @@ -292,4 +380,24 @@ mod tests { assert_eq!(loaded.value, value); assert_eq!(loaded.content_hash, hash); } + + #[test] + fn error_keeps_the_underlying_io_kind_for_rust_callers() { + let error = RegistryError::Io { + path: PathBuf::from("/tmp/registry.json"), + cause: RegistryErrorCause::io_with_context( + "创建导出目录失败", + std::io::Error::from(std::io::ErrorKind::PermissionDenied), + ), + }; + + // 展示文案保持原样,Rust 侧则能沿 source() 一路拿回 io::Error 并按 ErrorKind 分流。 + assert!(error.to_string().contains("创建导出目录失败")); + let cause = std::error::Error::source(&error).expect("cause"); + let io = cause + .source() + .and_then(|source| source.downcast_ref::()) + .expect("io source"); + assert_eq!(io.kind(), std::io::ErrorKind::PermissionDenied); + } }