feat: 完善知识库版本管理与检索审计后台
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
|
||||
import { api } from "../services/api";
|
||||
import type {
|
||||
KnowledgeDetail,
|
||||
KnowledgeItem,
|
||||
KnowledgeSyncJob,
|
||||
KnowledgeVersion,
|
||||
KnowledgeVersionContent,
|
||||
MigrationSummary,
|
||||
} from "../types/api";
|
||||
|
||||
const rows = ref<KnowledgeItem[]>([]);
|
||||
const migration = ref<MigrationSummary | null>(null);
|
||||
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 versionContent = ref<KnowledgeVersionContent | null>(null);
|
||||
const activeTab = ref("overview");
|
||||
const filter = reactive({ keyword: "", type: "", open: "", source: "", pending: "" });
|
||||
const form = reactive({
|
||||
name: "",
|
||||
feishuSpaceId: "",
|
||||
feishuNodeId: "",
|
||||
knowledgeType: "general",
|
||||
reviewMode: "manual",
|
||||
status: 0,
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const filteredRows = computed(() => rows.value.filter((item) => {
|
||||
if (filter.keyword && !`${item.name}${item.sourceTitle || ""}`.includes(filter.keyword)) return false;
|
||||
if (filter.type && item.knowledgeType !== filter.type) return false;
|
||||
if (filter.open && String(item.status) !== filter.open) return false;
|
||||
if (filter.source && item.sourceStatus !== filter.source) return false;
|
||||
if (filter.pending === "yes" && !item.pendingVersionId) return false;
|
||||
if (filter.pending === "no" && item.pendingVersionId) return false;
|
||||
return true;
|
||||
}));
|
||||
|
||||
onMounted(load);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
[rows.value, migration.value] = await Promise.all([api.knowledge(), api.migrationSummary()]);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createKnowledge() {
|
||||
if (!form.name || !form.feishuSpaceId || !form.feishuNodeId) {
|
||||
ElMessage.warning("请填写名称、SpaceID 和 NodeID");
|
||||
return;
|
||||
}
|
||||
await api.createKnowledge({ ...form });
|
||||
Object.assign(form, { name: "", feishuSpaceId: "", feishuNodeId: "", knowledgeType: "general", reviewMode: "manual", status: 0, remark: "" });
|
||||
ElMessage.success("知识库已新增,请进行首次同步");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function openDetail(item: KnowledgeItem) {
|
||||
detailOpen.value = true;
|
||||
activeTab.value = "overview";
|
||||
versionContent.value = null;
|
||||
await refreshDetail(item.id);
|
||||
}
|
||||
|
||||
async function refreshDetail(id: number) {
|
||||
[detail.value, versions.value, jobs.value] = await Promise.all([
|
||||
api.knowledgeDetail(id), api.knowledgeVersions(id), api.knowledgeSyncJobs(id),
|
||||
]);
|
||||
}
|
||||
|
||||
async function sync(item: KnowledgeItem, mode: "review" | "replace") {
|
||||
operatingId.value = item.id;
|
||||
try {
|
||||
await api.syncKnowledge(item.id, mode);
|
||||
ElMessage.success("同步处理完成,已生成待审核版本");
|
||||
await load();
|
||||
if (detailOpen.value) await refreshDetail(item.id);
|
||||
} finally {
|
||||
operatingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigration() {
|
||||
await ElMessageBox.confirm(
|
||||
"将依次重新读取全部现有飞书文章并生成待审核版本。迁移审核完成前,新检索链路不会使用这些知识库。",
|
||||
"开始知识迁移",
|
||||
{ confirmButtonText: "开始迁移", cancelButtonText: "取消", type: "warning" },
|
||||
);
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await api.runKnowledgeMigration();
|
||||
ElMessage.success(`迁移处理完成:${result.success}/${result.total} 成功`);
|
||||
await load();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmManifest() {
|
||||
if (!detail.value?.pendingVersion) return;
|
||||
await api.confirmKnowledgeManifest(detail.value.knowledge.id, detail.value.pendingVersion.id);
|
||||
ElMessage.success("知识库说明卡已确认");
|
||||
await refreshDetail(detail.value.knowledge.id);
|
||||
}
|
||||
|
||||
async function review(action: "approve" | "reject") {
|
||||
if (!detail.value?.pendingVersion) return;
|
||||
let reason = "";
|
||||
if (action === "reject") {
|
||||
const result = await ElMessageBox.prompt(
|
||||
"请记录拒绝原因。知识内容不能在后台修改,请前往飞书源文章修改后重新同步。",
|
||||
"拒绝版本",
|
||||
{ confirmButtonText: "确认拒绝", cancelButtonText: "取消", inputValidator: (value) => Boolean(value?.trim()) || "请填写拒绝原因" },
|
||||
);
|
||||
reason = result.value;
|
||||
}
|
||||
await api.reviewKnowledgeVersion(detail.value.knowledge.id, detail.value.pendingVersion.id, action, reason);
|
||||
ElMessage.success(action === "approve" ? "版本已审核通过" : "版本已拒绝");
|
||||
await refreshDetail(detail.value.knowledge.id);
|
||||
}
|
||||
|
||||
async function publish(keepOpen: boolean) {
|
||||
if (!detail.value?.pendingVersion) return;
|
||||
await ElMessageBox.confirm(
|
||||
`将发布 V${detail.value.pendingVersion.versionNo},从用户下一条消息开始生效;发布后知识库将${keepOpen ? "保持开放" : "处于关闭状态"}。`,
|
||||
"发布知识版本",
|
||||
{ confirmButtonText: "确认发布", cancelButtonText: "取消", type: "warning" },
|
||||
);
|
||||
await api.publishKnowledgeVersion(detail.value.knowledge.id, detail.value.pendingVersion.id, keepOpen);
|
||||
ElMessage.success("正式版本已发布");
|
||||
await load();
|
||||
await refreshDetail(detail.value.knowledge.id);
|
||||
}
|
||||
|
||||
async function rollback(version: KnowledgeVersion) {
|
||||
if (!detail.value) return;
|
||||
await ElMessageBox.confirm(
|
||||
`将从当前版本回滚到 V${version.versionNo},从用户下一条消息开始生效。`,
|
||||
"回滚知识版本",
|
||||
{ confirmButtonText: "确认回滚", cancelButtonText: "取消", type: "warning" },
|
||||
);
|
||||
await api.rollbackKnowledgeVersion(detail.value.knowledge.id, version.id, detail.value.knowledge.status === 1);
|
||||
ElMessage.success("知识版本已回滚");
|
||||
await load();
|
||||
await refreshDetail(detail.value.knowledge.id);
|
||||
}
|
||||
|
||||
async function toggleOpen(item: KnowledgeItem) {
|
||||
const next = item.status !== 1;
|
||||
await ElMessageBox.confirm(
|
||||
next ? "开放后,该知识库将从用户下一条消息开始进入 Agent 动态知识目录。" : "关闭后,当前生成不受影响,从用户下一条消息开始无法检索该知识库。",
|
||||
next ? "开放知识库" : "关闭知识库",
|
||||
{ confirmButtonText: next ? "确认开放" : "确认关闭", cancelButtonText: "取消", type: "warning" },
|
||||
);
|
||||
await api.setKnowledgeOpen(item.id, next);
|
||||
ElMessage.success(next ? "知识库已开放" : "知识库已关闭");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function archive(item: KnowledgeItem) {
|
||||
await ElMessageBox.confirm("归档后知识库会立即关闭,但历史正式版本和检索记录将保留。", "归档知识库", {
|
||||
confirmButtonText: "确认归档", cancelButtonText: "取消", type: "warning",
|
||||
});
|
||||
await api.setKnowledgeLifecycle(item.id, "archived");
|
||||
ElMessage.success("知识库已归档");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function remove(item: KnowledgeItem) {
|
||||
await ElMessageBox.confirm(
|
||||
`将物理删除“${item.name}”及关联数据。该操作不可恢复,并可能影响历史检索记录定位。`,
|
||||
"物理删除知识库",
|
||||
{ confirmButtonText: "确认删除", cancelButtonText: "取消", type: "error" },
|
||||
);
|
||||
await api.deleteKnowledge(item.id);
|
||||
ElMessage.success("知识库已删除");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function showVersion(version: KnowledgeVersion) {
|
||||
versionContent.value = await api.knowledgeVersionContent(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";
|
||||
if (["pending_review", "processing", "pending"].includes(status)) return "warning";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function typeLabel(value: string) {
|
||||
return { course: "课程知识库", qa: "答疑知识库", general: "通用知识库" }[value] || value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-head inline">
|
||||
<div><h2>知识库管理</h2><p>一篇飞书文章对应一个知识库;同步、审核和发布相互独立。</p></div>
|
||||
<el-button type="primary" :loading="loading" @click="runMigration">迁移现有知识库</el-button>
|
||||
</div>
|
||||
|
||||
<section class="migration-overview">
|
||||
<div><span>知识库总数</span><strong>{{ migration?.total ?? 0 }}</strong></div>
|
||||
<div><span>待迁移</span><strong>{{ migration?.counts.pending ?? 0 }}</strong></div>
|
||||
<div><span>待审核</span><strong>{{ migration?.counts.pending_review ?? 0 }}</strong></div>
|
||||
<div><span>迁移完成</span><strong>{{ migration?.counts.completed ?? 0 }}</strong></div>
|
||||
<div><span>失败</span><strong>{{ migration?.counts.failed ?? 0 }}</strong></div>
|
||||
</section>
|
||||
|
||||
<section class="knowledge-work-panel">
|
||||
<h3>新增知识库</h3>
|
||||
<div class="knowledge-create-grid">
|
||||
<el-input v-model="form.feishuSpaceId" placeholder="SpaceID" />
|
||||
<el-input v-model="form.feishuNodeId" placeholder="NodeID" />
|
||||
<el-input v-model="form.name" placeholder="后台显示名称" />
|
||||
<el-select v-model="form.knowledgeType">
|
||||
<el-option label="课程知识库" value="course" /><el-option label="答疑知识库" value="qa" /><el-option label="通用知识库" value="general" />
|
||||
</el-select>
|
||||
<el-select v-model="form.reviewMode">
|
||||
<el-option label="必须人工审核" value="manual" /><el-option label="允许自动发布" value="automatic" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="createKnowledge">保存并准备同步</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="knowledge-filters">
|
||||
<el-input v-model="filter.keyword" placeholder="搜索后台名称或飞书标题" clearable />
|
||||
<el-select v-model="filter.type" placeholder="知识库类型" clearable><el-option label="课程" value="course" /><el-option label="答疑" value="qa" /><el-option label="通用" value="general" /></el-select>
|
||||
<el-select v-model="filter.open" placeholder="开放状态" clearable><el-option label="已开放" value="1" /><el-option label="已关闭" value="0" /></el-select>
|
||||
<el-select v-model="filter.source" placeholder="源状态" clearable><el-option label="正常" value="normal" /><el-option label="源异常" value="error" /></el-select>
|
||||
<el-select v-model="filter.pending" placeholder="待发布版本" clearable><el-option label="有待发布版本" value="yes" /><el-option label="无待发布版本" value="no" /></el-select>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="filteredRows" stripe>
|
||||
<el-table-column label="知识库" min-width="240"><template #default="{ row }"><strong>{{ row.name }}</strong><small class="table-subline">{{ row.sourceTitle || "尚未同步源标题" }}</small></template></el-table-column>
|
||||
<el-table-column label="类型/审核" width="150"><template #default="{ row }">{{ typeLabel(row.knowledgeType) }}<small class="table-subline">{{ row.reviewMode === 'manual' ? '人工审核' : '允许自动发布' }}</small></template></el-table-column>
|
||||
<el-table-column label="版本" width="130"><template #default="{ row }"><span>正式 {{ row.currentVersionId ? `#${row.currentVersionId}` : '无' }}</span><small class="table-subline">待发布 {{ row.pendingVersionId ? `#${row.pendingVersionId}` : '无' }}</small></template></el-table-column>
|
||||
<el-table-column label="状态" width="160"><template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? '已开放' : '已关闭' }}</el-tag><el-tag class="status-gap" :type="statusType(row.sourceStatus)">{{ row.sourceStatus === 'normal' ? '源正常' : '源异常' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
||||
<el-table-column label="操作" width="270" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :loading="operatingId === row.id" @click="sync(row, 'review')">同步</el-button><el-button link @click="toggleOpen(row)">{{ row.status === 1 ? '关闭' : '开放' }}</el-button><el-dropdown trigger="click"><el-button link>更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item @click="sync(row, 'replace')">同步并准备替换</el-dropdown-item><el-dropdown-item @click="archive(row)">归档</el-dropdown-item><el-dropdown-item divided @click="remove(row)">物理删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></template></el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-drawer v-model="detailOpen" size="76%" title="知识库详情" destroy-on-close>
|
||||
<template v-if="detail">
|
||||
<div class="knowledge-detail-head">
|
||||
<div><h3>{{ detail.knowledge.name }}</h3><p>{{ typeLabel(detail.knowledge.knowledgeType) }} · {{ detail.knowledge.reviewMode === 'manual' ? '必须人工审核' : '允许自动发布' }}</p></div>
|
||||
<div><el-tag :type="detail.knowledge.status === 1 ? 'success' : 'info'">{{ detail.knowledge.status === 1 ? '已开放' : '已关闭' }}</el-tag><el-tag :type="statusType(detail.knowledge.sourceStatus)">{{ detail.knowledge.sourceStatus === 'normal' ? '源文章正常' : '源文章异常' }}</el-tag></div>
|
||||
</div>
|
||||
<el-alert v-if="detail.knowledge.sourceError" :title="detail.knowledge.sourceError" type="error" :closable="false" />
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="基本信息" name="overview">
|
||||
<dl class="knowledge-meta"><dt>SpaceID</dt><dd>{{ detail.knowledge.feishuSpaceId }}</dd><dt>NodeID</dt><dd>{{ detail.knowledge.feishuNodeId }}</dd><dt>正式版本</dt><dd>{{ detail.currentVersion ? `V${detail.currentVersion.versionNo}` : '暂无' }}</dd><dt>待发布版本</dt><dd>{{ detail.pendingVersion ? `V${detail.pendingVersion.versionNo}` : '暂无' }}</dd></dl>
|
||||
<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><el-button v-if="!detail.manifest.confirmed && detail.pendingVersion" type="primary" @click="confirmManifest">确认说明卡</el-button></section>
|
||||
<div v-if="detail.pendingVersion" class="version-actions"><el-button type="success" @click="review('approve')">审核通过</el-button><el-button type="danger" @click="review('reject')">审核拒绝</el-button><el-button type="primary" @click="publish(true)">发布并保持开放</el-button><el-button @click="publish(false)">发布后关闭</el-button></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="内容结构" name="content">
|
||||
<el-empty v-if="!versionContent" description="请从版本记录中选择一个版本查看" />
|
||||
<template v-else><el-collapse><el-collapse-item v-for="section in versionContent.sections" :key="section.id" :title="`${section.sectionKey} ${section.title}`"><pre class="source-content">{{ section.content }}</pre></el-collapse-item></el-collapse><h4>知识卡</h4><el-table :data="versionContent.cards"><el-table-column prop="title" label="标题" /><el-table-column prop="summary" label="摘要" min-width="300" /><el-table-column prop="riskLevel" label="风险" width="100" /><el-table-column prop="reviewStatus" label="审核" width="110" /></el-table></template>
|
||||
</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="mode" label="模式" width="110" /><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>
|
||||
</el-drawer>
|
||||
</template>
|
||||
Reference in New Issue
Block a user