Browse Source

画布页面加载成功

master
MSI-7950X\刘泽明 6 days ago
parent
commit
dd80461ac6
  1. 12
      backend/modules/rag/knowledge.py
  2. 3
      frontend/src/api/index.ts
  3. 106
      frontend/src/views/flow/FlowCanvas.vue
  4. 435
      frontend/src/views/flow/FlowEditor.vue

12
backend/modules/rag/knowledge.py

@ -1,4 +1,5 @@
import os import os
import asyncio
import logging import logging
from agentscope.embedding import OpenAITextEmbedding from agentscope.embedding import OpenAITextEmbedding
from agentscope.rag import SimpleKnowledge, QdrantStore, TextReader, PDFReader, WordReader, ExcelReader from agentscope.rag import SimpleKnowledge, QdrantStore, TextReader, PDFReader, WordReader, ExcelReader
@ -98,7 +99,13 @@ async def add_text(text: str, source: str = "manual") -> str:
async def search(query: str, limit: int = 5, score_threshold: float = 0.3) -> list[dict]: async def search(query: str, limit: int = 5, score_threshold: float = 0.3) -> list[dict]:
try: try:
kb = get_knowledge_base() kb = get_knowledge_base()
docs = await kb.retrieve(query=query, limit=limit, score_threshold=score_threshold) if not kb or not hasattr(kb, 'retrieve'):
logger.warning("知识库未初始化或不可用")
return []
docs = await asyncio.wait_for(
kb.retrieve(query=query, limit=limit, score_threshold=score_threshold),
timeout=10.0
)
return [ return [
{ {
"id": doc.id, "id": doc.id,
@ -108,6 +115,9 @@ async def search(query: str, limit: int = 5, score_threshold: float = 0.3) -> li
} }
for doc in docs for doc in docs
] ]
except asyncio.TimeoutError:
logger.warning(f"知识检索超时 (query={query[:50]})")
return []
except Exception as e: except Exception as e:
logger.error(f"知识检索失败: {e}") logger.error(f"知识检索失败: {e}")
return [] return []

3
frontend/src/api/index.ts

@ -20,6 +20,9 @@ api.interceptors.response.use(
(error) => { (error) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem('token') localStorage.removeItem('token')
if (router.currentRoute.value.path !== '/login') {
router.push('/login')
}
return Promise.reject(error) return Promise.reject(error)
} }
const msg = error.response?.data?.message || error.message || '请求失败' const msg = error.response?.data?.message || error.message || '请求失败'

106
frontend/src/views/flow/FlowCanvas.vue

@ -0,0 +1,106 @@
<template>
<VueFlow
:nodes="localNodes"
:edges="localEdges"
@nodes-change="onNodesChange"
@edges-change="onEdgesChange"
@connect="handleConnect"
@node-click="$emit('node-click', $event)"
@pane-click="$emit('pane-click')"
@drop="$emit('drop', $event)"
@dragover.prevent
>
<Background :gap="20" :size="1" />
<Controls position="bottom-right" />
<MiniMap
position="bottom-left"
:node-stroke-color="'#409EFF'"
:node-color="nodeColor"
pannable
zoomable
/>
<template #node-custom="nodeProps">
<FlowNode
:id="nodeProps.id"
:data="nodeProps.data"
:selected="nodeProps.selected"
@delete="$emit('node-delete', nodeProps.id)"
/>
</template>
</VueFlow>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { MarkerType } from '@vue-flow/core'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import FlowNode from './FlowNode.vue'
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'
const props = defineProps<{
nodes: any[]
edges: any[]
nodeColor: (node: any) => string
}>()
const emit = defineEmits([
'connect', 'node-click', 'pane-click', 'drop', 'node-delete',
'update:nodes', 'update:edges',
])
const localNodes = ref<any[]>([])
const localEdges = ref<any[]>([])
watch(() => props.nodes, (val) => { localNodes.value = val || [] }, { immediate: true })
watch(() => props.edges, (val) => { localEdges.value = val || [] }, { immediate: true })
function onNodesChange() {
emit('update:nodes', [...localNodes.value])
}
function onEdgesChange() {
emit('update:edges', [...localEdges.value])
}
const store = useVueFlow()
function handleConnect(connection: any) {
const sourceHandle = connection.sourceHandle || 'source'
const edge: any = {
id: `edge_${connection.source}_${connection.target}_${sourceHandle}`,
source: connection.source,
target: connection.target,
sourceHandle,
targetHandle: connection.targetHandle,
type: 'smoothstep',
animated: true,
markerEnd: MarkerType.ArrowClosed,
style: sourceHandle === 'false' ? { stroke: '#F56C6C', strokeWidth: 2 } : { stroke: '#409EFF', strokeWidth: 2 },
}
localEdges.value.push(edge)
emit('update:edges', [...localEdges.value])
emit('connect', connection)
}
defineExpose({
undo: () => (store as any).undo?.(),
redo: () => (store as any).redo?.(),
get canUndo() { return !!(store as any).canUndo },
get canRedo() { return !!(store as any).canRedo },
addNodes: (n: any[]) => { localNodes.value = [...localNodes.value, ...n]; emit('update:nodes', [...localNodes.value]) },
addEdges: (e: any[]) => { localEdges.value = [...localEdges.value, ...e]; emit('update:edges', [...localEdges.value]) },
removeNodes: (ids: string[]) => {
localNodes.value = localNodes.value.filter((n: any) => !ids.includes(n.id))
localEdges.value = localEdges.value.filter((e: any) => !ids.includes(e.source) && !ids.includes(e.target))
emit('update:nodes', [...localNodes.value])
emit('update:edges', [...localEdges.value])
},
})
</script>

435
frontend/src/views/flow/FlowEditor.vue

@ -2,6 +2,8 @@
<div class="flow-editor-page"> <div class="flow-editor-page">
<el-page-header @back="$router.back()" :content="isEdit ? '编辑: ' + flowName : '创建新流'" /> <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"> <el-card style="margin-top: 20px">
<div class="editor-toolbar"> <div class="editor-toolbar">
<el-input v-model="flowName" placeholder="流名称" style="width: 200px" /> <el-input v-model="flowName" placeholder="流名称" style="width: 200px" />
@ -14,7 +16,7 @@
</div> </div>
</el-card> </el-card>
<div class="editor-body"> <div v-if="!initError" class="editor-body">
<div class="node-panel"> <div class="node-panel">
<div class="panel-title">节点面板</div> <div class="panel-title">节点面板</div>
<div <div
@ -43,51 +45,16 @@
<span>设计画布</span> <span>设计画布</span>
<el-button size="small" @click="clearCanvas">清空</el-button> <el-button size="small" @click="clearCanvas">清空</el-button>
</div> </div>
<VueFlow <FlowCanvas
ref="vueFlowRef" ref="canvasRef"
v-model:nodes="nodes" v-model:nodes="nodes"
v-model:edges="edges" v-model:edges="edges"
:default-viewport="{ zoom: 1, x: 0, y: 0 }" :node-color="getMiniMapColor"
: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-click="onNodeClick" @node-click="onNodeClick"
@pane-click="onPaneClick" @pane-click="onPaneClick"
@drop="onDrop" @drop="onDrop"
@dragover.prevent @node-delete="removeNode"
> />
<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>
</div> </div>
<div class="config-panel" v-if="selectedNodeId"> <div class="config-panel" v-if="selectedNodeId">
@ -209,26 +176,16 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core' import { Promotion, ChatDotRound, Tools, Connection, Bell, DataAnalysis, Search } from '@element-plus/icons-vue'
import { Background } from '@vue-flow/background' import FlowCanvas from './FlowCanvas.vue'
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'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const flowId = computed(() => route.params.id as string) const flowId = computed(() => route.params.id as string)
const isEdit = computed(() => !!flowId.value) const isEdit = computed(() => !!flowId.value)
const initError = ref('')
const flowName = ref('新工作流') const flowName = ref('新工作流')
const flowDesc = ref('') const flowDesc = ref('')
const flowStatus = ref('') const flowStatus = ref('')
@ -238,6 +195,12 @@ const selectedNodeData = ref<any>({})
const mcpServers = ref<any[]>([]) const mcpServers = ref<any[]>([])
const agentList = ref<any[]>([]) const agentList = ref<any[]>([])
const nodes = ref<any[]>([])
const edges = ref<any[]>([])
const canvasRef = ref<any>(null)
let nodeCounter = 0
const nodeTypes = [ const nodeTypes = [
{ type: 'trigger', label: '触发节点', icon: Promotion, typeDesc: '企微触发' }, { type: 'trigger', label: '触发节点', icon: Promotion, typeDesc: '企微触发' },
{ type: 'llm', label: 'LLM处理', icon: ChatDotRound, typeDesc: 'AI处理' }, { type: 'llm', label: 'LLM处理', icon: ChatDotRound, typeDesc: 'AI处理' },
@ -260,24 +223,18 @@ const colorMap: Record<string, string> = {
output: '#722ed1', output: '#722ed1',
} }
const vueFlowStore = useVueFlow() function undo() { canvasRef.value?.undo() }
const { nodes, edges, addNodes, addEdges, removeNodes, removeEdges, toObject } = vueFlowStore function redo() { canvasRef.value?.redo() }
const storeAny = vueFlowStore as any const canUndo = computed(() => canvasRef.value?.canUndo ?? false)
const undo = () => storeAny.undo?.() const canRedo = computed(() => canvasRef.value?.canRedo ?? false)
const redo = () => storeAny.redo?.()
const canUndo = computed(() => !!(storeAny.canUndo ?? false))
const canRedo = computed(() => !!(storeAny.canRedo ?? false))
let nodeCounter = 0
const selectedNode = computed(() => { const selectedNode = computed(() => nodes.value.find((n: any) => n.id === selectedNodeId.value) || null)
return nodes.value.find((n: any) => n.id === selectedNodeId.value) || null
})
function getMiniMapColor(node: any) { function getMiniMapColor(node: any) {
return colorMap[node?.data?.type || node?.type] || '#409EFF' 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) { if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', JSON.stringify(node)) event.dataTransfer.setData('application/vueflow', JSON.stringify(node))
event.dataTransfer.effectAllowed = 'move' event.dataTransfer.effectAllowed = 'move'
@ -287,74 +244,31 @@ function onDragStart(event: DragEvent, node: (typeof nodeTypes)[0]) {
function onDrop(event: DragEvent) { function onDrop(event: DragEvent) {
const dataStr = event.dataTransfer?.getData('application/vueflow') const dataStr = event.dataTransfer?.getData('application/vueflow')
if (!dataStr) return if (!dataStr) return
const nodeData = JSON.parse(dataStr) 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 id = `node_${++nodeCounter}`
const newNode = { canvasRef.value?.addNodes?.([{
id, id,
type: 'custom', type: 'custom',
position, position: { x: 100 + nodeCounter * 50, y: 100 + nodeCounter * 30 },
data: { data: {
label: nodeData.label, label: nodeData.label,
type: nodeData.type, type: nodeData.type,
typeDesc: nodeData.typeDesc, typeDesc: nodeData.typeDesc,
color: colorMap[nodeData.type] || '#409EFF', color: colorMap[nodeData.type] || '#409EFF',
config: nodeData.type === 'llm' config: nodeData.type === 'llm' ? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7, agent_id: '' } : {},
? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7, agent_id: '' }
: {},
}, },
draggable: true, draggable: true,
connectable: 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) { function onNodeClick({ node }: any) {
selectedNodeId.value = node.id selectedNodeId.value = node.id
const d = node.data || {} const d = node.data || {}
selectedNodeData.value = { selectedNodeData.value = { label: d.label || '', typeDesc: d.typeDesc || '', ...(d.config || {}) }
label: d.label || '',
typeDesc: d.typeDesc || '',
...(d.config || {}),
}
} }
function onPaneClick() { function onPaneClick() { selectedNodeId.value = '' }
selectedNodeId.value = ''
}
function onConfigLabelChange() { function onConfigLabelChange() {
const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value) const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value)
@ -379,288 +293,121 @@ function onAgentSelect(val: string) {
function onConfigChange() { function onConfigChange() {
const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value) const idx = nodes.value.findIndex((n: any) => n.id === selectedNodeId.value)
if (idx === -1) return if (idx === -1) return
const found = nodes.value[idx] const found = nodes.value[idx]
const nodeType = found.data.type const nodeType = found.data.type
const cfg: any = {} const cfg: any = {}
if (nodeType === 'trigger') cfg.event_type = selectedNodeData.value.event_type
if (nodeType === 'trigger') { 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 }
cfg.event_type = selectedNodeData.value.event_type else if (nodeType === 'tool') cfg.tool_name = selectedNodeData.value.tool_name
} else if (nodeType === 'llm') { else if (nodeType === 'mcp') { cfg.mcp_server = selectedNodeData.value.mcp_server; cfg.tool_name = selectedNodeData.value.tool_name }
cfg.system_prompt = selectedNodeData.value.system_prompt else if (nodeType === 'wecom_notify') { cfg.message_template = selectedNodeData.value.message_template; cfg.target = selectedNodeData.value.target }
cfg.model = selectedNodeData.value.model else if (nodeType === 'condition') cfg.condition = selectedNodeData.value.condition
cfg.temperature = selectedNodeData.value.temperature else if (nodeType === 'rag') { cfg.knowledge_base = selectedNodeData.value.knowledge_base; cfg.top_k = selectedNodeData.value.top_k }
cfg.agent_id = selectedNodeData.value.agent_id else if (nodeType === 'output') cfg.format = selectedNodeData.value.format
} 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 } const updated = { ...found }
updated.data = { ...found.data, config: cfg } updated.data = { ...found.data, config: cfg }
nodes.value[idx] = updated nodes.value[idx] = updated
} }
function removeNode(id: string) { function removeNode(id: string) {
removeNodes([id]) canvasRef.value?.removeNodes?.([id])
edges.value = edges.value.filter((e: any) => e.source !== id && e.target !== id)
if (selectedNodeId.value === id) selectedNodeId.value = '' if (selectedNodeId.value === id) selectedNodeId.value = ''
} }
function clearCanvas() { function clearCanvas() { nodes.value = []; edges.value = []; nodeCounter = 0; selectedNodeId.value = '' }
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()
}
async function loadFlow() { async function loadFlow() {
if (!isEdit.value) return if (!isEdit.value) return
try { try {
const { flowApi } = await import('@/api')
const res: any = await flowApi.getFlow(flowId.value) const res: any = await flowApi.getFlow(flowId.value)
const flow = res?.data || res || {} const flow = res?.data || res || {}
flowName.value = flow.name || '' flowName.value = flow.name || ''
flowDesc.value = flow.description || '' flowDesc.value = flow.description || ''
flowStatus.value = flow.status || '' flowStatus.value = flow.status || ''
const definition = flow.definition_json || {} const definition = flow.definition_json || {}
const loadedNodes: any[] = [] const loadedNodes: any[] = []
const loadedEdges: any[] = [] const loadedEdges: any[] = []
const defNodes = definition.nodes || [] const defNodes = definition.nodes || []
for (let i = 0; i < defNodes.length; i++) { for (let i = 0; i < defNodes.length; i++) {
const n = defNodes[i] const n = defNodes[i]
const nt = nodeTypes.find(t => t.type === n.type) const nt = nodeTypes.find(t => t.type === n.type)
loadedNodes.push({ loadedNodes.push({
id: n.id || `node_${i}`, id: n.id || `node_${i}`, type: 'custom',
type: 'custom',
position: n.position || { x: 100 + (i % 4) * 250, y: 60 + Math.floor(i / 4) * 150 }, position: n.position || { x: 100 + (i % 4) * 250, y: 60 + Math.floor(i / 4) * 150 },
data: { data: { label: n.label || n.id || `节点${i}`, type: n.type, typeDesc: nt?.typeDesc || n.type, color: colorMap[n.type] || '#409EFF', config: n.config || {} },
label: n.label || n.id || `节点${i}`, draggable: true, connectable: true,
type: n.type,
typeDesc: nt?.typeDesc || n.type,
color: colorMap[n.type] || '#409EFF',
config: n.config || {},
},
draggable: true,
connectable: true,
}) })
} }
const defEdges = definition.edges || [] const defEdges = definition.edges || []
for (const e of defEdges) { for (const e of defEdges) {
const source = e.source || e.from const source = e.source || e.from
const target = e.target || e.to const target = e.target || e.to
const cond = e.condition || e.sourceHandle 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, style: cond === 'false' ? { 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,
markerEnd: MarkerType.ArrowClosed,
style: isFalse
? { stroke: '#F56C6C', strokeWidth: 2 }
: { stroke: '#409EFF', strokeWidth: 2 },
})
} }
nodes.value = loadedNodes nodes.value = loadedNodes
edges.value = loadedEdges edges.value = loadedEdges
nodeCounter = defNodes.length nodeCounter = defNodes.length
} catch { /**/ } } catch (e: any) {
console.error('loadFlow error:', e)
}
} }
async function loadMcpServers() { async function loadMcpServers() {
try { try { const { mcpApi } = await import('@/api'); const res: any = await mcpApi.getServers(); mcpServers.value = Array.isArray(res) ? res : (res?.data || []) } catch {}
const res: any = await mcpApi.getServers()
mcpServers.value = Array.isArray(res) ? res : (res?.data || [])
} catch { /**/ }
} }
async function loadAgents() { 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 { try {
const res: any = await agentApi.getList() const { flowApi } = await import('@/api')
agentList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) 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 }))
} catch { /**/ } 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 () => { onMounted(async () => {
if (isEdit.value) { try {
await loadFlow() 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> </script>
<style scoped> <style scoped>
.flow-editor-page { .flow-editor-page { height: 100%; display: flex; flex-direction: column; }
height: 100%; .editor-toolbar { display: flex; align-items: center; gap: 8px; }
display: flex; .editor-body { display: flex; gap: 12px; margin-top: 12px; flex: 1; min-height: 0; }
flex-direction: column; .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; }
.editor-toolbar { .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; }
display: flex; .node-item:hover { border-color: #409EFF; background: #ecf5ff; }
align-items: center; .panel-hint { font-size: 12px; color: #909399; line-height: 1.8; }
gap: 8px; .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; }
.editor-body { .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; }
display: flex; .config-panel { width: 280px; background: #fff; border-radius: 4px; padding: 12px; border: 1px solid #ebeef5; overflow-y: auto; flex-shrink: 0; }
gap: 12px; .config-actions { margin-top: 8px; padding-top: 8px; border-top: 1px solid #ebeef5; }
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;
}
</style> </style>
Loading…
Cancel
Save