ai-robot-core/ai-service-admin/src/views/monitoring/index.vue

182 lines
7.0 KiB
Vue
Raw Normal View History

<template>
<div class="monitoring-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="title">会话监控 [AC-ASA-09]</span>
<div class="header-ops">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="会话状态" clearable style="width: 120px">
<el-option label="活跃" value="active" />
<el-option label="已关闭" value="closed" />
<el-option label="已过期" value="expired" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<!-- 使用 BaseTable 展示会话列表 [AC-ASA-09] -->
<base-table
:data="tableData"
:total="total"
v-model:page-num="queryParams.page"
v-model:page-size="queryParams.pageSize"
@pagination="getList"
v-loading="loading"
>
<el-table-column prop="sessionId" label="会话 ID" width="180" show-overflow-tooltip />
<el-table-column prop="tenantId" label="租户 ID" width="120" />
<el-table-column prop="messageCount" label="消息数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="statusMap[scope.row.status]?.type" size="small">
{{ statusMap[scope.row.status]?.label || scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column label="操作" fixed="right" width="120" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleTrace(scope.row)">全链路追踪</el-button>
</template>
</el-table-column>
</base-table>
</el-card>
<!-- 全链路追踪详情抽屉 [AC-ASA-07] -->
<el-drawer
v-model="drawerVisible"
title="会话全链路追踪详情"
size="50%"
destroy-on-close
>
<div v-loading="detailLoading" class="detail-container">
<el-empty v-if="!sessionDetail && !detailLoading" description="暂无追踪详情" />
<el-timeline v-else>
<el-timeline-item
v-for="(msg, index) in sessionDetail?.messages"
:key="index"
:timestamp="msg.timestamp"
placement="top"
:type="msg.role === 'user' ? 'primary' : 'success'"
>
<el-card shadow="never" class="msg-card">
<div class="msg-header">
<span class="role-tag" :class="msg.role">{{ msg.role === 'user' ? 'USER' : 'ASSISTANT' }}</span>
</div>
<div class="msg-content">{{ msg.content }}</div>
<!-- 展示追踪信息检索命中工具调用等 [AC-ASA-07] -->
<div v-if="msg.trace" class="trace-info">
<el-collapse class="trace-collapse">
<el-collapse-item v-if="msg.trace.retrieval" title="检索追踪 (Retrieval)" name="retrieval">
<div v-for="(hit, hIdx) in msg.trace.retrieval" :key="hIdx" class="hit-item">
<div class="hit-meta">
<el-tag size="small" type="success">Score: {{ hit.score }}</el-tag>
<span class="hit-source" v-if="hit.source">来源: {{ hit.source }}</span>
</div>
<div class="hit-text">{{ hit.content }}</div>
</div>
</el-collapse-item>
<el-collapse-item v-if="msg.trace.tool_calls" title="工具调用 (Tool Calls)" name="tools">
<pre class="code-block"><code>{{ JSON.stringify(msg.trace.tool_calls, null, 2) }}</code></pre>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import BaseTable from '@/components/BaseTable.vue'
import { listSessions, getSessionDetail } from '@/api/monitoring'
const statusMap: Record<string, { label: string, type: string }> = {
active: { label: '活跃', type: 'success' },
closed: { label: '已关闭', type: 'info' },
expired: { label: '已过期', type: 'warning' }
}
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = reactive({
page: 1,
pageSize: 10,
status: ''
})
const drawerVisible = ref(false)
const detailLoading = ref(false)
const sessionDetail = ref<any>(null)
const getList = async () => {
loading.value = true
try {
const res: any = await listSessions(queryParams)
tableData.value = res.data || []
total.value = res.pagination?.total || 0
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.page = 1
getList()
}
const resetQuery = () => {
queryParams.status = ''
handleQuery()
}
/** 获取并展示全链路追踪详情 [AC-ASA-07] */
const handleTrace = async (row: any) => {
drawerVisible.value = true
detailLoading.value = true
try {
sessionDetail.value = await getSessionDetail(row.sessionId)
} finally {
detailLoading.value = false
}
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.monitoring-container { padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.title { font-size: 16px; font-weight: bold; }
.detail-container { padding: 10px 20px; }
.msg-card { border-radius: 8px; margin-bottom: 10px; }
.msg-header { margin-bottom: 8px; }
.role-tag { font-size: 11px; font-weight: bold; padding: 2px 6px; border-radius: 4px; }
.role-tag.user { background-color: #ecf5ff; color: #409eff; }
.role-tag.assistant { background-color: #f0f9eb; color: #67c23a; }
.msg-content { font-size: 14px; line-height: 1.6; color: #333; white-space: pre-wrap; }
.trace-info { margin-top: 15px; border-top: 1px solid #f0f0f0; padding-top: 10px; }
.trace-collapse { border: none; }
:deep(.el-collapse-item__header) { height: 36px; font-size: 13px; color: #909399; }
.hit-item { padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-bottom: 8px; }
.hit-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.hit-source { font-size: 11px; color: #999; }
.hit-text { font-size: 12px; color: #666; line-height: 1.5; }
.code-block { background-color: #fafafa; border: 1px solid #eaeaea; padding: 8px; border-radius: 4px; font-size: 12px; overflow-x: auto; margin: 0; }
</style>