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.3 镜像打包
1. docker build -f installer/Dockerfile -t maxkb:latest 1. docker build -f installer/Dockerfile -t maxkb:latest .
## 2. 数据库外挂配置 ## 2. 数据库外挂配置

View File

@ -3,4 +3,4 @@
prefix: '/admin', prefix: '/admin',
chatPrefix: '/chat', 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/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@logicflow/core": "^1.2.27", "@logicflow/core": "^1.2.27",
"@logicflow/extension": "^2.1.15", "@logicflow/extension": "^1.2.27",
"@vavt/cm-extension": "^1.9.1", "@vavt/cm-extension": "^1.9.1",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.3.0",
"axios": "^1.8.4", "axios": "^1.8.4",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,13 @@ class LoopBodyNodeView extends AppNode {
} }
get_up_node_field_list(contain_self: boolean, use_cache: boolean) { 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_id = this.props.model.properties.loop_node_id
const loop_node = this.props.graphModel.getNodeModelById(loop_node_id) if (typeof loop_node_id === 'string') {
return loop_node.get_up_node_field_list(contain_self, use_cache) 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 { class LoopBodyModel extends AppNodeModel {

View File

@ -121,13 +121,6 @@ const renderGraphData = (data?: any) => {
) )
initDefaultShortcut(lf.value, lf.value.graphModel) 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.refresh_loop_fields = refresh_loop_fields
lf.value.graphModel.get_parent_nodes = () => { lf.value.graphModel.get_parent_nodes = () => {
return props.nodeModel.graphModel.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 showNode = this.properties.showNode === undefined ? true : this.properties.showNode
const anchors: any = [] const anchors: any = []
if (this.type !== WorkflowType.Base) { if (this.type && this.type !== WorkflowType.Base.toString()) {
if (this.type !== WorkflowType.Start) { if (this.type !== WorkflowType.Start.toString()) {
anchors.push({ anchors.push({
x: x - width / 2 + 10, x: x - width / 2 + 10,
y: showNode ? y : y - 15, y: showNode ? y : y - 15,

View File

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