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;
}

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from io import StringIO
from typing import Any
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import Response
from sqlalchemy import exists, or_, select
from sqlalchemy.orm import Session
@@ -160,6 +160,18 @@ def ai_logs(
return api_success([_ai_log_dict(item) for item in logs])
@router.get("/ai-log/{log_id}")
def ai_log_detail(
log_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
log = db.get(AiRequestLog, log_id)
if log is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AI 请求日志不存在")
return api_success(_ai_log_dict(log, include_prompt=True))
def _chat_query(
*,
keyword: str,