Files
X-Financial/web/src/views/AgentTraceCenterView.vue

337 lines
11 KiB
Vue
Raw Normal View History

<template>
<section class="agent-trace-center">
<section class="trace-filters panel">
<label class="trace-field trace-search">
<span>关键字</span>
<input
v-model="filters.keyword"
type="search"
placeholder="Run ID、会话 ID、摘要、场景"
@keydown.enter.prevent="refresh"
/>
</label>
<label class="trace-field">
<span>Agent</span>
<select v-model="filters.agent">
<option value="">全部 Agent</option>
<option value="orchestrator">Orchestrator</option>
<option value="user_agent">User Agent</option>
<option value="hermes">Hermes</option>
</select>
</label>
<label class="trace-field">
<span>状态</span>
<select v-model="filters.status">
<option value="">全部状态</option>
<option value="succeeded">成功</option>
<option value="blocked">阻断</option>
<option value="failed">失败</option>
<option value="running">运行中</option>
</select>
</label>
<label class="trace-field">
<span>来源</span>
<select v-model="filters.source">
<option value="">全部来源</option>
<option value="user_message">用户消息</option>
<option value="schedule">定时任务</option>
<option value="system_event">系统事件</option>
</select>
</label>
</section>
<div class="trace-layout">
<section class="trace-list panel">
<div class="trace-list-head">
<div>
<strong>运行记录</strong>
<span> {{ traces.length }} </span>
</div>
<div class="trace-list-actions">
<button v-if="hasActiveFilter" class="trace-mini-action" type="button" @click="resetFilters">
清空筛选
</button>
<button class="trace-mini-action" type="button" :disabled="loading" @click="refresh">
<i :class="loading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
<span>{{ loading ? '刷新中' : '刷新' }}</span>
</button>
</div>
</div>
<div v-if="loading && !traces.length" class="trace-state">
<i class="mdi mdi-loading mdi-spin"></i>
<strong>正在加载 Trace</strong>
</div>
<div v-else-if="errorMessage" class="trace-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>加载失败</strong>
<p>{{ errorMessage }}</p>
</div>
<div v-else-if="!traces.length" class="trace-state">
<i class="mdi mdi-timeline-question-outline"></i>
<strong>暂无 Trace 记录</strong>
<p>当前筛选条件下没有可展示的 Agent 运行链路</p>
</div>
<table v-else class="trace-table">
<thead>
<tr>
<th>开始时间</th>
<th>场景</th>
<th>状态</th>
<th>摘要</th>
<th>Run ID</th>
</tr>
</thead>
<tbody>
<tr
v-for="trace in traces"
:key="trace.runId"
:class="{ active: trace.runId === selectedRunId }"
tabindex="0"
@click="openTrace(trace.runId)"
@keydown.enter.prevent="openTrace(trace.runId)"
>
<td>{{ formatTraceDateTime(trace.startedAt) }}</td>
<td>
<strong>{{ trace.title }}</strong>
<span>{{ trace.sourceLabel }} · {{ trace.agent }}</span>
</td>
<td>
<span class="trace-status" :class="resolveTraceStatusTone(trace.status)">
{{ trace.statusLabel }}
</span>
</td>
<td>
<strong>{{ trace.summary || '暂无摘要' }}</strong>
<span>{{ trace.eventCount || trace.toolCallCount }} 个事件 · {{ trace.failedToolCallCount }} 个失败工具</span>
</td>
<td class="trace-run-id">{{ trace.runId }}</td>
</tr>
</tbody>
</table>
</section>
<section class="trace-detail panel">
<div v-if="detailLoading" class="trace-state">
<i class="mdi mdi-loading mdi-spin"></i>
<strong>正在读取链路详情</strong>
</div>
<div v-else-if="detailError" class="trace-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>详情加载失败</strong>
<p>{{ detailError }}</p>
</div>
<div v-else-if="!detail" class="trace-state">
<i class="mdi mdi-timeline-clock-outline"></i>
<strong>选择一条运行记录</strong>
<p>点击左侧 Run ID可查看完整事件时间线输入输出和会话上下文</p>
</div>
<template v-else>
<header class="trace-detail-head">
<div>
<span class="trace-kicker">{{ detail.agent || 'agent' }}</span>
<h4>{{ detail.runId }}</h4>
<p>{{ detail.summary || detail.errorMessage || '暂无运行摘要' }}</p>
</div>
<span class="trace-status" :class="resolveTraceStatusTone(detail.status)">
{{ detail.status || 'unknown' }}
</span>
</header>
<div v-if="detail.fallbackGenerated" class="trace-inline-alert">
当前运行没有持久化 trace event已从 AgentRun语义解析和工具调用合成只读时间线
</div>
<div class="trace-metrics">
<div>
<span>会话</span>
<strong>{{ detail.conversationId || '-' }}</strong>
</div>
<div>
<span>工具调用</span>
<strong>{{ detail.toolCalls.length }}</strong>
</div>
<div>
<span>事件数</span>
<strong>{{ detail.events.length }}</strong>
</div>
</div>
<div class="trace-detail-grid">
<section class="trace-event-list">
<button
v-for="event in detail.events"
:key="event.id"
class="trace-event"
:class="{ active: selectedEvent?.id === event.id }"
type="button"
@click="selectedEventId = event.id"
>
<span class="event-index">{{ event.sequence }}</span>
<span class="event-copy">
<strong>{{ event.title || event.eventName }}</strong>
<small>{{ event.stage }} · {{ formatTraceDuration(event.durationMs) }}</small>
</span>
<span class="trace-status mini" :class="resolveTraceStatusTone(event.status)">
{{ event.statusLabel }}
</span>
</button>
</section>
<section class="trace-event-payload">
<template v-if="selectedEvent">
<div class="payload-head">
<div>
<strong>{{ selectedEvent.title }}</strong>
<span>{{ formatTraceDateTime(selectedEvent.startedAt) }}</span>
</div>
<span>{{ selectedEvent.summary || selectedEvent.eventName }}</span>
</div>
<p v-if="selectedEvent.errorMessage" class="trace-error-text">
{{ selectedEvent.errorMessage }}
</p>
<div class="payload-columns">
<div>
<h5>输入</h5>
<pre>{{ formatTraceJson(selectedEvent.inputJson) }}</pre>
</div>
<div>
<h5>输出</h5>
<pre>{{ formatTraceJson(selectedEvent.outputJson) }}</pre>
</div>
</div>
</template>
</section>
</div>
</template>
</section>
</div>
</section>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchAgentTraceDetail, fetchAgentTraces } from '../services/agentTraces.js'
import {
formatTraceDateTime,
formatTraceDuration,
formatTraceJson,
normalizeTraceDetail,
normalizeTraceListItem,
resolveTraceStatusTone
} from '../utils/agentTraceViewModel.js'
defineOptions({
name: 'AgentTraceCenterView'
})
const route = useRoute()
const router = useRouter()
const traces = ref([])
const detail = ref(null)
const selectedRunId = ref('')
const selectedEventId = ref('')
const loading = ref(false)
const detailLoading = ref(false)
const errorMessage = ref('')
const detailError = ref('')
const filters = ref({
keyword: '',
agent: '',
status: '',
source: ''
})
const hasActiveFilter = computed(() =>
Object.values(filters.value).some((value) => String(value || '').trim())
)
const selectedEvent = computed(() =>
detail.value?.events.find((event) => event.id === selectedEventId.value) || detail.value?.events[0] || null
)
async function refresh() {
loading.value = true
errorMessage.value = ''
try {
const payload = await fetchAgentTraces({
keyword: filters.value.keyword,
agent: filters.value.agent,
status: filters.value.status,
source: filters.value.source,
limit: 80
})
traces.value = Array.isArray(payload) ? payload.map(normalizeTraceListItem) : []
} catch (error) {
errorMessage.value = error?.message || 'Agent Trace 加载失败,请稍后重试。'
} finally {
loading.value = false
}
}
async function openTrace(runId) {
const normalizedRunId = String(runId || '').trim()
if (!normalizedRunId) {
return
}
selectedRunId.value = normalizedRunId
detailLoading.value = true
detailError.value = ''
router.replace({
name: 'app-settings',
query: {
...route.query,
section: 'agentTraces',
run_id: normalizedRunId
}
})
try {
detail.value = normalizeTraceDetail(await fetchAgentTraceDetail(normalizedRunId))
selectedEventId.value = detail.value.events[0]?.id || ''
} catch (error) {
detail.value = null
detailError.value = error?.message || 'Agent Trace 详情加载失败,请稍后重试。'
} finally {
detailLoading.value = false
}
}
function resetFilters() {
filters.value = {
keyword: '',
agent: '',
status: '',
source: ''
}
void refresh()
}
watch(
() => [filters.value.agent, filters.value.status, filters.value.source],
() => {
void refresh()
}
)
watch(
() => route.query.run_id,
(runId) => {
const normalizedRunId = String(runId || '').trim()
if (normalizedRunId && normalizedRunId !== selectedRunId.value) {
void openTrace(normalizedRunId)
}
}
)
onMounted(async () => {
await refresh()
const initialRunId = String(route.query.run_id || '').trim()
if (initialRunId) {
await openTrace(initialRunId)
}
})
</script>
<style scoped src="../assets/styles/views/agent-trace-center-view.css"></style>