feat: 完善知识库版本管理与检索审计后台
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user