From 00deeb711b3a0d5b7ab83b300a606e8df70e8801 Mon Sep 17 00:00:00 2001 From: wanjia Date: Mon, 17 Mar 2025 16:46:37 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_backup.json | Bin 0 -> 32184 bytes role_based_system/urls.py | 10 +- user_management/migrations/0001_initial.py | 156 +++----- user_management/views.py | 433 ++++++++++++++++++++- 4 files changed, 498 insertions(+), 101 deletions(-) create mode 100644 data_backup.json diff --git a/data_backup.json b/data_backup.json new file mode 100644 index 0000000000000000000000000000000000000000..368f99f29df2764e372a7080258ccf53215cb47b GIT binary patch literal 32184 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 bzJ2P=vUTo^Uf1}_T@Ge#h^>E3eO3J*YAGM> literal 0 HcmV?d00001 diff --git a/role_based_system/urls.py b/role_based_system/urls.py index 6bb0d406..2f510785 100644 --- a/role_based_system/urls.py +++ b/role_based_system/urls.py @@ -34,8 +34,8 @@ urlpatterns = [ ] # 添加调试工具栏(仅在DEBUG模式下) -if settings.DEBUG: - import debug_toolbar - urlpatterns = [ - path('__debug__/', include(debug_toolbar.urls)), - ] + urlpatterns +# if settings.DEBUG: +# import debug_toolbar +# urlpatterns = [ +# path('__debug__/', include(debug_toolbar.urls)), +# ] + urlpatterns diff --git a/user_management/migrations/0001_initial.py b/user_management/migrations/0001_initial.py index cd1103fd..c306a64f 100644 --- a/user_management/migrations/0001_initial.py +++ b/user_management/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.5 on 2025-02-26 09:23 +# Generated by Django 5.1.5 on 2025-03-17 05:47 import django.contrib.auth.models import django.contrib.auth.validators @@ -73,79 +73,22 @@ class Migration(migrations.Migration): name='KnowledgeBase', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, verbose_name='知识库名称')), + ('user_id', models.UUIDField(verbose_name='创建者ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='知识库名称')), ('desc', models.TextField(blank=True, null=True, verbose_name='知识库描述')), ('type', models.CharField(choices=[('admin', '管理级知识库'), ('leader', '部门级知识库'), ('member', '成员级知识库'), ('private', '私有知识库'), ('secret', '公司级别私密知识库')], default='private', max_length=20, verbose_name='知识库类型')), ('department', models.CharField(blank=True, max_length=50, null=True)), ('group', models.CharField(blank=True, max_length=50, null=True)), - ('user_id', models.CharField(max_length=50, verbose_name='创建者ID')), ('documents', models.JSONField(default=list)), ('char_length', models.IntegerField(default=0)), ('document_count', models.IntegerField(default=0)), ('external_id', models.UUIDField(blank=True, null=True)), ('create_time', models.DateTimeField(auto_now_add=True)), ('update_time', models.DateTimeField(auto_now=True)), - ('owners', models.ManyToManyField(related_name='owned_knowledge_bases', to=settings.AUTH_USER_MODEL, verbose_name='所有者')), ], options={ 'db_table': 'knowledge_bases', - }, - ), - migrations.CreateModel( - name='ChatHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('conversation_id', models.CharField(db_index=True, max_length=100)), - ('parent_id', models.CharField(blank=True, max_length=100, null=True)), - ('role', models.CharField(choices=[('user', '用户'), ('assistant', 'AI助手'), ('system', '系统')], max_length=20)), - ('content', models.TextField()), - ('tokens', models.IntegerField(default=0, help_text='消息token数')), - ('metadata', models.JSONField(blank=True, default=dict)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('is_deleted', models.BooleanField(default=False)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user_management.knowledgebase')), - ], - options={ - 'db_table': 'chat_history', - 'ordering': ['created_at'], - }, - ), - migrations.CreateModel( - name='KnowledgeBasePermission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('can_read', models.BooleanField(default=False, verbose_name='查看权限')), - ('can_edit', models.BooleanField(default=False, verbose_name='修改权限')), - ('can_delete', models.BooleanField(default=False, verbose_name='删除权限')), - ('status', models.CharField(choices=[('active', '生效中'), ('expired', '已过期'), ('revoked', '已撤销')], default='active', max_length=10, verbose_name='状态')), - ('granted_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')), - ('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='过期时间')), - ('granted_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL, verbose_name='授权人')), - ('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_permissions', to='user_management.knowledgebase', verbose_name='知识库')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='knowledge_base_permissions', to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'verbose_name': '知识库权限', - 'verbose_name_plural': '知识库权限', - 'db_table': 'knowledge_base_permissions', - }, - ), - migrations.CreateModel( - name='Notification', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('type', models.CharField(choices=[('permission_request', '权限申请'), ('permission_approved', '权限批准'), ('permission_rejected', '权限拒绝'), ('permission_expired', '权限过期'), ('system_notice', '系统通知')], max_length=20)), - ('title', models.CharField(max_length=100)), - ('content', models.TextField()), - ('is_read', models.BooleanField(default=False)), - ('related_resource', models.CharField(blank=True, max_length=100)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_notifications', to=settings.AUTH_USER_MODEL)), - ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_notifications', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'), models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx')], }, ), migrations.CreateModel( @@ -181,40 +124,65 @@ class Migration(migrations.Migration): 'db_table': 'user_profiles', }, ), - migrations.AddIndex( - model_name='knowledgebase', - index=models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'), + migrations.CreateModel( + name='ChatHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('conversation_id', models.CharField(db_index=True, max_length=100)), + ('parent_id', models.CharField(blank=True, max_length=100, null=True)), + ('role', models.CharField(choices=[('user', '用户'), ('assistant', 'AI助手'), ('system', '系统')], max_length=20)), + ('content', models.TextField()), + ('tokens', models.IntegerField(default=0, help_text='消息token数')), + ('metadata', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_deleted', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user_management.knowledgebase')), + ], + options={ + 'db_table': 'chat_history', + 'ordering': ['created_at'], + 'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx')], + }, ), - migrations.AddIndex( - model_name='knowledgebase', - index=models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), + migrations.CreateModel( + name='KnowledgeBasePermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_read', models.BooleanField(default=False, verbose_name='查看权限')), + ('can_edit', models.BooleanField(default=False, verbose_name='修改权限')), + ('can_delete', models.BooleanField(default=False, verbose_name='删除权限')), + ('status', models.CharField(choices=[('active', '生效中'), ('expired', '已过期'), ('revoked', '已撤销')], default='active', max_length=10, verbose_name='状态')), + ('granted_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')), + ('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='过期时间')), + ('granted_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL, verbose_name='授权人')), + ('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_permissions', to='user_management.knowledgebase', verbose_name='知识库')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='knowledge_base_permissions', to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'verbose_name': '知识库权限', + 'verbose_name_plural': '知识库权限', + 'db_table': 'knowledge_base_permissions', + 'indexes': [models.Index(fields=['knowledge_base', 'user', 'status'], name='knowledge_b_knowled_88e81e_idx')], + 'unique_together': {('knowledge_base', 'user')}, + }, ), - migrations.AddIndex( - model_name='knowledgebase', - index=models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx'), - ), - migrations.AddIndex( - model_name='chathistory', - index=models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), - ), - migrations.AddIndex( - model_name='chathistory', - index=models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'), - ), - migrations.AddIndex( - model_name='knowledgebasepermission', - index=models.Index(fields=['knowledge_base', 'user', 'status'], name='knowledge_b_knowled_88e81e_idx'), - ), - migrations.AlterUniqueTogether( - name='knowledgebasepermission', - unique_together={('knowledge_base', 'user')}, - ), - migrations.AddIndex( - model_name='notification', - index=models.Index(fields=['receiver', '-created_at'], name='user_manage_receive_fcb0eb_idx'), - ), - migrations.AddIndex( - model_name='notification', - index=models.Index(fields=['type', 'is_read'], name='user_manage_type_362052_idx'), + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('type', models.CharField(choices=[('permission_request', '权限申请'), ('permission_approved', '权限批准'), ('permission_rejected', '权限拒绝'), ('permission_expired', '权限过期'), ('system_notice', '系统通知')], max_length=20)), + ('title', models.CharField(max_length=100)), + ('content', models.TextField()), + ('is_read', models.BooleanField(default=False)), + ('related_resource', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_notifications', to=settings.AUTH_USER_MODEL)), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['receiver', '-created_at'], name='user_manage_receive_fcb0eb_idx'), models.Index(fields=['type', 'is_read'], name='user_manage_type_362052_idx')], + }, ), ] diff --git a/user_management/views.py b/user_management/views.py index 3de07ca9..f2b19adf 100644 --- a/user_management/views.py +++ b/user_management/views.py @@ -42,6 +42,7 @@ from rest_framework import serializers import traceback + # 添加模型导入 from .models import ( User, @@ -1665,7 +1666,6 @@ class KnowledgeBaseViewSet(viewsets.ModelViewSet): "data": None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - class PermissionViewSet(viewsets.ModelViewSet): serializer_class = PermissionSerializer permission_classes = [IsAuthenticated] @@ -1693,10 +1693,70 @@ class PermissionViewSet(viewsets.ModelViewSet): query = Q(applicant=user) # 自己发出的申请 query |= Q(knowledge_base_id__in=managed_kb_ids) # 有管理权限的知识库的申请 + # 使用 select_related 优化查询,预加载关联的对象 return Permission.objects.filter(query).distinct().select_related( - 'knowledge_base', 'applicant', 'approver' + 'knowledge_base', # 预加载知识库信息 + 'applicant', # 预加载申请人信息 + 'approver' # 预加载审批人信息 ) + def list(self, request, *args, **kwargs): + """获取权限申请列表,包含详细信息""" + queryset = self.get_queryset() + page = self.paginate_queryset(queryset) + + if page is not None: + data = [] + for permission in page: + permission_data = { + 'id': str(permission.id), + 'knowledge_base': { + 'id': str(permission.knowledge_base.id), + 'name': permission.knowledge_base.name, + 'type': permission.knowledge_base.type, + }, + 'applicant': { + 'id': str(permission.applicant.id), + 'username': permission.applicant.username, + 'name': permission.applicant.name, + 'department': permission.applicant.department, + }, + 'permissions': permission.permissions, + 'status': permission.status, + 'created_at': permission.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'expires_at': permission.expires_at.strftime('%Y-%m-%d %H:%M:%S') if permission.expires_at else None, + } + + # 添加审批人信息(如果已审批) + if permission.approver: + permission_data['approver'] = { + 'id': str(permission.approver.id), + 'username': permission.approver.username, + 'name': permission.approver.name, + 'department': permission.approver.department, + } + permission_data['response_message'] = permission.response_message + + data.append(permission_data) + + return Response({ + 'code': 200, + 'message': '获取权限申请列表成功', + 'data': { + 'total': self.paginator.page.paginator.count, + 'results': data + } + }) + + return Response({ + 'code': 200, + 'message': '获取权限申请列表成功', + 'data': { + 'total': queryset.count(), + 'results': [] + } + }) + def perform_create(self, serializer): """创建权限申请并发送通知给知识库创建者""" # 获取知识库 @@ -2001,6 +2061,375 @@ class PermissionViewSet(viewsets.ModelViewSet): return False + @action(detail=False, methods=['get']) + def user_permissions(self, request): + """获取指定用户的所有知识库权限""" + try: + # 获取用户名参数 + username = request.query_params.get('username') + if not username: + return Response({ + 'code': 400, + 'message': '请提供用户名', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取用户 + try: + target_user = User.objects.get(username=username) + except User.DoesNotExist: + return Response({ + 'code': 404, + 'message': f'用户 {username} 不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 获取该用户的所有权限记录 + permissions = KBPermissionModel.objects.filter( + user=target_user, + status='active' + ).select_related('knowledge_base', 'granted_by') + + # 构建响应数据 + permissions_data = [] + for perm in permissions: + perm_data = { + 'id': str(perm.id), + 'knowledge_base': { + 'id': str(perm.knowledge_base.id), + 'name': perm.knowledge_base.name, + 'type': perm.knowledge_base.type, + 'department': perm.knowledge_base.department, + 'group': perm.knowledge_base.group + }, + 'permissions': { + 'can_read': perm.can_read, + 'can_edit': perm.can_edit, + 'can_delete': perm.can_delete + }, + 'granted_by': { + 'id': str(perm.granted_by.id) if perm.granted_by else None, + 'username': perm.granted_by.username if perm.granted_by else None, + 'name': perm.granted_by.name if perm.granted_by else None + }, + 'created_at': perm.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'expires_at': perm.expires_at.strftime('%Y-%m-%d %H:%M:%S') if perm.expires_at else None, + 'status': perm.status + } + permissions_data.append(perm_data) + + return Response({ + 'code': 200, + 'message': '获取用户权限成功', + 'data': { + 'user': { + 'id': str(target_user.id), + 'username': target_user.username, + 'name': target_user.name, + 'department': target_user.department, + 'role': target_user.role + }, + 'permissions': permissions_data + } + }) + + except Exception as e: + logger.error(f"获取用户权限失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'获取用户权限失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['get']) + def all_permissions(self, request): + """管理员获取所有用户的知识库权限(不包括私有知识库)""" + try: + # 检查是否是管理员 + if request.user.role != 'admin': + return Response({ + 'code': 403, + 'message': '只有管理员可以查看所有权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 获取查询参数 + page = int(request.query_params.get('page', 1)) + page_size = int(request.query_params.get('page_size', 10)) + status_filter = request.query_params.get('status') # active/expired + department = request.query_params.get('department') + kb_type = request.query_params.get('kb_type') # 知识库类型筛选 + + # 构建基础查询 + queryset = KBPermissionModel.objects.filter( + ~Q(knowledge_base__type='private') # 排除私有知识库 + ).select_related( + 'user', + 'knowledge_base', + 'granted_by' + ) + + # 应用过滤条件 + if status_filter == 'active': + queryset = queryset.filter( + Q(expires_at__gt=timezone.now()) | Q(expires_at__isnull=True), + status='active' + ) + elif status_filter == 'expired': + queryset = queryset.filter( + Q(expires_at__lte=timezone.now()) | Q(status='inactive') + ) + + if department: + queryset = queryset.filter(user__department=department) + + if kb_type: + queryset = queryset.filter(knowledge_base__type=kb_type) + + # 计算总数 + total = queryset.count() + + # 分页 + start = (page - 1) * page_size + end = start + page_size + permissions = queryset.order_by('-granted_at')[start:end] + + # 获取所有相关的创建者ID + creator_ids = set(perm.knowledge_base.user_id for perm in permissions) + creators = { + str(user.id): user + for user in User.objects.filter(id__in=creator_ids) + } + + # 构建响应数据 + permissions_data = [] + for perm in permissions: + creator = creators.get(str(perm.knowledge_base.user_id)) + perm_data = { + 'id': str(perm.id), + 'user': { + 'id': str(perm.user.id), + 'username': perm.user.username, + 'name': getattr(perm.user, 'name', perm.user.username), + 'department': getattr(perm.user, 'department', None), + 'role': getattr(perm.user, 'role', None) + }, + 'knowledge_base': { + 'id': str(perm.knowledge_base.id), + 'name': perm.knowledge_base.name, + 'type': perm.knowledge_base.type, + 'department': perm.knowledge_base.department, + 'group': perm.knowledge_base.group, + 'creator': { + 'id': str(perm.knowledge_base.user_id), + 'name': creator.name if creator else None, + 'username': creator.username if creator else None + } + }, + 'permissions': { + 'can_read': perm.can_read, + 'can_edit': perm.can_edit, + 'can_delete': perm.can_delete + }, + 'granted_by': { + 'id': str(perm.granted_by.id) if perm.granted_by else None, + 'username': perm.granted_by.username if perm.granted_by else None, + 'name': getattr(perm.granted_by, 'name', perm.granted_by.username) if perm.granted_by else None + }, + 'granted_at': perm.granted_at.strftime('%Y-%m-%d %H:%M:%S'), + 'expires_at': perm.expires_at.strftime('%Y-%m-%d %H:%M:%S') if perm.expires_at else None, + 'status': perm.status + } + permissions_data.append(perm_data) + + return Response({ + 'code': 200, + 'message': '获取权限列表成功', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'results': permissions_data + } + }) + + except Exception as e: + logger.error(f"获取所有权限失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'获取所有权限失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def update_permission(self, request): + """管理员更新用户的知识库权限""" + try: + # 检查是否是管理员 + if request.user.role != 'admin': + return Response({ + 'code': 403, + 'message': '只有管理员可以直接修改权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 验证必要参数 + user_id = request.data.get('user_id') + knowledge_base_id = request.data.get('knowledge_base_id') + permissions = request.data.get('permissions') + expires_at_str = request.data.get('expires_at') + + if not all([user_id, knowledge_base_id, permissions]): + return Response({ + 'code': 400, + 'message': '缺少必要参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证权限参数格式 + required_permission_fields = ['can_read', 'can_edit', 'can_delete'] + if not all(field in permissions for field in required_permission_fields): + return Response({ + 'code': 400, + 'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取用户和知识库 + try: + user = User.objects.get(id=user_id) + knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id) + except User.DoesNotExist: + return Response({ + 'code': 404, + 'message': f'用户ID {user_id} 不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except KnowledgeBase.DoesNotExist: + return Response({ + 'code': 404, + 'message': f'知识库ID {knowledge_base_id} 不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 检查知识库类型和用户角色的匹配 + if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id): + return Response({ + 'code': 403, + 'message': '不能修改其他用户的私有知识库权限', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 处理过期时间 + expires_at = None + if expires_at_str: + try: + # 将字符串转换为datetime对象 + expires_at = timezone.datetime.strptime( + expires_at_str, + '%Y-%m-%dT%H:%M:%SZ' + ) + # 确保时区感知 + expires_at = timezone.make_aware(expires_at) + + # 检查是否早于当前时间 + if expires_at <= timezone.now(): + return Response({ + 'code': 400, + 'message': '过期时间不能早于或等于当前时间', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + return Response({ + 'code': 400, + 'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 根据用户角色限制权限 + if user.role == 'member' and permissions.get('can_delete'): + return Response({ + 'code': 400, + 'message': '普通成员不能获得删除权限', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 更新或创建权限记录 + try: + with transaction.atomic(): + permission, created = KBPermissionModel.objects.update_or_create( + user=user, + knowledge_base=knowledge_base, + defaults={ + 'can_read': permissions.get('can_read', False), + 'can_edit': permissions.get('can_edit', False), + 'can_delete': permissions.get('can_delete', False), + 'granted_by': request.user, + 'status': 'active', + 'expires_at': expires_at + } + ) + + # 发送通知给用户 + self.send_notification( + user=user, + title="知识库权限更新", + content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限", + notification_type="permission_updated", + related_object_id=permission.id + ) + except IntegrityError as e: + return Response({ + 'code': 500, + 'message': f'数据库操作失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + 'code': 200, + 'message': f"{'创建' if created else '更新'}权限成功", + 'data': { + 'id': str(permission.id), + 'user': { + 'id': str(user.id), + 'username': user.username, + 'name': user.name, + 'department': user.department, + 'role': user.role + }, + 'knowledge_base': { + 'id': str(knowledge_base.id), + 'name': knowledge_base.name, + 'type': knowledge_base.type, + 'department': knowledge_base.department, + 'group': knowledge_base.group + }, + 'permissions': { + 'can_read': permission.can_read, + 'can_edit': permission.can_edit, + 'can_delete': permission.can_delete + }, + 'granted_by': { + 'id': str(request.user.id), + 'username': request.user.username, + 'name': request.user.name + }, + 'expires_at': permission.expires_at.strftime('%Y-%m-%d %H:%M:%S') if permission.expires_at else None, + 'created': created + } + }) + + except Exception as e: + logger.error(f"更新权限失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'更新权限失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + class NotificationViewSet(viewsets.ModelViewSet): """通知视图集""" queryset = Notification.objects.all() From c0bba14ee841d8bbd5f817783cc243afaeaff494 Mon Sep 17 00:00:00 2001 From: wanjia Date: Thu, 20 Mar 2025 13:48:36 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E8=B0=83=E7=94=A8api=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- role_based_system/settings.py | 2 +- user_management/views.py | 277 ++++++++++++++++++++++++++++++---- 2 files changed, 246 insertions(+), 33 deletions(-) diff --git a/role_based_system/settings.py b/role_based_system/settings.py index 1fe78058..9a12460e 100644 --- a/role_based_system/settings.py +++ b/role_based_system/settings.py @@ -14,7 +14,7 @@ import os from pathlib import Path # API 配置 -API_BASE_URL = 'http://81.69.223.133:48329' +API_BASE_URL = 'http://180.163.88.62:30331' DEPARTMENT_GROUPS = { "技术部": ["开发组", "测试组", "运维组"], diff --git a/user_management/views.py b/user_management/views.py index f2b19adf..8cfcc1dd 100644 --- a/user_management/views.py +++ b/user_management/views.py @@ -40,6 +40,8 @@ from django.utils.decorators import method_decorator import uuid from rest_framework import serializers import traceback +import requests +import json @@ -178,55 +180,145 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): """创建聊天记录""" try: data = request.data - required_fields = ['dataset_id', 'dataset_name', 'question', 'answer'] - - # 检查必填字段 - for field in required_fields: - if field not in data: - return Response({ - 'code': 400, - 'message': f'缺少必填字段: {field}', - 'data': None - }, status=status.HTTP_400_BAD_REQUEST) - - # 获取或创建对话ID - conversation_id = data.get('conversation_id', str(uuid.uuid4())) - - # 获取知识库 - 不进行 UUID 转换 - try: - knowledge_base = KnowledgeBase.objects.filter(id=data['dataset_id']).first() - if not knowledge_base: - return Response({ - 'code': 404, - 'message': '知识库不存在', - 'data': None - }, status=status.HTTP_404_NOT_FOUND) - except Exception as e: + + # 检查必填字段 - 支持单知识库或多知识库模式 + if 'question' not in data: return Response({ 'code': 400, - 'message': f'无效的知识库ID: {str(e)}', + 'message': '缺少必填字段: question', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 检查知识库ID:支持dataset_id或dataset_id_list格式 + dataset_ids = [] + if 'dataset_id' in data: + dataset_ids.append(data['dataset_id']) + elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], list): + dataset_ids = data['dataset_id_list'] + else: + return Response({ + 'code': 400, + 'message': '缺少必填字段: dataset_id 或 dataset_id_list', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not dataset_ids: + return Response({ + 'code': 400, + 'message': '至少需要提供一个知识库ID', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证所有知识库并收集external_ids + external_id_list = [] + user = request.user + primary_knowledge_base = None # 主知识库,用于关联聊天记录 + + for idx, kb_id in enumerate(dataset_ids): + try: + knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first() + if not knowledge_base: + return Response({ + 'code': 404, + 'message': f'知识库不存在: {kb_id}', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 保存第一个知识库作为主知识库 + if idx == 0: + primary_knowledge_base = knowledge_base + + # 检查知识库权限 + can_read = False + + # 检查权限表 + permission = KBPermissionModel.objects.filter( + knowledge_base=knowledge_base, + user=user, + can_read=True, + status='active' + ).first() + + if permission: + can_read = True + else: + # 使用_can_read方法判断 + can_read = self._can_read( + type=knowledge_base.type, + user=user, + department=knowledge_base.department, + group=knowledge_base.group, + creator_id=knowledge_base.user_id + ) + + if not can_read: + return Response({ + 'code': 403, + 'message': f'无权访问知识库: {knowledge_base.name}', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 添加知识库的external_id到列表 + if knowledge_base.external_id: + external_id_list.append(knowledge_base.external_id) + else: + logger.warning(f"知识库 {knowledge_base.id} ({knowledge_base.name}) 没有external_id") + + except Exception as e: + return Response({ + 'code': 400, + 'message': f'处理知识库ID出错: {str(e)}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + if not external_id_list: + return Response({ + 'code': 400, + 'message': '没有有效的知识库external_id', 'data': None }, status=status.HTTP_400_BAD_REQUEST) - # 创建用户问题记录 + # 获取或创建对话ID + conversation_id = data.get('conversation_id', str(uuid.uuid4())) + + # 调用外部API获取答案 (传递多个knowledge base的external_id) + answer = self._get_answer_from_external_api( + dataset_external_id_list=external_id_list, + question=data['question'] + ) + + if not answer: + return Response({ + 'code': 500, + 'message': '获取AI回答失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 创建用户问题记录 (关联到主知识库) question_record = ChatHistory.objects.create( user=request.user, - knowledge_base=knowledge_base, + knowledge_base=primary_knowledge_base, conversation_id=conversation_id, role='user', content=data['question'], - metadata={'model_name': data.get('model_name', 'default')} + metadata={ + 'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'), + 'dataset_id_list': dataset_ids + } ) # 创建AI回答记录 answer_record = ChatHistory.objects.create( user=request.user, - knowledge_base=knowledge_base, + knowledge_base=primary_knowledge_base, conversation_id=conversation_id, parent_id=str(question_record.id), role='assistant', - content=data['answer'], - metadata={'model_name': data.get('model_name', 'default')} + content=answer, + metadata={ + 'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'), + 'dataset_id_list': dataset_ids + } ) return Response({ @@ -235,7 +327,8 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'data': { 'id': answer_record.id, 'conversation_id': conversation_id, - 'dataset_id': str(knowledge_base.id), + 'dataset_id': str(primary_knowledge_base.id), + 'dataset_name': primary_knowledge_base.name, 'role': 'assistant', 'content': answer_record.content, 'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S') @@ -250,6 +343,126 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'message': f'创建聊天记录失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def _get_answer_from_external_api(self, dataset_external_id_list, question): + """调用外部API获取AI回答""" + try: + # 确保所有ID都是字符串 + dataset_external_ids = [str(id) if isinstance(id, uuid.UUID) else id for id in dataset_external_id_list] + + logger.info(f"准备调用外部API,知识库ID列表: {dataset_external_ids}") + + # 第一个API调用创建聊天 + chat_request_data = { + "id": "d333dee2-b3c2-11ef-af2c-a4bb6dafa942", + "model_id": "58c5deb4-f2e2-11ef-9a1b-0242ac120009", + "dataset_id_list": dataset_external_ids, + "multiple_rounds_dialogue": False, + "dataset_setting": { + "top_n": 10, + "similarity": "0.3", + "max_paragraph_char_number": 10000, + "search_mode": "blend", + "no_references_setting": { + "value": "{question}", + "status": "ai_questioning" + } + }, + "model_setting": { + "prompt": "**相关文档内容**:{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**:{question}" + }, + "problem_optimization": False + } + + logger.info(f"发送创建聊天请求:{settings.API_BASE_URL}/api/application/chat/open") + + try: + # 测试JSON序列化,提前捕获可能的错误 + json_data = json.dumps(chat_request_data) + logger.debug(f"请求数据序列化成功,长度: {len(json_data)}") + except TypeError as e: + logger.error(f"JSON序列化失败: {str(e)}") + return None + + chat_response = requests.post( + url=f"{settings.API_BASE_URL}/api/application/chat/open", + json=chat_request_data, + headers={"Content-Type": "application/json"}, + timeout=30 + ) + + logger.info(f"API响应状态码: {chat_response.status_code}") + + if chat_response.status_code != 200: + logger.error(f"外部API调用失败: {chat_response.text}") + return None + + chat_data = chat_response.json() + logger.debug(f"API响应数据: {chat_data}") + + if chat_data.get('code') != 200 or not chat_data.get('data'): + logger.error(f"外部API返回错误: {chat_data}") + return None + + chat_id = chat_data['data'] + logger.info(f"聊天创建成功,chat_id: {chat_id}") + + # 第二个API调用发送消息 + message_request_data = { + "message": question, + "re_chat": False, + "stream": True + } + + logger.info(f"发送聊天消息请求: {settings.API_BASE_URL}/api/application/chat_message/{chat_id}") + message_response = requests.post( + url=f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}", + json=message_request_data, + headers={"Content-Type": "application/json"}, + stream=True, + timeout=60 + ) + + if message_response.status_code != 200: + logger.error(f"外部API聊天消息调用失败: {message_response.status_code}, {message_response.text}") + return None + + # 拼接流式响应 - 修复SSE格式解析 + full_content = "" + try: + for line in message_response.iter_lines(): + if line: + line_text = line.decode('utf-8') + # 处理SSE格式 (data: {...}) + if line_text.startswith('data: '): + json_str = line_text[6:] # 去掉 "data: " 前缀 + logger.debug(f"处理SSE数据: {json_str}") + try: + chunk = json.loads(json_str) + if 'content' in chunk: + content_part = chunk['content'] + full_content += content_part + logger.debug(f"追加内容: '{content_part}'") + if chunk.get('is_end', False): + logger.debug("收到结束标记") + except json.JSONDecodeError as e: + logger.error(f"JSON解析错误: {str(e)}, 原始数据: {json_str}") + else: + logger.debug(f"收到非SSE格式数据: {line_text}") + except Exception as e: + logger.error(f"处理流式响应出错: {str(e)}") + if full_content: + logger.info(f"已接收部分内容: {len(full_content)} 字符") + return full_content.strip() + return None + + logger.info(f"聊天回答拼接完成,总长度: {len(full_content)}") + return full_content.strip() if full_content else "未能获取到有效回答" + + except Exception as e: + logger.error(f"调用外部API获取回答失败: {str(e)}") + logger.error(traceback.format_exc()) + return None def update(self, request, pk=None): """更新聊天记录""" From 91535943752ec39832c2625c9bbc37b911c6d249 Mon Sep 17 00:00:00 2001 From: wanjia Date: Fri, 21 Mar 2025 15:10:29 +0800 Subject: [PATCH 3/3] =?UTF-8?q?conversation=5Fid=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...002_alter_chathistory_metadata_and_more.py | 22 + user_management/models.py | 73 ++- user_management/views.py | 528 ++++++++++++++---- 3 files changed, 527 insertions(+), 96 deletions(-) create mode 100644 user_management/migrations/0002_alter_chathistory_metadata_and_more.py diff --git a/user_management/migrations/0002_alter_chathistory_metadata_and_more.py b/user_management/migrations/0002_alter_chathistory_metadata_and_more.py new file mode 100644 index 00000000..7fd6f00e --- /dev/null +++ b/user_management/migrations/0002_alter_chathistory_metadata_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.5 on 2025-03-21 04:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='chathistory', + name='metadata', + field=models.JSONField(blank=True, default=dict, help_text="\n {\n 'model_id': 'xxx',\n 'dataset_id_list': ['id1', 'id2', ...],\n 'dataset_external_id_list': ['ext1', 'ext2', ...],\n 'primary_knowledge_base': 'id1'\n }\n "), + ), + migrations.AddIndex( + model_name='chathistory', + index=models.Index(fields=['conversation_id', 'is_deleted'], name='chat_histor_convers_89bc43_idx'), + ), + ] diff --git a/user_management/models.py b/user_management/models.py index 91f35cab..eb3ce915 100644 --- a/user_management/models.py +++ b/user_management/models.py @@ -1,3 +1,4 @@ +from itertools import count from django.contrib.auth.models import AbstractUser from django.db import models from django.utils import timezone @@ -286,13 +287,23 @@ class ChatHistory(models.Model): ] user = models.ForeignKey(User, on_delete=models.CASCADE) + # 保留与主知识库的关联 knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE) + # 用于标识知识库组合的对话 conversation_id = models.CharField(max_length=100, db_index=True) parent_id = models.CharField(max_length=100, null=True, blank=True) role = models.CharField(max_length=20, choices=ROLE_CHOICES) content = models.TextField() tokens = models.IntegerField(default=0, help_text="消息token数") - metadata = models.JSONField(default=dict, blank=True) + # 扩展metadata字段,用于存储知识库组合信息 + metadata = models.JSONField(default=dict, blank=True, help_text=""" + { + 'model_id': 'xxx', + 'dataset_id_list': ['id1', 'id2', ...], + 'dataset_external_id_list': ['ext1', 'ext2', ...], + 'primary_knowledge_base': 'id1' + } + """) created_at = models.DateTimeField(auto_now_add=True) is_deleted = models.BooleanField(default=False) @@ -302,6 +313,8 @@ class ChatHistory(models.Model): indexes = [ models.Index(fields=['conversation_id', 'created_at']), models.Index(fields=['user', 'created_at']), + # 添加新的索引以支持知识库组合查询 + models.Index(fields=['conversation_id', 'is_deleted']), ] def __str__(self): @@ -315,11 +328,69 @@ class ChatHistory(models.Model): is_deleted=False ).order_by('created_at') + @classmethod + def get_conversations_by_knowledge_bases(cls, dataset_ids, user): + """根据知识库组合获取对话历史""" + # 对知识库ID列表排序以确保一致性 + sorted_kb_ids = sorted(dataset_ids) + conversation_id = str(uuid.uuid5( + uuid.NAMESPACE_DNS, + '-'.join(sorted_kb_ids) + )) + + return cls.objects.filter( + conversation_id=conversation_id, + user=user, + is_deleted=False + ).order_by('created_at') + + @classmethod + def get_knowledge_base_combinations(cls, user): + """获取用户的所有知识库组合""" + return cls.objects.filter( + user=user, + is_deleted=False + ).values('conversation_id').annotate( + last_message=max('created_at'), + message_count=count('id') + ).values( + 'conversation_id', + 'last_message', + 'message_count', + 'metadata' + ).order_by('-last_message') + + def get_knowledge_bases(self): + """获取此消息关联的所有知识库""" + if self.metadata and 'dataset_id_list' in self.metadata: + return KnowledgeBase.objects.filter( + id__in=self.metadata['dataset_id_list'] + ) + return KnowledgeBase.objects.filter(id=self.knowledge_base.id) + def soft_delete(self): """软删除消息""" self.is_deleted = True self.save() + def to_dict(self): + """转换为字典格式""" + return { + 'id': str(self.id), + 'conversation_id': self.conversation_id, + 'role': self.role, + 'content': self.content, + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'metadata': self.metadata, + 'knowledge_bases': [ + { + 'id': str(kb.id), + 'name': kb.name, + 'type': kb.type + } for kb in self.get_knowledge_bases() + ] + } + class UserProfile(models.Model): """用户档案模型""" user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') diff --git a/user_management/views.py b/user_management/views.py index 8cfcc1dd..6ee9d9f6 100644 --- a/user_management/views.py +++ b/user_management/views.py @@ -26,7 +26,7 @@ import os from rest_framework.test import APIRequestFactory from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey -from django.http import Http404 +from django.http import Http404, HttpResponse from django.db import IntegrityError from channels.exceptions import ChannelFull from django.conf import settings @@ -93,79 +93,73 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): ) def list(self, request): - """获取聊天记录列表""" + """获取对话列表概览""" try: # 获取查询参数 - dataset_id = request.query_params.get('dataset_id') page = int(request.query_params.get('page', 1)) page_size = int(request.query_params.get('page_size', 10)) - query = self.get_queryset() + # 获取所有对话的概览 + latest_chats = self.get_queryset().values( + 'conversation_id' + ).annotate( + latest_id=Max('id'), + message_count=Count('id'), + last_message=Max('created_at') + ).order_by('-last_message') - if dataset_id: - # 获取特定知识库的完整对话历史 - records = query.filter( - knowledge_base__id=dataset_id - ).order_by('created_at') + # 计算分页 + total = latest_chats.count() + start = (page - 1) * page_size + end = start + page_size + chats = latest_chats[start:end] - conversation = { - 'dataset_id': dataset_id, - 'dataset_name': records.first().knowledge_base.name if records.exists() else None, - 'messages': [{ - 'id': record.id, - 'role': record.role, - 'content': record.content, - 'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S') - } for record in records] + results = [] + for chat in chats: + # 获取最新消息记录 + latest_record = ChatHistory.objects.get(id=chat['latest_id']) + + # 从metadata中获取完整的知识库信息 + dataset_info = [] + if latest_record.metadata: + dataset_id_list = latest_record.metadata.get('dataset_id_list', []) + dataset_names = latest_record.metadata.get('dataset_names', []) + + # 如果有知识库ID列表 + if dataset_id_list: + # 如果同时有名称列表且长度匹配 + if dataset_names and len(dataset_names) == len(dataset_id_list): + dataset_info = [{ + 'id': str(id), + 'name': name + } for id, name in zip(dataset_id_list, dataset_names)] + else: + # 如果没有名称列表,则只返回ID + datasets = KnowledgeBase.objects.filter(id__in=dataset_id_list) + dataset_info = [{ + 'id': str(ds.id), + 'name': ds.name + } for ds in datasets] + + results.append({ + 'conversation_id': chat['conversation_id'], + 'message_count': chat['message_count'], + 'last_message': latest_record.content, + 'last_time': chat['last_message'].strftime('%Y-%m-%d %H:%M:%S'), + 'dataset_id_list': [ds['id'] for ds in dataset_info], # 添加完整的知识库ID列表 + 'datasets': dataset_info # 包含ID和名称的完整信息 + }) + + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'results': results } - - return Response({ - 'code': 200, - 'message': '获取成功', - 'data': conversation - }) - else: - # 获取所有对话的概览 - latest_chats = query.values( - 'conversation_id', - 'knowledge_base__id', - 'knowledge_base__name' - ).annotate( - latest_id=Max('id'), - message_count=Count('id'), - last_message=Max('created_at') - ).order_by('-last_message') - - # 计算分页 - total = latest_chats.count() - start = (page - 1) * page_size - end = start + page_size - - # 获取分页数据 - chats = latest_chats[start:end] - - results = [] - for chat in chats: - latest_record = ChatHistory.objects.get(id=chat['latest_id']) - results.append({ - 'conversation_id': chat['conversation_id'], - 'dataset_id': str(chat['knowledge_base__id']), - 'dataset_name': chat['knowledge_base__name'], - 'message_count': chat['message_count'], - 'last_message': latest_record.content, - 'last_time': chat['last_message'].strftime('%Y-%m-%d %H:%M:%S') - }) - - return Response({ - 'code': 200, - 'message': '获取成功', - 'data': { - 'total': total, - 'page': page, - 'page_size': page_size, - 'results': results - } - }) + }) except Exception as e: logger.error(f"获取聊天记录失败: {str(e)}") @@ -176,6 +170,119 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @action(detail=False, methods=['get']) + def conversation_detail(self, request): + """获取特定对话的详细信息""" + try: + conversation_id = request.query_params.get('conversation_id') + if not conversation_id: + return Response({ + 'code': 400, + 'message': '缺少conversation_id参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 获取对话历史 + messages = self.get_queryset().filter( + conversation_id=conversation_id + ).order_by('created_at') + + if not messages.exists(): + return Response({ + 'code': 404, + 'message': '对话不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 获取知识库信息 + first_message = messages.first() + dataset_info = [] + if first_message and first_message.metadata: + if 'dataset_id_list' in first_message.metadata: + datasets = KnowledgeBase.objects.filter( + id__in=first_message.metadata['dataset_id_list'] + ) + dataset_info = [{ + 'id': str(ds.id), + 'name': ds.name, + 'type': ds.type + } for ds in datasets] + + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': { + 'conversation_id': conversation_id, + 'datasets': dataset_info, + 'messages': [{ + 'id': str(msg.id), + 'role': msg.role, + 'content': msg.content, + 'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S') + } for msg in messages] + } + }) + + except Exception as e: + logger.error(f"获取对话详情失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'获取对话详情失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['get']) + def available_datasets(self, request): + """获取用户可访问的知识库列表""" + try: + user = request.user + + # 获取用户有权限访问的知识库 + accessible_datasets = [] + + # 1. 获取用户通过权限表直接授权的知识库 + permission_datasets = KnowledgeBase.objects.filter( + id__in=KBPermissionModel.objects.filter( + user=user, + can_read=True, + status='active' + ).values_list('knowledge_base_id', flat=True) + ) + + # 2. 获取用户根据角色可以访问的知识库 + role_datasets = KnowledgeBase.objects.filter( + Q(type='member', department=user.department) | # 成员级知识库 + Q(type='leader', department=user.department) | # 部门级知识库,组长可访问 + Q(type='admin') # 管理级知识库,管理员可访问 + ).exclude( + Q(type='private') & ~Q(user_id=str(user.id)) # 排除不属于自己的私人知识库 + ) + + # 3. 合并并去重 + accessible_datasets = list(set(list(permission_datasets) + list(role_datasets))) + + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': [{ + 'id': str(ds.id), + 'name': ds.name, + 'type': ds.type, + 'department': ds.department, + 'description': ds.desc + } for ds in accessible_datasets] + }) + + except Exception as e: + logger.error(f"获取可用知识库列表失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'获取可用知识库列表失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def create(self, request): """创建聊天记录""" try: @@ -192,9 +299,23 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): # 检查知识库ID:支持dataset_id或dataset_id_list格式 dataset_ids = [] if 'dataset_id' in data: - dataset_ids.append(data['dataset_id']) - elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], list): - dataset_ids = data['dataset_id_list'] + dataset_id = data['dataset_id'] + # 直接使用标准UUID格式 + dataset_ids.append(str(dataset_id)) + elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], (list, str)): + # 处理可能的字符串格式 + if isinstance(data['dataset_id_list'], str): + try: + # 尝试解析JSON字符串 + dataset_list = json.loads(data['dataset_id_list']) + if isinstance(dataset_list, list): + dataset_ids = [str(id) for id in dataset_list] + except json.JSONDecodeError: + # 如果解析失败,可能是单个ID + dataset_ids = [str(data['dataset_id_list'])] + else: + # 如果已经是列表,直接使用标准UUID格式 + dataset_ids = [str(id) for id in data['dataset_id_list']] else: return Response({ 'code': 400, @@ -212,9 +333,9 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): # 验证所有知识库并收集external_ids external_id_list = [] user = request.user - primary_knowledge_base = None # 主知识库,用于关联聊天记录 + knowledge_bases = [] # 存储所有知识库对象 - for idx, kb_id in enumerate(dataset_ids): + for kb_id in dataset_ids: try: knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first() if not knowledge_base: @@ -224,9 +345,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'data': None }, status=status.HTTP_404_NOT_FOUND) - # 保存第一个知识库作为主知识库 - if idx == 0: - primary_knowledge_base = knowledge_base + knowledge_bases.append(knowledge_base) # 检查知识库权限 can_read = False @@ -242,7 +361,6 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): if permission: can_read = True else: - # 使用_can_read方法判断 can_read = self._can_read( type=knowledge_base.type, user=user, @@ -279,7 +397,20 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): }, status=status.HTTP_400_BAD_REQUEST) # 获取或创建对话ID - conversation_id = data.get('conversation_id', str(uuid.uuid4())) + conversation_id = data.get('conversation_id') + + # 如果没有提供 conversation_id,根据知识库组合生成新的ID + if not conversation_id: + # 对知识库ID列表排序以确保相同组合生成相同的hash + sorted_kb_ids = sorted(dataset_ids) + # 使用知识库ID组合生成唯一的conversation_id + conversation_id = str(uuid.uuid5( + uuid.NAMESPACE_DNS, + '-'.join(sorted_kb_ids) + )) + logger.info(f"为知识库组合 {sorted_kb_ids} 生成新的conversation_id: {conversation_id}") + else: + logger.info(f"使用现有conversation_id: {conversation_id}") # 调用外部API获取答案 (传递多个knowledge base的external_id) answer = self._get_answer_from_external_api( @@ -294,41 +425,44 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - # 创建用户问题记录 (关联到主知识库) + # 准备完整的metadata + metadata = { + 'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'), + 'dataset_id_list': [str(id) for id in dataset_ids], + 'dataset_external_id_list': [str(id) for id in external_id_list], + 'dataset_names': [kb.name for kb in knowledge_bases] # 添加知识库名称列表 + } + + # 创建用户问题记录 question_record = ChatHistory.objects.create( user=request.user, - knowledge_base=primary_knowledge_base, - conversation_id=conversation_id, + knowledge_base=knowledge_bases[0], # 仍然需要一个主知识库,使用第一个 + conversation_id=str(conversation_id), role='user', content=data['question'], - metadata={ - 'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'), - 'dataset_id_list': dataset_ids - } + metadata=metadata ) # 创建AI回答记录 answer_record = ChatHistory.objects.create( user=request.user, - knowledge_base=primary_knowledge_base, - conversation_id=conversation_id, + knowledge_base=knowledge_bases[0], # 仍然需要一个主知识库,使用第一个 + conversation_id=str(conversation_id), parent_id=str(question_record.id), role='assistant', content=answer, - metadata={ - 'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'), - 'dataset_id_list': dataset_ids - } + metadata=metadata ) + # 返回完整的响应 return Response({ 'code': 200, 'message': '创建成功', 'data': { - 'id': answer_record.id, - 'conversation_id': conversation_id, - 'dataset_id': str(primary_knowledge_base.id), - 'dataset_name': primary_knowledge_base.name, + 'id': str(answer_record.id), + 'conversation_id': str(conversation_id), + 'dataset_id_list': [str(id) for id in dataset_ids], + 'dataset_names': [kb.name for kb in knowledge_bases], # 返回所有知识库名称 'role': 'assistant', 'content': answer_record.content, 'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S') @@ -342,7 +476,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'code': 500, 'message': f'创建聊天记录失败: {str(e)}', 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + }, status.HTTP_500_INTERNAL_SERVER_ERROR) def _get_answer_from_external_api(self, dataset_external_id_list, question): """调用外部API获取AI回答""" @@ -354,7 +488,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): # 第一个API调用创建聊天 chat_request_data = { - "id": "d333dee2-b3c2-11ef-af2c-a4bb6dafa942", + "id": "65031f4d-c86d-430e-8089-d8ff2731a837", "model_id": "58c5deb4-f2e2-11ef-9a1b-0242ac120009", "dataset_id_list": dataset_external_ids, "multiple_rounds_dialogue": False, @@ -617,6 +751,210 @@ class ChatHistoryViewSet(viewsets.ModelViewSet): 'message': f'搜索失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['get']) + def export(self, request): + """导出聊天记录为Excel文件""" + try: + # 获取查询参数 + conversation_id = request.query_params.get('conversation_id') + dataset_id = request.query_params.get('dataset_id') + history_days = request.query_params.get('history_days', '7') # 默认导出最近7天 + + # 至少需要一个筛选条件 + if not conversation_id and not dataset_id: + return Response({ + 'code': 400, + 'message': '需要提供conversation_id或dataset_id参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 验证权限 + user = request.user + if dataset_id: + knowledge_base = KnowledgeBase.objects.filter(id=dataset_id).first() + if not knowledge_base: + return Response({ + 'code': 404, + 'message': '知识库不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 检查是否有读取权限 + can_read = False + permission = KBPermissionModel.objects.filter( + knowledge_base=knowledge_base, + user=user, + can_read=True, + status='active' + ).first() + + if permission: + can_read = True + else: + can_read = self._can_read( + type=knowledge_base.type, + user=user, + department=knowledge_base.department, + group=knowledge_base.group, + creator_id=knowledge_base.user_id + ) + + if not can_read: + return Response({ + 'code': 403, + 'message': '无权访问该知识库', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 查询确认有聊天记录存在 + query = self.get_queryset() + if conversation_id: + records = query.filter(conversation_id=conversation_id) + elif dataset_id: + records = query.filter(knowledge_base__id=dataset_id) + + if not records.exists(): + return Response({ + 'code': 404, + 'message': '未找到相关对话记录', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 调用外部API导出Excel文件 - 使用GET请求 + application_id = "65031f4d-c86d-430e-8089-d8ff2731a837" # 固定值 + export_url = f"{settings.API_BASE_URL}/api/application/{application_id}/chat/export?history_day={history_days}" + + logger.info(f"发送导出请求:{export_url}") + + export_response = requests.get( + url=export_url, + timeout=60, + stream=True # 使用流式传输处理大文件 + ) + + # 检查响应状态 + if export_response.status_code != 200: + logger.error(f"导出API调用失败: {export_response.status_code}, {export_response.text}") + return Response({ + 'code': 500, + 'message': '导出失败,外部服务返回错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 创建响应对象并设置文件下载头 + response = HttpResponse( + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename="data.xlsx"' + + # 将API响应内容写入响应对象 + for chunk in export_response.iter_content(chunk_size=8192): + if chunk: + response.write(chunk) + + logger.info("导出成功完成") + return response + + except Exception as e: + logger.error(f"导出聊天记录失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'导出聊天记录失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['get']) + def chat_list(self, request): + """获取对话列表""" + try: + # 获取查询参数 + history_days = request.query_params.get('history_days', '7') # 默认7天 + + # 构建API请求 + application_id = "65031f4d-c86d-430e-8089-d8ff2731a837" + api_url = f"{settings.API_BASE_URL}/api/application/{application_id}/chat" + + # 添加查询参数 + params = { + 'history_day': history_days + } + + logger.info(f"发送获取对话列表请求:{api_url}") + + # 调用外部API + response = requests.get( + url=api_url, + params=params, + timeout=30 + ) + + if response.status_code != 200: + logger.error(f"获取对话列表失败: {response.status_code}, {response.text}") + return Response({ + 'code': 500, + 'message': '获取对话列表失败,外部服务返回错误', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 解析响应数据 + try: + result = response.json() + + if result.get('code') != 200: + logger.error(f"外部API返回错误: {result}") + return Response({ + 'code': result.get('code', 500), + 'message': result.get('message', '获取对话列表失败'), + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 处理返回的数据 + chat_list = result.get('data', []) + + # 格式化返回数据 + formatted_chats = [] + for chat in chat_list: + formatted_chat = { + 'id': chat['id'], + 'chat_id': chat['chat_id'], + 'abstract': chat['abstract'], + 'message_count': chat['chat_record_count'], + 'created_at': datetime.fromisoformat(chat['create_time'].replace('Z', '+00:00')).strftime('%Y-%m-%d %H:%M:%S'), + 'updated_at': datetime.fromisoformat(chat['update_time'].replace('Z', '+00:00')).strftime('%Y-%m-%d %H:%M:%S'), + 'star_count': chat['star_num'], + 'trample_count': chat['trample_num'], + 'mark_sum': chat['mark_sum'], + 'is_deleted': chat['is_deleted'] + } + formatted_chats.append(formatted_chat) + + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': { + 'total': len(formatted_chats), + 'results': formatted_chats + } + }) + + except json.JSONDecodeError as e: + logger.error(f"解析响应数据失败: {str(e)}") + return Response({ + 'code': 500, + 'message': '解析响应数据失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + except Exception as e: + logger.error(f"获取对话列表失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'获取对话列表失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _highlight_keyword(self, text, keyword): """高亮关键词"""