From fdb81b320fd65c936ba3958cf1470d28bd35c8e0 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Wed, 16 Apr 2025 12:50:44 +0800 Subject: [PATCH] feat: implement Tool model and related API for Tool management --- apps/common/constants/permission_constants.py | 2 + apps/maxkb/settings/base.py | 1 + apps/maxkb/urls.py | 1 + apps/tools/__init__.py | 0 apps/tools/admin.py | 3 + apps/tools/api/__init__.py | 1 + apps/tools/api/tool.py | 20 ++++++ apps/tools/apps.py | 6 ++ apps/tools/migrations/0001_initial.py | 41 ++++++++++++ apps/tools/migrations/__init__.py | 0 apps/tools/models/__init__.py | 1 + apps/tools/models/tool.py | 38 +++++++++++ apps/tools/serializers/__init__.py | 1 + apps/tools/serializers/tool.py | 66 +++++++++++++++++++ apps/tools/tests.py | 3 + apps/tools/urls.py | 8 +++ apps/tools/views/__init__.py | 1 + apps/tools/views/tool.py | 28 ++++++++ pyproject.toml | 1 + 19 files changed, 222 insertions(+) create mode 100644 apps/tools/__init__.py create mode 100644 apps/tools/admin.py create mode 100644 apps/tools/api/__init__.py create mode 100644 apps/tools/api/tool.py create mode 100644 apps/tools/apps.py create mode 100644 apps/tools/migrations/0001_initial.py create mode 100644 apps/tools/migrations/__init__.py create mode 100644 apps/tools/models/__init__.py create mode 100644 apps/tools/models/tool.py create mode 100644 apps/tools/serializers/__init__.py create mode 100644 apps/tools/serializers/tool.py create mode 100644 apps/tools/tests.py create mode 100644 apps/tools/urls.py create mode 100644 apps/tools/views/__init__.py create mode 100644 apps/tools/views/tool.py diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index c8d45d2e66c..a2a5a94cfd9 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -109,6 +109,8 @@ class PermissionConstants(Enum): RoleConstants.USER]) USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN]) USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN]) + TOOL_CREATE = Permission(group=Group.USER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, + RoleConstants.USER]) def get_workspace_application_permission(self): return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate, diff --git a/apps/maxkb/settings/base.py b/apps/maxkb/settings/base.py index 43d3d0ad327..1c601d49b56 100644 --- a/apps/maxkb/settings/base.py +++ b/apps/maxkb/settings/base.py @@ -39,6 +39,7 @@ 'drf_spectacular', 'drf_spectacular_sidecar', 'users.apps.UsersConfig', + 'tools.apps.ToolConfig', 'common', 'system_manage' ] diff --git a/apps/maxkb/urls.py b/apps/maxkb/urls.py index 8d56f0e6adf..ae906767df7 100644 --- a/apps/maxkb/urls.py +++ b/apps/maxkb/urls.py @@ -30,6 +30,7 @@ SpectacularRedocView.authentication_classes = [AnonymousAuthentication] urlpatterns = [ path("api/", include("users.urls")), + path("api/", include("tools.urls")) ] urlpatterns += [ path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由,下面两个ui也是根据这个配置文件来生成的 diff --git a/apps/tools/__init__.py b/apps/tools/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/tools/admin.py b/apps/tools/admin.py new file mode 100644 index 00000000000..8c38f3f3dad --- /dev/null +++ b/apps/tools/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/tools/api/__init__.py b/apps/tools/api/__init__.py new file mode 100644 index 00000000000..9bad5790a57 --- /dev/null +++ b/apps/tools/api/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/apps/tools/api/tool.py b/apps/tools/api/tool.py new file mode 100644 index 00000000000..39c3e23a2d1 --- /dev/null +++ b/apps/tools/api/tool.py @@ -0,0 +1,20 @@ +# coding=utf-8 + +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer +from tools.serializers.tool import ToolModelSerializer, ToolCreateRequest + + +class ToolCreateResponse(ResultSerializer): + def get_data(self): + return ToolModelSerializer() + + +class ToolCreateAPI(APIMixin): + @staticmethod + def get_request(): + return ToolCreateRequest + + @staticmethod + def get_response(): + return ToolCreateResponse diff --git a/apps/tools/apps.py b/apps/tools/apps.py new file mode 100644 index 00000000000..770f5d91614 --- /dev/null +++ b/apps/tools/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ToolConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tools' diff --git a/apps/tools/migrations/0001_initial.py b/apps/tools/migrations/0001_initial.py new file mode 100644 index 00000000000..fab62935e55 --- /dev/null +++ b/apps/tools/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2 on 2025-04-17 06:03 + +import django.db.models.deletion +import uuid_utils.compat +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Tool', + fields=[ + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, 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', models.JSONField(default=list, verbose_name='输入字段列表')), + ('init_field_list', models.JSONField(default=list, verbose_name='启动字段列表')), + ('icon', models.CharField(default='/ui/favicon.ico', max_length=256, verbose_name='函数库icon')), + ('is_active', models.BooleanField(default=True)), + ('scope', models.CharField(choices=[('SHARED', '共享'), ('WORKSPACE', '工作空间可用')], default='WORKSPACE', max_length=20, verbose_name='可用范围')), + ('tool_type', models.CharField(choices=[('INTERNAL', '内置'), ('PUBLIC', '公开')], default='PUBLIC', max_length=20, verbose_name='函数类型')), + ('template_id', models.UUIDField(default=None, null=True, verbose_name='模版id')), + ('module_id', models.CharField(default='root', max_length=64, null=True, verbose_name='模块id')), + ('init_params', models.CharField(max_length=102400, null=True, verbose_name='初始化参数')), + ('create_time', models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, null=True, verbose_name='修改时间')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')), + ], + options={ + 'db_table': 'tool', + }, + ), + ] diff --git a/apps/tools/migrations/__init__.py b/apps/tools/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/tools/models/__init__.py b/apps/tools/models/__init__.py new file mode 100644 index 00000000000..8c9d8dde4d2 --- /dev/null +++ b/apps/tools/models/__init__.py @@ -0,0 +1 @@ +from .tool import * diff --git a/apps/tools/models/tool.py b/apps/tools/models/tool.py new file mode 100644 index 00000000000..00cb0808d4e --- /dev/null +++ b/apps/tools/models/tool.py @@ -0,0 +1,38 @@ +import uuid_utils.compat as uuid +from django.db import models + +from users.models import User + + +class ToolScope(models.TextChoices): + SHARED = "SHARED", '共享' + WORKSPACE = "WORKSPACE", "工作空间可用" + + +class ToolType(models.TextChoices): + INTERNAL = "INTERNAL", '内置' + PUBLIC = "PUBLIC", "公开" + + +class Tool(models.Model): + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, 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 = models.JSONField(verbose_name="输入字段列表", default=list) + init_field_list = models.JSONField(verbose_name="启动字段列表", default=list) + icon = models.CharField(max_length=256, verbose_name="函数库icon", default="/ui/favicon.ico") + is_active = models.BooleanField(default=True) + scope = models.CharField(max_length=20, verbose_name='可用范围', choices=ToolScope.choices, + default=ToolScope.WORKSPACE) + tool_type = models.CharField(max_length=20, verbose_name='函数类型', choices=ToolType.choices, + default=ToolType.PUBLIC) + template_id = models.UUIDField(max_length=128, verbose_name="模版id", null=True, default=None) + module_id = models.CharField(max_length=64, verbose_name="模块id", null=True, default='root') + init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True) + create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True, null=True) + update_time = models.DateTimeField(verbose_name="修改时间", auto_now=True, null=True) + + class Meta: + db_table = "tool" diff --git a/apps/tools/serializers/__init__.py b/apps/tools/serializers/__init__.py new file mode 100644 index 00000000000..9bad5790a57 --- /dev/null +++ b/apps/tools/serializers/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py new file mode 100644 index 00000000000..3ecd14fbd04 --- /dev/null +++ b/apps/tools/serializers/tool.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +import re + +import uuid_utils.compat as uuid +from django.core import validators +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from tools.models import Tool, ToolScope + + +class ToolModelSerializer(serializers.ModelSerializer): + class Meta: + model = Tool + fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list', 'init_field_list', 'init_params', + 'scope', 'is_active', 'user_id', 'template_id', + 'create_time', 'update_time'] + + +class ToolInputField(serializers.Serializer): + name = serializers.CharField(required=True, label=_('variable name')) + is_required = serializers.BooleanField(required=True, label=_('required')) + type = serializers.CharField(required=True, label=_('type'), validators=[ + validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"), + message=_('fields only support string|int|dict|array|float'), code=500) + ]) + source = serializers.CharField(required=True, label=_('source'), validators=[ + validators.RegexValidator(regex=re.compile("^custom|reference$"), + message=_('The field only supports custom|reference'), code=500) + ]) + + +class ToolCreateRequest(serializers.Serializer): + name = serializers.CharField(required=True, label=_('tool name')) + + desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, + label=_('tool description')) + + code = serializers.CharField(required=True, label=_('tool content')) + + input_field_list = serializers.ListField(child=ToolInputField(), required=True, label=_('input field list')) + + init_field_list = serializers.ListField(required=False, default=list, label=_('init field list')) + + is_active = serializers.BooleanField(required=False, label=_('Is active')) + + +class ToolSerializer(serializers.Serializer): + class Create(serializers.Serializer): + user_id = serializers.UUIDField(required=True, label=_('user id')) + + def insert(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + ToolCreateRequest(data=instance).is_valid(raise_exception=True) + tool = Tool(id=uuid.uuid7(), + 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'), + init_field_list=instance.get('init_field_list'), + scope=ToolScope.WORKSPACE, + is_active=False) + tool.save() + return ToolModelSerializer(tool).data diff --git a/apps/tools/tests.py b/apps/tools/tests.py new file mode 100644 index 00000000000..7ce503c2dd9 --- /dev/null +++ b/apps/tools/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/tools/urls.py b/apps/tools/urls.py new file mode 100644 index 00000000000..caea5a0ffa2 --- /dev/null +++ b/apps/tools/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +app_name = "tool" +urlpatterns = [ + path('workspace//tool/create', views.ToolCreateView.as_view()), +] diff --git a/apps/tools/views/__init__.py b/apps/tools/views/__init__.py new file mode 100644 index 00000000000..d3bf330deeb --- /dev/null +++ b/apps/tools/views/__init__.py @@ -0,0 +1 @@ +from .tool import * \ No newline at end of file diff --git a/apps/tools/views/tool.py b/apps/tools/views/tool.py new file mode 100644 index 00000000000..397d7713514 --- /dev/null +++ b/apps/tools/views/tool.py @@ -0,0 +1,28 @@ +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants +from common.result import result +from tools.api.tool import ToolCreateAPI +from tools.serializers.tool import ToolSerializer + + +class ToolCreateView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['POST'], + description=_('Create tool'), + operation_id=_('Create tool'), + request=ToolCreateAPI.get_request(), + responses=ToolCreateAPI.get_response(), + tags=[_('Tool')]) + @has_permissions(PermissionConstants.TOOL_CREATE) + # @log(menu='Tool', operate="Create tool", + # get_operation_object=lambda r, k: r.data.get('name')) + def post(self, request: Request, workspace_id: str): + print(workspace_id) + return result.success(ToolSerializer.Create(data={'user_id': request.user.id}).insert(request.data)) diff --git a/pyproject.toml b/pyproject.toml index c6badc00df6..152b37fe89f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ django-db-connection-pool = "1.2.5" psycopg = {extras = ["binary"], version = "3.2.6"} python-dotenv = "1.1.0" uuid-utils = "0.10.0" +diskcache = "5.6.3" [build-system] requires = ["poetry-core"]