Add AI request audit detail drawer

This commit is contained in:
2026-07-09 10:44:52 +08:00
parent 42569a1057
commit e1a130177c
4 changed files with 103 additions and 16 deletions

View File

@@ -34,6 +34,8 @@ const operationLogs = ref<Record<string, unknown>[]>([]);
const promptContent = ref("");
const chatDetail = ref<ChatDetail | null>(null);
const chatDetailOpen = ref(false);
const selectedAiLog = ref<AiLogRecord | null>(null);
const aiLogDetailOpen = ref(false);
const recordTab = ref("chats");
const editingModelId = ref<number | null>(null);
const editingKnowledgeId = ref<number | null>(null);
@@ -960,6 +962,11 @@ async function openChatDetail(row: ChatRecord) {
chatDetailOpen.value = true;
}
async function openAiLogDetail(row: AiLogRecord) {
selectedAiLog.value = await api.aiLogDetail(row.id);
aiLogDetailOpen.value = true;
}
async function exportChats() {
await api.exportChats(buildChatQuery());
}
@@ -1463,29 +1470,24 @@ function buildChatQuery() {
</el-tab-pane>
<el-tab-pane label="AI 请求" name="aiLogs">
<el-table :data="aiLogs" stripe>
<el-table-column type="expand" width="48">
<template #default="{ row }">
<section v-if="row.retrievedChunks?.length" class="retrieval-chunks">
<article v-for="chunk in row.retrievedChunks" :key="`${row.id}-${chunk.index}`" class="retrieval-chunk">
<div class="chunk-head">
<strong>#{{ chunk.index }} {{ chunk.knowledgeName || '未知知识库' }}</strong>
<span>{{ chunk.title || '未命名片段' }}</span>
</div>
<pre>{{ chunk.content }}</pre>
<a v-if="chunk.sourceUrl" :href="chunk.sourceUrl" target="_blank" rel="noreferrer">查看来源</a>
</article>
</section>
<el-empty v-else description="本条请求未保存召回片段" :image-size="64" />
</template>
</el-table-column>
<el-table-column prop="sessionId" label="会话ID" width="90" />
<el-table-column prop="modelName" label="模型" width="160" />
<el-table-column prop="knowledgeIds" label="知识库" width="120" />
<el-table-column prop="retrieveCount" label="命中" width="80" />
<el-table-column label="片段" width="80">
<template #default="{ row }">
{{ row.retrievedChunks?.length || 0 }}
</template>
</el-table-column>
<el-table-column prop="totalToken" label="Token" width="90" />
<el-table-column prop="costMs" label="耗时(ms)" width="100" />
<el-table-column prop="status" label="状态" width="100" />
<el-table-column prop="errorMessage" label="错误" min-width="220" show-overflow-tooltip />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openAiLogDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="操作日志" name="operationLogs">
@@ -1560,5 +1562,53 @@ function buildChatQuery() {
</el-collapse>
</template>
</el-drawer>
<el-drawer v-model="aiLogDetailOpen" size="820px" title="AI 请求详情">
<template v-if="selectedAiLog">
<section class="chat-summary">
<div><span>请求ID</span><strong>{{ selectedAiLog.id }}</strong></div>
<div><span>会话ID</span><strong>{{ selectedAiLog.sessionId || '-' }}</strong></div>
<div><span>消息ID</span><strong>{{ selectedAiLog.messageId || '-' }}</strong></div>
<div><span>用户ID</span><strong>{{ selectedAiLog.userId || '-' }}</strong></div>
<div><span>模型</span><strong>{{ selectedAiLog.modelName || '-' }}</strong></div>
<div><span>状态</span><strong>{{ selectedAiLog.status }}</strong></div>
</section>
<section class="audit-detail-grid">
<span>知识库{{ selectedAiLog.knowledgeIds || '-' }}</span>
<span>命中数{{ selectedAiLog.retrieveCount }}</span>
<span>片段数{{ selectedAiLog.retrievedChunks?.length || 0 }}</span>
<span>输入 Token{{ selectedAiLog.inputToken || '-' }}</span>
<span>输出 Token{{ selectedAiLog.outputToken || '-' }}</span>
<span> Token{{ selectedAiLog.totalToken || '-' }}</span>
<span>耗时{{ selectedAiLog.costMs || '-' }}ms</span>
<span>时间{{ selectedAiLog.createdAt }}</span>
</section>
<p v-if="selectedAiLog.errorMessage" class="error-text">{{ selectedAiLog.errorMessage }}</p>
<h3 class="detail-title">召回知识片段</h3>
<section v-if="selectedAiLog.retrievedChunks?.length" class="retrieval-chunks">
<article
v-for="chunk in selectedAiLog.retrievedChunks"
:key="`${selectedAiLog.id}-${chunk.index}`"
class="retrieval-chunk"
>
<div class="chunk-head">
<strong>片段 {{ chunk.index }}/{{ selectedAiLog.retrievedChunks.length }}</strong>
<span>{{ chunk.knowledgeName || '未知知识库' }}</span>
<span v-if="chunk.knowledgeId">知识库ID{{ chunk.knowledgeId }}</span>
</div>
<div class="chunk-title">{{ chunk.title || '未命名片段' }}</div>
<pre>{{ chunk.content }}</pre>
<a v-if="chunk.sourceUrl" :href="chunk.sourceUrl" target="_blank" rel="noreferrer">查看来源</a>
</article>
</section>
<el-empty v-else description="本条请求未保存召回片段" :image-size="72" />
<h3 class="detail-title">完整 Prompt</h3>
<pre class="prompt-preview">{{ selectedAiLog.prompt || '无 Prompt 记录' }}</pre>
</template>
</el-drawer>
</main>
</template>

View File

@@ -139,6 +139,7 @@ export const api = {
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
aiLogs: (query: { sessionId?: number; userId?: number; status?: string } = {}) =>
request<AiLogRecord[]>(`/admin/ai-log/list${queryString(query)}`),
aiLogDetail: (id: number) => request<AiLogRecord>(`/admin/ai-log/${id}`),
operationLogs: () => request<Record<string, unknown>[]>("/admin/log/list"),
clearFeishuCache: () =>
request<{ cleared: number; message: string }>("/admin/feishu/cache/clear", { method: "POST", body: "{}" }),

View File

@@ -712,6 +712,19 @@ textarea {
font-size: 13px;
}
.audit-detail-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px 14px;
margin: 14px 0;
padding: 12px;
border: 1px solid #dce8e4;
border-radius: 6px;
background: #fbfdfc;
color: #40524b;
font-size: 13px;
}
.retrieval-chunks {
display: grid;
gap: 12px;
@@ -743,6 +756,13 @@ textarea {
font-size: 13px;
}
.chunk-title {
margin-bottom: 8px;
color: #203832;
font-weight: 600;
line-height: 1.5;
}
.retrieval-chunk pre {
max-height: 260px;
margin: 0;
@@ -775,6 +795,10 @@ textarea {
grid-template-columns: 1fr;
}
.audit-detail-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.agent-preview-panel {
height: 620px;
}