|
|
|
@ -2,6 +2,8 @@ |
|
|
|
<div class="flow-editor-page"> |
|
|
|
<el-page-header @back="$router.back()" :content="isEdit ? '编辑: ' + flowName : '创建新流'" /> |
|
|
|
|
|
|
|
<el-alert v-if="initError" type="error" :title="initError" show-icon closable @close="initError = ''" style="margin-top: 12px" /> |
|
|
|
|
|
|
|
<el-card style="margin-top: 20px"> |
|
|
|
<div class="editor-toolbar"> |
|
|
|
<el-input v-model="flowName" placeholder="流名称" style="width: 200px" /> |
|
|
|
@ -14,7 +16,7 @@ |
|
|
|
</div> |
|
|
|
</el-card> |
|
|
|
|
|
|
|
<div class="editor-body"> |
|
|
|
<div v-if="!initError" class="editor-body"> |
|
|
|
<div class="node-panel"> |
|
|
|
<div class="panel-title">节点面板</div> |
|
|
|
<div |
|
|
|
@ -43,51 +45,16 @@ |
|
|
|
<span>设计画布</span> |
|
|
|
<el-button size="small" @click="clearCanvas">清空</el-button> |
|
|
|
</div> |
|
|
|
<VueFlow |
|
|
|
ref="vueFlowRef" |
|
|
|
<FlowCanvas |
|
|
|
ref="canvasRef" |
|
|
|
v-model:nodes="nodes" |
|
|
|
v-model:edges="edges" |
|
|
|
:default-viewport="{ zoom: 1, x: 0, y: 0 }" |
|
|
|
:min-zoom="0.2" |
|
|
|
:max-zoom="4" |
|
|
|
:snap-to-grid="true" |
|
|
|
:snap-grid="[20, 20]" |
|
|
|
:undo-redo="true" |
|
|
|
:connection-line-style="{ stroke: '#409EFF', strokeWidth: 2 }" |
|
|
|
:default-edge-options="{ |
|
|
|
type: 'smoothstep', |
|
|
|
animated: true, |
|
|
|
markerEnd: MarkerType.ArrowClosed, |
|
|
|
style: { stroke: '#409EFF', strokeWidth: 2 }, |
|
|
|
}" |
|
|
|
fit-view-on-init |
|
|
|
@nodes-change="onNodesChange" |
|
|
|
@edges-change="onEdgesChange" |
|
|
|
@connect="onConnect" |
|
|
|
:node-color="getMiniMapColor" |
|
|
|
@node-click="onNodeClick" |
|
|
|
@pane-click="onPaneClick" |
|
|
|
@drop="onDrop" |
|
|
|
@dragover.prevent |
|
|
|
> |
|
|
|
<Background :gap="20" :size="1" /> |
|
|
|
<Controls position="bottom-right" /> |
|
|
|
<MiniMap |
|
|
|
position="bottom-left" |
|
|
|
:node-stroke-color="'#409EFF'" |
|
|
|
:node-color="getMiniMapColor" |
|
|
|
pannable |
|
|
|
zoomable |
|
|
|
/> |
|
|
|
|
|
|
|
<template #node-custom="nodeProps"> |
|
|
|
<FlowNode |
|
|
|
:id="nodeProps.id" |
|
|
|
:data="nodeProps.data" |
|
|
|
:selected="nodeProps.selected" |
|
|
|
@delete="removeNode(nodeProps.id)" |
|
|
|
/> |
|
|
|
</template> |
|
|
|
</VueFlow> |
|
|
|
@node-delete="removeNode" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="config-panel" v-if="selectedNodeId"> |
|
|
|
@ -209,26 +176,16 @@ |
|
|
|
import { ref, computed, onMounted } from 'vue' |
|
|
|
import { useRoute, useRouter } from 'vue-router' |
|
|
|
import { ElMessage } from 'element-plus' |
|
|
|
import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core' |
|
|
|
import { Background } from '@vue-flow/background' |
|
|
|
import { Controls } from '@vue-flow/controls' |
|
|
|
import { MiniMap } from '@vue-flow/minimap' |
|
|
|
import '@vue-flow/core/dist/style.css' |
|
|
|
import '@vue-flow/core/dist/theme-default.css' |
|
|
|
import '@vue-flow/controls/dist/style.css' |
|
|
|
import '@vue-flow/minimap/dist/style.css' |
|
|
|
import { flowApi, mcpApi, agentApi } from '@/api' |
|
|
|
import { |
|
|
|
Promotion, ChatDotRound, Tools, Connection, Bell, |
|
|
|
DataAnalysis, Search, |
|
|
|
} from '@element-plus/icons-vue' |
|
|
|
import FlowNode from './FlowNode.vue' |
|
|
|
import { Promotion, ChatDotRound, Tools, Connection, Bell, DataAnalysis, Search } from '@element-plus/icons-vue' |
|
|
|
import FlowCanvas from './FlowCanvas.vue' |
|
|
|
|
|
|
|
const route = useRoute() |
|
|
|
const router = useRouter() |
|
|
|
const flowId = computed(() => route.params.id as string) |
|
|
|
const isEdit = computed(() => !!flowId.value) |
|
|
|
|
|
|
|
const initError = ref('') |
|
|
|
|
|
|
|
const flowName = ref('新工作流') |
|
|
|
const flowDesc = ref('') |
|
|
|
const flowStatus = ref('') |
|
|
|
@ -238,6 +195,12 @@ const selectedNodeData = ref<any>({}) |
|
|
|
const mcpServers = ref<any[]>([]) |
|
|
|
const agentList = ref<any[]>([]) |
|
|
|
|
|
|
|
const nodes = ref<any[]>([]) |
|
|
|
const edges = ref<any[]>([]) |
|
|
|
const canvasRef = ref<any>(null) |
|
|
|
|
|
|
|
let nodeCounter = 0 |
|
|
|
|
|
|
|
const nodeTypes = [ |
|
|
|
{ type: 'trigger', label: '触发节点', icon: Promotion, typeDesc: '企微触发' }, |
|
|
|
{ type: 'llm', label: 'LLM处理', icon: ChatDotRound, typeDesc: 'AI处理' }, |
|
|
|
@ -260,24 +223,18 @@ const colorMap: Record<string, string> = { |
|
|
|
output: '#722ed1', |
|
|
|
} |
|
|
|
|
|
|
|
const vueFlowStore = useVueFlow() |
|
|
|
const { nodes, edges, addNodes, addEdges, removeNodes, removeEdges, toObject } = vueFlowStore |
|
|
|
const storeAny = vueFlowStore as any |
|
|
|
const undo = () => storeAny.undo?.() |
|
|
|
const redo = () => storeAny.redo?.() |
|
|
|
const canUndo = computed(() => !!(storeAny.canUndo ?? false)) |
|
|
|
const canRedo = computed(() => !!(storeAny.canRedo ?? false)) |
|
|
|
let nodeCounter = 0 |
|
|
|
function undo() { canvasRef.value?.undo() } |
|
|
|
function redo() { canvasRef.value?.redo() } |
|
|
|
const canUndo = computed(() => canvasRef.value?.canUndo ?? false) |
|
|
|
const canRedo = computed(() => canvasRef.value?.canRedo ?? false) |
|
|
|
|
|
|
|
const selectedNode = computed(() => { |
|
|
|
return nodes.value.find((n: any) => n.id === selectedNodeId.value) || null |
|
|
|
}) |
|
|
|
const selectedNode = computed(() => nodes.value.find((n: any) => n.id === selectedNodeId.value) || null) |
|
|
|
|
|
|
|
function getMiniMapColor(node: any) { |
|
|
|
return colorMap[node?.data?.type || node?.type] || '#409EFF' |
|
|
|
} |
|
|
|
|
|
|
|
function onDragStart(event: DragEvent, node: (typeof nodeTypes)[0]) { |
|
|
|
function onDragStart(event: DragEvent, node: any) { |
|
|
|
if (event.dataTransfer) { |
|
|
|
event.dataTransfer.setData('application/vueflow', JSON.stringify(node)) |
|
|
|
event.dataTransfer.effectAllowed = 'move' |
|
|
|
@ -287,74 +244,31 @@ function onDragStart(event: DragEvent, node: (typeof nodeTypes)[0]) { |
|
|
|
function onDrop(event: DragEvent) { |
|
|
|
const dataStr = event.dataTransfer?.getData('application/vueflow') |
|
|
|
if (!dataStr) return |
|
|
|
|
|
|
|
const nodeData = JSON.parse(dataStr) |
|
|
|
|
|
|
|
let position = { x: 100, y: 100 } |
|
|
|
try { |
|
|
|
const bounds = (event.currentTarget as HTMLElement)?.getBoundingClientRect() |
|
|
|
if (bounds) { |
|
|
|
position = { |
|
|
|
x: event.clientX - bounds.left - 80, |
|
|
|
y: event.clientY - bounds.top - 20, |
|
|
|
} |
|
|
|
} |
|
|
|
} catch { /* fallback */ } |
|
|
|
|
|
|
|
const id = `node_${++nodeCounter}` |
|
|
|
const newNode = { |
|
|
|
canvasRef.value?.addNodes?.([{ |
|
|
|
id, |
|
|
|
type: 'custom', |
|
|
|
position, |
|
|
|
position: { x: 100 + nodeCounter * 50, y: 100 + nodeCounter * 30 }, |
|
|
|
data: { |
|
|
|
label: nodeData.label, |
|
|
|
type: nodeData.type, |
|
|
|
typeDesc: nodeData.typeDesc, |
|
|
|
color: colorMap[nodeData.type] || '#409EFF', |
|
|
|
config: nodeData.type === 'llm' |
|
|
|
? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7, agent_id: '' } |
|
|
|
: {}, |
|
|
|
config: nodeData.type === 'llm' ? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7, agent_id: '' } : {}, |
|
|
|
}, |
|
|
|
draggable: true, |
|
|
|
connectable: true, |
|
|
|
} |
|
|
|
addNodes([newNode]) |
|
|
|
} |
|
|
|
|
|
|
|
function onConnect(connection: any) { |
|
|
|
const sourceHandle = connection.sourceHandle || 'source' |
|
|
|
const edgeId = `edge_${connection.source}_${connection.target}_${sourceHandle}` |
|
|
|
|
|
|
|
const isConditionFalse = sourceHandle === 'false' |
|
|
|
const newEdge = { |
|
|
|
id: edgeId, |
|
|
|
source: connection.source, |
|
|
|
target: connection.target, |
|
|
|
sourceHandle: connection.sourceHandle, |
|
|
|
targetHandle: connection.targetHandle, |
|
|
|
type: 'smoothstep', |
|
|
|
animated: true, |
|
|
|
markerEnd: MarkerType.ArrowClosed, |
|
|
|
style: isConditionFalse |
|
|
|
? { stroke: '#F56C6C', strokeWidth: 2 } |
|
|
|
: { stroke: '#409EFF', strokeWidth: 2 }, |
|
|
|
} |
|
|
|
addEdges([newEdge]) |
|
|
|
}]) |
|
|
|
} |
|
|
|
|
|
|
|
function onNodeClick({ node }: any) { |
|
|
|
selectedNodeId.value = node.id |
|
|
|
const d = node.data || {} |
|
|
|
selectedNodeData.value = { |
|
|
|
label: d.label || '', |
|
|
|
typeDesc: d.typeDesc || '', |
|
|
|
...(d.config || {}), |
|
|
|
} |
|
|
|
selectedNodeData.value = { label: d.label || '', typeDesc: d.typeDesc || '', ...(d.config || {}) } |
|
|
|
} |
|
|
|
|
|
|
|
function onPaneClick() { |
|
|
|
selectedNodeId.value = '' |
|
|
|
} |
|
|
|
function onPaneClick() { selectedNodeId.value = '' } |
|
|
|
|
|
|
|
function onConfigLabelChange() { |
|
|
|
const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value) |
|
|
|
@ -379,288 +293,121 @@ function onAgentSelect(val: string) { |
|
|
|
function onConfigChange() { |
|
|
|
const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value) |
|
|
|
if (idx === -1) return |
|
|
|
|
|
|
|
const found = nodes.value[idx] |
|
|
|
const nodeType = found.data.type |
|
|
|
const cfg: any = {} |
|
|
|
|
|
|
|
if (nodeType === 'trigger') { |
|
|
|
cfg.event_type = selectedNodeData.value.event_type |
|
|
|
} else if (nodeType === 'llm') { |
|
|
|
cfg.system_prompt = selectedNodeData.value.system_prompt |
|
|
|
cfg.model = selectedNodeData.value.model |
|
|
|
cfg.temperature = selectedNodeData.value.temperature |
|
|
|
cfg.agent_id = selectedNodeData.value.agent_id |
|
|
|
} else if (nodeType === 'tool') { |
|
|
|
cfg.tool_name = selectedNodeData.value.tool_name |
|
|
|
} else if (nodeType === 'mcp') { |
|
|
|
cfg.mcp_server = selectedNodeData.value.mcp_server |
|
|
|
cfg.tool_name = selectedNodeData.value.tool_name |
|
|
|
} else if (nodeType === 'wecom_notify') { |
|
|
|
cfg.message_template = selectedNodeData.value.message_template |
|
|
|
cfg.target = selectedNodeData.value.target |
|
|
|
} else if (nodeType === 'condition') { |
|
|
|
cfg.condition = selectedNodeData.value.condition |
|
|
|
} else if (nodeType === 'rag') { |
|
|
|
cfg.knowledge_base = selectedNodeData.value.knowledge_base |
|
|
|
cfg.top_k = selectedNodeData.value.top_k |
|
|
|
} else if (nodeType === 'output') { |
|
|
|
cfg.format = selectedNodeData.value.format |
|
|
|
} |
|
|
|
|
|
|
|
if (nodeType === 'trigger') cfg.event_type = selectedNodeData.value.event_type |
|
|
|
else if (nodeType === 'llm') { cfg.system_prompt = selectedNodeData.value.system_prompt; cfg.model = selectedNodeData.value.model; cfg.temperature = selectedNodeData.value.temperature; cfg.agent_id = selectedNodeData.value.agent_id } |
|
|
|
else if (nodeType === 'tool') cfg.tool_name = selectedNodeData.value.tool_name |
|
|
|
else if (nodeType === 'mcp') { cfg.mcp_server = selectedNodeData.value.mcp_server; cfg.tool_name = selectedNodeData.value.tool_name } |
|
|
|
else if (nodeType === 'wecom_notify') { cfg.message_template = selectedNodeData.value.message_template; cfg.target = selectedNodeData.value.target } |
|
|
|
else if (nodeType === 'condition') cfg.condition = selectedNodeData.value.condition |
|
|
|
else if (nodeType === 'rag') { cfg.knowledge_base = selectedNodeData.value.knowledge_base; cfg.top_k = selectedNodeData.value.top_k } |
|
|
|
else if (nodeType === 'output') cfg.format = selectedNodeData.value.format |
|
|
|
const updated = { ...found } |
|
|
|
updated.data = { ...found.data, config: cfg } |
|
|
|
nodes.value[idx] = updated |
|
|
|
} |
|
|
|
|
|
|
|
function removeNode(id: string) { |
|
|
|
removeNodes([id]) |
|
|
|
edges.value = edges.value.filter((e: any) => e.source !== id && e.target !== id) |
|
|
|
canvasRef.value?.removeNodes?.([id]) |
|
|
|
if (selectedNodeId.value === id) selectedNodeId.value = '' |
|
|
|
} |
|
|
|
|
|
|
|
function clearCanvas() { |
|
|
|
nodes.value = [] |
|
|
|
edges.value = [] |
|
|
|
nodeCounter = 0 |
|
|
|
selectedNodeId.value = '' |
|
|
|
} |
|
|
|
|
|
|
|
function onNodesChange() {} |
|
|
|
function onEdgesChange() {} |
|
|
|
|
|
|
|
async function saveFlow() { |
|
|
|
if (!flowName.value) { ElMessage.warning('请输入流名称'); return } |
|
|
|
saving.value = true |
|
|
|
try { |
|
|
|
const serializedNodes = nodes.value.map((n: any) => ({ |
|
|
|
id: n.id, |
|
|
|
type: n.data?.type || n.type, |
|
|
|
label: n.data?.label || n.id, |
|
|
|
config: n.data?.config || {}, |
|
|
|
position: n.position, |
|
|
|
})) |
|
|
|
const serializedEdges = edges.value.map((e: any) => ({ |
|
|
|
source: e.source, |
|
|
|
target: e.target, |
|
|
|
sourceHandle: e.sourceHandle || 'source', |
|
|
|
condition: e.sourceHandle === 'false' ? 'false' : (e.sourceHandle === 'true' ? 'true' : undefined), |
|
|
|
})) |
|
|
|
|
|
|
|
const payload = { name: flowName.value, description: flowDesc.value, nodes: serializedNodes, edges: serializedEdges, trigger: {} } |
|
|
|
|
|
|
|
if (isEdit.value) { |
|
|
|
await flowApi.updateFlow(flowId.value, payload) |
|
|
|
ElMessage.success('保存成功') |
|
|
|
} else { |
|
|
|
const res: any = await flowApi.createFlow(payload) |
|
|
|
const data = res?.data || res || {} |
|
|
|
if (data.id) { |
|
|
|
router.replace(`/admin/flow/editor/${data.id}`) |
|
|
|
ElMessage.success('创建成功,请继续编辑') |
|
|
|
} |
|
|
|
} |
|
|
|
} finally { |
|
|
|
saving.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async function testFlow() { |
|
|
|
if (!isEdit.value) { ElMessage.info('请先保存再进行验证'); return } |
|
|
|
try { |
|
|
|
const res: any = await flowApi.testFlow(flowId.value) |
|
|
|
const data = res?.data || res || {} |
|
|
|
if (data.valid) { |
|
|
|
ElMessage.success(`验证通过: ${data.node_count}个节点, ${data.edge_count}条边`) |
|
|
|
} else { |
|
|
|
ElMessage.warning(`验证问题: ${(data.issues || []).join(', ')}`) |
|
|
|
} |
|
|
|
} catch { /**/ } |
|
|
|
} |
|
|
|
|
|
|
|
async function publishFlow() { |
|
|
|
if (!isEdit.value) { ElMessage.warning('请先保存'); return } |
|
|
|
await flowApi.publishFlow(flowId.value) |
|
|
|
ElMessage.success('流已上架到企微') |
|
|
|
await loadFlow() |
|
|
|
} |
|
|
|
function clearCanvas() { nodes.value = []; edges.value = []; nodeCounter = 0; selectedNodeId.value = '' } |
|
|
|
|
|
|
|
async function loadFlow() { |
|
|
|
if (!isEdit.value) return |
|
|
|
try { |
|
|
|
const { flowApi } = await import('@/api') |
|
|
|
const res: any = await flowApi.getFlow(flowId.value) |
|
|
|
const flow = res?.data || res || {} |
|
|
|
flowName.value = flow.name || '' |
|
|
|
flowDesc.value = flow.description || '' |
|
|
|
flowStatus.value = flow.status || '' |
|
|
|
|
|
|
|
const definition = flow.definition_json || {} |
|
|
|
const loadedNodes: any[] = [] |
|
|
|
const loadedEdges: any[] = [] |
|
|
|
|
|
|
|
const defNodes = definition.nodes || [] |
|
|
|
for (let i = 0; i < defNodes.length; i++) { |
|
|
|
const n = defNodes[i] |
|
|
|
const nt = nodeTypes.find(t => t.type === n.type) |
|
|
|
loadedNodes.push({ |
|
|
|
id: n.id || `node_${i}`, |
|
|
|
type: 'custom', |
|
|
|
id: n.id || `node_${i}`, type: 'custom', |
|
|
|
position: n.position || { x: 100 + (i % 4) * 250, y: 60 + Math.floor(i / 4) * 150 }, |
|
|
|
data: { |
|
|
|
label: n.label || n.id || `节点${i}`, |
|
|
|
type: n.type, |
|
|
|
typeDesc: nt?.typeDesc || n.type, |
|
|
|
color: colorMap[n.type] || '#409EFF', |
|
|
|
config: n.config || {}, |
|
|
|
}, |
|
|
|
draggable: true, |
|
|
|
connectable: true, |
|
|
|
data: { label: n.label || n.id || `节点${i}`, type: n.type, typeDesc: nt?.typeDesc || n.type, color: colorMap[n.type] || '#409EFF', config: n.config || {} }, |
|
|
|
draggable: true, connectable: true, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
const defEdges = definition.edges || [] |
|
|
|
for (const e of defEdges) { |
|
|
|
const source = e.source || e.from |
|
|
|
const target = e.target || e.to |
|
|
|
const cond = e.condition || e.sourceHandle |
|
|
|
const isFalse = cond === 'false' |
|
|
|
loadedEdges.push({ |
|
|
|
id: e.id || `edge_${source}_${target}`, |
|
|
|
source, |
|
|
|
target, |
|
|
|
sourceHandle: cond || 'source', |
|
|
|
type: 'smoothstep', |
|
|
|
animated: true, |
|
|
|
markerEnd: MarkerType.ArrowClosed, |
|
|
|
style: isFalse |
|
|
|
? { stroke: '#F56C6C', strokeWidth: 2 } |
|
|
|
: { stroke: '#409EFF', strokeWidth: 2 }, |
|
|
|
}) |
|
|
|
loadedEdges.push({ id: e.id || `edge_${source}_${target}`, source, target, sourceHandle: cond || 'source', type: 'smoothstep', animated: true, style: cond === 'false' ? { stroke: '#F56C6C', strokeWidth: 2 } : { stroke: '#409EFF', strokeWidth: 2 } }) |
|
|
|
} |
|
|
|
|
|
|
|
nodes.value = loadedNodes |
|
|
|
edges.value = loadedEdges |
|
|
|
nodeCounter = defNodes.length |
|
|
|
} catch { /**/ } |
|
|
|
} catch (e: any) { |
|
|
|
console.error('loadFlow error:', e) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async function loadMcpServers() { |
|
|
|
try { |
|
|
|
const res: any = await mcpApi.getServers() |
|
|
|
mcpServers.value = Array.isArray(res) ? res : (res?.data || []) |
|
|
|
} catch { /**/ } |
|
|
|
try { const { mcpApi } = await import('@/api'); const res: any = await mcpApi.getServers(); mcpServers.value = Array.isArray(res) ? res : (res?.data || []) } catch {} |
|
|
|
} |
|
|
|
|
|
|
|
async function loadAgents() { |
|
|
|
try { const { agentApi } = await import('@/api'); const res: any = await agentApi.getList(); agentList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) } catch {} |
|
|
|
} |
|
|
|
|
|
|
|
async function saveFlow() { |
|
|
|
if (!flowName.value) { ElMessage.warning('请输入流名称'); return } |
|
|
|
saving.value = true |
|
|
|
try { |
|
|
|
const res: any = await agentApi.getList() |
|
|
|
agentList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) |
|
|
|
} catch { /**/ } |
|
|
|
const { flowApi } = await import('@/api') |
|
|
|
const serializedNodes = nodes.value.map((n: any) => ({ id: n.id, type: n.data?.type || n.type, label: n.data?.label || n.id, config: n.data?.config || {}, position: n.position })) |
|
|
|
const serializedEdges = edges.value.map((e: any) => ({ source: e.source, target: e.target, sourceHandle: e.sourceHandle || 'source', condition: e.sourceHandle === 'false' ? 'false' : (e.sourceHandle === 'true' ? 'true' : undefined) })) |
|
|
|
const payload = { name: flowName.value, description: flowDesc.value, nodes: serializedNodes, edges: serializedEdges, trigger: {} } |
|
|
|
if (isEdit.value) { await flowApi.updateFlow(flowId.value, payload); ElMessage.success('保存成功') } |
|
|
|
else { const res: any = await flowApi.createFlow(payload); const data = res?.data || res || {}; if (data.id) { router.replace(`/admin/flow/editor/${data.id}`); ElMessage.success('创建成功') } } |
|
|
|
} finally { saving.value = false } |
|
|
|
} |
|
|
|
|
|
|
|
async function testFlow() { |
|
|
|
if (!isEdit.value) { ElMessage.info('请先保存再进行验证'); return } |
|
|
|
try { const { flowApi } = await import('@/api'); const res: any = await flowApi.testFlow(flowId.value); const data = res?.data || res || {}; if (data.valid) { ElMessage.success(`验证通过: ${data.node_count}个节点, ${data.edge_count}条边`) } else { ElMessage.warning(`验证问题: ${(data.issues || []).join(', ')}`) } } catch {} |
|
|
|
} |
|
|
|
|
|
|
|
async function publishFlow() { |
|
|
|
if (!isEdit.value) { ElMessage.warning('请先保存'); return } |
|
|
|
try { const { flowApi } = await import('@/api'); await flowApi.publishFlow(flowId.value); ElMessage.success('流已上架到企微'); await loadFlow() } catch {} |
|
|
|
} |
|
|
|
|
|
|
|
onMounted(async () => { |
|
|
|
if (isEdit.value) { |
|
|
|
await loadFlow() |
|
|
|
try { |
|
|
|
if (isEdit.value) { await loadFlow() } |
|
|
|
await Promise.allSettled([loadMcpServers(), loadAgents()]) |
|
|
|
} catch (e: any) { |
|
|
|
console.error('FlowEditor init error:', e) |
|
|
|
initError.value = '初始化失败: ' + (e?.message || '未知错误') |
|
|
|
} |
|
|
|
await loadMcpServers() |
|
|
|
await loadAgents() |
|
|
|
}) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
.flow-editor-page { |
|
|
|
height: 100%; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
.editor-toolbar { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
.editor-body { |
|
|
|
display: flex; |
|
|
|
gap: 12px; |
|
|
|
margin-top: 12px; |
|
|
|
flex: 1; |
|
|
|
min-height: 0; |
|
|
|
} |
|
|
|
.node-panel { |
|
|
|
width: 160px; |
|
|
|
background: #fff; |
|
|
|
border-radius: 4px; |
|
|
|
padding: 12px; |
|
|
|
border: 1px solid #ebeef5; |
|
|
|
overflow-y: auto; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
.panel-title { |
|
|
|
font-weight: bold; |
|
|
|
margin-bottom: 12px; |
|
|
|
padding-bottom: 8px; |
|
|
|
border-bottom: 1px solid #ebeef5; |
|
|
|
} |
|
|
|
.node-item { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 8px; |
|
|
|
padding: 8px; |
|
|
|
margin-bottom: 8px; |
|
|
|
border: 1px solid #e4e7ed; |
|
|
|
border-radius: 4px; |
|
|
|
cursor: grab; |
|
|
|
font-size: 13px; |
|
|
|
background: #f5f7fa; |
|
|
|
transition: all 0.2s; |
|
|
|
user-select: none; |
|
|
|
} |
|
|
|
.node-item:hover { |
|
|
|
border-color: #409EFF; |
|
|
|
background: #ecf5ff; |
|
|
|
} |
|
|
|
.panel-hint { |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
line-height: 1.8; |
|
|
|
} |
|
|
|
.panel-hint p::before { |
|
|
|
content: '• '; |
|
|
|
color: #409EFF; |
|
|
|
} |
|
|
|
.canvas-wrapper { |
|
|
|
flex: 1; |
|
|
|
background: #fff; |
|
|
|
border-radius: 4px; |
|
|
|
border: 1px solid #ebeef5; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
min-width: 0; |
|
|
|
} |
|
|
|
.canvas-header { |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
padding: 8px 16px; |
|
|
|
border-bottom: 1px solid #ebeef5; |
|
|
|
font-weight: bold; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
.canvas-wrapper :deep(.vue-flow) { |
|
|
|
flex: 1; |
|
|
|
min-height: 0; |
|
|
|
} |
|
|
|
.config-panel { |
|
|
|
width: 280px; |
|
|
|
background: #fff; |
|
|
|
border-radius: 4px; |
|
|
|
padding: 12px; |
|
|
|
border: 1px solid #ebeef5; |
|
|
|
overflow-y: auto; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
.config-actions { |
|
|
|
margin-top: 8px; |
|
|
|
padding-top: 8px; |
|
|
|
border-top: 1px solid #ebeef5; |
|
|
|
} |
|
|
|
.flow-editor-page { height: 100%; display: flex; flex-direction: column; } |
|
|
|
.editor-toolbar { display: flex; align-items: center; gap: 8px; } |
|
|
|
.editor-body { display: flex; gap: 12px; margin-top: 12px; flex: 1; min-height: 0; } |
|
|
|
.node-panel { width: 160px; background: #fff; border-radius: 4px; padding: 12px; border: 1px solid #ebeef5; overflow-y: auto; flex-shrink: 0; } |
|
|
|
.panel-title { font-weight: bold; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #ebeef5; } |
|
|
|
.node-item { display: flex; align-items: center; gap: 8px; padding: 8px; margin-bottom: 8px; border: 1px solid #e4e7ed; border-radius: 4px; cursor: grab; font-size: 13px; background: #f5f7fa; transition: all 0.2s; user-select: none; } |
|
|
|
.node-item:hover { border-color: #409EFF; background: #ecf5ff; } |
|
|
|
.panel-hint { font-size: 12px; color: #909399; line-height: 1.8; } |
|
|
|
.panel-hint p::before { content: '• '; color: #409EFF; } |
|
|
|
.canvas-wrapper { flex: 1; background: #fff; border-radius: 4px; border: 1px solid #ebeef5; display: flex; flex-direction: column; min-width: 0; } |
|
|
|
.canvas-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; border-bottom: 1px solid #ebeef5; font-weight: bold; flex-shrink: 0; } |
|
|
|
.config-panel { width: 280px; background: #fff; border-radius: 4px; padding: 12px; border: 1px solid #ebeef5; overflow-y: auto; flex-shrink: 0; } |
|
|
|
.config-actions { margin-top: 8px; padding-top: 8px; border-top: 1px solid #ebeef5; } |
|
|
|
</style> |