diff --git a/crates/erp-ai/src/error.rs b/crates/erp-ai/src/error.rs index 5dff165..f3d4e35 100644 --- a/crates/erp-ai/src/error.rs +++ b/crates/erp-ai/src/error.rs @@ -31,6 +31,21 @@ pub enum AiError { #[error("数据库错误: {0}")] DbError(String), + + #[error("AI 配额已耗尽: {reason}")] + QuotaExhausted { tenant_id: uuid::Uuid, reason: String }, + + #[error("缓存错误: {0}")] + CacheError(String), + + #[error("知识库错误: {0}")] + KnowledgeError(String), + + #[error("分析队列错误: {0}")] + QueueError(String), + + #[error("AI 配置错误: {0}")] + ConfigError(String), } impl From for AppError { @@ -43,6 +58,7 @@ impl From for AppError { AppError::Internal(format!("AI 提供商 {p} 不可用")) } AiError::RateLimitExceeded => AppError::TooManyRequests, + AiError::QuotaExhausted { .. } => AppError::TooManyRequests, AiError::VersionMismatch => AppError::VersionMismatch, AiError::DbError(msg) => AppError::Internal(msg), other => AppError::Internal(other.to_string()), @@ -161,4 +177,54 @@ mod tests { other => panic!("期望 AppError::Internal,得到 {:?}", other), } } + + #[test] + fn quota_exhausted_maps_to_429() { + let err = AiError::QuotaExhausted { + tenant_id: uuid::Uuid::now_v7(), + reason: "monthly budget".into(), + }; + let app: AppError = err.into(); + assert!(matches!(app, AppError::TooManyRequests)); + } + + #[test] + fn cache_error_maps_to_internal() { + let err = AiError::CacheError("redis timeout".into()); + let app: AppError = err.into(); + match app { + AppError::Internal(msg) => assert!(msg.contains("redis timeout")), + other => panic!("期望 AppError::Internal,得到 {:?}", other), + } + } + + #[test] + fn knowledge_error_maps_to_internal() { + let err = AiError::KnowledgeError("rule not found".into()); + let app: AppError = err.into(); + match app { + AppError::Internal(msg) => assert!(msg.contains("rule not found")), + other => panic!("期望 AppError::Internal,得到 {:?}", other), + } + } + + #[test] + fn queue_error_maps_to_internal() { + let err = AiError::QueueError("queue full".into()); + let app: AppError = err.into(); + match app { + AppError::Internal(msg) => assert!(msg.contains("queue full")), + other => panic!("期望 AppError::Internal,得到 {:?}", other), + } + } + + #[test] + fn config_error_maps_to_internal() { + let err = AiError::ConfigError("missing provider".into()); + let app: AppError = err.into(); + match app { + AppError::Internal(msg) => assert!(msg.contains("missing provider")), + other => panic!("期望 AppError::Internal,得到 {:?}", other), + } + } }