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

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}>

View File

@@ -30,6 +30,7 @@ import { t } from "./i18n";
// ========== 类型定义 ===
export interface AuthUserInfo {
id?: number;
userId?: number;
username: string;
displayName?: string;
avatar?: string;
@@ -333,6 +334,7 @@ export async function loginAndRegister(
await setUserInfo({
id: response.id,
userId: response.userId,
username,
displayName: response.name,
currentDomain: domain,
@@ -509,6 +511,7 @@ export async function reRegisterClient(): Promise<ClientRegisterResponse | null>
await setUserInfo({
...currentUserInfo,
id: response.id,
userId: response.userId,
username: username || "",
displayName: response.name,
currentDomain: domain || currentUserInfo?.currentDomain,
@@ -675,6 +678,7 @@ export async function syncConfigToServer(options?: {
await setUserInfo({
...currentUserInfo,
id: response.id,
userId: response.userId,
username: username || "",
displayName: response.name,
currentDomain: preservedCurrentDomain,

View File

@@ -74,7 +74,15 @@ export interface KnowledgeComponent {
spaceId?: number;
created?: string;
modified?: string;
creatorId?: number;
creatorName?: string;
creator?: {
id?: number;
userName?: string;
nickName?: string;
avatar?: string;
};
permissions?: string[];
[key: string]: unknown;
}
@@ -91,6 +99,7 @@ export interface KnowledgeInfo {
pubStatus?: string;
created?: string;
modified?: string;
creatorId?: number;
creatorName?: string;
workflowId?: string | number;
workflowName?: string;

View File

@@ -37,7 +37,7 @@ export interface AuthUserInfo {
username: string;
displayName?: string;
token?: string;
userId?: string;
userId?: number;
email?: string;
currentDomain?: string;
agentId?: number;

View File

@@ -82,19 +82,20 @@
flex-wrap: wrap;
}
.search,
.innerSearch {
.search {
width: min(420px, 100%);
}
.innerSearch {
width: min(360px, 100%);
}
.toolbarCreateButton {
flex: 0 0 auto;
}
.search :global(.ant-input-affix-wrapper),
.search:global(.ant-input-affix-wrapper),
.innerSearch :global(.ant-input-affix-wrapper),
.innerSearch:global(.ant-input-affix-wrapper) {
.search:global(.ant-input-affix-wrapper) {
height: 42px;
border-radius: 8px;
border-color: #dcd9d1;
@@ -102,6 +103,35 @@
box-shadow: none;
}
.innerSearch :global(.ant-input-group) {
display: flex;
}
.innerSearch :global(.ant-input-affix-wrapper) {
height: 38px;
border-color: #dcd9d1;
border-radius: 7px 0 0 7px;
background: #ffffff;
box-shadow: none;
}
.innerSearch :global(.ant-input-search-button) {
width: 42px;
height: 38px;
padding: 0;
border-color: #dcd9d1;
border-radius: 0 7px 7px 0 !important;
color: #5e625c;
background: #ffffff;
box-shadow: none;
}
.innerSearch :global(.ant-input-affix-wrapper-focused),
.innerSearch :global(.ant-input-affix-wrapper:focus-within),
.innerSearch :global(.ant-input-search-button:hover) {
border-color: #74aaa5;
}
.count {
margin-left: auto;
color: #888b85;
@@ -132,15 +162,17 @@
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-template-columns: repeat(3, minmax(0, 340px));
gap: 16px;
justify-content: start;
}
.card {
min-height: 202px;
min-width: 0;
min-height: 218px;
display: flex;
flex-direction: column;
padding: 18px;
padding: 16px;
background: #ffffff;
border: 1px solid #e8e4dc;
border-radius: 8px;
@@ -210,11 +242,28 @@
white-space: nowrap;
}
.cardTitle span {
display: block;
margin-top: 4px;
.cardOwnerRow {
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
margin-top: 5px;
}
.cardOwnerRow span {
min-width: 0;
overflow: hidden;
color: #8a8d89;
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.ownerTag {
flex: 0 0 auto;
margin-inline-end: 0;
line-height: 18px;
}
.cardDescription {
@@ -235,7 +284,7 @@
justify-content: space-between;
gap: 12px;
margin-top: auto;
padding-top: 14px;
padding-top: 12px;
color: #8a8d89;
font-size: 12px;
}
@@ -251,10 +300,19 @@
display: flex;
align-items: center;
gap: 8px;
margin-top: 14px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #efede7;
flex-wrap: wrap;
}
.cardOwnerActions {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
.detailHeader {
align-items: flex-start;
}
@@ -274,6 +332,20 @@
white-space: nowrap;
}
.detailOwnerRow {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
margin-top: 7px;
color: #777b75;
font-size: 12px;
}
.detailOwnerRow :global(.ant-tag) {
margin-inline-end: 0;
}
.detailActions {
flex: 0 0 auto;
display: flex;
@@ -504,6 +576,10 @@
}
@media (max-width: 1180px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 340px));
}
.statRow {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -533,4 +609,20 @@
.statRow {
grid-template-columns: minmax(0, 1fr);
}
.grid {
grid-template-columns: minmax(0, 340px);
}
}
@media (max-width: 420px) {
.grid {
grid-template-columns: minmax(0, 1fr);
}
.cardMeta {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
}