From c47c70afb0ef97e535423844006da68a136ab98d Mon Sep 17 00:00:00 2001 From: CaptainB Date: Sat, 11 Oct 2025 15:45:05 +0800 Subject: [PATCH] feat: implement knowledge tag management functionality --- apps/common/constants/permission_constants.py | 57 ++++ apps/knowledge/api/document.py | 36 +++ apps/knowledge/api/tag.py | 107 ++++++++ .../migrations/0003_tag_documenttag.py | 54 ++++ apps/knowledge/models/knowledge.py | 29 ++ apps/knowledge/serializers/document.py | 177 ++++++++++++- apps/knowledge/serializers/tag.py | 250 ++++++++++++++++++ apps/knowledge/urls.py | 7 + apps/knowledge/views/__init__.py | 1 + apps/knowledge/views/document.py | 113 +++++++- apps/knowledge/views/tag.py | 138 ++++++++++ ui/src/api/knowledge/document.ts | 169 +++++++----- ui/src/api/knowledge/knowledge.ts | 50 +++- .../system-resource-management/document.ts | 38 +++ .../system-resource-management/knowledge.ts | 50 +++- ui/src/api/system-shared/document.ts | 39 +++ ui/src/api/system-shared/knowledge.ts | 48 +++- ui/src/locales/lang/en-US/views/document.ts | 13 + ui/src/locales/lang/zh-CN/views/document.ts | 13 + ui/src/locales/lang/zh-Hant/views/document.ts | 13 + .../views/document/component/AddTagDialog.vue | 97 +++++++ .../document/component/CreateTagDialog.vue | 107 ++++++++ .../document/component/EditTagDialog.vue | 98 +++++++ ui/src/views/document/component/TagDrawer.vue | 232 ++++++++++++++++ .../document/component/TagSettingDrawer.vue | 214 +++++++++++++++ ui/src/views/document/index.vue | 78 +++++- 26 files changed, 2146 insertions(+), 82 deletions(-) create mode 100644 apps/knowledge/api/tag.py create mode 100644 apps/knowledge/migrations/0003_tag_documenttag.py create mode 100644 apps/knowledge/serializers/tag.py create mode 100644 apps/knowledge/views/tag.py create mode 100644 ui/src/views/document/component/AddTagDialog.vue create mode 100644 ui/src/views/document/component/CreateTagDialog.vue create mode 100644 ui/src/views/document/component/EditTagDialog.vue create mode 100644 ui/src/views/document/component/TagDrawer.vue create mode 100644 ui/src/views/document/component/TagSettingDrawer.vue diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index d6c08bf6a..b8b16f205 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -39,8 +39,11 @@ class Group(Enum): SYSTEM_RES_KNOWLEDGE = "SYSTEM_RESOURCE_KNOWLEDGE" KNOWLEDGE_HIT_TEST = "KNOWLEDGE_HIT_TEST" KNOWLEDGE_DOCUMENT = "KNOWLEDGE_DOCUMENT" + KNOWLEDGE_TAG = "KNOWLEDGE_TAG" SYSTEM_KNOWLEDGE_DOCUMENT = "SYSTEM_KNOWLEDGE_DOCUMENT" SYSTEM_RES_KNOWLEDGE_DOCUMENT = "SYSTEM_RESOURCE_KNOWLEDGE_DOCUMENT" + SYSTEM_RES_KNOWLEDGE_TAG = "SYSTEM_RES_KNOWLEDGE_TAG" + SYSTEM_KNOWLEDGE_TAG = "SYSTEM_KNOWLEDGE_TAG" KNOWLEDGE_PROBLEM = "KNOWLEDGE_PROBLEM" SYSTEM_KNOWLEDGE_PROBLEM = "SYSTEM_KNOWLEDGE_PROBLEM" @@ -696,6 +699,28 @@ class PermissionConstants(Enum): resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] ) + KNOWLEDGE_TAG_READ = Permission( + group=Group.KNOWLEDGE_TAG, operate=Operate.READ, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] + ) + KNOWLEDGE_TAG_CREATE = Permission( + group=Group.KNOWLEDGE_TAG, operate=Operate.CREATE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] + ) + KNOWLEDGE_TAG_EDIT = Permission( + group=Group.KNOWLEDGE_TAG, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] + ) + KNOWLEDGE_TAG_DELETE = Permission( + group=Group.KNOWLEDGE_TAG, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] + ) APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION_READ = Permission( group=Group.APPLICATION_WORKSPACE_USER_RESOURCE_PERMISSION, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE], @@ -1199,6 +1224,22 @@ class PermissionConstants(Enum): group=Group.SYSTEM_KNOWLEDGE_DOCUMENT, operate=Operate.MIGRATE, role_list=[RoleConstants.ADMIN], parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE" ) + SHARED_KNOWLEDGE_TAG_READ = Permission( + group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.READ, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE" + ) + SHARED_KNOWLEDGE_TAG_CREATE = Permission( + group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE" + ) + SHARED_KNOWLEDGE_TAG_EDIT = Permission( + group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE" + ) + SHARED_KNOWLEDGE_TAG_DELETE = Permission( + group=Group.SYSTEM_KNOWLEDGE_TAG, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE" + ) SHARED_KNOWLEDGE_PROBLEM_READ = Permission( group=Group.SYSTEM_KNOWLEDGE_PROBLEM, operate=Operate.READ, role_list=[RoleConstants.ADMIN], parent_group=[SystemGroup.SHARED_KNOWLEDGE], is_ee=settings.edition == "EE" @@ -1427,6 +1468,22 @@ class PermissionConstants(Enum): group=Group.SYSTEM_RES_KNOWLEDGE_PROBLEM, operate=Operate.RELATE, role_list=[RoleConstants.ADMIN], parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE" ) + RESOURCE_KNOWLEDGE_TAG_READ = Permission( + group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.READ, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE" + ) + RESOURCE_KNOWLEDGE_TAG_CREATE = Permission( + group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE" + ) + RESOURCE_KNOWLEDGE_TAG_EDIT = Permission( + group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE" + ) + RESOURCE_KNOWLEDGE_TAG_DELETE = Permission( + group=Group.SYSTEM_RES_KNOWLEDGE_TAG, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN], + parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE" + ) RESOURCE_KNOWLEDGE_CHAT_USER_READ = Permission( group=Group.SYSTEM_RES_KNOWLEDGE_CHAT_USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN], parent_group=[SystemGroup.RESOURCE_KNOWLEDGE], is_ee=settings.edition == "EE" diff --git a/apps/knowledge/api/document.py b/apps/knowledge/api/document.py index eda06d8df..6ced11c6c 100644 --- a/apps/knowledge/api/document.py +++ b/apps/knowledge/api/document.py @@ -535,3 +535,39 @@ class DocumentDownloadSourceAPI(APIMixin): @staticmethod def get_response(): return DefaultResultSerializer + + +class DocumentTagsAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="knowledge_id", + description="知识库id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="document_id", + description="文档id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + ] + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return DefaultResultSerializer \ No newline at end of file diff --git a/apps/knowledge/api/tag.py b/apps/knowledge/api/tag.py new file mode 100644 index 000000000..a6ad861ee --- /dev/null +++ b/apps/knowledge/api/tag.py @@ -0,0 +1,107 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from common.mixins.api_mixin import APIMixin +from common.result import DefaultResultSerializer +from knowledge.serializers.tag import TagCreateSerializer, TagEditSerializer + + +class TagCreateAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="knowledge_id", + description="知识库id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + ] + + @staticmethod + def get_request(): + return TagCreateSerializer + + @staticmethod + def get_response(): + return DefaultResultSerializer + + +class TagDeleteAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="knowledge_id", + description="知识库id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="tag_id", + description="标签id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + ] + + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return DefaultResultSerializer + + +class TagEditAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="knowledge_id", + description="知识库id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="tag_id", + description="标签id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + ] + + @staticmethod + def get_request(): + return TagEditSerializer + + @staticmethod + def get_response(): + return DefaultResultSerializer diff --git a/apps/knowledge/migrations/0003_tag_documenttag.py b/apps/knowledge/migrations/0003_tag_documenttag.py new file mode 100644 index 000000000..824f6b1f9 --- /dev/null +++ b/apps/knowledge/migrations/0003_tag_documenttag.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.7 on 2025-10-11 07:38 + +import django.db.models.deletion +import uuid_utils.compat +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('knowledge', '0002_alter_file_source_type'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('key', models.CharField(db_index=True, max_length=64, verbose_name='标签键')), + ('value', models.CharField(db_index=True, max_length=128, verbose_name='标签值')), + ('knowledge', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='knowledge.knowledge', verbose_name='知识库')), + ], + options={ + 'db_table': 'tag', + }, + ), + migrations.CreateModel( + name='DocumentTag', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('document', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='knowledge.document', verbose_name='文档')), + ('tag', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='knowledge.tag', verbose_name='标签')), + ], + options={ + 'db_table': 'document_tag', + }, + ), + migrations.AddIndex( + model_name='tag', + index=models.Index(fields=['knowledge', 'key'], name='tag_knowled_cba590_idx'), + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together={('knowledge', 'key', 'value')}, + ), + migrations.AlterUniqueTogether( + name='documenttag', + unique_together={('document', 'tag')}, + ), + ] diff --git a/apps/knowledge/models/knowledge.py b/apps/knowledge/models/knowledge.py index 072068071..dca940b48 100644 --- a/apps/knowledge/models/knowledge.py +++ b/apps/knowledge/models/knowledge.py @@ -162,6 +162,35 @@ class Document(AppModelMixin): class Meta: db_table = "document" +class Tag(AppModelMixin): + """ + 标签表 - 存储标签的key-value定义 + """ + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") + knowledge = models.ForeignKey(Knowledge, on_delete=models.DO_NOTHING, verbose_name="知识库", db_constraint=False) + key = models.CharField(max_length=64, verbose_name="标签键", db_index=True) + value = models.CharField(max_length=128, verbose_name="标签值", db_index=True) + + class Meta: + db_table = "tag" + unique_together = [['knowledge', 'key', 'value']] # 在同一知识库内key-value组合唯一 + indexes = [ + models.Index(fields=['knowledge', 'key']), + ] + + +class DocumentTag(AppModelMixin): + """ + 文档标签关联表 + """ + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") + document = models.ForeignKey(Document, on_delete=models.DO_NOTHING, verbose_name="文档", db_constraint=False) + tag = models.ForeignKey(Tag, on_delete=models.DO_NOTHING, verbose_name="标签", db_constraint=False) + + class Meta: + db_table = "document_tag" + unique_together = [['document', 'tag']] # 文档和标签的组合唯一 + class Paragraph(AppModelMixin): """ diff --git a/apps/knowledge/serializers/document.py b/apps/knowledge/serializers/document.py index a2270a73b..f76f68aba 100644 --- a/apps/knowledge/serializers/document.py +++ b/apps/knowledge/serializers/document.py @@ -3,6 +3,7 @@ import json import os import re import traceback +from collections import defaultdict from functools import reduce from tempfile import TemporaryDirectory from typing import Dict, List @@ -15,8 +16,8 @@ from django.core import validators from django.db import transaction, models from django.db.models import QuerySet, Func, F, Value from django.db.models.aggregates import Max -from django.db.models.fields.json import KeyTextTransform -from django.db.models.functions import Substr, Reverse, Coalesce, Cast +from django.db.models.functions import Substr, Reverse +from django.db.models.query_utils import Q from django.http import HttpResponse from django.utils.translation import gettext_lazy as _, gettext, get_language, to_locale from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE @@ -47,7 +48,7 @@ from common.utils.fork import Fork from common.utils.logger import maxkb_logger from common.utils.split_model import get_split_model, flat_map from knowledge.models import Knowledge, Paragraph, Problem, Document, KnowledgeType, ProblemParagraphMapping, State, \ - TaskType, File, FileSourceType + TaskType, File, FileSourceType, Tag, DocumentTag from knowledge.serializers.common import ProblemParagraphManage, BatchSerializer, \ get_embedding_model_id_by_knowledge_id, MetaSerializer, write_image, zip_dir from knowledge.serializers.paragraph import ParagraphSerializers, ParagraphInstanceSerializer, \ @@ -1286,6 +1287,35 @@ class DocumentSerializers(serializers.Serializer): except AlreadyQueued as e: pass + def batch_add_tag(self, instance: Dict, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + document_id_list = instance.get("document_ids") + tag_id_list = instance.get("tag_ids") + # 批量查询已存在的标签关联关系 + existing_relations = { + (str(doc_id), str(tag_id)) + for doc_id, tag_id in QuerySet(DocumentTag).filter( + document_id__in=document_id_list, + tag_id__in=tag_id_list + ).values_list('document_id', 'tag_id') + } + + # 批量创建不存在的关联关系 + new_relations = [ + DocumentTag( + id=uuid.uuid7(), + document_id=document_id, + tag_id=tag_id, + ) + for document_id in document_id_list + for tag_id in tag_id_list + if (document_id, tag_id) not in existing_relations + ] + + if new_relations: + QuerySet(DocumentTag).bulk_create(new_relations) + class BatchGenerateRelated(serializers.Serializer): workspace_id = serializers.CharField(required=True, label=_('workspace id')) knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id')) @@ -1328,10 +1358,149 @@ class DocumentSerializers(serializers.Serializer): QuerySet(Document).filter(id__in=document_id_list))() try: for document_id in document_id_list: - generate_related_by_document_id.delay(document_id, model_id, model_params_setting, prompt, state_list) + generate_related_by_document_id.delay( + document_id, model_id, model_params_setting, prompt, state_list + ) except AlreadyQueued as e: pass + class Tags(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('workspace id')) + knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id')) + document_id = serializers.UUIDField(required=True, label=_('document id')) + name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('search value')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + if not QuerySet(Document).filter( + id=self.data.get('document_id'), + knowledge_id=self.data.get('knowledge_id') + ).exists(): + raise AppApiException(500, _('Document id does not exist')) + + def list(self): + self.is_valid(raise_exception=True) + + tag_ids = QuerySet(DocumentTag).filter( + document_id=self.data.get('document_id') + ).values_list('tag_id', flat=True) + + if self.data.get('name'): + tag_ids = QuerySet(Tag).filter( + knowledge_id=self.data.get('knowledge_id'), + id__in=tag_ids, + ).filter( + Q(key__icontains=self.data.get('name')) | Q(value__icontains=self.data.get('name')) + ).values_list('id', flat=True) + + # 获取所有标签,按创建时间排序保持稳定顺序 + tags = QuerySet(Tag).filter( + knowledge_id=self.data.get('knowledge_id'), + id__in=tag_ids + ).values('key', 'value', 'id', 'create_time', 'update_time').order_by('create_time', 'key', 'value') + + # 按key分组 + grouped_tags = defaultdict(list) + for tag in tags: + grouped_tags[tag['key']].append({ + 'id': tag['id'], + 'value': tag['value'], + 'create_time': tag['create_time'], + 'update_time': tag['update_time'] + }) + + # 转换为期望的格式,保持key的顺序 + result = [] + # 按key排序以确保结果顺序一致 + for key in sorted(grouped_tags.keys()): + values = grouped_tags[key] + # 按创建时间对values进行排序 + values.sort(key=lambda x: x['create_time']) + result.append({ + 'key': key, + 'values': values, + }) + + return result + + class AddTags(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('workspace id')) + knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id')) + document_id = serializers.UUIDField(required=True, label=_('document id')) + tag_ids = serializers.ListField( + required=True, label=_('tag ids'), child=serializers.UUIDField(required=True, label=_('tag id')) + ) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + if not QuerySet(Document).filter( + id=self.data.get('document_id'), + knowledge_id=self.data.get('knowledge_id') + ).exists(): + raise AppApiException(500, _('Document id does not exist')) + + def add_tags(self): + self.is_valid(raise_exception=True) + document_id = self.data.get('document_id') + tag_ids = self.data.get('tag_ids') + existing_tag_ids = set( + QuerySet(DocumentTag).filter( + document_id=document_id, tag_id__in=tag_ids + ).values_list('tag_id', flat=True) + ) + new_tags = [ + DocumentTag( + id=uuid.uuid7(), + document_id=document_id, + tag_id=tag_id + ) for tag_id in tag_ids if tag_id not in existing_tag_ids + ] + if new_tags: + QuerySet(DocumentTag).bulk_create(new_tags) + + class DeleteTags(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('workspace id')) + knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id')) + document_id = serializers.UUIDField(required=True, label=_('document id')) + tag_ids = serializers.ListField( + required=True, label=_('tag ids'), child=serializers.UUIDField(required=True, label=_('tag id')) + ) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + if not QuerySet(Document).filter( + id=self.data.get('document_id'), + knowledge_id=self.data.get('knowledge_id') + ).exists(): + raise AppApiException(500, _('Document id does not exist')) + + def delete_tags(self): + self.is_valid(raise_exception=True) + document_id = self.data.get('document_id') + tag_ids = self.data.get('tag_ids') + QuerySet(DocumentTag).filter( + document_id=document_id, + tag_id__in=tag_ids + ).delete() + class FileBufferHandle: buffer = None diff --git a/apps/knowledge/serializers/tag.py b/apps/knowledge/serializers/tag.py new file mode 100644 index 000000000..8c9a9cddf --- /dev/null +++ b/apps/knowledge/serializers/tag.py @@ -0,0 +1,250 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:AI Assistant + @file: tag.py + @date:2025/10/13 + @desc: 标签系统相关序列化器 +""" +from collections import defaultdict +from typing import Dict + +import uuid_utils.compat as uuid +from django.db import transaction +from django.db.models import QuerySet +from django.db.models.query_utils import Q +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.exception.app_exception import AppApiException +from knowledge.models import Tag, Knowledge, DocumentTag + + +class TagModelSerializer(serializers.ModelSerializer): + """标签模型序列化器""" + + class Meta: + model = Tag + fields = ['id', 'knowledge_id', 'key', 'value', 'create_time', 'update_time'] + read_only_fields = ['id', 'create_time', 'update_time'] + + +class TagCreateSerializer(serializers.Serializer): + """创建标签序列化器""" + key = serializers.CharField(required=True, max_length=64, label=_('Tag Key')) + value = serializers.CharField(required=True, max_length=128, label=_('Tag Value')) + + +class TagEditSerializer(serializers.Serializer): + key = serializers.CharField(required=False, max_length=64, label=_('Tag Key')) + value = serializers.CharField(required=False, max_length=128, label=_('Tag Value')) + + +class TagSerializers(serializers.Serializer): + class Create(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('Workspace ID')) + knowledge_id = serializers.UUIDField(required=True, label=_('Knowledge ID')) + tags = serializers.ListField(required=True, label=_('Tags'), child=TagCreateSerializer()) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + + def insert(self): + self.is_valid(raise_exception=True) + + knowledge_id = self.data.get('knowledge_id') + + # 获取数据库中已存在的key-value组合 + existing_tags = set( + QuerySet(Tag).filter(knowledge_id=knowledge_id) + .values_list('key', 'value', named=False) + ) + + # 过滤掉已存在的标签 + tag_objects = [] + for tag_data in self.data.get('tags', []): + key = tag_data.get('key') + value = tag_data.get('value') + + # 检查key-value组合是否已存在 + if (key, value) not in existing_tags: + tag = Tag( + id=uuid.uuid7(), + knowledge_id=knowledge_id, + key=key, + value=value + ) + tag_objects.append(tag) + # 将新标签添加到已存在集合中,避免本次批量插入中的重复 + existing_tags.add((key, value)) + + # 批量插入未重复的标签 + if tag_objects: + Tag.objects.bulk_create(tag_objects) + + class Operate(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('Workspace ID')) + knowledge_id = serializers.UUIDField(required=True, label=_('Knowledge ID')) + tag_id = serializers.UUIDField(required=True, label=_('Tag ID')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + + @transaction.atomic + def edit(self, instance: Dict): + self.is_valid(raise_exception=True) + tag = QuerySet(Tag).get(id=self.data.get('tag_id')) + if tag is None: + raise AppApiException(500, _('Tag id does not exist')) + + # 如果key发生变化,更新所有相同key的标签 + if instance.get('key') and instance.get('key') != tag.key: + old_key = tag.key + new_key = instance.get('key') + + # 检查新key是否已存在于同一个knowledge中 + existing_key_exists = QuerySet(Tag).filter( + knowledge_id=tag.knowledge_id, + key=new_key + ).exists() + + if existing_key_exists: + raise AppApiException(500, _('Tag key already exists')) + + # 批量更新所有具有相同old_key的标签 + QuerySet(Tag).filter( + knowledge_id=tag.knowledge_id, + key=old_key + ).update(key=new_key) + + # 如果只是value变化,只更新当前标签 + if instance.get('value') and instance.get('value') != tag.value: + # 检查新key是否已存在于同一个knowledge中 + existing_value_exists = QuerySet(Tag).filter( + knowledge_id=tag.knowledge_id, + key=instance.get('key'), + value=instance.get('value') + ).exists() + + if existing_value_exists: + raise AppApiException(500, _('Tag value already exists')) + QuerySet(Tag).filter( + id=tag.id + ).update(value=instance.get('value')) + + @transaction.atomic + def delete(self, delete_type: str): + self.is_valid(raise_exception=True) + if delete_type == 'key': + # 删除同一knowledge_id下相同key的所有标签 + tag = QuerySet(Tag).get(id=self.data.get('tag_id')) + if tag is None: + raise AppApiException(500, _('Tag id does not exist')) + QuerySet(Tag).filter( + knowledge_id=tag.knowledge_id, + key=tag.key + ).delete() + QuerySet(DocumentTag).filter(tag_id=tag.id).delete() + else: + # 仅删除当前标签 + QuerySet(Tag).filter(id=self.data.get('tag_id')).delete() + QuerySet(DocumentTag).filter(tag_id=self.data.get('tag_id')).delete() + + class BatchDelete(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('Workspace ID')) + knowledge_id = serializers.UUIDField(required=True, label=_('Knowledge ID')) + tag_ids = serializers.ListField(required=True, label=_('Tag IDs'), child=serializers.UUIDField()) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + + @transaction.atomic + def batch_delete(self): + self.is_valid(raise_exception=True) + tag_ids = self.data.get('tag_ids', []) + if not tag_ids: + return + + # 获取要删除的标签的key + tags_to_delete = QuerySet(Tag).filter(id__in=tag_ids) + keys_to_delete = set(tags_to_delete.values_list('key', flat=True)) + + # 删除具有相同key的所有标签 + QuerySet(Tag).filter( + knowledge_id=self.data.get('knowledge_id'), + key__in=keys_to_delete + ).delete() + + # 删除关联的DocumentTag + QuerySet(DocumentTag).filter(tag_id__in=tag_ids).delete() + + class Query(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_('Workspace ID')) + knowledge_id = serializers.UUIDField(required=True, label=_('Knowledge ID')) + name = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('search value')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Knowledge).filter(id=self.data.get('knowledge_id')) + if workspace_id and workspace_id != 'None': + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Knowledge id does not exist')) + + def list(self): + self.is_valid(raise_exception=True) + if self.data.get('name'): + name = self.data.get('name') + tags = QuerySet(Tag).filter( + knowledge_id=self.data.get('knowledge_id') + ).filter( + Q(key__icontains=name) | Q(value__icontains=name) + ).values('key', 'value', 'id', 'create_time', 'update_time').order_by('create_time', 'key', 'value') + else: + # 获取所有标签,按创建时间排序保持稳定顺序 + tags = QuerySet(Tag).filter( + knowledge_id=self.data.get('knowledge_id') + ).values('key', 'value', 'id', 'create_time', 'update_time').order_by('create_time', 'key', 'value') + + # 按key分组 + grouped_tags = defaultdict(list) + for tag in tags: + grouped_tags[tag['key']].append({ + 'id': tag['id'], + 'value': tag['value'], + 'create_time': tag['create_time'], + 'update_time': tag['update_time'] + }) + + # 转换为期望的格式,保持key的顺序 + result = [] + # 按key排序以确保结果顺序一致 + for key in sorted(grouped_tags.keys()): + values = grouped_tags[key] + # 按创建时间对values进行排序 + values.sort(key=lambda x: x['create_time']) + result.append({ + 'key': key, + 'values': values, + }) + + return result diff --git a/apps/knowledge/urls.py b/apps/knowledge/urls.py index 64f34bd26..f523a27a1 100644 --- a/apps/knowledge/urls.py +++ b/apps/knowledge/urls.py @@ -19,6 +19,10 @@ urlpatterns = [ path('workspace//knowledge//hit_test', views.KnowledgeView.HitTest.as_view()), path('workspace//knowledge//export', views.KnowledgeView.Export.as_view()), path('workspace//knowledge//export_zip', views.KnowledgeView.ExportZip.as_view()), + path('workspace//knowledge//tags', views.KnowledgeTagView.as_view()), + path('workspace//knowledge//tags/batch_delete', views.KnowledgeTagView.BatchDelete.as_view()), + path('workspace//knowledge//tags/', views.KnowledgeTagView.Operate.as_view()), + path('workspace//knowledge//tags//', views.KnowledgeTagView.Delete.as_view()), path('workspace//knowledge//document', views.DocumentView.as_view()), path('workspace//knowledge//document/split', views.DocumentView.Split.as_view()), path('workspace//knowledge//document/split_pattern', views.DocumentView.SplitPattern.as_view()), @@ -32,6 +36,7 @@ urlpatterns = [ path('workspace//knowledge//document/table', views.TableDocumentView.as_view()), path('workspace//knowledge//document/batch_hit_handling', views.DocumentView.BatchEditHitHandling.as_view()), path('workspace//knowledge//document/batch_cancel_task', views.DocumentView.BatchCancelTask.as_view()), + path('workspace//knowledge//document/batch_add_tag', views.DocumentView.BatchAddTag.as_view()), path('workspace//knowledge//document/migrate/', views.DocumentView.Migrate.as_view()), path('workspace//knowledge//document/', views.DocumentView.Operate.as_view()), path('workspace//knowledge//document//sync', views.DocumentView.SyncWeb.as_view()), @@ -40,6 +45,8 @@ urlpatterns = [ path('workspace//knowledge//document//export', views.DocumentView.Export.as_view()), path('workspace//knowledge//document//export_zip', views.DocumentView.ExportZip.as_view()), path('workspace//knowledge//document//download_source_file', views.DocumentView.DownloadSourceFile.as_view()), + path('workspace//knowledge//document//tags', views.DocumentView.Tags.as_view()), + path('workspace//knowledge//document//tags/batch_delete', views.DocumentView.Tags.BatchDelete.as_view()), path('workspace//knowledge//document//paragraph', views.ParagraphView.as_view()), path('workspace//knowledge//document//paragraph/batch_delete', views.ParagraphView.BatchDelete.as_view()), path('workspace//knowledge//document//paragraph/batch_generate_related', views.ParagraphView.BatchGenerateRelated.as_view()), diff --git a/apps/knowledge/views/__init__.py b/apps/knowledge/views/__init__.py index 586b2d335..ed401ad8a 100644 --- a/apps/knowledge/views/__init__.py +++ b/apps/knowledge/views/__init__.py @@ -2,3 +2,4 @@ from .document import * from .knowledge import * from .paragraph import * from .problem import * +from .tag import * diff --git a/apps/knowledge/views/document.py b/apps/knowledge/views/document.py index d430f505f..558d2ccb1 100644 --- a/apps/knowledge/views/document.py +++ b/apps/knowledge/views/document.py @@ -13,7 +13,7 @@ from knowledge.api.document import DocumentSplitAPI, DocumentBatchAPI, DocumentB DocumentReadAPI, DocumentEditAPI, DocumentDeleteAPI, TableDocumentCreateAPI, QaDocumentCreateAPI, \ WebDocumentCreateAPI, CancelTaskAPI, BatchCancelTaskAPI, SyncWebAPI, RefreshAPI, BatchEditHitHandlingAPI, \ DocumentTreeReadAPI, DocumentSplitPatternAPI, BatchRefreshAPI, BatchGenerateRelatedAPI, TemplateExportAPI, \ - DocumentExportAPI, DocumentMigrateAPI, DocumentDownloadSourceAPI + DocumentExportAPI, DocumentMigrateAPI, DocumentDownloadSourceAPI, DocumentTagsAPI from knowledge.serializers.common import get_knowledge_operation_object from knowledge.serializers.document import DocumentSerializers from knowledge.views.common import get_knowledge_document_operation_object, get_document_operation_object_batch, \ @@ -506,6 +506,37 @@ class DocumentView(APIView): data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id} ).batch_refresh(request.data)) + class BatchAddTag(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['POST'], + summary=_('Batch add tags to documents'), + operation_id=_('Batch add tags to documents'), # type: ignore + request=DocumentTagsAPI.get_request(), + parameters=DocumentTagsAPI.get_parameters(), + responses=DocumentTagsAPI.get_response(), + tags=[_('Knowledge Base/Documentation')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_knowledge_permission(), + PermissionConstants.KNOWLEDGE_DOCUMENT_EDIT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()], CompareConstants.AND), + ) + @log( + menu='document', operate="Batch add tags to documents", + get_operation_object=lambda r, keywords: get_knowledge_document_operation_object( + get_knowledge_operation_object(keywords.get('knowledge_id')), + get_document_operation_object_batch(r.data.get('document_ids')) + ), + ) + def post(self, request: Request, workspace_id: str, knowledge_id: str): + return result.success(DocumentSerializers.Batch( + data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id} + ).batch_add_tag(request.data)) + class BatchGenerateRelated(APIView): authentication_classes = [TokenAuth] @@ -655,6 +686,86 @@ class DocumentView(APIView): 'workspace_id': workspace_id, 'document_id': document_id, 'knowledge_id': knowledge_id }).download_source_file() + class Tags(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + summary=_('Get document tags'), + description=_('Get document tags'), + operation_id=_('Get document tags'), # type: ignore + parameters=DocumentTagsAPI.get_parameters(), + responses=DocumentTagsAPI.get_response(), + tags=[_('Knowledge Base/Documentation')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_knowledge_permission(), + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()], CompareConstants.AND), + ) + def get(self, request: Request, workspace_id: str, knowledge_id: str, document_id: str): + return result.success(DocumentSerializers.Tags(data={ + 'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'document_id': document_id, + 'name': request.query_params.get('name') + }).list()) + + @extend_schema( + summary=_('Add document tags'), + description=_('Add document tags'), + operation_id=_('Add document tags'), # type: ignore + parameters=DocumentTagsAPI.get_parameters(), + responses=DocumentTagsAPI.get_response(), + tags=[_('Knowledge Base/Documentation')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_knowledge_permission(), + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()], CompareConstants.AND), + ) + def post(self, request: Request, workspace_id: str, knowledge_id: str, document_id: str): + return result.success(DocumentSerializers.AddTags(data={ + 'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'document_id': document_id, + 'tag_ids': request.data + }).add_tags()) + + class BatchDelete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + summary=_('Delete document tags'), + description=_('Delete document tags'), + operation_id=_('Delete document tags'), # type: ignore + parameters=DocumentTagsAPI.get_parameters(), + request=DocumentTagsAPI.get_request(), + responses=DocumentTagsAPI.get_response(), + tags=[_('Knowledge Base/Documentation')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_knowledge_permission(), + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.KNOWLEDGE.get_workspace_knowledge_permission()], + CompareConstants.AND), + ) + @log( + menu='document', operate="Delete document tags", + get_operation_object=lambda r, keywords: get_knowledge_document_operation_object( + get_knowledge_operation_object(keywords.get('knowledge_id')), + get_document_operation_object(keywords.get('document_id')) + ), + ) + def put(self, request: Request, workspace_id: str, knowledge_id: str, document_id: str): + return result.success(DocumentSerializers.DeleteTags(data={ + 'workspace_id': workspace_id, + 'knowledge_id': knowledge_id, + 'document_id': document_id, + 'tag_ids': request.data + }).delete_tags()) + class Migrate(APIView): authentication_classes = [TokenAuth] diff --git a/apps/knowledge/views/tag.py b/apps/knowledge/views/tag.py new file mode 100644 index 000000000..96e53f92a --- /dev/null +++ b/apps/knowledge/views/tag.py @@ -0,0 +1,138 @@ +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, RoleConstants +from common.log.log import log +from common.result import result +from knowledge.api.tag import TagCreateAPI, TagDeleteAPI, TagEditAPI +from knowledge.serializers.common import get_knowledge_operation_object +from knowledge.serializers.tag import TagSerializers + + +class KnowledgeTagView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + summary=_("Create Knowledge Tag"), + description=_("Create a new knowledge tag"), + parameters=TagCreateAPI.get_parameters(), + request=TagCreateAPI.get_request(), + responses=TagCreateAPI.get_response(), + tags=[_('Knowledge Base/Tag')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_CREATE.get_workspace_permission(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + RoleConstants.USER.get_workspace_role() + ) + @log( + menu='tag', operate="Create a knowledge tag", + get_operation_object=lambda r, keywords: get_knowledge_operation_object(keywords.get('knowledge_id')) + ) + def post(self, request: Request, workspace_id: str, knowledge_id: str): + return result.success(TagSerializers.Create( + data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'tags': request.data} + ).insert()) + + @extend_schema( + summary=_("Get Knowledge Tag"), + description=_("Get knowledge tag"), + parameters=TagCreateAPI.get_parameters(), + request=TagCreateAPI.get_request(), + responses=TagCreateAPI.get_response(), + tags=[_('Knowledge Base/Tag')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_READ.get_workspace_permission(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + RoleConstants.USER.get_workspace_role() + ) + @log( + menu='tag', operate="Create a knowledge tag", + get_operation_object=lambda r, keywords: get_knowledge_operation_object(keywords.get('knowledge_id')) + ) + def get(self, request: Request, workspace_id: str, knowledge_id: str): + return result.success(TagSerializers.Query(data={ + 'workspace_id': workspace_id, + 'knowledge_id': knowledge_id, + 'name': request.query_params.get('name') + }).list()) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + summary=_("Update Knowledge Tag"), + description=_("Update a knowledge tag"), + parameters=TagEditAPI.get_parameters(), + request=TagEditAPI.get_request(), + responses=TagEditAPI.get_response(), + tags=[_('Knowledge Base/Tag')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_EDIT.get_workspace_permission(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + RoleConstants.USER.get_workspace_role() + ) + @log( + menu='tag', operate="Update a knowledge tag", + get_operation_object=lambda r, keywords: get_knowledge_operation_object(keywords.get('knowledge_id')) + ) + def put(self, request: Request, workspace_id: str, knowledge_id: str, tag_id: str): + return result.success(TagSerializers.Operate( + data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'tag_id': tag_id} + ).edit(request.data)) + + class Delete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + summary=_("Delete Knowledge Tag"), + description=_("Delete a knowledge tag"), + parameters=TagDeleteAPI.get_parameters(), + request=TagDeleteAPI.get_request(), + responses=TagDeleteAPI.get_response(), + tags=[_('Knowledge Base/Tag')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_DELETE.get_workspace_permission(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + RoleConstants.USER.get_workspace_role() + ) + @log( + menu='tag', operate="Delete a knowledge tag", + get_operation_object=lambda r, keywords: get_knowledge_operation_object(keywords.get('knowledge_id')) + ) + def delete(self, request: Request, workspace_id: str, knowledge_id: str, tag_id: str, delete_type: str): + return result.success(TagSerializers.Operate( + data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'tag_id': tag_id} + ).delete(delete_type)) + + class BatchDelete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + summary=_("Batch Delete Knowledge Tag"), + description=_("Batch Delete a knowledge tag"), + parameters=TagDeleteAPI.get_parameters(), + request=TagDeleteAPI.get_request(), + responses=TagDeleteAPI.get_response(), + tags=[_('Knowledge Base/Tag')] # type: ignore + ) + @has_permissions( + PermissionConstants.KNOWLEDGE_TAG_DELETE.get_workspace_permission(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + RoleConstants.USER.get_workspace_role() + ) + @log( + menu='tag', operate="Batch Delete knowledge tag", + get_operation_object=lambda r, keywords: get_knowledge_operation_object(keywords.get('knowledge_id')) + ) + def put(self, request: Request, workspace_id: str, knowledge_id: str): + return result.success(TagSerializers.BatchDelete( + data={'workspace_id': workspace_id, 'knowledge_id': knowledge_id, 'tag_ids': request.data} + ).batch_delete()) diff --git a/ui/src/api/knowledge/document.ts b/ui/src/api/knowledge/document.ts index 54a79a44c..a4dad8944 100644 --- a/ui/src/api/knowledge/document.ts +++ b/ui/src/api/knowledge/document.ts @@ -1,14 +1,14 @@ import { Result } from '@/request/Result' -import { get, post, del, put, exportExcel, exportFile } from '@/request/index' +import { del, exportExcel, exportFile, get, post, put } from '@/request/index' import type { Ref } from 'vue' -import type { KeyValue } from '@/api/type/common' -import type { pageRequest } from '@/api/type/common' +import type { KeyValue, pageRequest } from '@/api/type/common' import useStore from '@/stores' -const prefix: any = { _value: '/workspace/' } + +const prefix: any = {_value: '/workspace/'} Object.defineProperty(prefix, 'value', { get: function () { - const { user } = useStore() + const {user} = useStore() return this._value + user.getWorkspaceId() + '/knowledge' }, }) @@ -18,7 +18,7 @@ Object.defineProperty(prefix, 'value', { * @param 参数 knowledge_id, * param { " name": "string", - } + } */ const getDocumentList: (knowledge_id: string, loading?: Ref) => Promise> = ( @@ -32,9 +32,9 @@ const getDocumentList: (knowledge_id: string, loading?: Ref) => Promise * 文档分页列表 * @param 参数 knowledge_id, * param { - "name": "string", - folder_id: "string", - } + "name": "string", + folder_id: "string", + } */ const getDocumentPage: ( @@ -69,9 +69,9 @@ const getDocumentDetail: ( * @param 参数 * knowledge_id, document_id, * { - "name": "string", - "is_active": true, - "meta": {} + "name": "string", + "is_active": true, + "meta": {} } */ const putDocument: ( @@ -99,11 +99,11 @@ const delDocument: ( * 批量取消文档任务 * @param 参数 knowledge_id, *{ - "id_list": [ - "3fa85f64-5717-4562-b3fc-2c963f66afa6" - ], - "type": 0 -} + "id_list": [ + "3fa85f64-5717-4562-b3fc-2c963f66afa6" + ], + "type": 0 + } */ const putBatchCancelTask: ( @@ -192,10 +192,10 @@ const exportDocumentZip: ( * @param 参数 * knowledge_id, document_id, * { - "state_list": [ - "string" - ] -} + "state_list": [ + "string" + ] + } */ const putDocumentRefresh: ( knowledge_id: string, @@ -205,7 +205,7 @@ const putDocumentRefresh: ( ) => Promise> = (knowledge_id, document_id, state_list, loading) => { return put( `${prefix.value}/${knowledge_id}/document/${document_id}/refresh`, - { state_list }, + {state_list}, undefined, loading, ) @@ -232,23 +232,23 @@ const putDocumentSync: ( /** * 创建批量文档 * @param 参数 -{ - "name": "string", - "paragraphs": [ - { - "content": "string", - "title": "string", - "problem_list": [ - { - "id": "string", - "content": "string" - } - ], - "is_active": true - } - ], - "source_file_id": string -} + { + "name": "string", + "paragraphs": [ + { + "content": "string", + "title": "string", + "problem_list": [ + { + "id": "string", + "content": "string" + } + ], + "is_active": true + } + ], + "source_file_id": string + } */ const putMulDocument: ( knowledge_id: string, @@ -268,8 +268,8 @@ const putMulDocument: ( * 批量删除文档 * @param 参数 knowledge_id, * { - "id_list": [String] -} + "id_list": [String] + } */ const delMulDocument: ( knowledge_id: string, @@ -278,7 +278,7 @@ const delMulDocument: ( ) => Promise> = (knowledge_id, data, loading) => { return put( `${prefix.value}/${knowledge_id}/document/batch_delete`, - { id_list: data }, + {id_list: data}, undefined, loading, ) @@ -287,16 +287,16 @@ const delMulDocument: ( /** * 批量关联 * @param 参数 knowledge_id, -{ - "document_id_list": [ - "string" - ], - "model_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "prompt": "string", - "state_list": [ - "string" - ] -} + { + "document_id_list": [ + "string" + ], + "model_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "prompt": "string", + "state_list": [ + "string" + ] + } */ const putBatchGenerateRelated: ( knowledge_id: string, @@ -336,14 +336,14 @@ const putBatchEditHitHandling: ( * 批量刷新文档向量库 * @param knowledge_id 知识库id * @param data -{ - "id_list": [ - "string" - ], - "state_list": [ - "string" - ] -} + { + "id_list": [ + "string" + ], + "state_list": [ + "string" + ] + } * @param loading * @returns */ @@ -355,7 +355,7 @@ const putBatchRefresh: ( ) => Promise> = (knowledge_id, data, stateList, loading) => { return put( `${prefix.value}/${knowledge_id}/document/batch_refresh`, - { id_list: data, state_list: stateList }, + {id_list: data, state_list: stateList}, undefined, loading, ) @@ -372,7 +372,7 @@ const putMulSyncDocument: ( ) => Promise> = (knowledge_id, data, loading) => { return put( `${prefix.value}/${knowledge_id}/document/batch_sync`, - { id_list: data }, + {id_list: data}, undefined, loading, ) @@ -462,7 +462,7 @@ const exportQATemplate: (fileName: string, type: string, loading?: Ref) type, loading, ) => { - return exportExcel(fileName, `/workspace/knowledge/document/template/export`, { type }, loading) + return exportExcel(fileName, `/workspace/knowledge/document/template/export`, {type}, loading) } /** @@ -477,7 +477,7 @@ const exportTableTemplate: (fileName: string, type: string, loading?: Ref, ) => Promise> = (knowledge_id, data, loading) => { - return put(`${prefix.value}/lark/${knowledge_id}/_batch`, { id_list: data }, undefined, loading) + return put(`${prefix.value}/lark/${knowledge_id}/_batch`, {id_list: data}, undefined, loading) } /** @@ -564,6 +564,41 @@ const importLarkDocument: ( return post(`${prefix.value}/lark/${knowledge_id}/import`, data, null, loading) } +const getDocumentTags: ( + knowledge_id: string, + document_id: string, + params: any, + loading?: Ref, +) => Promise>> = (knowledge_id, document_id, params, loading) => { + return get(`${prefix.value}/${knowledge_id}/document/${document_id}/tags`, params, loading) +} + +const postDocumentTags: ( + knowledge_id: string, + document_id: string, + data: any, + loading?: Ref, +) => Promise> = (knowledge_id, document_id, data, loading) => { + return post(`${prefix.value}/${knowledge_id}/document/${document_id}/tags`, data, null, loading) +} + +const postMulDocumentTags: ( + knowledge_id: string, + data: any, + loading?: Ref, +) => Promise> = (knowledge_id, data, loading) => { + return post(`${prefix.value}/${knowledge_id}/document/batch_add_tag`, data, null, loading) +} + +const delMulDocumentTag: ( + knowledge_id: string, + document_id: string, + tags: any, + loading?: Ref, +) => Promise> = (knowledge_id, document_id, tags, loading) => { + return put(`${prefix.value}/${knowledge_id}/document/${document_id}/tags/batch_delete`, tags, null, loading) +} + export default { getDocumentList, getDocumentPage, @@ -595,4 +630,8 @@ export default { putLarkDocumentSync, putMulLarkSyncDocument, importLarkDocument, + getDocumentTags, + postDocumentTags, + postMulDocumentTags, + delMulDocumentTag } diff --git a/ui/src/api/knowledge/knowledge.ts b/ui/src/api/knowledge/knowledge.ts index d7ca7dd73..1e38e9fdf 100644 --- a/ui/src/api/knowledge/knowledge.ts +++ b/ui/src/api/knowledge/knowledge.ts @@ -255,6 +255,49 @@ const putLarkKnowledge: ( return put(`${prefix.value}/lark/${knowledge_id}`, data, undefined, loading) } + +const getTags: (knowledge_id: string, params: any, loading?: Ref) => Promise> = ( + knowledge_id, + params, + loading, +) => { + return get(`${prefix.value}/${knowledge_id}/tags`, params, loading) +} + +const postTags: (knowledge_id: string, tags: any, loading?: Ref) => Promise> = ( + knowledge_id, + tags, + loading, +) => { + return post(`${prefix.value}/${knowledge_id}/tags`, tags, null, loading) +} + +const putTag: (knowledge_id: string, tag_id: string, tag: any, loading?: Ref) => Promise> = ( + knowledge_id, + tag_id, + tag, + loading, +) => { + return put(`${prefix.value}/${knowledge_id}/tags/${tag_id}`, tag, null, loading) +} + +const delTag: (knowledge_id: string, tag_id: string, type: string, loading?: Ref) => Promise> = ( + knowledge_id, + tag_id, + type, + loading, +) => { + return del(`${prefix.value}/${knowledge_id}/tags/${tag_id}/${type}`, null, loading) +} + +const delMulTag: (knowledge_id: string, tags: any, loading?: Ref) => Promise> = ( + knowledge_id, + tags, + loading, +) => { + return put(`${prefix.value}/${knowledge_id}/tags/batch_delete`, tags, null, loading) +} + export default { getKnowledgeList, getKnowledgeListPage, @@ -271,5 +314,10 @@ export default { getKnowledgeModel, postWebKnowledge, postLarkKnowledge, - putLarkKnowledge + putLarkKnowledge, + getTags, + postTags, + putTag, + delTag, + delMulTag } diff --git a/ui/src/api/system-resource-management/document.ts b/ui/src/api/system-resource-management/document.ts index 8031880e7..b99533b40 100644 --- a/ui/src/api/system-resource-management/document.ts +++ b/ui/src/api/system-resource-management/document.ts @@ -523,6 +523,40 @@ const importLarkDocument: ( return post(`${prefix}/lark/${knowledge_id}/import`, data, null, loading) } +const getDocumentTags: ( + knowledge_id: string, + document_id: string, + params: any, + loading?: Ref, +) => Promise>> = (knowledge_id, document_id, params, loading) => { + return get(`${prefix}/${knowledge_id}/document/${document_id}/tags`, params, loading) +} + +const postDocumentTags: ( + knowledge_id: string, + document_id: string, + data: any, + loading?: Ref, +) => Promise> = (knowledge_id, document_id, data, loading) => { + return post(`${prefix}/${knowledge_id}/document/${document_id}/tags`, data, null, loading) +} + +const postMulDocumentTags: ( + knowledge_id: string, + data: any, + loading?: Ref, +) => Promise> = (knowledge_id, data, loading) => { + return post(`${prefix}/${knowledge_id}/document/batch_add_tag`, data, null, loading) +} + +const delMulDocumentTag: ( + knowledge_id: string, + document_id: string, + tags: any, + loading?: Ref, +) => Promise> = (knowledge_id, document_id, tags, loading) => { + return put(`${prefix}/${knowledge_id}/document/${document_id}/tags/batch_delete`, tags, null, loading) +} export default { getDocumentList, @@ -555,4 +589,8 @@ export default { putLarkDocumentSync, putMulLarkSyncDocument, importLarkDocument, + getDocumentTags, + postDocumentTags, + postMulDocumentTags, + delMulDocumentTag } diff --git a/ui/src/api/system-resource-management/knowledge.ts b/ui/src/api/system-resource-management/knowledge.ts index 72434913d..264e90b0d 100644 --- a/ui/src/api/system-resource-management/knowledge.ts +++ b/ui/src/api/system-resource-management/knowledge.ts @@ -205,6 +205,49 @@ const putLarkKnowledge: ( return put(`${prefix}/lark/${knowledge_id}`, data, undefined, loading) } +const getTags: (knowledge_id: string, params: any, loading?: Ref) => Promise> = ( + knowledge_id, + params, + loading, +) => { + return get(`${prefix}/${knowledge_id}/tags`, params, loading) +} + +const postTags: (knowledge_id: string, tags: any, loading?: Ref) => Promise> = ( + knowledge_id, + tags, + loading, +) => { + return post(`${prefix}/${knowledge_id}/tags`, tags, null, loading) +} + +const putTag: (knowledge_id: string, tag_id: string, tag: any, loading?: Ref) => Promise> = ( + knowledge_id, + tag_id, + tag, + loading, +) => { + return put(`${prefix}/${knowledge_id}/tags/${tag_id}`, tag, null, loading) +} + + +const delTag: (knowledge_id: string, tag_id: string, type: string, loading?: Ref) => Promise> = ( + knowledge_id, + tag_id, + type, + loading, +) => { + return del(`${prefix}/${knowledge_id}/tags/${tag_id}/${type}`, null, loading) +} + +const delMulTag: (knowledge_id: string, tags: any, loading?: Ref) => Promise> = ( + knowledge_id, + tags, + loading, +) => { + return put(`${prefix}/${knowledge_id}/tags/batch_delete`, tags, null, loading) +} + export default { getKnowledgeList, @@ -219,7 +262,12 @@ export default { putKnowledgeHitTest, putSyncWebKnowledge, getKnowledgeModel, - putLarkKnowledge + putLarkKnowledge, + getTags, + postTags, + putTag, + delTag, + delMulTag } as { [key: string]: any } diff --git a/ui/src/api/system-shared/document.ts b/ui/src/api/system-shared/document.ts index 080c776da..19b449fd7 100644 --- a/ui/src/api/system-shared/document.ts +++ b/ui/src/api/system-shared/document.ts @@ -524,6 +524,41 @@ const importLarkDocument: ( } +const getDocumentTags: ( + knowledge_id: string, + document_id: string, + params: any, + loading?: Ref, +) => Promise>> = (knowledge_id, document_id, params, loading) => { + return get(`${prefix}/${knowledge_id}/document/${document_id}/tags`, params, loading) +} + +const postDocumentTags: ( + knowledge_id: string, + document_id: string, + data: any, + loading?: Ref, +) => Promise> = (knowledge_id, document_id, data, loading) => { + return post(`${prefix}/${knowledge_id}/document/${document_id}/tags`, data, null, loading) +} + +const postMulDocumentTags: ( + knowledge_id: string, + data: any, + loading?: Ref, +) => Promise> = (knowledge_id, data, loading) => { + return post(`${prefix}/${knowledge_id}/document/batch_add_tag`, data, null, loading) +} + +const delMulDocumentTag: ( + knowledge_id: string, + document_id: string, + tags: any, + loading?: Ref, +) => Promise> = (knowledge_id, document_id, tags, loading) => { + return put(`${prefix}/${knowledge_id}/document/${document_id}/tags/batch_delete`, tags, null, loading) +} + export default { getDocumentList, getDocumentPage, @@ -555,4 +590,8 @@ export default { putLarkDocumentSync, putMulLarkSyncDocument, importLarkDocument, + getDocumentTags, + postDocumentTags, + postMulDocumentTags, + delMulDocumentTag } diff --git a/ui/src/api/system-shared/knowledge.ts b/ui/src/api/system-shared/knowledge.ts index 62e33e1ae..3da5facaf 100644 --- a/ui/src/api/system-shared/knowledge.ts +++ b/ui/src/api/system-shared/knowledge.ts @@ -249,6 +249,47 @@ const putLarkKnowledge: ( return put(`${prefix}/lark/${knowledge_id}`, data, undefined, loading) } +const getTags: (knowledge_id: string, params: any, loading?: Ref) => Promise> = ( + knowledge_id, + params, + loading, +) => { + return get(`${prefix}/${knowledge_id}/tags`, params, loading) +} + +const postTags: (knowledge_id: string, tags: any, loading?: Ref) => Promise> = ( + knowledge_id, + tags, + loading, +) => { + return post(`${prefix}/${knowledge_id}/tags`, tags, null, loading) +} + +const putTag: (knowledge_id: string, tag_id: string, tag: any, loading?: Ref) => Promise> = ( + knowledge_id, + tag_id, + tag, + loading, +) => { + return put(`${prefix}/${knowledge_id}/tags/${tag_id}`, tag, null, loading) +} + +const delTag: (knowledge_id: string, tag_id: string, type: string, loading?: Ref) => Promise> = ( + knowledge_id, + tag_id, + type, + loading, +) => { + return del(`${prefix}/${knowledge_id}/tags/${tag_id}/${type}`, null, loading) +} + +const delMulTag: (knowledge_id: string, tags: any, loading?: Ref) => Promise> = ( + knowledge_id, + tags, + loading, +) => { + return put(`${prefix}/${knowledge_id}/tags/batch_delete`, tags, null, loading) +} export default { getKnowledgeList, @@ -266,7 +307,12 @@ export default { getKnowledgeModel, postWebKnowledge, postLarkKnowledge, - putLarkKnowledge + putLarkKnowledge, + getTags, + postTags, + putTag, + delTag, + delMulTag } as { [key: string]: any } diff --git a/ui/src/locales/lang/en-US/views/document.ts b/ui/src/locales/lang/en-US/views/document.ts index 70385566c..00c105d6b 100644 --- a/ui/src/locales/lang/en-US/views/document.ts +++ b/ui/src/locales/lang/en-US/views/document.ts @@ -98,6 +98,19 @@ export default { import: 'Start Import', preview: 'Apply', }, + tag: { + label: 'Tag Management', + key: 'Tag', + value: 'Value', + add: 'Add Tag', + setting: 'Tag Settings', + create: 'Create Tag', + edit: 'Edit Tag', + deleteConfirm: 'Confirm delete tag: ', + deleteTip: 'After deletion, resources using this tag will have the tag removed. Please proceed with caution!', + requiredMessage1: 'Please enter a tag', + requiredMessage2: 'Please enter a value', + }, table: { name: 'Document Name', char_length: 'Character', diff --git a/ui/src/locales/lang/zh-CN/views/document.ts b/ui/src/locales/lang/zh-CN/views/document.ts index 593788580..0ecebb67d 100644 --- a/ui/src/locales/lang/zh-CN/views/document.ts +++ b/ui/src/locales/lang/zh-CN/views/document.ts @@ -93,6 +93,19 @@ export default { import: '开始导入', preview: '生成预览', }, + tag: { + label: '标签管理', + key: '标签', + value: '标签值', + add: '添加标签', + setting: '标签设置', + create: '创建标签', + edit: '编辑标签', + deleteConfirm: '是否删除标签: ', + deleteTip: '删除后使用该标签的资源将会删除该标签,请谨慎操作!', + requiredMessage1: '请输入标签', + requiredMessage2: '请输入标签值', + }, table: { name: '文件名称', char_length: '字符数', diff --git a/ui/src/locales/lang/zh-Hant/views/document.ts b/ui/src/locales/lang/zh-Hant/views/document.ts index d1bce7bb1..2152c63c5 100644 --- a/ui/src/locales/lang/zh-Hant/views/document.ts +++ b/ui/src/locales/lang/zh-Hant/views/document.ts @@ -96,6 +96,19 @@ export default { import: '開始導入', preview: '生成預覽', }, + tag: { + label: '標籤管理', + key: '標籤', + value: '標籤值', + add: '添加標籤', + setting: '標籤設置', + create: '創建標籤', + edit: '編輯標籤', + deleteConfirm: '是否刪除標籤: ', + deleteTip: '刪除後使用該標籤的資源將會刪除該標籤,請謹慎操作!', + requiredMessage1: '請輸入標籤', + requiredMessage2: '請輸入標籤值', + }, table: { name: '文件名稱', char_length: '字符數', diff --git a/ui/src/views/document/component/AddTagDialog.vue b/ui/src/views/document/component/AddTagDialog.vue new file mode 100644 index 000000000..517869f39 --- /dev/null +++ b/ui/src/views/document/component/AddTagDialog.vue @@ -0,0 +1,97 @@ + + + diff --git a/ui/src/views/document/component/CreateTagDialog.vue b/ui/src/views/document/component/CreateTagDialog.vue new file mode 100644 index 000000000..75be566a4 --- /dev/null +++ b/ui/src/views/document/component/CreateTagDialog.vue @@ -0,0 +1,107 @@ + + + diff --git a/ui/src/views/document/component/EditTagDialog.vue b/ui/src/views/document/component/EditTagDialog.vue new file mode 100644 index 000000000..2cd862c33 --- /dev/null +++ b/ui/src/views/document/component/EditTagDialog.vue @@ -0,0 +1,98 @@ + + + diff --git a/ui/src/views/document/component/TagDrawer.vue b/ui/src/views/document/component/TagDrawer.vue new file mode 100644 index 000000000..448c76bc0 --- /dev/null +++ b/ui/src/views/document/component/TagDrawer.vue @@ -0,0 +1,232 @@ + + + + diff --git a/ui/src/views/document/component/TagSettingDrawer.vue b/ui/src/views/document/component/TagSettingDrawer.vue new file mode 100644 index 000000000..198b572fa --- /dev/null +++ b/ui/src/views/document/component/TagSettingDrawer.vue @@ -0,0 +1,214 @@ + + + + diff --git a/ui/src/views/document/index.vue b/ui/src/views/document/index.vue index 2d71bfa2c..0f1ec3f18 100644 --- a/ui/src/views/document/index.vue +++ b/ui/src/views/document/index.vue @@ -57,6 +57,13 @@ > {{ $t('common.setting') }} + + {{ $t('views.document.tag.add') }} + @@ -96,15 +103,19 @@ - - +
+ + + {{ $t('views.document.tag.label') }} + +
{{ $t('views.document.generateQuestion.title') }} + + {{ $t('views.document.tag.setting') }} + + + +