mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Support loop node (#4045)
This commit is contained in:
parent
e7ce9a0524
commit
7264545ab6
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: workflow_manage.py
|
||||
@date:2024/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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/9/15 12:08
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: i_loop_break_node.py
|
||||
@date:2025/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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/9/15 12:16
|
||||
@desc:
|
||||
"""
|
||||
from .base_loop_break_node import BaseLoopBreakNode
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: base_loop_break_node.py
|
||||
@date:2025/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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/9/15 12:08
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: i_loop_continue_node.py
|
||||
@date:2025/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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/9/15 12:13
|
||||
@desc:
|
||||
"""
|
||||
from .base_loop_continue_node import BaseLoopContinueNode
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: base_loop_continue_node.py
|
||||
@date:2025/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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2025/3/11 18:24
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_loop_node.py
|
||||
@date:2025/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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/3/11 18:24
|
||||
@desc:
|
||||
"""
|
||||
from .base_loop_node import BaseLoopNode
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_loop_node.py
|
||||
@date:2025/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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/6/11 15:30
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: i_start_node.py
|
||||
@date:2024/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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/6/11 15:36
|
||||
@desc:
|
||||
"""
|
||||
from .base_start_node import BaseLoopStartStepNode
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: maxkb
|
||||
@Author:虎
|
||||
@file: base_start_node.py
|
||||
@date:2024/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,
|
||||
}
|
||||
|
|
@ -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]):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: LoopToResponse.py
|
||||
@date:2025/3/12 17:21
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
|
||||
from common.handle.impl.response.system_to_response import SystemToResponse
|
||||
|
||||
|
||||
class LoopToResponse(SystemToResponse):
|
||||
|
||||
def to_stream_chunk_response(self, chat_id, chat_record_id, node_id, up_node_id_list, content, is_end,
|
||||
completion_tokens,
|
||||
prompt_tokens, other_params: dict = None):
|
||||
if other_params is None:
|
||||
other_params = {}
|
||||
return {'chat_id': str(chat_id), 'chat_record_id': str(chat_record_id), 'operate': True,
|
||||
'content': content, 'node_id': node_id, 'up_node_id_list': up_node_id_list,
|
||||
'is_end': is_end,
|
||||
'usage': {'completion_tokens': completion_tokens,
|
||||
'prompt_tokens': prompt_tokens,
|
||||
'total_tokens': completion_tokens + prompt_tokens},
|
||||
**other_params}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export default {
|
|||
ReferencingError: '引用变量错误',
|
||||
NoReferencing: '不存在的引用变量',
|
||||
placeholder: '请选择变量',
|
||||
loop: '循环变量',
|
||||
},
|
||||
condition: {
|
||||
title: '执行条件',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<template>
|
||||
<img src="@/assets/workflow/icon_chat_color.svg" style="width: 18px" alt="" />
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue