You've already forked DataMate
feat(knowledge-base): 添加知识库文件全库检索功能
- 新增相对路径字段替代原有的metadata存储方式 - 实现跨知识库文件检索接口searchFiles - 添加前端全库检索页面和相关API调用 - 优化文件路径处理和数据库索引配置 - 统一请求参数类型定义为RequestPayload和RequestParams - 简化RagFile模型中的元数据结构设计
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||
@@ -58,7 +58,6 @@ type KBFileRow = KBFile & {
|
||||
};
|
||||
|
||||
const PATH_SEPARATOR = "/";
|
||||
const RELATIVE_PATH_KEY = "relativePath";
|
||||
const normalizePath = (value?: string) =>
|
||||
(value ?? "").replace(/\\/g, PATH_SEPARATOR);
|
||||
|
||||
@@ -81,17 +80,13 @@ const splitRelativePath = (fullPath: string, prefix: string) => {
|
||||
};
|
||||
|
||||
const resolveFileRelativePath = (file: KBFile) => {
|
||||
const metadata = file?.metadata as Record<string, unknown> | undefined;
|
||||
const metadataPath =
|
||||
metadata && typeof metadata[RELATIVE_PATH_KEY] === "string"
|
||||
? String(metadata[RELATIVE_PATH_KEY])
|
||||
: "";
|
||||
const rawPath = metadataPath || file.fileName || file.name || "";
|
||||
const rawPath = file.relativePath || file.fileName || file.name || "";
|
||||
return normalizePath(rawPath).replace(/^\/+/, "");
|
||||
};
|
||||
|
||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { message } = App.useApp();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
||||
@@ -158,6 +153,16 @@ const KnowledgeBaseDetailPage: React.FC = () => {
|
||||
}
|
||||
}, [id, fetchKnowledgeBaseDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const prefixParam = searchParams.get("prefix");
|
||||
const fileNameParam = searchParams.get("fileName");
|
||||
setFilePrefix(prefixParam ? normalizePrefix(prefixParam) : "");
|
||||
setFileKeyword(fileNameParam ? fileNameParam : "");
|
||||
}, [id, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchFiles();
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function KnowledgeBasePage() {
|
||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||
message.success("知识库删除成功");
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
message.error("知识库删除失败");
|
||||
}
|
||||
};
|
||||
@@ -47,7 +47,7 @@ export default function KnowledgeBasePage() {
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: (item) => {
|
||||
onClick: (item: KnowledgeBaseItem) => {
|
||||
setIsEdit(true);
|
||||
setCurrentKB(item);
|
||||
},
|
||||
@@ -64,7 +64,7 @@ export default function KnowledgeBasePage() {
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
},
|
||||
onClick: (item) => handleDeleteKB(item),
|
||||
onClick: (item: KnowledgeBaseItem) => handleDeleteKB(item),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function KnowledgeBasePage() {
|
||||
fixed: "left" as const,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
render: (_: unknown, kb: KnowledgeBaseItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
||||
@@ -111,7 +111,7 @@ export default function KnowledgeBasePage() {
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 150,
|
||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
||||
render: (_: unknown, kb: KnowledgeBaseItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{operations.map((op) => (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
@@ -132,17 +132,22 @@ export default function KnowledgeBasePage() {
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">知识生成</h1>
|
||||
<CreateKnowledgeBase
|
||||
isEdit={isEdit}
|
||||
data={currentKB}
|
||||
onUpdate={() => {
|
||||
fetchData();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEdit(false);
|
||||
setCurrentKB(null);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => navigate("/data/knowledge-base/search")}>
|
||||
全库搜索
|
||||
</Button>
|
||||
<CreateKnowledgeBase
|
||||
isEdit={isEdit}
|
||||
data={currentKB}
|
||||
onUpdate={() => {
|
||||
fetchData();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEdit(false);
|
||||
setCurrentKB(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchControls
|
||||
@@ -161,7 +166,9 @@ export default function KnowledgeBasePage() {
|
||||
<CardView
|
||||
data={tableData}
|
||||
operations={operations}
|
||||
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
|
||||
onView={(item: KnowledgeBaseItem) =>
|
||||
navigate(`/data/knowledge-base/detail/${item.id}`)
|
||||
}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : (
|
||||
@@ -177,4 +184,4 @@ export default function KnowledgeBasePage() {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
217
frontend/src/pages/KnowledgeBase/Search/KnowledgeBaseSearch.tsx
Normal file
217
frontend/src/pages/KnowledgeBase/Search/KnowledgeBaseSearch.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { App, Badge, Breadcrumb, Button, Input, Table } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
KBFileStatus,
|
||||
KnowledgeBaseFileSearchResult,
|
||||
} from "../knowledge-base.model";
|
||||
import { KBFileStatusMap } from "../knowledge-base.const";
|
||||
import { queryKnowledgeBaseFilesSearchUsingGet } from "../knowledge-base.api";
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
|
||||
const PATH_SEPARATOR = "/";
|
||||
|
||||
const normalizePath = (value?: string) =>
|
||||
(value ?? "").replace(/\\/g, PATH_SEPARATOR);
|
||||
|
||||
const resolvePrefix = (relativePath?: string) => {
|
||||
const normalized = normalizePath(relativePath);
|
||||
const parts = normalized.split(PATH_SEPARATOR).filter(Boolean);
|
||||
if (parts.length <= 1) {
|
||||
return "";
|
||||
}
|
||||
parts.pop();
|
||||
return `${parts.join(PATH_SEPARATOR)}${PATH_SEPARATOR}`;
|
||||
};
|
||||
|
||||
export default function KnowledgeBaseSearch() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [activeKeyword, setActiveKeyword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searched, setSearched] = useState(false);
|
||||
const [results, setResults] = useState<KnowledgeBaseFileSearchResult[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchResults = useCallback(
|
||||
async (keyword: string, page?: number, pageSize?: number) => {
|
||||
const resolvedPage = page ?? pagination.current;
|
||||
const resolvedPageSize = pageSize ?? pagination.pageSize;
|
||||
if (!keyword) {
|
||||
setResults([]);
|
||||
setPagination((prev) => ({ ...prev, total: 0, current: resolvedPage }));
|
||||
setSearched(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await queryKnowledgeBaseFilesSearchUsingGet({
|
||||
fileName: keyword,
|
||||
page: Math.max(resolvedPage - 1, 0),
|
||||
size: resolvedPageSize,
|
||||
});
|
||||
const content = Array.isArray(data?.content) ? data.content : [];
|
||||
setResults(content);
|
||||
setPagination({
|
||||
current: resolvedPage,
|
||||
pageSize: resolvedPageSize,
|
||||
total: data?.totalElements ?? 0,
|
||||
});
|
||||
setSearched(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to search knowledge base files:", error);
|
||||
message.error("检索失败,请稍后重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[message, pagination]
|
||||
);
|
||||
|
||||
const handleSearch = (value?: string) => {
|
||||
const keyword = (value ?? searchTerm).trim();
|
||||
if (!keyword) {
|
||||
message.warning("请输入文件名");
|
||||
return;
|
||||
}
|
||||
setActiveKeyword(keyword);
|
||||
fetchResults(keyword, 1, pagination.pageSize);
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "知识库",
|
||||
dataIndex: "knowledgeBaseName",
|
||||
key: "knowledgeBaseName",
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
render: (text: string) => text || "-",
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "fileName",
|
||||
key: "fileName",
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "相对路径",
|
||||
dataIndex: "relativePath",
|
||||
key: "relativePath",
|
||||
ellipsis: true,
|
||||
render: (value: string) => value || "-",
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 120,
|
||||
render: (status?: KBFileStatus) => {
|
||||
const config = status ? KBFileStatusMap[status] : undefined;
|
||||
if (!config) {
|
||||
return <Badge color="default" text={status || "-"} />;
|
||||
}
|
||||
return <Badge color={config.color} text={config.label} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updatedAt",
|
||||
key: "updatedAt",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (value: string) => formatDateTime(value) || "-",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 120,
|
||||
align: "right" as const,
|
||||
render: (_: unknown, record: KnowledgeBaseFileSearchResult) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
const prefix = resolvePrefix(record.relativePath);
|
||||
const searchParams = new URLSearchParams();
|
||||
if (prefix) {
|
||||
searchParams.set("prefix", prefix);
|
||||
}
|
||||
navigate(
|
||||
`/data/knowledge-base/detail/${record.knowledgeBaseId}?${searchParams.toString()}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
定位
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>
|
||||
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>全库搜索</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold">知识库全库检索</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input.Search
|
||||
allowClear
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
onSearch={handleSearch}
|
||||
placeholder="输入文件名,回车或点击搜索"
|
||||
enterButton="搜索"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={results}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
const nextKeyword = activeKeyword.trim();
|
||||
if (!nextKeyword) {
|
||||
message.warning("请输入文件名");
|
||||
return;
|
||||
}
|
||||
fetchResults(nextKeyword, page, pageSize || pagination.pageSize);
|
||||
},
|
||||
}}
|
||||
locale={{
|
||||
emptyText: searched ? "暂无匹配文件" : "请输入文件名开始检索",
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
const prefix = resolvePrefix(record.relativePath);
|
||||
const searchParams = new URLSearchParams();
|
||||
if (prefix) {
|
||||
searchParams.set("prefix", prefix);
|
||||
}
|
||||
navigate(
|
||||
`/data/knowledge-base/detail/${record.knowledgeBaseId}?${searchParams.toString()}`
|
||||
);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { get, post, put, del } from "@/utils/request";
|
||||
|
||||
type RequestPayload = Record<string, unknown>;
|
||||
type RequestParams = Record<string, unknown>;
|
||||
|
||||
// 获取知识库列表
|
||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
||||
export function queryKnowledgeBasesUsingPost(params: RequestPayload) {
|
||||
return post("/api/knowledge-base/list", params);
|
||||
}
|
||||
|
||||
// 创建知识库
|
||||
export function createKnowledgeBaseUsingPost(data: any) {
|
||||
export function createKnowledgeBaseUsingPost(data: RequestPayload) {
|
||||
return post("/api/knowledge-base/create", data);
|
||||
}
|
||||
|
||||
@@ -16,7 +19,7 @@ export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
|
||||
}
|
||||
|
||||
// 更新知识库
|
||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
|
||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: RequestPayload) {
|
||||
return put(`/api/knowledge-base/${baseId}`, data);
|
||||
}
|
||||
|
||||
@@ -26,17 +29,22 @@ export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
||||
}
|
||||
|
||||
// 获取知识生成文件列表
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data: RequestParams) {
|
||||
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 全库检索知识库文件
|
||||
export function queryKnowledgeBaseFilesSearchUsingGet(params: RequestParams) {
|
||||
return get("/api/knowledge-base/files/search", params);
|
||||
}
|
||||
|
||||
// 添加文件到知识库
|
||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: RequestPayload) {
|
||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
// 删除知识生成文件
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: any) {
|
||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: RequestPayload) {
|
||||
return del(`/api/knowledge-base/${baseId}/files`, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,50 +29,26 @@ export interface KBFile {
|
||||
id: string;
|
||||
fileName: string;
|
||||
name?: string;
|
||||
relativePath?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: KBFileStatus;
|
||||
chunkCount: number;
|
||||
metadata: Record<string, any>;
|
||||
metadata: Record<string, unknown>;
|
||||
knowledgeBaseId: string;
|
||||
fileId: string;
|
||||
updatedBy: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: number;
|
||||
content: string;
|
||||
position: number;
|
||||
tokens: number;
|
||||
embedding?: number[];
|
||||
similarity?: string;
|
||||
export interface KnowledgeBaseFileSearchResult {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
knowledgeBaseName: string;
|
||||
fileName: string;
|
||||
relativePath?: string;
|
||||
status?: KBFileStatus;
|
||||
chunkCount?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
vectorId?: string;
|
||||
sliceOperator?: string;
|
||||
parentChunkId?: number;
|
||||
metadata?: {
|
||||
source: string;
|
||||
page?: number;
|
||||
section?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorizationRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
operation: "create" | "update" | "delete" | "reprocess";
|
||||
fileId: number;
|
||||
fileName: string;
|
||||
chunksProcessed: number;
|
||||
vectorsGenerated: number;
|
||||
status: "success" | "failed" | "partial";
|
||||
duration: string;
|
||||
config: {
|
||||
embeddingModel: string;
|
||||
chunkSize: number;
|
||||
sliceMethod: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user