feat: polish knowledge base publish governance UX
This commit is contained in:
@@ -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<string, { color: string; label: string }> = {
|
||||
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<string>("all");
|
||||
const [documentsLoading, setDocumentsLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
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<KnowledgeBaseGovernanceCheckItem[]>(() => {
|
||||
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<KnowledgeBaseVersionComparisonRow[]>(() => {
|
||||
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() {
|
||||
<Text type="secondary">发布人:{selectedKnowledgeBase.published_by || "-"}</Text>
|
||||
<Text type="secondary">发布时间:{selectedKnowledgeBase.published_at ? formatDateTime(selectedKnowledgeBase.published_at) : "-"}</Text>
|
||||
</Space>
|
||||
<Alert
|
||||
showIcon
|
||||
type={publishBlockerCount > 0 ? "error" : publishWarningCount > 0 ? "warning" : "success"}
|
||||
message={`${publishActionLabel}前检查:通过 ${publishPreflightChecks.filter((item) => item.status === "pass").length} 项,注意 ${publishWarningCount} 项,阻塞 ${publishBlockerCount} 项`}
|
||||
description="这一步先把当前知识库的文档、索引与法源元数据过一遍,避免带着未完成状态直接进生产可用版本。"
|
||||
/>
|
||||
<List
|
||||
size="small"
|
||||
bordered
|
||||
dataSource={publishPreflightChecks}
|
||||
renderItem={(item) => {
|
||||
const meta = getGovernanceCheckMeta(item.status);
|
||||
return (
|
||||
<List.Item>
|
||||
<Space direction="vertical" size={2} style={{ width: "100%" }}>
|
||||
<Space wrap size={[8, 8]}>
|
||||
<Text strong>{item.label}</Text>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
</Space>
|
||||
<Text type="secondary">{item.detail}</Text>
|
||||
</Space>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Space wrap>
|
||||
<Button type="primary" loading={publishing} onClick={() => void handlePublishKnowledgeBase()}>
|
||||
<Button type="primary" loading={publishing} disabled={publishBlockerCount > 0} onClick={() => void handlePublishKnowledgeBase()}>
|
||||
发布知识库
|
||||
</Button>
|
||||
<Button loading={unpublishing} onClick={() => void handleUnpublishKnowledgeBase()}>
|
||||
<Button loading={unpublishing} disabled={selectedKnowledgeBase.status?.toLowerCase() !== "active"} onClick={() => void handleUnpublishKnowledgeBase()}>
|
||||
下线知识库
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -679,7 +896,7 @@ export default function KnowledgeBasesPage() {
|
||||
<Select
|
||||
placeholder="选择历史版本"
|
||||
loading={versionsLoading}
|
||||
options={rollbackVersionOptions}
|
||||
options={rollbackVersionOptions.filter((item) => item.value !== selectedKnowledgeBase.current_version_label)}
|
||||
disabled={!rollbackVersionOptions.length}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -687,7 +904,61 @@ export default function KnowledgeBasesPage() {
|
||||
<Input placeholder="例如:恢复到稳定知识库版本" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button loading={rollingBack} disabled={!rollbackVersionOptions.length} onClick={() => void handleRollbackKnowledgeBase()}>
|
||||
<Divider style={{ margin: "8px 0" }}>版本对比与回滚确认</Divider>
|
||||
{rollbackTargetVersion ? (
|
||||
<List
|
||||
size="small"
|
||||
bordered
|
||||
header={`当前版本与 ${rollbackTargetVersion.version_label} 的差异`}
|
||||
dataSource={versionComparisonRows}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<Space direction="vertical" size={2} style={{ width: "100%" }}>
|
||||
<Space wrap size={[8, 8]}>
|
||||
<Text strong>{item.label}</Text>
|
||||
<Tag color={item.changed ? "gold" : "default"}>{item.changed ? "有差异" : "一致"}</Tag>
|
||||
</Space>
|
||||
<Text type="secondary">当前:{item.current}</Text>
|
||||
<Text type="secondary">目标:{item.target}</Text>
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Alert showIcon type="info" message="选择一个历史版本后,这里会展示与当前知识库的关键差异。" />
|
||||
)}
|
||||
<Button
|
||||
loading={rollingBack}
|
||||
disabled={!rollbackVersionOptions.length || !rollbackTargetVersion || rollbackTargetVersion.version_label === selectedKnowledgeBase?.current_version_label}
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: rollbackTargetVersion ? `确认回滚到 ${rollbackTargetVersion.version_label} 吗?` : "确认回滚知识库吗?",
|
||||
okText: "确认回滚",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
content: rollbackTargetVersion ? (
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<Text>
|
||||
当前版本:{formatDisplayText(selectedKnowledgeBase?.current_version_label, "未发布")},目标版本:{rollbackTargetVersion.version_label}。
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
本次共检测到 {changedVersionComparisonCount} 项差异,回滚后系统会生成新的治理快照,而不是直接覆盖历史版本。
|
||||
</Text>
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message={
|
||||
rollbackTargetVersion.change_summary
|
||||
? `目标版本说明:${rollbackTargetVersion.change_summary}`
|
||||
: "目标版本未填写版本说明,请结合历史记录确认。"
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
) : undefined,
|
||||
onOk: async () => handleRollbackKnowledgeBase(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
回滚知识库
|
||||
</Button>
|
||||
<List
|
||||
|
||||
@@ -1660,3 +1660,35 @@
|
||||
### 当前状态更新
|
||||
- 现在“默认流程”与“仓库中的正式治理状态”已经对齐到同一套 `Codex` 无人审批口径。
|
||||
- 当前剩余不是功能阻塞,而是需要把这批 `2026-05-15` 的治理状态刷新结果纳入仓库版本,作为新的正式基线。
|
||||
## 2026-05-15 knowledge-bases 发布治理前端体验打磨
|
||||
### 本轮完成项
|
||||
- `frontend/src/app/knowledge-bases/page.tsx`
|
||||
- 在“发布治理”面板中新增发布前检查摘要:
|
||||
- 知识文档数量
|
||||
- 索引完成度
|
||||
- 法源元数据完整度
|
||||
- 知识库说明
|
||||
- 版本快照状态
|
||||
- 发布按钮现在会在存在阻塞项时禁用,避免带着未完成索引直接发版
|
||||
- 回滚目标默认优先落到“上一历史版本”,不再默认指向当前版本
|
||||
- 新增“版本对比与回滚确认”区域:
|
||||
- 对比当前版本与目标历史版本的名称、说明、检索模式、状态、发布人、发布时间、版本说明
|
||||
- 明确标出“一致 / 有差异”
|
||||
- 回滚按钮改成二次确认:
|
||||
- 弹窗中展示当前版本、目标版本、差异项数量、目标版本说明
|
||||
- 明确说明回滚会生成新的治理快照,而不是覆盖历史版本
|
||||
- 文档加载口径补强:
|
||||
- 发布治理检查会按当前知识库文档总数拉取足量文档,避免只看默认 20 条分页结果导致检查失真
|
||||
|
||||
### 本轮验证
|
||||
- `npm --prefix frontend run lint`
|
||||
- 结果:通过
|
||||
- `npm --prefix frontend run build`
|
||||
- 结果:通过
|
||||
|
||||
### 当前状态更新
|
||||
- `knowledge_bases` 的发布治理已经从“有按钮可操作”推进到“发布前可检查、回滚前可比对、执行前可确认”的前端闭环
|
||||
- 当前剩余更偏锦上添花的项:
|
||||
- 发布前治理说明文案进一步润色
|
||||
- 历史版本更多字段级 diff
|
||||
- 与 `/files` 或治理资产详情页的更深联动
|
||||
|
||||
Reference in New Issue
Block a user