Files
hms/crates/erp-ai/src/error.rs

235 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use erp_core::error::AppError;
#[derive(Debug, thiserror::Error)]
pub enum AiError {
#[error("验证失败: {0}")]
Validation(String),
#[error("分析未找到: {0}")]
AnalysisNotFound(String),
#[error("Prompt 模板未找到: {0}")]
PromptNotFound(String),
#[error("AI 提供商不可用: {0}")]
ProviderUnavailable(String),
#[error("AI 提供商错误: {0}")]
ProviderError(String),
#[error("数据脱敏失败: {0}")]
SanitizationError(String),
#[error("模板渲染失败: {0}")]
TemplateError(String),
#[error("速率超限")]
RateLimitExceeded,
#[error("版本不匹配")]
VersionMismatch,
#[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),
#[error("不支持的操作: {0}")]
UnsupportedOperation(String),
}
impl From<AiError> for AppError {
fn from(e: AiError) -> Self {
match e {
AiError::Validation(msg) => AppError::Validation(msg),
AiError::AnalysisNotFound(id) => AppError::NotFound(format!("分析结果: {id}")),
AiError::PromptNotFound(name) => AppError::NotFound(format!("Prompt 模板: {name}")),
AiError::ProviderUnavailable(p) => 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()),
}
}
}
impl From<sea_orm::DbErr> for AiError {
fn from(e: sea_orm::DbErr) -> Self {
AiError::DbError(e.to_string())
}
}
pub type AiResult<T> = Result<T, AiError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_maps_to_app_error_validation() {
let err = AiError::Validation("字段缺失".to_string());
let app: AppError = err.into();
match app {
AppError::Validation(msg) => assert!(msg.contains("字段缺失")),
other => panic!("期望 AppError::Validation得到 {:?}", other),
}
}
#[test]
fn analysis_not_found_maps_to_not_found() {
let err = AiError::AnalysisNotFound("abc-123".to_string());
let app: AppError = err.into();
match app {
AppError::NotFound(msg) => assert!(msg.contains("分析结果")),
other => panic!("期望 AppError::NotFound得到 {:?}", other),
}
}
#[test]
fn prompt_not_found_maps_to_not_found() {
let err = AiError::PromptNotFound("lab_report_interpretation".to_string());
let app: AppError = err.into();
match app {
AppError::NotFound(msg) => assert!(msg.contains("Prompt 模板")),
other => panic!("期望 AppError::NotFound得到 {:?}", other),
}
}
#[test]
fn provider_unavailable_maps_to_internal() {
let err = AiError::ProviderUnavailable("Claude".to_string());
let app: AppError = err.into();
match app {
AppError::Internal(msg) => assert!(msg.contains("Claude")),
other => panic!("期望 AppError::Internal得到 {:?}", other),
}
}
#[test]
fn provider_error_maps_to_internal() {
let err = AiError::ProviderError("超时".to_string());
let app: AppError = err.into();
match app {
AppError::Internal(msg) => assert!(msg.contains("超时")),
other => panic!("期望 AppError::Internal得到 {:?}", other),
}
}
#[test]
fn sanitization_error_maps_to_internal() {
let err = AiError::SanitizationError("PII 泄漏".to_string());
let app: AppError = err.into();
match app {
AppError::Internal(msg) => assert!(msg.contains("PII 泄漏")),
other => panic!("期望 AppError::Internal得到 {:?}", other),
}
}
#[test]
fn template_error_maps_to_internal() {
let err = AiError::TemplateError("语法错误".to_string());
let app: AppError = err.into();
match app {
AppError::Internal(msg) => assert!(msg.contains("语法错误")),
other => panic!("期望 AppError::Internal得到 {:?}", other),
}
}
#[test]
fn rate_limit_maps_to_too_many_requests() {
let err = AiError::RateLimitExceeded;
let app: AppError = err.into();
match app {
AppError::TooManyRequests => {}
other => panic!("期望 AppError::TooManyRequests得到 {:?}", other),
}
}
#[test]
fn version_mismatch_maps_directly() {
let err = AiError::VersionMismatch;
let app: AppError = err.into();
match app {
AppError::VersionMismatch => {}
other => panic!("期望 AppError::VersionMismatch得到 {:?}", other),
}
}
#[test]
fn db_error_maps_to_internal() {
let err = AiError::DbError("连接失败".to_string());
let app: AppError = err.into();
match app {
AppError::Internal(msg) => assert!(msg.contains("连接失败")),
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),
}
}
}