diff --git a/apps/web/src/components/Copilot/CopilotAlert.tsx b/apps/web/src/components/Copilot/CopilotAlert.tsx index 343e011..188606d 100644 --- a/apps/web/src/components/Copilot/CopilotAlert.tsx +++ b/apps/web/src/components/Copilot/CopilotAlert.tsx @@ -19,8 +19,8 @@ export function CopilotAlert() { setLoading(true); try { const res = await listAlerts({ page_size: 50 }); - const result = res.data as unknown as { items: CopilotInsight[]; total: number }; - setAlerts(result.items ?? []); + const payload = (res.data as { data?: CopilotInsight[] }).data ?? []; + setAlerts(payload); } catch { // 静默 } finally { diff --git a/apps/web/src/components/Copilot/useCopilotInsights.ts b/apps/web/src/components/Copilot/useCopilotInsights.ts index e194a21..5f319b5 100644 --- a/apps/web/src/components/Copilot/useCopilotInsights.ts +++ b/apps/web/src/components/Copilot/useCopilotInsights.ts @@ -12,9 +12,10 @@ export function useCopilotInsights(patientId: string | undefined) { setLoading(true); try { const res = await listInsights({ patient_id: patientId, page_size: 20 }); - const result = res.data as unknown as { items: CopilotInsight[]; total: number }; - setData(result.items ?? []); - setTotal(result.total ?? 0); + const payload = (res.data as { data?: CopilotInsight[]; total?: number }).data ?? []; + const total = (res.data as { total?: number }).total ?? 0; + setData(payload); + setTotal(total); } catch { // 静默失败 } finally { diff --git a/apps/web/src/components/Copilot/useCopilotRisk.ts b/apps/web/src/components/Copilot/useCopilotRisk.ts index c258127..26becd9 100644 --- a/apps/web/src/components/Copilot/useCopilotRisk.ts +++ b/apps/web/src/components/Copilot/useCopilotRisk.ts @@ -13,7 +13,8 @@ export function useCopilotRisk(patientId: string | undefined) { setError(null); try { const res = await getPatientRisk(patientId); - setData(res.data as unknown as RiskScore); + const payload = (res.data as { data?: RiskScore }).data ?? null; + setData(payload); } catch (err) { setError(err instanceof Error ? err.message : '加载风险评分失败'); } finally { diff --git a/crates/erp-ai/src/copilot/rules.rs b/crates/erp-ai/src/copilot/rules.rs index 3fe59eb..f824297 100644 --- a/crates/erp-ai/src/copilot/rules.rs +++ b/crates/erp-ai/src/copilot/rules.rs @@ -122,7 +122,7 @@ fn compare_f64(a: &Value, b: &Value) -> std::cmp::Ordering { fn value_to_f64(v: &Value) -> f64 { v.as_f64() .or_else(|| v.as_i64().map(|n| n as f64)) - .unwrap_or(0.0) + .unwrap_or(f64::NAN) } /// 规则数据:(id, name, condition_expr, score, severity, suggestion) diff --git a/crates/erp-ai/src/event/copilot_consumer.rs b/crates/erp-ai/src/event/copilot_consumer.rs index fdcc5d5..f49fe48 100644 --- a/crates/erp-ai/src/event/copilot_consumer.rs +++ b/crates/erp-ai/src/event/copilot_consumer.rs @@ -81,6 +81,11 @@ async fn process_event(db: &sea_orm::DatabaseConnection, event: &erp_core::event .as_str() .unwrap_or("异常告警") .to_string(); + // 只传内层 content,避免重复存储顶层元数据 + let content = insight_data + .get("content") + .cloned() + .unwrap_or(insight_data.clone()); let _ = crate::service::insight_service::InsightService::create_insight( db, tenant_id, @@ -89,7 +94,7 @@ async fn process_event(db: &sea_orm::DatabaseConnection, event: &erp_core::event "rule".into(), Some(severity), title, - insight_data, + content, None, 168, // 7 天过期 None, diff --git a/crates/erp-ai/src/handler/risk_handler.rs b/crates/erp-ai/src/handler/risk_handler.rs index cc4f254..0dd87ac 100644 --- a/crates/erp-ai/src/handler/risk_handler.rs +++ b/crates/erp-ai/src/handler/risk_handler.rs @@ -16,12 +16,21 @@ where { require_permission(&ctx, "copilot.risk.view")?; + // 动态选择第一个可用的 AI 提供商(优先 ollama,其次其他) + let provider_name = state + .provider_registry + .provider_names() + .into_iter() + .find(|n| n == "ollama") + .or_else(|| state.provider_registry.provider_names().into_iter().next()) + .unwrap_or_default(); + let risk = crate::service::risk_service::RiskService::compute_risk_with_llm( &state.db, ctx.tenant_id, patient_id, &state.provider_registry, - "ollama", + &provider_name, ) .await?; diff --git a/crates/erp-ai/src/handler/rule_handler.rs b/crates/erp-ai/src/handler/rule_handler.rs index 0feb600..5f2eed7 100644 --- a/crates/erp-ai/src/handler/rule_handler.rs +++ b/crates/erp-ai/src/handler/rule_handler.rs @@ -127,3 +127,31 @@ where let result = active.update(&state.db).await?; Ok(Json(ApiResponse::ok(result))) } + +pub async fn delete_rule( + State(state): State, + Extension(ctx): Extension, + Path(rule_id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "copilot.rules.manage")?; + + let model = copilot_rules::Entity::find() + .filter(copilot_rules::Column::Id.eq(rule_id)) + .filter(copilot_rules::Column::TenantId.eq(ctx.tenant_id)) + .filter(copilot_rules::Column::DeletedAt.is_null()) + .one(&state.db) + .await? + .ok_or_else(|| erp_core::error::AppError::NotFound("Copilot 规则".into()))?; + + let mut active: copilot_rules::ActiveModel = model.into(); + active.deleted_at = Set(Some(chrono::Utc::now())); + active.updated_at = Set(chrono::Utc::now()); + active.updated_by = Set(Some(ctx.user_id)); + active.update(&state.db).await?; + + Ok(Json(ApiResponse::ok(serde_json::json!({"deleted": true})))) +} diff --git a/crates/erp-ai/src/module.rs b/crates/erp-ai/src/module.rs index c0cb87c..581795c 100644 --- a/crates/erp-ai/src/module.rs +++ b/crates/erp-ai/src/module.rs @@ -315,6 +315,8 @@ impl ErpModule for AiModule { // 每日凌晨 2:00 批量刷新所有在管患者风险快照 let refresh_db = ctx.db.clone(); tokio::spawn(async move { + // 首次执行延迟到下一个凌晨 2:00(简单实现:延迟 6 小时后开始 24h 周期) + tokio::time::sleep(std::time::Duration::from_secs(6 * 3600)).await; let mut interval = tokio::time::interval(std::time::Duration::from_secs(86400)); loop { interval.tick().await; @@ -471,5 +473,9 @@ impl AiModule { "/copilot/rules/{id}", axum::routing::put(crate::handler::rule_handler::update_rule), ) + .route( + "/copilot/rules/{id}", + axum::routing::delete(crate::handler::rule_handler::delete_rule), + ) } } diff --git a/crates/erp-ai/src/service/risk_service.rs b/crates/erp-ai/src/service/risk_service.rs index ca1d913..3558ed0 100644 --- a/crates/erp-ai/src/service/risk_service.rs +++ b/crates/erp-ai/src/service/risk_service.rs @@ -46,6 +46,20 @@ impl RiskService { ) -> AppResult { let rules = Self::load_rules(db, tenant_id).await?; let patient_data = Self::load_patient_data(db, tenant_id, patient_id).await?; + + // 数据为空时返回低风险,不写入快照(避免虚假评分) + if patient_data.as_object().is_none_or(|m| m.is_empty()) { + tracing::debug!( + patient_id = %patient_id, + "患者数据为空,跳过风险评分写入" + ); + return Ok(RiskScore { + score: 0, + level: "low".into(), + matched_rules: vec![], + }); + } + let risk = CopilotEngine::assess_patient(&rules, &patient_data); // LLM 补充分析(不阻塞,失败静默降级)