feat(ai): AI 健康管家 V2 基础设施 — 功能开关 + 角色沙箱准备 + 体征页 AI 趋势分析
- 迁移 000153: 新增 ai_feature_flags / ai_usage_daily / ai_suggestion_feedback 三张表, ai_tenant_configs 增加 billing_enabled 列, seed 12 个功能开关 + 2 个管理权限码 - 新增 FeatureFlagService: 5 分钟缓存 + DB 回退 + 即时更新 - VitalSignsTab 添加 AI 趋势分析按钮 (SSE 流式) - 新增 3 个 Entity (ai_feature_flags / ai_usage_daily / ai_suggestion_feedback) - AiState 扩展 feature_flags 字段 - 设计规格 + 讨论记录文档 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -540,56 +540,35 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(providers = ?registry.provider_names(), "AI Provider 注册完成");
|
||||
tracing::info!(providers = ?registry.provider_names(), "AI Provider 注册完成(静态配置)");
|
||||
|
||||
// 根据 default_provider 配置构建 AnalysisService 的默认 provider
|
||||
let default_provider: Box<dyn erp_ai::provider::AiProvider> = match config
|
||||
.ai
|
||||
.default_provider
|
||||
.as_str()
|
||||
// 尝试从 DB 加载 provider 配置覆盖静态值(DB 为空时 fallback 到静态配置)
|
||||
{
|
||||
"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());
|
||||
let tenant_id =
|
||||
match std::env::var("DEFAULT_TENANT_ID").or_else(|_| std::env::var("TENANT_ID")) {
|
||||
Ok(id) => uuid::Uuid::parse_str(&id).ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
if let Some(tid) = tenant_id {
|
||||
let values = erp_ai::config_resolver::load_ai_config_raw(tid, &db).await;
|
||||
if !values.is_empty() {
|
||||
let kek = *erp_core::crypto::PiiCrypto::dev_default().kek();
|
||||
match registry.reload_providers(&values, &kek).await {
|
||||
Ok(()) => {
|
||||
tracing::info!(providers = ?registry.provider_names(), "AI Provider 已从 DB 重新加载");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "DB Provider 加载失败,继续使用静态配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
Box::new(claude)
|
||||
} else {
|
||||
tracing::info!("未配置 DEFAULT_TENANT_ID,跳过 DB Provider 加载");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let analysis_svc =
|
||||
erp_ai::service::analysis::AnalysisService::new(default_provider, db.clone())
|
||||
erp_ai::service::analysis::AnalysisService::new(registry.clone(), db.clone())
|
||||
.with_knowledge_source(std::sync::Arc::new(
|
||||
erp_ai::knowledge::structured_source::StructuredKnowledgeSource::new(
|
||||
db.clone(),
|
||||
@@ -627,6 +606,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
cache,
|
||||
risk_service: std::sync::Arc::new(erp_ai::service::risk_service::RiskService),
|
||||
insight_service: std::sync::Arc::new(erp_ai::service::insight_service::InsightService),
|
||||
feature_flags: std::sync::Arc::new(
|
||||
erp_ai::service::feature_flag_service::FeatureFlagService::new(db.clone()),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user