知你客服
This commit is contained in:
@@ -98,6 +98,70 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 工具调用可视化 -->
|
||||
<el-card v-if="toolCalls.length > 0" class="tool-calls-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>🔧 工具调用</span>
|
||||
<el-tag type="info" size="small">{{ toolCalls.length }} 个工具调用</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(toolCall, index) in toolCalls"
|
||||
:key="index"
|
||||
:timestamp="formatTime(toolCall.timestamp)"
|
||||
:type="toolCall.status === 'success' ? 'success' : toolCall.status === 'failed' ? 'danger' : 'primary'"
|
||||
:icon="toolCall.status === 'success' ? 'CircleCheck' : toolCall.status === 'failed' ? 'CircleClose' : 'Loading'"
|
||||
>
|
||||
<div class="tool-call-content">
|
||||
<div class="tool-call-header">
|
||||
<el-tag :type="toolCall.status === 'success' ? 'success' : toolCall.status === 'failed' ? 'danger' : 'info'" size="small">
|
||||
{{ toolCall.tool_name }}
|
||||
</el-tag>
|
||||
<span v-if="toolCall.duration" class="tool-call-duration">
|
||||
耗时: {{ toolCall.duration }}ms
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 工具参数 -->
|
||||
<div v-if="toolCall.tool_args" class="tool-call-section">
|
||||
<div class="section-title">📥 参数</div>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="查看参数" :name="`args-${index}`">
|
||||
<pre class="json-viewer-small">{{ formatJSON(toolCall.tool_args) }}</pre>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 工具结果 -->
|
||||
<div v-if="toolCall.tool_result" class="tool-call-section">
|
||||
<div class="section-title">
|
||||
{{ toolCall.status === 'success' ? '✅ 结果' : '❌ 错误' }}
|
||||
</div>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="查看结果" :name="`result-${index}`">
|
||||
<pre class="json-viewer-small" :class="{ 'error-result': toolCall.status === 'failed' }">
|
||||
{{ formatToolResult(toolCall.tool_result, toolCall.tool_result_length) }}
|
||||
</pre>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="toolCall.error" class="tool-call-error">
|
||||
<el-alert
|
||||
type="error"
|
||||
:title="toolCall.error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
|
||||
<!-- 执行时间线 -->
|
||||
<el-card class="timeline-card" shadow="never">
|
||||
<template #header>
|
||||
@@ -228,6 +292,98 @@ const statusType = computed(() => {
|
||||
return 'info'
|
||||
})
|
||||
|
||||
// 提取工具调用信息
|
||||
const toolCalls = computed(() => {
|
||||
const calls: Array<{
|
||||
tool_name: string
|
||||
tool_call_id?: string
|
||||
tool_args?: any
|
||||
tool_result?: string
|
||||
tool_result_length?: number
|
||||
status: 'success' | 'failed' | 'requested'
|
||||
timestamp: string
|
||||
duration?: number
|
||||
error?: string
|
||||
}> = []
|
||||
|
||||
nodeLogs.value.forEach(log => {
|
||||
// 检查是否是工具调用相关的日志
|
||||
if (log.data) {
|
||||
const data = log.data
|
||||
|
||||
// 工具调用请求
|
||||
if (data.tool_name && data.status === 'requested') {
|
||||
calls.push({
|
||||
tool_name: data.tool_name,
|
||||
tool_call_id: data.tool_call_id,
|
||||
tool_args: data.tool_args,
|
||||
status: 'requested',
|
||||
timestamp: log.timestamp
|
||||
})
|
||||
}
|
||||
|
||||
// 工具调用成功
|
||||
if (data.tool_name && data.status === 'success') {
|
||||
const existingCall = calls.find(c =>
|
||||
c.tool_name === data.tool_name &&
|
||||
c.tool_call_id === data.tool_call_id &&
|
||||
c.status === 'requested'
|
||||
)
|
||||
|
||||
if (existingCall) {
|
||||
existingCall.status = 'success'
|
||||
existingCall.tool_result = data.tool_result
|
||||
existingCall.tool_result_length = data.tool_result_length
|
||||
existingCall.duration = log.duration || data.duration
|
||||
} else {
|
||||
calls.push({
|
||||
tool_name: data.tool_name,
|
||||
tool_call_id: data.tool_call_id,
|
||||
tool_args: data.tool_args,
|
||||
tool_result: data.tool_result,
|
||||
tool_result_length: data.tool_result_length,
|
||||
status: 'success',
|
||||
timestamp: log.timestamp,
|
||||
duration: log.duration || data.duration
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 工具调用失败
|
||||
if (data.tool_name && data.status === 'failed') {
|
||||
const existingCall = calls.find(c =>
|
||||
c.tool_name === data.tool_name &&
|
||||
c.tool_call_id === data.tool_call_id &&
|
||||
c.status === 'requested'
|
||||
)
|
||||
|
||||
if (existingCall) {
|
||||
existingCall.status = 'failed'
|
||||
existingCall.error = data.error
|
||||
existingCall.duration = log.duration || data.duration
|
||||
} else {
|
||||
calls.push({
|
||||
tool_name: data.tool_name,
|
||||
tool_call_id: data.tool_call_id,
|
||||
tool_args: data.tool_args,
|
||||
error: data.error,
|
||||
status: 'failed',
|
||||
timestamp: log.timestamp,
|
||||
duration: log.duration || data.duration
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 按时间排序
|
||||
calls.sort((a, b) => {
|
||||
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
})
|
||||
|
||||
return calls
|
||||
})
|
||||
|
||||
// 监听props变化,加载日志
|
||||
watch([() => props.visible, () => props.executionId, () => props.nodeId],
|
||||
([newVisible, newExecutionId, newNodeId]) => {
|
||||
@@ -336,6 +492,30 @@ const getLogIcon = (level: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化工具结果
|
||||
const formatToolResult = (result: string, length?: number) => {
|
||||
if (!result) return ''
|
||||
|
||||
// 如果结果太长,显示预览
|
||||
if (length && length > 500) {
|
||||
try {
|
||||
// 尝试解析JSON
|
||||
const parsed = JSON.parse(result)
|
||||
const formatted = JSON.stringify(parsed, null, 2)
|
||||
return formatted.substring(0, 1000) + '\n\n... (结果已截断,完整结果长度: ' + length + ' 字符)'
|
||||
} catch {
|
||||
return result.substring(0, 1000) + '\n\n... (结果已截断,完整结果长度: ' + length + ' 字符)'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(result)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
const copyToClipboard = async (data: any) => {
|
||||
try {
|
||||
@@ -442,4 +622,46 @@ const copyToClipboard = async (data: any) => {
|
||||
.log-data {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tool-calls-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tool-call-content {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.tool-call-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tool-call-duration {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-call-section {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-call-error {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.error-result {
|
||||
background: #fef0f0;
|
||||
border-color: #fde2e2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -284,7 +284,71 @@
|
||||
|
||||
<!-- 右侧配置面板 -->
|
||||
<div class="config-panel" v-if="selectedNode">
|
||||
<h3>节点配置</h3>
|
||||
<!-- 配置面板头部 -->
|
||||
<div class="config-panel-header">
|
||||
<div class="config-panel-title">
|
||||
<h3>节点配置</h3>
|
||||
</div>
|
||||
<div class="config-panel-actions">
|
||||
<!-- 测试该节点 -->
|
||||
<el-tooltip content="测试该节点" placement="bottom">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleTestNode"
|
||||
:loading="testingNode"
|
||||
:disabled="!selectedNode || !!testInputError"
|
||||
class="config-action-btn"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 更多操作 -->
|
||||
<el-dropdown trigger="click" @command="handleConfigMoreAction">
|
||||
<el-tooltip content="更多" placement="bottom">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="config-action-btn"
|
||||
>
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="save">
|
||||
<el-icon><Check /></el-icon>
|
||||
保存配置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="duplicate" divided>
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
复制节点
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除节点
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 关闭面板 -->
|
||||
<el-tooltip content="关闭" placement="bottom">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="closeConfigPanel"
|
||||
class="config-action-btn"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置标签页 -->
|
||||
<el-tabs v-model="configActiveTab" type="border-card">
|
||||
@@ -301,6 +365,199 @@
|
||||
<el-input v-model="selectedNode.data.label" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 输入变量配置 -->
|
||||
<el-divider />
|
||||
<el-collapse v-model="inputOutputCollapse" style="margin-bottom: 16px;">
|
||||
<el-collapse-item name="input">
|
||||
<template #title>
|
||||
<div style="display: flex; align-items: center; width: 100%;">
|
||||
<span style="flex: 1;">输入</span>
|
||||
<el-icon style="margin-right: 8px;"><InfoFilled /></el-icon>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="addInputVariable"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!inputVariables.length" style="text-align: center; padding: 20px; color: #909399;">
|
||||
<el-icon style="font-size: 32px; margin-bottom: 8px;"><Box /></el-icon>
|
||||
<div>暂未配置输入变量</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-else
|
||||
:data="inputVariables"
|
||||
border
|
||||
size="small"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-table-column label="变量名" width="150">
|
||||
<template #default="{ row, $index }">
|
||||
<el-input
|
||||
v-model="row.name"
|
||||
placeholder="变量名"
|
||||
size="small"
|
||||
@blur="updateInputVariable($index)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变量值" min-width="200">
|
||||
<template #default="{ row, $index }">
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<el-select
|
||||
v-model="row.type"
|
||||
size="small"
|
||||
style="width: 100px;"
|
||||
@change="updateInputVariable($index)"
|
||||
>
|
||||
<el-option label="str." value="string" />
|
||||
<el-option label="int." value="integer" />
|
||||
<el-option label="float." value="number" />
|
||||
<el-option label="bool." value="boolean" />
|
||||
<el-option label="object" value="object" />
|
||||
<el-option label="array" value="array" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="row.value"
|
||||
placeholder="输入或引用参数值"
|
||||
size="small"
|
||||
style="flex: 1;"
|
||||
@blur="updateInputVariable($index)"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon
|
||||
style="cursor: pointer;"
|
||||
@click="showVariableSelector($index, 'input')"
|
||||
>
|
||||
<Aim />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="removeInputVariable($index)"
|
||||
>
|
||||
<el-icon><Minus /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
|
||||
提示:可以使用{{变量名}}、{{变量名.子变量名}}、{{变量名[数组索引]}}的方式引用输入参数中的变量
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<!-- 输出变量配置 -->
|
||||
<el-collapse-item name="output">
|
||||
<template #title>
|
||||
<div style="display: flex; align-items: center; width: 100%;">
|
||||
<span style="flex: 1;">输出</span>
|
||||
<el-icon style="margin-right: 8px;"><InfoFilled /></el-icon>
|
||||
<div style="margin-right: 8px; font-size: 12px; color: #909399;">
|
||||
输出格式
|
||||
<el-select
|
||||
v-model="selectedNode.data.output_format"
|
||||
size="small"
|
||||
style="width: 100px; margin-left: 4px;"
|
||||
@click.stop
|
||||
>
|
||||
<el-option label="JSON" value="json" />
|
||||
<el-option label="Text" value="text" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="addOutputVariable"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!outputVariables.length" style="text-align: center; padding: 20px; color: #909399;">
|
||||
<el-icon style="font-size: 32px; margin-bottom: 8px;"><Box /></el-icon>
|
||||
<div>暂未配置输出变量</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-else
|
||||
:data="outputVariables"
|
||||
border
|
||||
size="small"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-table-column label="变量名" width="150">
|
||||
<template #default="{ row, $index }">
|
||||
<el-input
|
||||
v-model="row.name"
|
||||
placeholder="变量名"
|
||||
size="small"
|
||||
@blur="updateOutputVariable($index)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变量类型" min-width="200">
|
||||
<template #default="{ row, $index }">
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<el-select
|
||||
v-model="row.type"
|
||||
size="small"
|
||||
style="flex: 1;"
|
||||
@change="updateOutputVariable($index)"
|
||||
>
|
||||
<el-option label="str. String" value="string" />
|
||||
<el-option label="int. Integer" value="integer" />
|
||||
<el-option label="float. Number" value="number" />
|
||||
<el-option label="bool. Boolean" value="boolean" />
|
||||
<el-option label="object. Object" value="object" />
|
||||
<el-option label="Array<Object>" value="array" />
|
||||
<el-option label="Array<String>" value="string[]" />
|
||||
<el-option label="Array<Number>" value="number[]" />
|
||||
</el-select>
|
||||
<el-icon
|
||||
style="cursor: pointer; font-size: 16px;"
|
||||
@click="expandOutputVariable($index)"
|
||||
>
|
||||
<ArrowDown v-if="expandedOutputVariableIndex !== $index" />
|
||||
<ArrowUp v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="removeOutputVariable($index)"
|
||||
>
|
||||
<el-icon><Minus /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="expandedOutputVariableIndex !== null && expandedOutputVariableIndex >= 0 && outputVariables && outputVariables[expandedOutputVariableIndex]" style="margin-top: 12px; padding: 12px; background: #f5f7fa; border-radius: 4px;">
|
||||
<div style="font-size: 12px; color: #606266; margin-bottom: 8px;">
|
||||
<strong>{{ outputVariables[expandedOutputVariableIndex]?.name }}</strong> 结构预览
|
||||
</div>
|
||||
<pre style="font-size: 11px; color: #909399; margin: 0; white-space: pre-wrap;">{{ JSON.stringify(getOutputVariableStructure(expandedOutputVariableIndex), null, 2) }}</pre>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<!-- 快速模板 & 变量插入 -->
|
||||
<el-divider />
|
||||
<div class="quick-actions">
|
||||
@@ -1462,13 +1719,6 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<div class="config-actions">
|
||||
<el-button type="primary" @click="handleSaveNode">保存配置</el-button>
|
||||
<el-button @click="handleCopyNode">复制节点</el-button>
|
||||
<el-button type="danger" @click="handleDeleteNode">删除节点</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -2098,9 +2348,9 @@
|
||||
|
||||
<!-- 配置模式选择 -->
|
||||
<el-radio-group v-model="configAssistantMode" style="width: 100%; margin-bottom: 15px;">
|
||||
<el-radio-button label="simple">简单模式</el-radio-button>
|
||||
<el-radio-button label="template">模板模式</el-radio-button>
|
||||
<el-radio-button label="wizard">向导模式</el-radio-button>
|
||||
<el-radio-button value="simple">简单模式</el-radio-button>
|
||||
<el-radio-button value="template">模板模式</el-radio-button>
|
||||
<el-radio-button value="wizard">向导模式</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<!-- 简单模式 -->
|
||||
@@ -2461,7 +2711,7 @@
|
||||
type="success"
|
||||
@click="handleTestNode"
|
||||
:loading="testingNode"
|
||||
:disabled="!selectedNode || testInputError"
|
||||
:disabled="!selectedNode || !!testInputError"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
@@ -2648,7 +2898,7 @@ import { Controls } from '@vue-flow/controls'
|
||||
import { MiniMap } from '@vue-flow/minimap'
|
||||
import type { Node, Edge, NodeClickEvent, EdgeClickEvent, Connection, Viewport } from '@vue-flow/core'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Check, Warning, ZoomIn, ZoomOut, FullScreen, DocumentCopy, User, VideoPlay, InfoFilled, WarningFilled, Rank, ArrowDown, Sort, Grid, Operation, Document, Search, Timer, Box, Edit, Picture, Download, Delete, CircleCheck, CircleClose, Aim, ArrowLeft, ArrowRight, Refresh, Loading, Star, ChatDotRound, Connection as ConnectionIcon, Setting, DataAnalysis, Link, Upload, UploadFilled, DocumentAdd, QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { Check, Warning, ZoomIn, ZoomOut, FullScreen, DocumentCopy, User, VideoPlay, InfoFilled, WarningFilled, Rank, ArrowDown, ArrowUp, Sort, Grid, Operation, Document, Search, Timer, Box, Edit, Picture, Download, Delete, CircleCheck, CircleClose, Aim, ArrowLeft, ArrowRight, Refresh, Loading, Star, ChatDotRound, Connection as ConnectionIcon, Setting, DataAnalysis, Link, Upload, UploadFilled, DocumentAdd, QuestionFilled, Plus, Minus, MoreFilled, Close } from '@element-plus/icons-vue'
|
||||
import { useWorkflowStore } from '@/stores/workflow'
|
||||
import api from '@/api'
|
||||
import type { WorkflowNode, WorkflowEdge } from '@/types'
|
||||
@@ -3723,6 +3973,159 @@ const getVarTypeTag = (type: string) => {
|
||||
// 变量面板展开状态
|
||||
const variablePanelActive = ref<string[]>([])
|
||||
|
||||
// 输入输出变量管理
|
||||
const inputOutputCollapse = ref<string[]>([])
|
||||
const expandedOutputVariableIndex = ref<number | null>(null)
|
||||
|
||||
// 输入变量列表
|
||||
const inputVariables = computed(() => {
|
||||
if (!selectedNode.value || !selectedNode.value.data) return []
|
||||
if (!selectedNode.value.data.input_variables) {
|
||||
selectedNode.value.data.input_variables = []
|
||||
}
|
||||
return selectedNode.value.data.input_variables
|
||||
})
|
||||
|
||||
// 输出变量列表
|
||||
const outputVariables = computed(() => {
|
||||
if (!selectedNode.value || !selectedNode.value.data) return []
|
||||
if (!selectedNode.value.data.output_variables) {
|
||||
selectedNode.value.data.output_variables = []
|
||||
}
|
||||
return selectedNode.value.data.output_variables
|
||||
})
|
||||
|
||||
// 添加输入变量
|
||||
const addInputVariable = () => {
|
||||
if (!selectedNode.value) return
|
||||
if (!selectedNode.value.data.input_variables) {
|
||||
selectedNode.value.data.input_variables = []
|
||||
}
|
||||
selectedNode.value.data.input_variables.push({
|
||||
name: `input_${selectedNode.value.data.input_variables.length + 1}`,
|
||||
type: 'string',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除输入变量
|
||||
const removeInputVariable = (index: number) => {
|
||||
if (!selectedNode.value || !selectedNode.value.data) return
|
||||
if (!selectedNode.value.data.input_variables) {
|
||||
selectedNode.value.data.input_variables = []
|
||||
return
|
||||
}
|
||||
if (index >= 0 && index < selectedNode.value.data.input_variables.length) {
|
||||
selectedNode.value.data.input_variables.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新输入变量
|
||||
const updateInputVariable = (index: number) => {
|
||||
// 变量已通过 v-model 自动更新,这里可以添加验证逻辑
|
||||
if (selectedNode.value && selectedNode.value.data && selectedNode.value.data.input_variables) {
|
||||
if (index >= 0 && index < selectedNode.value.data.input_variables.length) {
|
||||
const variable = selectedNode.value.data.input_variables[index]
|
||||
if (variable && !variable.name) {
|
||||
ElMessage.warning('变量名不能为空')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加输出变量
|
||||
const addOutputVariable = () => {
|
||||
if (!selectedNode.value || !selectedNode.value.data) return
|
||||
if (!selectedNode.value.data.output_variables) {
|
||||
selectedNode.value.data.output_variables = []
|
||||
}
|
||||
selectedNode.value.data.output_variables.push({
|
||||
name: `output_${selectedNode.value.data.output_variables.length + 1}`,
|
||||
type: 'string'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除输出变量
|
||||
const removeOutputVariable = (index: number) => {
|
||||
if (!selectedNode.value || !selectedNode.value.data) return
|
||||
if (!selectedNode.value.data.output_variables) {
|
||||
selectedNode.value.data.output_variables = []
|
||||
return
|
||||
}
|
||||
if (index >= 0 && index < selectedNode.value.data.output_variables.length) {
|
||||
selectedNode.value.data.output_variables.splice(index, 1)
|
||||
if (expandedOutputVariableIndex.value === index) {
|
||||
expandedOutputVariableIndex.value = null
|
||||
} else if (expandedOutputVariableIndex.value !== null && expandedOutputVariableIndex.value > index) {
|
||||
expandedOutputVariableIndex.value = expandedOutputVariableIndex.value - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新输出变量
|
||||
const updateOutputVariable = (index: number) => {
|
||||
// 变量已通过 v-model 自动更新,这里可以添加验证逻辑
|
||||
if (selectedNode.value && selectedNode.value.data && selectedNode.value.data.output_variables) {
|
||||
if (index >= 0 && index < selectedNode.value.data.output_variables.length) {
|
||||
const variable = selectedNode.value.data.output_variables[index]
|
||||
if (variable && !variable.name) {
|
||||
ElMessage.warning('变量名不能为空')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开/收起输出变量结构
|
||||
const expandOutputVariable = (index: number) => {
|
||||
if (!selectedNode.value || !selectedNode.value.data) return
|
||||
if (!selectedNode.value.data.output_variables) {
|
||||
selectedNode.value.data.output_variables = []
|
||||
return
|
||||
}
|
||||
if (index >= 0 && index < selectedNode.value.data.output_variables.length) {
|
||||
if (expandedOutputVariableIndex.value === index) {
|
||||
expandedOutputVariableIndex.value = null
|
||||
} else {
|
||||
expandedOutputVariableIndex.value = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取输出变量结构预览
|
||||
const getOutputVariableStructure = (index: number) => {
|
||||
if (!selectedNode.value || !selectedNode.value.data || !selectedNode.value.data.output_variables) return {}
|
||||
if (index < 0 || index >= selectedNode.value.data.output_variables.length) return {}
|
||||
const variable = selectedNode.value.data.output_variables[index]
|
||||
if (!variable) return {}
|
||||
|
||||
// 根据类型生成示例结构
|
||||
const type = variable.type
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return { [variable.name]: 'string value' }
|
||||
case 'integer':
|
||||
return { [variable.name]: 0 }
|
||||
case 'number':
|
||||
return { [variable.name]: 0.0 }
|
||||
case 'boolean':
|
||||
return { [variable.name]: true }
|
||||
case 'object':
|
||||
return { [variable.name]: { key: 'value' } }
|
||||
case 'array':
|
||||
case 'string[]':
|
||||
case 'number[]':
|
||||
return { [variable.name]: [] }
|
||||
default:
|
||||
return { [variable.name]: null }
|
||||
}
|
||||
}
|
||||
|
||||
// 显示变量选择器(用于输入变量的值)
|
||||
const showVariableSelector = (index: number, type: 'input' | 'output') => {
|
||||
// TODO: 实现变量选择器弹窗
|
||||
ElMessage.info('变量选择器功能开发中...')
|
||||
}
|
||||
|
||||
// 变量自动补全相关
|
||||
const promptTextareaRef = ref<any>(null)
|
||||
const autocompleteDropdownRef = ref<HTMLElement | null>(null)
|
||||
@@ -5565,11 +5968,39 @@ watch(nodeTestInput, (value) => {
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(value)
|
||||
const parsed = JSON.parse(value)
|
||||
// 验证解析后的结果必须是对象
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
testInputError.value = '输入必须是JSON对象'
|
||||
return
|
||||
}
|
||||
testInputError.value = ''
|
||||
} catch (error: any) {
|
||||
testInputError.value = 'JSON格式错误: ' + error.message
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 当节点切换时,重置测试输入错误
|
||||
watch(selectedNode, () => {
|
||||
testInputError.value = ''
|
||||
// 重新验证当前输入
|
||||
if (nodeTestInput.value) {
|
||||
const value = nodeTestInput.value
|
||||
if (value.trim() === '') {
|
||||
testInputError.value = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
testInputError.value = '输入必须是JSON对象'
|
||||
return
|
||||
}
|
||||
testInputError.value = ''
|
||||
} catch (error: any) {
|
||||
testInputError.value = 'JSON格式错误: ' + error.message
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 测试用例存储
|
||||
@@ -5921,6 +6352,45 @@ const handleSaveNode = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭配置面板
|
||||
const closeConfigPanel = () => {
|
||||
selectedNode.value = null
|
||||
selectedEdge.value = null
|
||||
}
|
||||
|
||||
// 处理配置面板更多操作
|
||||
const handleConfigMoreAction = (command: string) => {
|
||||
if (!selectedNode.value) return
|
||||
|
||||
switch (command) {
|
||||
case 'save':
|
||||
// 保存配置
|
||||
handleSaveNode()
|
||||
break
|
||||
case 'duplicate':
|
||||
// 复制节点
|
||||
handleCopyNode()
|
||||
break
|
||||
case 'delete':
|
||||
// 删除节点
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除节点 "${selectedNode.value.data?.label || selectedNode.value.id}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
handleDeleteNode()
|
||||
closeConfigPanel()
|
||||
}).catch(() => {
|
||||
// 取消删除
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 测试节点
|
||||
const handleTestNode = async () => {
|
||||
if (!selectedNode.value) {
|
||||
@@ -6660,24 +7130,71 @@ const handleAutoLayout = async () => {
|
||||
// 布局参数
|
||||
const nodeWidth = 200
|
||||
const nodeHeight = 80
|
||||
const horizontalSpacing = 280 // 水平间距(增大间距,避免节点重叠)
|
||||
const verticalSpacing = 180 // 垂直间距(增大间距,使布局更清晰)
|
||||
const horizontalSpacing = 320 // 水平间距(节点之间的水平距离)
|
||||
const verticalSpacing = 150 // 垂直间距(用于有分支的情况)
|
||||
const startX = 100
|
||||
const startY = 100
|
||||
const startY = 200
|
||||
|
||||
// 计算每层的布局(水平居中)
|
||||
layers.forEach((layer, layerIndex) => {
|
||||
const layerY = startY + layerIndex * verticalSpacing
|
||||
const layerWidth = (layer.length - 1) * horizontalSpacing
|
||||
const layerStartX = startX - layerWidth / 2
|
||||
|
||||
layer.forEach((nodeId, nodeIndex) => {
|
||||
const nodeX = layerStartX + nodeIndex * horizontalSpacing
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: layerY }
|
||||
// 检查是否是简单的线性工作流(每层只有一个节点)
|
||||
let isLinearWorkflow = true
|
||||
layers.forEach(layer => {
|
||||
if (layer.length > 1) {
|
||||
isLinearWorkflow = false
|
||||
}
|
||||
})
|
||||
|
||||
// 如果每层只有一个节点,使用水平线性布局(从左到右,所有节点在同一水平线)
|
||||
if (isLinearWorkflow && layers.length > 1) {
|
||||
// 水平线性布局:所有节点水平排列在同一水平线上
|
||||
layers.forEach((layer, layerIndex) => {
|
||||
layer.forEach((nodeId) => {
|
||||
const nodeX = startX + layerIndex * horizontalSpacing
|
||||
const nodeY = startY // 所有节点在同一水平线上
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: nodeY }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 层次布局:有分支的工作流
|
||||
// 优化策略:尽量让单节点层水平排列,多节点层才垂直排列
|
||||
let baseY = startY
|
||||
let currentX = startX
|
||||
let consecutiveSingleNodeLayers = 0
|
||||
|
||||
layers.forEach((layer, layerIndex) => {
|
||||
if (layer.length === 1) {
|
||||
// 单节点层:水平排列
|
||||
consecutiveSingleNodeLayers++
|
||||
const nodeId = layer[0]
|
||||
const nodeX = currentX
|
||||
// 如果连续多个单节点层,保持水平对齐
|
||||
const nodeY = baseY + (consecutiveSingleNodeLayers > 3 ? 20 : 0) // 如果连续太多,稍微下移
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: nodeY }
|
||||
})
|
||||
currentX += horizontalSpacing
|
||||
} else {
|
||||
// 多节点层:水平居中排列,使用新的Y坐标
|
||||
consecutiveSingleNodeLayers = 0
|
||||
baseY += verticalSpacing
|
||||
currentX = startX // 重置X位置
|
||||
|
||||
const layerWidth = (layer.length - 1) * horizontalSpacing
|
||||
const layerStartX = startX
|
||||
|
||||
layer.forEach((nodeId, nodeIndex) => {
|
||||
const nodeX = layerStartX + nodeIndex * horizontalSpacing
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: baseY }
|
||||
})
|
||||
})
|
||||
|
||||
// 更新currentX为下一层的起始位置
|
||||
currentX = layerStartX + layerWidth + horizontalSpacing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 自动调整视口,使所有节点可见
|
||||
await nextTick()
|
||||
@@ -7574,16 +8091,63 @@ onUnmounted(() => {
|
||||
min-width: 380px;
|
||||
background: #fff;
|
||||
border-left: 1px solid #ddd;
|
||||
padding: 15px 20px 15px 15px; /* 右侧预留滚动条空间,避免按钮被遮挡 */
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-panel h3 {
|
||||
margin: 0 0 15px 0;
|
||||
.config-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px 15px 15px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.config-panel-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-panel-title h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.config-action-btn {
|
||||
padding: 6px 8px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-action-btn:hover {
|
||||
background: #f5f7fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.config-panel :deep(.el-tabs) {
|
||||
padding: 15px 20px 15px 15px;
|
||||
}
|
||||
|
||||
.node-test-section {
|
||||
|
||||
Reference in New Issue
Block a user