Files
X-Financial/web/src/views/AgentTraceCenterView.vue
caoxiaozhu 92444e7eae feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
2026-06-01 17:07:14 +08:00

337 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>