refactor(knowledge): keep latest version only
This commit is contained in:
@@ -2,15 +2,12 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import { api } from "../services/api";
|
||||
import type { KnowledgeVersion, KnowledgeVersionContent } from "../types/api";
|
||||
import type { KnowledgeVersionContent } from "../types/api";
|
||||
|
||||
const props = defineProps<{
|
||||
versions: KnowledgeVersion[];
|
||||
versionId: number | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ "update:versionId": [versionId: number] }>();
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const content = ref<KnowledgeVersionContent | null>(null);
|
||||
@@ -50,22 +47,6 @@ async function loadVersion(versionId: number | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectVersion(value: number) {
|
||||
emit("update:versionId", value);
|
||||
}
|
||||
|
||||
function versionStatus(version: KnowledgeVersion) {
|
||||
const labels: Record<string, string> = {
|
||||
published: "正式版本",
|
||||
processing: "同步处理中",
|
||||
pending_review: "未生效版本",
|
||||
approved: "未生效版本",
|
||||
rejected: "未生效版本",
|
||||
discarded: "已废弃版本",
|
||||
superseded: "历史版本",
|
||||
};
|
||||
return labels[version.status] ?? version.status;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,22 +54,8 @@ function versionStatus(version: KnowledgeVersion) {
|
||||
<header class="content-preview-toolbar">
|
||||
<div>
|
||||
<h4>知识库内容预览</h4>
|
||||
<p>按同步后的章节结构查看实际进入检索链路的内容。</p>
|
||||
<p>查看当前最新版实际进入检索链路的章节内容。</p>
|
||||
</div>
|
||||
<el-select
|
||||
:model-value="versionId"
|
||||
class="content-version-select"
|
||||
placeholder="选择预览版本"
|
||||
:disabled="versions.length === 0"
|
||||
@update:model-value="selectVersion"
|
||||
>
|
||||
<el-option
|
||||
v-for="version in versions"
|
||||
:key="version.id"
|
||||
:label="`V${version.versionNo} · ${versionStatus(version)}`"
|
||||
:value="version.id"
|
||||
/>
|
||||
</el-select>
|
||||
</header>
|
||||
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon>
|
||||
@@ -150,7 +117,6 @@ function versionStatus(version: KnowledgeVersion) {
|
||||
.content-preview-toolbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 16px; }
|
||||
.content-preview-toolbar h4 { margin: 0; font-size: 17px; color: #203b32; }
|
||||
.content-preview-toolbar p { margin: 6px 0 0; color: #71817b; font-size: 13px; }
|
||||
.content-version-select { width: 220px; flex: 0 0 220px; }
|
||||
.content-preview-summary { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
||||
.content-preview-summary span { padding: 5px 10px; border-radius: 999px; background: #f0f6f3; color: #526860; font-size: 12px; }
|
||||
.content-preview-layout { display: grid; grid-template-columns: 250px minmax(0, 1fr); height: clamp(420px, 62vh, 680px); min-height: 0; overflow: hidden; border: 1px solid #dfe8e5; border-radius: 12px; background: #fff; }
|
||||
@@ -170,7 +136,6 @@ function versionStatus(version: KnowledgeVersion) {
|
||||
.content-card-panel { margin-top: 16px; }
|
||||
@media (max-width: 900px) {
|
||||
.content-preview-toolbar { align-items: stretch; flex-direction: column; }
|
||||
.content-version-select { width: 100%; flex-basis: auto; }
|
||||
.content-preview-layout { grid-template-columns: 1fr; height: auto; min-height: 0; overflow: visible; }
|
||||
.content-outline { display: flex; height: auto; gap: 8px; overflow-x: auto; overflow-y: hidden; border-right: 0; border-bottom: 1px solid #e5ece9; }
|
||||
.content-outline > strong { display: none; }
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
KnowledgeDetail,
|
||||
KnowledgeItem,
|
||||
KnowledgeSyncJob,
|
||||
KnowledgeVersion,
|
||||
MigrationSummary,
|
||||
} from "../types/api";
|
||||
|
||||
@@ -22,7 +21,6 @@ const loading = ref(false);
|
||||
const operatingId = ref<number | null>(null);
|
||||
const detailOpen = ref(false);
|
||||
const detail = ref<KnowledgeDetail | null>(null);
|
||||
const versions = ref<KnowledgeVersion[]>([]);
|
||||
const jobs = ref<KnowledgeSyncJob[]>([]);
|
||||
const previewVersionId = ref<number | null>(null);
|
||||
const activeTab = ref("overview");
|
||||
@@ -136,11 +134,6 @@ async function handleMore(command: string, item: KnowledgeItem) {
|
||||
try {
|
||||
if (command === "detail") return await openDetail(item);
|
||||
if (command === "preview") return emit("preview", item.id);
|
||||
if (command === "versions") {
|
||||
await openDetail(item);
|
||||
if (detailOpen.value) activeTab.value = "versions";
|
||||
return;
|
||||
}
|
||||
if (command === "archive") return await archive(item);
|
||||
if (command === "restore") return await restore(item);
|
||||
if (command === "delete") return await remove(item);
|
||||
@@ -196,7 +189,7 @@ async function openDetail(item: KnowledgeItem) {
|
||||
previewVersionId.value = null;
|
||||
try {
|
||||
const refreshedDetail = await refreshDetail(item.id);
|
||||
previewVersionId.value = refreshedDetail.currentVersion?.id ?? versions.value[0]?.id ?? null;
|
||||
previewVersionId.value = refreshedDetail.currentVersion?.id ?? null;
|
||||
} catch (error) {
|
||||
detailOpen.value = false;
|
||||
ElMessage.error(errorMessage(error, "知识库详情加载失败"));
|
||||
@@ -204,11 +197,10 @@ async function openDetail(item: KnowledgeItem) {
|
||||
}
|
||||
|
||||
async function refreshDetail(id: number) {
|
||||
const [nextDetail, nextVersions, nextJobs] = await Promise.all([
|
||||
api.knowledgeDetail(id), api.knowledgeVersions(id), api.knowledgeSyncJobs(id),
|
||||
const [nextDetail, nextJobs] = await Promise.all([
|
||||
api.knowledgeDetail(id), api.knowledgeSyncJobs(id),
|
||||
]);
|
||||
detail.value = nextDetail;
|
||||
versions.value = nextVersions;
|
||||
jobs.value = nextJobs;
|
||||
return nextDetail;
|
||||
}
|
||||
@@ -234,7 +226,7 @@ async function sync(item: KnowledgeItem) {
|
||||
|
||||
async function runMigration() {
|
||||
if (!await confirmAction(
|
||||
"将依次重新读取全部飞书文章。每个知识库同步成功后会立即替换为最新版本,原版本仍可回滚。",
|
||||
"将依次重新读取全部飞书文章。同步成功后仅保留最新版,旧版本和旧内容会被清理。",
|
||||
"开始知识迁移",
|
||||
{ confirmButtonText: "开始迁移", cancelButtonText: "取消", type: "warning" },
|
||||
)) return;
|
||||
@@ -250,23 +242,6 @@ async function runMigration() {
|
||||
}
|
||||
}
|
||||
|
||||
async function rollback(version: KnowledgeVersion) {
|
||||
if (!detail.value) return;
|
||||
if (!await confirmAction(
|
||||
`将从当前版本回滚到 V${version.versionNo},从用户下一条消息开始生效。`,
|
||||
"回滚知识版本",
|
||||
{ confirmButtonText: "确认回滚", cancelButtonText: "取消", type: "warning" },
|
||||
)) return;
|
||||
try {
|
||||
await api.rollbackKnowledgeVersion(detail.value.knowledge.id, version.id, detail.value.knowledge.status === 1);
|
||||
ElMessage.success("知识版本已回滚");
|
||||
await load();
|
||||
await refreshDetail(detail.value.knowledge.id);
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "版本回滚失败"));
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleOpen(item: KnowledgeItem): Promise<boolean> {
|
||||
const next = item.status !== 1;
|
||||
if (!await confirmAction(
|
||||
@@ -289,7 +264,7 @@ async function toggleOpen(item: KnowledgeItem): Promise<boolean> {
|
||||
}
|
||||
|
||||
async function archive(item: KnowledgeItem) {
|
||||
if (!await confirmAction("归档后知识库会立即关闭,但历史正式版本和检索记录将保留。", "归档知识库", {
|
||||
if (!await confirmAction("归档后知识库会立即关闭,当前最新版和检索记录仍会保留。", "归档知识库", {
|
||||
confirmButtonText: "确认归档", cancelButtonText: "取消", type: "warning",
|
||||
})) return;
|
||||
await api.setKnowledgeLifecycle(item.id, "archived");
|
||||
@@ -308,11 +283,6 @@ async function remove(item: KnowledgeItem) {
|
||||
await load();
|
||||
}
|
||||
|
||||
async function showVersion(version: KnowledgeVersion) {
|
||||
previewVersionId.value = version.id;
|
||||
activeTab.value = "content";
|
||||
}
|
||||
|
||||
function statusType(status: string) {
|
||||
if (["published", "passed", "normal", "completed"].includes(status)) return "success";
|
||||
if (["failed", "blocked", "error", "rejected"].includes(status)) return "danger";
|
||||
@@ -344,7 +314,7 @@ async function confirmAction(
|
||||
|
||||
<template>
|
||||
<div class="page-head inline">
|
||||
<div><h2>知识库管理</h2><p>一篇飞书文章对应一个知识库;同步成功后最新版本自动生效,历史版本可随时回滚。</p></div>
|
||||
<div><h2>知识库管理</h2><p>一篇飞书文章对应一个知识库;同步成功后自动生效,并且只保留最新版。</p></div>
|
||||
<el-button type="primary" :loading="loading" @click="runMigration">迁移现有知识库</el-button>
|
||||
</div>
|
||||
|
||||
@@ -393,7 +363,7 @@ async function confirmAction(
|
||||
<el-table-column label="当前版本" width="130"><template #default="{ row }"><span>{{ row.currentVersionId ? `#${row.currentVersionId}` : '尚未同步' }}</span></template></el-table-column>
|
||||
<el-table-column label="状态" width="210"><template #default="{ row }"><div class="knowledge-status-cell"><el-switch v-model="row.status" :active-value="1" :inactive-value="0" active-text="开放" inactive-text="关闭" inline-prompt :loading="operatingId === row.id" :disabled="row.lifecycleStatus === 'archived'" :before-change="() => toggleOpen(row)" /><el-tag :type="statusType(row.sourceStatus)">{{ row.sourceStatus === 'normal' ? '源正常' : '源异常' }}</el-tag></div></template></el-table-column>
|
||||
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
||||
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row)">同步最新版本</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item command="versions">查看版本</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" :disabled="row.lifecycleStatus !== 'archived'" divided>删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row)">同步最新版</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" :disabled="row.lifecycleStatus !== 'archived'" divided>删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
||||
</el-table>
|
||||
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="load" />
|
||||
|
||||
@@ -411,9 +381,8 @@ async function confirmAction(
|
||||
<section v-if="detail.manifest" class="manifest-panel"><h4>Knowledge Manifest</h4><p><strong>用途:</strong>{{ detail.manifest.purpose }}</p><p><strong>适用:</strong>{{ detail.manifest.applicableQuestions }}</p><p><strong>不适用:</strong>{{ detail.manifest.inapplicableQuestions }}</p><p><strong>核心主题:</strong>{{ detail.manifest.coreTopics }}</p><p><strong>边界:</strong>{{ detail.manifest.boundaries }}</p></section>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="内容预览" name="content" lazy>
|
||||
<KnowledgeContentPreview v-model:version-id="previewVersionId" :versions="versions" />
|
||||
<KnowledgeContentPreview :version-id="previewVersionId" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="版本记录" name="versions"><el-table :data="versions"><el-table-column label="版本" width="90"><template #default="{ row }">V{{ row.versionNo }}</template></el-table-column><el-table-column prop="status" label="状态" width="130" /><el-table-column prop="qualityStatus" label="质检" width="120" /><el-table-column prop="createdAt" label="生成时间" /><el-table-column prop="publishedAt" label="发布时间" /><el-table-column label="操作" width="180"><template #default="{ row }"><el-button link type="primary" @click="showVersion(row)">查看内容</el-button><el-button v-if="['published','superseded'].includes(row.status) && row.id !== detail?.knowledge.currentVersionId" link @click="rollback(row)">回滚</el-button></template></el-table-column></el-table></el-tab-pane>
|
||||
<el-tab-pane label="同步记录" name="jobs"><el-table :data="jobs"><el-table-column prop="id" label="任务" width="80" /><el-table-column prop="status" label="状态" width="130" /><el-table-column prop="stage" label="阶段" width="130" /><el-table-column label="进度" width="180"><template #default="{ row }"><el-progress :percentage="row.progress" /></template></el-table-column><el-table-column prop="sourceError" label="失败原因" min-width="260" /></el-table></el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
KnowledgeItem,
|
||||
KnowledgeDetail,
|
||||
KnowledgeSyncJob,
|
||||
KnowledgeVersion,
|
||||
KnowledgeVersionContent,
|
||||
MigrationSummary,
|
||||
KnowledgeBatchUpdateResult,
|
||||
@@ -150,13 +149,10 @@ export const api = {
|
||||
{ method: "POST", body: JSON.stringify(body) },
|
||||
),
|
||||
knowledgeDetail: (id: number) => request<KnowledgeDetail>(`/admin/knowledge/${id}/detail`),
|
||||
knowledgeVersions: (id: number) => request<KnowledgeVersion[]>(`/admin/knowledge/${id}/versions`),
|
||||
knowledgeVersionContent: (versionId: number) => request<KnowledgeVersionContent>(`/admin/knowledge/version/${versionId}/content`),
|
||||
knowledgeSyncJobs: (id: number) => request<KnowledgeSyncJob[]>(`/admin/knowledge/${id}/sync-jobs`),
|
||||
syncKnowledge: (id: number) =>
|
||||
request<KnowledgeSyncJob>(`/admin/knowledge/${id}/sync`, { method: "POST" }),
|
||||
rollbackKnowledgeVersion: (id: number, versionId: number, keepOpen: boolean) =>
|
||||
request<KnowledgeItem>(`/admin/knowledge/${id}/version/${versionId}/rollback`, { method: "POST", body: JSON.stringify({ keepOpen }) }),
|
||||
setKnowledgeOpen: (id: number, open: boolean) =>
|
||||
request<KnowledgeItem>(`/admin/knowledge/${id}/open-status`, { method: "PUT", body: JSON.stringify({ open }) }),
|
||||
setKnowledgeLifecycle: (id: number, status: "active" | "archived") =>
|
||||
|
||||
Reference in New Issue
Block a user