处理agent答非所问的问题

This commit is contained in:
rjb
2026-01-20 09:40:16 +08:00
parent e4aa6cdb79
commit f6568f252a
11 changed files with 1420 additions and 44 deletions

View File

@@ -0,0 +1,445 @@
<template>
<el-drawer
v-model="visible"
:title="`节点执行详情: ${nodeLabel}`"
size="600px"
:close-on-click-modal="false"
>
<div v-if="loading" class="loading-container">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="error" class="error-container">
<el-alert
type="error"
:title="error"
:closable="false"
show-icon
/>
</div>
<div v-else-if="nodeLogs.length === 0" class="empty-container">
<el-empty description="暂无执行日志" />
</div>
<div v-else class="execution-detail">
<!-- 节点基本信息 -->
<el-card class="info-card" shadow="never">
<template #header>
<div class="card-header">
<span>节点信息</span>
<el-tag :type="statusType" size="small">{{ statusText }}</el-tag>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="节点ID">{{ nodeId }}</el-descriptions-item>
<el-descriptions-item label="节点类型">{{ nodeType }}</el-descriptions-item>
<el-descriptions-item label="执行时间" v-if="executionTime">
{{ executionTime }}ms
</el-descriptions-item>
<el-descriptions-item label="执行状态">
<el-tag :type="statusType" size="small">{{ statusText }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 输入数据 -->
<el-card v-if="inputData" class="data-card" shadow="never">
<template #header>
<div class="card-header">
<span>输入数据</span>
<el-button
text
size="small"
@click="copyToClipboard(inputData)"
title="复制"
>
<el-icon><DocumentCopy /></el-icon>
</el-button>
</div>
</template>
<pre class="json-viewer">{{ formatJSON(inputData) }}</pre>
</el-card>
<!-- 输出数据 -->
<el-card v-if="outputData" class="data-card" shadow="never">
<template #header>
<div class="card-header">
<span>输出数据</span>
<el-button
text
size="small"
@click="copyToClipboard(outputData)"
title="复制"
>
<el-icon><DocumentCopy /></el-icon>
</el-button>
</div>
</template>
<pre class="json-viewer">{{ formatJSON(outputData) }}</pre>
</el-card>
<!-- 错误信息 -->
<el-card v-if="errorInfo" class="error-card" shadow="never">
<template #header>
<div class="card-header">
<span>错误信息</span>
<el-tag type="danger" size="small">失败</el-tag>
</div>
</template>
<div class="error-content">
<div v-if="errorInfo.error_type" class="error-type">
<strong>错误类型</strong>{{ errorInfo.error_type }}
</div>
<div class="error-message">
<strong>错误消息</strong>
<pre>{{ errorInfo.error_message || errorInfo.error }}</pre>
</div>
</div>
</el-card>
<!-- 执行时间线 -->
<el-card class="timeline-card" shadow="never">
<template #header>
<span>执行时间线</span>
</template>
<el-timeline>
<el-timeline-item
v-for="(log, index) in nodeLogs"
:key="index"
:timestamp="formatTime(log.timestamp)"
:type="getLogType(log.level)"
:icon="getLogIcon(log.level)"
>
<div class="timeline-content">
<div class="log-message">{{ log.message }}</div>
<div v-if="log.duration" class="log-duration">
耗时: {{ log.duration }}ms
</div>
<div v-if="log.data && Object.keys(log.data).length > 0" class="log-data">
<el-collapse>
<el-collapse-item title="查看详情" :name="index">
<pre class="json-viewer-small">{{ formatJSON(log.data) }}</pre>
</el-collapse-item>
</el-collapse>
</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { DocumentCopy } from '@element-plus/icons-vue'
import api from '@/api'
interface ExecutionLog {
id: string
node_id: string
node_type: string
level: string
message: string
data?: any
timestamp: string
duration?: number
}
const props = defineProps<{
visible: boolean
nodeId?: string
nodeLabel?: string
nodeType?: string
executionId?: string
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
const loading = ref(false)
const error = ref<string | null>(null)
const nodeLogs = ref<ExecutionLog[]>([])
// 计算属性
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
const inputData = computed(() => {
// 从日志中提取输入数据
const startLog = nodeLogs.value.find(log =>
log.message && (log.message.includes('开始执行') || log.message.includes('开始'))
)
if (startLog?.data?.input) {
return startLog.data.input
}
return null
})
const outputData = computed(() => {
// 从日志中提取输出数据
const completeLog = nodeLogs.value.find(log =>
log.message && (log.message.includes('执行完成') || log.message.includes('完成'))
)
if (completeLog?.data?.output) {
return completeLog.data.output
}
return null
})
const errorInfo = computed(() => {
// 从日志中提取错误信息
const errorLog = nodeLogs.value.find(log =>
log.level === 'ERROR' || (log.message && (log.message.includes('失败') || log.message.includes('错误')))
)
if (errorLog?.data) {
return {
error_type: errorLog.data.error_type,
error_message: errorLog.data.error || errorLog.data.error_message || errorLog.message
}
}
return null
})
const executionTime = computed(() => {
// 从日志中提取执行时间
const completeLog = nodeLogs.value.find(log =>
log.message && (log.message.includes('执行完成') || log.message.includes('完成'))
)
return completeLog?.duration || null
})
const statusText = computed(() => {
if (errorInfo.value) return '失败'
if (outputData.value) return '已完成'
if (inputData.value) return '执行中'
return '未知'
})
const statusType = computed(() => {
if (errorInfo.value) return 'danger'
if (outputData.value) return 'success'
if (inputData.value) return 'warning'
return 'info'
})
// 监听props变化加载日志
watch([() => props.visible, () => props.executionId, () => props.nodeId],
([newVisible, newExecutionId, newNodeId]) => {
if (newVisible && newExecutionId && newNodeId) {
loadNodeLogs()
}
},
{ immediate: true }
)
// 加载节点日志
const loadNodeLogs = async () => {
if (!props.executionId || !props.nodeId) {
return
}
loading.value = true
error.value = null
nodeLogs.value = []
try {
const response = await api.get(`/api/v1/execution-logs/executions/${props.executionId}`, {
params: {
node_id: props.nodeId,
limit: 100
}
})
nodeLogs.value = response.data || []
// 按时间排序
nodeLogs.value.sort((a, b) => {
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
})
} catch (err: any) {
console.error('加载节点日志失败:', err)
const errorDetail = err.response?.data?.detail || err.message || '加载日志失败'
error.value = errorDetail
// 如果是404错误显示更友好的提示
if (err.response?.status === 404) {
ElMessage.warning('执行记录不存在或已过期,无法查看执行详情')
} else {
ElMessage.error('加载节点日志失败: ' + errorDetail)
}
} finally {
loading.value = false
}
}
// 格式化JSON
const formatJSON = (data: any) => {
if (!data) return ''
try {
if (typeof data === 'string') {
// 尝试解析为JSON
const parsed = JSON.parse(data)
return JSON.stringify(parsed, null, 2)
}
return JSON.stringify(data, null, 2)
} catch {
return String(data)
}
}
// 格式化时间
const formatTime = (timestamp: string) => {
if (!timestamp) return ''
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}
// 获取日志类型
const getLogType = (level: string) => {
switch (level) {
case 'ERROR':
return 'danger'
case 'WARN':
return 'warning'
case 'INFO':
return 'primary'
default:
return 'info'
}
}
// 获取日志图标
const getLogIcon = (level: string) => {
switch (level) {
case 'ERROR':
return 'CircleClose'
case 'WARN':
return 'Warning'
case 'INFO':
return 'CircleCheck'
default:
return 'InfoFilled'
}
}
// 复制到剪贴板
const copyToClipboard = async (data: any) => {
try {
const text = formatJSON(data)
await navigator.clipboard.writeText(text)
ElMessage.success('已复制到剪贴板')
} catch (err) {
ElMessage.error('复制失败')
}
}
</script>
<style scoped>
.loading-container,
.error-container,
.empty-container {
padding: 20px;
text-align: center;
}
.execution-detail {
padding: 0;
}
.info-card,
.data-card,
.error-card,
.timeline-card {
margin-bottom: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.json-viewer {
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
margin: 0;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
}
.json-viewer-small {
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 8px;
margin: 0;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
}
.error-content {
padding: 8px 0;
}
.error-type {
margin-bottom: 12px;
color: #f56c6c;
}
.error-message {
color: #f56c6c;
}
.error-message pre {
background: #fef0f0;
border: 1px solid #fde2e2;
border-radius: 4px;
padding: 12px;
margin-top: 8px;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 12px;
line-height: 1.6;
}
.timeline-content {
padding-left: 8px;
}
.log-message {
font-weight: 500;
margin-bottom: 4px;
}
.log-duration {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
.log-data {
margin-top: 8px;
}
</style>

View File

@@ -114,9 +114,10 @@
:pan-on-drag="true"
:pan-on-scroll="true"
:zoom-on-scroll="true"
:zoom-on-double-click="true"
:zoom-on-double-click="false"
:fit-view-on-init="false"
@node-click="onNodeClick"
@node-double-click="onNodeDoubleClick"
@edge-click="onEdgeClick"
@pane-click="onPaneClick"
@nodes-change="onNodesChange"
@@ -1233,6 +1234,15 @@
</el-button>
</template>
</el-dialog>
<!-- 节点执行详情面板 -->
<NodeExecutionDetail
v-model:visible="nodeDetailVisible"
:node-id="selectedNode?.id"
:node-label="selectedNode?.data?.label || selectedNode?.id"
:node-type="selectedNode?.type"
:execution-id="currentExecutionId"
/>
</div>
</template>
@@ -1251,6 +1261,7 @@ import api from '@/api'
import type { WorkflowNode, WorkflowEdge } from '@/types'
import { StartNode, LLMNode, ConditionNode, EndNode, DefaultNode } from './NodeTypes'
import { useCollaboration } from '@/composables/useCollaboration'
import NodeExecutionDetail from './NodeExecutionDetail.vue'
const props = defineProps<{
workflowId?: string
@@ -1271,6 +1282,8 @@ const selectedNode = ref<Node | null>(null)
const selectedEdge = ref<Edge | null>(null)
const draggedNodeType = ref<any>(null)
const testingNode = ref(false)
const nodeDetailVisible = ref(false)
const currentExecutionId = ref<string | null>(null)
// 节点复制相关
const copiedNode = ref<Node | null>(null)
@@ -1640,6 +1653,20 @@ const onNodeClick = (event: NodeClickEvent) => {
nodeTestResult.value = null
}
// 节点双击 - 显示执行详情
const onNodeDoubleClick = (event: NodeClickEvent) => {
// 阻止双击时的默认行为(如缩放)
event.event?.preventDefault?.()
if (currentExecutionId.value) {
// 确保选中节点
selectedNode.value = event.node
nodeDetailVisible.value = true
} else {
ElMessage.info('暂无执行记录,请先执行工作流')
}
}
// 判断是否为定时任务节点类型(计算属性)
const isScheduleNodeSelected = computed(() => {
return selectedNode.value && (selectedNode.value.type === 'schedule' || selectedNode.value.type === 'delay' || selectedNode.value.type === 'timer')
@@ -2699,6 +2726,11 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
console.log('[rjb] Current nodes:', nodes.value.map(n => ({ id: n.id, type: n.type, class: n.class })))
console.log('[rjb] All node IDs:', nodes.value.map(n => n.id))
// 提取execution_id
if (newStatus && newStatus.execution_id) {
currentExecutionId.value = newStatus.execution_id
}
// 使用 nextTick 确保 DOM 更新
nextTick(() => {
// 清除所有节点的执行状态