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 promptContent = ref("");
|
||||||
const chatDetail = ref<ChatDetail | null>(null);
|
const chatDetail = ref<ChatDetail | null>(null);
|
||||||
const chatDetailOpen = ref(false);
|
const chatDetailOpen = ref(false);
|
||||||
|
const selectedAiLog = ref<AiLogRecord | null>(null);
|
||||||
|
const aiLogDetailOpen = ref(false);
|
||||||
const recordTab = ref("chats");
|
const recordTab = ref("chats");
|
||||||
const editingModelId = ref<number | null>(null);
|
const editingModelId = ref<number | null>(null);
|
||||||
const editingKnowledgeId = ref<number | null>(null);
|
const editingKnowledgeId = ref<number | null>(null);
|
||||||
@@ -960,6 +962,11 @@ async function openChatDetail(row: ChatRecord) {
|
|||||||
chatDetailOpen.value = true;
|
chatDetailOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openAiLogDetail(row: AiLogRecord) {
|
||||||
|
selectedAiLog.value = await api.aiLogDetail(row.id);
|
||||||
|
aiLogDetailOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function exportChats() {
|
async function exportChats() {
|
||||||
await api.exportChats(buildChatQuery());
|
await api.exportChats(buildChatQuery());
|
||||||
}
|
}
|
||||||
@@ -1463,29 +1470,24 @@ function buildChatQuery() {
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="AI 请求" name="aiLogs">
|
<el-tab-pane label="AI 请求" name="aiLogs">
|
||||||
<el-table :data="aiLogs" stripe>
|
<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="sessionId" label="会话ID" width="90" />
|
||||||
<el-table-column prop="modelName" label="模型" width="160" />
|
<el-table-column prop="modelName" label="模型" width="160" />
|
||||||
<el-table-column prop="knowledgeIds" label="知识库" width="120" />
|
<el-table-column prop="knowledgeIds" label="知识库" width="120" />
|
||||||
<el-table-column prop="retrieveCount" label="命中" width="80" />
|
<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="totalToken" label="Token" width="90" />
|
||||||
<el-table-column prop="costMs" label="耗时(ms)" width="100" />
|
<el-table-column prop="costMs" label="耗时(ms)" width="100" />
|
||||||
<el-table-column prop="status" label="状态" 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 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-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="操作日志" name="operationLogs">
|
<el-tab-pane label="操作日志" name="operationLogs">
|
||||||
@@ -1560,5 +1562,53 @@ function buildChatQuery() {
|
|||||||
</el-collapse>
|
</el-collapse>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</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>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export const api = {
|
|||||||
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
|
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
|
||||||
aiLogs: (query: { sessionId?: number; userId?: number; status?: string } = {}) =>
|
aiLogs: (query: { sessionId?: number; userId?: number; status?: string } = {}) =>
|
||||||
request<AiLogRecord[]>(`/admin/ai-log/list${queryString(query)}`),
|
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"),
|
operationLogs: () => request<Record<string, unknown>[]>("/admin/log/list"),
|
||||||
clearFeishuCache: () =>
|
clearFeishuCache: () =>
|
||||||
request<{ cleared: number; message: string }>("/admin/feishu/cache/clear", { method: "POST", body: "{}" }),
|
request<{ cleared: number; message: string }>("/admin/feishu/cache/clear", { method: "POST", body: "{}" }),
|
||||||
|
|||||||
@@ -712,6 +712,19 @@ textarea {
|
|||||||
font-size: 13px;
|
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 {
|
.retrieval-chunks {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -743,6 +756,13 @@ textarea {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chunk-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #203832;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.retrieval-chunk pre {
|
.retrieval-chunk pre {
|
||||||
max-height: 260px;
|
max-height: 260px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -775,6 +795,10 @@ textarea {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audit-detail-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.agent-preview-panel {
|
.agent-preview-panel {
|
||||||
height: 620px;
|
height: 620px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
from sqlalchemy import exists, or_, select
|
from sqlalchemy import exists, or_, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -160,6 +160,18 @@ def ai_logs(
|
|||||||
return api_success([_ai_log_dict(item) for item in 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(
|
def _chat_query(
|
||||||
*,
|
*,
|
||||||
keyword: str,
|
keyword: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user