- 知识库 REST API: 10 个端点 (references/guides CRUD + re-embed) - search_medical_knowledge Agent Tool: 语义检索参考资料和临床指南 - VectorKnowledgeSource: 实现 KnowledgeSource trait,自动降级 - 沙箱配置: Patient/MedicalStaff 允许使用知识库检索 - 前端 AiKnowledgePage: Tabs(参考资料/临床指南) + Table + Modal CRUD - 权限码 seed 迁移: ai.knowledge.list + ai.knowledge.manage + 菜单
509 lines
14 KiB
TypeScript
509 lines
14 KiB
TypeScript
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 (
|
|
<Card title="AI 知识库管理">
|
|
<Tabs
|
|
items={[
|
|
{ key: 'references', label: '参考资料', children: <ReferencesTab /> },
|
|
{ key: 'guides', label: '临床指南', children: <GuidesTab /> },
|
|
]}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// === References Tab ===
|
|
|
|
function ReferencesTab() {
|
|
const [data, setData] = useState<KnowledgeReference[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editing, setEditing] = useState<KnowledgeReference | null>(null);
|
|
const [filterType, setFilterType] = useState<string | undefined>();
|
|
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 <Tag>{found?.label ?? v}</Tag>;
|
|
},
|
|
},
|
|
{ title: '来源', dataIndex: 'source_name', key: 'source_name', width: 150, ellipsis: true },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'is_enabled',
|
|
key: 'is_enabled',
|
|
width: 80,
|
|
render: (v: boolean) => (
|
|
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag>
|
|
),
|
|
},
|
|
{
|
|
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) => (
|
|
<Space size="small">
|
|
<Tooltip title="编辑">
|
|
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
|
</Tooltip>
|
|
<Tooltip title="重新生成向量">
|
|
<Button
|
|
type="text"
|
|
icon={<ThunderboltOutlined />}
|
|
onClick={() => handleReEmbed(record.id)}
|
|
/>
|
|
</Tooltip>
|
|
<Popconfirm
|
|
title="确定删除此参考资料?"
|
|
onConfirm={() => handleDelete(record.id)}
|
|
okText="删除"
|
|
cancelText="取消"
|
|
>
|
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Space style={{ marginBottom: 16 }}>
|
|
<Select
|
|
allowClear
|
|
placeholder="按分析类型过滤"
|
|
style={{ width: 180 }}
|
|
options={ANALYSIS_TYPES}
|
|
value={filterType}
|
|
onChange={setFilterType}
|
|
/>
|
|
<AuthButton code="ai.knowledge.manage">
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
新增参考资料
|
|
</Button>
|
|
</AuthButton>
|
|
<Button icon={<ReloadOutlined />} onClick={fetchData}>
|
|
刷新
|
|
</Button>
|
|
</Space>
|
|
|
|
<Table
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
|
size="small"
|
|
/>
|
|
|
|
<Modal
|
|
title={editing ? '编辑参考资料' : '新增参考资料'}
|
|
open={modalOpen}
|
|
onOk={handleSubmit}
|
|
onCancel={() => setModalOpen(false)}
|
|
width={600}
|
|
destroyOnClose
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="analysis_type"
|
|
label="分析类型"
|
|
rules={[{ required: true, message: '请选择分析类型' }]}
|
|
>
|
|
<Select options={ANALYSIS_TYPES} />
|
|
</Form.Item>
|
|
<Form.Item name="source_name" label="来源名称" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="content_summary"
|
|
label="内容摘要"
|
|
rules={[{ required: true, message: '请输入内容摘要' }]}
|
|
>
|
|
<Input.TextArea rows={4} />
|
|
</Form.Item>
|
|
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// === Guides Tab ===
|
|
|
|
function GuidesTab() {
|
|
const [data, setData] = useState<KnowledgeGuide[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editing, setEditing] = useState<KnowledgeGuide | null>(null);
|
|
const [filterType, setFilterType] = useState<string | undefined>();
|
|
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 <Tag>{found?.label ?? v}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '分类',
|
|
dataIndex: 'category',
|
|
key: 'category',
|
|
width: 100,
|
|
render: (v: string | null) => v || '-',
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'is_enabled',
|
|
key: 'is_enabled',
|
|
width: 80,
|
|
render: (v: boolean) => (
|
|
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag>
|
|
),
|
|
},
|
|
{
|
|
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) => (
|
|
<Space size="small">
|
|
<Tooltip title="编辑">
|
|
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
|
</Tooltip>
|
|
<Tooltip title="重新生成向量">
|
|
<Button
|
|
type="text"
|
|
icon={<ThunderboltOutlined />}
|
|
onClick={() => handleReEmbed(record.id)}
|
|
/>
|
|
</Tooltip>
|
|
<Popconfirm
|
|
title="确定删除此临床指南?"
|
|
onConfirm={() => handleDelete(record.id)}
|
|
okText="删除"
|
|
cancelText="取消"
|
|
>
|
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Space style={{ marginBottom: 16 }}>
|
|
<Select
|
|
allowClear
|
|
placeholder="按分析类型过滤"
|
|
style={{ width: 180 }}
|
|
options={ANALYSIS_TYPES}
|
|
value={filterType}
|
|
onChange={setFilterType}
|
|
/>
|
|
<AuthButton code="ai.knowledge.manage">
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
新增临床指南
|
|
</Button>
|
|
</AuthButton>
|
|
<Button icon={<ReloadOutlined />} onClick={fetchData}>
|
|
刷新
|
|
</Button>
|
|
</Space>
|
|
|
|
<Table
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }}
|
|
size="small"
|
|
/>
|
|
|
|
<Modal
|
|
title={editing ? '编辑临床指南' : '新增临床指南'}
|
|
open={modalOpen}
|
|
onOk={handleSubmit}
|
|
onCancel={() => setModalOpen(false)}
|
|
width={700}
|
|
destroyOnClose
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="analysis_type"
|
|
label="分析类型"
|
|
rules={[{ required: true, message: '请选择分析类型' }]}
|
|
>
|
|
<Select options={ANALYSIS_TYPES} />
|
|
</Form.Item>
|
|
<Form.Item name="category" label="分类">
|
|
<Input placeholder="如:心血管、内分泌" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="content"
|
|
label="指南内容"
|
|
rules={[{ required: true, message: '请输入指南内容' }]}
|
|
>
|
|
<Input.TextArea rows={8} />
|
|
</Form.Item>
|
|
<Form.Item name="is_enabled" label="启用" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|