fix: 补齐知识库元数据管理与生命周期校验

This commit is contained in:
2026-07-12 15:30:23 +08:00
parent c3639fe30d
commit 8266d845c0
13 changed files with 640 additions and 59 deletions

View File

@@ -242,6 +242,11 @@ async function switchMenu(menu: string) {
await loadCurrentMenu();
}
async function previewKnowledge(knowledgeId: number) {
await switchMenu("prompt");
agentForm.knowledgeIds = [knowledgeId];
}
async function loadDashboard() {
stats.value = await api.dashboard(dashboardFilters.start, dashboardFilters.end);
}
@@ -955,7 +960,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</template>
<template v-if="activeMenu === 'knowledge'">
<KnowledgeManagementView />
<KnowledgeManagementView @preview="previewKnowledge" />
</template>
<template v-if="activeMenu === 'prompt'">

View File

@@ -12,6 +12,8 @@ import type {
MigrationSummary,
} from "../types/api";
const emit = defineEmits<{ preview: [knowledgeId: number] }>();
const rows = ref<KnowledgeItem[]>([]);
const migration = ref<MigrationSummary | null>(null);
const loading = ref(false);
@@ -22,6 +24,11 @@ const versions = ref<KnowledgeVersion[]>([]);
const jobs = ref<KnowledgeSyncJob[]>([]);
const versionContent = ref<KnowledgeVersionContent | null>(null);
const activeTab = ref("overview");
const selectedRows = ref<KnowledgeItem[]>([]);
const editOpen = ref(false);
const editSaving = ref(false);
const batchOpen = ref(false);
const batchSaving = ref(false);
const filter = reactive({ keyword: "", type: "", open: "", source: "", pending: "" });
const form = reactive({
name: "",
@@ -32,6 +39,8 @@ const form = reactive({
status: 0,
remark: "",
});
const editForm = reactive({ id: 0, name: "", knowledgeType: "general", reviewMode: "manual" });
const batchForm = reactive({ knowledgeType: "", reviewMode: "" });
const filteredRows = computed(() => rows.value.filter((item) => {
if (filter.keyword && !`${item.name}${item.sourceTitle || ""}`.includes(filter.keyword)) return false;
@@ -49,27 +58,131 @@ async function load() {
loading.value = true;
try {
[rows.value, migration.value] = await Promise.all([api.knowledge(), api.migrationSummary()]);
} catch (error) {
ElMessage.error(errorMessage(error, "知识库列表加载失败"));
} finally {
loading.value = false;
}
}
function openEdit() {
if (!detail.value) return;
Object.assign(editForm, {
id: detail.value.knowledge.id,
name: detail.value.knowledge.name,
knowledgeType: detail.value.knowledge.knowledgeType,
reviewMode: detail.value.knowledge.reviewMode,
});
editOpen.value = true;
}
async function saveMetadata() {
if (!editForm.name.trim()) {
ElMessage.warning("请输入后台知识库名称");
return;
}
editSaving.value = true;
try {
await api.updateKnowledgeMetadata(editForm.id, {
name: editForm.name.trim(), knowledgeType: editForm.knowledgeType, reviewMode: editForm.reviewMode,
});
ElMessage.success("知识库基本信息已更新");
editOpen.value = false;
await Promise.all([load(), refreshDetail(editForm.id)]);
} catch (error) {
ElMessage.error(errorMessage(error, "基本信息保存失败"));
} finally {
editSaving.value = false;
}
}
function openBatchEdit() {
if (!selectedRows.value.length) {
ElMessage.warning("请先勾选需要修改的知识库");
return;
}
Object.assign(batchForm, { knowledgeType: "", reviewMode: "" });
batchOpen.value = true;
}
async function saveBatchMetadata() {
if (!batchForm.knowledgeType && !batchForm.reviewMode) {
ElMessage.warning("请至少选择一个修改项");
return;
}
if (!await confirmAction(
`将修改已选择的 ${selectedRows.value.length} 个知识库。该操作不影响正式版本和开放状态。`,
"确认批量修改",
{ confirmButtonText: "确认修改", cancelButtonText: "取消", type: "warning" },
)) return;
batchSaving.value = true;
try {
const result = await api.batchUpdateKnowledgeMetadata({
knowledgeIds: selectedRows.value.map((item) => item.id),
knowledgeType: batchForm.knowledgeType || undefined,
reviewMode: batchForm.reviewMode || undefined,
});
ElMessage[result.failed ? "warning" : "success"](`批量修改完成:成功 ${result.success} 个,失败 ${result.failed}`);
batchOpen.value = false;
await load();
} catch (error) {
ElMessage.error(errorMessage(error, "批量修改失败"));
} finally {
batchSaving.value = false;
}
}
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);
} catch (error) {
if (error !== "cancel" && error !== "close") ElMessage.error(errorMessage(error, "操作失败"));
}
}
async function restore(item: KnowledgeItem) {
if (!await confirmAction("恢复后知识库仍保持关闭,需要单独确认开放。", "恢复知识库", {
confirmButtonText: "确认恢复", cancelButtonText: "取消", type: "warning",
})) return;
await api.setKnowledgeLifecycle(item.id, "active");
ElMessage.success("知识库已恢复,当前仍为关闭状态");
await load();
}
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();
try {
await api.createKnowledge({ ...form });
Object.assign(form, { name: "", feishuSpaceId: "", feishuNodeId: "", knowledgeType: "general", reviewMode: "manual", status: 0, remark: "" });
ElMessage.success("知识库已新增,请进行首次同步");
await load();
} catch (error) {
ElMessage.error(errorMessage(error, "知识库新增失败"));
}
}
async function openDetail(item: KnowledgeItem) {
detailOpen.value = true;
activeTab.value = "overview";
versionContent.value = null;
await refreshDetail(item.id);
try {
await refreshDetail(item.id);
} catch (error) {
detailOpen.value = false;
ElMessage.error(errorMessage(error, "知识库详情加载失败"));
}
}
async function refreshDetail(id: number) {
@@ -81,26 +194,35 @@ async function refreshDetail(id: number) {
async function sync(item: KnowledgeItem, mode: "review" | "replace") {
operatingId.value = item.id;
try {
await api.syncKnowledge(item.id, mode);
ElMessage.success("同步处理完成,已生成待审核版本");
const result = await api.syncKnowledge(item.id, mode);
const message = result.status === "unchanged"
? "飞书内容未变化,未生成新版本"
: result.status === "published"
? "同步、质检和自动发布已完成"
: "同步处理完成,已生成待审核版本";
ElMessage.success(message);
await load();
if (detailOpen.value) await refreshDetail(item.id);
} catch (error) {
ElMessage.error(errorMessage(error, "知识库同步失败"));
} finally {
operatingId.value = null;
}
}
async function runMigration() {
await ElMessageBox.confirm(
if (!await confirmAction(
"将依次重新读取全部现有飞书文章并生成待审核版本。迁移审核完成前,新检索链路不会使用这些知识库。",
"开始知识迁移",
{ confirmButtonText: "开始迁移", cancelButtonText: "取消", type: "warning" },
);
)) return;
loading.value = true;
try {
const result = await api.runKnowledgeMigration();
ElMessage.success(`迁移处理完成:${result.success}/${result.total} 成功`);
await load();
} catch (error) {
ElMessage.error(errorMessage(error, "知识迁移失败"));
} finally {
loading.value = false;
}
@@ -108,80 +230,104 @@ async function runMigration() {
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);
try {
await api.confirmKnowledgeManifest(detail.value.knowledge.id, detail.value.pendingVersion.id);
ElMessage.success("知识库说明卡已确认");
await refreshDetail(detail.value.knowledge.id);
} catch (error) {
ElMessage.error(errorMessage(error, "说明卡确认失败"));
}
}
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;
try {
const result = await ElMessageBox.prompt(
"请记录拒绝原因。知识内容不能在后台修改,请前往飞书源文章修改后重新同步。",
"拒绝版本",
{ confirmButtonText: "确认拒绝", cancelButtonText: "取消", inputValidator: (value) => Boolean(value?.trim()) || "请填写拒绝原因" },
);
reason = result.value;
} catch {
return;
}
}
try {
await api.reviewKnowledgeVersion(detail.value.knowledge.id, detail.value.pendingVersion.id, action, reason);
ElMessage.success(action === "approve" ? "版本已审核通过" : "版本已拒绝");
await refreshDetail(detail.value.knowledge.id);
} catch (error) {
ElMessage.error(errorMessage(error, "版本审核失败"));
}
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(
if (!await confirmAction(
`将发布 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);
)) return;
try {
await api.publishKnowledgeVersion(detail.value.knowledge.id, detail.value.pendingVersion.id, keepOpen);
ElMessage.success("正式版本已发布");
await load();
await refreshDetail(detail.value.knowledge.id);
} catch (error) {
ElMessage.error(errorMessage(error, "版本发布失败"));
}
}
async function rollback(version: KnowledgeVersion) {
if (!detail.value) return;
await ElMessageBox.confirm(
if (!await confirmAction(
`将从当前版本回滚到 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);
)) 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) {
const next = item.status !== 1;
await ElMessageBox.confirm(
if (!await confirmAction(
next ? "开放后,该知识库将从用户下一条消息开始进入 Agent 动态知识目录。" : "关闭后,当前生成不受影响,从用户下一条消息开始无法检索该知识库。",
next ? "开放知识库" : "关闭知识库",
{ confirmButtonText: next ? "确认开放" : "确认关闭", cancelButtonText: "取消", type: "warning" },
);
await api.setKnowledgeOpen(item.id, next);
ElMessage.success(next ? "知识库已开放" : "知识库已关闭");
await load();
)) return;
try {
await api.setKnowledgeOpen(item.id, next);
ElMessage.success(next ? "知识库已开放" : "知识库已关闭");
await load();
} catch (error) {
ElMessage.error(errorMessage(error, next ? "知识库开放失败" : "知识库关闭失败"));
}
}
async function archive(item: KnowledgeItem) {
await ElMessageBox.confirm("归档后知识库会立即关闭,但历史正式版本和检索记录将保留。", "归档知识库", {
if (!await confirmAction("归档后知识库会立即关闭,但历史正式版本和检索记录将保留。", "归档知识库", {
confirmButtonText: "确认归档", cancelButtonText: "取消", type: "warning",
});
})) return;
await api.setKnowledgeLifecycle(item.id, "archived");
ElMessage.success("知识库已归档");
await load();
}
async function remove(item: KnowledgeItem) {
await ElMessageBox.confirm(
if (!await confirmAction(
`将物理删除“${item.name}”及关联数据。该操作不可恢复,并可能影响历史检索记录定位。`,
"物理删除知识库",
{ confirmButtonText: "确认删除", cancelButtonText: "取消", type: "error" },
);
)) return;
await api.deleteKnowledge(item.id);
ElMessage.success("知识库已删除");
await load();
@@ -202,6 +348,23 @@ function statusType(status: string) {
function typeLabel(value: string) {
return { course: "课程知识库", qa: "答疑知识库", general: "通用知识库" }[value] || value;
}
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
async function confirmAction(
message: string,
title: string,
options: Parameters<typeof ElMessageBox.confirm>[2],
) {
try {
await ElMessageBox.confirm(message, title, options);
return true;
} catch {
return false;
}
}
</script>
<template>
@@ -242,13 +405,19 @@ function typeLabel(value: string) {
<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>
<div class="knowledge-batch-bar">
<span>已选择 {{ selectedRows.length }} 个知识库</span>
<el-button :disabled="selectedRows.length === 0" @click="openBatchEdit">批量修改类型/审核方式</el-button>
</div>
<el-table v-loading="loading" :data="filteredRows" stripe @selection-change="selectedRows = $event">
<el-table-column type="selection" width="48" />
<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-column label="操作" width="270" fixed="right"><template #default="{ row }"><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, 'review')">同步</el-button><el-button link :disabled="row.lifecycleStatus === 'archived'" @click="toggleOpen(row)">{{ row.status === 1 ? '关闭' : '开放' }}</el-button><el-dropdown trigger="click" @command="handleMore($event, row)"><el-button link>更多</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></template></el-table-column>
</el-table>
<el-drawer v-model="detailOpen" size="76%" title="知识库详情" destroy-on-close>
@@ -260,9 +429,12 @@ function typeLabel(value: string) {
<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>
<div class="knowledge-section-head"><h4>基本信息</h4><el-button type="primary" plain @click="openEdit">编辑基本信息</el-button></div>
<dl class="knowledge-meta"><dt>后台名称</dt><dd>{{ detail.knowledge.name }}</dd><dt>知识库类型</dt><dd>{{ typeLabel(detail.knowledge.knowledgeType) }}</dd><dt>审核方式</dt><dd>{{ detail.knowledge.reviewMode === 'manual' ? '必须人工审核发布' : '允许自动处理后发布' }}</dd><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>
<div v-if="detail.pendingVersion?.status === 'pending_review'" class="version-actions"><el-button type="success" @click="review('approve')">审核通过</el-button><el-button type="danger" @click="review('reject')">审核拒绝</el-button></div>
<div v-else-if="detail.pendingVersion?.status === 'approved'" class="version-actions"><el-button type="primary" @click="publish(true)">发布并保持开放</el-button><el-button @click="publish(false)">发布后关闭</el-button></div>
<el-alert v-else-if="detail.pendingVersion?.status === 'rejected'" title="该版本已被拒绝,请在飞书修改源文章后重新同步。" type="warning" :closable="false" />
</el-tab-pane>
<el-tab-pane label="内容结构" name="content">
<el-empty v-if="!versionContent" description="请从版本记录中选择一个版本查看" />
@@ -273,4 +445,47 @@ function typeLabel(value: string) {
</el-tabs>
</template>
</el-drawer>
<el-dialog v-model="editOpen" title="编辑知识库基本信息" width="min(520px, 92vw)" destroy-on-close>
<el-form label-position="top">
<el-form-item label="后台知识库名称" required><el-input v-model="editForm.name" maxlength="100" show-word-limit /></el-form-item>
<el-form-item label="知识库类型" required><el-select v-model="editForm.knowledgeType"><el-option label="课程知识库" value="course" /><el-option label="答疑知识库" value="qa" /><el-option label="通用知识库" value="general" /></el-select></el-form-item>
<el-form-item label="审核方式" required><el-select v-model="editForm.reviewMode"><el-option label="必须人工审核发布" value="manual" /><el-option label="允许自动处理后发布" value="automatic" /></el-select></el-form-item>
<el-alert title="本次修改不会重新同步飞书,也不会改变当前正式版本和开放状态。" type="info" :closable="false" />
</el-form>
<template #footer><el-button @click="editOpen = false">取消</el-button><el-button type="primary" :loading="editSaving" @click="saveMetadata">保存</el-button></template>
</el-dialog>
<el-dialog v-model="batchOpen" title="批量修改知识库" width="min(520px, 92vw)" destroy-on-close>
<el-alert :title="`已选择 ${selectedRows.length} 个知识库,未选择的字段将保持原值。`" type="info" :closable="false" />
<el-form label-position="top" class="batch-edit-form">
<el-form-item label="知识库类型"><el-select v-model="batchForm.knowledgeType" clearable placeholder="不修改"><el-option label="课程知识库" value="course" /><el-option label="答疑知识库" value="qa" /><el-option label="通用知识库" value="general" /></el-select></el-form-item>
<el-form-item label="审核方式"><el-select v-model="batchForm.reviewMode" clearable placeholder="不修改"><el-option label="必须人工审核发布" value="manual" /><el-option label="允许自动处理后发布" value="automatic" /></el-select></el-form-item>
</el-form>
<template #footer><el-button @click="batchOpen = false">取消</el-button><el-button type="primary" :loading="batchSaving" @click="saveBatchMetadata">下一步</el-button></template>
</el-dialog>
</template>
<style scoped>
.knowledge-batch-bar,
.knowledge-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.knowledge-batch-bar {
min-height: 48px;
padding: 8px 0;
color: #66756f;
}
.knowledge-section-head h4 {
margin: 0;
}
.batch-edit-form {
margin-top: 20px;
}
</style>

View File

@@ -14,6 +14,7 @@ import type {
KnowledgeVersion,
KnowledgeVersionContent,
MigrationSummary,
KnowledgeBatchUpdateResult,
RetrievalLogItem,
AttentionItem,
ModelItem,
@@ -42,9 +43,9 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const token = getToken();
if (token) headers.set("Authorization", `Bearer ${token}`);
const response = await fetch(`${API_BASE}${path}`, { ...init, headers });
const body = (await response.json()) as ApiResponse<T>;
const body = (await response.json()) as ApiResponse<T> & { detail?: string };
if (!response.ok || body.code !== 0) {
throw new Error(body.message || `${response.status} ${response.statusText}`);
throw new Error(body.detail || body.message || `${response.status} ${response.statusText}`);
}
return body.data;
}
@@ -119,6 +120,10 @@ export const api = {
request<KnowledgeItem>("/admin/knowledge", { method: "POST", body: JSON.stringify(payload) }),
updateKnowledge: (id: number, payload: Record<string, unknown>) =>
request<KnowledgeItem>(`/admin/knowledge/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
updateKnowledgeMetadata: (id: number, payload: { name: string; knowledgeType: string; reviewMode: string }) =>
request<KnowledgeItem>(`/admin/knowledge/${id}/metadata`, { method: "PATCH", body: JSON.stringify(payload) }),
batchUpdateKnowledgeMetadata: (payload: { knowledgeIds: number[]; knowledgeType?: string; reviewMode?: string }) =>
request<KnowledgeBatchUpdateResult>("/admin/knowledge/batch/metadata", { method: "PATCH", body: JSON.stringify(payload) }),
deleteKnowledge: (id: number) => request<null>(`/admin/knowledge/${id}`, { method: "DELETE" }),
importKnowledgeFromSpace: (body: { node_token: string }) =>
request<{ imported: number; skipped: number; id: number; name: string }>(

View File

@@ -178,6 +178,13 @@ export interface MigrationSummary {
items: KnowledgeItem[];
}
export interface KnowledgeBatchUpdateResult {
total: number;
success: number;
failed: number;
results: Array<{ knowledgeId: number; ok: boolean; error?: string }>;
}
export interface RetrievalLogItem {
id: number;
sessionId?: number | null;