diff --git a/apps/application/migrations/0003_application_icon.py b/apps/application/migrations/0003_application_icon.py new file mode 100644 index 000000000..6e040becc --- /dev/null +++ b/apps/application/migrations/0003_application_icon.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2024-04-23 11:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('application', '0002_chat_client_id'), + ] + + operations = [ + migrations.AddField( + model_name='application', + name='icon', + field=models.CharField(default='/ui/favicon.ico', max_length=256, verbose_name='应用icon'), + ), + ] diff --git a/apps/application/models/application.py b/apps/application/models/application.py index e7c99e404..101816d38 100644 --- a/apps/application/models/application.py +++ b/apps/application/models/application.py @@ -37,6 +37,7 @@ class Application(AppModelMixin): dataset_setting = models.JSONField(verbose_name="数据集参数设置", default=get_dataset_setting_dict) model_setting = models.JSONField(verbose_name="模型参数相关设置", default=get_model_setting_dict) problem_optimization = models.BooleanField(verbose_name="问题优化", default=False) + icon = models.CharField(max_length=256, verbose_name="应用icon", default="/ui/favicon.ico") @staticmethod def get_default_model_prompt(): diff --git a/apps/application/serializers/application_serializers.py b/apps/application/serializers/application_serializers.py index f8ad3e09e..30af7f56e 100644 --- a/apps/application/serializers/application_serializers.py +++ b/apps/application/serializers/application_serializers.py @@ -29,9 +29,10 @@ from common.constants.authentication_type import AuthenticationType from common.db.search import get_dynamics_model, native_search, native_page_search from common.db.sql_execute import select_list from common.exception.app_exception import AppApiException, NotFound404 +from common.field.common import UploadedImageField from common.util.field_message import ErrMessage from common.util.file_util import get_file_content -from dataset.models import DataSet, Document +from dataset.models import DataSet, Document, Image from dataset.serializers.common_serializers import list_paragraph from embedding.models import SearchMode from setting.models import AuthOperate @@ -251,6 +252,7 @@ class ApplicationSerializer(serializers.Serializer): # 问题补全 problem_optimization = serializers.BooleanField(required=False, allow_null=True, error_messages=ErrMessage.boolean("问题补全")) + icon = serializers.CharField(required=False, allow_null=True, error_messages=ErrMessage.char("icon图标")) class Create(serializers.Serializer): user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) @@ -380,7 +382,8 @@ class ApplicationSerializer(serializers.Serializer): def reset_application(application: Dict): application['multiple_rounds_dialogue'] = True if application.get('dialogue_number') > 0 else False del application['dialogue_number'] - application['dataset_setting'] = {**application['dataset_setting'], 'search_mode': 'embedding'} + if 'dataset_setting' in application: + application['dataset_setting'] = {**application['dataset_setting'], 'search_mode': 'embedding'} return application def page(self, current_page: int, page_size: int, with_valid=True): @@ -393,7 +396,25 @@ class ApplicationSerializer(serializers.Serializer): class ApplicationModel(serializers.ModelSerializer): class Meta: model = Application - fields = ['id', 'name', 'desc', 'prologue', 'dialogue_number'] + fields = ['id', 'name', 'desc', 'prologue', 'dialogue_number', 'icon'] + + class IconOperate(serializers.Serializer): + application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("应用id")) + user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) + image = UploadedImageField(required=True, error_messages=ErrMessage.image("图片")) + + def edit(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + application = QuerySet(Application).filter(id=self.data.get('application_id')).first() + if application is None: + raise AppApiException(500, '不存在的应用id') + image_id = uuid.uuid1() + image = Image(id=image_id, image=self.data.get('image').read(), image_name=self.data.get('image').name) + image.save() + application.icon = f'/api/image/{image_id}' + application.save() + return {**ApplicationSerializer.Query.reset_application(ApplicationSerializerModel(application).data)} class Operate(serializers.Serializer): application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("应用id")) @@ -457,7 +478,7 @@ class ApplicationSerializer(serializers.Serializer): update_keys = ['name', 'desc', 'model_id', 'multiple_rounds_dialogue', 'prologue', 'status', 'dataset_setting', 'model_setting', 'problem_optimization', - 'api_key_is_active'] + 'api_key_is_active', 'icon'] for update_key in update_keys: if update_key in instance and instance.get(update_key) is not None: if update_key == 'multiple_rounds_dialogue': diff --git a/apps/application/swagger_api/application_api.py b/apps/application/swagger_api/application_api.py index 7af1c437a..542fd9f75 100644 --- a/apps/application/swagger_api/application_api.py +++ b/apps/application/swagger_api/application_api.py @@ -12,6 +12,17 @@ from common.mixins.api_mixin import ApiMixin class ApplicationApi(ApiMixin): + class EditApplicationIcon(ApiMixin): + @staticmethod + def get_request_params_api(): + return [ + openapi.Parameter(name='file', + in_=openapi.IN_FORM, + type=openapi.TYPE_FILE, + required=True, + description='上传文件') + ] + class Authentication(ApiMixin): @staticmethod def get_request_body_api(): @@ -143,7 +154,9 @@ class ApplicationApi(ApiMixin): 'dataset_setting': ApplicationApi.DatasetSetting.get_request_body_api(), 'model_setting': ApplicationApi.ModelSetting.get_request_body_api(), 'problem_optimization': openapi.Schema(type=openapi.TYPE_BOOLEAN, title="问题优化", - description="是否开启问题优化", default=True) + description="是否开启问题优化", default=True), + 'icon': openapi.Schema(type=openapi.TYPE_STRING, title="icon", + description="icon", default="/ui/favicon.ico") } ) diff --git a/apps/application/urls.py b/apps/application/urls.py index 507759286..30866c81a 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -8,6 +8,7 @@ urlpatterns = [ path('application/profile', views.Application.Profile.as_view()), path('application/embed', views.Application.Embed.as_view()), path('application/authentication', views.Application.Authentication.as_view()), + path('application//edit_icon', views.Application.EditIcon.as_view()), path('application//statistics/customer_count', views.ApplicationStatistics.CustomerCount.as_view()), path('application//statistics/customer_count_trend', diff --git a/apps/application/views/application_views.py b/apps/application/views/application_views.py index 7953220b4..3ebed0899 100644 --- a/apps/application/views/application_views.py +++ b/apps/application/views/application_views.py @@ -10,6 +10,7 @@ from django.http import HttpResponse from drf_yasg.utils import swagger_auto_schema from rest_framework.decorators import action +from rest_framework.parsers import MultiPartParser from rest_framework.request import Request from rest_framework.views import APIView @@ -131,6 +132,28 @@ class ApplicationStatistics(APIView): class Application(APIView): authentication_classes = [TokenAuth] + class EditIcon(APIView): + authentication_classes = [TokenAuth] + parser_classes = [MultiPartParser] + + @action(methods=['PUT'], detail=False) + @swagger_auto_schema(operation_summary="修改应用icon", + operation_id="修改应用icon", + tags=['应用'], + manual_parameters=ApplicationApi.EditApplicationIcon.get_request_params_api(), + request_body=ApplicationApi.Operate.get_request_body_api()) + @has_permissions(ViewPermission( + [RoleConstants.ADMIN, RoleConstants.USER], + [lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE, + dynamic_tag=keywords.get('application_id'))], + compare=CompareConstants.AND), PermissionConstants.APPLICATION_EDIT, + compare=CompareConstants.AND) + def put(self, request: Request, application_id: str): + return result.success( + ApplicationSerializer.IconOperate( + data={'application_id': application_id, 'user_id': request.user.id, + 'image': request.FILES.get('file')}).edit(request.data)) + class Embed(APIView): @action(methods=["GET"], detail=False) @swagger_auto_schema(operation_summary="获取嵌入js", diff --git a/apps/common/field/common.py b/apps/common/field/common.py index 3d6c95f11..c615e587a 100644 --- a/apps/common/field/common.py +++ b/apps/common/field/common.py @@ -32,3 +32,11 @@ class FunctionField(serializers.Field): def to_representation(self, value): return value + + +class UploadedImageField(serializers.ImageField): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def to_representation(self, value): + return value diff --git a/apps/common/middleware/static_headers_middleware.py b/apps/common/middleware/static_headers_middleware.py index 8f5606ab8..79b799a70 100644 --- a/apps/common/middleware/static_headers_middleware.py +++ b/apps/common/middleware/static_headers_middleware.py @@ -17,7 +17,14 @@ class StaticHeadersMiddleware(MiddlewareMixin): if request.path.startswith('/ui/chat/'): access_token = request.path.replace('/ui/chat/', '') application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first() - if application_access_token is not None and application_access_token.white_active: - # 添加自定义的响应头 - response['Content-Security-Policy'] = f'frame-ancestors {" ".join(application_access_token.white_list)}' + if application_access_token is not None: + if application_access_token.white_active: + # 添加自定义的响应头 + response[ + 'Content-Security-Policy'] = f'frame-ancestors {" ".join(application_access_token.white_list)}' + response.content = (response.content.decode('utf-8').replace( + '', + f'') + .replace('MaxKB', f'{application_access_token.application.name}').encode( + "utf-8")) return response diff --git a/apps/common/util/field_message.py b/apps/common/util/field_message.py index 3bcf067e1..93b51b920 100644 --- a/apps/common/util/field_message.py +++ b/apps/common/util/field_message.py @@ -95,3 +95,12 @@ class ErrMessage: 'invalid': gettext_lazy('【%s】日期格式错误。请改用以下格式之一: {format}。'), 'datetime': gettext_lazy('【%s】应为日期,但得到的是日期时间。') } + + @staticmethod + def image(field: str): + return { + 'required': gettext_lazy('【%s】此字段必填。' % field), + 'null': gettext_lazy('【%s】此字段不能为null。' % field), + 'invalid_image': gettext_lazy('【%s】上载有效的图像。您上载的文件不是图像或图像已损坏。' % field), + 'max_length': gettext_lazy('请确保此文件名最多包含 {max_length} 个字符(长度为 {length})。') + } diff --git a/apps/dataset/migrations/0002_image.py b/apps/dataset/migrations/0002_image.py new file mode 100644 index 000000000..a5fb59eb1 --- /dev/null +++ b/apps/dataset/migrations/0002_image.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.13 on 2024-04-22 19:31 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('dataset', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Image', + 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')), + ('image', models.BinaryField(verbose_name='图片数据')), + ('image_name', models.CharField(default='', max_length=256, verbose_name='图片名称')), + ], + options={ + 'db_table': 'image', + }, + ), + ] diff --git a/apps/dataset/models/data_set.py b/apps/dataset/models/data_set.py index 4cea2ef90..9ee76ffe2 100644 --- a/apps/dataset/models/data_set.py +++ b/apps/dataset/models/data_set.py @@ -105,3 +105,12 @@ class ProblemParagraphMapping(AppModelMixin): class Meta: db_table = "problem_paragraph_mapping" + + +class Image(AppModelMixin): + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id") + image = models.BinaryField(verbose_name="图片数据") + image_name = models.CharField(max_length=256, verbose_name="图片名称", default="") + + class Meta: + db_table = "image" diff --git a/apps/dataset/serializers/image_serializers.py b/apps/dataset/serializers/image_serializers.py new file mode 100644 index 000000000..46a1d72bc --- /dev/null +++ b/apps/dataset/serializers/image_serializers.py @@ -0,0 +1,42 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: image_serializers.py + @date:2024/4/22 16:36 + @desc: +""" +import uuid + +from django.db.models import QuerySet +from django.http import HttpResponse +from rest_framework import serializers + +from common.exception.app_exception import NotFound404 +from common.field.common import UploadedImageField +from common.util.field_message import ErrMessage +from dataset.models import Image + + +class ImageSerializer(serializers.Serializer): + image = UploadedImageField(required=True, error_messages=ErrMessage.image("图片")) + + def upload(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + image_id = uuid.uuid1() + image = Image(id=image_id, image=self.data.get('image').read(), image_name=self.data.get('image').name) + image.save() + return f'/api/image/{image_id}' + + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=True) + + def get(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + image_id = self.data.get('id') + image = QuerySet(Image).filter(id=image_id).first() + if image is None: + raise NotFound404(404, "不存在的图片") + return HttpResponse(image.image, status=200, headers={'Content-Type': 'image/png'}) diff --git a/apps/dataset/swagger_api/image_api.py b/apps/dataset/swagger_api/image_api.py new file mode 100644 index 000000000..f69b94719 --- /dev/null +++ b/apps/dataset/swagger_api/image_api.py @@ -0,0 +1,22 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: image_api.py + @date:2024/4/23 11:23 + @desc: +""" +from drf_yasg import openapi + +from common.mixins.api_mixin import ApiMixin + + +class ImageApi(ApiMixin): + @staticmethod + def get_request_params_api(): + return [openapi.Parameter(name='file', + in_=openapi.IN_FORM, + type=openapi.TYPE_FILE, + required=True, + description='上传图片文件') + ] diff --git a/apps/dataset/urls.py b/apps/dataset/urls.py index 38a92c845..2868bcbbd 100644 --- a/apps/dataset/urls.py +++ b/apps/dataset/urls.py @@ -40,5 +40,6 @@ urlpatterns = [ path('dataset//problem//', views.Problem.Page.as_view()), path('dataset//problem/', views.Problem.Operate.as_view()), path('dataset//problem//paragraph', views.Problem.Paragraph.as_view()), - + path('image/', views.Image.Operate.as_view()), + path('image', views.Image.as_view()) ] diff --git a/apps/dataset/views/__init__.py b/apps/dataset/views/__init__.py index b82d9ef72..6b2abcfb1 100644 --- a/apps/dataset/views/__init__.py +++ b/apps/dataset/views/__init__.py @@ -10,3 +10,4 @@ from .dataset import * from .document import * from .paragraph import * from .problem import * +from .image import * diff --git a/apps/dataset/views/image.py b/apps/dataset/views/image.py new file mode 100644 index 000000000..124336f87 --- /dev/null +++ b/apps/dataset/views/image.py @@ -0,0 +1,43 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: image.py + @date:2024/4/22 16:23 + @desc: +""" +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework.decorators import action +from rest_framework.parsers import MultiPartParser +from rest_framework.views import APIView +from rest_framework.views import Request + +from common.auth import TokenAuth +from common.response import result +from dataset.serializers.image_serializers import ImageSerializer + + +class Image(APIView): + authentication_classes = [TokenAuth] + parser_classes = [MultiPartParser] + + @action(methods=['POST'], detail=False) + @swagger_auto_schema(operation_summary="上传图片", + operation_id="上传图片", + manual_parameters=[openapi.Parameter(name='file', + in_=openapi.IN_FORM, + type=openapi.TYPE_FILE, + required=True, + description='上传文件')], + tags=["图片"]) + def post(self, request: Request): + return result.success(ImageSerializer(data={'image': request.FILES.get('file')}).upload()) + + class Operate(APIView): + @action(methods=['GET'], detail=False) + @swagger_auto_schema(operation_summary="获取图片", + operation_id="获取图片", + tags=["图片"]) + def get(self, request: Request, image_id: str): + return ImageSerializer.Operate(data={'id': image_id}).get() diff --git a/ui/src/api/application-overview.ts b/ui/src/api/application-overview.ts index e6a4d2c4d..0513a0d34 100644 --- a/ui/src/api/application-overview.ts +++ b/ui/src/api/application-overview.ts @@ -67,10 +67,24 @@ const getStatistics: ( return get(`${prefix}/${application_id}/statistics/chat_record_aggregate_trend`, data, loading) } +/** + * 修改应用icon + * @param 参数 application_id + * data: file + */ +const putAppIcon: ( + application_id: string, + data: any, + loading?: Ref +) => Promise> = (application_id, data, loading) => { + return put(`${prefix}/${application_id}/edit_icon`, data, undefined, loading) +} + export default { getAPIKey, postAPIKey, delAPIKey, putAPIKey, - getStatistics + getStatistics, + putAppIcon } diff --git a/ui/src/api/image.ts b/ui/src/api/image.ts new file mode 100644 index 000000000..425e8c6c3 --- /dev/null +++ b/ui/src/api/image.ts @@ -0,0 +1,15 @@ +import { Result } from '@/request/Result' +import { get, post, del, put } from '@/request/index' + +const prefix = '/image' +/** + * 上传图片 + * @param 参数 file:file + */ +const postImage: (data: any) => Promise> = (data) => { + return post(`${prefix}`, data) +} + +export default { + postImage +} diff --git a/ui/src/api/type/application.ts b/ui/src/api/type/application.ts index eefd8f31f..b39ed6707 100644 --- a/ui/src/api/type/application.ts +++ b/ui/src/api/type/application.ts @@ -10,6 +10,7 @@ interface ApplicationFormType { dataset_setting?: any model_setting?: any problem_optimization?: boolean + icon?: string | undefined } interface chatType { id: string diff --git a/ui/src/layout/components/breadcrumb/index.vue b/ui/src/layout/components/breadcrumb/index.vue index e2089c864..2e98be0e8 100644 --- a/ui/src/layout/components/breadcrumb/index.vue +++ b/ui/src/layout/components/breadcrumb/index.vue @@ -18,6 +18,7 @@ class="mr-8" :size="24" /> + ) { + return new Promise((resolve, reject) => { + applicationApi + .putApplication(id, data, loading) + .then((data) => { + resolve(data) + }) + .catch((error) => { + reject(error) + }) + }) } } }) diff --git a/ui/src/styles/element-plus.scss b/ui/src/styles/element-plus.scss index 338741239..fc45acca0 100644 --- a/ui/src/styles/element-plus.scss +++ b/ui/src/styles/element-plus.scss @@ -306,6 +306,10 @@ .el-popover { --el-popover-padding: 16px; } + +.el-radio { + --el-radio-font-weight: 400; +} .el-radio__input.is-checked + .el-radio__label { color: var(--app-text-color); } diff --git a/ui/src/utils/application.ts b/ui/src/utils/application.ts new file mode 100644 index 000000000..b88917356 --- /dev/null +++ b/ui/src/utils/application.ts @@ -0,0 +1,6 @@ +export const defaultIcon = '/ui/favicon.ico' + +// 是否显示字母 / icon +export function isAppIcon(url: string | undefined) { + return url === defaultIcon ? '' : url +} diff --git a/ui/src/views/application-overview/component/EditAvatarDialog.vue b/ui/src/views/application-overview/component/EditAvatarDialog.vue new file mode 100644 index 000000000..53b271542 --- /dev/null +++ b/ui/src/views/application-overview/component/EditAvatarDialog.vue @@ -0,0 +1,141 @@ + + + diff --git a/ui/src/views/application-overview/index.vue b/ui/src/views/application-overview/index.vue index d981e5063..bbda53a29 100644 --- a/ui/src/views/application-overview/index.vue +++ b/ui/src/views/application-overview/index.vue @@ -5,14 +5,37 @@

应用信息

- +
+ + + + + + + +
+

{{ detail?.name }}

@@ -102,6 +125,7 @@ +