diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index e0c18bf..7ffe36d 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -46,7 +46,6 @@ const AiPromptList = lazy(() => import('./pages/health/AiPromptList'));
const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList'));
const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard'));
const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage'));
-const AiKnowledgePage = lazy(() => import('./pages/health/AiKnowledgePage'));
const KnowledgeV2Page = lazy(() => import('./pages/ai/KnowledgeV2Page'));
const AiChatPage = lazy(() => import('./pages/ai/ChatPage'));
const AlertList = lazy(() => import('./pages/health/AlertList'));
@@ -258,7 +257,7 @@ export default function App() {
"/health/follow-up-records", "/health/consultations",
"/health/points-rules", "/health/points-products", "/health/points-orders",
"/health/offline-events", "/health/ai-prompts", "/health/ai-analysis",
- "/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/ai-knowledge-v2", "/health/alerts", "/health/alert-dashboard",
+ "/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/alerts", "/health/alert-dashboard",
"/ai/chat",
"/health/alert-rules", "/health/devices", "/health/realtime-monitor",
"/health/oauth-clients", "/health/dialysis", "/health/action-inbox",
@@ -331,8 +330,7 @@ export default function App() {
} />
} />
} />
- } />
- } />
+ } />
} />
} />
} />
diff --git a/apps/web/src/api/ai/knowledge.ts b/apps/web/src/api/ai/knowledge.ts
deleted file mode 100644
index 40bf9c0..0000000
--- a/apps/web/src/api/ai/knowledge.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import client from '../client';
-
-// === Types ===
-
-export interface KnowledgeReference {
- id: string;
- tenant_id: string;
- title: string;
- analysis_type: string;
- source_name: string;
- content_summary: string;
- tags: Record | null;
- is_enabled: boolean;
- created_at: string;
- updated_at: string;
-}
-
-export interface KnowledgeGuide {
- id: string;
- tenant_id: string;
- title: string;
- analysis_type: string;
- content: string;
- category: string | null;
- is_enabled: boolean;
- created_at: string;
- updated_at: string;
-}
-
-export interface CreateReferenceReq {
- title: string;
- analysis_type: string;
- source_name: string;
- content_summary: string;
- tags?: Record;
- is_enabled?: boolean;
-}
-
-export interface UpdateReferenceReq {
- title?: string;
- analysis_type?: string;
- source_name?: string;
- content_summary?: string;
- tags?: Record;
- is_enabled?: boolean;
-}
-
-export interface CreateGuideReq {
- title: string;
- analysis_type: string;
- content: string;
- category?: string;
- is_enabled?: boolean;
-}
-
-export interface UpdateGuideReq {
- title?: string;
- analysis_type?: string;
- content?: string;
- category?: string;
- is_enabled?: boolean;
-}
-
-// === API ===
-
-export const knowledgeApi = {
- // References
- listReferences: async (params?: { analysis_type?: string }) => {
- const resp = await client.get('/ai/knowledge/references', { params });
- return resp.data.data as { data: KnowledgeReference[]; total: number };
- },
- createReference: async (data: CreateReferenceReq) => {
- const resp = await client.post('/ai/knowledge/references', data);
- return resp.data.data as { id: string };
- },
- updateReference: async (id: string, data: UpdateReferenceReq) => {
- const resp = await client.put(`/ai/knowledge/references/${id}`, data);
- return resp.data.data as { id: string };
- },
- deleteReference: async (id: string) => {
- const resp = await client.delete(`/ai/knowledge/references/${id}`);
- return resp.data.data as { id: string };
- },
- reEmbedReference: async (id: string) => {
- const resp = await client.post(`/ai/knowledge/references/${id}/re-embed`);
- return resp.data.data as { id: string };
- },
-
- // Guides
- listGuides: async (params?: { analysis_type?: string }) => {
- const resp = await client.get('/ai/knowledge/guides', { params });
- return resp.data.data as { data: KnowledgeGuide[]; total: number };
- },
- createGuide: async (data: CreateGuideReq) => {
- const resp = await client.post('/ai/knowledge/guides', data);
- return resp.data.data as { id: string };
- },
- updateGuide: async (id: string, data: UpdateGuideReq) => {
- const resp = await client.put(`/ai/knowledge/guides/${id}`, data);
- return resp.data.data as { id: string };
- },
- deleteGuide: async (id: string) => {
- const resp = await client.delete(`/ai/knowledge/guides/${id}`);
- return resp.data.data as { id: string };
- },
- reEmbedGuide: async (id: string) => {
- const resp = await client.post(`/ai/knowledge/guides/${id}/re-embed`);
- return resp.data.data as { id: string };
- },
-};
diff --git a/apps/web/src/pages/health/AiKnowledgePage.tsx b/apps/web/src/pages/health/AiKnowledgePage.tsx
deleted file mode 100644
index 418ecb7..0000000
--- a/apps/web/src/pages/health/AiKnowledgePage.tsx
+++ /dev/null
@@ -1,508 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-import {
- Card,
- Table,
- Button,
- Space,
- Modal,
- Form,
- Input,
- Select,
- Switch,
- message,
- Popconfirm,
- Tabs,
- Tag,
- Tooltip,
-} from 'antd';
-import {
- PlusOutlined,
- EditOutlined,
- DeleteOutlined,
- ReloadOutlined,
- ThunderboltOutlined,
-} from '@ant-design/icons';
-import {
- knowledgeApi,
- type KnowledgeReference,
- type KnowledgeGuide,
- type CreateReferenceReq,
- type UpdateReferenceReq,
- type CreateGuideReq,
- type UpdateGuideReq,
-} from '../../api/ai/knowledge';
-import { AuthButton } from '../../components/AuthButton';
-
-const ANALYSIS_TYPES = [
- { value: 'lab_report', label: '化验报告' },
- { value: 'trend', label: '趋势分析' },
- { value: 'report_summary', label: '报告摘要' },
- { value: 'dialysis_risk', label: '透析风险' },
- { value: 'checkup_plan', label: '体检计划' },
- { value: 'follow_up', label: '随访总结' },
-];
-
-export default function AiKnowledgePage() {
- return (
-
- },
- { key: 'guides', label: '临床指南', children: },
- ]}
- />
-
- );
-}
-
-// === References Tab ===
-
-function ReferencesTab() {
- const [data, setData] = useState([]);
- const [loading, setLoading] = useState(false);
- const [modalOpen, setModalOpen] = useState(false);
- const [editing, setEditing] = useState(null);
- const [filterType, setFilterType] = useState();
- const [form] = Form.useForm();
-
- const fetchData = useCallback(async () => {
- setLoading(true);
- try {
- const result = await knowledgeApi.listReferences(
- filterType ? { analysis_type: filterType } : undefined,
- );
- setData(result.data);
- } catch {
- message.error('加载参考资料失败');
- } finally {
- setLoading(false);
- }
- }, [filterType]);
-
- useEffect(() => {
- fetchData();
- }, [fetchData]);
-
- const openCreate = () => {
- setEditing(null);
- form.resetFields();
- form.setFieldsValue({ is_enabled: true });
- setModalOpen(true);
- };
-
- const openEdit = (record: KnowledgeReference) => {
- setEditing(record);
- form.setFieldsValue({
- title: record.title,
- analysis_type: record.analysis_type,
- source_name: record.source_name,
- content_summary: record.content_summary,
- is_enabled: record.is_enabled,
- });
- setModalOpen(true);
- };
-
- const handleSubmit = async () => {
- const values = await form.validateFields();
- try {
- if (editing) {
- const req: UpdateReferenceReq = {
- title: values.title,
- analysis_type: values.analysis_type,
- source_name: values.source_name,
- content_summary: values.content_summary,
- is_enabled: values.is_enabled,
- };
- await knowledgeApi.updateReference(editing.id, req);
- message.success('更新成功');
- } else {
- const req: CreateReferenceReq = {
- title: values.title,
- analysis_type: values.analysis_type,
- source_name: values.source_name,
- content_summary: values.content_summary,
- is_enabled: values.is_enabled,
- };
- await knowledgeApi.createReference(req);
- message.success('创建成功');
- }
- setModalOpen(false);
- fetchData();
- } catch {
- message.error(editing ? '更新失败' : '创建失败');
- }
- };
-
- const handleDelete = async (id: string) => {
- try {
- await knowledgeApi.deleteReference(id);
- message.success('删除成功');
- fetchData();
- } catch {
- message.error('删除失败');
- }
- };
-
- const handleReEmbed = async (id: string) => {
- try {
- await knowledgeApi.reEmbedReference(id);
- message.success('向量重新生成已触发');
- } catch {
- message.error('向量重新生成失败');
- }
- };
-
- const columns = [
- { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
- {
- title: '分析类型',
- dataIndex: 'analysis_type',
- key: 'analysis_type',
- width: 120,
- render: (v: string) => {
- const found = ANALYSIS_TYPES.find((t) => t.value === v);
- return {found?.label ?? v};
- },
- },
- { title: '来源', dataIndex: 'source_name', key: 'source_name', width: 150, ellipsis: true },
- {
- title: '状态',
- dataIndex: 'is_enabled',
- key: 'is_enabled',
- width: 80,
- render: (v: boolean) => (
- {v ? '启用' : '禁用'}
- ),
- },
- {
- title: '更新时间',
- dataIndex: 'updated_at',
- key: 'updated_at',
- width: 170,
- render: (v: string) => (v ? new Date(v).toLocaleString() : '-'),
- },
- {
- title: '操作',
- key: 'actions',
- width: 200,
- render: (_: unknown, record: KnowledgeReference) => (
-
-
- } onClick={() => openEdit(record)} />
-
-
- }
- onClick={() => handleReEmbed(record.id)}
- />
-
- handleDelete(record.id)}
- okText="删除"
- cancelText="取消"
- >
- } />
-
-
- ),
- },
- ];
-
- return (
- <>
-
-
-
- } onClick={openCreate}>
- 新增参考资料
-
-
- } onClick={fetchData}>
- 刷新
-
-
-
- `共 ${total} 条` }}
- size="small"
- />
-
- setModalOpen(false)}
- width={600}
- destroyOnClose
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-// === Guides Tab ===
-
-function GuidesTab() {
- const [data, setData] = useState([]);
- const [loading, setLoading] = useState(false);
- const [modalOpen, setModalOpen] = useState(false);
- const [editing, setEditing] = useState(null);
- const [filterType, setFilterType] = useState();
- const [form] = Form.useForm();
-
- const fetchData = useCallback(async () => {
- setLoading(true);
- try {
- const result = await knowledgeApi.listGuides(
- filterType ? { analysis_type: filterType } : undefined,
- );
- setData(result.data);
- } catch {
- message.error('加载临床指南失败');
- } finally {
- setLoading(false);
- }
- }, [filterType]);
-
- useEffect(() => {
- fetchData();
- }, [fetchData]);
-
- const openCreate = () => {
- setEditing(null);
- form.resetFields();
- form.setFieldsValue({ is_enabled: true });
- setModalOpen(true);
- };
-
- const openEdit = (record: KnowledgeGuide) => {
- setEditing(record);
- form.setFieldsValue({
- title: record.title,
- analysis_type: record.analysis_type,
- content: record.content,
- category: record.category,
- is_enabled: record.is_enabled,
- });
- setModalOpen(true);
- };
-
- const handleSubmit = async () => {
- const values = await form.validateFields();
- try {
- if (editing) {
- const req: UpdateGuideReq = {
- title: values.title,
- analysis_type: values.analysis_type,
- content: values.content,
- category: values.category,
- is_enabled: values.is_enabled,
- };
- await knowledgeApi.updateGuide(editing.id, req);
- message.success('更新成功');
- } else {
- const req: CreateGuideReq = {
- title: values.title,
- analysis_type: values.analysis_type,
- content: values.content,
- category: values.category,
- is_enabled: values.is_enabled,
- };
- await knowledgeApi.createGuide(req);
- message.success('创建成功');
- }
- setModalOpen(false);
- fetchData();
- } catch {
- message.error(editing ? '更新失败' : '创建失败');
- }
- };
-
- const handleDelete = async (id: string) => {
- try {
- await knowledgeApi.deleteGuide(id);
- message.success('删除成功');
- fetchData();
- } catch {
- message.error('删除失败');
- }
- };
-
- const handleReEmbed = async (id: string) => {
- try {
- await knowledgeApi.reEmbedGuide(id);
- message.success('向量重新生成已触发');
- } catch {
- message.error('向量重新生成失败');
- }
- };
-
- const columns = [
- { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
- {
- title: '分析类型',
- dataIndex: 'analysis_type',
- key: 'analysis_type',
- width: 120,
- render: (v: string) => {
- const found = ANALYSIS_TYPES.find((t) => t.value === v);
- return {found?.label ?? v};
- },
- },
- {
- title: '分类',
- dataIndex: 'category',
- key: 'category',
- width: 100,
- render: (v: string | null) => v || '-',
- },
- {
- title: '状态',
- dataIndex: 'is_enabled',
- key: 'is_enabled',
- width: 80,
- render: (v: boolean) => (
- {v ? '启用' : '禁用'}
- ),
- },
- {
- title: '更新时间',
- dataIndex: 'updated_at',
- key: 'updated_at',
- width: 170,
- render: (v: string) => (v ? new Date(v).toLocaleString() : '-'),
- },
- {
- title: '操作',
- key: 'actions',
- width: 200,
- render: (_: unknown, record: KnowledgeGuide) => (
-
-
- } onClick={() => openEdit(record)} />
-
-
- }
- onClick={() => handleReEmbed(record.id)}
- />
-
- handleDelete(record.id)}
- okText="删除"
- cancelText="取消"
- >
- } />
-
-
- ),
- },
- ];
-
- return (
- <>
-
-
-
- } onClick={openCreate}>
- 新增临床指南
-
-
- } onClick={fetchData}>
- 刷新
-
-
-
- `共 ${total} 条` }}
- size="small"
- />
-
- setModalOpen(false)}
- width={700}
- destroyOnClose
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/routeConfig.ts b/apps/web/src/routeConfig.ts
index 636cda4..7dab879 100644
--- a/apps/web/src/routeConfig.ts
+++ b/apps/web/src/routeConfig.ts
@@ -151,10 +151,6 @@ const ENTRIES: RoutePermissionEntry[] = [
path: "/health/ai-knowledge",
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
},
- {
- path: "/health/ai-knowledge-v2",
- permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
- },
// ===== 健康管理 — 积分商城 =====
{
diff --git a/crates/erp-server/migration/src/m20260527_000168_ai_knowledge_v2_menu.rs b/crates/erp-server/migration/src/m20260527_000168_ai_knowledge_v2_menu.rs
index d545e3d..153aae0 100644
--- a/crates/erp-server/migration/src/m20260527_000168_ai_knowledge_v2_menu.rs
+++ b/crates/erp-server/migration/src/m20260527_000168_ai_knowledge_v2_menu.rs
@@ -6,41 +6,17 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
- // 安全插入:仅在 sys_menu 表存在且有 ai-knowledge 菜单时添加 V2 菜单
+ // 将旧版 AI 知识库菜单更新为 V2 版本
let sql = r#"
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sys_menu') THEN
- INSERT INTO sys_menu (id, parent_id, name, path, icon, sort, permission, component, is_external, is_cached, status, visible, created_at, updated_at, deleted_at)
- SELECT
- gen_random_uuid(),
- parent_id,
- '知识库 V2',
- '/health/ai-knowledge-v2',
- 'DatabaseOutlined',
- 54,
- 'ai.knowledge.list',
- 'ai/KnowledgeV2Page',
- false,
- false,
- 'active',
- true,
- now(),
- now(),
- NULL
- FROM sys_menu
- WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL
- LIMIT 1
- ON CONFLICT DO NOTHING;
-
- IF FOUND THEN
- INSERT INTO sys_role_menu (role_id, menu_id)
- SELECT r.id, m.id
- FROM sys_role r, sys_menu m
- WHERE r.code = 'admin' AND r.deleted_at IS NULL
- AND m.path = '/health/ai-knowledge-v2' AND m.deleted_at IS NULL
- ON CONFLICT DO NOTHING;
- END IF;
+ UPDATE sys_menu
+ SET name = '知识库管理',
+ icon = 'DatabaseOutlined',
+ component = 'ai/KnowledgeV2Page',
+ updated_at = now()
+ WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL;
END IF;
END;
$$ LANGUAGE plpgsql
@@ -59,7 +35,12 @@ impl MigrationTrait for Migration {
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sys_menu') THEN
- DELETE FROM sys_menu WHERE path = '/health/ai-knowledge-v2';
+ UPDATE sys_menu
+ SET name = 'AI 知识库',
+ icon = 'BookOutlined',
+ component = 'health/AiKnowledgePage',
+ updated_at = now()
+ WHERE path = '/health/ai-knowledge' AND deleted_at IS NULL;
END IF;
END;
$$ LANGUAGE plpgsql