fix(workflow): 修复工作流节点类型安全和事件处理问题

- 添加对模型类型的安全检查,防止空值导致的错误
- 修复条件节点和循环节点中的数组访问安全性问题
- 统一工作流模式注入和传递机制
- 优化节点拖拽事件处理,确保事件对象正确传递
- 修复循环体节点字段列表获取的安全性检查
- 调整图形渲染逻辑,避免空画布时的视图适配问题
- 更新依赖版本以解决兼容性问题
v3.2
tanlianwang 2026-03-11 09:54:03 +08:00
parent 8c6ca1206f
commit eb6f7a3c0a
16 changed files with 73 additions and 69 deletions

View File

@ -27,7 +27,7 @@
### 1.3 镜像打包
1. docker build -f installer/Dockerfile -t maxkb:latest
1. docker build -f installer/Dockerfile -t maxkb:latest .
## 2. 数据库外挂配置

View File

@ -3,4 +3,4 @@
prefix: '/admin',
chatPrefix: '/chat',
}
})()</script><script type="module" crossorigin src="./assets/admin-CkbVdLTL.js"></script><link rel="stylesheet" crossorigin href="./assets/admin-DNnRQ7pR.css"></head><body><div id="app"></div></body></html>
})()</script><script type="module" crossorigin src="./assets/admin-4Nw6PvBR.js"></script><link rel="stylesheet" crossorigin href="./assets/admin-Bbyck9zg.css"></head><body><div id="app"></div></body></html>

View File

@ -21,7 +21,7 @@
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@logicflow/core": "^1.2.27",
"@logicflow/extension": "^2.1.15",
"@logicflow/extension": "^1.2.27",
"@vavt/cm-extension": "^1.9.1",
"@vueuse/core": "^13.3.0",
"axios": "^1.8.4",

View File

@ -38,7 +38,7 @@
class="list-item flex align-center border border-r-6 p-8-12 cursor"
style="width: calc(50% - 6px)"
@click.stop="clickNodes(item)"
@mousedown.stop="onmousedown(item)"
@mousedown="onmousedown(item, undefined, undefined, $event)"
>
<component
:is="iconComponent(`${item.type}-icon`)"
@ -90,7 +90,7 @@
<NodeContent
:list="toolList"
@clickNodes="(val: any) => clickNodes(toolLibNode, val, 'tool')"
@onmousedown="(val: any) => onmousedown(toolLibNode, val, 'tool')"
@onmousedown="(val: any, event: MouseEvent) => onmousedown(toolLibNode, val, 'tool', event)"
/>
</el-scrollbar>
</LayoutContainer>
@ -112,7 +112,7 @@
<NodeContent
:list="applicationList"
@clickNodes="(val: any) => clickNodes(applicationNode, val, 'application')"
@onmousedown="(val: any) => onmousedown(applicationNode, val, 'application')"
@onmousedown="(val: any, event: MouseEvent) => onmousedown(applicationNode, val, 'application', event)"
/>
</el-scrollbar>
</LayoutContainer>
@ -208,7 +208,7 @@ function clickNodes(item: any, data?: any, type?: string) {
emit('clickNodes', item)
}
function onmousedown(item: any, data?: any, type?: string) {
function onmousedown(item: any, data?: any, type?: string, event?: MouseEvent) {
if (data) {
item['properties']['stepName'] = data.name
if (type == 'tool') {
@ -239,7 +239,7 @@ function onmousedown(item: any, data?: any, type?: string) {
}
}
}
props.workflowRef?.onmousedown(item)
props.workflowRef?.onmousedown(item, event)
emit('onmousedown', item)
}

View File

@ -20,7 +20,7 @@
class="list-item flex align-center border border-r-6 p-8-12 cursor"
style="width: calc(50% - 6px)"
@click.stop="emit('clickNodes', item)"
@mousedown.stop="emit('onmousedown', item)"
@mousedown="emit('onmousedown', item, $event)"
>
<el-avatar
v-if="isAppIcon(item?.icon)"
@ -81,7 +81,7 @@ const props = defineProps<{
const emit = defineEmits<{
(e: 'clickNodes', item: any): void
(e: 'onmousedown', item: any): void
(e: 'onmousedown', item: any, event: MouseEvent): void
}>()
const filterText = ref('')

View File

@ -159,7 +159,9 @@ import { ComplexPermission } from '@/utils/permission/type'
import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/data'
import permissionMap from '@/permission'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { WorkflowMode } from '@/enums/application'
provide('getApplicationDetail', () => detail)
provide('workflowMode', WorkflowMode.Application)
const { theme } = useStore()
const router = useRouter()
const route = useRoute()

View File

@ -1,5 +1,5 @@
<template>
<div @mousedown="mousedown" class="workflow-node-container p-16" style="overflow: visible">
<div class="workflow-node-container p-16" style="overflow: visible">
<div
class="step-container app-card p-16"
:class="{ isSelected: props.nodeModel.isSelected, error: node_status !== 200 }"
@ -9,10 +9,6 @@
<div class="flex-between">
<div
class="flex align-center"
@dragstart.prevent
@drag.prevent
@dragover.prevent
@dragend.prevent
style="width: 69%"
>
<component
@ -24,7 +20,7 @@
<h4 class="ellipsis-1 break-all">{{ nodeModel.properties.stepName }}</h4>
</div>
<div @mousemove.stop @mousedown.stop @keydown.stop @click.stop>
<div>
<el-button text @click="showNode = !showNode">
<el-icon class="arrow-icon color-secondary" :class="showNode ? 'rotate-180' : ''"
><ArrowDownBold />
@ -78,7 +74,7 @@
</div>
</div>
<el-collapse-transition>
<div @mousedown.stop @keydown.stop @click.stop v-show="showNode" class="mt-16">
<div v-show="showNode" class="mt-16">
<el-alert
v-if="node_status != 200"
class="mb-16"
@ -124,9 +120,6 @@
<el-collapse-transition>
<DropdownMenu
v-if="showAnchor"
@mousemove.stop
@mousedown.stop
@click.stop
@wheel="handleWheel"
:show="showAnchor"
:id="id"
@ -172,16 +165,18 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, provide } from 'vue'
import DropdownMenu from '@/views/application-workflow/component/DropdownMenu.vue'
import { set } from 'lodash'
import { iconComponent } from '../icons/utils'
import { copyClick } from '@/utils/clipboard'
import { WorkflowType } from '@/enums/application'
import { WorkflowType, WorkflowMode } from '@/enums/application'
import { MsgError, MsgConfirm } from '@/utils/message'
import type { FormInstance } from 'element-plus'
import { t } from '@/locales'
import { useRoute } from 'vue-router'
provide('workflowMode', WorkflowMode.Application)
const route = useRoute()
const {
params: { id },

View File

@ -62,26 +62,26 @@ class AppNode extends HtmlResize.view {
}
get_node_field_list() {
const result = []
if (this.props.model.type === 'start-node') {
if (this.props.model.type && this.props.model.type === 'start-node') {
result.push({
value: 'global',
label: t('views.applicationWorkflow.variable.global'),
type: 'global',
children: this.props.model.properties?.config?.globalFields || [],
children: (this.props.model.properties as any)?.config?.globalFields || [],
})
result.push({
value: 'chat',
label: t('views.applicationWorkflow.variable.chat'),
type: 'chat',
children: this.props.model.properties?.config?.chatFields || [],
children: (this.props.model.properties as any)?.config?.chatFields || [],
})
}
result.push({
value: this.props.model.id,
icon: this.props.model.properties.node_data?.icon,
icon: (this.props.model.properties as any)?.node_data?.icon,
label: this.props.model.properties.stepName,
type: this.props.model.type,
children: this.props.model.properties?.config?.fields || [],
children: (this.props.model.properties as any)?.config?.fields || [],
})
return result
}
@ -107,10 +107,8 @@ class AppNode extends HtmlResize.view {
(pre, next) => [...pre, ...next],
[],
)
const start_node_field_list = (
this.props.graphModel.getNodeModelById('start-node') ||
this.props.graphModel.getNodeModelById('loop-start-node')
).get_node_field_list()
const startNode = this.props.graphModel.getNodeModelById('start-node') || this.props.graphModel.getNodeModelById('loop-start-node')
const start_node_field_list = startNode ? startNode.get_node_field_list() : []
return [...start_node_field_list, ...result]
}
@ -228,8 +226,8 @@ class AppNode extends HtmlResize.view {
this.targetId(),
this.component,
root,
model,
graphModel,
model as any,
graphModel as any,
undefined,
this.props.graphModel.get_provide,
)
@ -334,8 +332,10 @@ class AppNodeModel extends HtmlResize.model {
const style = super.getAnchorStyle(anchorInfo)
if (anchorInfo.type === 'left') {
style.fill = 'red'
style.hover.fill = 'transparent'
style.hover.stroke = 'transpanrent'
if (style.hover) {
style.hover.fill = 'transparent'
style.hover.stroke = 'transpanrent'
}
style.className = 'lf-hide-default'
} else {
style.fill = 'green'
@ -414,8 +414,8 @@ class AppNodeModel extends HtmlResize.model {
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
const anchors: any = []
if (this.type !== WorkflowType.Base) {
if (![WorkflowType.Start, WorkflowType.LoopStartNode.toString()].includes(this.type)) {
if (this.type && this.type !== WorkflowType.Base.toString()) {
if (![WorkflowType.Start.toString(), WorkflowType.LoopStartNode.toString()].includes(this.type)) {
anchors.push({
x: x - width / 2 + 10,
y: showNode ? y : y - 15,

View File

@ -6,7 +6,7 @@
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted, inject } from 'vue'
import AppEdge from './common/edge'
import loopEdge from './common/loopEdge'
import Control from './common/NodeControl.vue'
@ -18,6 +18,8 @@ import Dagre from '@/workflow/plugins/dagre'
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
import { WorkflowMode } from '@/enums/application'
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
const workflow_mode = inject('workflowMode') || WorkflowMode.Application
const loop_workflow_mode = inject('loopWorkflowMode') || WorkflowMode.ApplicationLoop
defineOptions({ name: 'WorkFlow' })
const TeleportContainer = getTeleport()
@ -71,6 +73,8 @@ const renderGraphData = (data?: any) => {
},
isSilentMode: false,
container: container,
stopMoveGraph: false,
stopZoomGraph: false,
})
lf.value.setTheme({
bezier: {
@ -78,7 +82,6 @@ const renderGraphData = (data?: any) => {
strokeWidth: 1,
},
})
lf.value.graphModel.get = 'sdasdaad'
lf.value.on('graph:rendered', () => {
flowId.value = lf.value.graphModel.flowId
})
@ -96,7 +99,8 @@ const renderGraphData = (data?: any) => {
return {
getNode: () => node,
getGraph: () => graph,
workflowMode: WorkflowMode.Application,
workflowMode: workflow_mode,
loopWorkflowMode: loop_workflow_mode,
}
}
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
@ -108,10 +112,12 @@ const renderGraphData = (data?: any) => {
//
data.nodeModel.clear_next_node_field(false)
})
// lf.value.openSelectionSelect()
// lf.value.extension.selectionSelect.setSelectionSense(true, false)
setTimeout(() => {
lf.value?.fitView()
if (lf.value.graphModel?.nodes.length > 1) {
lf.value?.fitView()
} else {
lf.value?.translateCenter()
}
}, 500)
}
}
@ -133,12 +139,12 @@ const getGraphData = () => {
return _graph_data
}
const onmousedown = (shapeItem: ShapeItem) => {
const onmousedown = (shapeItem: ShapeItem, event?: MouseEvent) => {
if (shapeItem.type) {
lf.value.dnd.startDrag({
type: shapeItem.type,
properties: { ...shapeItem.properties },
})
}, event)
}
if (shapeItem.callback) {

View File

@ -45,13 +45,14 @@ class ConditionModel extends AppNodeModel {
type: 'left'
})
if (branch_condition_list) {
for (let index = 0; index < branch_condition_list.length; index++) {
const element = branch_condition_list[index]
const h = get_up_index_height(branch_condition_list, index)
const conditionList = Array.isArray(branch_condition_list) ? branch_condition_list : []
if (conditionList.length > 0) {
for (let index = 0; index < conditionList.length; index++) {
const element = conditionList[index]
const h = get_up_index_height(conditionList, index)
anchors.push({
x: x + width / 2 - 10,
y: showNode ? y - height / 2 + 75 + h + element.height / 2 : y - 15,
y: showNode ? y - height / 2 + 75 + h + (element.height || 0) / 2 : y - 15,
id: `${id}_${element.id}_right`,
type: 'right'
})

View File

@ -46,12 +46,13 @@ class IntentModel extends AppNodeModel {
type: 'left'
})
if (branch_condition_list) {
const conditionList = Array.isArray(branch_condition_list) ? branch_condition_list : []
if (conditionList.length > 0) {
const FORM_ITEMS_HEIGHT = 397 // 上方表单占用高度
for (let index = 0; index < branch_condition_list.length; index++) {
const element = branch_condition_list[index]
for (let index = 0; index < conditionList.length; index++) {
const element = conditionList[index]
anchors.push({
x: x + width / 2 - 10,

View File

@ -1,5 +1,5 @@
<template>
<div @mousedown="mousedown" class="workflow-node-container p-16" style="overflow: visible">
<div class="workflow-node-container p-16" style="overflow: visible">
<div
class="step-container app-card p-16"
:class="{ isSelected: props.nodeModel.isSelected, error: node_status !== 200 }"
@ -27,7 +27,7 @@
</el-button>
</div>
<el-collapse-transition>
<div @mousedown.stop @keydown.stop @click.stop v-show="showNode" class="mt-16">
<div v-show="showNode" class="mt-16">
<el-alert
v-if="node_status != 200"
class="mb-16"

View File

@ -10,8 +10,13 @@ class LoopBodyNodeView extends AppNode {
}
get_up_node_field_list(contain_self: boolean, use_cache: boolean) {
const loop_node_id = this.props.model.properties.loop_node_id
const loop_node = this.props.graphModel.getNodeModelById(loop_node_id)
return loop_node.get_up_node_field_list(contain_self, use_cache)
if (typeof loop_node_id === 'string') {
const loop_node = this.props.graphModel.getNodeModelById(loop_node_id)
if (loop_node && typeof loop_node.get_up_node_field_list === 'function') {
return loop_node.get_up_node_field_list(contain_self, use_cache)
}
}
return []
}
}
class LoopBodyModel extends AppNodeModel {

View File

@ -121,13 +121,6 @@ const renderGraphData = (data?: any) => {
)
initDefaultShortcut(lf.value, lf.value.graphModel)
lf.value.graphModel.get_provide = (node: any, graph: any) => {
return {
getNode: () => node,
getGraph: () => graph,
workflowMode: WorkflowMode.ApplicationLoop,
}
}
lf.value.graphModel.refresh_loop_fields = refresh_loop_fields
lf.value.graphModel.get_parent_nodes = () => {
return props.nodeModel.graphModel.nodes

View File

@ -25,8 +25,8 @@ class LoopModel extends AppNodeModel {
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
const anchors: any = []
if (this.type !== WorkflowType.Base) {
if (this.type !== WorkflowType.Start) {
if (this.type && this.type !== WorkflowType.Base.toString()) {
if (this.type !== WorkflowType.Start.toString()) {
anchors.push({
x: x - width / 2 + 10,
y: showNode ? y : y - 15,

View File

@ -7,13 +7,13 @@ class LoopStartNode extends AppNode {
}
get_node_field_list() {
const result = []
if (this.props.model.type === 'loop-start-node') {
if (this.props.model.type && this.props.model.type === 'loop-start-node') {
result.push({
value: 'loop',
label: t('views.applicationWorkflow.variable.loop'),
type: 'loop',
children:
(this.props.model.properties.loop_input_field_list
(Array.isArray(this.props.model.properties.loop_input_field_list)
? this.props.model.properties.loop_input_field_list
: []
).map((i: any) => {
@ -27,9 +27,10 @@ class LoopStartNode extends AppNode {
result.push({
value: this.props.model.id,
icon: (this.props.model.properties as any)?.node_data?.icon,
label: this.props.model.properties.stepName,
type: this.props.model.type,
children: this.props.model.properties?.config?.fields || [],
children: (this.props.model.properties as any)?.config?.fields || [],
})
return result