From 979967ed7d402867cd18c8a2d1bd6c93ff00a35c Mon Sep 17 00:00:00 2001 From: wanjia Date: Wed, 26 Mar 2025 12:26:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E5=88=97=E8=A1=A8=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- role_based_system/settings.py | 2 +- role_based_system/urls.py | 10 +- ...wners_alter_knowledgebase_name_and_more.py | 27 + user_management/views.py | 1277 +++++++++++++++-- 4 files changed, 1183 insertions(+), 133 deletions(-) create mode 100644 user_management/migrations/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py 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/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/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py b/user_management/migrations/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py new file mode 100644 index 00000000..e5951885 --- /dev/null +++ b/user_management/migrations/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.5 on 2025-03-26 02:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='knowledgebase', + name='owners', + ), + migrations.AlterField( + model_name='knowledgebase', + name='name', + field=models.CharField(max_length=100, unique=True, verbose_name='知识库名称'), + ), + migrations.AlterField( + model_name='knowledgebase', + name='user_id', + field=models.UUIDField(verbose_name='创建者ID'), + ), + ] diff --git a/user_management/views.py b/user_management/views.py index 3de07ca9..40133cfe 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 @@ -40,6 +40,9 @@ from django.utils.decorators import method_decorator import uuid from rest_framework import serializers import traceback +import requests +import json + # 添加模型导入 @@ -90,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)}") @@ -173,68 +170,299 @@ 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: 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_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, + '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 + knowledge_bases = [] # 存储所有知识库对象 + + for kb_id in 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) + + knowledge_bases.append(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 = 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') + + # 如果没有提供 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( + 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) + + # 准备完整的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=knowledge_base, - conversation_id=conversation_id, + knowledge_base=knowledge_bases[0], # 仍然需要一个主知识库,使用第一个 + conversation_id=str(conversation_id), role='user', content=data['question'], - metadata={'model_name': data.get('model_name', 'default')} + metadata=metadata ) # 创建AI回答记录 answer_record = ChatHistory.objects.create( user=request.user, - knowledge_base=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=data['answer'], - metadata={'model_name': data.get('model_name', 'default')} + content=answer, + metadata=metadata ) + # 返回完整的响应 return Response({ 'code': 200, 'message': '创建成功', 'data': { - 'id': answer_record.id, - 'conversation_id': conversation_id, - 'dataset_id': str(knowledge_base.id), + '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') @@ -248,7 +476,127 @@ 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回答""" + 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": "65031f4d-c86d-430e-8089-d8ff2731a837", + "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): """更新聊天记录""" @@ -403,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): """高亮关键词""" @@ -604,14 +1156,9 @@ class KnowledgeBaseViewSet(viewsets.ModelViewSet): department = request.data.get('department') group = request.data.get('group') - # 权限验证 + # 修改权限验证 if type == 'admin': - if user.role != 'admin': - return Response({ - 'code': 403, - 'message': '只有管理员可以创建管理员级知识库', - 'data': None - }, status=status.HTTP_403_FORBIDDEN) + # 移除管理员权限检查,允许所有用户创建 department = None group = None @@ -1665,7 +2212,6 @@ class KnowledgeBaseViewSet(viewsets.ModelViewSet): "data": None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - class PermissionViewSet(viewsets.ModelViewSet): serializer_class = PermissionSerializer permission_classes = [IsAuthenticated] @@ -1677,31 +2223,112 @@ class PermissionViewSet(viewsets.ModelViewSet): def get_queryset(self): """ 获取权限申请列表: - 1. 自己发出的申请 - 2. 对自己有管理权限的知识库收到的申请 + 1. applicant_id 是当前用户 (看到自己发起的申请) + 2. approver_id 是当前用户 (看到自己需要审批的申请) """ - user = self.request.user + user_id = str(self.request.user.id) - # 获取用户有管理权限的知识库ID列表 - managed_kb_ids = KBPermissionModel.objects.filter( - user=user, - can_edit=True, # 假设有编辑权限就可以管理权限申请 - status='active' - ).values_list('knowledge_base_id', flat=True) + # 构建查询条件:申请人是自己 或 审批人是自己 + query = Q(applicant_id=user_id) | Q(approver_id=user_id) - # 构建查询条件 - query = Q(applicant=user) # 自己发出的申请 - query |= Q(knowledge_base_id__in=managed_kb_ids) # 有管理权限的知识库的申请 - - return Permission.objects.filter(query).distinct().select_related( - 'knowledge_base', 'applicant', 'approver' + return Permission.objects.filter(query).select_related( + 'knowledge_base', + 'applicant', + 'approver' ) + def list(self, request, *args, **kwargs): + """获取权限申请列表,包含详细信息""" + try: + queryset = self.get_queryset() + user_id = str(request.user.id) + + # 获取分页参数 + page = int(request.query_params.get('page', 1)) + page_size = int(request.query_params.get('page_size', 10)) + + # 计算总数 + total = queryset.count() + + # 手动分页 + start = (page - 1) * page_size + end = start + page_size + permissions = queryset[start:end] + + # 构建响应数据 + data = [] + for permission in permissions: + # 检查当前用户是否是申请人或审批人 + if user_id not in [str(permission.applicant_id), str(permission.approver_id)]: + continue + + # 构建响应数据 + 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, + }, + 'approver': { + 'id': str(permission.approver.id) if permission.approver else '', + 'username': permission.approver.username if permission.approver else '', + 'name': permission.approver.name if permission.approver else '', + 'department': permission.approver.department if permission.approver else '', + }, + '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, + 'response_message': permission.response_message or '', + # 添加角色标识,用于前端展示 + 'role': 'applicant' if str(permission.applicant_id) == user_id else 'approver' + } + + data.append(permission_data) + + return Response({ + 'code': 200, + 'message': '获取权限申请列表成功', + 'data': { + 'total': len(data), # 使用过滤后的实际数量 + 'page': page, + 'page_size': page_size, + 'results': 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) + def perform_create(self, serializer): """创建权限申请并发送通知给知识库创建者""" # 获取知识库 + # 获取知识库 knowledge_base = serializer.validated_data['knowledge_base'] + # 检查是否是申请访问自己的知识库 + if str(knowledge_base.user_id) == str(self.request.user.id): + raise ValidationError({ + "code": 400, + "message": "您是此知识库的创建者,无需申请权限", + "data": None + }) + # 获取知识库创建者作为审批者 + approver = User.objects.get(id=knowledge_base.user_id) + # 验证权限请求 requested_permissions = serializer.validated_data.get('permissions', {}) expires_at = serializer.validated_data.get('expires_at') @@ -1735,10 +2362,11 @@ class PermissionViewSet(viewsets.ModelViewSet): if existing_permission: raise ValidationError("您已有此知识库的访问权限") - # 保存权限申请 + # 保存权限申请,设置审批者 permission = serializer.save( applicant=self.request.user, - status='pending' + status='pending', + approver=approver # 创建时就设置审批者 ) # 获取权限类型字符串 @@ -2001,6 +2629,401 @@ 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') + 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) + + # 按用户分组处理数据 + user_permissions = {} + for perm in queryset: + user_id = str(perm.user.id) + if user_id not in user_permissions: + user_permissions[user_id] = { + 'user_info': { + 'id': 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) + }, + 'permissions': [], + 'stats': { + 'total': 0, + 'by_type': { + 'admin': 0, + 'secret': 0, + 'leader': 0, + 'member': 0 + }, + 'by_permission': { + 'read_only': 0, + 'read_write': 0, + 'full_access': 0 + } + } + } + + # 添加权限信息 + 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, + 'creator': { + 'id': str(perm.knowledge_base.user_id), + 'name': getattr(User.objects.filter(id=perm.knowledge_base.user_id).first(), 'name', None), + 'username': getattr(User.objects.filter(id=perm.knowledge_base.user_id).first(), 'username', 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', None) 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 + } + + user_permissions[user_id]['permissions'].append(perm_data) + + # 更新统计信息 + stats = user_permissions[user_id]['stats'] + stats['total'] += 1 + stats['by_type'][perm.knowledge_base.type] += 1 + + # 统计权限级别 + if perm.can_delete: + stats['by_permission']['full_access'] += 1 + elif perm.can_edit: + stats['by_permission']['read_write'] += 1 + elif perm.can_read: + stats['by_permission']['read_only'] += 1 + + # 转换为列表并分页 + users_list = list(user_permissions.values()) + total = len(users_list) + start = (page - 1) * page_size + end = start + page_size + paginated_users = users_list[start:end] + + return Response({ + 'code': 200, + 'message': '获取权限列表成功', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'results': paginated_users + } + }) + + 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()