mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-25 17:22:55 +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 '')]
|
||||
|
||||
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,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 .image_generate_step_node import *
|
||||
from .image_understand_step_node import *
|
||||
from .loop_node import *
|
||||
from .loop_start_node import *
|
||||
from .mcp_node import BaseMcpNode
|
||||
from .question_node import *
|
||||
from .reranker_node import *
|
||||
|
|
@ -30,7 +32,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuest
|
|||
BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode,
|
||||
BaseDocumentExtractNode,
|
||||
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
|
||||
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
|
||||
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode, BaseLoopNode, BaseLoopStartStepNode]
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
|
|
@ -500,6 +508,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 +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
|
||||
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):
|
||||
"""
|
||||
获取下一个可执行节点列表
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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 {
|
||||
|
|
@ -24,4 +24,13 @@ export enum WorkflowType {
|
|||
SpeechToTextNode = 'speech-to-text-node',
|
||||
ImageGenerateNode = 'image-generate-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>
|
||||
</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: {
|
||||
|
|
@ -165,7 +168,7 @@ const filter_menu_nodes = computed(() => {
|
|||
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,15 +52,7 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,15 +77,40 @@ const validate = () => {
|
|||
}
|
||||
return Promise.resolve('')
|
||||
}
|
||||
defineExpose({ validate })
|
||||
onMounted(() => {
|
||||
options.value = props.global
|
||||
? props.nodeModel
|
||||
.get_up_node_field_list(false, true)
|
||||
.filter(
|
||||
|
||||
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 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,
|
||||
)
|
||||
: 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>
|
||||
<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,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 = [
|
||||
{
|
||||
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'),
|
||||
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.VariableAssignNode]: variableAssignNode,
|
||||
[WorkflowType.McpNode]: mcpNode,
|
||||
[WorkflowType.LoopNode]: loopNode,
|
||||
[WorkflowType.LoopBodyNode]: loopBodyNode,
|
||||
[WorkflowType.LoopStartNode]: loopStartNode,
|
||||
}
|
||||
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,7 +10,8 @@ const end_nodes: Array<string> = [
|
|||
WorkflowType.Application,
|
||||
WorkflowType.SpeechToTextNode,
|
||||
WorkflowType.TextToSpeechNode,
|
||||
WorkflowType.ImageGenerateNode,
|
||||
WorkflowType.ImageGenerateNode,
|
||||
WorkflowType.LoopBodyNode,
|
||||
]
|
||||
export class WorkFlowInstance {
|
||||
nodes
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import LogicFlow from '@logicflow/core'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import AppEdge from './common/edge'
|
||||
import loopEdge from './common/loopEdge'
|
||||
import Control from './common/NodeControl.vue'
|
||||
import { baseNodes } from '@/workflow/common/data'
|
||||
import '@logicflow/extension/lib/style/index.css'
|
||||
|
|
@ -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,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