From 2e555ca72a97346c4ad67335fc9b0b7014962cec Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 25 Apr 2026 14:01:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20erp-ai=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E9=9B=86=E6=88=90=20=E2=80=94=20Config/State/=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AiConfig 到 AppConfig - 新增 FromRef for AiState - 注册 AiModule 到 ModuleRegistry - 合并 AI protected routes - 修复 sync_module_permissions 只同步 health.% 的 bug Co-Authored-By: Claude Opus 4.7 --- crates/erp-server/Cargo.toml | 1 + crates/erp-server/config/default.toml | 10 ++++++++++ crates/erp-server/src/config.rs | 13 +++++++++++++ crates/erp-server/src/main.rs | 14 ++++++++++++-- crates/erp-server/src/state.rs | 28 +++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/erp-server/Cargo.toml b/crates/erp-server/Cargo.toml index 56c85cb..d84bcd8 100644 --- a/crates/erp-server/Cargo.toml +++ b/crates/erp-server/Cargo.toml @@ -28,6 +28,7 @@ erp-workflow.workspace = true erp-message.workspace = true erp-plugin.workspace = true erp-health.workspace = true +erp-ai.workspace = true anyhow.workspace = true uuid.workspace = true chrono.workspace = true diff --git a/crates/erp-server/config/default.toml b/crates/erp-server/config/default.toml index 1f226d6..a3a3afb 100644 --- a/crates/erp-server/config/default.toml +++ b/crates/erp-server/config/default.toml @@ -32,3 +32,13 @@ secret = "__MUST_SET_VIA_ENV__" [health] aes_key = "__MUST_SET_VIA_ENV__" hmac_key = "__MUST_SET_VIA_ENV__" + +[ai] +default_provider = "claude" +api_key = "" +base_url = "https://api.anthropic.com" +model = "claude-sonnet-4-6" +max_tokens = 2048 +temperature = 0.3 +cache_ttl_seconds = 604800 +rate_limit_patient_daily = 10 diff --git a/crates/erp-server/src/config.rs b/crates/erp-server/src/config.rs index 642f136..39bcdb3 100644 --- a/crates/erp-server/src/config.rs +++ b/crates/erp-server/src/config.rs @@ -11,6 +11,7 @@ pub struct AppConfig { pub cors: CorsConfig, pub wechat: WechatConfig, pub health: HealthConfig, + pub ai: AiConfig, } #[derive(Debug, Clone, Deserialize)] @@ -69,6 +70,18 @@ pub struct HealthConfig { pub hmac_key: String, } +#[derive(Debug, Clone, Deserialize)] +pub struct AiConfig { + pub default_provider: String, + pub api_key: String, + pub base_url: Option, + pub model: String, + pub max_tokens: u32, + pub temperature: f32, + pub cache_ttl_seconds: u64, + pub rate_limit_patient_daily: u32, +} + impl AppConfig { pub fn load() -> anyhow::Result { let config = config::Config::builder() diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 003da2d..03979fd 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -343,13 +343,22 @@ async fn main() -> anyhow::Result<()> { "Health module initialized" ); + // Initialize AI module + let ai_module = erp_ai::AiModule; + tracing::info!( + module = ai_module.name(), + version = ai_module.version(), + "AI module initialized" + ); + // Initialize module registry and register modules let registry = ModuleRegistry::new() .register(auth_module) .register(config_module) .register(workflow_module) .register(message_module) - .register(health_module); + .register(health_module) + .register(ai_module); tracing::info!( module_count = registry.modules().len(), "Modules registered" @@ -464,6 +473,7 @@ async fn main() -> anyhow::Result<()> { .merge(erp_message::MessageModule::protected_routes()) .merge(erp_plugin::module::PluginModule::protected_routes()) .merge(erp_health::HealthModule::protected_routes()) + .merge(erp_ai::AiModule::protected_routes()) .merge(handlers::audit_log::audit_log_router()) .layer(axum::middleware::from_fn_with_state( state.clone(), @@ -627,7 +637,7 @@ async fn sync_module_permissions( SELECT r.id, p.id, p.tenant_id, 'all', NOW(), NOW(), $1, $1, NULL, 1 FROM permissions p JOIN roles r ON r.code = 'admin' AND r.tenant_id = p.tenant_id AND r.deleted_at IS NULL - WHERE p.tenant_id = $2 AND p.code LIKE 'health.%' + WHERE p.tenant_id = $2 ON CONFLICT DO NOTHING"#, [system_user_id.into(), tenant_id.into()], )).await?; diff --git a/crates/erp-server/src/state.rs b/crates/erp-server/src/state.rs index a9b9e65..aa6327a 100644 --- a/crates/erp-server/src/state.rs +++ b/crates/erp-server/src/state.rs @@ -115,3 +115,31 @@ impl FromRef for erp_health::HealthState { } } } + +/// Allow erp-ai handlers to extract their required state. +impl FromRef for erp_ai::AiState { + fn from_ref(state: &AppState) -> Self { + let mut provider = erp_ai::provider::claude::ClaudeProvider::new( + state.config.ai.api_key.clone(), + ); + if let Some(ref base_url) = state.config.ai.base_url { + provider = provider.with_base_url(base_url.clone()); + } + let db = state.db.clone(); + let event_bus = state.event_bus.clone(); + + let analysis = std::sync::Arc::new( + erp_ai::service::analysis::AnalysisService::new(Box::new(provider), db.clone()), + ); + let prompt = std::sync::Arc::new(erp_ai::service::prompt::PromptService::new(db.clone())); + let usage = std::sync::Arc::new(erp_ai::service::usage::UsageService::new(db.clone())); + + Self { + db, + event_bus, + analysis, + prompt, + usage, + } + } +}