From 2660f1afff697f6af69ac80e7486c329c33e8722 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 19 May 2026 00:54:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20Phase=202A-3=20=E9=9A=8F=E8=AE=BF?= =?UTF-8?q?=E9=A1=B5=20AI=20=E8=BE=85=E5=8A=A9=E7=94=9F=E6=88=90=E5=B0=8F?= =?UTF-8?q?=E7=BB=93=20=E2=80=94=20SSE=20=E7=AB=AF=E7=82=B9=20+=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnalysisType 新增 FollowUpSummary 变体(as_str/prompt_name) - HealthDataProvider 新增 get_follow_up_summary_data() + FollowUpSummaryDataDto - erp-health 实现随访数据查询(task + records + PII 解密) - 新增 /ai/analyze/follow-up-summary SSE 端点 - SanitizationService 新增 sanitize_follow_up_data() - 前端 analysisSse.ts/AiAnalysisCard 支持 follow-up-summary 类型 - FollowUpTaskList 操作列新增「AI 小结」按钮 --- apps/web/src/api/ai/analysisSse.ts | 4 +- apps/web/src/components/ai/AiAnalysisCard.tsx | 7 +- .../web/src/pages/health/FollowUpTaskList.tsx | 28 ++++++- crates/erp-ai/src/dto/mod.rs | 9 +++ crates/erp-ai/src/handler/mod.rs | 78 +++++++++++++++++++ crates/erp-ai/src/module.rs | 4 + crates/erp-ai/src/sanitization/mod.rs | 10 ++- crates/erp-ai/tests/agent_test.rs | 13 +++- crates/erp-core/src/health_provider.rs | 26 +++++++ crates/erp-health/src/health_provider_impl.rs | 57 ++++++++++++-- 10 files changed, 223 insertions(+), 13 deletions(-) diff --git a/apps/web/src/api/ai/analysisSse.ts b/apps/web/src/api/ai/analysisSse.ts index cecf879..4dee7af 100644 --- a/apps/web/src/api/ai/analysisSse.ts +++ b/apps/web/src/api/ai/analysisSse.ts @@ -1,9 +1,10 @@ -export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary'; +export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary' | 'follow-up-summary'; interface AnalyzeBody { report_id?: string; patient_id?: string; metrics?: string[]; + source_id?: string; } const ENDPOINT_MAP: Record = { @@ -11,6 +12,7 @@ const ENDPOINT_MAP: Record = { 'trends': '/ai/analyze/trends', 'checkup-plan': '/ai/analyze/checkup-plan', 'report-summary': '/ai/analyze/report-summary', + 'follow-up-summary': '/ai/analyze/follow-up-summary', }; export interface SseCallbacks { diff --git a/apps/web/src/components/ai/AiAnalysisCard.tsx b/apps/web/src/components/ai/AiAnalysisCard.tsx index ccfb8d6..03a9b6d 100644 --- a/apps/web/src/components/ai/AiAnalysisCard.tsx +++ b/apps/web/src/components/ai/AiAnalysisCard.tsx @@ -11,6 +11,7 @@ export interface AiAnalysisCardProps { triggerLabel?: string; permission?: string; metrics?: string[]; + taskId?: string; } type AnalysisState = 'idle' | 'loading' | 'success' | 'error'; @@ -21,6 +22,7 @@ export function AiAnalysisCard({ triggerLabel = 'AI 分析', permission = 'ai.analysis.manage', metrics, + taskId, }: AiAnalysisCardProps) { const [state, setState] = useState('idle'); const [content, setContent] = useState(''); @@ -38,6 +40,9 @@ export function AiAnalysisCard({ if (analysisType === 'trends' || analysisType === 'checkup-plan') { body.patient_id = sourceRef; } + if (analysisType === 'follow-up-summary') { + body.source_id = taskId || sourceRef; + } if (metrics) { body.metrics = metrics; } @@ -55,7 +60,7 @@ export function AiAnalysisCard({ setErrorMsg('分析请求失败'); setState('error'); } - }, [analysisType, sourceRef, metrics]); + }, [analysisType, sourceRef, metrics, taskId]); const handleReset = useCallback(() => { setState('idle'); diff --git a/apps/web/src/pages/health/FollowUpTaskList.tsx b/apps/web/src/pages/health/FollowUpTaskList.tsx index acae252..85ec68b 100644 --- a/apps/web/src/pages/health/FollowUpTaskList.tsx +++ b/apps/web/src/pages/health/FollowUpTaskList.tsx @@ -11,7 +11,7 @@ import { Space, Popconfirm, } from 'antd'; -import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-design/icons'; +import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; import { dayjs } from '../../utils/dayjs'; import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp'; @@ -26,6 +26,7 @@ import { formatDate, formatDateTime } from '../../utils/format'; import { usePaginatedData } from '../../hooks/usePaginatedData'; import { useApiRequest } from '../../hooks/useApiRequest'; import { useDictionary } from '../../hooks/useDictionary'; +import { AiAnalysisCard } from '../../components/ai/AiAnalysisCard'; const STATUS_OPTIONS = [ { value: 'pending', label: '待处理' }, @@ -117,6 +118,9 @@ export default function FollowUpTaskList() { const [assignForm] = Form.useForm(); const [assignTask, setAssignTask] = useState(null); + // AI summary state + const [aiSummaryTaskId, setAiSummaryTaskId] = useState(null); + // --- Handlers --- const handleTableChange = (pagination: TablePaginationConfig) => { refresh(pagination.current ?? 1); @@ -280,7 +284,7 @@ export default function FollowUpTaskList() { { title: '操作', key: 'actions', - width: 220, + width: 280, render: (_: unknown, record: FollowUpTask) => ( @@ -292,6 +296,14 @@ export default function FollowUpTaskList() { > 填写记录 +