From eddc53d043186e4ebe7f231989a1982583bbd362 Mon Sep 17 00:00:00 2001 From: yuqei Date: Fri, 15 May 2026 13:30:58 +0800 Subject: [PATCH] feat: connect knowledge base publish workflow UI --- frontend/src/app/knowledge-bases/page.tsx | 213 +++++++++++++++++- frontend/src/services/knowledge-base.ts | 27 +++ frontend/src/types/api.ts | 22 ++ ...µ·草与审查助手-当前工作进度保存-2026-05-11.md | 58 ++++- .../governance-productization-backlog.csv | 2 +- 5 files changed, 308 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/knowledge-bases/page.tsx b/frontend/src/app/knowledge-bases/page.tsx index e1c4f6f6..8c71c5a4 100644 --- a/frontend/src/app/knowledge-bases/page.tsx +++ b/frontend/src/app/knowledge-bases/page.tsx @@ -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 = { 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(); const [uploadForm] = Form.useForm(); + const [rollbackForm] = Form.useForm(); 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(null); const [listError, setListError] = useState(null); @@ -180,6 +212,8 @@ export default function KnowledgeBasesPage() { const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(null); const [documents, setDocuments] = useState([]); + const [versions, setVersions] = useState([]); + const [versionsLoading, setVersionsLoading] = useState(false); const [sourceTypeFilter, setSourceTypeFilter] = useState("all"); const [regionFilter, setRegionFilter] = useState("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 (
@@ -514,6 +650,75 @@ export default function KnowledgeBasesPage() { + + + {selectedKnowledgeBase ? ( + <> + + {selectedKnowledgeBaseStatus.label} + + {getRetrievalModeMeta(selectedKnowledgeBase.retrieval_mode).label} + + {selectedKnowledgeBase.current_version_label ? 当前版本 {selectedKnowledgeBase.current_version_label} : null} + + {selectedKnowledgeBase.description || "当前知识库暂无说明。"} + + 发布人:{selectedKnowledgeBase.published_by || "-"} + 发布时间:{selectedKnowledgeBase.published_at ? formatDateTime(selectedKnowledgeBase.published_at) : "-"} + + + + + +
+ + + +
+ + ( + + + + {item.version_label} + {getKnowledgeBaseStatusMeta(item.status).label} + + {item.change_summary || "无变更说明"} + + {item.published_at ? formatDateTime(item.published_at) : formatDateTime(item.created_at)} + {" · "} + {item.published_by || "-"} + + + + )} + /> + + ) : ( + + )} +
+
+ diff --git a/frontend/src/services/knowledge-base.ts b/frontend/src/services/knowledge-base.ts index ef7a1554..26e3167a 100644 --- a/frontend/src/services/knowledge-base.ts +++ b/frontend/src/services/knowledge-base.ts @@ -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>(`/knowledge-bases/${knowledgeBaseId}/versions`); + return response.data; +} + +export async function publishKnowledgeBase(knowledgeBaseId: string) { + const response = await http.post>(`/knowledge-bases/${knowledgeBaseId}/publish`); + return response.data; +} + +export async function unpublishKnowledgeBase(knowledgeBaseId: string) { + const response = await http.post>(`/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>(`/knowledge-bases/${knowledgeBaseId}/rollback`, formData); + return response.data; +} + export async function listKnowledgeBaseDocuments(knowledgeBaseId: string, page = 1, pageSize = 20) { const response = await http.get>>(`/knowledge-bases/${knowledgeBaseId}/documents`, { params: { diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 8f58a07c..31e0ff19 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -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; export type KnowledgeBaseDocumentListResponse = PageResponse; diff --git a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md b/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md index 11fa07a1..77a551d2 100644 --- a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md +++ b/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md @@ -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 默认无参正式流程验证与治理状态刷新 diff --git a/project-docs/formal-launch-ops/governance-productization-backlog.csv b/project-docs/formal-launch-ops/governance-productization-backlog.csv index 8b53b9d9..07eee8ec 100644 --- a/project-docs/formal-launch-ops/governance-productization-backlog.csv +++ b/project-docs/formal-launch-ops/governance-productization-backlog.csv @@ -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