feat: 完善知识库版本管理与检索审计后台

This commit is contained in:
2026-07-11 19:22:18 +08:00
parent 6cadca8c8e
commit 872233adc6
6 changed files with 741 additions and 50 deletions

View File

@@ -3,6 +3,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { computed, onMounted, reactive, ref } from "vue";
import AdminLoginView from "./components/AdminLoginView.vue";
import KnowledgeManagementView from "./components/KnowledgeManagementView.vue";
import { systemSettingDefinitions, systemSettingSections, type SystemSettingValue } from "./config/systemSettings";
import { api, clearToken, getToken, saveToken } from "./services/api";
import type {
@@ -14,6 +15,8 @@ import type {
ChatRecord,
DashboardStats,
KnowledgeItem,
RetrievalLogItem,
AttentionItem,
ModelItem,
SystemConfigItem,
UserImportResult,
@@ -36,6 +39,10 @@ const configs = ref<SystemConfigItem[]>([]);
const chats = ref<ChatRecord[]>([]);
const aiLogs = ref<AiLogRecord[]>([]);
const operationLogs = ref<Record<string, unknown>[]>([]);
const retrievalLogs = ref<RetrievalLogItem[]>([]);
const attentionRecords = ref<AttentionItem[]>([]);
const selectedRetrievalLog = ref<Record<string, unknown> | null>(null);
const retrievalDetailOpen = ref(false);
const promptContent = ref("");
const chatDetail = ref<ChatDetail | null>(null);
const chatDetailOpen = ref(false);
@@ -47,6 +54,7 @@ const editingKnowledgeId = ref<number | null>(null);
const batchNodeToken = ref("");
const batchImporting = ref(false);
const agentDebugging = ref(false);
const agentDebugTrace = ref<Record<string, unknown>[]>([]);
const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string }[]>([
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
]);
@@ -263,6 +271,8 @@ async function loadCurrentMenu() {
aiLogs.value = await api.aiLogs();
operationLogs.value = await api.operationLogs();
}
if (activeMenu.value === "retrievals") retrievalLogs.value = await api.retrievalLogs();
if (activeMenu.value === "attention") attentionRecords.value = await api.attentionList();
} finally {
loading.value = false;
}
@@ -711,6 +721,7 @@ async function debugAgent() {
return;
}
agentPreviewMessages.value.push({ role: "assistant", content: result.answer });
agentDebugTrace.value = result.retrievalTrace || [];
ElMessage.success(result.message);
} catch (error) {
const message = error instanceof Error ? error.message : "Agent 调试失败";
@@ -725,6 +736,35 @@ function clearAgentPreview() {
agentPreviewMessages.value = [
{ role: "assistant", content: "预览已清空,可以继续发起新的 Agent 调试。" },
];
agentDebugTrace.value = [];
}
async function openRetrievalDetail(row: RetrievalLogItem) {
selectedRetrievalLog.value = await api.retrievalLogDetail(row.id);
retrievalDetailOpen.value = true;
}
async function updateAttentionStatus(row: AttentionItem, status: string) {
await api.updateAttention(row.id, status);
ElMessage.success("人工关注状态已更新");
attentionRecords.value = await api.attentionList();
}
async function deleteAttentionRecord(row: AttentionItem) {
try {
await ElMessageBox.confirm("确定删除这条人工关注记录吗?操作将写入审计日志。", "删除人工关注记录", {
confirmButtonText: "确认删除", cancelButtonText: "取消", type: "warning",
});
} catch {
return;
}
await api.deleteAttention(row.id);
ElMessage.success("人工关注记录已删除");
attentionRecords.value = await api.attentionList();
}
function attentionPriorityLabel(priority: string) {
return { urgent: "紧急", important: "重要", normal: "普通" }[priority as "urgent" | "important" | "normal"] || priority;
}
async function clearFeishuCache() {
@@ -750,6 +790,11 @@ async function openChatDetail(row: ChatRecord) {
chatDetailOpen.value = true;
}
async function openChatSession(sessionId: number) {
chatDetail.value = await api.chatDetail(sessionId);
chatDetailOpen.value = true;
}
async function openAiLogDetail(row: AiLogRecord) {
selectedAiLog.value = await api.aiLogDetail(row.id);
aiLogDetailOpen.value = true;
@@ -792,6 +837,8 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
<button :class="{ active: activeMenu === 'models' }" @click="switchMenu('models')">模型管理</button>
<button :class="{ active: activeMenu === 'configs' }" @click="switchMenu('configs')">系统配置</button>
<button :class="{ active: activeMenu === 'records' }" @click="switchMenu('records')">记录审计</button>
<button :class="{ active: activeMenu === 'retrievals' }" @click="switchMenu('retrievals')">检索日志</button>
<button :class="{ active: activeMenu === 'attention' }" @click="switchMenu('attention')">人工关注</button>
</aside>
<section class="workspace">
@@ -908,53 +955,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</template>
<template v-if="activeMenu === 'knowledge'">
<div class="page-head inline">
<div><h2>知识库管理</h2><p>维护飞书知识库 SpaceID NodeID</p></div>
<el-button type="warning" @click="clearFeishuCache">刷新飞书缓存</el-button>
</div>
<el-form class="knowledge-form" :model="knowledgeForm">
<el-input v-model="knowledgeForm.name" placeholder="知识库名称" />
<el-input v-model="knowledgeForm.feishuSpaceId" placeholder="SpaceID" />
<el-input v-model="knowledgeForm.feishuNodeId" placeholder="NodeID" />
<el-input v-model="knowledgeForm.remark" placeholder="备注" />
<div class="form-switch-row">
<span>启用</span>
<el-switch v-model="knowledgeForm.status" :active-value="1" :inactive-value="0" />
</div>
<div class="form-actions-inline">
<el-button type="primary" @click="saveKnowledge">{{ editingKnowledgeId ? "保存" : "新增" }}</el-button>
<el-button @click="resetKnowledgeForm">清空</el-button>
</div>
</el-form>
<el-divider />
<div class="batch-import-row">
<el-input v-model="batchNodeToken" placeholder="输入 NodeToken如 wikcnXXX自动获取标题与 SpaceID" style="max-width: 400px" />
<el-button type="primary" @click="batchImportKnowledge" :loading="batchImporting">导入</el-button>
</div>
<el-table :data="knowledge" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" min-width="160" />
<el-table-column prop="feishuSpaceId" label="SpaceID" min-width="180" show-overflow-tooltip />
<el-table-column prop="feishuNodeId" label="NodeID" min-width="180" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="saveKnowledgeRow(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editKnowledge(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteKnowledge(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<KnowledgeManagementView />
</template>
<template v-if="activeMenu === 'prompt'">
@@ -1054,6 +1055,19 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
<el-button type="primary" :loading="agentDebugging" @click="debugAgent">发送</el-button>
</div>
</aside>
<aside class="agent-trace-panel">
<div class="agent-preview-head">
<h3>检索追踪</h3>
<span>本轮工具调用</span>
</div>
<div class="agent-trace-list">
<el-empty v-if="agentDebugTrace.length === 0" description="发送问题后查看目录、检索与章节回读" :image-size="64" />
<article v-for="(step, index) in agentDebugTrace" :key="index">
<strong>{{ index + 1 }}. {{ step.tool }}</strong>
<pre>{{ JSON.stringify(step, null, 2) }}</pre>
</article>
</div>
</aside>
</section>
</template>
@@ -1212,6 +1226,36 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</section>
</template>
<template v-if="activeMenu === 'retrievals'">
<div class="page-head"><h2>检索日志</h2><p>查看每条回答使用的知识目录Agent 决策搜索候选重排和章节回读</p></div>
<el-table :data="retrievalLogs" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="sessionId" label="会话" width="90" />
<el-table-column prop="question" label="用户问题" min-width="280" show-overflow-tooltip />
<el-table-column label="调用知识" width="100"><template #default="{ row }"><el-tag :type="row.knowledgeCalled ? 'success' : 'info'">{{ row.knowledgeCalled ? '已调用' : '未调用' }}</el-tag></template></el-table-column>
<el-table-column prop="selectedKnowledgeIds" label="知识库" width="120" />
<el-table-column prop="status" label="状态" width="100" />
<el-table-column prop="toolCostMs" label="工具耗时" width="110" />
<el-table-column prop="totalCostMs" label="总耗时" width="100" />
<el-table-column prop="createdAt" label="时间" width="180" />
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openRetrievalDetail(row)">详情</el-button></template></el-table-column>
</el-table>
</template>
<template v-if="activeMenu === 'attention'">
<div class="page-head"><h2>人工关注</h2><p>处理安全风险复杂卡住知识缺失和用户主动求助记录</p></div>
<el-table :data="attentionRecords" stripe>
<el-table-column label="优先级" width="100"><template #default="{ row }"><el-tag :type="row.priority === 'urgent' ? 'danger' : row.priority === 'important' ? 'warning' : 'info'">{{ attentionPriorityLabel(row.priority) }}</el-tag></template></el-table-column>
<el-table-column prop="problemSummary" label="问题摘要" min-width="220" />
<el-table-column prop="triggerReason" label="触发原因" min-width="220" />
<el-table-column prop="userId" label="用户" width="80" />
<el-table-column prop="sessionId" label="会话" width="80" />
<el-table-column label="状态" width="150"><template #default="{ row }"><el-select :model-value="row.status" size="small" @change="updateAttentionStatus(row, $event)"><el-option label="待处理" value="pending" /><el-option label="处理中" value="processing" /><el-option label="已处理" value="resolved" /><el-option label="无需处理" value="ignored" /></el-select></template></el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="130" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openChatSession(row.sessionId)">原会话</el-button><el-button link type="danger" @click="deleteAttentionRecord(row)">删除</el-button></template></el-table-column>
</el-table>
</template>
<template v-if="activeMenu === 'records'">
<div class="page-head"><h2>记录审计</h2><p>聊天AI 请求和后台操作记录</p></div>
<el-tabs v-model="recordTab">
@@ -1292,6 +1336,21 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</section>
</section>
<el-drawer v-model="retrievalDetailOpen" size="820px" title="检索日志详情">
<template v-if="selectedRetrievalLog">
<dl class="knowledge-meta">
<dt>问题</dt><dd>{{ selectedRetrievalLog.question }}</dd>
<dt>安全规则</dt><dd>{{ selectedRetrievalLog.safetyRuleVersion }}</dd>
<dt>目录哈希</dt><dd>{{ selectedRetrievalLog.catalogHash }}</dd>
<dt>最终章节</dt><dd>{{ selectedRetrievalLog.finalSectionIds || '未读取知识章节' }}</dd>
</dl>
<h3>本轮 KnowledgeList</h3><pre class="trace-json">{{ JSON.stringify(selectedRetrievalLog.catalogSnapshot, null, 2) }}</pre>
<h3>Agent 工具轨迹</h3><pre class="trace-json">{{ JSON.stringify(selectedRetrievalLog.toolTrace, null, 2) }}</pre>
<h3>候选切片与取舍</h3><el-table :data="selectedRetrievalLog.candidates"><el-table-column prop="knowledgeId" label="知识库" width="90" /><el-table-column prop="versionId" label="版本" width="80" /><el-table-column prop="sectionId" label="章节" width="80" /><el-table-column prop="lexicalScore" label="词法分" width="100" /><el-table-column prop="rerankScore" label="重排分" width="100" /><el-table-column label="采用" width="80"><template #default="{ row }">{{ row.selected ? '是' : '否' }}</template></el-table-column><el-table-column prop="discardReason" label="舍弃原因" min-width="180" /></el-table>
<h3>最终回答</h3><pre class="trace-json">{{ selectedRetrievalLog.finalAnswer || '尚未生成回答' }}</pre>
</template>
</el-drawer>
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
<template v-if="chatDetail">
<section class="chat-summary">