完善知识库容量权限与界面

This commit is contained in:
baiyanyun
2026-07-14 16:39:40 +08:00
parent 5b14b3f48d
commit 9002abfe61
16 changed files with 669 additions and 160 deletions

View File

@@ -21,7 +21,6 @@ import {
} from "antd";
import {
ArrowLeftOutlined,
BookOutlined,
CheckOutlined,
CloudUploadOutlined,
DatabaseOutlined,
@@ -39,6 +38,7 @@ import {
} from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import ImageWithFallback from "../common/ImageWithFallback";
import { getCurrentAuth } from "../../services/core/auth";
import {
AiAgentContext,
AiManualComponent,
@@ -183,6 +183,7 @@ function getPublishedWorkflowId(workflow: PublishedWorkflow): number {
}
export default function KnowledgePage() {
const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const [spaces, setSpaces] = useState<SpaceInfo[]>([]);
const [selectedSpaceId, setSelectedSpaceId] = useState<number | null>(null);
const [knowledges, setKnowledges] = useState<KnowledgeComponent[]>([]);
@@ -280,6 +281,26 @@ export default function KnowledgePage() {
[knowledges, selectedKnowledgeId],
);
const isKnowledgeOwner = useCallback(
(knowledge?: KnowledgeComponent | KnowledgeInfo | null) => {
if (!knowledge) return false;
const creator = (knowledge as KnowledgeComponent).creator;
const creatorId = Number(knowledge.creatorId || creator?.id || 0);
return Boolean(currentUserId && creatorId && currentUserId === creatorId);
},
[currentUserId],
);
const canManageSelectedKnowledge = isKnowledgeOwner(
knowledgeInfo || selectedKnowledgeComponent,
);
const ensureCanManageSelectedKnowledge = useCallback(() => {
if (canManageSelectedKnowledge) return true;
message.warning("该知识库由其他用户创建,仅支持查看和加入对话");
return false;
}, [canManageSelectedKnowledge]);
const loadAgentComponents = useCallback(async () => {
try {
const context = await resolveAiAgentContext();
@@ -489,6 +510,9 @@ export default function KnowledgePage() {
useEffect(() => {
loadSpaces();
loadAgentComponents();
void getCurrentAuth().then((auth) => {
setCurrentUserId(auth.userInfo?.userId || null);
});
}, [loadAgentComponents, loadSpaces]);
useEffect(() => {
@@ -530,6 +554,10 @@ export default function KnowledgePage() {
const openKnowledgeForm = async (
knowledge?: KnowledgeComponent | KnowledgeInfo,
) => {
if (knowledge && !isKnowledgeOwner(knowledge)) {
message.warning("该知识库由其他用户创建,不能编辑");
return;
}
setKnowledgeModalOpen(true);
void (async () => {
const spaceId =
@@ -591,6 +619,10 @@ export default function KnowledgePage() {
};
const saveKnowledge = async () => {
if (editingKnowledge && !isKnowledgeOwner(editingKnowledge)) {
message.warning("该知识库由其他用户创建,不能编辑");
return;
}
let values: KnowledgeFormValues;
try {
values = await knowledgeForm.validateFields();
@@ -643,6 +675,10 @@ export default function KnowledgePage() {
};
const confirmDeleteKnowledge = (knowledge: KnowledgeComponent) => {
if (!isKnowledgeOwner(knowledge)) {
message.warning("该知识库由其他用户创建,不能删除");
return;
}
const knowledgeId = getKnowledgeId(knowledge);
Modal.confirm({
title: "删除知识库",
@@ -767,6 +803,7 @@ export default function KnowledgePage() {
const files = Array.from(event.target.files || []);
event.target.value = "";
if (!files.length || !selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
const validationError = files.map(validateKnowledgeFile).find(Boolean);
if (validationError) {
@@ -792,6 +829,7 @@ export default function KnowledgePage() {
const saveCustomDocument = async () => {
if (!selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
const values = await customDocForm.validateFields();
setSavingCustomDoc(true);
try {
@@ -812,6 +850,7 @@ export default function KnowledgePage() {
};
const openRenameDocument = (doc: KnowledgeDocumentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
setRenamingDoc(doc);
renameDocForm.setFieldsValue({ name: doc.name });
setRenameDocModalOpen(true);
@@ -819,6 +858,7 @@ export default function KnowledgePage() {
const saveDocumentName = async () => {
if (!renamingDoc) return;
if (!ensureCanManageSelectedKnowledge()) return;
const values = await renameDocForm.validateFields();
setSavingRename(true);
try {
@@ -856,6 +896,7 @@ export default function KnowledgePage() {
};
const confirmDeleteDocument = (doc: KnowledgeDocumentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
Modal.confirm({
title: "删除文档",
content: `确认删除「${doc.name}」吗?`,
@@ -874,6 +915,7 @@ export default function KnowledgePage() {
doc: KnowledgeDocumentInfo,
action: "qa" | "embedding" | "retry",
) => {
if (!ensureCanManageSelectedKnowledge()) return;
try {
if (action === "qa") {
await generateKnowledgeDocumentQa(doc.id);
@@ -892,6 +934,7 @@ export default function KnowledgePage() {
};
const openSegmentModal = (segment?: KnowledgeRawSegmentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
setEditingSegment(segment || null);
segmentForm.setFieldsValue({
rawTxt: segment?.rawTxt || "",
@@ -901,6 +944,7 @@ export default function KnowledgePage() {
};
const saveSegment = async () => {
if (!ensureCanManageSelectedKnowledge()) return;
const spaceId =
selectedDocument?.spaceId || knowledgeInfo?.spaceId || selectedSpaceId;
if (!selectedDocument || !spaceId) return;
@@ -932,6 +976,7 @@ export default function KnowledgePage() {
};
const confirmDeleteSegment = (segment: KnowledgeRawSegmentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
Modal.confirm({
title: "删除分段",
content: "确认删除该分段吗?",
@@ -947,6 +992,7 @@ export default function KnowledgePage() {
};
const openQaModal = (qa?: KnowledgeQaInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
setEditingQa(qa || null);
qaForm.setFieldsValue({
question: qa?.question || "",
@@ -957,6 +1003,7 @@ export default function KnowledgePage() {
const saveQa = async () => {
if (!selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
const values = await qaForm.validateFields();
try {
if (editingQa) {
@@ -983,6 +1030,7 @@ export default function KnowledgePage() {
};
const confirmDeleteQa = (qa: KnowledgeQaInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
Modal.confirm({
title: "删除 QA",
content: "确认删除该问答吗?",
@@ -1001,6 +1049,7 @@ export default function KnowledgePage() {
const file = event.target.files?.[0];
event.target.value = "";
if (!file || !selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
try {
await importKnowledgeQaExcel({ kbId: selectedKnowledgeId, file });
message.success("QA 已导入");
@@ -1068,7 +1117,7 @@ export default function KnowledgePage() {
},
{
title: "操作",
width: 260,
width: canManageSelectedKnowledge ? 260 : 120,
fixed: "right",
render: (_, record) => (
<div className={styles.tableActions}>
@@ -1082,34 +1131,38 @@ export default function KnowledgePage() {
>
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "qa")}
>
QA
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "embedding")}
>
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "retry")}
>
</Button>
<Button
type="link"
size="small"
onClick={() => openRenameDocument(record)}
>
</Button>
{canManageSelectedKnowledge && (
<>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "qa")}
>
QA
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "embedding")}
>
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "retry")}
>
</Button>
<Button
type="link"
size="small"
onClick={() => openRenameDocument(record)}
>
</Button>
</>
)}
{record.docUrl && (
<Button
type="link"
@@ -1119,14 +1172,16 @@ export default function KnowledgePage() {
</Button>
)}
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteDocument(record)}
>
</Button>
{canManageSelectedKnowledge && (
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteDocument(record)}
>
</Button>
)}
</div>
),
},
@@ -1149,29 +1204,33 @@ export default function KnowledgePage() {
width: 170,
render: (_, record) => formatDateTime(record.modified || record.created),
},
{
title: "操作",
width: 120,
render: (_, record) => (
<div className={styles.tableActions}>
<Button
type="link"
size="small"
onClick={() => openSegmentModal(record)}
>
</Button>
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteSegment(record)}
>
</Button>
</div>
),
},
...(canManageSelectedKnowledge
? [
{
title: "操作",
width: 120,
render: (_: unknown, record: KnowledgeRawSegmentInfo) => (
<div className={styles.tableActions}>
<Button
type="link"
size="small"
onClick={() => openSegmentModal(record)}
>
</Button>
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteSegment(record)}
>
</Button>
</div>
),
},
]
: []),
];
const qaColumns: ColumnsType<KnowledgeQaInfo> = [
@@ -1191,25 +1250,33 @@ export default function KnowledgePage() {
width: 170,
render: (_, record) => formatDateTime(record.modified || record.created),
},
{
title: "操作",
width: 120,
render: (_, record) => (
<div className={styles.tableActions}>
<Button type="link" size="small" onClick={() => openQaModal(record)}>
</Button>
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteQa(record)}
>
</Button>
</div>
),
},
...(canManageSelectedKnowledge
? [
{
title: "操作",
width: 120,
render: (_: unknown, record: KnowledgeQaInfo) => (
<div className={styles.tableActions}>
<Button
type="link"
size="small"
onClick={() => openQaModal(record)}
>
</Button>
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteQa(record)}
>
</Button>
</div>
),
},
]
: []),
];
const renderKnowledgeIcon = (
@@ -1295,6 +1362,12 @@ export default function KnowledgePage() {
<div className={styles.grid}>
{filteredKnowledges.map((knowledge) => {
const knowledgeId = getKnowledgeId(knowledge);
const owned = isKnowledgeOwner(knowledge);
const creatorName =
knowledge.creator?.nickName ||
knowledge.creator?.userName ||
knowledge.creatorName ||
"未知创建人";
const joinedComponent = joinedKnowledgeMap.get(knowledgeId);
const joined =
!!joinedComponent &&
@@ -1311,7 +1384,19 @@ export default function KnowledgePage() {
</div>
<div className={styles.cardTitle}>
<h3>{knowledge.name}</h3>
<span></span>
<div className={styles.cardOwnerRow}>
<Tag
color={owned ? "cyan" : "default"}
className={styles.ownerTag}
>
{owned ? "我的知识库" : "只读"}
</Tag>
{!owned && (
<span title={creatorName}>
{creatorName}
</span>
)}
</div>
</div>
</div>
<p className={styles.cardDescription}>
@@ -1319,9 +1404,14 @@ export default function KnowledgePage() {
</p>
<div className={styles.cardMeta}>
<span>
{formatBytes(Number(knowledge.fileSize || 0))}
{formatBytes(Number(knowledge.fileSize || 0))}
</span>
<span>
<span
title={formatDateTime(
knowledge.modified || knowledge.created,
)}
>
{" "}
{formatDateTime(
knowledge.modified || knowledge.created,
)}
@@ -1329,6 +1419,7 @@ export default function KnowledgePage() {
</div>
<div className={styles.cardActions}>
<Button
type="primary"
size="small"
icon={joined ? <CheckOutlined /> : <TeamOutlined />}
loading={joiningIds.has(knowledgeId)}
@@ -1339,25 +1430,33 @@ export default function KnowledgePage() {
>
{joined ? "已加入" : "加入对话"}
</Button>
<Button
size="small"
icon={<EditOutlined />}
onClick={(event) => {
event.stopPropagation();
openKnowledgeForm(knowledge);
}}
>
</Button>
<Button
size="small"
danger
icon={<DeleteOutlined />}
onClick={(event) => {
event.stopPropagation();
confirmDeleteKnowledge(knowledge);
}}
/>
{owned && (
<div className={styles.cardOwnerActions}>
<Tooltip title="编辑知识库">
<Button
size="small"
icon={<EditOutlined />}
aria-label="编辑知识库"
onClick={(event) => {
event.stopPropagation();
openKnowledgeForm(knowledge);
}}
/>
</Tooltip>
<Tooltip title="删除知识库">
<Button
size="small"
danger
icon={<DeleteOutlined />}
aria-label="删除知识库"
onClick={(event) => {
event.stopPropagation();
confirmDeleteKnowledge(knowledge);
}}
/>
</Tooltip>
</div>
)}
</div>
</article>
);
@@ -1376,7 +1475,11 @@ export default function KnowledgePage() {
allowClear
placeholder="搜索文档"
value={documentKeyword}
onChange={(event) => setDocumentKeyword(event.target.value)}
onChange={(event) => {
const value = event.target.value;
setDocumentKeyword(value);
if (!value) void loadDocuments("");
}}
onSearch={(value) => loadDocuments(value)}
className={styles.innerSearch}
/>
@@ -1387,22 +1490,26 @@ export default function KnowledgePage() {
className={styles.hiddenInput}
onChange={handleDocumentFiles}
/>
<Button
icon={<CloudUploadOutlined />}
loading={uploadingDocs}
onClick={() => documentFileInputRef.current?.click()}
>
</Button>
<Button
icon={<FileAddOutlined />}
onClick={() => {
customDocForm.resetFields();
setCustomDocModalOpen(true);
}}
>
</Button>
{canManageSelectedKnowledge && (
<>
<Button
icon={<CloudUploadOutlined />}
loading={uploadingDocs}
onClick={() => documentFileInputRef.current?.click()}
>
</Button>
<Button
icon={<FileAddOutlined />}
onClick={() => {
customDocForm.resetFields();
setCustomDocModalOpen(true);
}}
>
</Button>
</>
)}
<Tooltip title="刷新文档状态">
<Button icon={<ReloadOutlined />} onClick={refreshDocumentStatus} />
</Tooltip>
@@ -1443,13 +1550,15 @@ export default function KnowledgePage() {
}}
options={documents.map((doc) => ({ label: doc.name, value: doc.id }))}
/>
<Button
icon={<PlusOutlined />}
disabled={!selectedDocument}
onClick={() => openSegmentModal()}
>
</Button>
{canManageSelectedKnowledge && (
<Button
icon={<PlusOutlined />}
disabled={!selectedDocument}
onClick={() => openSegmentModal()}
>
</Button>
)}
<Button
icon={<ReloadOutlined />}
disabled={!selectedDocument}
@@ -1498,13 +1607,19 @@ export default function KnowledgePage() {
allowClear
placeholder="搜索问题"
value={qaKeyword}
onChange={(event) => setQaKeyword(event.target.value)}
onChange={(event) => {
const value = event.target.value;
setQaKeyword(value);
if (!value) void loadQaList("");
}}
onSearch={(value) => loadQaList(value)}
className={styles.innerSearch}
/>
<Button icon={<PlusOutlined />} onClick={() => openQaModal()}>
QA
</Button>
{canManageSelectedKnowledge && (
<Button icon={<PlusOutlined />} onClick={() => openQaModal()}>
QA
</Button>
)}
<input
ref={qaFileInputRef}
type="file"
@@ -1512,12 +1627,14 @@ export default function KnowledgePage() {
className={styles.hiddenInput}
onChange={handleQaImport}
/>
<Button
icon={<CloudUploadOutlined />}
onClick={() => qaFileInputRef.current?.click()}
>
Excel
</Button>
{canManageSelectedKnowledge && (
<Button
icon={<CloudUploadOutlined />}
onClick={() => qaFileInputRef.current?.click()}
>
Excel
</Button>
)}
<Button icon={<DownloadOutlined />} onClick={handleDownloadQaTemplate}>
</Button>
@@ -1563,6 +1680,13 @@ export default function KnowledgePage() {
const joining =
selectedKnowledgeId !== null && joiningIds.has(selectedKnowledgeId);
const knowledgeForAction = knowledgeInfo || selectedKnowledgeComponent;
const owned = isKnowledgeOwner(knowledgeForAction);
const creator = (knowledgeForAction as KnowledgeComponent | null)?.creator;
const creatorName =
creator?.nickName ||
creator?.userName ||
knowledgeForAction?.creatorName ||
"未知创建人";
return (
<>
@@ -1584,6 +1708,12 @@ export default function KnowledgePage() {
<strong>{knowledgeForAction?.name || "知识库详情"}</strong>
</div>
<h1>{knowledgeForAction?.name || "知识库详情"}</h1>
<div className={styles.detailOwnerRow}>
<Tag color={owned ? "cyan" : "default"}>
{owned ? "我的知识库" : "只读知识库"}
</Tag>
{!owned && <span>{creatorName}</span>}
</div>
<p>{knowledgeForAction?.description || "暂无描述"}</p>
</div>
</div>
@@ -1598,15 +1728,17 @@ export default function KnowledgePage() {
>
{joined ? "已加入对话" : "加入对话"}
</Button>
<Button
icon={<EditOutlined />}
disabled={!knowledgeForAction}
onClick={() =>
knowledgeForAction && openKnowledgeForm(knowledgeForAction)
}
>
</Button>
{owned && (
<Button
icon={<EditOutlined />}
disabled={!knowledgeForAction}
onClick={() =>
knowledgeForAction && openKnowledgeForm(knowledgeForAction)
}
>
</Button>
)}
</div>
</header>
<main className={styles.detailContent}>