mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: loop node Unfinished
This commit is contained in:
parent
4cb39127be
commit
232dae1d88
|
|
@ -168,7 +168,7 @@ class INode:
|
||||||
self.runtime_node_id, self.context.get('reasoning_content', '') if reasoning_content_enable else '')]
|
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,
|
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.status = 200
|
||||||
self.err_message = ''
|
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,
|
self.runtime_node_id = sha1(uuid.NAMESPACE_DNS.bytes + bytes(str(uuid.uuid5(uuid.NAMESPACE_DNS,
|
||||||
"".join([*sorted(up_node_id_list),
|
"".join([*sorted(up_node_id_list),
|
||||||
node.id]))),
|
node.id]))),
|
||||||
"utf-8")).hexdigest()
|
"utf-8")).hexdigest() + (
|
||||||
|
"__" + str(salt) if salt is not None else '')
|
||||||
|
|
||||||
def valid_args(self, node_params, flow_params):
|
def valid_args(self, node_params, flow_params):
|
||||||
flow_params_serializer_class = self.get_flow_params_serializer_class()
|
flow_params_serializer_class = self.get_flow_params_serializer_class()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
# 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,
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in self.node_context:
|
||||||
|
context[node.id] = node.context
|
||||||
|
return context
|
||||||
|
|
||||||
|
def reset_prompt(self, prompt: str):
|
||||||
|
prompt = super().reset_prompt(prompt)
|
||||||
|
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
|
||||||
|
|
@ -14,6 +14,8 @@ from .document_extract_node import *
|
||||||
from .form_node import *
|
from .form_node import *
|
||||||
from .image_generate_step_node import *
|
from .image_generate_step_node import *
|
||||||
from .image_understand_step_node import *
|
from .image_understand_step_node import *
|
||||||
|
from .loop_node import *
|
||||||
|
from .loop_start_node import *
|
||||||
from .mcp_node import BaseMcpNode
|
from .mcp_node import BaseMcpNode
|
||||||
from .question_node import *
|
from .question_node import *
|
||||||
from .reranker_node import *
|
from .reranker_node import *
|
||||||
|
|
@ -30,7 +32,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuest
|
||||||
BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||||
BaseDocumentExtractNode,
|
BaseDocumentExtractNode,
|
||||||
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
||||||
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
|
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode, BaseLoopNode, BaseLoopStartStepNode]
|
||||||
|
|
||||||
|
|
||||||
def get_node(node_type):
|
def get_node(node_type):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
@project: MaxKB
|
||||||
|
@Author:虎
|
||||||
|
@file: __init__.py
|
||||||
|
@date:2025/3/11 18:24
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
from .impl import *
|
||||||
|
|
@ -0,0 +1,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,252 @@
|
||||||
|
# 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 loop_number(number: int, workflow_manage_new_instance, node: INode):
|
||||||
|
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 index in range(current_index, number):
|
||||||
|
"""
|
||||||
|
指定次数循环
|
||||||
|
@return:
|
||||||
|
"""
|
||||||
|
instance = workflow_manage_new_instance({'index': index, 'item': index}, 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:
|
||||||
|
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
|
||||||
|
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 loop_array(array, workflow_manage_new_instance, node: INode):
|
||||||
|
loop_global_data = {}
|
||||||
|
loop_execute_details = []
|
||||||
|
for item, index in zip(array, range(len(array))):
|
||||||
|
"""
|
||||||
|
指定次数循环
|
||||||
|
@return:
|
||||||
|
"""
|
||||||
|
instance = workflow_manage_new_instance({'index': index, 'item': item}, loop_global_data)
|
||||||
|
response = instance.stream()
|
||||||
|
for chunk in response:
|
||||||
|
yield chunk
|
||||||
|
node_type = chunk.get('node_type')
|
||||||
|
if node_type == 'form-node':
|
||||||
|
break
|
||||||
|
loop_global_data = instance.context
|
||||||
|
runtime_details = instance.get_runtime_details()
|
||||||
|
loop_execute_details.append(runtime_details)
|
||||||
|
node.context['loop_execute_details'] = loop_execute_details
|
||||||
|
|
||||||
|
|
||||||
|
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_array(array, node_variable['workflow_manage_new_instance'], node)
|
||||||
|
return loop_number(number, node_variable['workflow_manage_new_instance'], node)
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
@ -187,6 +187,8 @@ class WorkflowManage:
|
||||||
is_result = False
|
is_result = False
|
||||||
if n.type == 'application-node':
|
if n.type == 'application-node':
|
||||||
is_result = True
|
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,
|
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}
|
'child_node': self.child_node, 'is_result': is_result}
|
||||||
|
|
||||||
|
|
@ -194,6 +196,12 @@ class WorkflowManage:
|
||||||
get_node_params=get_node_params)
|
get_node_params=get_node_params)
|
||||||
self.start_node.valid_args(
|
self.start_node.valid_args(
|
||||||
{**self.start_node.node_params, 'form_data': start_node_data}, self.start_node.workflow_params)
|
{**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':
|
if self.start_node.type == 'application-node':
|
||||||
application_node_dict = node_details.get('application_node_dict', {})
|
application_node_dict = node_details.get('application_node_dict', {})
|
||||||
self.start_node.context['application_node_dict'] = application_node_dict
|
self.start_node.context['application_node_dict'] = application_node_dict
|
||||||
|
|
@ -500,6 +508,10 @@ class WorkflowManage:
|
||||||
details_result[node.runtime_node_id] = details
|
details_result[node.runtime_node_id] = details
|
||||||
return details_result
|
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):
|
def get_answer_text_list(self):
|
||||||
result = []
|
result = []
|
||||||
answer_list = reduce(lambda x, y: [*x, *y],
|
answer_list = reduce(lambda x, y: [*x, *y],
|
||||||
|
|
@ -546,10 +558,6 @@ class WorkflowManage:
|
||||||
return all([any([self.dependent_node(up_node_id, node) for node in self.node_context]) for up_node_id in
|
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])
|
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):
|
def get_next_node_list(self, current_node, current_node_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}
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
<div class="item-content mb-16 lighter">
|
<div class="item-content mb-16 lighter">
|
||||||
<template v-for="(answer_text, index) in answer_text_list" :key="index">
|
<template v-for="(answer_text, index) in answer_text_list" :key="index">
|
||||||
<div class="avatar mr-8" v-if="showAvatar">
|
<div class="avatar mr-8" v-if="showAvatar">
|
||||||
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px"/>
|
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px" />
|
||||||
<LogoIcon v-else height="28px" width="28px"/>
|
<LogoIcon v-else height="28px" width="28px" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="content"
|
||||||
|
|
@ -75,12 +75,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted} from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import KnowledgeSourceComponent
|
import KnowledgeSourceComponent from '@/components/ai-chat/component/knowledge-source-component/index.vue'
|
||||||
from '@/components/ai-chat/component/knowledge-source-component/index.vue'
|
|
||||||
import MdRenderer from '@/components/markdown/MdRenderer.vue'
|
import MdRenderer from '@/components/markdown/MdRenderer.vue'
|
||||||
import OperationButton from '@/components/ai-chat/component/operation-button/index.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'
|
import bus from '@/bus'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -157,7 +156,7 @@ function showSource(row: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const regenerationChart = (chat: chatType) => {
|
const regenerationChart = (chat: chatType) => {
|
||||||
props.sendMessage(chat.problem_text, {re_chat: true})
|
props.sendMessage(chat.problem_text, { re_chat: true })
|
||||||
}
|
}
|
||||||
const stopChat = (chat: chatType) => {
|
const stopChat = (chat: chatType) => {
|
||||||
props.chatManagement.stop(chat.id)
|
props.chatManagement.stop(chat.id)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export enum SearchMode {
|
export enum SearchMode {
|
||||||
embedding = 'views.application.dialog.vectorSearch',
|
embedding = 'views.application.dialog.vectorSearch',
|
||||||
keywords = 'views.application.dialog.fullTextSearch',
|
keywords = 'views.application.dialog.fullTextSearch',
|
||||||
blend = 'views.application.dialog.hybridSearch'
|
blend = 'views.application.dialog.hybridSearch',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WorkflowType {
|
export enum WorkflowType {
|
||||||
|
|
@ -24,4 +24,13 @@ export enum WorkflowType {
|
||||||
SpeechToTextNode = 'speech-to-text-node',
|
SpeechToTextNode = 'speech-to-text-node',
|
||||||
ImageGenerateNode = 'image-generate-node',
|
ImageGenerateNode = 'image-generate-node',
|
||||||
McpNode = 'mcp-node',
|
McpNode = 'mcp-node',
|
||||||
|
LoopNode = 'loop-node',
|
||||||
|
LoopBodyNode = 'loop-body-node',
|
||||||
|
LoopStartNode = 'loop-start-node',
|
||||||
|
}
|
||||||
|
export enum WorkflowMode {
|
||||||
|
// 应用工作流
|
||||||
|
Application = 'application',
|
||||||
|
// 应用工作流循环
|
||||||
|
ApplicationLoop = 'application-loop',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, inject } from 'vue'
|
||||||
import { menuNodes, toolLibNode, applicationNode } from '@/workflow/common/data'
|
import { getMenuNodes, toolLibNode, applicationNode } from '@/workflow/common/data'
|
||||||
import { iconComponent } from '@/workflow/icons/utils'
|
import { iconComponent } from '@/workflow/icons/utils'
|
||||||
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
|
||||||
import { isWorkFlow } from '@/utils/application'
|
import { isWorkFlow } from '@/utils/application'
|
||||||
|
|
@ -129,9 +129,12 @@ import NodeContent from './NodeContent.vue'
|
||||||
import { SourceTypeEnum } from '@/enums/common'
|
import { SourceTypeEnum } from '@/enums/common'
|
||||||
import permissionMap from '@/permission'
|
import permissionMap from '@/permission'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { WorkflowMode } from '@/enums/application'
|
||||||
|
const workflowModel = inject('workflowMode') as WorkflowMode
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { user, folder } = useStore()
|
const { user, folder } = useStore()
|
||||||
|
|
||||||
|
const menuNodes = getMenuNodes(workflowModel || WorkflowMode.Application)
|
||||||
const search_text = ref<string>('')
|
const search_text = ref<string>('')
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
|
|
@ -165,7 +168,7 @@ const filter_menu_nodes = computed(() => {
|
||||||
if (!search_text.value) return menuNodes
|
if (!search_text.value) return menuNodes
|
||||||
const searchTerm = search_text.value.toLowerCase()
|
const searchTerm = search_text.value.toLowerCase()
|
||||||
|
|
||||||
return menuNodes.reduce((result: any[], item) => {
|
return (menuNodes || []).reduce((result: any[], item) => {
|
||||||
const filteredList = item.list.filter((listItem) =>
|
const filteredList = item.list.filter((listItem) =>
|
||||||
listItem.label.toLowerCase().includes(searchTerm),
|
listItem.label.toLowerCase().includes(searchTerm),
|
||||||
)
|
)
|
||||||
|
|
@ -254,7 +257,7 @@ async function getToolList() {
|
||||||
systemType: 'workspace',
|
systemType: 'workspace',
|
||||||
}).getToolList({
|
}).getToolList({
|
||||||
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
|
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
|
||||||
tool_type: 'CUSTOM'
|
tool_type: 'CUSTOM',
|
||||||
})
|
})
|
||||||
toolList.value = res.data?.tools || res.data || []
|
toolList.value = res.data?.tools || res.data || []
|
||||||
toolList.value = toolList.value?.filter((item: any) => item.is_active)
|
toolList.value = toolList.value?.filter((item: any) => item.is_active)
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, inject } from 'vue'
|
||||||
import { iconComponent } from '../icons/utils'
|
import { iconComponent } from '../icons/utils'
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
|
import { WorkflowMode } from '@/enums/application'
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nodeModel: any
|
nodeModel: any
|
||||||
modelValue: Array<any>
|
modelValue: Array<any>
|
||||||
global?: boolean
|
global?: boolean
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const workflowMode = inject('workflowMode') as WorkflowMode
|
||||||
const data = computed({
|
const data = computed({
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
|
|
@ -50,15 +52,7 @@ const wheel = (e: any) => {
|
||||||
|
|
||||||
function visibleChange(bool: boolean) {
|
function visibleChange(bool: boolean) {
|
||||||
if (bool) {
|
if (bool) {
|
||||||
options.value = props.global
|
initOptions()
|
||||||
? 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,15 +77,40 @@ const validate = () => {
|
||||||
}
|
}
|
||||||
return Promise.resolve('')
|
return Promise.resolve('')
|
||||||
}
|
}
|
||||||
defineExpose({ validate })
|
|
||||||
onMounted(() => {
|
const get_up_node_field_list = (contain_self: boolean, use_cache: boolean) => {
|
||||||
options.value = props.global
|
const result = props.nodeModel.get_up_node_field_list(contain_self, use_cache)
|
||||||
? props.nodeModel
|
if (props.nodeModel.graphModel.get_up_node_field_list) {
|
||||||
.get_up_node_field_list(false, true)
|
const _u = props.nodeModel.graphModel.get_up_node_field_list(contain_self, use_cache)
|
||||||
.filter(
|
|
||||||
|
_u.forEach((item: any) => {
|
||||||
|
result.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result.filter((v: any) => v.children && v.children.length > 0)
|
||||||
|
}
|
||||||
|
const initOptions = () => {
|
||||||
|
if (workflowMode == WorkflowMode.ApplicationLoop) {
|
||||||
|
options.value = props.global
|
||||||
|
? get_up_node_field_list(false, true).filter(
|
||||||
(v: any) => ['global', 'chat'].includes(v.value) && v.children && v.children.length > 0,
|
(v: any) => ['global', 'chat'].includes(v.value) && v.children && v.children.length > 0,
|
||||||
)
|
)
|
||||||
: props.nodeModel.get_up_node_field_list(false, true)
|
: get_up_node_field_list(false, true).filter((v: any) => v.children && v.children.length > 0)
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ validate })
|
||||||
|
onMounted(() => {
|
||||||
|
initOptions()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,9 @@ const nodeFields = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function showOperate(type: string) {
|
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) => {
|
const openNodeMenu = (anchorValue: any) => {
|
||||||
showAnchor.value = true
|
showAnchor.value = true
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ const getNodeName = (nodes: Array<any>, baseName: string) => {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
name = baseName + index
|
name = baseName + index
|
||||||
console.log(name)
|
|
||||||
}
|
}
|
||||||
if (!nodes.some((node: any) => node.properties.stepName === name.trim())) {
|
if (!nodes.some((node: any) => node.properties.stepName === name.trim())) {
|
||||||
return name
|
return name
|
||||||
|
|
@ -85,7 +84,7 @@ class AppNode extends HtmlResize.view {
|
||||||
if (!this.up_node_field_dict || !use_cache) {
|
if (!this.up_node_field_dict || !use_cache) {
|
||||||
const up_node_list = this.props.graphModel.getNodeIncomingNode(this.props.model.id)
|
const up_node_list = this.props.graphModel.getNodeIncomingNode(this.props.model.id)
|
||||||
this.up_node_field_dict = up_node_list
|
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))
|
.map((node) => node.get_up_node_field_dict(true, use_cache))
|
||||||
.reduce((pre, next) => ({ ...pre, ...next }), {})
|
.reduce((pre, next) => ({ ...pre, ...next }), {})
|
||||||
}
|
}
|
||||||
|
|
@ -103,9 +102,10 @@ class AppNode extends HtmlResize.view {
|
||||||
(pre, next) => [...pre, ...next],
|
(pre, next) => [...pre, ...next],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const start_node_field_list = this.props.graphModel
|
const start_node_field_list = (
|
||||||
.getNodeModelById('start-node')
|
this.props.graphModel.getNodeModelById('start-node') ||
|
||||||
.get_node_field_list()
|
this.props.graphModel.getNodeModelById('loop-start-node')
|
||||||
|
).get_node_field_list()
|
||||||
return [...start_node_field_list, ...result]
|
return [...start_node_field_list, ...result]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +219,15 @@ class AppNode extends HtmlResize.view {
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
if (isActive()) {
|
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 {
|
} else {
|
||||||
this.r = h(this.component, {
|
this.r = h(this.component, {
|
||||||
properties: this.props.model.properties,
|
properties: this.props.model.properties,
|
||||||
|
|
@ -395,7 +403,7 @@ class AppNodeModel extends HtmlResize.model {
|
||||||
const anchors: any = []
|
const anchors: any = []
|
||||||
|
|
||||||
if (this.type !== WorkflowType.Base) {
|
if (this.type !== WorkflowType.Base) {
|
||||||
if (this.type !== WorkflowType.Start) {
|
if (![WorkflowType.Start, WorkflowType.LoopStartNode.toString()].includes(this.type)) {
|
||||||
anchors.push({
|
anchors.push({
|
||||||
x: x - width / 2 + 10,
|
x: x - width / 2 + 10,
|
||||||
y: showNode ? y : y - 15,
|
y: showNode ? y : y - 15,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { WorkflowType } from '@/enums/application'
|
import { WorkflowType, WorkflowMode } from '@/enums/application'
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
|
|
||||||
export const startNode = {
|
export const startNode = {
|
||||||
|
|
@ -360,7 +360,113 @@ export const toolNode = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
label: t('loop.item', '循环参数'),
|
||||||
|
value: 'item',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common.result'),
|
||||||
|
value: 'result',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loopBodyNode = {
|
||||||
|
type: WorkflowType.LoopBodyNode,
|
||||||
|
text: t('views.applicationWorkflow.nodes.loopBodyNode.text', '循环体'),
|
||||||
|
label: t('views.applicationWorkflow.nodes.loopBodyNode.label', '循环体'),
|
||||||
|
height: 600,
|
||||||
|
properties: {
|
||||||
|
width: 1800,
|
||||||
|
stepName: t('views.applicationWorkflow.nodes.loopBodyNode.label', '循环体'),
|
||||||
|
config: {
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const menuNodes = [
|
export const menuNodes = [
|
||||||
|
{
|
||||||
|
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: [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'),
|
label: t('views.applicationWorkflow.nodes.classify.aiCapability'),
|
||||||
list: [
|
list: [
|
||||||
|
|
@ -383,6 +489,14 @@ export const menuNodes = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const getMenuNodes = (workflowMode: WorkflowMode) => {
|
||||||
|
if (workflowMode == WorkflowMode.Application) {
|
||||||
|
return menuNodes
|
||||||
|
}
|
||||||
|
if (workflowMode == WorkflowMode.ApplicationLoop) {
|
||||||
|
return applicationLoopMenuNodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具配置数据
|
* 工具配置数据
|
||||||
|
|
@ -462,6 +576,9 @@ export const nodeDict: any = {
|
||||||
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
|
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
|
||||||
[WorkflowType.VariableAssignNode]: variableAssignNode,
|
[WorkflowType.VariableAssignNode]: variableAssignNode,
|
||||||
[WorkflowType.McpNode]: mcpNode,
|
[WorkflowType.McpNode]: mcpNode,
|
||||||
|
[WorkflowType.LoopNode]: loopNode,
|
||||||
|
[WorkflowType.LoopBodyNode]: loopBodyNode,
|
||||||
|
[WorkflowType.LoopStartNode]: loopStartNode,
|
||||||
}
|
}
|
||||||
export function isWorkFlow(type: string | undefined) {
|
export function isWorkFlow(type: string | undefined) {
|
||||||
return type === 'WORK_FLOW'
|
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) {
|
export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
|
||||||
const { keyboard } = lf
|
const { keyboard } = lf
|
||||||
const {
|
const {
|
||||||
options: { keyboard: keyboardOptions }
|
options: { keyboard: keyboardOptions },
|
||||||
} = keyboard
|
} = keyboard
|
||||||
const copy_node = () => {
|
const copy_node = () => {
|
||||||
CHILDREN_TRANSLATION_DISTANCE = TRANSLATION_DISTANCE
|
CHILDREN_TRANSLATION_DISTANCE = TRANSLATION_DISTANCE
|
||||||
|
|
@ -57,7 +57,7 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const base_nodes = elements.nodes.filter(
|
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) {
|
if (base_nodes.length > 0) {
|
||||||
MsgError(base_nodes[0]?.properties?.stepName + t('views.applicationWorkflow.tip.cannotCopy'))
|
MsgError(base_nodes[0]?.properties?.stepName + t('views.applicationWorkflow.tip.cannotCopy'))
|
||||||
|
|
@ -91,23 +91,39 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (elements.edges.length > 0 && elements.nodes.length == 0) {
|
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
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
|
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
|
||||||
confirmButtonText: t('common.confirm'),
|
confirmButtonText: t('common.confirm'),
|
||||||
confirmButtonClass: 'danger'
|
confirmButtonClass: 'danger',
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (!keyboardOptions?.enabled) return true
|
if (!keyboardOptions?.enabled) return true
|
||||||
if (graph.textEditElement) return true
|
if (graph.textEditElement) return true
|
||||||
|
|
||||||
elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
|
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
|
return false
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,26 @@ export function connect(
|
||||||
container: HTMLDivElement,
|
container: HTMLDivElement,
|
||||||
node: BaseNodeModel | BaseEdgeModel,
|
node: BaseNodeModel | BaseEdgeModel,
|
||||||
graph: GraphModel,
|
graph: GraphModel,
|
||||||
get_props?: any
|
get_props?: any,
|
||||||
|
get_provide?: any,
|
||||||
) {
|
) {
|
||||||
if (!get_props) {
|
if (!get_props) {
|
||||||
get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
|
get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
|
||||||
return { nodeModel: node, graph }
|
return { nodeModel: node, graph }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!get_provide) {
|
||||||
|
get_provide = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => ({
|
||||||
|
getNode: () => node,
|
||||||
|
getGraph: () => graph,
|
||||||
|
})
|
||||||
|
}
|
||||||
if (active) {
|
if (active) {
|
||||||
items[id] = markRaw(
|
items[id] = markRaw(
|
||||||
defineComponent({
|
defineComponent({
|
||||||
render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
|
render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
|
||||||
provide: () => ({
|
provide: () => get_provide(node, graph),
|
||||||
getNode: () => node,
|
}),
|
||||||
getGraph: () => graph
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,8 +54,8 @@ export function getTeleport(): any {
|
||||||
props: {
|
props: {
|
||||||
flowId: {
|
flowId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -65,16 +69,15 @@ export function getTeleport(): any {
|
||||||
|
|
||||||
// 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
|
// 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
|
||||||
// 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
|
// 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
|
||||||
if (id.startsWith(props.flowId)) {
|
|
||||||
children.push(items[id])
|
children.push(items[id])
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return h(
|
return h(
|
||||||
Fragment,
|
Fragment,
|
||||||
{},
|
{},
|
||||||
children.map((item) => h(item))
|
children.map((item) => h(item)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ const end_nodes: Array<string> = [
|
||||||
WorkflowType.Application,
|
WorkflowType.Application,
|
||||||
WorkflowType.SpeechToTextNode,
|
WorkflowType.SpeechToTextNode,
|
||||||
WorkflowType.TextToSpeechNode,
|
WorkflowType.TextToSpeechNode,
|
||||||
WorkflowType.ImageGenerateNode,
|
WorkflowType.ImageGenerateNode,
|
||||||
|
WorkflowType.LoopBodyNode,
|
||||||
]
|
]
|
||||||
export class WorkFlowInstance {
|
export class WorkFlowInstance {
|
||||||
nodes
|
nodes
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import LogicFlow from '@logicflow/core'
|
import LogicFlow from '@logicflow/core'
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import AppEdge from './common/edge'
|
import AppEdge from './common/edge'
|
||||||
|
import loopEdge from './common/loopEdge'
|
||||||
import Control from './common/NodeControl.vue'
|
import Control from './common/NodeControl.vue'
|
||||||
import { baseNodes } from '@/workflow/common/data'
|
import { baseNodes } from '@/workflow/common/data'
|
||||||
import '@logicflow/extension/lib/style/index.css'
|
import '@logicflow/extension/lib/style/index.css'
|
||||||
|
|
@ -35,22 +36,6 @@ const props = defineProps({
|
||||||
data: Object || null,
|
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()
|
const lf = ref()
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
renderGraphData()
|
renderGraphData()
|
||||||
|
|
@ -82,6 +67,7 @@ const renderGraphData = (data?: any) => {
|
||||||
},
|
},
|
||||||
isSilentMode: false,
|
isSilentMode: false,
|
||||||
container: container,
|
container: container,
|
||||||
|
saa: 'sssssss',
|
||||||
})
|
})
|
||||||
lf.value.setTheme({
|
lf.value.setTheme({
|
||||||
bezier: {
|
bezier: {
|
||||||
|
|
@ -89,11 +75,16 @@ const renderGraphData = (data?: any) => {
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
lf.value.graphModel.get = 'sdasdaad'
|
||||||
lf.value.on('graph:rendered', () => {
|
lf.value.on('graph:rendered', () => {
|
||||||
flowId.value = lf.value.graphModel.flowId
|
flowId.value = lf.value.graphModel.flowId
|
||||||
})
|
})
|
||||||
initDefaultShortcut(lf.value, lf.value.graphModel)
|
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.setDefaultEdgeType('app-edge')
|
||||||
|
|
||||||
lf.value.render(data ? data : {})
|
lf.value.render(data ? data : {})
|
||||||
|
|
@ -117,7 +108,17 @@ const validate = () => {
|
||||||
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
|
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
|
||||||
}
|
}
|
||||||
const getGraphData = () => {
|
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) => {
|
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,100 @@
|
||||||
|
<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 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.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,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,12 @@
|
||||||
|
import LoopStartNodeVue from './index.vue'
|
||||||
|
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||||
|
class LoopStartNode extends AppNode {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props, LoopStartNodeVue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
type: 'loop-start-node',
|
||||||
|
model: AppNodeModel,
|
||||||
|
view: LoopStartNode,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<NodeContainer :nodeModel="nodeModel"> </NodeContainer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cloneDeep, set } from 'lodash'
|
||||||
|
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
const props = defineProps<{ nodeModel: any }>()
|
||||||
|
onMounted(() => {})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
Loading…
Reference in New Issue