处理agent答非所问的问题
This commit is contained in:
445
frontend/src/components/WorkflowEditor/NodeExecutionDetail.vue
Normal file
445
frontend/src/components/WorkflowEditor/NodeExecutionDetail.vue
Normal 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>
|
||||
@@ -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(() => {
|
||||
// 清除所有节点的执行状态
|
||||
|
||||
Reference in New Issue
Block a user