mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-25 17:22:55 +00:00
feat: 函数库功能
This commit is contained in:
parent
845225cce1
commit
f421d1975d
|
|
@ -12,8 +12,11 @@ from .question_node import *
|
|||
from .search_dataset_node import *
|
||||
from .start_node import *
|
||||
from .direct_reply_node import *
|
||||
from .function_lib_node import *
|
||||
from .function_node import *
|
||||
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode]
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode,
|
||||
BaseFunctionNodeNode, BaseFunctionLibNodeNode]
|
||||
|
||||
|
||||
def get_node(node_type):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/8/8 17:45
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_function_lib_node.py
|
||||
@date:2024/8/8 16:21
|
||||
@desc:
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.field.common import ObjectField
|
||||
from common.util.field_message import ErrMessage
|
||||
|
||||
|
||||
class InputField(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名'))
|
||||
value = ObjectField(required=True, error_messages=ErrMessage.char("变量值"), model_type_list=[str, list])
|
||||
|
||||
|
||||
class FunctionLibNodeParamsSerializer(serializers.Serializer):
|
||||
function_lib_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid('函数库id'))
|
||||
input_field_list = InputField(required=True, many=True)
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean('是否返回内容'))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class IFunctionLibNode(INode):
|
||||
type = 'function-lib-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return FunctionLibNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/8/8 17:48
|
||||
@desc:
|
||||
"""
|
||||
from .base_function_lib_node import BaseFunctionLibNodeNode
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_function_lib_node.py
|
||||
@date:2024/8/8 17:49
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.function_lib_node.i_function_lib_node import IFunctionLibNode
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from function_lib.models.function import FunctionLib
|
||||
from smartdoc.const import CONFIG
|
||||
|
||||
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result() and 'result' in step_variable:
|
||||
result = step_variable['result'] + '\n'
|
||||
yield result
|
||||
workflow.answer += result
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def get_field_value(debug_field_list, name, is_required):
|
||||
result = [field for field in debug_field_list if field.get('name') == name]
|
||||
if len(result) > 0:
|
||||
return result[-1]['value']
|
||||
if is_required:
|
||||
raise AppApiException(500, f"{name}字段未设置值")
|
||||
return None
|
||||
|
||||
|
||||
def convert_value(name: str, value, _type, is_required, source, node):
|
||||
if not is_required and value is None:
|
||||
return None
|
||||
if source == 'reference':
|
||||
value = node.workflow_manage.get_reference_field(
|
||||
value[0],
|
||||
value[1:])
|
||||
return value
|
||||
try:
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
if _type == 'dict':
|
||||
return json.loads(value)
|
||||
if _type == 'array':
|
||||
return json.loads(value)
|
||||
return value
|
||||
except Exception as e:
|
||||
raise AppApiException(500, f'字段:{name}类型:{_type}值:{value}类型转换错误')
|
||||
|
||||
|
||||
class BaseFunctionLibNodeNode(IFunctionLibNode):
|
||||
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
|
||||
function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'),
|
||||
field.get('source'), self)
|
||||
for field in
|
||||
[{'value': get_field_value(input_field_list, field.get('name'), field.get('is_required'),
|
||||
), **field}
|
||||
for field in
|
||||
function_lib.input_field_list]}
|
||||
self.context['params'] = params
|
||||
result = function_executor.exec_code(function_lib.code, params)
|
||||
return NodeResult({'result': result}, {}, _write_context=write_context)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": self.context.get('result'),
|
||||
"params": self.context.get('params'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/13 10:43
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: i_function_lib_node.py
|
||||
@date:2024/8/8 16:21
|
||||
@desc:
|
||||
"""
|
||||
import re
|
||||
from typing import Type
|
||||
|
||||
from django.core import validators
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.field.common import ObjectField
|
||||
from common.util.field_message import ErrMessage
|
||||
|
||||
|
||||
class InputField(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名'))
|
||||
is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("是否必填"))
|
||||
type = serializers.CharField(required=True, error_messages=ErrMessage.char("类型"), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"),
|
||||
message="字段只支持string|int|dict|array|float", code=500)
|
||||
])
|
||||
source = serializers.CharField(required=True, error_messages=ErrMessage.char("来源"), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^custom|reference$"),
|
||||
message="字段只支持custom|reference", code=500)
|
||||
])
|
||||
value = ObjectField(required=True, error_messages=ErrMessage.char("变量值"), model_type_list=[str, list])
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
is_required = self.data.get('is_required')
|
||||
if is_required and self.data.get('value') is None:
|
||||
raise AppApiException(500, f'{self.data.get("name")}必填')
|
||||
|
||||
|
||||
class FunctionNodeParamsSerializer(serializers.Serializer):
|
||||
input_field_list = InputField(required=True, many=True)
|
||||
code = serializers.CharField(required=True, error_messages=ErrMessage.char("函数"))
|
||||
is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean('是否返回内容'))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
|
||||
|
||||
class IFunctionNode(INode):
|
||||
type = 'function-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return FunctionNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
|
||||
|
||||
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/13 11:19
|
||||
@desc:
|
||||
"""
|
||||
from .base_function_node import BaseFunctionNodeNode
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: base_function_lib_node.py
|
||||
@date:2024/8/8 17:49
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.function_node.i_function_node import IFunctionNode
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from smartdoc.const import CONFIG
|
||||
|
||||
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
|
||||
|
||||
|
||||
def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
|
||||
if step_variable is not None:
|
||||
for key in step_variable:
|
||||
node.context[key] = step_variable[key]
|
||||
if workflow.is_result() and 'result' in step_variable:
|
||||
result = step_variable['result'] + '\n'
|
||||
yield result
|
||||
workflow.answer += result
|
||||
node.context['run_time'] = time.time() - node.context['start_time']
|
||||
|
||||
|
||||
def convert_value(name: str, value, _type, is_required, source, node):
|
||||
if not is_required and value is None:
|
||||
return None
|
||||
if source == 'reference':
|
||||
value = node.workflow_manage.get_reference_field(
|
||||
value[0],
|
||||
value[1:])
|
||||
return value
|
||||
try:
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
if _type == 'dict':
|
||||
return json.loads(value)
|
||||
if _type == 'array':
|
||||
return json.loads(value)
|
||||
return value
|
||||
except Exception as e:
|
||||
raise AppApiException(500, f'字段:{name}类型:{_type}值:{value}类型转换错误')
|
||||
|
||||
|
||||
class BaseFunctionNodeNode(IFunctionNode):
|
||||
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
|
||||
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'), field.get('source'), self)
|
||||
for field in input_field_list}
|
||||
result = function_executor.exec_code(code, params)
|
||||
self.context['params'] = params
|
||||
return NodeResult({'result': result}, {}, _write_context=write_context)
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
"result": self.context.get('result'),
|
||||
"params": self.context.get('params'),
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ class Node:
|
|||
self.__setattr__(keyword, kwargs.get(keyword))
|
||||
|
||||
|
||||
end_nodes = ['ai-chat-node', 'reply-node']
|
||||
end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node']
|
||||
|
||||
|
||||
class Flow:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,21 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ObjectField(serializers.Field):
|
||||
def __init__(self, model_type_list, **kwargs):
|
||||
self.model_type_list = model_type_list
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
for model_type in self.model_type_list:
|
||||
if isinstance(data, model_type):
|
||||
return data
|
||||
self.fail('message类型错误', value=data)
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class InstanceField(serializers.Field):
|
||||
def __init__(self, model_type, **kwargs):
|
||||
self.model_type = model_type
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: function_code.py
|
||||
@date:2024/8/7 16:11
|
||||
@desc:
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from textwrap import dedent
|
||||
|
||||
from diskcache import Cache
|
||||
|
||||
from smartdoc.const import PROJECT_DIR
|
||||
|
||||
python_directory = sys.executable
|
||||
|
||||
|
||||
class FunctionExecutor:
|
||||
def __init__(self, sandbox=False):
|
||||
self.sandbox = sandbox
|
||||
if sandbox:
|
||||
self.sandbox_path = '/opt/maxkb/app/sandbox'
|
||||
self.user = 'sandbox'
|
||||
else:
|
||||
self.sandbox_path = os.path.join(PROJECT_DIR, 'data', 'sandbox')
|
||||
self.user = None
|
||||
self._createdir()
|
||||
if self.sandbox:
|
||||
os.system(f"chown -R {self.user}:{self.user} {self.sandbox_path}")
|
||||
|
||||
def _createdir(self):
|
||||
old_mask = os.umask(0o077)
|
||||
try:
|
||||
os.makedirs(self.sandbox_path, 0o700, exist_ok=True)
|
||||
finally:
|
||||
os.umask(old_mask)
|
||||
|
||||
def exec_code(self, code_str, keywords):
|
||||
_id = str(uuid.uuid1())
|
||||
success = '{"code":200,"msg":"成功","data":exec_result}'
|
||||
err = '{"code":500,"msg":str(e),"data":None}'
|
||||
path = r'' + self.sandbox_path + ''
|
||||
_exec_code = f"""
|
||||
try:
|
||||
locals_v={'{}'}
|
||||
keywords={keywords}
|
||||
globals_v=globals()
|
||||
exec({dedent(code_str)!a}, globals_v, locals_v)
|
||||
f_name, f = locals_v.popitem()
|
||||
for local in locals_v:
|
||||
globals_v[local] = locals_v[local]
|
||||
exec_result=f(**keywords)
|
||||
from diskcache import Cache
|
||||
cache = Cache({path!a})
|
||||
cache.set({_id!a},{success})
|
||||
except Exception as e:
|
||||
from diskcache import Cache
|
||||
cache = Cache({path!a})
|
||||
cache.set({_id!a},{err})
|
||||
"""
|
||||
if self.sandbox:
|
||||
subprocess_result = self._exec_sandbox(_exec_code, _id)
|
||||
else:
|
||||
subprocess_result = self._exec(_exec_code)
|
||||
if subprocess_result.returncode == 1:
|
||||
raise Exception(subprocess_result.stderr)
|
||||
cache = Cache(self.sandbox_path)
|
||||
result = cache.get(_id)
|
||||
cache.delete(_id)
|
||||
if result.get('code') == 200:
|
||||
return result.get('data')
|
||||
raise Exception(result.get('msg'))
|
||||
|
||||
def _exec_sandbox(self, _code, _id):
|
||||
exec_python_file = f'{self.sandbox_path}/{_id}.py'
|
||||
with open(exec_python_file, 'w') as file:
|
||||
file.write(_code)
|
||||
os.system(f"chown {self.user}:{self.user} {exec_python_file}")
|
||||
subprocess_result = subprocess.run(
|
||||
['su', '-c', python_directory + ' ' + exec_python_file, self.user],
|
||||
text=True,
|
||||
capture_output=True)
|
||||
os.remove(exec_python_file)
|
||||
return subprocess_result
|
||||
|
||||
@staticmethod
|
||||
def _exec(_code):
|
||||
return subprocess.run([python_directory, '-c', _code], text=True, capture_output=True)
|
||||
|
|
@ -146,7 +146,7 @@ def get_embedding_model_by_dataset_id_list(dataset_id_list: List):
|
|||
|
||||
def get_embedding_model_by_dataset_id(dataset_id: str):
|
||||
dataset = QuerySet(DataSet).select_related('embedding_mode').filter(id=dataset_id).first()
|
||||
return ModelManage.get_model(dataset.embedding_mode_id, lambda _id: get_model(dataset.embedding_mode))
|
||||
return ModelManage.get_model(str(dataset.embedding_mode_id), lambda _id: get_model(dataset.embedding_mode))
|
||||
|
||||
|
||||
def get_embedding_model_by_dataset(dataset):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FunctionLibConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'function_lib'
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 4.2.15 on 2024-08-13 10:04
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('users', '0004_alter_user_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FunctionLib',
|
||||
fields=[
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
|
||||
('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
|
||||
('name', models.CharField(max_length=64, verbose_name='函数名称')),
|
||||
('desc', models.CharField(max_length=128, verbose_name='描述')),
|
||||
('code', models.CharField(max_length=102400, verbose_name='python代码')),
|
||||
('input_field_list', django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(default=dict, verbose_name='输入字段'), default=list, size=None, verbose_name='输入字段列表')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'function_lib',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2024/8/2 14:55
|
||||
@desc:
|
||||
"""
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: function_lib.py
|
||||
@date:2024/8/2 14:59
|
||||
@desc:
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
|
||||
from common.mixins.app_model_mixin import AppModelMixin
|
||||
from users.models import User
|
||||
|
||||
|
||||
class FunctionLib(AppModelMixin):
|
||||
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户id")
|
||||
name = models.CharField(max_length=64, verbose_name="函数名称")
|
||||
desc = models.CharField(max_length=128, verbose_name="描述")
|
||||
code = models.CharField(max_length=102400, verbose_name="python代码")
|
||||
input_field_list = ArrayField(verbose_name="输入字段列表",
|
||||
base_field=models.JSONField(verbose_name="输入字段", default=dict)
|
||||
, default=list)
|
||||
|
||||
class Meta:
|
||||
db_table = "function_lib"
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: function_lib_serializer.py
|
||||
@date:2024/8/2 17:35
|
||||
@desc:
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from django.core import validators
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.db.search import page_search
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.util.field_message import ErrMessage
|
||||
from common.util.function_code import FunctionExecutor
|
||||
from function_lib.models.function import FunctionLib
|
||||
from smartdoc.const import CONFIG
|
||||
|
||||
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
|
||||
|
||||
|
||||
class FunctionLibModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FunctionLib
|
||||
fields = ['id', 'name', 'desc', 'code', 'input_field_list',
|
||||
'create_time', 'update_time']
|
||||
|
||||
|
||||
class FunctionLibInputField(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名'))
|
||||
is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("是否必填"))
|
||||
type = serializers.CharField(required=True, error_messages=ErrMessage.char("类型"), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"),
|
||||
message="字段只支持string|int|dict|array|float", code=500)
|
||||
])
|
||||
source = serializers.CharField(required=True, error_messages=ErrMessage.char("来源"), validators=[
|
||||
validators.RegexValidator(regex=re.compile("^custom|reference$"),
|
||||
message="字段只支持custom|reference", code=500)
|
||||
])
|
||||
|
||||
|
||||
class DebugField(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名'))
|
||||
value = serializers.CharField(required=False, allow_blank=True, allow_null=True,
|
||||
error_messages=ErrMessage.char("变量值"))
|
||||
|
||||
|
||||
class DebugInstance(serializers.Serializer):
|
||||
debug_field_list = DebugField(required=True, many=True)
|
||||
input_field_list = FunctionLibInputField(required=True, many=True)
|
||||
code = serializers.CharField(required=True, error_messages=ErrMessage.char("函数内容"))
|
||||
|
||||
|
||||
class EditFunctionLib(serializers.Serializer):
|
||||
name = serializers.CharField(required=False, error_messages=ErrMessage.char("函数名称"))
|
||||
|
||||
desc = serializers.CharField(required=False, error_messages=ErrMessage.char("函数描述"))
|
||||
|
||||
code = serializers.CharField(required=False, error_messages=ErrMessage.char("函数内容"))
|
||||
|
||||
input_field_list = FunctionLibInputField(required=False, many=True)
|
||||
|
||||
|
||||
class CreateFunctionLib(serializers.Serializer):
|
||||
name = serializers.CharField(required=True, error_messages=ErrMessage.char("函数名称"))
|
||||
|
||||
desc = serializers.CharField(required=False, allow_null=True, allow_blank=True,
|
||||
error_messages=ErrMessage.char("函数描述"))
|
||||
|
||||
code = serializers.CharField(required=True, error_messages=ErrMessage.char("函数内容"))
|
||||
|
||||
input_field_list = FunctionLibInputField(required=True, many=True)
|
||||
|
||||
|
||||
class FunctionLibSerializer(serializers.Serializer):
|
||||
class Query(serializers.Serializer):
|
||||
name = serializers.CharField(required=False, allow_null=True, allow_blank=True,
|
||||
error_messages=ErrMessage.char("函数名称"))
|
||||
|
||||
desc = serializers.CharField(required=False, allow_null=True, allow_blank=True,
|
||||
error_messages=ErrMessage.char("函数描述"))
|
||||
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
|
||||
def get_query_set(self):
|
||||
query_set = QuerySet(FunctionLib).filter(user_id=self.data.get('user_id'))
|
||||
if self.data.get('name') is not None:
|
||||
query_set = query_set.filter(name=self.data.get('name'))
|
||||
if self.data.get('desc') is not None:
|
||||
query_set = query_set.filter(name=self.data.get('desc'))
|
||||
query_set = query_set.order_by("-create_time")
|
||||
return query_set
|
||||
|
||||
def list(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
return [FunctionLibModelSerializer(item).data for item in self.get_query_set()]
|
||||
|
||||
def page(self, current_page: int, page_size: int, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
return page_search(current_page, page_size, self.get_query_set(),
|
||||
post_records_handler=lambda row: FunctionLibModelSerializer(row).data)
|
||||
|
||||
class Create(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
|
||||
def insert(self, instance, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
CreateFunctionLib(data=instance).is_valid(raise_exception=True)
|
||||
function_lib = FunctionLib(id=uuid.uuid1(), name=instance.get('name'), desc=instance.get('desc'),
|
||||
code=instance.get('code'),
|
||||
user_id=self.data.get('user_id'),
|
||||
input_field_list=instance.get('input_field_list'))
|
||||
function_lib.save()
|
||||
return FunctionLibModelSerializer(function_lib).data
|
||||
|
||||
class Debug(serializers.Serializer):
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
|
||||
def debug(self, debug_instance, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
DebugInstance(data=debug_instance).is_valid(raise_exception=True)
|
||||
input_field_list = debug_instance.get('input_field_list')
|
||||
code = debug_instance.get('code')
|
||||
debug_field_list = debug_instance.get('debug_field_list')
|
||||
params = {field.get('name'): self.convert_value(field.get('name'), field.get('value'), field.get('type'),
|
||||
field.get('is_required'))
|
||||
for field in
|
||||
[{'value': self.get_field_value(debug_field_list, field.get('name'), field.get('is_required')),
|
||||
**field} for field in
|
||||
input_field_list]}
|
||||
return function_executor.exec_code(code, params)
|
||||
|
||||
@staticmethod
|
||||
def get_field_value(debug_field_list, name, is_required):
|
||||
result = [field for field in debug_field_list if field.get('name') == name]
|
||||
if len(result) > 0:
|
||||
return result[-1].get('value')
|
||||
if is_required:
|
||||
raise AppApiException(500, f"{name}字段未设置值")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def convert_value(name: str, value: str, _type: str, is_required: bool):
|
||||
if not is_required and value is None:
|
||||
return None
|
||||
try:
|
||||
if _type == 'int':
|
||||
return int(value)
|
||||
if _type == 'float':
|
||||
return float(value)
|
||||
if _type == 'dict':
|
||||
return json.loads(value)
|
||||
if _type == 'array':
|
||||
return json.loads(value)
|
||||
return value
|
||||
except Exception as e:
|
||||
raise AppApiException(500, f'字段:{name}类型:{_type}值:{value}类型转换错误')
|
||||
|
||||
class Operate(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("函数id"))
|
||||
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
if not QuerySet(FunctionLib).filter(id=self.data.get('id'), user_id=self.data.get('user_id')).exists():
|
||||
raise AppApiException(500, '函数不存在')
|
||||
|
||||
def edit(self, instance, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
EditFunctionLib(data=instance).is_valid(raise_exception=True)
|
||||
edit_field_list = ['name', 'desc', 'code', 'input_field_list']
|
||||
edit_dict = {field: instance.get(field) for field in edit_field_list if (
|
||||
field in instance and instance.get(field) is not None)}
|
||||
QuerySet(FunctionLib).filter(id=self.data.get('id')).update(**edit_dict)
|
||||
return self.one(False)
|
||||
|
||||
def one(self, with_valid=True):
|
||||
if with_valid:
|
||||
self.is_valid(raise_exception=True)
|
||||
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
|
||||
return FunctionLibModelSerializer(function_lib).data
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: function_lib_api.py
|
||||
@date:2024/8/2 17:11
|
||||
@desc:
|
||||
"""
|
||||
from drf_yasg import openapi
|
||||
|
||||
from common.mixins.api_mixin import ApiMixin
|
||||
|
||||
|
||||
class FunctionLibApi(ApiMixin):
|
||||
@staticmethod
|
||||
def get_response_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['id', 'name', 'desc', 'code', 'input_field_list', 'create_time',
|
||||
'update_time'],
|
||||
properties={
|
||||
'id': openapi.Schema(type=openapi.TYPE_STRING, title="", description="主键id"),
|
||||
'name': openapi.Schema(type=openapi.TYPE_STRING, title="函数名称", description="函数名称"),
|
||||
'desc': openapi.Schema(type=openapi.TYPE_STRING, title="函数描述", description="函数描述"),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"),
|
||||
'input_field_list': openapi.Schema(type=openapi.TYPE_STRING, title="输入字段", description="输入字段"),
|
||||
'create_time': openapi.Schema(type=openapi.TYPE_STRING, title="创建时间", description="创建时间"),
|
||||
'update_time': openapi.Schema(type=openapi.TYPE_STRING, title="修改时间", description="修改时间"),
|
||||
}
|
||||
)
|
||||
|
||||
class Query(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_params_api():
|
||||
return [openapi.Parameter(name='name',
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=False,
|
||||
description='函数名称'),
|
||||
openapi.Parameter(name='desc',
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_STRING,
|
||||
required=False,
|
||||
description='函数描述')
|
||||
]
|
||||
|
||||
class Debug(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[],
|
||||
properties={
|
||||
'debug_field_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
description="输入变量列表",
|
||||
items=openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
required=[],
|
||||
properties={
|
||||
'name': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="变量名",
|
||||
description="变量名"),
|
||||
'value': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="变量值",
|
||||
description="变量值"),
|
||||
})),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"),
|
||||
'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
description="输入变量列表",
|
||||
items=openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
required=['name', 'is_required', 'source'],
|
||||
properties={
|
||||
'name': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="变量名",
|
||||
description="变量名"),
|
||||
'is_required': openapi.Schema(
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
title="是否必填",
|
||||
description="是否必填"),
|
||||
'type': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="字段类型",
|
||||
description="字段类型 string|int|dict|array|float"
|
||||
),
|
||||
'source': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="来源",
|
||||
description="来源只支持custom|reference"),
|
||||
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
class Edit(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=[],
|
||||
properties={
|
||||
'name': openapi.Schema(type=openapi.TYPE_STRING, title="函数名称", description="函数名称"),
|
||||
'desc': openapi.Schema(type=openapi.TYPE_STRING, title="函数描述", description="函数描述"),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"),
|
||||
'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
description="输入变量列表",
|
||||
items=openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
required=[],
|
||||
properties={
|
||||
'name': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="变量名",
|
||||
description="变量名"),
|
||||
'is_required': openapi.Schema(
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
title="是否必填",
|
||||
description="是否必填"),
|
||||
'type': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="字段类型",
|
||||
description="字段类型 string|int|dict|array|float"
|
||||
),
|
||||
'source': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="来源",
|
||||
description="来源只支持custom|reference"),
|
||||
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
class Create(ApiMixin):
|
||||
@staticmethod
|
||||
def get_request_body_api():
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['name', 'code', 'input_field_list'],
|
||||
properties={
|
||||
'name': openapi.Schema(type=openapi.TYPE_STRING, title="函数名称", description="函数名称"),
|
||||
'desc': openapi.Schema(type=openapi.TYPE_STRING, title="函数描述", description="函数描述"),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"),
|
||||
'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY,
|
||||
description="输入变量列表",
|
||||
items=openapi.Schema(type=openapi.TYPE_OBJECT,
|
||||
required=['name', 'is_required', 'source'],
|
||||
properties={
|
||||
'name': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="变量名",
|
||||
description="变量名"),
|
||||
'is_required': openapi.Schema(
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
title="是否必填",
|
||||
description="是否必填"),
|
||||
'type': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="字段类型",
|
||||
description="字段类型 string|int|dict|array|float"
|
||||
),
|
||||
'source': openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="来源",
|
||||
description="来源只支持custom|reference"),
|
||||
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "function_lib"
|
||||
urlpatterns = [
|
||||
path('function_lib', views.FunctionLibView.as_view()),
|
||||
path('function_lib/debug', views.FunctionLibView.Debug.as_view()),
|
||||
path('function_lib/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()),
|
||||
path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(),
|
||||
name="function_lib_page"),
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2024/8/2 14:53
|
||||
@desc:
|
||||
"""
|
||||
from .function_lib_views import *
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎
|
||||
@file: function_lib_views.py
|
||||
@date:2024/8/2 17:08
|
||||
@desc:
|
||||
"""
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.auth import TokenAuth, has_permissions
|
||||
from common.constants.permission_constants import RoleConstants
|
||||
from common.response import result
|
||||
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer
|
||||
from function_lib.swagger_api.function_lib_api import FunctionLibApi
|
||||
|
||||
|
||||
class FunctionLibView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=["GET"], detail=False)
|
||||
@swagger_auto_schema(operation_summary="获取函数列表",
|
||||
operation_id="获取函数列表",
|
||||
tags=["函数库"],
|
||||
manual_parameters=FunctionLibApi.Query.get_request_params_api())
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
def get(self, request: Request):
|
||||
return result.success(
|
||||
FunctionLibSerializer.Query(
|
||||
data={'name': request.query_params.get('name'),
|
||||
'desc': request.query_params.get('desc'),
|
||||
'user_id': request.user.id}).list())
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="创建函数",
|
||||
operation_id="创建函数",
|
||||
request_body=FunctionLibApi.Create.get_request_body_api(),
|
||||
tags=['函数库'])
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
def post(self, request: Request):
|
||||
return result.success(FunctionLibSerializer.Create(data={'user_id': request.user.id}).insert(request.data))
|
||||
|
||||
class Debug(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="调试函数",
|
||||
operation_id="调试函数",
|
||||
request_body=FunctionLibApi.Debug.get_request_body_api(),
|
||||
tags=['函数库'])
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
def post(self, request: Request):
|
||||
return result.success(
|
||||
FunctionLibSerializer.Debug(data={'user_id': request.user.id}).debug(
|
||||
request.data))
|
||||
|
||||
class Operate(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['PUT'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="修改函数",
|
||||
operation_id="修改函数",
|
||||
request_body=FunctionLibApi.Edit.get_request_body_api(),
|
||||
tags=['函数库'])
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
def put(self, request: Request, function_lib_id: str):
|
||||
return result.success(
|
||||
FunctionLibSerializer.Operate(data={'user_id': request.user.id, 'id': function_lib_id}).edit(
|
||||
request.data))
|
||||
|
||||
class Page(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="分页获取函数列表",
|
||||
operation_id="分页获取函数列表",
|
||||
manual_parameters=result.get_page_request_params(
|
||||
FunctionLibApi.Query.get_request_params_api()),
|
||||
responses=result.get_page_api_response(FunctionLibApi.get_response_body_api()),
|
||||
tags=['函数库'])
|
||||
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
|
||||
def get(self, request: Request, current_page: int, page_size: int):
|
||||
return result.success(
|
||||
FunctionLibSerializer.Query(
|
||||
data={'name': request.query_params.get('name'),
|
||||
'desc': request.query_params.get('desc'),
|
||||
'user_id': request.user.id}).page(
|
||||
current_page, page_size))
|
||||
|
|
@ -87,7 +87,8 @@ class Config(dict):
|
|||
"EMBEDDING_MODEL_PATH": os.path.join(PROJECT_DIR, 'models'),
|
||||
# 向量库配置
|
||||
"VECTOR_STORE_NAME": 'pg_vector',
|
||||
"DEBUG": False
|
||||
"DEBUG": False,
|
||||
'SANDBOX': False
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ INSTALLED_APPS = [
|
|||
"drf_yasg", # swagger 接口
|
||||
'django_filters', # 条件过滤
|
||||
'django_apscheduler',
|
||||
'common'
|
||||
'common',
|
||||
'function_lib'
|
||||
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Class-based views
|
|||
1. Add an import: forms other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: forms django.urls import include, path
|
||||
1. Import the include() function_lib: forms django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import os
|
||||
|
|
@ -34,7 +34,8 @@ urlpatterns = [
|
|||
path("api/", include("users.urls")),
|
||||
path("api/", include("dataset.urls")),
|
||||
path("api/", include("setting.urls")),
|
||||
path("api/", include("application.urls"))
|
||||
path("api/", include("application.urls")),
|
||||
path("api/", include("function_lib.urls"))
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ RUN apt-get update && \
|
|||
|
||||
COPY . /opt/maxkb/app
|
||||
RUN mkdir -p /opt/maxkb/app /opt/maxkb/model /opt/maxkb/conf && \
|
||||
rm -rf /opt/maxkb/app/ui
|
||||
rm -rf /opt/maxkb/app/ui &&\
|
||||
useradd sandbox && mkdir /opt/maxkb/app/sandbox && chown sandbox:sandbox /opt/maxkb/app/sandbox && chmod 700 /opt/maxkb/app/sandbox \
|
||||
|
||||
COPY --from=web-build ui /opt/maxkb/app/ui
|
||||
WORKDIR /opt/maxkb/app
|
||||
RUN python3 -m venv /opt/py3 && \
|
||||
|
|
@ -41,6 +43,7 @@ ENV MAXKB_VERSION="${DOCKER_IMAGE_TAG} (build at ${BUILD_AT}, commit: ${GITHUB_C
|
|||
MAXKB_DB_PASSWORD=Password123@postgres \
|
||||
MAXKB_EMBEDDING_MODEL_NAME=/opt/maxkb/model/embedding/shibing624_text2vec-base-chinese \
|
||||
MAXKB_EMBEDDING_MODEL_PATH=/opt/maxkb/model/embedding \
|
||||
MAXKB_SANDBOX=true \
|
||||
LANG=en_US.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH \
|
||||
POSTGRES_USER=root \
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ authors = ["shaohuzhang1 <shaohu.zhang@fit2cloud.com>"]
|
|||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
python = ">=3.11,<3.12"
|
||||
django = "4.2.15"
|
||||
djangorestframework = "^3.15.2"
|
||||
drf-yasg = "1.21.7"
|
||||
|
|
@ -45,6 +45,9 @@ xlrd = "^2.0.1"
|
|||
gunicorn = "^22.0.0"
|
||||
python-daemon = "3.0.1"
|
||||
gevent = "^24.2.1"
|
||||
restrictedpython = "^7.2"
|
||||
cgroups = "^0.1.0"
|
||||
wasm-exec = "^0.1.9"
|
||||
|
||||
boto3 = "^1.34.151"
|
||||
langchain-aws = "^0.1.13"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@logicflow/extension": "^1.2.27",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^0.28.0",
|
||||
"codemirror-editor-vue3": "^2.7.0",
|
||||
"cropperjs": "^1.6.2",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.5.6",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
import type { pageRequest } from '@/api/type/common'
|
||||
import type { functionLibData } from '@/api/type/function-lib'
|
||||
import { type Ref } from 'vue'
|
||||
|
||||
const prefix = '/function_lib'
|
||||
|
||||
/**
|
||||
* 获取函数列表
|
||||
* param {
|
||||
"name": "string",
|
||||
}
|
||||
*/
|
||||
const getAllFunctionLib: (param?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
param,
|
||||
loading
|
||||
) => {
|
||||
return get(`${prefix}`, param || {}, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页函数列表
|
||||
* page {
|
||||
"current_page": "string",
|
||||
"page_size": "string",
|
||||
}
|
||||
* param {
|
||||
"name": "string",
|
||||
}
|
||||
*/
|
||||
const getFunctionLib: (
|
||||
page: pageRequest,
|
||||
param: any,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<any>> = (page, param, loading) => {
|
||||
return get(`${prefix}/${page.current_page}/${page.page_size}`, param, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建函数
|
||||
* @param 参数
|
||||
*/
|
||||
const postFunctionLib: (data: functionLibData, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
data,
|
||||
loading
|
||||
) => {
|
||||
return post(`${prefix}`, data, undefined, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改函数
|
||||
* @param 参数
|
||||
|
||||
*/
|
||||
const putFunctionLib: (
|
||||
function_lib_id: string,
|
||||
data: functionLibData,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<any>> = (function_lib_id, data, loading) => {
|
||||
return put(`${prefix}/${function_lib_id}`, data, undefined, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试函数
|
||||
* @param 参数
|
||||
|
||||
*/
|
||||
const postFunctionLibDebug: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
data: any,
|
||||
loading
|
||||
) => {
|
||||
return post(`${prefix}/debug`, data, undefined, loading)
|
||||
}
|
||||
|
||||
export default {
|
||||
getFunctionLib,
|
||||
postFunctionLib,
|
||||
putFunctionLib,
|
||||
postFunctionLibDebug,
|
||||
getAllFunctionLib
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
interface functionLibData {
|
||||
id?: String
|
||||
name: String
|
||||
desc: String
|
||||
code?: String
|
||||
input_field_list?: Array<any>
|
||||
}
|
||||
|
||||
export type { functionLibData }
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6260_56998)">
|
||||
<path d="M8.45989 5.8569L8.57477 5.86299C8.79559 5.87481 8.96834 6.05752 8.96834 6.27866V6.82427C8.96834 7.07408 8.75005 7.26525 8.50052 7.25364C8.42661 7.2502 8.35257 7.24833 8.27834 7.24833C8.06408 7.24833 7.92822 7.30022 7.84948 7.38496C7.77816 7.4623 7.73167 7.61024 7.73167 7.85333V8.13167H8.55167C8.78179 8.13167 8.96834 8.31821 8.96834 8.54833V9.38881C8.96834 9.61893 8.78179 9.80547 8.55167 9.80547H7.73167V13.9133C7.73167 14.1435 7.54512 14.33 7.31501 14.33H6.32501C6.09489 14.33 5.90834 14.1435 5.90834 13.9133V9.80547H5.13859C4.90847 9.80547 4.72192 9.61893 4.72192 9.38881V8.54833C4.72192 8.31821 4.90847 8.13167 5.13859 8.13167H5.90834V7.74833C5.90834 7.19278 6.08218 6.73149 6.43374 6.37176C6.78941 6.00781 7.32033 5.83333 8.01 5.83333C8.15999 5.83333 8.30995 5.84119 8.45989 5.8569Z" fill="white"/>
|
||||
<path d="M12.4626 9.47701L11.5695 8.15211C11.492 8.03721 11.3625 7.96833 11.224 7.96833H10.1483C9.81198 7.96833 9.61424 8.34622 9.80596 8.62253L11.3934 10.9103L9.61243 13.5148C9.42332 13.7913 9.62135 14.1667 9.95637 14.1667H11.0006C11.1403 14.1667 11.2708 14.0966 11.3479 13.9801L12.4396 12.3325L13.5313 13.9801C13.6085 14.0966 13.7389 14.1667 13.8786 14.1667H14.9522C15.2886 14.1667 15.4864 13.7886 15.2945 13.5123L13.4629 10.8753L15.044 8.62451C15.238 8.34841 15.0405 7.96833 14.7031 7.96833H13.6913C13.552 7.96833 13.4219 8.03796 13.3446 8.15387L12.4626 9.47701Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.9226 2.74408C18.7663 2.5878 18.5543 2.5 18.3333 2.5H1.66665C1.44563 2.5 1.23367 2.5878 1.07739 2.74408C0.92111 2.90036 0.833313 3.11232 0.833313 3.33333V16.6667C0.833313 16.8877 0.92111 17.0996 1.07739 17.2559C1.23367 17.4122 1.44563 17.5 1.66665 17.5H18.3333C18.5543 17.5 18.7663 17.4122 18.9226 17.2559C19.0788 17.0996 19.1666 16.8877 19.1666 16.6667V3.33333C19.1666 3.11232 19.0788 2.90036 18.9226 2.74408ZM2.49998 4.16667H17.5V15.8333H2.49998V4.16667Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6260_56998">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -141,6 +141,27 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 函数库 -->
|
||||
<template
|
||||
v-if="
|
||||
item.type === WorkflowType.FunctionLib ||
|
||||
item.type === WorkflowType.FunctionLibCustom
|
||||
"
|
||||
>
|
||||
<div class="card-never border-r-4 mt-8">
|
||||
<h5 class="p-8-12">输入</h5>
|
||||
<div class="p-8-12 border-t-dashed lighter pre-wrap">
|
||||
{{ item.params || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-never border-r-4 mt-8">
|
||||
<h5 class="p-8-12">输出</h5>
|
||||
<div class="p-8-12 border-t-dashed lighter pre-wrap">
|
||||
{{ item.result || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="card-never border-r-4">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<Codemirror v-bind="$attrs" border :height="200" :option="cmOptions" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Codemirror from 'codemirror-editor-vue3'
|
||||
import 'codemirror/mode/python/python.js'
|
||||
|
||||
defineOptions({ name: 'CodemirrorEditor' })
|
||||
const cmOptions = {
|
||||
mode: 'text/x-python',
|
||||
autoRefresh: true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.codemirror-container.bordered {
|
||||
border: 1px solid #bbbfc4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -22,6 +22,7 @@ import MdPreview from './markdown/MdPreview.vue'
|
|||
import LogoFull from './logo/LogoFull.vue'
|
||||
import LogoIcon from './logo/LogoIcon.vue'
|
||||
import SendIcon from './logo/SendIcon.vue'
|
||||
import CodemirrorEditor from './codemirror-editor/index.vue'
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
|
|
@ -48,5 +49,6 @@ export default {
|
|||
app.component(LogoFull.name, LogoFull)
|
||||
app.component(LogoIcon.name, LogoIcon)
|
||||
app.component(SendIcon.name, SendIcon)
|
||||
app.component(CodemirrorEditor.name, CodemirrorEditor)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@ export enum WorkflowType {
|
|||
SearchDataset = 'search-dataset-node',
|
||||
Question = 'question-node',
|
||||
Condition = 'condition-node',
|
||||
Reply = 'reply-node'
|
||||
Reply = 'reply-node',
|
||||
FunctionLib = 'function-lib-node',
|
||||
FunctionLibCustom = 'function-node'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ instance.interceptors.response.use(
|
|||
(response: any) => {
|
||||
if (response.data) {
|
||||
if (response.data.code !== 200 && !(response.data instanceof Blob)) {
|
||||
if (!response.config.url.includes('/valid')) {
|
||||
if (
|
||||
!response.config.url.includes('/valid') &&
|
||||
!response.config.url.includes('/function_lib/debug')
|
||||
) {
|
||||
MsgError(response.data.message)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import Layout from '@/layout/layout-template/DetailLayout.vue'
|
||||
const functionLibRouter = {
|
||||
path: '/function-lib',
|
||||
name: 'function-lib',
|
||||
meta: { title: '函数库', permission: 'APPLICATION:READ' },
|
||||
redirect: '/function-lib',
|
||||
component: () => import('@/layout/layout-template/AppLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/function-lib',
|
||||
name: 'function-lib',
|
||||
component: () => import('@/views/function-lib/index.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default functionLibRouter
|
||||
|
|
@ -480,6 +480,10 @@ h5 {
|
|||
background: #3370ff;
|
||||
}
|
||||
|
||||
.avatar-green {
|
||||
background: #34c724;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
:root {
|
||||
--el-color-primary: #3370FF;
|
||||
--el-color-primary: #3370ff;
|
||||
--el-menu-item-height: 45px;
|
||||
--el-box-shadow-light: 0px 2px 4px 0px rgba(31, 35, 41, 0.12);
|
||||
--el-border-color: #dee0e3;
|
||||
|
|
@ -362,11 +362,25 @@
|
|||
}
|
||||
|
||||
// 提示横幅
|
||||
.el-alert__title {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
.el-alert--warning.is-light {
|
||||
background-color: #ffe7cc;
|
||||
color: var(--app-text-color);
|
||||
font-weight: 400;
|
||||
.el-alert__icon {
|
||||
color: #ff8800;
|
||||
}
|
||||
}
|
||||
.el-alert--success.is-light {
|
||||
background-color: #d6f4d3;
|
||||
.el-alert__icon {
|
||||
color: #34c724;
|
||||
}
|
||||
}
|
||||
.el-alert--danger.is-light {
|
||||
background-color: #fddbda;
|
||||
.el-alert__icon {
|
||||
color: #f54a45;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
>
|
||||
</div>
|
||||
<div>
|
||||
<el-button icon="Plus" @click="showPopover = !showPopover" v-click-outside="clickoutside">
|
||||
添加组件
|
||||
</el-button>
|
||||
<el-button icon="Plus" @click="showPopover = !showPopover"> 添加组件 </el-button>
|
||||
<el-button @click="clickShowDebug" :disabled="showDebug">
|
||||
<AppIcon iconName="app-play-outlined" class="mr-4"></AppIcon>
|
||||
调试</el-button
|
||||
|
|
@ -27,21 +25,63 @@
|
|||
</div>
|
||||
<!-- 下拉框 -->
|
||||
<el-collapse-transition>
|
||||
<div v-show="showPopover" class="workflow-dropdown-menu border border-r-4">
|
||||
<h5 class="title">基础组件</h5>
|
||||
<template v-for="(item, index) in menuNodes" :key="index">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12"
|
||||
@click="clickNodes(item)"
|
||||
@mousedown="onmousedown(item)"
|
||||
>
|
||||
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter">{{ item.label }}</div>
|
||||
<el-text type="info" size="small">{{ item.text }}</el-text>
|
||||
<div
|
||||
v-show="showPopover"
|
||||
class="workflow-dropdown-menu border border-r-4"
|
||||
v-click-outside="clickoutside"
|
||||
>
|
||||
<el-tabs v-model="activeName" class="workflow-dropdown-tabs">
|
||||
<el-tab-pane label="基础组件" name="base">
|
||||
<template v-for="(item, index) in menuNodes" :key="index">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12"
|
||||
@click="clickNodes(item)"
|
||||
@mousedown="onmousedown(item)"
|
||||
>
|
||||
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter">{{ item.label }}</div>
|
||||
<el-text type="info" size="small">{{ item.text }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="函数库" name="function">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12"
|
||||
@click="clickNodes(functionNode)"
|
||||
@mousedown="onmousedown(functionNode)"
|
||||
>
|
||||
<component
|
||||
:is="iconComponent(`function-lib-node-icon`)"
|
||||
class="mr-8 mt-4"
|
||||
:size="32"
|
||||
/>
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter">{{ functionNode.label }}</div>
|
||||
<el-text type="info" size="small">{{ functionNode.text }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-for="(item, index) in functionLibList" :key="index">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12"
|
||||
@click="clickNodes(functionLibNode, item)"
|
||||
@mousedown="onmousedown(functionLibNode, item)"
|
||||
>
|
||||
<component
|
||||
:is="iconComponent(`function-lib-node-icon`)"
|
||||
class="mr-8 mt-4"
|
||||
:size="32"
|
||||
/>
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter">{{ item.name }}</div>
|
||||
<el-text type="info" size="small">{{ item.desc }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
<!-- 主画布 -->
|
||||
|
|
@ -106,7 +146,7 @@
|
|||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import Workflow from '@/workflow/index.vue'
|
||||
import { menuNodes } from '@/workflow/common/data'
|
||||
import { menuNodes, functionLibNode, functionNode } from '@/workflow/common/data'
|
||||
import { iconComponent } from '@/workflow/icons/utils'
|
||||
import applicationApi from '@/api/application'
|
||||
import { isAppIcon } from '@/utils/application'
|
||||
|
|
@ -115,6 +155,7 @@ import { datetimeFormat } from '@/utils/time'
|
|||
import useStore from '@/stores'
|
||||
import { WorkFlowInstance } from '@/workflow/common/validate'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import functionLibApi from '@/api/function-lib'
|
||||
|
||||
const { user, application } = useStore()
|
||||
const router = useRouter()
|
||||
|
|
@ -137,6 +178,8 @@ const showPopover = ref(false)
|
|||
const showDebug = ref(false)
|
||||
const enlarge = ref(false)
|
||||
const saveTime = ref<any>('')
|
||||
const activeName = ref('base')
|
||||
const functionLibList = ref<any[]>([])
|
||||
|
||||
function publicHandle() {
|
||||
workflowRef.value
|
||||
|
|
@ -199,12 +242,22 @@ function clickoutsideDebug() {
|
|||
showDebug.value = false
|
||||
}
|
||||
|
||||
function clickNodes(item: any) {
|
||||
function clickNodes(item: any, data?: any) {
|
||||
if (data) {
|
||||
item['properties']['stepName'] = data.name
|
||||
item['properties']['node_data'] = data
|
||||
}
|
||||
workflowRef.value?.addNode(item)
|
||||
showPopover.value = false
|
||||
}
|
||||
|
||||
function onmousedown(item: any) {
|
||||
function onmousedown(item: any, data?: any) {
|
||||
if (data) {
|
||||
item['properties']['stepName'] = data.name
|
||||
item['properties']['node_data'] = { ...data, function_lib_id: data.id }
|
||||
}
|
||||
workflowRef.value?.onmousedown(item)
|
||||
showPopover.value = false
|
||||
}
|
||||
|
||||
function getGraphData() {
|
||||
|
|
@ -230,6 +283,12 @@ function saveApplication() {
|
|||
})
|
||||
}
|
||||
|
||||
function getList() {
|
||||
functionLibApi.getAllFunctionLib({}, loading).then((res: any) => {
|
||||
functionLibList.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时保存
|
||||
*/
|
||||
|
|
@ -250,6 +309,7 @@ const closeInterval = () => {
|
|||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
getList()
|
||||
// 初始化定时任务
|
||||
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND')) {
|
||||
initInterval()
|
||||
|
|
@ -298,6 +358,11 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
.workflow-dropdown-tabs {
|
||||
.el-tabs__nav-wrap {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-debug-container {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑变量' : '添加变量'"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="fieldFormRef"
|
||||
:rules="rules"
|
||||
:model="form"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item label="变量名" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入变量名"
|
||||
show-word-limit
|
||||
@blur="form.name = form.name.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据类型">
|
||||
<el-select v-model="form.type">
|
||||
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源">
|
||||
<el-select v-model="form.source">
|
||||
<el-option label="引用变量" value="reference" />
|
||||
<el-option label="自定义" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必填" @click.prevent>
|
||||
<el-switch size="small" v-model="form.is_required"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
|
||||
{{ isEdit ? '保存' : '添加' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const typeOptions = ['string', 'int', 'dict', 'array', 'float']
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const fieldFormRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
const form = ref<any>({
|
||||
name: '',
|
||||
type: typeOptions[0],
|
||||
source: 'reference',
|
||||
is_required: false
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '请输入变量名', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
name: '',
|
||||
type: typeOptions[0],
|
||||
source: 'reference',
|
||||
is_required: false
|
||||
}
|
||||
isEdit.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const open = (row: any) => {
|
||||
if (row) {
|
||||
form.value = cloneDeep(row)
|
||||
isEdit.value = true
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('refresh', form.value)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<el-drawer v-model="dubugVisible" size="60%" :append-to-body="true">
|
||||
<template #header>
|
||||
<div class="flex align-center" style="margin-left: -8px">
|
||||
<el-button class="cursor mr-4" link @click.prevent="dubugVisible = false">
|
||||
<el-icon :size="20">
|
||||
<Back />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<h4>调试</h4>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div v-if="form.debug_field_list.length > 0" class="mb-16">
|
||||
<h4 class="title-decoration-1 mb-16">输入变量</h4>
|
||||
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
|
||||
<el-form
|
||||
ref="FormRef"
|
||||
:model="form"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
hide-required-asterisk
|
||||
v-loading="loading"
|
||||
>
|
||||
<template v-for="(item, index) in form.debug_field_list" :key="index">
|
||||
<el-form-item
|
||||
:label="item.name"
|
||||
:prop="'debug_field_list.' + index + '.value'"
|
||||
:rules="{
|
||||
required: item.is_required,
|
||||
message: '请输入变量值',
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex">
|
||||
<span
|
||||
>{{ item.name }} <span class="danger" v-if="item.is_required">*</span></span
|
||||
>
|
||||
<el-tag type="info" class="info-tag ml-4">{{ item.type }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="item.value" placeholder="请输入变量值" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="submit(FormRef)" :loading="loading"> 运行 </el-button>
|
||||
<div v-if="showResult" class="mt-8">
|
||||
<h4 class="title-decoration-1 mb-16 mt-16">运行结果</h4>
|
||||
<div class="mb-16">
|
||||
<el-alert v-if="isSuccess" title="运行成功" type="success" show-icon :closable="false" />
|
||||
<el-alert v-else title="运行失败" type="error" show-icon :closable="false" />
|
||||
</div>
|
||||
|
||||
<p class="lighter mb-8">输出</p>
|
||||
|
||||
<el-card class="pre-wrap danger" shadow="never" style="max-height: 350px; overflow: scroll">
|
||||
{{ result || '-' }}
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import functionLibApi from '@/api/function-lib'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
const FormRef = ref()
|
||||
const loading = ref(false)
|
||||
const dubugVisible = ref(false)
|
||||
const showResult = ref(false)
|
||||
const isSuccess = ref(false)
|
||||
const result = ref('')
|
||||
|
||||
const form = ref<any>({
|
||||
debug_field_list: [],
|
||||
code: '',
|
||||
input_field_list: []
|
||||
})
|
||||
|
||||
watch(dubugVisible, (bool) => {
|
||||
if (!bool) {
|
||||
showResult.value = false
|
||||
isSuccess.value = false
|
||||
result.value = ''
|
||||
form.value = {
|
||||
debug_field_list: [],
|
||||
code: '',
|
||||
input_field_list: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) {
|
||||
functionLibApi
|
||||
.postFunctionLibDebug(form.value, loading)
|
||||
.then((res) => {
|
||||
showResult.value = true
|
||||
isSuccess.value = true
|
||||
result.value = res.data
|
||||
})
|
||||
.catch((res) => {
|
||||
showResult.value = true
|
||||
isSuccess.value = false
|
||||
result.value = res.data
|
||||
})
|
||||
} else {
|
||||
await formEl.validate((valid: any) => {
|
||||
if (valid) {
|
||||
functionLibApi.postFunctionLibDebug(form.value, loading).then((res) => {
|
||||
if (res.code === 500) {
|
||||
showResult.value = true
|
||||
isSuccess.value = false
|
||||
result.value = res.message
|
||||
} else {
|
||||
showResult.value = true
|
||||
isSuccess.value = true
|
||||
result.value = res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const open = (data: any) => {
|
||||
if (data.input_field_list.length > 0) {
|
||||
data.input_field_list.forEach((item: any) => {
|
||||
form.value.debug_field_list.push({
|
||||
value: '',
|
||||
...item
|
||||
})
|
||||
})
|
||||
}
|
||||
form.value.code = data.code
|
||||
form.value.input_field_list = data.input_field_list
|
||||
dubugVisible.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<template>
|
||||
<el-drawer v-model="visible" size="60%">
|
||||
<template #header>
|
||||
<h4>{{ isEdit ? '编辑函数' : '创建函数' }}</h4>
|
||||
</template>
|
||||
<div>
|
||||
<h4 class="title-decoration-1 mb-16">基础信息</h4>
|
||||
<el-form
|
||||
ref="FormRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item label="函数名称" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入函数名称"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
@blur="form.name = form.name.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input
|
||||
v-model="form.desc"
|
||||
type="textarea"
|
||||
placeholder="请输入函数的描述"
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
:autosize="{ minRows: 3 }"
|
||||
@blur="form.desc = form.desc.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="flex-between">
|
||||
<h4 class="title-decoration-1 mb-16">
|
||||
输入变量 <el-text type="info" class="color-secondary"> 使用函数时显示 </el-text>
|
||||
</h4>
|
||||
<el-button link type="primary" @click="openAddDialog()">
|
||||
<el-icon class="mr-4"><Plus /></el-icon> 添加
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="form.input_field_list" class="mb-16">
|
||||
<el-table-column prop="name" label="变量名" />
|
||||
<el-table-column label="数据类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" class="info-tag">{{ row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="必填">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch size="small" v-model="row.is_required" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="source" label="来源">
|
||||
<template #default="{ row }">
|
||||
{{ row.source === 'custom' ? '自定义' : '引用变量' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="left" width="80">
|
||||
<template #default="{ row, $index }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" content="修改" placement="top">
|
||||
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" content="删除" placement="top">
|
||||
<el-button type="primary" text @click="deleteField($index)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<h4 class="title-decoration-1 mb-16">
|
||||
Python 代码 <el-text type="info" class="color-secondary"> 使用函数时不显示 </el-text>
|
||||
</h4>
|
||||
|
||||
<CodemirrorEditor v-model:value="form.code" v-if="showEditor" @change="changeCode" />
|
||||
|
||||
<h4 class="title-decoration-1 mb-16 mt-16">
|
||||
输出变量 <el-text type="info" class="color-secondary"> 使用函数时显示 </el-text>
|
||||
</h4>
|
||||
<div class="flex-between border-r-4 p-8-12 mb-8 layout-bg lighter">
|
||||
<span>结果 {result}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button :loading="loading" @click="visible = false">取消</el-button>
|
||||
<el-button :loading="loading" @click="openDebug">调试</el-button>
|
||||
<el-button type="primary" @click="submit(FormRef)" :loading="loading">
|
||||
{{ isEdit ? '保存' : '创建' }}</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<FunctionDebugDrawer ref="FunctionDebugDrawerRef" />
|
||||
<FieldFormDialog ref="FieldFormDialogRef" @refresh="refreshFieldList" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import FieldFormDialog from './FieldFormDialog.vue'
|
||||
import FunctionDebugDrawer from './FunctionDebugDrawer.vue'
|
||||
import type { functionLibData } from '@/api/type/function-lib'
|
||||
import functionLibApi from '@/api/function-lib'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
const FieldFormDialogRef = ref()
|
||||
const FunctionDebugDrawerRef = ref()
|
||||
|
||||
const FormRef = ref()
|
||||
|
||||
const isEdit = ref(false)
|
||||
const loading = ref(false)
|
||||
const visible = ref(false)
|
||||
const showEditor = ref(false)
|
||||
const currentIndex = ref<any>(null)
|
||||
|
||||
const form = ref<functionLibData>({
|
||||
name: '',
|
||||
desc: '',
|
||||
code: '',
|
||||
input_field_list: []
|
||||
})
|
||||
|
||||
watch(visible, (bool) => {
|
||||
if (!bool) {
|
||||
isEdit.value = false
|
||||
showEditor.value = true
|
||||
currentIndex.value = null
|
||||
form.value = {
|
||||
name: '',
|
||||
desc: '',
|
||||
code: '',
|
||||
input_field_list: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '请输入函数名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
function openDebug() {
|
||||
FunctionDebugDrawerRef.value.open(form.value)
|
||||
}
|
||||
|
||||
function deleteField(index: any) {
|
||||
form.value.input_field_list?.splice(index, 1)
|
||||
}
|
||||
|
||||
function openAddDialog(data?: any, index?: any) {
|
||||
if (typeof index !== 'undefined') {
|
||||
currentIndex.value = index
|
||||
}
|
||||
|
||||
FieldFormDialogRef.value.open(data)
|
||||
}
|
||||
|
||||
function refreshFieldList(data: any) {
|
||||
if (currentIndex.value !== null) {
|
||||
form.value.input_field_list?.splice(currentIndex.value, 1, data)
|
||||
} else {
|
||||
form.value.input_field_list?.push(data)
|
||||
}
|
||||
currentIndex.value = null
|
||||
}
|
||||
|
||||
function changeCode(value: string) {
|
||||
form.value.code = value
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (isEdit.value) {
|
||||
functionLibApi.putFunctionLib(form.value?.id as string, form.value, loading).then((res) => {
|
||||
MsgSuccess('编辑成功')
|
||||
emit('refresh', res.data)
|
||||
visible.value = false
|
||||
})
|
||||
} else {
|
||||
functionLibApi.postFunctionLib(form.value, loading).then((res) => {
|
||||
MsgSuccess('创建成功')
|
||||
emit('refresh')
|
||||
visible.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const open = (data: any) => {
|
||||
if (data) {
|
||||
isEdit.value = true
|
||||
form.value = cloneDeep(data)
|
||||
}
|
||||
visible.value = true
|
||||
setTimeout(() => {
|
||||
showEditor.value = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div class="function-lib-list-container p-24" style="padding-top: 16px">
|
||||
<div class="flex-between mb-16">
|
||||
<h4>函数库</h4>
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
@change="searchHandle"
|
||||
placeholder="按函数名称搜索"
|
||||
prefix-icon="Search"
|
||||
class="w-240"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div v-loading.fullscreen.lock="paginationConfig.current_page === 1 && loading">
|
||||
<InfiniteScroll
|
||||
:size="functionLibList.length"
|
||||
:total="paginationConfig.total"
|
||||
:page_size="paginationConfig.page_size"
|
||||
v-model:current_page="paginationConfig.current_page"
|
||||
@load="getList"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-row :gutter="15">
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb-16">
|
||||
<CardAdd title="创建函数" @click="openCreateDialog()" />
|
||||
</el-col>
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
:xl="4"
|
||||
v-for="(item, index) in functionLibList"
|
||||
:key="index"
|
||||
class="mb-16"
|
||||
>
|
||||
<CardBox
|
||||
:title="item.name"
|
||||
:description="item.desc"
|
||||
class="function-lib-card cursor"
|
||||
@click="openCreateDialog(item)"
|
||||
>
|
||||
<template #icon>
|
||||
<AppAvatar class="mr-12 avatar-green" shape="square" :size="32">
|
||||
<img src="@/assets/icon_function_outlined.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="footer-content">
|
||||
<el-tooltip effect="dark" content="复制" placement="top">
|
||||
<el-button text>
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip effect="dark" content="删除" placement="top">
|
||||
<el-button text @click.stop="deleteFunctionLib(item)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</CardBox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
<FunctionFormDrawer ref="FunctionFormDrawerRef" @refresh="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import functionLibApi from '@/api/function-lib'
|
||||
import FunctionFormDrawer from './component/FunctionFormDrawer.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const FunctionFormDrawerRef = ref()
|
||||
|
||||
const functionLibList = ref<any[]>([])
|
||||
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const searchValue = ref('')
|
||||
|
||||
function openCreateDialog(data?: any) {
|
||||
FunctionFormDrawerRef.value.open(data)
|
||||
}
|
||||
|
||||
function searchHandle() {
|
||||
paginationConfig.total = 0
|
||||
paginationConfig.current_page = 1
|
||||
functionLibList.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
function deleteFunctionLib(row: any) {
|
||||
// MsgConfirm(
|
||||
// // @ts-ignore
|
||||
// `${t('views.function-lib.function-libList.card.delete.confirmTitle')}${row.name} ?`,
|
||||
// t('views.function-lib.function-libList.card.delete.confirmMessage'),
|
||||
// {
|
||||
// confirmButtonText: t('views.function-lib.function-libList.card.delete.confirmButton'),
|
||||
// cancelButtonText: t('views.function-lib.function-libList.card.delete.cancelButton'),
|
||||
// confirmButtonClass: 'danger'
|
||||
// }
|
||||
// )
|
||||
// .then(() => {})
|
||||
// .catch(() => {})
|
||||
}
|
||||
|
||||
function getList() {
|
||||
functionLibApi
|
||||
.getFunctionLib(paginationConfig, searchValue.value && { name: searchValue.value }, loading)
|
||||
.then((res: any) => {
|
||||
functionLibList.value = [...functionLibList.value, ...res.data.records]
|
||||
paginationConfig.total = res.data.total
|
||||
})
|
||||
}
|
||||
|
||||
function refresh(data: any) {
|
||||
if (data) {
|
||||
const index = functionLibList.value.findIndex((v) => v.id === data.id)
|
||||
functionLibList.value.splice(index, 1, data)
|
||||
} else {
|
||||
paginationConfig.total = 0
|
||||
paginationConfig.current_page = 1
|
||||
functionLibList.value = []
|
||||
getList()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<EditModel ref="eidtModelRef" @submit="emit('change')"></EditModel>
|
||||
<EditModel ref="editModelRef" @submit="emit('change')"></EditModel>
|
||||
</card-box>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
@ -130,7 +130,7 @@ const errMessage = computed(() => {
|
|||
return ''
|
||||
})
|
||||
const emit = defineEmits(['change', 'update:model'])
|
||||
const eidtModelRef = ref<InstanceType<typeof EditModel>>()
|
||||
const editModelRef = ref<InstanceType<typeof EditModel>>()
|
||||
let interval: any
|
||||
const deleteModel = () => {
|
||||
MsgConfirm(`删除模型 `, `是否删除模型:${props.model.name} ?`, {
|
||||
|
|
@ -154,7 +154,7 @@ const cancelDownload = () => {
|
|||
const openEditModel = () => {
|
||||
const provider = props.provider_list.find((p) => p.provider === props.model.provider)
|
||||
if (provider) {
|
||||
eidtModelRef.value?.open(provider, props.model)
|
||||
editModelRef.value?.open(provider, props.model)
|
||||
}
|
||||
}
|
||||
const icon = computed(() => {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,42 @@ export const replyNode = {
|
|||
}
|
||||
export const menuNodes = [aiChatNode, searchDatasetNode, questionNode, conditionNode, replyNode]
|
||||
|
||||
/**
|
||||
* 自定义函数配置数据
|
||||
*/
|
||||
export const functionNode = {
|
||||
type: WorkflowType.FunctionLibCustom,
|
||||
text: '通过执行自定义脚本,实现数据处理',
|
||||
label: '自定义函数',
|
||||
properties: {
|
||||
stepName: '自定义函数',
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: '结果',
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const functionLibNode = {
|
||||
type: WorkflowType.FunctionLib,
|
||||
text: '通过执行自定义脚本,实现数据处理',
|
||||
label: '自定义函数',
|
||||
properties: {
|
||||
stepName: '自定义函数',
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: '结果',
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const compareList = [
|
||||
{ value: 'is_null', label: '为空' },
|
||||
{ value: 'is_not_null', label: '不为空' },
|
||||
|
|
@ -165,7 +201,9 @@ export const nodeDict: any = {
|
|||
[WorkflowType.Condition]: conditionNode,
|
||||
[WorkflowType.Base]: baseNode,
|
||||
[WorkflowType.Start]: startNode,
|
||||
[WorkflowType.Reply]: replyNode
|
||||
[WorkflowType.Reply]: replyNode,
|
||||
[WorkflowType.FunctionLib]: functionLibNode,
|
||||
[WorkflowType.FunctionLibCustom]: functionNode
|
||||
}
|
||||
export function isWorkFlow(type: string | undefined) {
|
||||
return type === 'WORK_FLOW'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { WorkflowType } from '@/enums/workflow'
|
||||
|
||||
const end_nodes = [WorkflowType.AiChat, WorkflowType.Reply]
|
||||
const end_nodes = [
|
||||
WorkflowType.AiChat,
|
||||
WorkflowType.Reply,
|
||||
WorkflowType.FunctionLib,
|
||||
WorkflowType.FunctionLibCustom
|
||||
]
|
||||
export class WorkFlowInstance {
|
||||
nodes
|
||||
edges
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import FunctionLibNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class FunctionLibNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, FunctionLibNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'function-lib-node',
|
||||
model: AppNodeModel,
|
||||
view: FunctionLibNode
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-16">节点设置</h5>
|
||||
<h5 class="lighter mb-8">输入变量</h5>
|
||||
<el-form
|
||||
@submit.prevent
|
||||
@mousemove.stop
|
||||
@mousedown.stop
|
||||
@keydown.stop
|
||||
@click.stop
|
||||
ref="FunctionNodeFormRef"
|
||||
:model="chat_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<el-card shadow="never" class="card-never mb-16" style="--el-card-padding: 12px">
|
||||
<div v-if="chat_data.input_field_list?.length > 0">
|
||||
<template v-for="(item, index) in chat_data.input_field_list" :key="index">
|
||||
<el-form-item
|
||||
:label="item.name"
|
||||
:prop="'input_field_list.' + index + '.value'"
|
||||
:rules="{
|
||||
required: item.is_required,
|
||||
message: '请输入变量值',
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<div>
|
||||
<span
|
||||
>{{ item.name }} <span class="danger" v-if="item.is_required">*</span></span
|
||||
>
|
||||
<el-tag type="info" class="info-tag ml-4">{{ item.type }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<NodeCascader
|
||||
v-if="item.source === 'reference'"
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
placeholder="请选择变量"
|
||||
v-model="item.value"
|
||||
/>
|
||||
<el-input v-else v-model="item.value" placeholder="请输入变量值" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-text type="info" v-else> 暂无数据 </el-text>
|
||||
</el-card>
|
||||
<el-form-item label="返回内容" @click.prevent>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span>返回内容<span class="danger">*</span></span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content>
|
||||
关闭后该节点的内容则不输出给用户。 如果你想让用户看到该节点的输出内容,请打开开关。
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch size="small" v-model="chat_data.is_result" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { set } from 'lodash'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { isLastNode } from '@/workflow/common/data'
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const nodeCascaderRef = ref()
|
||||
|
||||
const form = {
|
||||
input_field_list: [],
|
||||
is_result: false
|
||||
}
|
||||
|
||||
const chat_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 FunctionNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const validate = () => {
|
||||
return FunctionNodeFormRef.value?.validate().catch((err) => {
|
||||
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)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import FunctionNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class FunctionLibCustomNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, FunctionNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'function-node',
|
||||
model: AppNodeModel,
|
||||
view: FunctionLibCustomNode
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-16">节点设置</h5>
|
||||
<div class="flex-between">
|
||||
<h5 class="lighter mb-8">输入变量</h5>
|
||||
<el-button link type="primary" @click="openAddDialog()">
|
||||
<el-icon class="mr-4"><Plus /></el-icon> 添加
|
||||
</el-button>
|
||||
</div>
|
||||
<el-form
|
||||
@submit.prevent
|
||||
@mousemove.stop
|
||||
@mousedown.stop
|
||||
@keydown.stop
|
||||
@click.stop
|
||||
ref="FunctionNodeFormRef"
|
||||
:model="chat_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<el-card shadow="never" class="card-never mb-16" style="--el-card-padding: 12px">
|
||||
<div v-if="chat_data.input_field_list?.length > 0">
|
||||
<template v-for="(item, index) in chat_data.input_field_list" :key="index">
|
||||
<el-form-item
|
||||
:label="item.name"
|
||||
:prop="'input_field_list.' + index + '.value'"
|
||||
:rules="{
|
||||
required: item.is_required,
|
||||
message: '请输入变量值',
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<div>
|
||||
<span
|
||||
>{{ item.name }} <span class="danger" v-if="item.is_required">*</span></span
|
||||
>
|
||||
<el-tag type="info" class="info-tag ml-4">{{ item.type }}</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<el-button text @click.stop="openAddDialog(item, index)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
<el-button text @click="deleteField(index)" style="margin-left: 4px !important">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<NodeCascader
|
||||
v-if="item.source === 'reference'"
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
placeholder="请选择变量"
|
||||
v-model="item.value"
|
||||
/>
|
||||
<el-input v-else v-model="item.value" placeholder="请输入变量值" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-text type="info" v-else> 暂无数据 </el-text>
|
||||
</el-card>
|
||||
|
||||
<h5 class="lighter mb-8">Python 代码</h5>
|
||||
<CodemirrorEditor
|
||||
v-model:value="chat_data.code"
|
||||
@change="changeCode"
|
||||
@wheel="wheel"
|
||||
@keydown="isKeyDown = true"
|
||||
@keyup="isKeyDown = false"
|
||||
class="mb-8"
|
||||
v-if="showEditor"
|
||||
/>
|
||||
<el-form-item label="返回内容" @click.prevent>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span>返回内容<span class="danger">*</span></span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content>
|
||||
关闭后该节点的内容则不输出给用户。 如果你想让用户看到该节点的输出内容,请打开开关。
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch size="small" v-model="chat_data.is_result" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<FieldFormDialog ref="FieldFormDialogRef" @refresh="refreshFieldList" />
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep, set } from 'lodash'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import FieldFormDialog from '@/views/function-lib/component/FieldFormDialog.vue'
|
||||
import { isLastNode } from '@/workflow/common/data'
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const isKeyDown = ref(false)
|
||||
const wheel = (e: any) => {
|
||||
if (isKeyDown.value) {
|
||||
e.preventDefault()
|
||||
} else {
|
||||
e.stopPropagation()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const FieldFormDialogRef = ref()
|
||||
const nodeCascaderRef = ref()
|
||||
|
||||
const form = {
|
||||
code: '',
|
||||
input_field_list: [],
|
||||
is_result: false
|
||||
}
|
||||
|
||||
const currentIndex = ref<any>(null)
|
||||
const showEditor = ref(false)
|
||||
|
||||
const chat_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 FunctionNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const validate = () => {
|
||||
return FunctionNodeFormRef.value?.validate().catch((err) => {
|
||||
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||
})
|
||||
}
|
||||
|
||||
function changeCode(value: string) {
|
||||
set(props.nodeModel.properties.node_data, 'code', value)
|
||||
}
|
||||
|
||||
function openAddDialog(data?: any, index?: any) {
|
||||
if (typeof index !== 'undefined') {
|
||||
currentIndex.value = index
|
||||
}
|
||||
|
||||
FieldFormDialogRef.value.open(data)
|
||||
}
|
||||
|
||||
function deleteField(index: any) {
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.input_field_list)
|
||||
list.splice(index, 1)
|
||||
set(props.nodeModel.properties.node_data, 'input_field_list', list)
|
||||
}
|
||||
|
||||
function refreshFieldList(data: any) {
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.input_field_list)
|
||||
const obj = {
|
||||
value: '',
|
||||
...data
|
||||
}
|
||||
if (currentIndex.value !== null) {
|
||||
list.splice(currentIndex.value, 1, obj)
|
||||
} else {
|
||||
list.push(obj)
|
||||
}
|
||||
set(props.nodeModel.properties.node_data, 'input_field_list', list)
|
||||
currentIndex.value = null
|
||||
}
|
||||
|
||||
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)
|
||||
setTimeout(() => {
|
||||
showEditor.value = true
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
Loading…
Reference in New Issue