mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: loopNode
This commit is contained in:
parent
e11c550fc2
commit
5837650e35
|
|
@ -15,7 +15,7 @@ from .function_lib_node import *
|
|||
from .function_node import *
|
||||
from .question_node import *
|
||||
from .reranker_node import *
|
||||
|
||||
from .loop_node import *
|
||||
from .document_extract_node import *
|
||||
from .image_understand_step_node import *
|
||||
from .image_generate_step_node import *
|
||||
|
|
@ -31,7 +31,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestio
|
|||
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||
BaseDocumentExtractNode,
|
||||
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
||||
BaseImageGenerateNode, BaseVariableAssignNode]
|
||||
BaseImageGenerateNode, BaseVariableAssignNode, BaseLoopNode]
|
||||
|
||||
|
||||
def get_node(node_type):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2025/3/11 18:24
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_loop_node.py
|
||||
@date:2025/3/11 18:19
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.field_message import ErrMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ILoopNodeSerializer(serializers.Serializer):
|
||||
loop_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("loop_type")))
|
||||
array = serializers.ListField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("array")))
|
||||
number = serializers.IntegerField(required=False, allow_null=True,
|
||||
error_messages=ErrMessage.char(_("number")))
|
||||
loop_body = serializers.DictField(required=True, error_messages=ErrMessage.char("循环体"))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
loop_type = self.data.get('loop_type')
|
||||
if loop_type == 'ARRAY':
|
||||
array = self.data.get('array')
|
||||
if array is None or len(array) == 0:
|
||||
message = _('{field}, this field is required.', field='array')
|
||||
raise AppApiException(500, message)
|
||||
elif loop_type == 'NUMBER':
|
||||
number = self.data.get('number')
|
||||
if number is None:
|
||||
message = _('{field}, this field is required.', field='number')
|
||||
raise AppApiException(500, message)
|
||||
|
||||
|
||||
class ILoopNode(INode):
|
||||
type = 'loop-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return ILoopNodeSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, loop_type, array, number, loop_body, stream, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/3/11 18:24
|
||||
@desc:
|
||||
"""
|
||||
from .base_loop_node import BaseLoopNode
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_loop_node.py
|
||||
@date:2025/3/11 18:24
|
||||
@desc:
|
||||
"""
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from application.flow.i_step_node import NodeResult, WorkFlowPostHandler, INode
|
||||
from application.flow.step_node.loop_node.i_loop_node import ILoopNode
|
||||
from application.flow.tools import Reasoning
|
||||
from common.handle.impl.response.loop_to_response import LoopToResponse
|
||||
|
||||
|
||||
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
|
||||
reasoning_content: str):
|
||||
node.context['answer'] = answer
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
node.context['reasoning_content'] = reasoning_content
|
||||
if workflow.is_result(node, NodeResult(node_variable, workflow_variable)):
|
||||
node.answer_text = answer
|
||||
|
||||
|
||||
def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据 (流式)
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 全局数据
|
||||
@param node: 节点
|
||||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
answer = ''
|
||||
reasoning_content = ''
|
||||
for chunk in response:
|
||||
content_chunk = chunk.get('content', '')
|
||||
reasoning_content_chunk = chunk.get('reasoning_content', '')
|
||||
reasoning_content += reasoning_content_chunk
|
||||
answer += content_chunk
|
||||
yield {'content': content_chunk,
|
||||
'reasoning_content': reasoning_content_chunk}
|
||||
|
||||
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
|
||||
|
||||
|
||||
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
|
||||
"""
|
||||
写入上下文数据
|
||||
@param node_variable: 节点数据
|
||||
@param workflow_variable: 全局数据
|
||||
@param node: 节点实例对象
|
||||
@param workflow: 工作流管理器
|
||||
"""
|
||||
response = node_variable.get('result')
|
||||
model_setting = node.context.get('model_setting',
|
||||
{'reasoning_content_enable': False, 'reasoning_content_end': '</think>',
|
||||
'reasoning_content_start': '<think>'})
|
||||
reasoning = Reasoning(model_setting.get('reasoning_content_start'), model_setting.get('reasoning_content_end'))
|
||||
reasoning_result = reasoning.get_reasoning_content(response)
|
||||
reasoning_result_end = reasoning.get_end_reasoning_content()
|
||||
content = reasoning_result.get('content') + reasoning_result_end.get('content')
|
||||
if 'reasoning_content' in response.response_metadata:
|
||||
reasoning_content = response.response_metadata.get('reasoning_content', '')
|
||||
else:
|
||||
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get('reasoning_content')
|
||||
_write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content)
|
||||
|
||||
|
||||
def loop_number(number, loop_body):
|
||||
"""
|
||||
指定次数循环
|
||||
@return:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def loop_array(array, loop_body):
|
||||
"""
|
||||
循环数组
|
||||
@return:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def loop_loop(loop_body):
|
||||
"""
|
||||
无线循环
|
||||
@return:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class LoopWorkFlowPostHandler(WorkFlowPostHandler):
|
||||
def handler(self, chat_id,
|
||||
chat_record_id,
|
||||
answer,
|
||||
workflow):
|
||||
pass
|
||||
|
||||
|
||||
class BaseLoopNode(ILoopNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
self.context['result'] = details.get('result')
|
||||
self.answer_text = str(details.get('result'))
|
||||
|
||||
def execute(self, loop_type, array, number, loop_body, stream, **kwargs) -> NodeResult:
|
||||
from application.flow.workflow_manage import WorkflowManage, Flow
|
||||
workflow_manage = WorkflowManage(Flow.new_instance(loop_body), self.workflow_manage.params,
|
||||
LoopWorkFlowPostHandler(self.workflow_manage.work_flow_post_handler.chat_info
|
||||
,
|
||||
self.workflow_manage.work_flow_post_handler.client_id,
|
||||
self.workflow_manage.work_flow_post_handler.client_type)
|
||||
, base_to_response=LoopToResponse())
|
||||
result = workflow_manage.stream()
|
||||
return NodeResult({"result": result}, {}, _write_context=write_context_stream)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": self.context.get('result'),
|
||||
"params": self.context.get('params'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ class BaseSearchDatasetNode(ISearchDatasetStepNode):
|
|||
'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
|
||||
'data': '\n'.join(
|
||||
[f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
|
||||
paragraph_list])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
|
||||
result])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
|
||||
'directly_return': '\n'.join(
|
||||
[paragraph.get('content') for paragraph in
|
||||
result if
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ def get_global_variable(node):
|
|||
class BaseStartStepNode(IStarNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
base_node = self.workflow_manage.get_base_node()
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
default_global_variable = {}
|
||||
if base_node is not None:
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
|
||||
workflow_variable = {**default_global_variable, **get_global_variable(self)}
|
||||
self.context['question'] = details.get('question')
|
||||
self.context['run_time'] = details.get('run_time')
|
||||
|
|
@ -50,7 +53,9 @@ class BaseStartStepNode(IStarNode):
|
|||
|
||||
def execute(self, question, **kwargs) -> NodeResult:
|
||||
base_node = self.workflow_manage.get_base_node()
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
default_global_variable = {}
|
||||
if base_node is not None:
|
||||
default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
|
||||
workflow_variable = {**default_global_variable, **get_global_variable(self)}
|
||||
"""
|
||||
开始节点 初始化全局变量
|
||||
|
|
|
|||
|
|
@ -338,6 +338,12 @@ class WorkflowManage:
|
|||
node.node_chunk.end()
|
||||
self.node_context.append(node)
|
||||
|
||||
def stream(self):
|
||||
close_old_connections()
|
||||
language = get_language()
|
||||
self.run_chain_async(self.start_node, None, language)
|
||||
return self.await_result()
|
||||
|
||||
def run(self):
|
||||
close_old_connections()
|
||||
language = get_language()
|
||||
|
|
@ -801,6 +807,8 @@ class WorkflowManage:
|
|||
@return:
|
||||
"""
|
||||
base_node_list = [node for node in self.flow.nodes if node.type == 'base-node']
|
||||
if len(base_node_list) == 0:
|
||||
return None
|
||||
return base_node_list[0]
|
||||
|
||||
def get_node_cls_by_id(self, node_id, up_node_id_list=None,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: LoopToResponse.py
|
||||
@date:2025/3/12 17:21
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
|
||||
from common.handle.impl.response.system_to_response import SystemToResponse
|
||||
|
||||
|
||||
class LoopToResponse(SystemToResponse):
|
||||
|
||||
def to_stream_chunk_response(self, chat_id, chat_record_id, node_id, up_node_id_list, content, is_end,
|
||||
completion_tokens,
|
||||
prompt_tokens, other_params: dict = None):
|
||||
if other_params is None:
|
||||
other_params = {}
|
||||
return {'chat_id': str(chat_id), 'chat_record_id': str(chat_record_id), 'operate': True,
|
||||
'content': content, 'node_id': node_id, 'up_node_id_list': up_node_id_list,
|
||||
'is_end': is_end,
|
||||
'usage': {'completion_tokens': completion_tokens,
|
||||
'prompt_tokens': prompt_tokens,
|
||||
'total_tokens': completion_tokens + prompt_tokens},
|
||||
**other_params}
|
||||
|
|
@ -16,5 +16,7 @@ export enum WorkflowType {
|
|||
FormNode = 'form-node',
|
||||
TextToSpeechNode = 'text-to-speech-node',
|
||||
SpeechToTextNode = 'speech-to-text-node',
|
||||
ImageGenerateNode = 'image-generate-node'
|
||||
ImageGenerateNode = 'image-generate-node',
|
||||
LoopNode = 'loop-node',
|
||||
LoopBodyNode = 'loop-body-node'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
>
|
||||
<div v-resize="resizeStepContainer">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center" style="width: 70%;">
|
||||
<div class="flex align-center" style="width: 70%">
|
||||
<component
|
||||
:is="iconComponent(`${nodeModel.type}-icon`)"
|
||||
class="mr-8"
|
||||
|
|
@ -290,6 +290,7 @@ const resizeStepContainer = (wh: any) => {
|
|||
}
|
||||
|
||||
function clickNodes(item: any) {
|
||||
console.log('clickNodes', item)
|
||||
const width = item.properties.width ? item.properties.width : 214
|
||||
const nodeModel = props.nodeModel.graphModel.addNode({
|
||||
type: item.type,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class AppNode extends HtmlResize.view {
|
|||
getNodesName(number + 1)
|
||||
}
|
||||
}
|
||||
props.model.properties.config = nodeDict[props.model.type].properties.config
|
||||
if (props.model.properties.height) {
|
||||
props.model.height = props.model.properties.height
|
||||
}
|
||||
|
|
@ -115,12 +114,11 @@ class AppNode extends HtmlResize.view {
|
|||
} else {
|
||||
isConnect = this.props.graphModel.edges.some((edge) => edge.sourceAnchorId == anchorData.id)
|
||||
}
|
||||
|
||||
return lh(
|
||||
'foreignObject',
|
||||
{
|
||||
...anchorData,
|
||||
x: x - 10,
|
||||
x: x - 14,
|
||||
y: y - 12,
|
||||
width: 30,
|
||||
height: 30
|
||||
|
|
@ -134,7 +132,7 @@ class AppNode extends HtmlResize.view {
|
|||
}
|
||||
},
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: isConnect
|
||||
__html: (type == 'children' ? true : isConnect)
|
||||
? `<svg width="100%" height="100%" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_5119_232585)">
|
||||
<path d="M20.9998 29.8333C28.0875 29.8333 33.8332 24.0876 33.8332 17C33.8332 9.91231 28.0875 4.16663 20.9998 4.16663C13.9122 4.16663 8.1665 9.91231 8.1665 17C8.1665 24.0876 13.9122 29.8333 20.9998 29.8333Z" fill="white"/>
|
||||
|
|
|
|||
|
|
@ -319,6 +319,65 @@ export const textToSpeechNode = {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const loopNode = {
|
||||
type: WorkflowType.LoopNode,
|
||||
visible: false,
|
||||
text: t('views.applicationWorkflow.nodes.loopNode.text', '循环节点'),
|
||||
label: t('views.applicationWorkflow.nodes.loopNode.label', '循环节点'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.loopNode.label', '循环节点'),
|
||||
workflow: {
|
||||
edges: [],
|
||||
nodes: [
|
||||
{
|
||||
x: 480,
|
||||
y: 3340,
|
||||
id: 'start-node',
|
||||
type: 'start-node',
|
||||
properties: {
|
||||
config: {
|
||||
fields: [],
|
||||
globalFields: []
|
||||
},
|
||||
fields: [],
|
||||
height: 361.333,
|
||||
showNode: true,
|
||||
stepName: '开始',
|
||||
globalFields: []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('loop.item', '循环参数'),
|
||||
value: 'item'
|
||||
},
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const loopBodyNode = {
|
||||
type: WorkflowType.LoopBodyNode,
|
||||
text: t('views.applicationWorkflow.nodes.loopBodyNode.text', '循环体'),
|
||||
label: t('views.applicationWorkflow.nodes.loopBodyNode.label', '循环体'),
|
||||
height: 600,
|
||||
properties: {
|
||||
width: 1800,
|
||||
stepName: t('views.applicationWorkflow.nodes.loopBodyNode.label', '循环体'),
|
||||
config: {
|
||||
fields: []
|
||||
}
|
||||
}
|
||||
}
|
||||
export const menuNodes = [
|
||||
aiChatNode,
|
||||
imageUnderstandNode,
|
||||
|
|
@ -332,7 +391,8 @@ export const menuNodes = [
|
|||
documentExtractNode,
|
||||
speechToTextNode,
|
||||
textToSpeechNode,
|
||||
variableAssignNode
|
||||
variableAssignNode,
|
||||
loopNode
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
@ -426,7 +486,9 @@ export const nodeDict: any = {
|
|||
[WorkflowType.TextToSpeechNode]: textToSpeechNode,
|
||||
[WorkflowType.SpeechToTextNode]: speechToTextNode,
|
||||
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
|
||||
[WorkflowType.VariableAssignNode]: variableAssignNode
|
||||
[WorkflowType.VariableAssignNode]: variableAssignNode,
|
||||
[WorkflowType.LoopNode]: loopNode,
|
||||
[WorkflowType.LoopBodyNode]: loopBodyNode
|
||||
}
|
||||
export function isWorkFlow(type: string | undefined) {
|
||||
return type === 'WORK_FLOW'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
|
||||
|
||||
class CustomEdgeModel2 extends BezierEdgeModel {
|
||||
getArrowStyle() {
|
||||
const arrowStyle = super.getArrowStyle()
|
||||
arrowStyle.offset = 0
|
||||
arrowStyle.verticalLength = 0
|
||||
return arrowStyle
|
||||
}
|
||||
|
||||
getEdgeStyle() {
|
||||
const style = super.getEdgeStyle()
|
||||
// svg属性
|
||||
style.strokeWidth = 2
|
||||
style.stroke = '#BBBFC4'
|
||||
style.offset = 0
|
||||
return style
|
||||
}
|
||||
/**
|
||||
* 重写此方法,使保存数据是能带上锚点数据。
|
||||
*/
|
||||
getData() {
|
||||
const data: any = super.getData()
|
||||
if (data) {
|
||||
data.sourceAnchorId = this.sourceAnchorId
|
||||
data.targetAnchorId = this.targetAnchorId
|
||||
}
|
||||
return data
|
||||
}
|
||||
/**
|
||||
* 给边自定义方案,使其支持基于锚点的位置更新边的路径
|
||||
*/
|
||||
updatePathByAnchor() {
|
||||
// TODO
|
||||
const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
|
||||
const sourceAnchor = sourceNodeModel
|
||||
.getDefaultAnchor()
|
||||
.find((anchor: any) => anchor.id === this.sourceAnchorId)
|
||||
|
||||
const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
|
||||
const targetAnchor = targetNodeModel
|
||||
.getDefaultAnchor()
|
||||
.find((anchor: any) => anchor.id === this.targetAnchorId)
|
||||
if (sourceAnchor && targetAnchor) {
|
||||
const startPoint = {
|
||||
x: sourceAnchor.x,
|
||||
y: sourceAnchor.y - 10
|
||||
}
|
||||
this.updateStartPoint(startPoint)
|
||||
const endPoint = {
|
||||
x: targetAnchor.x,
|
||||
y: targetAnchor.y + 3
|
||||
}
|
||||
|
||||
this.updateEndPoint(endPoint)
|
||||
}
|
||||
|
||||
// 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
|
||||
this.pointsList = []
|
||||
this.initPoints()
|
||||
}
|
||||
setAttributes(): void {
|
||||
super.setAttributes()
|
||||
this.isHitable = true
|
||||
this.zIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'loop-edge',
|
||||
view: BezierEdge,
|
||||
model: CustomEdgeModel2
|
||||
}
|
||||
|
|
@ -91,12 +91,20 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
|
|||
return
|
||||
}
|
||||
if (elements.edges.length > 0 && elements.nodes.length == 0) {
|
||||
elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
|
||||
elements.edges.forEach((edge: any) => {
|
||||
if (edge.type === 'app-edge') {
|
||||
lf.deleteEdge(edge.id)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const nodes = elements.nodes.filter((node) => ['start-node', 'base-node'].includes(node.type))
|
||||
const nodes = elements.nodes.filter((node) =>
|
||||
['start-node', 'base-node', 'loop-body-node'].includes(node.type)
|
||||
)
|
||||
if (nodes.length > 0) {
|
||||
MsgError(`${nodes[0].properties?.stepName}${t('views.applicationWorkflow.delete.deleteMessage')}`)
|
||||
MsgError(
|
||||
`${nodes[0].properties?.stepName}${t('views.applicationWorkflow.delete.deleteMessage')}`
|
||||
)
|
||||
return
|
||||
}
|
||||
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
|
||||
|
|
@ -107,7 +115,17 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
|
|||
if (graph.textEditElement) return true
|
||||
|
||||
elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
|
||||
elements.nodes.forEach((node: any) => lf.deleteNode(node.id))
|
||||
elements.nodes.forEach((node: any) => {
|
||||
if (node.type === 'loop-node') {
|
||||
const next = lf.getNodeOutgoingNode(node.id)
|
||||
next.forEach((n: any) => {
|
||||
if (n.type === 'loop-body-node') {
|
||||
lf.deleteNode(n.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
lf.deleteNode(node.id)
|
||||
})
|
||||
})
|
||||
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ export function getTeleport(): any {
|
|||
|
||||
// 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
|
||||
// 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
|
||||
if (id.startsWith(props.flowId)) {
|
||||
children.push(items[id])
|
||||
}
|
||||
// if (id.startsWith(props.flowId)) {
|
||||
children.push(items[id])
|
||||
// }
|
||||
})
|
||||
return h(
|
||||
Fragment,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ const end_nodes: Array<string> = [
|
|||
WorkflowType.Application,
|
||||
WorkflowType.SpeechToTextNode,
|
||||
WorkflowType.TextToSpeechNode,
|
||||
WorkflowType.ImageGenerateNode,
|
||||
WorkflowType.ImageGenerateNode,
|
||||
WorkflowType.LoopBodyNode
|
||||
]
|
||||
export class WorkFlowInstance {
|
||||
nodes
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import LogicFlow from '@logicflow/core'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import AppEdge from './common/edge'
|
||||
import loopEdge from './common/loopEdge'
|
||||
import Control from './common/NodeControl.vue'
|
||||
import { baseNodes } from '@/workflow/common/data'
|
||||
import '@logicflow/extension/lib/style/index.css'
|
||||
|
|
@ -93,7 +94,11 @@ const renderGraphData = (data?: any) => {
|
|||
flowId.value = lf.value.graphModel.flowId
|
||||
})
|
||||
initDefaultShortcut(lf.value, lf.value.graphModel)
|
||||
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
|
||||
lf.value.batchRegister([
|
||||
...Object.keys(nodes).map((key) => nodes[key].default),
|
||||
AppEdge,
|
||||
loopEdge
|
||||
])
|
||||
lf.value.setDefaultEdgeType('app-edge')
|
||||
|
||||
lf.value.render(data ? data : {})
|
||||
|
|
@ -117,7 +122,18 @@ const validate = () => {
|
|||
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
|
||||
}
|
||||
const getGraphData = () => {
|
||||
return lf.value.getGraphData()
|
||||
const graph_data = lf.value.getGraphData()
|
||||
graph_data.nodes = graph_data.nodes.filter((node: any) => {
|
||||
if (node.type === 'loop-body-node') {
|
||||
const node_model = lf.value.getNodeModelById(node.id)
|
||||
console.log(node_model)
|
||||
node_model.set_loop_body()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
graph_data.edges = graph_data.edges.filter((node: any) => node.type !== 'loop-edge')
|
||||
return graph_data
|
||||
}
|
||||
|
||||
const onmousedown = (shapeItem: ShapeItem) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
<template>
|
||||
<div @mousedown="mousedown" 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 }"
|
||||
style="overflow: visible"
|
||||
>
|
||||
<div v-resize="resizeStepContainer">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center" style="width: 70%">
|
||||
<component
|
||||
:is="iconComponent(`${nodeModel.type}-icon`)"
|
||||
class="mr-8"
|
||||
:size="24"
|
||||
:item="nodeModel?.properties.node_data"
|
||||
/>
|
||||
<h4 class="ellipsis-1 break-all">{{ nodeModel.properties.stepName }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div @mousedown.stop @keydown.stop @click.stop v-show="showNode" class="mt-16">
|
||||
<el-alert
|
||||
v-if="node_status != 200"
|
||||
class="mb-16"
|
||||
:title="
|
||||
props.nodeModel.type === 'application-node'
|
||||
? $t('views.applicationWorkflow.tip.applicationNodeError')
|
||||
: $t('views.applicationWorkflow.tip.functionNodeError')
|
||||
"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<slot></slot>
|
||||
<template v-if="nodeFields.length > 0">
|
||||
<h5 class="title-decoration-1 mb-8 mt-8">
|
||||
{{ $t('common.param.outputParam') }}
|
||||
</h5>
|
||||
<template v-for="(item, index) in nodeFields" :key="index">
|
||||
<div
|
||||
class="flex-between border-r-4 p-8-12 mb-8 layout-bg lighter"
|
||||
@mouseenter="showicon = index"
|
||||
@mouseleave="showicon = null"
|
||||
>
|
||||
<span style="max-width: 92%">{{ item.label }} {{ '{' + item.value + '}' }}</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.setting.copyParam')"
|
||||
placement="top"
|
||||
v-if="showicon === index"
|
||||
>
|
||||
<el-button link @click="copyClick(item.globeLabel)" style="padding: 0">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('views.applicationWorkflow.nodeName')"
|
||||
v-model="nodeNameDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
append-to-body
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form label-position="top" ref="titleFormRef" :model="form">
|
||||
<el-form-item
|
||||
prop="title"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('common.inputPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<el-input v-model="form.title" @blur="form.title = form.title.trim()" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="nodeNameDialogVisible = false">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="editName(titleFormRef)">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { set } from 'lodash'
|
||||
import { iconComponent } from '../../icons/utils'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const height = ref<{
|
||||
stepContainerHeight: number
|
||||
inputContainerHeight: number
|
||||
outputContainerHeight: number
|
||||
}>({
|
||||
stepContainerHeight: 0,
|
||||
inputContainerHeight: 0,
|
||||
outputContainerHeight: 0
|
||||
})
|
||||
const showAnchor = ref<boolean>(false)
|
||||
const anchorData = ref<any>()
|
||||
const titleFormRef = ref()
|
||||
const nodeNameDialogVisible = ref<boolean>(false)
|
||||
const form = ref<any>({
|
||||
title: ''
|
||||
})
|
||||
|
||||
const showNode = computed({
|
||||
set: (v) => {
|
||||
set(props.nodeModel.properties, 'showNode', v)
|
||||
},
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.showNode !== undefined) {
|
||||
return props.nodeModel.properties.showNode
|
||||
}
|
||||
set(props.nodeModel.properties, 'showNode', true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
const node_status = computed(() => {
|
||||
if (props.nodeModel.properties.status) {
|
||||
return props.nodeModel.properties.status
|
||||
}
|
||||
return 200
|
||||
})
|
||||
|
||||
const editName = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (
|
||||
!props.nodeModel.graphModel.nodes?.some(
|
||||
(node: any) => node.properties.stepName === form.value.title
|
||||
)
|
||||
) {
|
||||
set(props.nodeModel.properties, 'stepName', form.value.title)
|
||||
nodeNameDialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
} else {
|
||||
MsgError(t('views.applicationWorkflow.tip.repeatedNodeError'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const mousedown = () => {
|
||||
props.nodeModel.graphModel.clearSelectElements()
|
||||
set(props.nodeModel, 'isSelected', true)
|
||||
set(props.nodeModel, 'isHovered', true)
|
||||
props.nodeModel.graphModel.toFront(props.nodeModel.id)
|
||||
}
|
||||
const showicon = ref<number | null>(null)
|
||||
|
||||
const resizeStepContainer = (wh: any) => {
|
||||
if (wh.height) {
|
||||
if (!props.nodeModel.virtual) {
|
||||
height.value.stepContainerHeight = wh.height
|
||||
props.nodeModel.setHeight(height.value.stepContainerHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
nodeModel: any
|
||||
}>()
|
||||
const nodeFields = computed(() => {
|
||||
if (props.nodeModel.properties.config.fields) {
|
||||
const fields = props.nodeModel.properties.config.fields?.map((field: any) => {
|
||||
return {
|
||||
label: field.label,
|
||||
value: field.value,
|
||||
globeLabel: `{{${props.nodeModel.properties.stepName}.${field.value}}}`,
|
||||
globeValue: `{{context['${props.nodeModel.id}'].${field.value}}}`
|
||||
}
|
||||
})
|
||||
return fields
|
||||
}
|
||||
return []
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.workflow-node-container {
|
||||
.step-container {
|
||||
border: 2px solid #ffffff !important;
|
||||
box-sizing: border-box;
|
||||
&:hover {
|
||||
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
||||
}
|
||||
&.isSelected {
|
||||
border: 2px solid var(--el-color-primary) !important;
|
||||
}
|
||||
&.error {
|
||||
border: 1px solid #f54a45 !important;
|
||||
}
|
||||
}
|
||||
.arrow-icon {
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
:deep(.el-card) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import LoopNode from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
import { WorkflowType } from '@/enums/workflow'
|
||||
class LoopBodyNodeView extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, LoopNode)
|
||||
}
|
||||
}
|
||||
class LoopBodyModel extends AppNodeModel {
|
||||
refreshBranch() {
|
||||
// 更新节点连接边的path
|
||||
this.incoming.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
getDefaultAnchor() {
|
||||
const { id, x, y, width, height } = this
|
||||
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
|
||||
const anchors: any = []
|
||||
anchors.push({
|
||||
edgeAddable: false,
|
||||
x: x,
|
||||
y: y - height / 2 + 10,
|
||||
id: `${id}_children`,
|
||||
type: 'children'
|
||||
})
|
||||
|
||||
return anchors
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'loop-body-node',
|
||||
model: LoopBodyModel,
|
||||
view: LoopBodyNodeView
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<LoopBodyContainer :nodeModel="nodeModel">
|
||||
<div ref="containerRef" @wheel.stop style="height: 550px"></div>
|
||||
</LoopBodyContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { set } from 'lodash'
|
||||
import AppEdge from '@/workflow/common/edge'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import LogicFlow from '@logicflow/core'
|
||||
import Dagre from '@/workflow/plugins/dagre'
|
||||
import { initDefaultShortcut } from '@/workflow/common/shortcut'
|
||||
import LoopBodyContainer from '@/workflow/nodes/loop-body-node/LoopBodyContainer.vue'
|
||||
|
||||
const nodes: any = import.meta.glob('@/workflow/nodes/**/index.ts', { eager: true })
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
const containerRef = ref()
|
||||
|
||||
const validate = () => {
|
||||
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
|
||||
}
|
||||
const set_loop_body = () => {
|
||||
const loop_node_id = props.nodeModel.properties.loop_node_id
|
||||
const loop_node = props.nodeModel.graphModel.getNodeModelById(loop_node_id)
|
||||
loop_node.properties.node_data.loop_body = lf.value.getGraphData()
|
||||
}
|
||||
const lf = ref()
|
||||
|
||||
const renderGraphData = (data?: any) => {
|
||||
const container: any = containerRef.value
|
||||
if (container) {
|
||||
lf.value = new LogicFlow({
|
||||
plugins: [Dagre],
|
||||
textEdit: false,
|
||||
adjustEdge: false,
|
||||
adjustEdgeStartAndEnd: false,
|
||||
background: {
|
||||
backgroundColor: '#f5f6f7'
|
||||
},
|
||||
grid: {
|
||||
size: 10,
|
||||
type: 'dot',
|
||||
config: {
|
||||
color: '#DEE0E3',
|
||||
thickness: 1
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
enabled: true
|
||||
},
|
||||
isSilentMode: false,
|
||||
container: container
|
||||
})
|
||||
lf.value.setTheme({
|
||||
bezier: {
|
||||
stroke: '#afafaf',
|
||||
strokeWidth: 1
|
||||
}
|
||||
})
|
||||
|
||||
initDefaultShortcut(lf.value, lf.value.graphModel)
|
||||
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
|
||||
lf.value.setDefaultEdgeType('app-edge')
|
||||
lf.value.render(data ? data : {})
|
||||
|
||||
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
|
||||
id_list.forEach((id: string) => {
|
||||
lf.value.deleteEdge(id)
|
||||
})
|
||||
})
|
||||
lf.value.graphModel.eventCenter.on('anchor:drop', (data: any) => {
|
||||
// 清除当前节点下面的子节点的所有缓存
|
||||
data.nodeModel.clear_next_node_field(false)
|
||||
})
|
||||
lf.value.graphModel.eventCenter.on('anchor:drop', (data: any) => {
|
||||
// 清除当前节点下面的子节点的所有缓存
|
||||
data.nodeModel.clear_next_node_field(false)
|
||||
})
|
||||
lf.value.graphModel.eventCenter.on('history:change', (data: any) => {
|
||||
set(props.nodeModel.properties, 'workflow', lf.value.getGraphData())
|
||||
})
|
||||
setTimeout(() => {
|
||||
lf.value?.fitView()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
renderGraphData(props.nodeModel.properties.workflow)
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
set(props.nodeModel, 'set_loop_body', set_loop_body)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import LoopNode from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
import { WorkflowType } from '@/enums/workflow'
|
||||
class LoopNodeView extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, LoopNode)
|
||||
}
|
||||
}
|
||||
class LoopModel extends AppNodeModel {
|
||||
refreshBranch() {
|
||||
// 更新节点连接边的path
|
||||
this.incoming.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
getDefaultAnchor() {
|
||||
const { id, x, y, width, height } = this
|
||||
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
|
||||
const anchors: any = []
|
||||
|
||||
if (this.type !== WorkflowType.Base) {
|
||||
if (this.type !== WorkflowType.Start) {
|
||||
anchors.push({
|
||||
x: x - width / 2 + 10,
|
||||
y: showNode ? y : y - 15,
|
||||
id: `${id}_left`,
|
||||
edgeAddable: false,
|
||||
type: 'left'
|
||||
})
|
||||
}
|
||||
anchors.push({
|
||||
x: x + width / 2 - 10,
|
||||
y: showNode ? y : y - 15,
|
||||
id: `${id}_right`,
|
||||
type: 'right'
|
||||
})
|
||||
}
|
||||
anchors.push({
|
||||
x: x,
|
||||
y: y + height / 2 - 25,
|
||||
id: `${id}_children`,
|
||||
type: 'children'
|
||||
})
|
||||
|
||||
return anchors
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'loop-node',
|
||||
model: LoopModel,
|
||||
view: LoopNodeView
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="replyNodeFormRef"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.loopNode.loopType.label', '循环类型')"
|
||||
@click.prevent
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span
|
||||
>{{ $t('views.applicationWorkflow.nodes.loopNode.loopType.label', '循环类型')
|
||||
}}<span class="danger">*</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="form_data.loop_type" type="small">
|
||||
<el-option
|
||||
:label="$t('views.applicationWorkflow.nodes.loopNode.array', '数组循环')"
|
||||
value="ARRAY"
|
||||
/>
|
||||
<el-option
|
||||
:label="$t('views.applicationWorkflow.nodes.loopNode.number', '指定次数循环')"
|
||||
value="NUMBER"
|
||||
/>
|
||||
<el-option
|
||||
:label="$t('views.applicationWorkflow.nodes.loopNode.loop', '无限循环')"
|
||||
value="LOOP"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form_data.loop_type == 'ARRAY'"
|
||||
:label="$t('views.applicationWorkflow.nodes.loopNode.loopType.label', '循环数组')"
|
||||
@click.prevent
|
||||
prop="array"
|
||||
:rules="{
|
||||
message: $t(
|
||||
'views.applicationWorkflow.nodes.loopNode.array.requiredMessage',
|
||||
'循环数组必填'
|
||||
),
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('views.applicationWorkflow.nodes.loopNode.array.placeholder', '请选择循环数组')
|
||||
"
|
||||
v-model="form_data.array"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-else-if="form_data.loop_type == 'NUMBER'"
|
||||
:label="$t('views.applicationWorkflow.nodes.loopNode.loopType.label', '循环数组')"
|
||||
@click.prevent
|
||||
prop="number"
|
||||
:rules="{
|
||||
message: $t(
|
||||
'views.applicationWorkflow.nodes.loopNode.array.requiredMessage',
|
||||
'循环数组必填'
|
||||
),
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
>
|
||||
<el-input-number v-model="form_data.number" :min="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { set } from 'lodash'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { isLastNode } from '@/workflow/common/data'
|
||||
import { loopBodyNode } from '@/workflow/common/data'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const form = {
|
||||
loop_type: 'ARRAY',
|
||||
array: [],
|
||||
number: 1
|
||||
}
|
||||
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
|
||||
const replyNodeFormRef = ref()
|
||||
const nodeCascaderRef = ref()
|
||||
const validate = () => {
|
||||
return Promise.all([
|
||||
nodeCascaderRef.value ? nodeCascaderRef.value.validate() : Promise.resolve(''),
|
||||
replyNodeFormRef.value?.validate()
|
||||
]).catch((err: any) => {
|
||||
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
||||
if (isLastNode(props.nodeModel)) {
|
||||
set(props.nodeModel.properties.node_data, 'is_result', true)
|
||||
}
|
||||
}
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
const nodeOutgoingNode = props.nodeModel.graphModel.getNodeOutgoingNode(props.nodeModel.id)
|
||||
if (!nodeOutgoingNode.some((item: any) => item.type == loopBodyNode.type)) {
|
||||
const nodeModel = props.nodeModel.graphModel.addNode({
|
||||
type: loopBodyNode.type,
|
||||
properties: {
|
||||
...loopBodyNode.properties,
|
||||
workflow: props.nodeModel.properties.node_data.loop_body,
|
||||
loop_node_id: props.nodeModel.id
|
||||
},
|
||||
x: props.nodeModel.x,
|
||||
y: props.nodeModel.y + loopBodyNode.height
|
||||
})
|
||||
props.nodeModel.graphModel.addEdge({
|
||||
type: 'loop-edge',
|
||||
sourceNodeId: props.nodeModel.id,
|
||||
sourceAnchorId: props.nodeModel.id + '_children',
|
||||
targetNodeId: nodeModel.id,
|
||||
virtual: true
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.variable.global') }}</h5>
|
||||
<h5 v-if="nodeModel.properties.config.globalFields.length > 0" class="title-decoration-1 mb-8">
|
||||
{{ $t('views.applicationWorkflow.variable.global') }}
|
||||
</h5>
|
||||
<div
|
||||
v-for="(item, index) in nodeModel.properties.config.globalFields"
|
||||
:key="index"
|
||||
|
|
@ -63,7 +65,6 @@ const refreshFieldList = () => {
|
|||
const refreshFieldList = getRefreshFieldList()
|
||||
set(props.nodeModel.properties.config, 'globalFields', [...globalFields, ...refreshFieldList])
|
||||
}
|
||||
props.nodeModel.graphModel.eventCenter.on('refreshFieldList', refreshFieldList)
|
||||
|
||||
const refreshFileUploadConfig = () => {
|
||||
let fields = cloneDeep(props.nodeModel.properties.config.fields)
|
||||
|
|
@ -101,11 +102,13 @@ const refreshFileUploadConfig = () => {
|
|||
|
||||
set(props.nodeModel.properties.config, 'fields', [...fields, ...fileUploadFields])
|
||||
}
|
||||
props.nodeModel.graphModel.eventCenter.on('refreshFileUploadConfig', refreshFileUploadConfig)
|
||||
|
||||
onMounted(() => {
|
||||
refreshFieldList()
|
||||
refreshFileUploadConfig()
|
||||
console.log(props.nodeModel.graphModel)
|
||||
// refreshFieldList()
|
||||
// refreshFileUploadConfig()
|
||||
// props.nodeModel.graphModel.eventCenter.on('refreshFileUploadConfig', refreshFileUploadConfig)
|
||||
// props.nodeModel.graphModel.eventCenter.on('refreshFieldList', refreshFieldList)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue