feat: Support loop node (#4045)

This commit is contained in:
shaohuzhang1 2025-09-16 15:49:49 +08:00 committed by GitHub
parent e7ce9a0524
commit 7264545ab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 2613 additions and 119 deletions

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare.compare import Compare
class ContainCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class EqualCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class GECompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class GTCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare import Compare
from application.flow.compare import Compare
class IsNotNullCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare import Compare
from application.flow.compare import Compare
class IsNotTrueCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare import Compare
from application.flow.compare import Compare
class IsNullCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare import Compare
from application.flow.compare import Compare
class IsTrueCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LECompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LenEqualCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LenGECompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LenGTCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LenLECompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LenLTCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class LTCompare(Compare):

View File

@ -8,7 +8,7 @@
"""
from typing import List
from application.flow.step_node.condition_node.compare.compare import Compare
from application.flow.compare import Compare
class NotContainCompare(Compare):

View File

@ -168,7 +168,7 @@ class INode:
self.runtime_node_id, self.context.get('reasoning_content', '') if reasoning_content_enable else '')]
def __init__(self, node, workflow_params, workflow_manage, up_node_id_list=None,
get_node_params=lambda node: node.properties.get('node_data')):
get_node_params=lambda node: node.properties.get('node_data'), salt=None):
# 当前步骤上下文,用于存储当前步骤信息
self.status = 200
self.err_message = ''
@ -188,7 +188,8 @@ class INode:
self.runtime_node_id = sha1(uuid.NAMESPACE_DNS.bytes + bytes(str(uuid.uuid5(uuid.NAMESPACE_DNS,
"".join([*sorted(up_node_id_list),
node.id]))),
"utf-8")).hexdigest()
"utf-8")).hexdigest() + (
"__" + str(salt) if salt is not None else '')
def valid_args(self, node_params, flow_params):
flow_params_serializer_class = self.get_flow_params_serializer_class()

View File

@ -0,0 +1,193 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file workflow_manage.py
@date2024/1/9 17:40
@desc:
"""
from concurrent.futures import ThreadPoolExecutor
from typing import List
from django.db import close_old_connections
from django.utils.translation import get_language
from langchain_core.prompts import PromptTemplate
from application.flow.common import Workflow
from application.flow.i_step_node import WorkFlowPostHandler, INode
from application.flow.step_node import get_node
from application.flow.workflow_manage import WorkflowManage
from common.handle.base_to_response import BaseToResponse
from common.handle.impl.response.system_to_response import SystemToResponse
executor = ThreadPoolExecutor(max_workers=200)
class NodeResultFuture:
def __init__(self, r, e, status=200):
self.r = r
self.e = e
self.status = status
def result(self):
if self.status == 200:
return self.r
else:
raise self.e
def await_result(result, timeout=1):
try:
result.result(timeout)
return False
except Exception as e:
return True
class NodeChunkManage:
def __init__(self, work_flow):
self.node_chunk_list = []
self.current_node_chunk = None
self.work_flow = work_flow
def add_node_chunk(self, node_chunk):
self.node_chunk_list.append(node_chunk)
def contains(self, node_chunk):
return self.node_chunk_list.__contains__(node_chunk)
def pop(self):
if self.current_node_chunk is None:
try:
current_node_chunk = self.node_chunk_list.pop(0)
self.current_node_chunk = current_node_chunk
except IndexError as e:
pass
if self.current_node_chunk is not None:
try:
chunk = self.current_node_chunk.chunk_list.pop(0)
return chunk
except IndexError as e:
if self.current_node_chunk.is_end():
self.current_node_chunk = None
if self.work_flow.answer_is_not_empty():
chunk = self.work_flow.base_to_response.to_stream_chunk_response(
self.work_flow.params['chat_id'],
self.work_flow.params['chat_record_id'],
'\n\n', False, 0, 0)
self.work_flow.append_answer('\n\n')
return chunk
return self.pop()
return None
class LoopWorkflowManage(WorkflowManage):
def __init__(self, flow: Workflow,
params,
work_flow_post_handler: WorkFlowPostHandler,
parentWorkflowManage,
loop_params,
get_loop_context,
base_to_response: BaseToResponse = SystemToResponse(),
start_node_id=None,
start_node_data=None, chat_record=None, child_node=None):
self.parentWorkflowManage = parentWorkflowManage
self.loop_params = loop_params
self.get_loop_context = get_loop_context
self.loop_field_list = []
super().__init__(flow, params, work_flow_post_handler, base_to_response, None, None, None,
None,
None, start_node_id, start_node_data, chat_record, child_node)
def get_node_cls_by_id(self, node_id, up_node_id_list=None,
get_node_params=lambda node: node.properties.get('node_data')):
for node in self.flow.nodes:
if node.id == node_id:
node_instance = get_node(node.type)(node,
self.params, self, up_node_id_list,
get_node_params,
salt=self.get_index())
return node_instance
return None
def stream(self):
close_old_connections()
language = get_language()
self.run_chain_async(self.start_node, None, language)
return self.await_result()
def get_index(self):
return self.loop_params.get('index')
def get_start_node(self):
start_node_list = [node for node in self.flow.nodes if
['loop-start-node'].__contains__(node.type)]
return start_node_list[0]
def get_reference_field(self, node_id: str, fields: List[str]):
"""
@param node_id: 节点id
@param fields: 字段
@return:
"""
if node_id == 'global':
return self.parentWorkflowManage.get_reference_field(node_id, fields)
elif node_id == 'chat':
return self.parentWorkflowManage.get_reference_field(node_id, fields)
elif node_id == 'loop':
loop_context = self.get_loop_context()
return INode.get_field(loop_context, fields)
else:
node = self.get_node_by_id(node_id)
if node:
return node.get_reference_field(fields)
return self.parentWorkflowManage.get_reference_field(node_id, fields)
def get_workflow_content(self):
context = {
'global': self.context,
'chat': self.chat_context,
'loop': self.get_loop_context(),
}
for node in self.node_context:
context[node.id] = node.context
return context
def init_fields(self):
super().init_fields()
loop_field_list = []
loop_start_node = self.flow.get_node('loop-start-node')
loop_input_field_list = loop_start_node.properties.get('loop_input_field_list')
node_name = loop_start_node.properties.get('stepName')
node_id = loop_start_node.id
if loop_input_field_list is not None:
for f in loop_input_field_list:
loop_field_list.append(
{'label': f.get('label'), 'value': f.get('field'), 'node_id': node_id, 'node_name': node_name})
self.loop_field_list = loop_field_list
def reset_prompt(self, prompt: str):
prompt = super().reset_prompt(prompt)
for field in self.loop_field_list:
chatLabel = f"loop.{field.get('value')}"
chatValue = f"context.get('loop').get('{field.get('value', '')}','')"
prompt = prompt.replace(chatLabel, chatValue)
prompt = self.parentWorkflowManage.reset_prompt(prompt)
return prompt
def generate_prompt(self, prompt: str):
"""
格式化生成提示词
@param prompt: 提示词信息
@return: 格式化后的提示词
"""
context = {**self.get_workflow_content(), **self.parentWorkflowManage.get_workflow_content()}
prompt = self.reset_prompt(prompt)
prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
value = prompt_template.format(context=context)
return value

View File

@ -15,6 +15,11 @@ from .form_node import *
from .image_generate_step_node import *
from .image_to_video_step_node import BaseImageToVideoNode
from .image_understand_step_node import *
from .intent_node import *
from .loop_break_node import BaseLoopBreakNode
from .loop_continue_node import BaseLoopContinueNode
from .loop_node import *
from .loop_start_node import *
from .mcp_node import BaseMcpNode
from .question_node import *
from .reranker_node import *
@ -26,7 +31,6 @@ from .text_to_video_step_node.impl.base_text_to_video_node import BaseTextToVide
from .tool_lib_node import *
from .tool_node import *
from .variable_assign_node import BaseVariableAssignNode
from .intent_node import *
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode,
@ -34,7 +38,9 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuest
BaseDocumentExtractNode,
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode, BaseTextToVideoNode, BaseImageToVideoNode,
BaseIntentNode]
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
BaseLoopContinueNode,
BaseLoopBreakNode]
def get_node(node_type):

View File

@ -9,7 +9,7 @@
from typing import List
from application.flow.i_step_node import NodeResult
from application.flow.step_node.condition_node.compare import compare_handle_list
from application.flow.compare import compare_handle_list
from application.flow.step_node.condition_node.i_condition_node import IConditionNode

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/9/15 12:08
@desc:
"""
from .impl import *

View File

@ -0,0 +1,39 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file i_loop_break_node.py
@date2025/9/15 12:14
@desc:
"""
from typing import Type
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.flow.i_step_node import INode
from application.flow.i_step_node import NodeResult
class ConditionSerializer(serializers.Serializer):
compare = serializers.CharField(required=True, label=_("Comparator"))
value = serializers.CharField(required=True, label=_("value"))
field = serializers.ListField(required=True, label=_("Fields"))
class LoopBreakNodeSerializer(serializers.Serializer):
condition = serializers.CharField(required=True, label=_("Condition or|and"))
condition_list = ConditionSerializer(many=True)
class ILoopBreakNode(INode):
type = 'loop-break-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return LoopBreakNodeSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data)
def execute(self, condition, condition_list, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/9/15 12:16
@desc:
"""
from .base_loop_break_node import BaseLoopBreakNode

View File

@ -0,0 +1,58 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file base_loop_break_node.py
@date2025/9/15 12:17
@desc:
"""
import time
from typing import List, Dict
from application.flow.compare import compare_handle_list
from application.flow.i_step_node import NodeResult
from application.flow.step_node.loop_break_node.i_loop_break_node import ILoopBreakNode
def _write_context(step_variable: Dict, global_variable: Dict, node, workflow):
if step_variable.get("is_break"):
yield "BREAK"
node.context['run_time'] = time.time() - node.context['start_time']
class BaseLoopBreakNode(ILoopBreakNode):
def execute(self, condition, condition_list, **kwargs) -> NodeResult:
r = [self.assertion(row.get('field'), row.get('compare'), row.get('value')) for row in
condition_list]
is_break = all(r) if condition == 'and' else any(r)
if is_break:
self.node_params['is_result'] = True
return NodeResult({'is_break': is_break}, {},
_write_context=_write_context,
_is_interrupt=lambda n, v, w: is_break)
def assertion(self, field_list: List[str], compare: str, value):
try:
value = self.workflow_manage.generate_prompt(value)
except Exception as e:
pass
field_value = None
try:
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
except Exception as e:
pass
for compare_handler in compare_handle_list:
if compare_handler.support(field_list[0], field_list[1:], field_value, compare, value):
return compare_handler.compare(field_value, compare, value)
def get_details(self, index: int, **kwargs):
return {
'name': self.node.properties.get('stepName'),
"index": index,
"question": self.context.get('question'),
'run_time': self.context.get('run_time'),
'type': self.node.type,
'status': self.status,
'err_message': self.err_message
}

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/9/15 12:08
@desc:
"""
from .impl import *

View File

@ -0,0 +1,38 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file i_loop_continue_node.py
@date2025/9/15 12:13
@desc:
"""
from typing import Type
from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult
from django.utils.translation import gettext_lazy as _
class ConditionSerializer(serializers.Serializer):
compare = serializers.CharField(required=True, label=_("Comparator"))
value = serializers.CharField(required=True, label=_("value"))
field = serializers.ListField(required=True, label=_("Fields"))
class LoopContinueNodeSerializer(serializers.Serializer):
condition = serializers.CharField(required=True, label=_("Condition or|and"))
condition_list = ConditionSerializer(many=True)
class ILoopContinueNode(INode):
type = 'loop-continue-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return LoopContinueNodeSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data)
def execute(self, condition, condition_list, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/9/15 12:13
@desc:
"""
from .base_loop_continue_node import BaseLoopContinueNode

View File

@ -0,0 +1,48 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file base_loop_continue_node.py
@date2025/9/15 12:13
@desc:
"""
from typing import List
from application.flow.compare import compare_handle_list
from application.flow.i_step_node import NodeResult
from application.flow.step_node.loop_continue_node.i_loop_continue_node import ILoopContinueNode
class BaseLoopContinueNode(ILoopContinueNode):
def execute(self, condition, condition_list, **kwargs) -> NodeResult:
condition_list = [self.assertion(row.get('field'), row.get('compare'), row.get('value')) for row in
condition_list]
is_continue = all(condition_list) if condition == 'and' else any(condition_list)
if is_continue:
return NodeResult({'is_continue': is_continue, 'branch_id': 'continue'}, {})
return NodeResult({'is_continue': is_continue}, {})
def assertion(self, field_list: List[str], compare: str, value):
try:
value = self.workflow_manage.generate_prompt(value)
except Exception as e:
pass
field_value = None
try:
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
except Exception as e:
pass
for compare_handler in compare_handle_list:
if compare_handler.support(field_list[0], field_list[1:], field_value, compare, value):
return compare_handler.compare(field_value, compare, value)
def get_details(self, index: int, **kwargs):
return {
'name': self.node.properties.get('stepName'),
"index": index,
"question": self.context.get('question'),
'run_time': self.context.get('run_time'),
'type': self.node.type,
'status': self.status,
'err_message': self.err_message
}

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py
@date2025/3/11 18:24
@desc:
"""
from .impl import *

View File

@ -0,0 +1,56 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file i_loop_node.py
@date2025/3/11 18:19
@desc:
"""
from typing import Type
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult
from common.exception.app_exception import AppApiException
class ILoopNodeSerializer(serializers.Serializer):
loop_type = serializers.CharField(required=True, label=_("loop_type"))
array = serializers.ListField(required=False, allow_null=True,
label=_("array"))
number = serializers.IntegerField(required=False, allow_null=True,
label=_("number"))
loop_body = serializers.DictField(required=True, label="循环体")
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):
array = self.node_params_serializer.data.get('array')
if self.node_params_serializer.data.get('loop_type') == 'ARRAY':
array = self.workflow_manage.get_reference_field(
array[0],
array[1:])
return self.execute(**{**self.node_params_serializer.data, "array": array}, **self.flow_params_serializer.data)
def execute(self, loop_type, array, number, loop_body, stream, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py.py
@date2025/3/11 18:24
@desc:
"""
from .base_loop_node import BaseLoopNode

View File

@ -0,0 +1,261 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file base_loop_node.py
@date2025/3/11 18:24
@desc:
"""
import time
from typing import Dict, List
from application.flow.common import Answer
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 application.models import ChatRecord
from common.handle.impl.response.loop_to_response import LoopToResponse
def _is_interrupt_exec(node, node_variable: Dict, workflow_variable: Dict):
return node.context.get('is_interrupt_exec', False)
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')
workflow_manage = node_variable.get('workflow_manage')
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}
runtime_details = workflow_manage.get_runtime_details()
_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 get_answer_list(instance, child_node_node_dict, runtime_node_id):
answer_list = instance.get_record_answer_list()
for a in answer_list:
_v = child_node_node_dict.get(a.get('runtime_node_id'))
if _v:
a['runtime_node_id'] = runtime_node_id
a['child_node'] = _v
return answer_list
def insert_or_replace(arr, index, value):
if index < len(arr):
arr[index] = value # 替换
else:
# 在末尾插入足够多的None然后替换最后一个
arr.extend([None] * (index - len(arr) + 1))
arr[index] = value
return arr
def generate_loop_number(number: int):
def i(current_index: int):
return iter([(index, index) for index in range(current_index, number)])
return i
def generate_loop_array(array):
def i(current_index: int):
return iter([(array[index], index) for index in range(current_index, len(array))])
return i
def generate_while_loop(current_index: int):
index = current_index
while True:
yield index, index
def loop(workflow_manage_new_instance, node: INode, generate_loop):
loop_global_data = {}
break_outer = False
is_interrupt_exec = False
loop_node_data = node.context.get('loop_node_data') or []
loop_answer_data = node.context.get("loop_answer_data") or []
current_index = node.context.get("current_index") or 0
node_params = node.node_params
start_node_id = node_params.get('child_node', {}).get('runtime_node_id')
start_node_data = None
chat_record = None
child_node = None
if start_node_id:
chat_record_id = node_params.get('child_node', {}).get('chat_record_id')
child_node = node_params.get('child_node', {}).get('child_node')
start_node_data = node_params.get('node_data')
chat_record = ChatRecord(id=chat_record_id, answer_text_list=[], answer_text='',
details=loop_node_data[current_index])
for item, index in generate_loop(current_index):
"""
指定次数循环
@return:
"""
instance = workflow_manage_new_instance({'index': index, 'item': item}, loop_global_data, start_node_id,
start_node_data, chat_record, child_node)
response = instance.stream()
answer = ''
current_index = index
reasoning_content = ''
child_node_node_dict = {}
for chunk in response:
if chunk.get('node_type') == 'loop-break-node' and chunk.get('content', '') == 'BREAK':
break_outer = True
continue
child_node = chunk.get('child_node')
runtime_node_id = chunk.get('runtime_node_id', '')
chat_record_id = chunk.get('chat_record_id', '')
child_node_node_dict[runtime_node_id] = {
'runtime_node_id': runtime_node_id,
'chat_record_id': chat_record_id,
'child_node': child_node}
content_chunk = chunk.get('content', '')
reasoning_content_chunk = chunk.get('reasoning_content', '')
reasoning_content += reasoning_content_chunk
answer += content_chunk
yield chunk
if chunk.get('node_status', "SUCCESS") == 'ERROR':
raise Exception(chunk.get('content'))
node_type = chunk.get('node_type')
if node_type == 'form-node':
break_outer = True
is_interrupt_exec = True
start_node_id = None
start_node_data = None
chat_record = None
child_node = None
loop_global_data = instance.context
insert_or_replace(loop_node_data, index, instance.get_runtime_details())
insert_or_replace(loop_answer_data, index,
get_answer_list(instance, child_node_node_dict, node.runtime_node_id))
if break_outer:
break
node.context['is_interrupt_exec'] = is_interrupt_exec
node.context['loop_node_data'] = loop_node_data
node.context['loop_answer_data'] = loop_answer_data
node.context["index"] = current_index
node.context["item"] = current_index
def get_write_context(loop_type, array, number, loop_body, stream):
def inner_write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
if loop_type == 'ARRAY':
return loop(node_variable['workflow_manage_new_instance'], node, generate_loop_array(array))
if loop_type == 'LOOP':
return loop(node_variable['workflow_manage_new_instance'], node, generate_while_loop)
return loop(node_variable['workflow_manage_new_instance'], node, generate_loop_number(number))
return inner_write_context
class LoopWorkFlowPostHandler(WorkFlowPostHandler):
def handler(self, 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 get_answer_list(self) -> List[Answer] | None:
result = []
for answer_list in (self.context.get("loop_answer_data") or []):
for a in answer_list:
if isinstance(a, dict):
result.append(Answer(**a))
return result
def get_loop_context(self):
return self.context
def execute(self, loop_type, array, number, loop_body, stream, **kwargs) -> NodeResult:
from application.flow.loop_workflow_manage import LoopWorkflowManage, Workflow
def workflow_manage_new_instance(loop_data, global_data, start_node_id=None,
start_node_data=None, chat_record=None, child_node=None):
workflow_manage = LoopWorkflowManage(Workflow.new_instance(loop_body), self.workflow_manage.params,
LoopWorkFlowPostHandler(
self.workflow_manage.work_flow_post_handler.chat_info),
self.workflow_manage,
loop_data,
self.get_loop_context,
base_to_response=LoopToResponse(),
start_node_id=start_node_id,
start_node_data=start_node_data,
chat_record=chat_record,
child_node=child_node
)
return workflow_manage
return NodeResult({'workflow_manage_new_instance': workflow_manage_new_instance}, {},
_write_context=get_write_context(loop_type, array, number, loop_body, stream),
_is_interrupt=_is_interrupt_exec)
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,
'current_index': self.context.get("index"),
"current_item": self.context.get("item"),
'loop_type': self.context.get("loop_type"),
'status': self.status,
'loop_node_data': self.context.get("loop_node_data"),
'loop_answer_data': self.context.get("loop_answer_data"),
'err_message': self.err_message
}

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file __init__.py
@date2024/6/11 15:30
@desc:
"""
from .impl import *

View File

@ -0,0 +1,20 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file i_start_node.py
@date2024/6/3 16:54
@desc:
"""
from application.flow.i_step_node import INode, NodeResult
class ILoopStarNode(INode):
type = 'loop-start-node'
def _run(self):
return self.execute(**self.flow_params_serializer.data)
def execute(self, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file __init__.py
@date2024/6/11 15:36
@desc:
"""
from .base_start_node import BaseLoopStartStepNode

View File

@ -0,0 +1,55 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file base_start_node.py
@date2024/6/3 17:17
@desc:
"""
from typing import Type
from rest_framework import serializers
from application.flow.i_step_node import NodeResult
from application.flow.step_node.loop_start_node.i_loop_start_node import ILoopStarNode
class BaseLoopStartStepNode(ILoopStarNode):
def save_context(self, details, workflow_manage):
self.context['index'] = details.get('current_index')
self.context['item'] = details.get('current_item')
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
pass
def execute(self, **kwargs) -> NodeResult:
"""
开始节点 初始化全局变量
"""
loop_params = self.workflow_manage.loop_params
node_variable = {
'index': loop_params.get("index"),
'item': loop_params.get("item")
}
self.workflow_manage.chat_context = self.workflow_manage.get_chat_info().get_chat_variable()
return NodeResult(node_variable, {})
def get_details(self, index: int, **kwargs):
global_fields = []
for field in self.node.properties.get('config')['globalFields']:
key = field['value']
global_fields.append({
'label': field['label'],
'key': key,
'value': self.workflow_manage.context[key] if key in self.workflow_manage.context else ''
})
return {
'name': self.node.properties.get('stepName'),
"index": index,
"current_index": self.context.get('index'),
"current_item": self.context.get('item'),
'run_time': self.context.get('run_time'),
'type': self.node.type,
'status': self.status,
'err_message': self.err_message,
}

View File

@ -2,11 +2,8 @@
import json
from typing import List
from django.db.models import QuerySet
from application.flow.i_step_node import NodeResult
from application.flow.step_node.variable_assign_node.i_variable_assign_node import IVariableAssignNode
from application.models import Chat
class BaseVariableAssignNode(IVariableAssignNode):
@ -15,10 +12,23 @@ class BaseVariableAssignNode(IVariableAssignNode):
self.context['result_list'] = details.get('result_list')
def global_evaluation(self, variable, value):
self.workflow_manage.context[variable['fields'][1]] = value
from application.flow.loop_workflow_manage import LoopWorkflowManage
if isinstance(self.workflow_manage, LoopWorkflowManage):
self.workflow_manage.parentWorkflowManage.context[variable['fields'][1]] = value
else:
self.workflow_manage.context[variable['fields'][1]] = value
def loop_evaluation(self, variable, value):
from application.flow.loop_workflow_manage import LoopWorkflowManage
if isinstance(self.workflow_manage, LoopWorkflowManage):
self.workflow_manage.get_loop_context()[variable['fields'][1]] = value
def chat_evaluation(self, variable, value):
self.workflow_manage.chat_context[variable['fields'][1]] = value
from application.flow.loop_workflow_manage import LoopWorkflowManage
if isinstance(self.workflow_manage, LoopWorkflowManage):
self.workflow_manage.parentWorkflowManage.chat_context[variable['fields'][1]] = value
else:
self.workflow_manage.chat_context[variable['fields'][1]] = value
def handle(self, variable, evaluation):
result = {
@ -62,8 +72,17 @@ class BaseVariableAssignNode(IVariableAssignNode):
result = self.handle(variable, self.chat_evaluation)
result_list.append(result)
is_chat = True
if 'loop' == variable['fields'][0]:
result = self.handle(variable, self.loop_evaluation)
result_list.append(result)
if is_chat:
self.workflow_manage.get_chat_info().set_chat_variable(self.workflow_manage.chat_context)
from application.flow.loop_workflow_manage import LoopWorkflowManage
if isinstance(self.workflow_manage, LoopWorkflowManage):
self.workflow_manage.parentWorkflowManage.get_chat_info().set_chat_variable(
self.workflow_manage.chat_context)
else:
self.workflow_manage.get_chat_info().set_chat_variable(self.workflow_manage.chat_context)
return NodeResult({'variable_list': variable_list, 'result_list': result_list}, {})
def get_reference_content(self, fields: List[str]):

View File

@ -187,6 +187,8 @@ class WorkflowManage:
is_result = False
if n.type == 'application-node':
is_result = True
if n.type == 'loop-node':
is_result = True
return {**n.properties.get('node_data'), 'form_data': start_node_data, 'node_data': start_node_data,
'child_node': self.child_node, 'is_result': is_result}
@ -194,6 +196,12 @@ class WorkflowManage:
get_node_params=get_node_params)
self.start_node.valid_args(
{**self.start_node.node_params, 'form_data': start_node_data}, self.start_node.workflow_params)
if self.start_node.type == 'loop-node':
loop_node_data = node_details.get('loop_node_data', {})
self.start_node.context['loop_node_data'] = loop_node_data
self.start_node.context['current_index'] = node_details.get('current_index')
self.start_node.context['current_item'] = node_details.get('current_item')
self.start_node.context['loop_answer_data'] = node_details.get('loop_answer_data', {})
if self.start_node.type == 'application-node':
application_node_dict = node_details.get('application_node_dict', {})
self.start_node.context['application_node_dict'] = application_node_dict
@ -395,7 +403,8 @@ class WorkflowManage:
'child_node': child_node,
'node_is_end': node_is_end,
'real_node_id': real_node_id,
'reasoning_content': reasoning_content})
'reasoning_content': reasoning_content,
'node_status': "SUCCESS"})
current_node.node_chunk.add_chunk(chunk)
chunk = (self.base_to_response
.to_stream_chunk_response(self.params['chat_id'],
@ -408,7 +417,8 @@ class WorkflowManage:
'view_type': view_type,
'child_node': child_node,
'real_node_id': real_node_id,
'reasoning_content': ''}))
'reasoning_content': '',
'node_status': "SUCCESS"}))
current_node.node_chunk.add_chunk(chunk)
else:
list(result)
@ -426,7 +436,8 @@ class WorkflowManage:
'node_type': current_node.type,
'view_type': current_node.view_type,
'child_node': {},
'real_node_id': real_node_id})
'real_node_id': real_node_id,
'node_status': 'ERROR'})
current_node.node_chunk.add_chunk(chunk)
current_node.get_write_error_context(e)
self.status = 500
@ -500,6 +511,10 @@ class WorkflowManage:
details_result[node.runtime_node_id] = details
return details_result
def get_record_answer_list(self):
answer_text_list = self.get_answer_text_list()
return reduce(lambda pre, _n: [*pre, *_n], answer_text_list, [])
def get_answer_text_list(self):
result = []
answer_list = reduce(lambda x, y: [*x, *y],
@ -546,10 +561,6 @@ class WorkflowManage:
return all([any([self.dependent_node(up_node_id, node) for node in self.node_context]) for up_node_id in
up_node_id_list])
def get_up_node_id_list(self, node_id):
up_node_id_list = [edge.sourceNodeId for edge in self.flow.edges if edge.targetNodeId == node_id]
return up_node_id_list
def get_next_node_list(self, current_node, current_node_result):
"""
获取下一个可执行节点列表
@ -634,6 +645,7 @@ class WorkflowManage:
chatLabel = f"chat.{field.get('value')}"
chatValue = f"context.get('chat').get('{field.get('value', '')}','')"
prompt = prompt.replace(chatLabel, chatValue)
return prompt
def generate_prompt(self, prompt: str):

View File

@ -16,7 +16,6 @@ from rest_framework.views import APIView
from application.api.application_api import ApplicationCreateAPI, ApplicationQueryAPI, ApplicationImportAPI, \
ApplicationExportAPI, ApplicationOperateAPI, ApplicationEditAPI, TextToSpeechAPI, SpeechToTextAPI, PlayDemoTextAPI
from application.flow.step_node.condition_node.compare import Compare
from application.models import Application
from application.serializers.application import ApplicationSerializer, Query, ApplicationOperateSerializer
from common import result

View File

@ -0,0 +1,27 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file LoopToResponse.py
@date2025/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}

View File

@ -0,0 +1,3 @@
<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.52084 8.90627H4.17709C2.01973 8.90627 0.270844 7.15738 0.270844 5.00002C0.270844 3.45589 1.16679 2.12102 2.4672 1.48691L2.9899 2.39226C2.00033 2.84349 1.31251 3.84143 1.31251 5.00002C1.31251 6.58208 2.59503 7.8646 4.17709 7.8646H6.52084V7.16511C6.52084 7.0141 6.64327 6.89167 6.79428 6.89167C6.86051 6.89167 6.9245 6.91571 6.97434 6.95933L8.369 8.17965C8.48265 8.2791 8.49416 8.45184 8.39472 8.56549C8.38673 8.57463 8.37813 8.58322 8.369 8.59122L6.97434 9.81154C6.86069 9.91099 6.68794 9.89947 6.5885 9.78582C6.54488 9.73597 6.52084 9.67199 6.52084 9.60576V8.90627ZM5.47918 2.13544V2.83493C5.47918 2.90117 5.45514 2.96515 5.41152 3.01499C5.31208 3.12865 5.13933 3.14016 5.02568 3.04072L3.63103 1.82039C3.62189 1.8124 3.6133 1.80381 3.6053 1.79467C3.50586 1.68102 3.51738 1.50827 3.63103 1.40883L5.02568 0.188504C5.07553 0.14489 5.13951 0.12085 5.20574 0.12085C5.35676 0.12085 5.47918 0.243272 5.47918 0.394287V1.09378H7.82293C9.98029 1.09378 11.7292 2.84267 11.7292 5.00003C11.7292 6.53292 10.8462 7.85958 9.56116 8.49918L9.03884 7.5945C10.013 7.13717 10.6875 6.14737 10.6875 5.00003C10.6875 3.41796 9.40499 2.13544 7.82293 2.13544H5.47918Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5178_28485)">
<path d="M0 10C0 5.28595 0 2.92893 1.46447 1.46447C2.92893 0 5.28595 0 10 0C14.714 0 17.0711 0 18.5355 1.46447C20 2.92893 20 5.28595 20 10C20 14.714 20 17.0711 18.5355 18.5355C17.0711 20 14.714 20 10 20C5.28595 20 2.92893 20 1.46447 18.5355C0 17.0711 0 14.714 0 10Z" fill="#34C724"/>
<path d="M15.7291 9.99992C15.7291 13.164 13.164 15.7291 9.99998 15.7291C6.83592 15.7291 4.27081 13.164 4.27081 9.99992C4.27081 6.83586 6.83592 4.27075 9.99998 4.27075C13.164 4.27075 15.7291 6.83586 15.7291 9.99992ZM14.6875 9.99992C14.6875 7.41112 12.5885 5.31242 9.99998 5.31242C7.41144 5.31242 5.31248 7.41112 5.31248 9.99992C5.31248 12.5887 7.41144 14.6874 9.99998 14.6874C12.5885 14.6874 14.6875 12.5887 14.6875 9.99992ZM8.43748 7.91659H11.5625C11.8489 7.91659 12.0833 8.14966 12.0833 8.43742V11.5624C12.0833 11.8502 11.8489 12.0833 11.5625 12.0833H8.43748C8.15102 12.0833 7.91665 11.8502 7.91665 11.5624V8.43742C7.91665 8.14966 8.15102 7.91659 8.43748 7.91659Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_5178_28485">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2,8 +2,8 @@
<div class="item-content mb-16 lighter">
<template v-for="(answer_text, index) in answer_text_list" :key="index">
<div class="avatar mr-8" v-if="showAvatar">
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px"/>
<LogoIcon v-else height="28px" width="28px"/>
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px" />
<LogoIcon v-else height="28px" width="28px" />
</div>
<div
class="content"
@ -75,12 +75,11 @@
</div>
</template>
<script setup lang="ts">
import {computed, onMounted} from 'vue'
import KnowledgeSourceComponent
from '@/components/ai-chat/component/knowledge-source-component/index.vue'
import { computed, onMounted } from 'vue'
import KnowledgeSourceComponent from '@/components/ai-chat/component/knowledge-source-component/index.vue'
import MdRenderer from '@/components/markdown/MdRenderer.vue'
import OperationButton from '@/components/ai-chat/component/operation-button/index.vue'
import {type chatType} from '@/api/type/application'
import { type chatType } from '@/api/type/application'
import bus from '@/bus'
const props = defineProps<{
@ -157,7 +156,7 @@ function showSource(row: any) {
}
const regenerationChart = (chat: chatType) => {
props.sendMessage(chat.problem_text, {re_chat: true})
props.sendMessage(chat.problem_text, { re_chat: true })
}
const stopChat = (chat: chatType) => {
props.chatManagement.stop(chat.id)

View File

@ -1,7 +1,7 @@
export enum SearchMode {
embedding = 'views.application.dialog.vectorSearch',
keywords = 'views.application.dialog.fullTextSearch',
blend = 'views.application.dialog.hybridSearch'
blend = 'views.application.dialog.hybridSearch',
}
export enum WorkflowType {
@ -26,6 +26,16 @@ export enum WorkflowType {
McpNode = 'mcp-node',
IntentNode = 'intent-node',
TextToVideoGenerateNode = 'text-to-video-node',
ImageToVideoGenerateNode = 'image-to-video-node'
ImageToVideoGenerateNode = 'image-to-video-node',
LoopNode = 'loop-node',
LoopBodyNode = 'loop-body-node',
LoopStartNode = 'loop-start-node',
LoopContinueNode = 'loop-continue-node',
LoopBreakNode = 'loop-break-node',
}
export enum WorkflowMode {
// 应用工作流
Application = 'application',
// 应用工作流循环
ApplicationLoop = 'application-loop',
}

View File

@ -58,6 +58,7 @@ export default {
ReferencingError: '引用变量错误',
NoReferencing: '不存在的引用变量',
placeholder: '请选择变量',
loop: '循环变量',
},
condition: {
title: '执行条件',

View File

@ -119,8 +119,8 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { menuNodes, toolLibNode, applicationNode } from '@/workflow/common/data'
import { ref, onMounted, computed, inject } from 'vue'
import { getMenuNodes, toolLibNode, applicationNode } from '@/workflow/common/data'
import { iconComponent } from '@/workflow/icons/utils'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { isWorkFlow } from '@/utils/application'
@ -129,9 +129,12 @@ import NodeContent from './NodeContent.vue'
import { SourceTypeEnum } from '@/enums/common'
import permissionMap from '@/permission'
import { useRoute } from 'vue-router'
import { WorkflowMode } from '@/enums/application'
const workflowModel = inject('workflowMode') as WorkflowMode
const route = useRoute()
const { user, folder } = useStore()
const menuNodes = getMenuNodes(workflowModel || WorkflowMode.Application)
const search_text = ref<string>('')
const props = defineProps({
show: {
@ -162,10 +165,10 @@ const loading = ref(false)
const activeName = ref('base')
const filter_menu_nodes = computed(() => {
if (!search_text.value) return menuNodes
if (!search_text.value) return menuNodes || []
const searchTerm = search_text.value.toLowerCase()
return menuNodes.reduce((result: any[], item) => {
return (menuNodes || []).reduce((result: any[], item) => {
const filteredList = item.list.filter((listItem) =>
listItem.label.toLowerCase().includes(searchTerm),
)
@ -254,7 +257,7 @@ async function getToolList() {
systemType: 'workspace',
}).getToolList({
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
tool_type: 'CUSTOM'
tool_type: 'CUSTOM',
})
toolList.value = res.data?.tools || res.data || []
toolList.value = toolList.value?.filter((item: any) => item.is_active)

View File

@ -19,15 +19,17 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject } from 'vue'
import { iconComponent } from '../icons/utils'
import { t } from '@/locales'
import { WorkflowMode } from '@/enums/application'
const props = defineProps<{
nodeModel: any
modelValue: Array<any>
global?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const workflowMode = inject('workflowMode') as WorkflowMode
const data = computed({
set: (value) => {
emit('update:modelValue', value)
@ -50,20 +52,12 @@ const wheel = (e: any) => {
function visibleChange(bool: boolean) {
if (bool) {
options.value = props.global
? props.nodeModel
.get_up_node_field_list(false, true)
.filter(
(v: any) => ['global', 'chat'].includes(v.value) && v.children && v.children.length > 0,
)
: props.nodeModel
.get_up_node_field_list(false, true)
.filter((v: any) => v.children && v.children.length > 0)
initOptions()
}
}
const validate = () => {
const incomingNodeValue = props.nodeModel.get_up_node_field_list(false, true)
const incomingNodeValue = getOptionsValue()
if (!data.value || data.value.length === 0) {
return Promise.reject(t('views.applicationWorkflow.variable.ReferencingRequired'))
}
@ -83,15 +77,44 @@ const validate = () => {
}
return Promise.resolve('')
}
const get_up_node_field_list = (contain_self: boolean, use_cache: boolean) => {
const result = props.nodeModel.get_up_node_field_list(contain_self, use_cache)
if (props.nodeModel.graphModel.get_up_node_field_list) {
const _u = props.nodeModel.graphModel.get_up_node_field_list(contain_self, use_cache)
_u.forEach((item: any) => {
result.push(item)
})
}
return result.filter((v: any) => v.children && v.children.length > 0)
}
const getOptionsValue = () => {
if (workflowMode == WorkflowMode.ApplicationLoop) {
return props.global
? get_up_node_field_list(false, true).filter(
(v: any) =>
['global', 'chat', 'loop'].includes(v.value) && v.children && v.children.length > 0,
)
: get_up_node_field_list(false, true).filter((v: any) => v.children && v.children.length > 0)
} else {
return props.global
? props.nodeModel
.get_up_node_field_list(false, true)
.filter(
(v: any) => ['global', 'chat'].includes(v.value) && v.children && v.children.length > 0,
)
: props.nodeModel
.get_up_node_field_list(false, true)
.filter((v: any) => v.children && v.children.length > 0)
}
}
const initOptions = () => {
options.value = getOptionsValue()
}
defineExpose({ validate })
onMounted(() => {
options.value = props.global
? props.nodeModel
.get_up_node_field_list(false, true)
.filter(
(v: any) => ['global', 'chat'].includes(v.value) && v.children && v.children.length > 0,
)
: props.nodeModel.get_up_node_field_list(false, true)
initOptions()
})
</script>
<style scoped></style>

View File

@ -334,7 +334,9 @@ const nodeFields = computed(() => {
})
function showOperate(type: string) {
return type !== WorkflowType.Base && type !== WorkflowType.Start
return ![WorkflowType.Start, WorkflowType.Base, WorkflowType.LoopStartNode.toString()].includes(
type,
)
}
const openNodeMenu = (anchorValue: any) => {
showAnchor.value = true

View File

@ -18,7 +18,6 @@ const getNodeName = (nodes: Array<any>, baseName: string) => {
while (true) {
if (index > 0) {
name = baseName + index
console.log(name)
}
if (!nodes.some((node: any) => node.properties.stepName === name.trim())) {
return name
@ -85,7 +84,7 @@ class AppNode extends HtmlResize.view {
if (!this.up_node_field_dict || !use_cache) {
const up_node_list = this.props.graphModel.getNodeIncomingNode(this.props.model.id)
this.up_node_field_dict = up_node_list
.filter((node) => node.id != 'start-node')
.filter((node) => node.id != 'start-node' && node.id != 'loop-start-node')
.map((node) => node.get_up_node_field_dict(true, use_cache))
.reduce((pre, next) => ({ ...pre, ...next }), {})
}
@ -103,9 +102,10 @@ class AppNode extends HtmlResize.view {
(pre, next) => [...pre, ...next],
[],
)
const start_node_field_list = this.props.graphModel
.getNodeModelById('start-node')
.get_node_field_list()
const start_node_field_list = (
this.props.graphModel.getNodeModelById('start-node') ||
this.props.graphModel.getNodeModelById('loop-start-node')
).get_node_field_list()
return [...start_node_field_list, ...result]
}
@ -219,7 +219,15 @@ class AppNode extends HtmlResize.view {
if (root) {
if (isActive()) {
connect(this.targetId(), this.component, root, model, graphModel)
connect(
this.targetId(),
this.component,
root,
model,
graphModel,
undefined,
this.props.graphModel.get_provide,
)
} else {
this.r = h(this.component, {
properties: this.props.model.properties,
@ -395,7 +403,7 @@ class AppNodeModel extends HtmlResize.model {
const anchors: any = []
if (this.type !== WorkflowType.Base) {
if (this.type !== WorkflowType.Start) {
if (![WorkflowType.Start, WorkflowType.LoopStartNode.toString()].includes(this.type)) {
anchors.push({
x: x - width / 2 + 10,
y: showNode ? y : y - 15,

View File

@ -1,4 +1,4 @@
import { WorkflowType } from '@/enums/application'
import { WorkflowType, WorkflowMode } from '@/enums/application'
import { t } from '@/locales'
export const startNode = {
@ -360,6 +360,7 @@ export const toolNode = {
},
},
}
export const intentNode = {
type: WorkflowType.IntentNode,
text: t('views.applicationWorkflow.nodes.intentNode.label'),
@ -373,7 +374,7 @@ export const intentNode = {
label: t('common.classify'),
value: 'category',
},
{
{
label: t('common.reason'),
value: 'reason',
},
@ -382,6 +383,67 @@ export const intentNode = {
},
}
export const loopStartNode = {
id: WorkflowType.LoopStartNode,
type: WorkflowType.LoopStartNode,
x: 480,
y: 3340,
properties: {
height: 364,
stepName: t('views.applicationWorkflow.nodes.loopStartNode.label', '循环开始'),
config: {
fields: [
{
label: t('views.applicationWorkflow.nodes.startNode.index', '下标'),
value: 'index',
},
{
label: t('views.applicationWorkflow.nodes.startNode.item', '循环元素'),
value: 'item',
},
],
globalFields: [],
},
showNode: true,
},
}
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: 'loop-start-node',
type: 'loop-start-node',
properties: {
config: {
fields: [],
globalFields: [],
},
fields: [],
height: 361.333,
showNode: true,
stepName: '开始',
globalFields: [],
},
},
],
},
config: {
fields: [],
},
},
}
export const imageToVideoNode = {
type: WorkflowType.ImageToVideoGenerateNode,
text: t('views.applicationWorkflow.nodes.imageToVideoGenerate.text'),
@ -400,6 +462,33 @@ export const imageToVideoNode = {
},
}
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 loopContinueNode = {
type: WorkflowType.LoopContinueNode,
text: t('views.applicationWorkflow.nodes.continueNode.text', '跳过'),
label: t('views.applicationWorkflow.nodes.continueNode.label', '跳过'),
height: 600,
properties: {
width: 500,
stepName: t('views.applicationWorkflow.nodes.continueNode.label', '跳过'),
config: {
fields: [],
},
},
}
export const textToVideoNode = {
type: WorkflowType.TextToVideoGenerateNode,
text: t('views.applicationWorkflow.nodes.textToVideoGenerate.text'),
@ -417,6 +506,20 @@ export const textToVideoNode = {
},
},
}
export const loopBreakNode = {
type: WorkflowType.LoopBreakNode,
text: t('views.applicationWorkflow.nodes.breakNode.text', '退出循环'),
label: t('views.applicationWorkflow.nodes.breakNode.label', '退出循环'),
height: 600,
properties: {
stepName: t('views.applicationWorkflow.nodes.breakNode.label', '退出循环'),
config: {
fields: [],
},
},
}
export const menuNodes = [
{
label: t('views.applicationWorkflow.nodes.classify.aiCapability'),
@ -429,13 +532,35 @@ export const menuNodes = [
textToSpeechNode,
speechToTextNode,
textToVideoNode,
imageToVideoNode
imageToVideoNode,
],
},
{ label: t('views.knowledge.title'), list: [searchKnowledgeNode, rerankerNode] },
{
label: t('views.applicationWorkflow.nodes.classify.businessLogic'),
list: [conditionNode, formNode, variableAssignNode, replyNode],
list: [loopNode, conditionNode, formNode, variableAssignNode, replyNode],
},
{
label: t('views.applicationWorkflow.nodes.classify.other'),
list: [mcpNode, documentExtractNode, toolNode],
},
]
export const applicationLoopMenuNodes = [
{
label: t('views.applicationWorkflow.nodes.classify.aiCapability'),
list: [
aiChatNode,
questionNode,
imageGenerateNode,
imageUnderstandNode,
textToSpeechNode,
speechToTextNode,
],
},
{ label: t('views.knowledge.title'), list: [searchKnowledgeNode, rerankerNode] },
{
label: t('views.applicationWorkflow.nodes.classify.businessLogic'),
list: [loopBreakNode, loopContinueNode, conditionNode, formNode, variableAssignNode, replyNode],
},
{
label: t('views.applicationWorkflow.nodes.classify.other'),
@ -443,6 +568,14 @@ export const menuNodes = [
},
]
export const getMenuNodes = (workflowMode: WorkflowMode) => {
if (workflowMode == WorkflowMode.Application) {
return menuNodes
}
if (workflowMode == WorkflowMode.ApplicationLoop) {
return applicationLoopMenuNodes
}
}
/**
*
@ -525,6 +658,11 @@ export const nodeDict: any = {
[WorkflowType.TextToVideoGenerateNode]: textToVideoNode,
[WorkflowType.ImageToVideoGenerateNode]: imageToVideoNode,
[WorkflowType.IntentNode]: intentNode,
[WorkflowType.LoopNode]: loopNode,
[WorkflowType.LoopBodyNode]: loopBodyNode,
[WorkflowType.LoopStartNode]: loopStartNode,
[WorkflowType.LoopBreakNode]: loopBodyNode,
[WorkflowType.LoopContinueNode]: loopContinueNode,
}
export function isWorkFlow(type: string | undefined) {
return type === 'WORK_FLOW'

View File

@ -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
}

View File

@ -43,7 +43,7 @@ let CHILDREN_TRANSLATION_DISTANCE = 40
export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
const { keyboard } = lf
const {
options: { keyboard: keyboardOptions }
options: { keyboard: keyboardOptions },
} = keyboard
const copy_node = () => {
CHILDREN_TRANSLATION_DISTANCE = TRANSLATION_DISTANCE
@ -57,7 +57,7 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
return true
}
const base_nodes = elements.nodes.filter(
(node: any) => node.type === WorkflowType.Start || node.type === WorkflowType.Base
(node: any) => node.type === WorkflowType.Start || node.type === WorkflowType.Base,
)
if (base_nodes.length > 0) {
MsgError(base_nodes[0]?.properties?.stepName + t('views.applicationWorkflow.tip.cannotCopy'))
@ -91,23 +91,39 @@ 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
.filter((edge) => !['loop-edge'].includes(edge.type || ''))
.forEach((edge: any) => 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'), {
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger'
confirmButtonClass: 'danger',
}).then(() => {
if (!keyboardOptions?.enabled) return true
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

View File

@ -10,22 +10,26 @@ export function connect(
container: HTMLDivElement,
node: BaseNodeModel | BaseEdgeModel,
graph: GraphModel,
get_props?: any
get_props?: any,
get_provide?: any,
) {
if (!get_props) {
get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
return { nodeModel: node, graph }
}
}
if (!get_provide) {
get_provide = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => ({
getNode: () => node,
getGraph: () => graph,
})
}
if (active) {
items[id] = markRaw(
defineComponent({
render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
provide: () => ({
getNode: () => node,
getGraph: () => graph
})
})
provide: () => get_provide(node, graph),
}),
)
}
}
@ -50,8 +54,8 @@ export function getTeleport(): any {
props: {
flowId: {
type: String,
required: true
}
required: true,
},
},
setup(props) {
return () => {
@ -65,16 +69,15 @@ 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])
}
children.push(items[id])
})
return h(
Fragment,
{},
children.map((item) => h(item))
children.map((item) => h(item)),
)
}
}
},
})
}

View File

@ -10,9 +10,11 @@ const end_nodes: Array<string> = [
WorkflowType.Application,
WorkflowType.SpeechToTextNode,
WorkflowType.TextToSpeechNode,
WorkflowType.ImageGenerateNode,
WorkflowType.ImageGenerateNode,
WorkflowType.ImageToVideoGenerateNode,
WorkflowType.TextToVideoGenerateNode,
WorkflowType.ImageGenerateNode,
WorkflowType.LoopBodyNode,
]
export class WorkFlowInstance {
nodes

View File

@ -0,0 +1,6 @@
<template>
<el-avatar class="avatar-gradient" shape="square">
<img src="@/assets/workflow/icon_loop_break.svg" style="width: 100%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<el-avatar class="avatar-gradient" shape="square">
<img src="@/assets/workflow/icon_loop_break.svg" style="width: 100%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,4 @@
<template>
<img src="@/assets/workflow/icon_chat_color.svg" style="width: 18px" alt="" />
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<el-avatar class="avatar-gradient" shape="square">
<img src="@/assets/workflow/icon_loop.svg" style="width: 75%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<el-avatar shape="square" style="background: #D136D1;">
<img src="@/assets/workflow/icon_start.svg" style="width: 75%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -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'
@ -35,22 +36,6 @@ const props = defineProps({
data: Object || null,
})
const defaultData = {
nodes: [...baseNodes],
}
const graphData = computed({
get: () => {
if (props.data) {
return props.data
} else {
return defaultData
}
},
set: (value) => {
return value
},
})
const lf = ref()
onMounted(() => {
renderGraphData()
@ -82,6 +67,7 @@ const renderGraphData = (data?: any) => {
},
isSilentMode: false,
container: container,
saa: 'sssssss',
})
lf.value.setTheme({
bezier: {
@ -89,11 +75,16 @@ const renderGraphData = (data?: any) => {
strokeWidth: 1,
},
})
lf.value.graphModel.get = 'sdasdaad'
lf.value.on('graph:rendered', () => {
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 +108,17 @@ 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.forEach((node: any) => {
if (node.type === 'loop-body-node') {
const node_model = lf.value.getNodeModelById(node.id)
node_model.set_loop_body()
}
})
const _graph_data = lf.value.getGraphData()
_graph_data.nodes = _graph_data.nodes.filter((node: any) => node.type !== 'loop-body-node')
_graph_data.edges = graph_data.edges.filter((node: any) => node.type !== 'loop-edge')
return _graph_data
}
const onmousedown = (shapeItem: ShapeItem) => {

View File

@ -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; background: #fff"
>
<div v-resize="resizeStepContainer">
<div class="flex-between">
<div class="flex align-center" style="width: 600px">
<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, provide } from 'vue'
import { set } from 'lodash'
import { iconComponent } from '../../icons/utils'
import { copyClick } from '@/utils/clipboard'
import { ElMessage } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { t } from '@/locales'
import { WorkflowMode } from '@/enums/application'
provide('workflowMode', WorkflowMode.ApplicationLoop)
const height = ref<{
stepContainerHeight: number
inputContainerHeight: number
outputContainerHeight: number
}>({
stepContainerHeight: 0,
inputContainerHeight: 0,
outputContainerHeight: 0,
})
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 {
ElMessage.error(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>

View File

@ -0,0 +1,44 @@
import { WorkflowMode } from './../../../enums/application'
import LoopNode from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class LoopBodyNodeView extends AppNode {
constructor(props: any) {
super(props, LoopNode)
}
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)
}
}
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,
}

View File

@ -0,0 +1,107 @@
<template>
<LoopBodyContainer :nodeModel="nodeModel">
<div ref="containerRef" @wheel.stop style="height: 550px"></div>
</LoopBodyContainer>
</template>
<script setup lang="ts">
import { set, cloneDeep } 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'
import { WorkflowMode } from '@/enums/application'
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 refresh_loop_fields = (fields: Array<any>) => {
const loop_node_id = props.nodeModel.properties.loop_node_id
const loop_node = props.nodeModel.graphModel.getNodeModelById(loop_node_id)
loop_node.properties.config.fields = fields
}
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.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_up_node_field_list = props.nodeModel.get_up_node_field_list
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)
})
setTimeout(() => {
lf.value?.fitView()
}, 500)
}
}
onMounted(() => {
renderGraphData(cloneDeep(props.nodeModel.properties.workflow))
set(props.nodeModel, 'validate', validate)
set(props.nodeModel, 'set_loop_body', set_loop_body)
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,13 @@
import BreakNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class BreakNode extends AppNode {
constructor(props: any) {
super(props, BreakNodeVue)
}
}
export default {
type: 'loop-break-node',
model: AppNodeModel,
view: BreakNode,
}

View File

@ -0,0 +1,167 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<el-form
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="ContinueFromRef"
@submit.prevent
>
<div class="handle flex-between lighter">
<div class="info" v-if="form_data.condition_list.length > 1">
<span>{{ $t('views.applicationWorkflow.nodes.conditionNode.conditions.info') }}</span>
<el-select
:teleported="false"
v-model="form_data.condition"
size="small"
style="width: 60px; margin: 0 8px"
>
<el-option :label="$t('views.applicationWorkflow.condition.AND')" value="and" />
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="or" />
</el-select>
<span>{{ $t('views.applicationWorkflow.nodes.conditionNode.conditions.label') }}</span>
</div>
</div>
<template v-for="(condition, index) in form_data.condition_list" :key="index">
<el-row :gutter="8">
<el-col :span="11">
<el-form-item
:prop="'condition_list.' + index + '.field'"
:rules="{
type: 'array',
required: true,
message: $t('views.applicationWorkflow.variable.placeholder'),
trigger: 'change',
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="condition.field"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
:prop="'condition_list.' + index + '.compare'"
:rules="{
required: true,
message: $t(
'views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage',
),
trigger: 'change',
}"
>
<el-select
@wheel="wheel"
:teleported="false"
v-model="condition.compare"
:placeholder="
$t('views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage')
"
clearable
>
<template v-for="(item, index) in compareList" :key="index">
<el-option :label="item.label" :value="item.value" />
</template>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
v-if="
!['is_null', 'is_not_null', 'is_true', 'is_not_true'].includes(condition.compare)
"
:prop="'condition_list.' + index + '.value'"
:rules="{
required: true,
message: $t('views.applicationWorkflow.nodes.conditionNode.valueMessage'),
trigger: 'blur',
}"
>
<el-input
v-model="condition.value"
:placeholder="$t('views.applicationWorkflow.nodes.conditionNode.valueMessage')"
/>
</el-form-item>
</el-col>
<el-col :span="1">
<el-button link type="info" class="mt-4" @click="deleteCondition(index)">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</el-col>
</el-row> </template
></el-form>
<el-button link type="primary" @click="addCondition()">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ $t('views.applicationWorkflow.nodes.conditionNode.addCondition') }}
</el-button>
</NodeContainer>
</template>
<script setup lang="ts">
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { computed, ref, onMounted } from 'vue'
import { set, cloneDeep } from 'lodash'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { compareList } from '@/workflow/common/data'
import type { FormInstance } from 'element-plus'
const props = defineProps<{ nodeModel: any }>()
const form = {
condition_list: [],
condition: 'and',
}
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
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 addCondition = () => {
const condition_list = cloneDeep(form_data.value?.condition_list || [])
condition_list.push({
field: [],
compare: '',
value: '',
})
set(props.nodeModel.properties.node_data, 'condition_list', condition_list)
}
const deleteCondition = (index: number) => {
const condition_list = cloneDeep(form_data.value?.condition_list || [])
condition_list.splice(index, 1)
set(props.nodeModel.properties.node_data, 'condition_list', condition_list)
}
const ContinueFromRef = ref<FormInstance>()
const validate = () => {
const v_list = [ContinueFromRef.value?.validate()]
return Promise.all(v_list).catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
onMounted(() => {
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,13 @@
import ContinueNodeNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class ContinueNode extends AppNode {
constructor(props: any) {
super(props, ContinueNodeNodeVue)
}
}
export default {
type: 'loop-continue-node',
model: AppNodeModel,
view: ContinueNode,
}

View File

@ -0,0 +1,167 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<el-form
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="ContinueFromRef"
@submit.prevent
>
<div class="handle flex-between lighter">
<div class="info" v-if="form_data.condition_list.length > 1">
<span>{{ $t('views.applicationWorkflow.nodes.conditionNode.conditions.info') }}</span>
<el-select
:teleported="false"
v-model="form_data.condition"
size="small"
style="width: 60px; margin: 0 8px"
>
<el-option :label="$t('views.applicationWorkflow.condition.AND')" value="and" />
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="or" />
</el-select>
<span>{{ $t('views.applicationWorkflow.nodes.conditionNode.conditions.label') }}</span>
</div>
</div>
<template v-for="(condition, index) in form_data.condition_list" :key="index">
<el-row :gutter="8">
<el-col :span="11">
<el-form-item
:prop="'condition_list.' + index + '.field'"
:rules="{
type: 'array',
required: true,
message: $t('views.applicationWorkflow.variable.placeholder'),
trigger: 'change',
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="condition.field"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
:prop="'condition_list.' + index + '.compare'"
:rules="{
required: true,
message: $t(
'views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage',
),
trigger: 'change',
}"
>
<el-select
@wheel="wheel"
:teleported="false"
v-model="condition.compare"
:placeholder="
$t('views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage')
"
clearable
>
<template v-for="(item, index) in compareList" :key="index">
<el-option :label="item.label" :value="item.value" />
</template>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
v-if="
!['is_null', 'is_not_null', 'is_true', 'is_not_true'].includes(condition.compare)
"
:prop="'condition_list.' + index + '.value'"
:rules="{
required: true,
message: $t('views.applicationWorkflow.nodes.conditionNode.valueMessage'),
trigger: 'blur',
}"
>
<el-input
v-model="condition.value"
:placeholder="$t('views.applicationWorkflow.nodes.conditionNode.valueMessage')"
/>
</el-form-item>
</el-col>
<el-col :span="1">
<el-button link type="info" class="mt-4" @click="deleteCondition(index)">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</el-col>
</el-row> </template
></el-form>
<el-button link type="primary" @click="addCondition()">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ $t('views.applicationWorkflow.nodes.conditionNode.addCondition') }}
</el-button>
</NodeContainer>
</template>
<script setup lang="ts">
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { computed, ref, onMounted } from 'vue'
import { set, cloneDeep } from 'lodash'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { compareList } from '@/workflow/common/data'
import type { FormInstance } from 'element-plus'
const props = defineProps<{ nodeModel: any }>()
const form = {
condition_list: [],
condition: 'and',
}
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
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 addCondition = () => {
const condition_list = cloneDeep(form_data.value?.condition_list || [])
condition_list.push({
field: [],
compare: '',
value: '',
})
set(props.nodeModel.properties.node_data, 'condition_list', condition_list)
}
const deleteCondition = (index: number) => {
const condition_list = cloneDeep(form_data.value?.condition_list || [])
condition_list.splice(index, 1)
set(props.nodeModel.properties.node_data, 'condition_list', condition_list)
}
const ContinueFromRef = ref<FormInstance>()
const validate = () => {
const v_list = [ContinueFromRef.value?.validate()]
return Promise.all(v_list).catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
onMounted(() => {
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,56 @@
import LoopNode from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
import { WorkflowType } from '@/enums/application'
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,
}

View File

@ -0,0 +1,158 @@
<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, loopStartNode } 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)) {
let workflow = { nodes: [loopStartNode], edges: [] }
if (props.nodeModel.properties.node_data.loop_body) {
workflow = props.nodeModel.properties.node_data.loop_body
}
const nodeModel = props.nodeModel.graphModel.addNode({
type: loopBodyNode.type,
properties: {
...loopBodyNode.properties,
workflow: workflow,
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>

View File

@ -0,0 +1,118 @@
<template>
<el-dialog
:title="isEdit ? $t('common.param.editParam') : $t('common.param.addParam')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item
:label="$t('dynamicsForm.paramForm.field.label')"
:required="true"
prop="field"
:rules="rules.field"
>
<el-input
v-model="form.field"
:maxlength="64"
:placeholder="$t('dynamicsForm.paramForm.field.placeholder')"
show-word-limit
/>
</el-form-item>
<el-form-item
:label="$t('dynamicsForm.paramForm.name.label')"
:required="true"
prop="label"
:rules="rules.label"
>
<el-input
v-model="form.label"
:maxlength="64"
show-word-limit
:placeholder="$t('dynamicsForm.paramForm.name.placeholder')"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const currentIndex = ref(null)
const form = ref<any>({
field: '',
label: '',
})
const rules = reactive({
label: [
{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' },
],
field: [
{ required: true, message: t('dynamicsForm.paramForm.field.requiredMessage'), trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: t('dynamicsForm.paramForm.field.requiredMessage2'),
trigger: 'blur',
},
],
})
const dialogVisible = ref<boolean>(false)
const open = (row: any, index?: any) => {
if (row) {
form.value = cloneDeep(row)
isEdit.value = true
currentIndex.value = index
}
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
isEdit.value = false
currentIndex.value = null
form.value = {
field: '',
label: '',
}
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value, currentIndex.value)
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,103 @@
<template>
<div class="flex-between mb-16">
<h5 class="break-all ellipsis lighter" style="max-width: 80%">
{{ $t('views.applicationWorkflow.variable.loop', '循环变量') }}
</h5>
<div>
<span class="ml-4">
<el-button link type="primary" @click="openAddDialog()">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ $t('common.add') }}
</el-button>
</span>
</div>
</div>
<el-table
v-if="props.nodeModel.properties.loop_input_field_list?.length > 0"
:data="props.nodeModel.properties.loop_input_field_list"
class="mb-16"
ref="tableRef"
row-key="field"
>
<el-table-column prop="field" :label="$t('dynamicsForm.paramForm.field.label')" width="95">
<template #default="{ row }">
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
</template>
</el-table-column>
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
<template #default="{ row }">
<span>
<span :title="row.label" class="ellipsis-1">
{{ row.label }}
</span></span
>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
<AppIcon iconName="app-edit"></AppIcon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<LoopFieldDialog ref="ChatFieldDialogRef" @refresh="refreshFieldList"></LoopFieldDialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { set, cloneDeep } from 'lodash'
import LoopFieldDialog from './LoopFieldDialog.vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
const props = defineProps<{ nodeModel: any }>()
const tableRef = ref()
const ChatFieldDialogRef = ref()
const inputFieldList = ref<any[]>([])
function openAddDialog(data?: any, index?: any) {
ChatFieldDialogRef.value.open(data, index)
}
function deleteField(index: any) {
inputFieldList.value.splice(index, 1)
}
function refreshFieldList(data: any, index: any) {
for (let i = 0; i < inputFieldList.value.length; i++) {
if (inputFieldList.value[i].field === data.field && index !== i) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
return
}
}
if ([undefined, null].includes(index)) {
inputFieldList.value.push(data)
} else {
inputFieldList.value.splice(index, 1, data)
}
ChatFieldDialogRef.value.close()
}
onMounted(() => {
if (props.nodeModel.properties.loop_input_field_list) {
inputFieldList.value = cloneDeep(props.nodeModel.properties.loop_input_field_list)
}
set(props.nodeModel.properties, 'loop_input_field_list', inputFieldList)
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,42 @@
import LoopStartNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
import { t } from '@/locales'
class LoopStartNode extends AppNode {
constructor(props: any) {
super(props, LoopStartNodeVue)
}
get_node_field_list() {
const result = []
if (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
? this.props.model.properties.loop_input_field_list
: []
).map((i: any) => {
if (i.label && i.label.input_type === 'TooltipLabel') {
return { label: i.label.label, value: i.field || i.variable }
}
return { label: i.label || i.name, value: i.field || i.variable }
}) || [],
})
}
result.push({
value: this.props.model.id,
label: this.props.model.properties.stepName,
type: this.props.model.type,
children: this.props.model.properties?.config?.fields || [],
})
console.log(result)
return result
}
}
export default {
type: 'loop-start-node',
model: AppNodeModel,
view: LoopStartNode,
}

View File

@ -0,0 +1,59 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<LoopFieldTable :nodeModel="nodeModel"></LoopFieldTable>
<template v-if="loop_input_fields?.length">
<h5 class="title-decoration-1 mb-8">
{{ $t('views.applicationWorkflow.variable.loop', '循环变量') }}
</h5>
<div
v-for="(item, index) in loop_input_fields || []"
:key="index"
class="flex-between border-r-6 p-8-12 mb-8 layout-bg lighter"
@mouseenter="showicon = true"
@mouseleave="showicon = false"
>
<span class="break-all">{{ item.label }} {{ '{' + item.value + '}' }}</span>
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.setting.copyParam')"
placement="top"
v-if="showicon === true"
>
<el-button link @click="copyClick(`{{loop.${item.value}}}`)" style="padding: 0">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
</div>
</template>
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import LoopFieldTable from '@/workflow/nodes/loop-start-node/component/LoopFieldTable.vue'
import { ref, onMounted, computed, watch } from 'vue'
import { copyClick } from '@/utils/clipboard'
const props = defineProps<{ nodeModel: any }>()
const loop_input_fields = computed(() => {
return (
props.nodeModel.properties.loop_input_field_list
? props.nodeModel.properties.loop_input_field_list
: []
).map((i: any) => {
if (i.label && i.label.input_type === 'TooltipLabel') {
return { label: i.label.label, value: i.field || i.variable }
}
return { label: i.label || i.name, value: i.field || i.variable }
})
})
watch(loop_input_fields, () => {
props.nodeModel.graphModel.refresh_loop_fields(cloneDeep(loop_input_fields.value))
})
const showicon = ref(false)
onMounted(() => {
console.log(cloneDeep(loop_input_fields.value))
props.nodeModel.graphModel.refresh_loop_fields(cloneDeep(loop_input_fields.value))
})
</script>
<style lang="scss" scoped></style>