Add AI request audit detail drawer
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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: "{}" }),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user