diff --git a/Cargo.lock b/Cargo.lock index b8e2595..b142971 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1411,10 +1411,12 @@ dependencies = [ "async-trait", "axum", "chrono", + "dashmap", "erp-core", "futures", "handlebars", "hex", + "redis", "reqwest", "sea-orm", "serde", diff --git a/crates/erp-server/config/default.toml b/crates/erp-server/config/default.toml index c704243..90df9c7 100644 --- a/crates/erp-server/config/default.toml +++ b/crates/erp-server/config/default.toml @@ -40,15 +40,23 @@ hmac_key = "__MUST_SET_VIA_ENV__" kek = "__MUST_SET_VIA_ENV__" [ai] -default_provider = "claude" +default_provider = "ollama" # AI API 密钥。留空则禁用 AI 功能;生产环境必须通过 ERP__AI__API_KEY 设置。 api_key = "" -model = "claude-sonnet-4-6" +model = "qwen3:4b" max_tokens = 2048 temperature = 0.3 cache_ttl_seconds = 604800 rate_limit_patient_daily = 10 +[ai.providers.ollama] +provider_type = "ollama" +base_url = "http://localhost:11434" +default_model = "qwen3:4b" +max_tokens = 2048 +temperature = 0.3 +is_enabled = true + [storage] upload_dir = "./uploads" max_file_size = "10MB" diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index 71cf3e0..3fe6fce 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -511,16 +511,43 @@ async fn main() -> anyhow::Result<()> { tracing::info!(providers = ?registry.provider_names(), "AI Provider 注册完成"); - // 构建默认 provider 用于 AnalysisService(保持 Box 签名) - let mut default_claude = erp_ai::provider::claude::ClaudeProvider::new( - config.ai.api_key.clone(), - ); - if let Some(ref base_url) = config.ai.base_url { - default_claude = default_claude.with_base_url(base_url.clone()); - } + // 根据 default_provider 配置构建 AnalysisService 的默认 provider + let default_provider: Box = + match config.ai.default_provider.as_str() { + "ollama" => { + let pcfg = config.ai.providers.get("ollama"); + let base_url = pcfg.and_then(|c| c.base_url.clone()) + .unwrap_or_else(|| "http://localhost:11434".to_string()); + let model = pcfg.map(|c| c.default_model.clone()) + .unwrap_or_else(|| config.ai.model.clone()); + tracing::info!(base_url = %base_url, model = %model, "AnalysisService 使用 Ollama 提供商"); + Box::new(erp_ai::provider::ollama::OllamaProvider::new(base_url, model)) + } + "openai" => { + let pcfg = config.ai.providers.get("openai"); + let api_key = pcfg.and_then(|c| c.api_key_env.as_ref()) + .and_then(|env| std::env::var(env).ok()) + .unwrap_or_default(); + let base_url = pcfg.and_then(|c| c.base_url.clone()) + .unwrap_or_else(|| "https://api.openai.com".to_string()); + let model = pcfg.map(|c| c.default_model.clone()) + .unwrap_or_else(|| config.ai.model.clone()); + Box::new(erp_ai::provider::openai::OpenAIProvider::new(api_key, base_url, model)) + } + _ => { + // 默认 Claude + let mut claude = erp_ai::provider::claude::ClaudeProvider::new( + config.ai.api_key.clone(), + ); + if let Some(ref base_url) = config.ai.base_url { + claude = claude.with_base_url(base_url.clone()); + } + Box::new(claude) + } + }; let analysis_svc = erp_ai::service::analysis::AnalysisService::new( - Box::new(default_claude), + default_provider, db.clone(), ).with_knowledge_source(std::sync::Arc::new( erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(db.clone()),