From 510fae6bf126929a3135c860647d36c88d29e383 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Mon, 10 Mar 2025 16:07:20 +0800 Subject: [PATCH] feat: Support internal functionlib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --story=1017939 --user=刘瑞斌 【函数库】- 支持 系统内置函数 https://www.tapd.cn/57709429/s/1665943 --- .../impl/base_function_lib_node.py | 6 +- .../serializers/application_serializers.py | 8 +- ...function_type_functionlib_icon_and_more.py | 107 +++++++++++ ...ionlib_icon_functionlib_init_field_list.py | 23 --- apps/function_lib/models/function.py | 8 + apps/function_lib/serializers/__init__.py | 8 + .../serializers/function_lib_serializer.py | 138 +++++++++++++-- apps/function_lib/swagger_api/__init__.py | 8 + apps/function_lib/urls.py | 1 + apps/function_lib/views/function_lib_views.py | 14 +- ui/src/api/function-lib.ts | 10 +- ui/src/assets/fx/bochaai/icon.png | Bin 0 -> 13263 bytes ui/src/assets/fx/bochaai/index.vue | 29 +++ ui/src/assets/fx/google_search/icon.png | Bin 0 -> 6568 bytes ui/src/assets/fx/google_search/index.vue | 29 +++ ui/src/assets/fx/langsearch/icon.png | Bin 0 -> 68812 bytes ui/src/assets/fx/langsearch/index.vue | 29 +++ ui/src/locales/lang/en-US/common.ts | 1 + .../locales/lang/en-US/views/function-lib.ts | 2 + ui/src/locales/lang/zh-CN/common.ts | 1 + .../locales/lang/zh-CN/views/function-lib.ts | 2 + ui/src/locales/lang/zh-Hant/common.ts | 1 + .../lang/zh-Hant/views/function-lib.ts | 2 + .../component/EditAvatarDialog.vue | 7 +- .../component/FunctionDebugDrawer.vue | 18 +- .../component/FunctionFormDrawer.vue | 6 +- .../component/InitParamDrawer.vue | 21 +-- ui/src/views/function-lib/index.vue | 167 ++++++++++++++++-- 28 files changed, 558 insertions(+), 88 deletions(-) create mode 100644 apps/function_lib/migrations/0003_functionlib_function_type_functionlib_icon_and_more.py delete mode 100644 apps/function_lib/migrations/0003_functionlib_icon_functionlib_init_field_list.py create mode 100644 apps/function_lib/serializers/__init__.py create mode 100644 apps/function_lib/swagger_api/__init__.py create mode 100644 ui/src/assets/fx/bochaai/icon.png create mode 100644 ui/src/assets/fx/bochaai/index.vue create mode 100644 ui/src/assets/fx/google_search/icon.png create mode 100644 ui/src/assets/fx/google_search/index.vue create mode 100644 ui/src/assets/fx/langsearch/icon.png create mode 100644 ui/src/assets/fx/langsearch/index.vue diff --git a/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py b/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py index 4f5399f6b..ba1b78af6 100644 --- a/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py +++ b/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py @@ -16,6 +16,7 @@ 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 common.util.rsa_util import rsa_long_decrypt from function_lib.models.function import FunctionLib from smartdoc.const import CONFIG @@ -107,8 +108,11 @@ class BaseFunctionLibNodeNode(IFunctionLibNode): ), **field} for field in function_lib.input_field_list]} + self.context['params'] = params - result = function_executor.exec_code(function_lib.code, params) + # 合并初始化参数 + all_params = json.loads(rsa_long_decrypt(function_lib.init_params)) | params + result = function_executor.exec_code(function_lib.code, all_params) return NodeResult({'result': result}, {}, _write_context=write_context) def get_details(self, index: int, **kwargs): diff --git a/apps/application/serializers/application_serializers.py b/apps/application/serializers/application_serializers.py index 543f53ae2..7ea5b011c 100644 --- a/apps/application/serializers/application_serializers.py +++ b/apps/application/serializers/application_serializers.py @@ -45,7 +45,7 @@ from common.util.file_util import get_file_content from dataset.models import DataSet, Document, Image from dataset.serializers.common_serializers import list_paragraph, get_embedding_model_by_dataset_id_list from embedding.models import SearchMode -from function_lib.models.function import FunctionLib, PermissionType +from function_lib.models.function import FunctionLib, PermissionType, FunctionType from function_lib.serializers.function_lib_serializer import FunctionLibSerializer, FunctionLibModelSerializer from setting.models import AuthOperate, TeamMemberPermission from setting.models.model_management import Model @@ -810,8 +810,10 @@ class ApplicationSerializer(serializers.Serializer): if with_valid: self.is_valid(raise_exception=True) application = QuerySet(Application).filter(id=self.data.get("application_id")).first() - return FunctionLibSerializer.Query(data={'user_id': application.user_id, 'is_active': True}).list( - with_valid=True) + return FunctionLibSerializer.Query( + data={'user_id': application.user_id, 'is_active': True, + 'function_type': FunctionType.PUBLIC} + ).list(with_valid=True) def get_function_lib(self, function_lib_id, with_valid=True): if with_valid: diff --git a/apps/function_lib/migrations/0003_functionlib_function_type_functionlib_icon_and_more.py b/apps/function_lib/migrations/0003_functionlib_function_type_functionlib_icon_and_more.py new file mode 100644 index 000000000..28f9c288c --- /dev/null +++ b/apps/function_lib/migrations/0003_functionlib_function_type_functionlib_icon_and_more.py @@ -0,0 +1,107 @@ +# Generated by Django 4.2.15 on 2025-03-13 07:21 + +from django.db import migrations, models + +function_template = ''' +INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-10 06:20:35.945414 +00:00', '2025-03-10 09:19:23.608026 +00:00', 'c75cb48e-fd77-11ef-84d2-5618c4394482', '博查AI', '从博查搜索任何信息和网页URL', e'def bocha_search(query, apikey): + import requests + import json + url = "https://api.bochaai.com/v1/web-search" + payload = json.dumps({ + "query": query, + "Boolean": "true", + "count": 8 + }) + + headers = { + "Authorization": "Bearer " + apikey, #鉴权参数,示例:Bearer xxxxxx,API KEY请先前往博查AI开放平台(https://open.bochaai.com)> API KEY 管理中获取。 + "Content-Type": "application/json" + } + + response = requests.request("POST", url, headers=headers, data=payload) + if response.status_code == 200: + return response.json() + else: + raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}") + return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/src/assets/fx/bochaai/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "apikey", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "apikey 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "apikey长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL); +INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-02-26 03:36:48.187286 +00:00', '2025-03-11 07:23:46.123972 +00:00', 'e89ad2ae-f3f2-11ef-ad09-0242ac110002', 'Google Search', 'Google Web Search', e'def google_search(query, apikey, cx): + import requests + import json + url = "https://customsearch.googleapis.com/customsearch/v1" + params = { + "q": query, + "key": apikey, + "cx": cx, + "num": 10, # 每次最多返回10条 + } + + response = requests.get(url, params=params) + if response.status_code == 200: + return response.json() + else: + raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}") + return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/src/assets/fx/google_search/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "apikey", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "apikey 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "apikey长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "cx", "label": "cx", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "cx 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "cx长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL); +INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-02-25 07:44:40.141515 +00:00', '2025-03-11 06:33:53.248495 +00:00', '5e912f00-f34c-11ef-8a9c-5618c4394482', 'LangSearch API', e'A Web Search API supporting natural language search +', e' +def langsearch(query, apikey): + import json + import requests + + url = "https://api.langsearch.com/v1/web-search" + payload = json.dumps({ + "query": query, + "summary": True, + "freshness": "noLimit", + "livecrawl": True, + "count": 20 + }) + headers = { + "Authorization": apikey, + "Content-Type": "application/json" + } + # key从官网申请 https://langsearch.com/ + response = requests.request("POST", url, headers=headers, data=payload) + if response.status_code == 200: + return response.json() + else: + raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}") + return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/src/assets/fx/langsearch/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "apikey", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "apikey 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "apikey长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL); + +''' + + +class Migration(migrations.Migration): + dependencies = [ + ('function_lib', '0002_functionlib_is_active_functionlib_permission_type'), + ] + + operations = [ + migrations.AddField( + model_name='functionlib', + name='function_type', + field=models.CharField(choices=[('INTERNAL', '内置'), ('PUBLIC', '公开')], + default='PUBLIC', max_length=20, verbose_name='函数类型'), + ), + migrations.AddField( + model_name='functionlib', + name='icon', + field=models.CharField(default='/ui/favicon.ico', max_length=256, + verbose_name='函数库icon'), + ), + migrations.AddField( + model_name='functionlib', + name='init_field_list', + field=models.JSONField(default=list, verbose_name='启动字段列表'), + ), + migrations.AddField( + model_name='functionlib', + name='init_params', + field=models.CharField(max_length=102400, null=True, verbose_name='初始化参数'), + ), + migrations.AddField( + model_name='functionlib', + name='template_id', + field=models.UUIDField(default=None, null=True, verbose_name='模版id'), + ), + migrations.RunSQL(function_template) + ] diff --git a/apps/function_lib/migrations/0003_functionlib_icon_functionlib_init_field_list.py b/apps/function_lib/migrations/0003_functionlib_icon_functionlib_init_field_list.py deleted file mode 100644 index 3ea3a4637..000000000 --- a/apps/function_lib/migrations/0003_functionlib_icon_functionlib_init_field_list.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.15 on 2025-03-07 10:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('function_lib', '0002_functionlib_is_active_functionlib_permission_type'), - ] - - operations = [ - migrations.AddField( - model_name='functionlib', - name='icon', - field=models.CharField(default='/ui/favicon.ico', max_length=256, verbose_name='函数库icon'), - ), - migrations.AddField( - model_name='functionlib', - name='init_field_list', - field=models.JSONField(default=list, verbose_name='启动字段列表'), - ), - ] diff --git a/apps/function_lib/models/function.py b/apps/function_lib/models/function.py index a95339545..037f50995 100644 --- a/apps/function_lib/models/function.py +++ b/apps/function_lib/models/function.py @@ -19,6 +19,10 @@ class PermissionType(models.TextChoices): PUBLIC = "PUBLIC", '公开' PRIVATE = "PRIVATE", "私有" +class FunctionType(models.TextChoices): + INTERNAL = "INTERNAL", '内置' + PUBLIC = "PUBLIC", "公开" + class FunctionLib(AppModelMixin): id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id") @@ -34,6 +38,10 @@ class FunctionLib(AppModelMixin): is_active = models.BooleanField(default=True) permission_type = models.CharField(max_length=20, verbose_name='权限类型', choices=PermissionType.choices, default=PermissionType.PRIVATE) + function_type = models.CharField(max_length=20, verbose_name='函数类型', choices=FunctionType.choices, + default=FunctionType.PUBLIC) + template_id = models.UUIDField(max_length=128, verbose_name="模版id", null=True, default=None) + init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True) class Meta: db_table = "function_lib" diff --git a/apps/function_lib/serializers/__init__.py b/apps/function_lib/serializers/__init__.py new file mode 100644 index 000000000..a68550e90 --- /dev/null +++ b/apps/function_lib/serializers/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py.py + @date:2024/8/2 14:55 + @desc: +""" diff --git a/apps/function_lib/serializers/function_lib_serializer.py b/apps/function_lib/serializers/function_lib_serializer.py index a007cc287..52ff5a002 100644 --- a/apps/function_lib/serializers/function_lib_serializer.py +++ b/apps/function_lib/serializers/function_lib_serializer.py @@ -13,7 +13,7 @@ import uuid from django.core import validators from django.db import transaction -from django.db.models import QuerySet, Q +from django.db.models import QuerySet, Q, OuterRef, Exists from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ from rest_framework import serializers, status @@ -24,8 +24,9 @@ from common.field.common import UploadedFileField, UploadedImageField from common.response import result from common.util.field_message import ErrMessage from common.util.function_code import FunctionExecutor -from dataset.models import Image -from function_lib.models.function import FunctionLib +from common.util.rsa_util import rsa_long_decrypt, rsa_long_encrypt +from dataset.models import File +from function_lib.models.function import FunctionLib, PermissionType, FunctionType from smartdoc.const import CONFIG function_executor = FunctionExecutor(CONFIG.get('SANDBOX')) @@ -35,11 +36,33 @@ class FlibInstance: self.function_lib = function_lib self.version = version +def encryption(message: str): + """ + 加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890 + :param message: + :return: + """ + max_pre_len = 8 + max_post_len = 4 + message_len = len(message) + pre_len = int(message_len / 5 * 2) + post_len = int(message_len / 5 * 1) + pre_str = "".join([message[index] for index in + range(0, + max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int( + pre_len))]) + end_str = "".join( + [message[index] for index in + range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len), + message_len)]) + content = "***************" + return pre_str + content + end_str + class FunctionLibModelSerializer(serializers.ModelSerializer): class Meta: model = FunctionLib - fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list','init_field_list', 'permission_type', 'is_active', 'user_id', + fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list','init_field_list', 'init_params', 'permission_type', 'is_active', 'user_id', 'template_id', 'create_time', 'update_time'] @@ -66,6 +89,7 @@ class DebugInstance(serializers.Serializer): debug_field_list = DebugField(required=True, many=True) input_field_list = FunctionLibInputField(required=True, many=True) init_field_list = serializers.ListField(required=False, default=list) + init_params = serializers.JSONField(required=False, default=dict) code = serializers.CharField(required=True, error_messages=ErrMessage.char(_('function content'))) @@ -116,6 +140,8 @@ class FunctionLibSerializer(serializers.Serializer): user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id'))) select_user_id = serializers.CharField(required=False, allow_null=True, allow_blank=True) + function_type = serializers.CharField(required=False, allow_null=True, allow_blank=True) + def get_query_set(self): query_set = QuerySet(FunctionLib).filter( @@ -128,19 +154,38 @@ class FunctionLibSerializer(serializers.Serializer): query_set = query_set.filter(is_active=self.data.get('is_active')) if self.data.get('select_user_id') is not None: query_set = query_set.filter(user_id=self.data.get('select_user_id')) + if self.data.get('function_type') is not None: + query_set = query_set.filter(function_type=self.data.get('function_type')) query_set = query_set.order_by("-create_time") + + subquery = FunctionLib.objects.filter(template_id=OuterRef('id')) + subquery = subquery.filter(user_id=self.data.get('user_id')) + query_set = query_set.annotate(added=Exists(subquery)) + 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()] + rs = [] + for item in self.get_query_set(): + data = {**FunctionLibModelSerializer(item).data, 'init_params': None} + rs.append(data) + return rs def page(self, current_page: int, page_size: int, with_valid=True): if with_valid: self.is_valid(raise_exception=True) + + def post_records_handler(row): + return { + **FunctionLibModelSerializer(row).data, + 'added': row.added, + 'init_params': None + } + return page_search(current_page, page_size, self.get_query_set(), - post_records_handler=lambda row: FunctionLibModelSerializer(row).data) + post_records_handler=post_records_handler) class Create(serializers.Serializer): user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id'))) @@ -169,8 +214,7 @@ class FunctionLibSerializer(serializers.Serializer): input_field_list = debug_instance.get('input_field_list') code = debug_instance.get('code') debug_field_list = debug_instance.get('debug_field_list') - init_field_list = debug_instance.get('init_field_list') - init_params = {field.get('field'): field.get('value') if field.get('value', None) is not None else field.get('default_value') for field in init_field_list} + init_params = debug_instance.get('init_params') params = {field.get('name'): self.convert_value(field.get('name'), field.get('value'), field.get('type'), field.get('is_required')) for field in @@ -227,6 +271,9 @@ class FunctionLibSerializer(serializers.Serializer): def delete(self, with_valid=True): if with_valid: self.is_valid(raise_exception=True) + fun = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() + if fun.template_id is None and fun.icon != '/ui/favicon.ico': + QuerySet(File).filter(id=fun.icon.split('/')[-1]).delete() QuerySet(FunctionLib).filter(id=self.data.get('id')).delete() return True @@ -234,9 +281,19 @@ class FunctionLibSerializer(serializers.Serializer): 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', 'init_field_list', 'permission_type', 'is_active'] + edit_field_list = ['name', 'desc', 'code', 'icon', 'input_field_list', 'init_field_list', 'init_params', 'permission_type', 'is_active'] edit_dict = {field: instance.get(field) for field in edit_field_list if ( field in instance and instance.get(field) is not None)} + + function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() + if 'init_params' in edit_dict: + if function_lib.init_params: + old_init_params = json.loads(rsa_long_decrypt(function_lib.init_params)) + for key in edit_dict['init_params']: + if edit_dict['init_params'][key] == encryption(old_init_params[key]): + edit_dict['init_params'][key] = old_init_params[key] + + edit_dict['init_params'] = rsa_long_encrypt(json.dumps(edit_dict['init_params'])) QuerySet(FunctionLib).filter(id=self.data.get('id')).update(**edit_dict) return self.one(False) @@ -247,7 +304,15 @@ class FunctionLibSerializer(serializers.Serializer): Q(user_id=self.data.get('user_id')) | Q(permission_type='PUBLIC')).exists(): raise AppApiException(500, _('Function does not exist')) function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() - return FunctionLibModelSerializer(function_lib).data + if function_lib.init_params: + function_lib.init_params = json.loads(rsa_long_decrypt(function_lib.init_params)) + if function_lib.init_field_list: + password_fields = [i["field"] for i in function_lib.init_field_list if i.get("input_type") == "PasswordInput"] + if function_lib.init_params: + for k in function_lib.init_params: + if k in password_fields: + function_lib.init_params[k] = encryption(function_lib.init_params[k]) + return {**FunctionLibModelSerializer(function_lib).data, 'init_params': function_lib.init_params} def export(self, with_valid=True): try: @@ -284,6 +349,7 @@ class FunctionLibSerializer(serializers.Serializer): code=function_lib.get('code'), user_id=user_id, input_field_list=function_lib.get('input_field_list'), + init_field_list=function_lib.get('init_field_list'), permission_type='PRIVATE', is_active=function_lib.get('is_active')) function_lib_model.save() @@ -300,10 +366,54 @@ class FunctionLibSerializer(serializers.Serializer): functionLib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() if functionLib is None: raise AppApiException(500, _('Function does not exist')) - image_id = uuid.uuid1() - image = Image(id=image_id, image=self.data.get('image').read(), image_name=self.data.get('image').name) - image.save() - functionLib.icon = f'/api/image/{image_id}' + # 删除旧的图片 + if functionLib.icon != '/ui/favicon.ico': + QuerySet(File).filter(id=functionLib.icon.split('/')[-1]).delete() + if self.data.get('image') is None: + functionLib.icon = '/ui/favicon.ico' + else: + meta = { + 'debug': False + } + file_id = uuid.uuid1() + file = File(id=file_id, file_name=self.data.get('image').name, meta=meta) + file.save(self.data.get('image').read()) + + functionLib.icon = f'/api/file/{file_id}' functionLib.save() return functionLib.icon + + class InternalFunction(serializers.Serializer): + id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID"))) + user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID"))) + + def add(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + + if QuerySet(FunctionLib).filter(template_id=self.data.get('id')).filter( + user_id=self.data.get('user_id')).exists(): + raise AppApiException(500, _('Function already exists')) + + internal_function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() + if internal_function_lib is None: + raise AppApiException(500, _('Function does not exist')) + + function_lib = FunctionLib( + id=uuid.uuid1(), + name=internal_function_lib.name, + desc=internal_function_lib.desc, + code=internal_function_lib.code, + user_id=self.data.get('user_id'), + input_field_list=internal_function_lib.input_field_list, + init_field_list=internal_function_lib.init_field_list, + permission_type=PermissionType.PRIVATE, + template_id=internal_function_lib.id, + function_type=FunctionType.PUBLIC, + icon=internal_function_lib.icon, + is_active=False + ) + function_lib.save() + + return FunctionLibModelSerializer(function_lib).data diff --git a/apps/function_lib/swagger_api/__init__.py b/apps/function_lib/swagger_api/__init__.py new file mode 100644 index 000000000..a68550e90 --- /dev/null +++ b/apps/function_lib/swagger_api/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py.py + @date:2024/8/2 14:55 + @desc: +""" diff --git a/apps/function_lib/urls.py b/apps/function_lib/urls.py index d0c89f17a..036d68926 100644 --- a/apps/function_lib/urls.py +++ b/apps/function_lib/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ path('function_lib//export', views.FunctionLibView.Export.as_view()), path('function_lib/import', views.FunctionLibView.Import.as_view()), path('function_lib//edit_icon', views.FunctionLibView.EditIcon.as_view()), + path('function_lib//add_internal_fun', views.FunctionLibView.AddInternalFun.as_view()), path('function_lib/pylint', views.PyLintView.as_view()), path('function_lib/', views.FunctionLibView.Operate.as_view()), path("function_lib//", views.FunctionLibView.Page.as_view(), diff --git a/apps/function_lib/views/function_lib_views.py b/apps/function_lib/views/function_lib_views.py index 51c15ea02..c0213b65c 100644 --- a/apps/function_lib/views/function_lib_views.py +++ b/apps/function_lib/views/function_lib_views.py @@ -34,6 +34,7 @@ class FunctionLibView(APIView): FunctionLibSerializer.Query( data={'name': request.query_params.get('name'), 'desc': request.query_params.get('desc'), + 'function_type': request.query_params.get('function_type'), 'user_id': request.user.id}).list()) @action(methods=['POST'], detail=False) @@ -107,6 +108,7 @@ class FunctionLibView(APIView): FunctionLibSerializer.Query( data={'name': request.query_params.get('name'), 'desc': request.query_params.get('desc'), + 'function_type': request.query_params.get('function_type'), 'user_id': request.user.id, 'select_user_id': request.query_params.get('select_user_id')}).page( current_page, page_size)) @@ -148,4 +150,14 @@ class FunctionLibView(APIView): return result.success( FunctionLibSerializer.IconOperate( data={'id': id, 'user_id': request.user.id, - 'image': request.FILES.get('file')}).edit(request.data)) \ No newline at end of file + 'image': request.FILES.get('file')}).edit(request.data)) + + class AddInternalFun(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['GET'], detail=False) + @has_permissions(RoleConstants.ADMIN, RoleConstants.USER) + def get(self, request: Request, id: str): + return result.success( + FunctionLibSerializer.InternalFunction( + data={'id': id, 'user_id': request.user.id}).add()) \ No newline at end of file diff --git a/ui/src/api/function-lib.ts b/ui/src/api/function-lib.ts index 7fdbad8b0..004cd7217 100644 --- a/ui/src/api/function-lib.ts +++ b/ui/src/api/function-lib.ts @@ -120,6 +120,13 @@ const putFunctionLibIcon: ( return put(`${prefix}/${id}/edit_icon`, data, undefined, loading) } +const addInternalFunction: ( + id: string, + loading?: Ref +) => Promise> = (id, loading) => { + return get(`${prefix}/${id}/add_internal_fun`, undefined, loading) +} + const importFunctionLib: (data: any, loading?: Ref) => Promise> = ( data, loading @@ -137,5 +144,6 @@ export default { exportFunctionLib, importFunctionLib, pylint, - putFunctionLibIcon + putFunctionLibIcon, + addInternalFunction } diff --git a/ui/src/assets/fx/bochaai/icon.png b/ui/src/assets/fx/bochaai/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..530a086ed209af485ddeba1bcafe287b5165d039 GIT binary patch literal 13263 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�^{uMGPRv1kGPp5Cd_Jt5sF@my+ds4i&^7n@a6pZ4lO%3gtk zsw0WF>SvvN_FXPwWd3^js{t0uO+~2~`h;^VpA3Yl*9{a5P%EofblVHs6VDmHl2$4$ z##d5oy*;XPq%&lE|8r{WeW^b(W(H8BMKeoWthR@NCXe>)PjAK3lh5D1B8kw+Lk}Be z$EPNS?_Z1B4~ge&AY7kott1T;qKBn*(zpz`zilS6E}`qP7}PWyCYkJE>sL6GrYEm6 zuF~k#Y~07D0^Rh3_5~S($#Y8nt!FDkC3bA=b>1tB9cSvU4`C0HPwZ3TFv_#t7xT~2 z#H!1D`6q_k)bw;p%6HNk@%YCos%ZNsp)Oo+gH~lBCyJ@HI4&}^`7~UL{`<4Uf!v4x z-JOt0DCFhM$o-p9od{sb>-`o6PZ@nrD|1h4VaqSpuLXdIi-#Y`#S7%-(&qXg%q=9$ z&&S5aCCtSY7iH=3{{fs`t?X^S{?CAi_4S!o0Q3Lr!PDN^+QZY_+3kPE)KA~zy + + + + + + \ No newline at end of file diff --git a/ui/src/assets/fx/google_search/icon.png b/ui/src/assets/fx/google_search/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7b903159b0c47a6c56d9d23773fd2c7510282ebd GIT binary patch literal 6568 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 + + + + + + \ No newline at end of file diff --git a/ui/src/assets/fx/langsearch/icon.png b/ui/src/assets/fx/langsearch/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..72ca125f36681abc8a2bdd4085aeea02b3d339a8 GIT binary patch literal 68812 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 wy!SRsaA1 literal 0 HcmV?d00001 diff --git a/ui/src/assets/fx/langsearch/index.vue b/ui/src/assets/fx/langsearch/index.vue new file mode 100644 index 000000000..c03b178d7 --- /dev/null +++ b/ui/src/assets/fx/langsearch/index.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/ui/src/locales/lang/en-US/common.ts b/ui/src/locales/lang/en-US/common.ts index 12f83c7fe..b00021ee0 100644 --- a/ui/src/locales/lang/en-US/common.ts +++ b/ui/src/locales/lang/en-US/common.ts @@ -35,6 +35,7 @@ export default { private: 'Private', paramSetting: 'Parameter Settings', creator: 'Creator', + author: 'Author', debug: 'Debug', required: 'Required', noData: 'No data', diff --git a/ui/src/locales/lang/en-US/views/function-lib.ts b/ui/src/locales/lang/en-US/views/function-lib.ts index 2f56c9456..1ca7103b3 100644 --- a/ui/src/locales/lang/en-US/views/function-lib.ts +++ b/ui/src/locales/lang/en-US/views/function-lib.ts @@ -1,5 +1,7 @@ export default { title: 'Function', + internalTitle: 'Internal Function', + added: 'Added', createFunction: 'Create Function', editFunction: 'Edit Function', copyFunction: 'Copy Function', diff --git a/ui/src/locales/lang/zh-CN/common.ts b/ui/src/locales/lang/zh-CN/common.ts index da89789ee..ba790f282 100644 --- a/ui/src/locales/lang/zh-CN/common.ts +++ b/ui/src/locales/lang/zh-CN/common.ts @@ -35,6 +35,7 @@ export default { private: '私有', paramSetting: '参数设置', creator: '创建者', + author: '作者', debug: '调试', required: '必填', noData: '暂无数据', diff --git a/ui/src/locales/lang/zh-CN/views/function-lib.ts b/ui/src/locales/lang/zh-CN/views/function-lib.ts index 2ad28bad3..ac6c00dce 100644 --- a/ui/src/locales/lang/zh-CN/views/function-lib.ts +++ b/ui/src/locales/lang/zh-CN/views/function-lib.ts @@ -1,5 +1,7 @@ export default { title: '函数库', + internalTitle: '内置函数', + added: '已添加', createFunction: '创建函数', editFunction: '编辑函数', copyFunction: '复制函数', diff --git a/ui/src/locales/lang/zh-Hant/common.ts b/ui/src/locales/lang/zh-Hant/common.ts index ea4bbe118..5775f5813 100644 --- a/ui/src/locales/lang/zh-Hant/common.ts +++ b/ui/src/locales/lang/zh-Hant/common.ts @@ -35,6 +35,7 @@ export default { private: '私有', paramSetting: '參數設定', creator: '建立者', + author: '作者', debug: '調試', required: '必填', noData: '暂无数据', diff --git a/ui/src/locales/lang/zh-Hant/views/function-lib.ts b/ui/src/locales/lang/zh-Hant/views/function-lib.ts index f7c8eab49..6b4a9b339 100644 --- a/ui/src/locales/lang/zh-Hant/views/function-lib.ts +++ b/ui/src/locales/lang/zh-Hant/views/function-lib.ts @@ -1,5 +1,7 @@ export default { title: '函數庫', + internalTitle: '內置函數', + added: '已新增', createFunction: '建立函數', editFunction: '編輯函數', copyFunction: '複製函數', diff --git a/ui/src/views/function-lib/component/EditAvatarDialog.vue b/ui/src/views/function-lib/component/EditAvatarDialog.vue index 0dd3189eb..21c96c4cd 100644 --- a/ui/src/views/function-lib/component/EditAvatarDialog.vue +++ b/ui/src/views/function-lib/component/EditAvatarDialog.vue @@ -111,11 +111,8 @@ const onChange = (file: any) => { function submit() { if (radioType.value === 'default') { - // application.asyncPutApplication(props.id as string, { icon: defaultIcon }, loading).then((res: any) => { - // emit('refresh') - // MsgSuccess(t('views.applicationOverview.appInfo.EditAvatarDialog.setSuccess')) - // dialogVisible.value = false - // }) + emit('refresh', '/ui/favicon.ico') + dialogVisible.value = false } else if (radioType.value === 'custom' && iconFile.value) { let fd = new FormData() fd.append('file', iconFile.value.raw) diff --git a/ui/src/views/function-lib/component/FunctionDebugDrawer.vue b/ui/src/views/function-lib/component/FunctionDebugDrawer.vue index 84f81aff3..45e234790 100644 --- a/ui/src/views/function-lib/component/FunctionDebugDrawer.vue +++ b/ui/src/views/function-lib/component/FunctionDebugDrawer.vue @@ -16,9 +16,9 @@ {{ $t('common.param.initParam') }} -
{{ item.name }} *{{ item.name }} * {{ item.type }}
@@ -114,18 +114,19 @@ import type { FormInstance } from 'element-plus' import DynamicsForm from '@/components/dynamics-form/index.vue' const FormRef = ref() +const dynamicsFormRef = ref() const loading = ref(false) const debugVisible = ref(false) const showResult = ref(false) const isSuccess = ref(false) const result = ref('') -const init_form_data = ref({}) const form = ref({ debug_field_list: [], code: '', input_field_list: [], - init_field_list: [] + init_field_list: [], + init_params: {} }) watch(debugVisible, (bool) => { @@ -137,14 +138,15 @@ watch(debugVisible, (bool) => { debug_field_list: [], code: '', input_field_list: [], - init_field_list: [] + init_field_list: [], + init_params: {} } } }) const submit = async (formEl: FormInstance | undefined) => { const validate = formEl ? formEl.validate() : Promise.resolve() - validate.then(() => { + Promise.all([dynamicsFormRef.value?.validate(), validate]).then(() => { functionLibApi.postFunctionLibDebug(form.value, loading).then((res) => { if (res.code === 500) { showResult.value = true diff --git a/ui/src/views/function-lib/component/FunctionFormDrawer.vue b/ui/src/views/function-lib/component/FunctionFormDrawer.vue index 95d748b00..0e075608a 100644 --- a/ui/src/views/function-lib/component/FunctionFormDrawer.vue +++ b/ui/src/views/function-lib/component/FunctionFormDrawer.vue @@ -26,13 +26,13 @@ @mouseleave="showEditIcon = false" > - + +

diff --git a/ui/src/views/function-lib/component/InitParamDrawer.vue b/ui/src/views/function-lib/component/InitParamDrawer.vue index f5923ca00..cbc895a09 100644 --- a/ui/src/views/function-lib/component/InitParamDrawer.vue +++ b/ui/src/views/function-lib/component/InitParamDrawer.vue @@ -11,10 +11,10 @@

-
+
({}) const form = ref({ - init_field_list: [] + init_params: {} }) watch(debugVisible, (bool) => { @@ -63,17 +62,13 @@ watch(debugVisible, (bool) => { isSuccess.value = false result.value = '' form.value = { - init_field_list: [] + init_params: {} } } }) const submit = async () => { dynamicsFormRef.value.validate().then(() => { - form.value.init_field_list.forEach((item: any) => { - item.value = init_form_data.value[item.field] - }) - // console.log(init_form_data.value) functionLibApi.putFunctionLib(form.value?.id as string, form.value, loading) .then((res) => { MsgSuccess(t('common.editSuccess')) @@ -87,17 +82,15 @@ const open = (data: any) => { if (data) { form.value = cloneDeep(data) } - init_form_data.value = form.value.init_field_list + const init_params = form.value.init_field_list .map((item: any) => { - if (item.value) { - return { [item.field]: item.value } - } if (item.show_default_value === false) { return { [item.field]: undefined } } return { [item.field]: item.default_value } }) .reduce((x: any, y: any) => ({ ...x, ...y }), {}) + form.value.init_params = { ...init_params, ...form.value.init_params } debugVisible.value = true } diff --git a/ui/src/views/function-lib/index.vue b/ui/src/views/function-lib/index.vue index 25ba3a03a..d58165ebd 100644 --- a/ui/src/views/function-lib/index.vue +++ b/ui/src/views/function-lib/index.vue @@ -1,7 +1,11 @@