feat: connect knowledge base publish workflow UI

This commit is contained in:
2026-05-15 13:30:58 +08:00
parent 95616c03d2
commit eddc53d043
5 changed files with 308 additions and 14 deletions
+209 -4
View File
@@ -6,9 +6,19 @@ import { Alert, Button, Card, Empty, Form, Input, List, Select, Space, Table, Ta
import type { UploadFile } from "antd/es/upload/interface";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createKnowledgeBase, listKnowledgeBaseDocuments, listKnowledgeBases, retryKnowledgeBaseDocument, uploadKnowledgeBaseDocument } from "@/services/knowledge-base";
import {
createKnowledgeBase,
listKnowledgeBaseDocuments,
listKnowledgeBaseVersions,
listKnowledgeBases,
publishKnowledgeBase,
retryKnowledgeBaseDocument,
rollbackKnowledgeBase,
unpublishKnowledgeBase,
uploadKnowledgeBaseDocument,
} from "@/services/knowledge-base";
import { useAuthStore } from "@/stores/auth.store";
import type { KnowledgeBaseDocumentItem, KnowledgeBaseItem } from "@/types/api";
import type { KnowledgeBaseDocumentItem, KnowledgeBaseItem, KnowledgeBaseVersionItem } from "@/types/api";
const { Dragger } = Upload;
const { Title, Text } = Typography;
@@ -23,6 +33,10 @@ type KnowledgeBaseFormValues = {
region?: string;
tags?: string;
};
type KnowledgeBaseRollbackFormValues = {
target_version_label: string;
change_summary?: string;
};
const DOCUMENT_STATUS_META: Record<string, { color: string; label: string }> = {
pending: { color: "gold", label: "待处理" },
@@ -91,6 +105,20 @@ function getRetrievalModeMeta(value: string | null | undefined) {
return { color: "default", label: value || "Default" };
}
function getKnowledgeBaseStatusMeta(value: string | null | undefined) {
const normalized = (value || "").toLowerCase();
if (normalized === "active") {
return { color: "green", label: "已发布" };
}
if (normalized === "archived") {
return { color: "default", label: "已下线" };
}
if (normalized === "draft") {
return { color: "gold", label: "草稿" };
}
return { color: "blue", label: value || "未命名状态" };
}
function getErrorMessage(error: unknown, fallback: string) {
return error instanceof Error && error.message ? error.message : fallback;
}
@@ -170,9 +198,13 @@ export default function KnowledgeBasesPage() {
const currentUser = useAuthStore((state) => state.currentUser);
const [createForm] = Form.useForm<KnowledgeBaseFormValues>();
const [uploadForm] = Form.useForm<KnowledgeBaseFormValues>();
const [rollbackForm] = Form.useForm<KnowledgeBaseRollbackFormValues>();
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [uploading, setUploading] = useState(false);
const [publishing, setPublishing] = useState(false);
const [unpublishing, setUnpublishing] = useState(false);
const [rollingBack, setRollingBack] = useState(false);
const [refreshingDocuments, setRefreshingDocuments] = useState(false);
const [retryingDocumentId, setRetryingDocumentId] = useState<string | null>(null);
const [listError, setListError] = useState<string | null>(null);
@@ -180,6 +212,8 @@ export default function KnowledgeBasesPage() {
const [items, setItems] = useState<KnowledgeBaseItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [documents, setDocuments] = useState<KnowledgeBaseDocumentItem[]>([]);
const [versions, setVersions] = useState<KnowledgeBaseVersionItem[]>([]);
const [versionsLoading, setVersionsLoading] = useState(false);
const [sourceTypeFilter, setSourceTypeFilter] = useState<string>("all");
const [regionFilter, setRegionFilter] = useState<string>("all");
const [documentsLoading, setDocumentsLoading] = useState(false);
@@ -224,6 +258,16 @@ export default function KnowledgeBasesPage() {
return [{ value: "all", label: "全部适用区域" }, ...values.map((value) => ({ value, label: value }))];
}, [documents]);
const selectedKnowledgeBaseStatus = getKnowledgeBaseStatusMeta(selectedKnowledgeBase?.status);
const rollbackVersionOptions = useMemo(
() =>
versions.map((item) => ({
value: item.version_label,
label: `${item.version_label} · ${item.status}`,
})),
[versions],
);
const loadKnowledgeBases = useCallback(async () => {
if (currentUser?.role !== "admin") {
setLoading(false);
@@ -283,6 +327,29 @@ export default function KnowledgeBasesPage() {
}
}, [messageApi]);
const loadVersions = useCallback(async (knowledgeBaseId: string) => {
setVersionsLoading(true);
try {
const result = await listKnowledgeBaseVersions(knowledgeBaseId);
if (result.code !== 0) {
messageApi.error(result.message || "获取知识库版本失败");
setVersions([]);
rollbackForm.resetFields(["target_version_label", "change_summary"]);
return;
}
setVersions(result.data);
rollbackForm.setFieldsValue({
target_version_label: result.data[0]?.version_label,
});
} catch (error: unknown) {
messageApi.error(getErrorMessage(error, "获取知识库版本失败,请稍后重试"));
setVersions([]);
rollbackForm.resetFields(["target_version_label", "change_summary"]);
} finally {
setVersionsLoading(false);
}
}, [messageApi, rollbackForm]);
useEffect(() => {
const timer = window.setTimeout(() => {
void loadKnowledgeBases();
@@ -296,14 +363,15 @@ export default function KnowledgeBasesPage() {
if (!selectedId || currentUser?.role !== "admin") {
setDocuments([]);
setDocumentsError(null);
setVersions([]);
return;
}
void loadDocuments(selectedId);
void Promise.all([loadDocuments(selectedId), loadVersions(selectedId)]);
}, 0);
return () => window.clearTimeout(timer);
}, [currentUser?.role, loadDocuments, selectedId]);
}, [currentUser?.role, loadDocuments, loadVersions, selectedId]);
useEffect(() => {
if (!selectedId || currentUser?.role !== "admin") {
@@ -421,6 +489,74 @@ export default function KnowledgeBasesPage() {
}
};
const handlePublishKnowledgeBase = async () => {
if (!selectedId) {
return;
}
setPublishing(true);
try {
const result = await publishKnowledgeBase(selectedId);
if (result.code !== 0) {
messageApi.error(result.message || "发布知识库失败");
return;
}
messageApi.success("知识库已发布");
await Promise.all([loadKnowledgeBases(), loadVersions(selectedId)]);
} catch (error: unknown) {
messageApi.error(getErrorMessage(error, "发布知识库失败,请稍后重试"));
} finally {
setPublishing(false);
}
};
const handleUnpublishKnowledgeBase = async () => {
if (!selectedId) {
return;
}
setUnpublishing(true);
try {
const result = await unpublishKnowledgeBase(selectedId);
if (result.code !== 0) {
messageApi.error(result.message || "下线知识库失败");
return;
}
messageApi.success("知识库已下线");
await Promise.all([loadKnowledgeBases(), loadVersions(selectedId)]);
} catch (error: unknown) {
messageApi.error(getErrorMessage(error, "下线知识库失败,请稍后重试"));
} finally {
setUnpublishing(false);
}
};
const handleRollbackKnowledgeBase = async () => {
if (!selectedId) {
return;
}
const values = await rollbackForm.validateFields();
setRollingBack(true);
try {
const result = await rollbackKnowledgeBase(selectedId, {
target_version_label: values.target_version_label,
change_summary: values.change_summary?.trim() || undefined,
});
if (result.code !== 0) {
messageApi.error(result.message || "回滚知识库失败");
return;
}
messageApi.success(`知识库已回滚到 ${values.target_version_label}`);
await Promise.all([loadKnowledgeBases(), loadVersions(selectedId)]);
} catch (error: unknown) {
if (error instanceof Error && error.message === "Validation failed") {
setRollingBack(false);
return;
}
messageApi.error(getErrorMessage(error, "回滚知识库失败,请稍后重试"));
} finally {
setRollingBack(false);
}
};
if (currentUser?.role !== "admin") {
return (
<main className="page-shell review-history-page">
@@ -514,6 +650,75 @@ export default function KnowledgeBasesPage() {
</Space>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Card className="review-surface-card" title="发布治理">
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
{selectedKnowledgeBase ? (
<>
<Space wrap size={[8, 8]}>
<Tag color={selectedKnowledgeBaseStatus.color}>{selectedKnowledgeBaseStatus.label}</Tag>
<Tag color={getRetrievalModeMeta(selectedKnowledgeBase.retrieval_mode).color}>
{getRetrievalModeMeta(selectedKnowledgeBase.retrieval_mode).label}
</Tag>
{selectedKnowledgeBase.current_version_label ? <Tag>当前版本 {selectedKnowledgeBase.current_version_label}</Tag> : null}
</Space>
<Text>{selectedKnowledgeBase.description || "当前知识库暂无说明。"}</Text>
<Space direction="vertical" size={4}>
<Text type="secondary">发布人:{selectedKnowledgeBase.published_by || "-"}</Text>
<Text type="secondary">发布时间:{selectedKnowledgeBase.published_at ? formatDateTime(selectedKnowledgeBase.published_at) : "-"}</Text>
</Space>
<Space wrap>
<Button type="primary" loading={publishing} onClick={() => void handlePublishKnowledgeBase()}>
发布知识库
</Button>
<Button loading={unpublishing} onClick={() => void handleUnpublishKnowledgeBase()}>
下线知识库
</Button>
</Space>
<Form form={rollbackForm} layout="vertical">
<Form.Item label="回滚到版本" name="target_version_label" rules={[{ required: true, message: "请选择目标版本" }]}>
<Select
placeholder="选择历史版本"
loading={versionsLoading}
options={rollbackVersionOptions}
disabled={!rollbackVersionOptions.length}
/>
</Form.Item>
<Form.Item label="回滚说明" name="change_summary">
<Input placeholder="例如:恢复到稳定知识库版本" />
</Form.Item>
</Form>
<Button loading={rollingBack} disabled={!rollbackVersionOptions.length} onClick={() => void handleRollbackKnowledgeBase()}>
回滚知识库
</Button>
<List
header="版本历史"
loading={versionsLoading}
locale={{ emptyText: "当前知识库还没有发布历史。" }}
dataSource={versions}
renderItem={(item) => (
<List.Item>
<Space direction="vertical" size={2} style={{ width: "100%" }}>
<Space wrap size={[8, 8]}>
<Text strong>{item.version_label}</Text>
<Tag color={getKnowledgeBaseStatusMeta(item.status).color}>{getKnowledgeBaseStatusMeta(item.status).label}</Tag>
</Space>
<Text type="secondary">{item.change_summary || "无变更说明"}</Text>
<Text type="secondary">
{item.published_at ? formatDateTime(item.published_at) : formatDateTime(item.created_at)}
{" · "}
{item.published_by || "-"}
</Text>
</Space>
</List.Item>
)}
/>
</>
) : (
<Alert showIcon type="info" message="请先从左侧选择知识库,再进行发布、下线或回滚。" />
)}
</Space>
</Card>
<Card className="review-surface-card" title="上传文档">
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Alert showIcon type="info" message={selectedKnowledgeBase ? `当前知识库:${selectedKnowledgeBase.name}` : "请先从左侧选择知识库"} />
+27
View File
@@ -4,6 +4,8 @@ import type {
KnowledgeBaseCreatePayload,
KnowledgeBaseDocumentItem,
KnowledgeBaseItem,
KnowledgeBaseRollbackPayload,
KnowledgeBaseVersionItem,
PageResponse,
} from "@/types/api";
@@ -41,6 +43,31 @@ export async function createKnowledgeBase(payload: KnowledgeBaseCreatePayload) {
return response.data;
}
export async function listKnowledgeBaseVersions(knowledgeBaseId: string) {
const response = await http.get<ApiResponse<KnowledgeBaseVersionItem[]>>(`/knowledge-bases/${knowledgeBaseId}/versions`);
return response.data;
}
export async function publishKnowledgeBase(knowledgeBaseId: string) {
const response = await http.post<ApiResponse<KnowledgeBaseItem>>(`/knowledge-bases/${knowledgeBaseId}/publish`);
return response.data;
}
export async function unpublishKnowledgeBase(knowledgeBaseId: string) {
const response = await http.post<ApiResponse<KnowledgeBaseItem>>(`/knowledge-bases/${knowledgeBaseId}/unpublish`);
return response.data;
}
export async function rollbackKnowledgeBase(knowledgeBaseId: string, payload: KnowledgeBaseRollbackPayload) {
const formData = new FormData();
formData.append("target_version_label", payload.target_version_label);
if (payload.change_summary) {
formData.append("change_summary", payload.change_summary);
}
const response = await http.post<ApiResponse<KnowledgeBaseItem>>(`/knowledge-bases/${knowledgeBaseId}/rollback`, formData);
return response.data;
}
export async function listKnowledgeBaseDocuments(knowledgeBaseId: string, page = 1, pageSize = 20) {
const response = await http.get<ApiResponse<PageResponse<KnowledgeBaseDocumentItem>>>(`/knowledge-bases/${knowledgeBaseId}/documents`, {
params: {
+22
View File
@@ -232,10 +232,27 @@ export type KnowledgeBaseItem = {
status: string;
document_count: number;
retrieval_mode?: string | null;
current_version_label?: string | null;
published_at?: string | null;
published_by?: string | null;
created_at: string;
updated_at: string;
};
export type KnowledgeBaseVersionItem = {
id: string;
knowledge_base_id: string;
version_label: string;
name: string;
description: string | null;
retrieval_mode?: string | null;
status: string;
published_at?: string | null;
published_by?: string | null;
change_summary?: string | null;
created_at: string;
};
export type KnowledgeBaseDocumentItem = {
id: string;
knowledge_base_id: string;
@@ -274,6 +291,11 @@ export type KnowledgeBaseCreatePayload = {
description?: string;
};
export type KnowledgeBaseRollbackPayload = {
target_version_label: string;
change_summary?: string;
};
export type KnowledgeBaseListResponse = PageResponse<KnowledgeBaseItem>;
export type KnowledgeBaseDocumentListResponse = PageResponse<KnowledgeBaseDocumentItem>;
@@ -1579,17 +1579,57 @@
- 从代码链角度看,`knowledge_bases` 的远端发布链已经补齐:
- 共享平台有真实端点
- 合同平台有 SDK / client / backend API 桥接
- 但当前 `shared-ai-platform-java` 仓库存在用户已有的未提交改动,不适合未经确认直接把整个仓库工作树部署到远端运行环境。
- 因此这轮状态定义为:
- 代码实现:已完成
- 本地 / smoke 验证:已完成
- 运行中 shared-ai 服务部署:待单独确认
- 合同平台前端知识库页已接上发布治理面板
- 运行中的 shared-ai Java 服务也已更新到包含这组端点的版本:
- 远端仓库 `/opt/shared-ai-platform-java` 已 `git pull --ff-only` 到 `f925913`
- `docker compose -f docker-compose.server.yml up -d --build shared-ai-platform-java` 已执行
- `/health` 正常
- 远端 HTTP smoke 已通过:
- `publish -> versions -> unpublish -> rollback`
- 当前可将 `knowledge_bases` 发布链定义为:
- 代码实现:完成
- 本地 / smoke 验证:完成
- 远端运行时验证:完成
### 当前阻塞项
- `knowledge_bases` 发布链当前已不再是代码缺口。
- 目前剩下的是发布层面阻塞:
- `D:\shared-ai-platform-java` 工作区含用户已有未提交改动,直接部署会混入无关变更
- 若要把运行中的 shared-ai Java 服务也切到新端点,需要先确认是否允许按当前工作树部署,或先整理 shared repo 交付边界
- `knowledge_bases` 发布链已无代码级或运行级阻塞。
## 2026-05-15 knowledge-bases 页面已接上发布链
### 本轮完成项
- `frontend/src/services/knowledge-base.ts`
- 已补齐知识库发布链前端服务:
- `listKnowledgeBaseVersions`
- `publishKnowledgeBase`
- `unpublishKnowledgeBase`
- `rollbackKnowledgeBase`
- `frontend/src/types/api.ts`
- 已补齐知识库发布链前端类型:
- `KnowledgeBaseVersionItem`
- `KnowledgeBaseRollbackPayload`
- `KnowledgeBaseItem.current_version_label / published_at / published_by`
- `frontend/src/app/knowledge-bases/page.tsx`
- 已新增“发布治理”面板:
- 展示当前知识库发布状态、当前版本、发布人、发布时间
- 支持“发布知识库”
- 支持“下线知识库”
- 支持查看版本历史
- 支持选择历史版本并执行“回滚知识库”
- 保持现有上传文档、异常文档恢复、索引状态表不变,避免把本页重新做成另一套管理台
### 本轮验证
- 前端检查:
- `npm --prefix frontend run lint`
- 结果:通过
- `npm --prefix frontend run build`
- 结果:通过
- 说明:
- `ai-contract-platform` 的前端与 backend bridge 已经接上
- `shared-ai-platform-java` 的知识库发布端点 smoke 也已在上一轮通过
### 当前状态更新
- `knowledge_bases` 发布链现在已经形成“共享平台远端端点 -> 合同平台 SDK/client/backend -> 合同平台前端页面”的完整代码闭环。
- 当前运行中的 shared-ai Java 服务已经切到包含这组新端点的版本,前端按钮可以按新链路继续验证。
## 2026-05-15 默认无参正式流程验证与治理状态刷新
@@ -12,7 +12,7 @@ batch,priority,area,task_id,title,current_gap,target_deliverable,depends_on,acce
5B,P0,backend_api,GOV-API-004,新增证据上传绑定接口,当前系统没有专门的治理证据绑定接口,新增 /governance/evidences 上传绑定替换校验接口,GOV-DB-004,系统可阻止模板证据进入已批准状态,COMPLETED
5B,P0,backend_api,GOV-API-005,新增上线批准汇总接口,当前没有系统内 release approval summary,新增 /governance/releases/summary 和 /governance/releases/{id} 接口,GOV-DB-005,前端可直接展示当前上线阻断项与通过项,COMPLETED
5B,P0,backend_api,GOV-API-006,新增CSV台账导入接口,当前正式上线台账只靠仓库脚本导入,新增 /governance/imports/formal-launch 接口读取既有 CSV 映射到治理表,GOV-DB-001;GOV-DB-002;GOV-DB-003;GOV-DB-004,旧台账可一次性导入数据库且保留来源信息,COMPLETED
5B,P1,backend_api,GOV-API-007,模板规则知识库发布接口,当前内容模块没有生产发布动作,为 templates review-rules knowledge-bases 增加 publish/unpublish/rollback 接口,GOV-DB-005;GOV-DB-006,已批准资产才能被发布到生产可用集合,IN_PROGRESS
5B,P1,backend_api,GOV-API-007,模板规则知识库发布接口,当前内容模块没有生产发布动作,为 templates review-rules knowledge-bases 增加 publish/unpublish/rollback 接口,GOV-DB-005;GOV-DB-006,已批准资产才能被发布到生产可用集合,COMPLETED
5C,P0,frontend,GOV-WEB-001,新增复核总控中心页面,当前没有统一治理控制台,新增 /governance 页面展示资产统计阻断项角色视图和发布批次,GOV-API-001;GOV-API-005,管理员可在单页看到当前正式上线治理全貌,COMPLETED
5C,P0,frontend,GOV-WEB-002,新增复核人工作台页面,当前真实法务业务运营没有系统内待办页,新增 /governance/workbench 页面支持按角色查看待处理任务,GOV-API-001;GOV-API-002;GOV-API-003,复核人可只看到与自己相关的任务包,COMPLETED
5C,P0,frontend,GOV-WEB-003,新增资产详情与证据页,当前无法在系统内查看单资产的签署链和证据链,新增治理资产详情页和证据面板,GOV-API-001;GOV-API-003;GOV-API-004,可在页面内查看每个资产的复核记录和证据状态,COMPLETED
1 batch priority area task_id title current_gap target_deliverable depends_on acceptance status
12 5B P0 backend_api GOV-API-004 新增证据上传绑定接口 当前系统没有专门的治理证据绑定接口 新增 /governance/evidences 上传绑定替换校验接口 GOV-DB-004 系统可阻止模板证据进入已批准状态 COMPLETED
13 5B P0 backend_api GOV-API-005 新增上线批准汇总接口 当前没有系统内 release approval summary 新增 /governance/releases/summary 和 /governance/releases/{id} 接口 GOV-DB-005 前端可直接展示当前上线阻断项与通过项 COMPLETED
14 5B P0 backend_api GOV-API-006 新增CSV台账导入接口 当前正式上线台账只靠仓库脚本导入 新增 /governance/imports/formal-launch 接口读取既有 CSV 映射到治理表 GOV-DB-001;GOV-DB-002;GOV-DB-003;GOV-DB-004 旧台账可一次性导入数据库且保留来源信息 COMPLETED
15 5B P1 backend_api GOV-API-007 模板规则知识库发布接口 当前内容模块没有生产发布动作 为 templates review-rules knowledge-bases 增加 publish/unpublish/rollback 接口 GOV-DB-005;GOV-DB-006 已批准资产才能被发布到生产可用集合 IN_PROGRESS COMPLETED
16 5C P0 frontend GOV-WEB-001 新增复核总控中心页面 当前没有统一治理控制台 新增 /governance 页面展示资产统计阻断项角色视图和发布批次 GOV-API-001;GOV-API-005 管理员可在单页看到当前正式上线治理全貌 COMPLETED
17 5C P0 frontend GOV-WEB-002 新增复核人工作台页面 当前真实法务业务运营没有系统内待办页 新增 /governance/workbench 页面支持按角色查看待处理任务 GOV-API-001;GOV-API-002;GOV-API-003 复核人可只看到与自己相关的任务包 COMPLETED
18 5C P0 frontend GOV-WEB-003 新增资产详情与证据页 当前无法在系统内查看单资产的签署链和证据链 新增治理资产详情页和证据面板 GOV-API-001;GOV-API-003;GOV-API-004 可在页面内查看每个资产的复核记录和证据状态 COMPLETED