- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
337 lines
11 KiB
Vue
337 lines
11 KiB
Vue
<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>
|