182 lines
7.0 KiB
Vue
182 lines
7.0 KiB
Vue
|
|
<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>
|