diff --git a/Cargo.toml b/Cargo.toml index 72c801a..7dd1ed6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/erp-plugin-freelance", "crates/erp-plugin-itops", "crates/erp-health", + "crates/erp-ai", ] [workspace.package] @@ -81,7 +82,7 @@ validator = { version = "0.19", features = ["derive"] } async-trait = "0.1" # HTTP client -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.12", features = ["json", "stream"] } # Crypto aes = "0.8" @@ -100,3 +101,12 @@ erp-message = { path = "crates/erp-message" } erp-config = { path = "crates/erp-config" } erp-plugin = { path = "crates/erp-plugin" } erp-health = { path = "crates/erp-health" } +erp-ai = { path = "crates/erp-ai" } + +# Async streaming +futures = "0.3" +tokio-stream = "0.1" +async-stream = "0.3" + +# Template engine +handlebars = "6" diff --git a/crates/erp-ai/Cargo.toml b/crates/erp-ai/Cargo.toml new file mode 100644 index 0000000..a483f9b --- /dev/null +++ b/crates/erp-ai/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "erp-ai" +version.workspace = true +edition.workspace = true + +[dependencies] +erp-core.workspace = true +tokio = { workspace = true, features = ["full"] } +tokio-stream.workspace = true +futures.workspace = true +async-stream.workspace = true +serde.workspace = true +serde_json.workspace = true +uuid.workspace = true +chrono.workspace = true +axum.workspace = true +sea-orm.workspace = true +tracing.workspace = true +thiserror.workspace = true +utoipa.workspace = true +async-trait.workspace = true +reqwest = { version = "0.12", features = ["stream", "json"] } +handlebars = "6" +sha2 = "0.10" +hex = "0.4" diff --git a/crates/erp-ai/src/error.rs b/crates/erp-ai/src/error.rs new file mode 100644 index 0000000..a4f419a --- /dev/null +++ b/crates/erp-ai/src/error.rs @@ -0,0 +1,59 @@ +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), +} + +impl From 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::VersionMismatch => AppError::VersionMismatch, + AiError::DbError(msg) => AppError::Internal(msg), + other => AppError::Internal(other.to_string()), + } + } +} + +impl From for AiError { + fn from(e: sea_orm::DbErr) -> Self { + AiError::DbError(e.to_string()) + } +} + +pub type AiResult = Result; diff --git a/crates/erp-ai/src/lib.rs b/crates/erp-ai/src/lib.rs new file mode 100644 index 0000000..5f8857b --- /dev/null +++ b/crates/erp-ai/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; + +pub use error::{AiError, AiResult};