diff --git a/frontend/src/app/knowledge-bases/page.tsx b/frontend/src/app/knowledge-bases/page.tsx index 8c71c5a4..d72f1a6c 100644 --- a/frontend/src/app/knowledge-bases/page.tsx +++ b/frontend/src/app/knowledge-bases/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { InboxOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Alert, Button, Card, Empty, Form, Input, List, Select, Space, Table, Tag, Typography, Upload, message } from "antd"; +import { Alert, Button, Card, Divider, Empty, Form, Input, List, Modal, Select, Space, Table, Tag, Typography, Upload, message } from "antd"; import type { UploadFile } from "antd/es/upload/interface"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -37,6 +37,20 @@ type KnowledgeBaseRollbackFormValues = { target_version_label: string; change_summary?: string; }; +type KnowledgeBaseGovernanceCheckStatus = "pass" | "warning" | "block"; +type KnowledgeBaseGovernanceCheckItem = { + key: string; + label: string; + status: KnowledgeBaseGovernanceCheckStatus; + detail: string; +}; +type KnowledgeBaseVersionComparisonRow = { + key: string; + label: string; + current: string; + target: string; + changed: boolean; +}; const DOCUMENT_STATUS_META: Record = { pending: { color: "gold", label: "待处理" }, @@ -123,6 +137,24 @@ function getErrorMessage(error: unknown, fallback: string) { return error instanceof Error && error.message ? error.message : fallback; } +function formatDisplayText(value: string | null | undefined, empty = "未填写") { + if (!value) { + return empty; + } + const trimmed = value.trim(); + return trimmed ? trimmed : empty; +} + +function getGovernanceCheckMeta(status: KnowledgeBaseGovernanceCheckStatus) { + if (status === "pass") { + return { color: "success", label: "通过" }; + } + if (status === "warning") { + return { color: "warning", label: "注意" }; + } + return { color: "error", label: "阻塞" }; +} + function getDocumentStatusMeta(status: string) { return DOCUMENT_STATUS_META[status] ?? { color: "default", label: status }; } @@ -218,6 +250,7 @@ export default function KnowledgeBasesPage() { const [regionFilter, setRegionFilter] = useState("all"); const [documentsLoading, setDocumentsLoading] = useState(false); const [fileList, setFileList] = useState([]); + const rollbackTargetVersionLabel = Form.useWatch("target_version_label", rollbackForm); const selectedKnowledgeBase = useMemo( () => items.find((item) => item.id === selectedId) ?? null, @@ -268,6 +301,160 @@ export default function KnowledgeBasesPage() { [versions], ); + const rollbackTargetVersion = useMemo( + () => versions.find((item) => item.version_label === rollbackTargetVersionLabel) ?? null, + [rollbackTargetVersionLabel, versions], + ); + const publishActionLabel = useMemo(() => { + if (!selectedKnowledgeBase) { + return "发布知识库"; + } + return selectedKnowledgeBase.status?.toLowerCase() === "active" ? "发布新版本" : "发布知识库"; + }, [selectedKnowledgeBase]); + const publishPreflightChecks = useMemo(() => { + if (!selectedKnowledgeBase) { + return []; + } + + const totalDocuments = selectedKnowledgeBase.document_count ?? 0; + const indexedDocuments = documents.filter((item) => isDocumentReadyForChat(item)).length; + const failedDocumentsCount = documents.filter((item) => isDocumentFailedLike(item)).length; + const inFlightDocuments = documents.filter((item) => item.status === "pending" || item.status === "processing").length; + const documentsMissingSourceMetadata = documents.filter( + (item) => !item.source_type || !item.authority || !item.effective_date || !item.region || !(item.tags?.length), + ).length; + + const checks: KnowledgeBaseGovernanceCheckItem[] = [ + { + key: "document-count", + label: "知识文档数量", + status: totalDocuments > 0 ? "pass" : "block", + detail: totalDocuments > 0 ? `当前已纳入 ${totalDocuments} 份知识文档。` : "当前知识库还没有文档,无法形成可发布知识集合。", + }, + { + key: "source-metadata", + label: "法源元数据完整度", + status: documentsMissingSourceMetadata > 0 ? "warning" : "pass", + detail: + documentsMissingSourceMetadata > 0 + ? `${documentsMissingSourceMetadata} 份文档缺少 source_type / authority / effective_date / region / tags 中的至少一项,建议补齐后再发版。` + : "文档的法源元数据已补齐,便于后续治理和追溯。", + }, + { + key: "description", + label: "知识库说明", + status: selectedKnowledgeBase.description?.trim() ? "pass" : "warning", + detail: selectedKnowledgeBase.description?.trim() + ? "知识库说明已填写,便于团队理解本次发布范围。" + : "当前知识库说明为空,建议补一段适用范围说明。", + }, + { + key: "versioning", + label: "版本快照", + status: "pass", + detail: versions.length > 0 ? `已有 ${versions.length} 个历史版本,本次会继续生成新的治理快照。` : "这是首次发布,系统会自动生成 v1 治理快照。", + }, + ]; + + if (totalDocuments === 0) { + checks.splice(1, 0, { + key: "index-health", + label: "索引完成度", + status: "block", + detail: "没有可供检查的索引结果,请先上传并完成文档处理。", + }); + return checks; + } + + checks.splice(1, 0, { + key: "index-health", + label: "索引完成度", + status: failedDocumentsCount > 0 || inFlightDocuments > 0 || indexedDocuments !== totalDocuments ? "block" : "pass", + detail: + failedDocumentsCount > 0 || inFlightDocuments > 0 || indexedDocuments !== totalDocuments + ? `已索引 ${indexedDocuments}/${totalDocuments},处理中 ${inFlightDocuments},失败 ${failedDocumentsCount}。发布前需要全部文档处于可检索状态。` + : `全部 ${totalDocuments} 份文档均已完成索引,可参与正式检索。`, + }); + + return checks; + }, [documents, selectedKnowledgeBase, versions.length]); + const publishBlockerCount = useMemo( + () => publishPreflightChecks.filter((item) => item.status === "block").length, + [publishPreflightChecks], + ); + const publishWarningCount = useMemo( + () => publishPreflightChecks.filter((item) => item.status === "warning").length, + [publishPreflightChecks], + ); + const versionComparisonRows = useMemo(() => { + if (!selectedKnowledgeBase || !rollbackTargetVersion) { + return []; + } + + return [ + { + key: "version-label", + label: "版本标签", + current: formatDisplayText(selectedKnowledgeBase.current_version_label, "未发布"), + target: rollbackTargetVersion.version_label, + changed: selectedKnowledgeBase.current_version_label !== rollbackTargetVersion.version_label, + }, + { + key: "status", + label: "生命周期状态", + current: selectedKnowledgeBaseStatus.label, + target: getKnowledgeBaseStatusMeta(rollbackTargetVersion.status).label, + changed: (selectedKnowledgeBase.status || "").toLowerCase() !== (rollbackTargetVersion.status || "").toLowerCase(), + }, + { + key: "name", + label: "知识库名称", + current: formatDisplayText(selectedKnowledgeBase.name, "-"), + target: formatDisplayText(rollbackTargetVersion.name, "-"), + changed: selectedKnowledgeBase.name !== rollbackTargetVersion.name, + }, + { + key: "description", + label: "知识库说明", + current: formatDisplayText(selectedKnowledgeBase.description), + target: formatDisplayText(rollbackTargetVersion.description), + changed: formatDisplayText(selectedKnowledgeBase.description) !== formatDisplayText(rollbackTargetVersion.description), + }, + { + key: "retrieval-mode", + label: "检索模式", + current: getRetrievalModeMeta(selectedKnowledgeBase.retrieval_mode).label, + target: getRetrievalModeMeta(rollbackTargetVersion.retrieval_mode).label, + changed: (selectedKnowledgeBase.retrieval_mode || "").toLowerCase() !== (rollbackTargetVersion.retrieval_mode || "").toLowerCase(), + }, + { + key: "published-by", + label: "发布人", + current: formatDisplayText(selectedKnowledgeBase.published_by, "未记录"), + target: formatDisplayText(rollbackTargetVersion.published_by, "未记录"), + changed: formatDisplayText(selectedKnowledgeBase.published_by, "未记录") !== formatDisplayText(rollbackTargetVersion.published_by, "未记录"), + }, + { + key: "published-at", + label: "发布时间", + current: selectedKnowledgeBase.published_at ? formatDateTime(selectedKnowledgeBase.published_at) : "未发布", + target: rollbackTargetVersion.published_at ? formatDateTime(rollbackTargetVersion.published_at) : "未发布", + changed: (selectedKnowledgeBase.published_at || "") !== (rollbackTargetVersion.published_at || ""), + }, + { + key: "change-summary", + label: "版本说明", + current: selectedKnowledgeBase.status?.toLowerCase() === "active" ? "当前线上版本" : "当前草稿状态", + target: formatDisplayText(rollbackTargetVersion.change_summary, "未填写"), + changed: true, + }, + ]; + }, [rollbackTargetVersion, selectedKnowledgeBase, selectedKnowledgeBaseStatus.label]); + const changedVersionComparisonCount = useMemo( + () => versionComparisonRows.filter((item) => item.changed).length, + [versionComparisonRows], + ); + const loadKnowledgeBases = useCallback(async () => { if (currentUser?.role !== "admin") { setLoading(false); @@ -306,7 +493,8 @@ export default function KnowledgeBasesPage() { setDocumentsLoading(true); } try { - const result = await listKnowledgeBaseDocuments(knowledgeBaseId); + const documentCount = items.find((item) => item.id === knowledgeBaseId)?.document_count ?? 20; + const result = await listKnowledgeBaseDocuments(knowledgeBaseId, 1, Math.max(documentCount, 20)); if (result.code !== 0) { const nextMessage = result.message || "获取知识库文档失败"; setDocumentsError(nextMessage); @@ -325,7 +513,7 @@ export default function KnowledgeBasesPage() { setDocumentsLoading(false); setRefreshingDocuments(false); } - }, [messageApi]); + }, [items, messageApi]); const loadVersions = useCallback(async (knowledgeBaseId: string) => { setVersionsLoading(true); @@ -339,7 +527,7 @@ export default function KnowledgeBasesPage() { } setVersions(result.data); rollbackForm.setFieldsValue({ - target_version_label: result.data[0]?.version_label, + target_version_label: result.data[1]?.version_label ?? result.data[0]?.version_label, }); } catch (error: unknown) { messageApi.error(getErrorMessage(error, "获取知识库版本失败,请稍后重试")); @@ -490,7 +678,11 @@ export default function KnowledgeBasesPage() { }; const handlePublishKnowledgeBase = async () => { - if (!selectedId) { + if (!selectedId || !selectedKnowledgeBase) { + return; + } + if (publishBlockerCount > 0) { + messageApi.error(`发布前检查未通过,还有 ${publishBlockerCount} 个阻塞项需要先处理。`); return; } setPublishing(true); @@ -666,11 +858,36 @@ export default function KnowledgeBasesPage() { 发布人:{selectedKnowledgeBase.published_by || "-"} 发布时间:{selectedKnowledgeBase.published_at ? formatDateTime(selectedKnowledgeBase.published_at) : "-"} + 0 ? "error" : publishWarningCount > 0 ? "warning" : "success"} + message={`${publishActionLabel}前检查:通过 ${publishPreflightChecks.filter((item) => item.status === "pass").length} 项,注意 ${publishWarningCount} 项,阻塞 ${publishBlockerCount} 项`} + description="这一步先把当前知识库的文档、索引与法源元数据过一遍,避免带着未完成状态直接进生产可用版本。" + /> + { + const meta = getGovernanceCheckMeta(item.status); + return ( + + + + {item.label} + {meta.label} + + {item.detail} + + + ); + }} + /> - - @@ -679,7 +896,7 @@ export default function KnowledgeBasesPage() { -