diff --git a/apps/chat/chat_service.py b/apps/chat/chat_service.py deleted file mode 100644 index 4e57f94..0000000 --- a/apps/chat/chat_service.py +++ /dev/null @@ -1,142 +0,0 @@ -# apps/common/services/chat_service.py -import logging -import json -from uuid import uuid4 -from django.db import transaction -from apps.accounts.models import User -from apps.knowledge_base.models import KnowledgeBase -from apps.chat.models import ChatHistory -from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin - -logger = logging.getLogger(__name__) - -class ChatService(KnowledgeBasePermissionMixin): - @transaction.atomic - def create_chat_record(self, user, data, conversation_id=None): - """创建聊天记录,供chat、gmail、feishu模块复用""" - try: - # 验证必填字段 - if 'question' not in data: - raise ValueError("缺少必填字段: question") - - # 如果未提供conversation_id,生成新的 - if not conversation_id: - conversation_id = str(uuid4()) - logger.info(f"生成新的会话ID: {conversation_id}") - - # 处理知识库ID - dataset_ids = [] - if 'dataset_id' in data: - dataset_ids.append(str(data['dataset_id'])) - elif 'dataset_id_list' in data: - if isinstance(data['dataset_id_list'], str): - try: - dataset_list = json.loads(data['dataset_id_list']) - dataset_ids = [str(id) for id in dataset_list if isinstance(dataset_list, list)] - except json.JSONDecodeError: - dataset_ids = [str(data['dataset_id_list'])] - else: - dataset_ids = [str(id) for id in data['dataset_id_list']] - - if not dataset_ids: - raise ValueError("缺少必填字段: dataset_id 或 dataset_id_list") - - # 验证知识库权限 - knowledge_bases = [] - external_id_list = [] - for kb_id in dataset_ids: - knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first() - if not knowledge_base: - raise ValueError(f"知识库不存在: {kb_id}") - - if not self.check_knowledge_base_permission(knowledge_base, user, 'read'): - raise ValueError(f"无权访问知识库: {knowledge_base.name}") - - knowledge_bases.append(knowledge_base) - if knowledge_base.external_id: - external_id_list.append(str(knowledge_base.external_id)) - - # 创建metadata - metadata = { - 'model_id': data.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'), - 'dataset_id_list': dataset_ids, - 'dataset_external_id_list': external_id_list, - 'dataset_names': [kb.name for kb in knowledge_bases] - } - - # 设置标题 - title = data.get('title', 'New chat') - - # 创建用户问题记录 - question_record = ChatHistory.objects.create( - user=user, - knowledge_base=knowledge_bases[0], # 使用第一个知识库 - conversation_id=conversation_id, - title=title, - role='user', - content=data['question'], - metadata=metadata - ) - - return question_record, conversation_id, metadata, knowledge_bases, external_id_list - - except Exception as e: - logger.error(f"创建聊天记录失败: {str(e)}") - raise - - def get_conversation_detail(self, user, conversation_id): - """获取会话详情,供chat、gmail、feishu模块复用""" - try: - # 获取用户有权限的知识库ID - accessible_kb_ids = [ - kb.id for kb in KnowledgeBase.objects.all() - if self.check_knowledge_base_permission(kb, user, 'read') - ] - - # 查询会话记录 - messages = ChatHistory.objects.filter( - conversation_id=conversation_id, - is_deleted=False - ).filter( - Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids) - ).order_by('created_at') - - if not messages.exists(): - raise ValueError("对话不存在或无权限") - - # 获取知识库信息 - first_message = messages.first() - dataset_info = [] - if first_message and first_message.metadata and 'dataset_id_list' in first_message.metadata: - datasets = KnowledgeBase.objects.filter(id__in=first_message.metadata['dataset_id_list']) - accessible_datasets = [ - ds for ds in datasets if self.check_knowledge_base_permission(ds, user, 'read') - ] - dataset_info = [ - {'id': str(ds.id), 'name': ds.name, 'type': ds.type} - for ds in accessible_datasets - ] - - # 构建消息列表 - message_list = [ - { - 'id': str(msg.id), - 'parent_id': msg.parent_id, - 'role': msg.role, - 'content': msg.content, - 'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S'), - 'metadata': msg.metadata - } - for msg in messages - ] - - return { - 'conversation_id': conversation_id, - 'datasets': dataset_info, - 'messages': message_list - } - - except Exception as e: - logger.error(f"获取会话详情失败: {str(e)}") - raise - \ No newline at end of file diff --git a/apps/chat/serializers.py b/apps/chat/serializers.py index e69de29..f5c21b4 100644 --- a/apps/chat/serializers.py +++ b/apps/chat/serializers.py @@ -0,0 +1,19 @@ +# apps/chat/serializers.py +from rest_framework import serializers +from apps.chat.models import ChatHistory +from apps.knowledge_base.models import KnowledgeBase + +class ChatHistorySerializer(serializers.ModelSerializer): + knowledge_base_id = serializers.UUIDField(source='knowledge_base.id', read_only=True) + dataset_name = serializers.CharField(source='knowledge_base.name', read_only=True) + user_id = serializers.UUIDField(source='user.id', read_only=True) + + class Meta: + model = ChatHistory + fields = [ + 'id', 'user_id', 'knowledge_base_id', 'dataset_name', 'conversation_id', + 'title', 'role', 'content', 'parent_id', 'metadata', 'created_at', 'is_deleted' + ] + read_only_fields = ['id', 'user_id', 'knowledge_base_id', 'dataset_name', 'created_at', 'is_deleted'] + + \ No newline at end of file diff --git a/apps/chat/services/chat_api.py b/apps/chat/services/chat_api.py new file mode 100644 index 0000000..ae54106 --- /dev/null +++ b/apps/chat/services/chat_api.py @@ -0,0 +1,346 @@ +import requests +import json +import logging +from django.conf import settings +from rest_framework.exceptions import APIException +from daren_project import settings + +logger = logging.getLogger(__name__) + +class ExternalAPIError(APIException): + status_code = 500 + default_detail = '外部API调用失败' + default_code = 'external_api_error' + +def stream_chat_answer(conversation_id, question, dataset_external_ids, metadata): + """流式调用外部聊天API,返回生成器以实时传输回答""" + try: + # 构造聊天请求数据 + chat_request_data = { + "id": "d5d11efa-ea9a-11ef-9933-0242ac120006", + "model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006", + "dataset_id_list": [str(id) for id in 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"调用流式聊天API: {settings.API_BASE_URL}/api/application/chat/open") + chat_response = requests.post( + url=f"{settings.API_BASE_URL}/api/application/chat/open", + json=chat_request_data, + headers={"Content-Type": "application/json"} + ) + + logger.info(f"聊天API响应状态码: {chat_response.status_code}") + if chat_response.status_code != 200: + error_msg = f"聊天API调用失败: {chat_response.text}" + logger.error(error_msg) + yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n" + return + + chat_data = chat_response.json() + if chat_data.get('code') != 200 or not chat_data.get('data'): + error_msg = f"聊天API返回错误: {chat_data}" + logger.error(error_msg) + yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n" + return + + chat_id = chat_data['data'] + message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}" + logger.info(f"调用消息API: {message_url}") + + # 发起消息请求(流式) + message_request = requests.post( + url=message_url, + json={"message": question, "re_chat": False, "stream": True}, + headers={"Content-Type": "application/json"}, + stream=True + ) + + if message_request.status_code != 200: + error_msg = f"消息API调用失败: {message_request.status_code}, {message_request.text}" + logger.error(error_msg) + yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n" + return + + buffer = "" + for chunk in message_request.iter_content(chunk_size=1): + if not chunk: + continue + + chunk_str = chunk.decode('utf-8') + buffer += chunk_str + + if '\n\n' in buffer: + lines = buffer.split('\n\n') + for line in lines[:-1]: + if line.startswith('data: '): + try: + json_str = line[6:] + data = json.loads(json_str) + if 'content' in data: + content_part = data['content'] + response_data = { + 'code': 200, + 'message': 'partial', + 'data': { + 'content': content_part, + 'is_end': data.get('is_end', False) + } + } + yield f"data: {json.dumps(response_data)}\n\n" + + if data.get('is_end', False): + return + except json.JSONDecodeError as e: + logger.error(f"JSON解析错误: {e}, 数据: {line}") + + buffer = lines[-1] + + if buffer and buffer.startswith('data: '): + try: + json_str = buffer[6:] + data = json.loads(json_str) + if 'content' in data: + content_part = data['content'] + response_data = { + 'code': 200, + 'message': 'partial', + 'data': { + 'content': content_part, + 'is_end': data.get('is_end', False) + } + } + yield f"data: {json.dumps(response_data)}\n\n" + except json.JSONDecodeError: + logger.error(f"处理剩余数据时JSON解析错误: {buffer}") + + except Exception as e: + logger.error(f"流式聊天API处理出错: {str(e)}") + yield f"data: {json.dumps({'code': 500, 'message': f'流式处理出错: {str(e)}', 'data': {'is_end': True}})}\n\n" + +def get_chat_answer(dataset_external_ids, question): + """非流式调用外部聊天API获取回答""" + try: + chat_request_data = { + "id": "d5d11efa-ea9a-11ef-9933-0242ac120006", + "model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006", + "dataset_id_list": [str(id) for id in 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"调用非流式聊天API: {settings.API_BASE_URL}/api/application/chat/open") + chat_response = requests.post( + url=f"{settings.API_BASE_URL}/api/application/chat/open", + json=chat_request_data, + headers={"Content-Type": "application/json"} + ) + + logger.info(f"聊天API响应状态码: {chat_response.status_code}") + if chat_response.status_code != 200: + logger.error(f"聊天API调用失败: {chat_response.text}") + raise ExternalAPIError(f"聊天API调用失败: {chat_response.text}") + + chat_data = chat_response.json() + if chat_data.get('code') != 200 or not chat_data.get('data'): + logger.error(f"聊天API返回错误: {chat_data}") + raise ExternalAPIError(f"聊天API返回错误: {chat_data}") + + chat_id = chat_data['data'] + message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}" + logger.info(f"调用消息API: {message_url}") + + message_response = requests.post( + url=message_url, + json={"message": question, "re_chat": False, "stream": False}, + headers={"Content-Type": "application/json"} + ) + + if message_response.status_code != 200: + logger.error(f"消息API调用失败: {message_response.status_code}, {message_response.text}") + raise ExternalAPIError(f"消息API调用失败: {message_response.status_code}, {message_response.text}") + + response_data = message_response.json() + if response_data.get('code') != 200 or 'data' not in response_data: + logger.error(f"消息API返回错误: {response_data}") + raise ExternalAPIError(f"消息API返回错误: {response_data}") + + answer_content = response_data.get('data', {}).get('content', '') + return answer_content if answer_content else "无法获取回答内容" + + except requests.exceptions.RequestException as e: + logger.error(f"聊天API网络错误: {str(e)}") + raise ExternalAPIError(f"聊天API网络错误: {str(e)}") + except json.JSONDecodeError as e: + logger.error(f"解析聊天API响应JSON失败: {str(e)}") + raise ExternalAPIError(f"解析响应数据失败: {str(e)}") + except Exception as e: + logger.error(f"调用聊天API失败: {str(e)}") + raise ExternalAPIError(f"调用聊天API失败: {str(e)}") + +def generate_conversation_title(question, answer): + """调用DeepSeek API生成会话标题""" + try: + prompt = f"请根据以下用户问题和AI回答,为本次对话生成一个简洁的标题(不超过20个字):\n\n**用户问题**:\n{question}\n\n**AI回答**:\n{answer}\n\n生成的标题应概括对话主题,语言简洁明了,使用中文。" + deepseek_url = "https://api.deepseek.com/v1/chat/completions" + deepseek_data = { + "model": "deepseek-pro", + "messages": [ + {"role": "system", "content": "你是一个擅长总结的助手,生成简洁的对话标题。"}, + {"role": "user", "content": prompt} + ], + "max_tokens": 50, + "temperature": 0.7 + } + + logger.info(f"调用DeepSeek API生成标题: {deepseek_url}") + response = requests.post( + deepseek_url, + json=deepseek_data, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {settings.DEEPSEEK_API_KEY}" + } + ) + + logger.info(f"DeepSeek API响应状态码: {response.status_code}") + if response.status_code != 200: + logger.error(f"DeepSeek API调用失败: {response.text}") + raise ExternalAPIError(f"DeepSeek API调用失败: {response.text}") + + result = response.json() + title = result.get('choices', [{}])[0].get('message', {}).get('content', '').strip() + return title if title else None + + except requests.exceptions.RequestException as e: + logger.error(f"DeepSeek API网络错误: {str(e)}") + raise ExternalAPIError(f"DeepSeek API网络错误: {str(e)}") + except json.JSONDecodeError as e: + logger.error(f"解析DeepSeek API响应JSON失败: {str(e)}") + raise ExternalAPIError(f"解析响应数据失败: {str(e)}") + except Exception as e: + logger.error(f"调用DeepSeek API失败: {str(e)}") + raise ExternalAPIError(f"调用DeepSeek API失败: {str(e)}") + +def get_hit_test_documents(dataset_id, query_text): + """调用知识库hit_test接口获取相关文档信息""" + try: + url = f"{settings.API_BASE_URL}/api/dataset/{dataset_id}/hit_test" + params = { + "query_text": query_text, + "top_number": 10, + "similarity": 0.3, + "search_mode": "blend" + } + + logger.info(f"调用hit_test API: {url}") + response = requests.get(url=url, params=params) + + logger.info(f"hit_test API响应状态码: {response.status_code}") + if response.status_code != 200: + logger.error(f"hit_test API调用失败: {response.status_code}, {response.text}") + raise ExternalAPIError(f"hit_test API调用失败: {response.status_code}, {response.text}") + + result = response.json() + if result.get('code') != 200: + logger.error(f"hit_test API业务错误: {result}") + raise ExternalAPIError(f"hit_test API业务错误: {result}") + + documents = result.get('data', []) + return [ + { + "document_name": doc.get("document_name", ""), + "dataset_name": doc.get("dataset_name", ""), + "similarity": doc.get("similarity", 0), + "comprehensive_score": doc.get("comprehensive_score", 0) + } + for doc in documents + ] + + except requests.exceptions.RequestException as e: + logger.error(f"hit_test API网络错误: {str(e)}") + raise ExternalAPIError(f"hit_test API网络错误: {str(e)}") + except json.JSONDecodeError as e: + logger.error(f"解析hit_test API响应JSON失败: {str(e)}") + raise ExternalAPIError(f"解析响应数据失败: {str(e)}") + except Exception as e: + logger.error(f"调用hit_test API失败: {str(e)}") + raise ExternalAPIError(f"调用hit_test API失败: {str(e)}") + +def generate_conversation_title_from_deepseek(user_question, assistant_answer): + """调用SiliconCloud API生成会话标题,直接基于当前问题和回答内容""" + try: + # 从Django设置中获取API密钥 + api_key = settings.SILICON_CLOUD_API_KEY + if not api_key: + return "新对话" + + # 构建提示信息 + prompt = f"请根据用户的问题和助手的回答,生成一个简短的对话标题(不超过20个字)。\n\n用户问题: {user_question}\n\n助手回答: {assistant_answer}" + + import requests + + url = "https://api.siliconflow.cn/v1/chat/completions" + + payload = { + "model": "deepseek-ai/DeepSeek-V3", + "stream": False, + "max_tokens": 512, + "temperature": 0.7, + "top_p": 0.7, + "top_k": 50, + "frequency_penalty": 0.5, + "n": 1, + "stop": [], + "messages": [ + { + "role": "user", + "content": prompt + } + ] + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + response = requests.post(url, json=payload, headers=headers) + response_data = response.json() + + if response.status_code == 200 and 'choices' in response_data and response_data['choices']: + title = response_data['choices'][0]['message']['content'].strip() + return title[:50] # 截断过长的标题 + else: + logger.error(f"生成标题时出错: {response.text}") + return "新对话" + + except Exception as e: + logger.exception(f"生成对话标题时发生错误: {str(e)}") + return "新对话" diff --git a/apps/chat/urls.py b/apps/chat/urls.py index e69de29..9441658 100644 --- a/apps/chat/urls.py +++ b/apps/chat/urls.py @@ -0,0 +1,11 @@ +# apps/chat/urls.py +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from apps.chat.views import ChatHistoryViewSet + +router = DefaultRouter() +router.register(r'', ChatHistoryViewSet, basename='chat-history') + +urlpatterns = [ + path('', include(router.urls)), +] diff --git a/apps/chat/views.py b/apps/chat/views.py index 91ea44a..56d2e98 100644 --- a/apps/chat/views.py +++ b/apps/chat/views.py @@ -1,3 +1,792 @@ -from django.shortcuts import render +import logging +import json +import traceback +import uuid +from datetime import datetime +from django.db.models import Q, Max, Count +from django.http import HttpResponse, StreamingHttpResponse +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.decorators import action +from apps.accounts.models import User +from apps.knowledge_base.models import KnowledgeBase +from apps.chat.models import ChatHistory +from apps.chat.serializers import ChatHistorySerializer +from apps.common.services.chat_service import ChatService +from apps.chat.services.chat_api import ( + ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title, + get_hit_test_documents, generate_conversation_title_from_deepseek +) +from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin -# Create your views here. +logger = logging.getLogger(__name__) + +class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): + permission_classes = [IsAuthenticated] + serializer_class = ChatHistorySerializer + queryset = ChatHistory.objects.all() + + def get_queryset(self): + """确保用户只能看到自己的未删除的聊天记录以及有权限的知识库关联的聊天记录""" + user = self.request.user + accessible_kb_ids = [ + kb.id for kb in KnowledgeBase.objects.all() + if self.check_knowledge_base_permission(kb, user, 'read') + ] + return ChatHistory.objects.filter( + Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids), + is_deleted=False + ) + + def list(self, request): + """获取对话列表概览""" + try: + page = int(request.query_params.get('page', 1)) + page_size = int(request.query_params.get('page_size', 10)) + + 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') + + 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']) + dataset_info = [] + if latest_record.metadata: + dataset_id_list = latest_record.metadata.get('dataset_id_list', []) + dataset_names = latest_record.metadata.get('dataset_names', []) + 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: + 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], + 'datasets': dataset_info + }) + + 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)}") + import traceback + 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 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) + + chat_service = ChatService() + result = chat_service.get_conversation_detail(request.user, conversation_id) + + return Response({ + 'code': 200, + 'message': '获取成功', + 'data': result + }) + + except ValueError as e: + return Response({ + 'code': 404, + 'message': str(e), + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"获取对话详情失败: {str(e)}") + import traceback + 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 = [ + dataset for dataset in KnowledgeBase.objects.all() + if self.check_knowledge_base_permission(dataset, user, 'read') + ] + + 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)}") + return Response({ + 'code': 500, + 'message': f'获取可用知识库列表失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def create_conversation(self, request): + """创建会话 - 先选择知识库创建会话ID,不发送问题""" + try: + data = request.data + + # 检查知识库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) + + # 验证所有知识库 + 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) + + # 使用统一的权限检查方法 + if not self.check_knowledge_base_permission(knowledge_base, user, 'read'): + return Response({ + 'code': 403, + 'message': f'无权访问知识库: {knowledge_base.name}', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + except Exception as e: + return Response({ + 'code': 400, + 'message': f'处理知识库ID出错: {str(e)}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 创建一个新的会话ID + conversation_id = str(uuid.uuid4()) + logger.info(f"创建新的会话ID: {conversation_id}") + + # 准备metadata (仍然保存知识库名称用于内部处理) + metadata = { + 'dataset_id_list': [str(id) for id in dataset_ids], + 'dataset_names': [kb.name for kb in knowledge_bases] + } + + return Response({ + 'code': 200, + 'message': '会话创建成功', + 'data': { + 'conversation_id': conversation_id, + 'dataset_id_list': metadata['dataset_id_list'] + } + }) + + 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: + chat_service = ChatService() + question_record, conversation_id, metadata, knowledge_bases, external_id_list = chat_service.create_chat_record( + request.user, request.data, request.data.get('conversation_id') + ) + + use_stream = request.data.get('stream', True) + title = request.data.get('title', 'New chat') + + if use_stream: + def stream_response(): + answer_record = ChatHistory.objects.create( + user=question_record.user, + knowledge_base=knowledge_bases[0], + conversation_id=conversation_id, + title=title, + parent_id=str(question_record.id), + role='assistant', + content="", + metadata=metadata + ) + + yield f"data: {json.dumps({'code': 200, 'message': '开始流式传输', 'data': {'id': str(answer_record.id), 'conversation_id': conversation_id, 'content': '', 'is_end': False}})}\n\n" + + full_content = "" + for data in stream_chat_answer(conversation_id, request.data['question'], external_id_list, metadata): + parsed_data = json.loads(data[5:-2]) # 移除"data: "和"\n\n" + if parsed_data['code'] == 200 and 'content' in parsed_data['data']: + content_part = parsed_data['data']['content'] + full_content += content_part + response_data = { + 'code': 200, + 'message': 'partial', + 'data': { + 'id': str(answer_record.id), + 'conversation_id': conversation_id, + 'title': title, + 'content': content_part, + 'is_end': parsed_data['data']['is_end'] + } + } + yield f"data: {json.dumps(response_data)}\n\n" + + if parsed_data['data']['is_end']: + answer_record.content = full_content.strip() + answer_record.save() + + current_title = ChatHistory.objects.filter( + conversation_id=conversation_id + ).exclude( + title__in=["New chat", "新对话", ""] + ).values_list('title', flat=True).first() + + if current_title: + title_updated = current_title + else: + try: + generated_title = generate_conversation_title( + request.data['question'], full_content.strip() + ) + if generated_title: + ChatHistory.objects.filter( + conversation_id=conversation_id + ).update(title=generated_title) + title_updated = generated_title + else: + title_updated = "新对话" + except ExternalAPIError as e: + logger.error(f"自动生成标题失败: {str(e)}") + title_updated = "新对话" + + final_response = { + 'code': 200, + 'message': '完成', + 'data': { + 'id': str(answer_record.id), + 'conversation_id': conversation_id, + 'title': title_updated, + 'dataset_id_list': metadata.get('dataset_id_list', []), + 'dataset_names': metadata.get('dataset_names', []), + 'role': 'assistant', + 'content': full_content.strip(), + 'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'is_end': True + } + } + yield f"data: {json.dumps(final_response)}\n\n" + break + elif parsed_data['code'] != 200: + yield data + break + + if full_content: + try: + answer_record.content = full_content.strip() + answer_record.save() + except Exception as save_error: + logger.error(f"保存部分内容失败: {str(save_error)}") + + response = StreamingHttpResponse( + stream_response(), + content_type='text/event-stream', + status=status.HTTP_201_CREATED + ) + response['Cache-Control'] = 'no-cache, no-store' + response['Connection'] = 'keep-alive' + return response + else: + logger.info("使用非流式输出模式") + try: + answer = get_chat_answer(external_id_list, request.data['question']) + except ExternalAPIError as e: + logger.error(f"获取回答失败: {str(e)}") + return Response({ + 'code': 500, + 'message': f'获取回答失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + if answer is None: + return Response({ + 'code': 500, + 'message': '获取回答失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + answer_record = ChatHistory.objects.create( + user=request.user, + knowledge_base=knowledge_bases[0], + conversation_id=conversation_id, + title=title, + parent_id=str(question_record.id), + role='assistant', + content=answer, + metadata=metadata + ) + + existing_records = ChatHistory.objects.filter(conversation_id=conversation_id) + should_generate_title = not existing_records.exclude(id=question_record.id).exists() and (not title or title == 'New chat') + if should_generate_title: + try: + generated_title = generate_conversation_title( + request.data['question'], answer + ) + if generated_title: + ChatHistory.objects.filter(conversation_id=conversation_id).update(title=generated_title) + title = generated_title + except ExternalAPIError as e: + logger.error(f"自动生成标题失败: {str(e)}") + + return Response({ + 'code': 200, + 'message': '成功', + 'data': { + 'id': str(answer_record.id), + 'conversation_id': conversation_id, + 'title': title, + 'dataset_id_list': metadata.get('dataset_id_list', []), + 'dataset_names': metadata.get('dataset_names', []), + 'role': 'assistant', + 'content': answer, + 'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S') + } + }, status=status.HTTP_201_CREATED) + + except ValueError as e: + return Response({ + 'code': 400, + 'message': str(e), + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"创建聊天记录失败: {str(e)}") + import traceback + 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 hit_test(self, request): + """获取问题与知识库文档的匹配度""" + try: + data = request.data + if 'question' not in data or 'dataset_id_list' not in data or not data['dataset_id_list']: + return Response({ + 'code': 400, + 'message': '缺少必填字段: question 或 dataset_id_list', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + question = data['question'] + dataset_ids = data['dataset_id_list'] + if not isinstance(dataset_ids, list): + try: + dataset_ids = json.loads(dataset_ids) + if not isinstance(dataset_ids, list): + dataset_ids = [dataset_ids] + except (json.JSONDecodeError, TypeError): + dataset_ids = [dataset_ids] + + external_id_list = [] + for kb_id in dataset_ids: + kb = KnowledgeBase.objects.filter(id=kb_id).first() + if not kb: + return Response({ + 'code': 404, + 'message': f'知识库不存在: {kb_id}', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + if not self.check_knowledge_base_permission(kb, request.user, 'read'): + return Response({ + 'code': 403, + 'message': f'无权访问知识库: {kb.name}', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + if kb.external_id: + external_id_list.append(str(kb.external_id)) + + if not external_id_list: + return Response({ + 'code': 400, + 'message': '没有有效的知识库external_id', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + all_documents = [] + for dataset_id in external_id_list: + try: + doc_info = get_hit_test_documents(dataset_id, question) + if doc_info: + all_documents.extend(doc_info) + except ExternalAPIError as e: + logger.error(f"调用hit_test失败: 知识库ID={dataset_id}, 错误={str(e)}") + continue # 宽松处理,跳过失败的知识库 + + all_documents = sorted(all_documents, key=lambda x: x.get('similarity', 0), reverse=True) + return Response({ + 'code': 200, + 'message': '成功', + 'data': { + 'question': question, + 'matched_documents': all_documents, + 'total_count': len(all_documents) + } + }) + + except Exception as e: + logger.error(f"hit_test接口调用失败: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'hit_test接口调用失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def _highlight_keyword(self, text, keyword): + """高亮关键词""" + if not keyword or not text: + return text + return text.replace(keyword, f'{keyword}') + + def update(self, request, pk=None): + """更新聊天记录""" + try: + record = self.get_queryset().filter(id=pk).first() + if not record: + return Response({ + 'code': 404, + 'message': '记录不存在或无权限', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + data = request.data + updateable_fields = ['content', 'metadata'] + if 'content' in data: + record.content = data['content'] + if 'metadata' in data: + current_metadata = record.metadata or {} + current_metadata.update(data['metadata']) + record.metadata = current_metadata + + record.save() + return Response({ + 'code': 200, + 'message': '更新成功', + 'data': { + 'id': str(record.id), + 'conversation_id': record.conversation_id, + 'role': record.role, + 'content': record.content, + 'metadata': record.metadata, + 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + }) + + except Exception as e: + logger.error(f"更新聊天记录失败: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'更新聊天记录失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def destroy(self, request, pk=None): + """删除聊天记录(软删除)""" + try: + record = self.get_queryset().filter(id=pk).first() + if not record: + return Response({ + 'code': 404, + 'message': '记录不存在或无权限', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + record.soft_delete() + return Response({ + 'code': 200, + 'message': '删除成功', + 'data': None + }) + + except Exception as e: + logger.error(f"删除聊天记录失败: {str(e)}") + import traceback + 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 search(self, request): + """搜索聊天记录""" + try: + keyword = request.query_params.get('keyword', '').strip() + dataset_id = request.query_params.get('dataset_id') + start_date = request.query_params.get('start_date') + end_date = request.query_params.get('end_date') + page = int(request.query_params.get('page', 1)) + page_size = int(request.query_params.get('page_size', 10)) + + query = self.get_queryset() + if keyword: + query = query.filter( + Q(content__icontains=keyword) | + Q(knowledge_base__name__icontains=keyword) + ) + if dataset_id: + knowledge_base = KnowledgeBase.objects.filter(id=dataset_id).first() + if knowledge_base and not self.check_knowledge_base_permission(knowledge_base, request.user, 'read'): + return Response({ + 'code': 403, + 'message': '无权访问该知识库', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + query = query.filter(knowledge_base__id=dataset_id) + if start_date: + query = query.filter(created_at__gte=start_date) + if end_date: + query = query.filter(created_at__lte=end_date) + + total = query.count() + start = (page - 1) * page_size + end = start + page_size + records = query.order_by('-created_at')[start:end] + + results = [ + { + 'id': str(record.id), + 'conversation_id': record.conversation_id, + 'dataset_id': str(record.knowledge_base.id), + 'dataset_name': record.knowledge_base.name, + 'role': record.role, + 'content': record.content, + 'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'metadata': record.metadata, + 'highlights': {'content': self._highlight_keyword(record.content, keyword)} if keyword else {} + } + for record in records + ] + + 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)}") + import traceback + 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=['delete']) + def delete_conversation(self, request): + """通过conversation_id删除一组会话""" + 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) + + records = self.get_queryset().filter(conversation_id=conversation_id) + if not records.exists(): + return Response({ + 'code': 404, + 'message': '未找到该会话或无权限访问', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + records_count = records.count() + for record in records: + record.soft_delete() + + return Response({ + 'code': 200, + 'message': '删除成功', + 'data': { + 'conversation_id': conversation_id, + 'deleted_count': records_count + } + }) + + except Exception as e: + logger.error(f"删除会话失败: {str(e)}") + import traceback + 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'], url_path='generate-conversation-title') + def generate_conversation_title(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, + is_deleted=False, + user=request.user + ).order_by('created_at') + + if not messages.exists(): + return Response({ + 'code': 404, + 'message': '对话不存在或无权访问', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 检查是否有自定义标题参数 + custom_title = request.query_params.get('title') + if not custom_title: + return Response({ + 'code': 400, + 'message': '缺少title参数', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 更新所有相关记录的标题 + ChatHistory.objects.filter( + conversation_id=conversation_id, + user=request.user + ).update(title=custom_title) + + return Response({ + 'code': 200, + 'message': '更新会话标题成功', + 'data': { + 'conversation_id': conversation_id, + 'title': custom_title + } + }) + + 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) diff --git a/apps/common/services/chat_service.py b/apps/common/services/chat_service.py index e69de29..f8ccea1 100644 --- a/apps/common/services/chat_service.py +++ b/apps/common/services/chat_service.py @@ -0,0 +1,143 @@ +# apps/common/services/chat_service.py +import logging +import json +from uuid import uuid4 +from django.db import transaction +from apps.accounts.models import User +from apps.knowledge_base.models import KnowledgeBase +from apps.chat.models import ChatHistory +from apps.permissions.services.permission_service import KnowledgeBasePermissionMixin + +logger = logging.getLogger(__name__) + +class ChatService(KnowledgeBasePermissionMixin): + @transaction.atomic + def create_chat_record(self, user, data, conversation_id=None): + """创建聊天记录,供chat、gmail、feishu模块复用""" + try: + # 验证必填字段 + if 'question' not in data: + raise ValueError("缺少必填字段: question") + + # 如果未提供conversation_id,生成新的 + if not conversation_id: + conversation_id = str(uuid4()) + logger.info(f"生成新的会话ID: {conversation_id}") + + # 处理知识库ID + dataset_ids = [] + if 'dataset_id' in data: + dataset_ids.append(str(data['dataset_id'])) + elif 'dataset_id_list' in data: + if isinstance(data['dataset_id_list'], str): + try: + dataset_list = json.loads(data['dataset_id_list']) + dataset_ids = [str(id) for id in dataset_list if isinstance(dataset_list, list)] + except json.JSONDecodeError: + dataset_ids = [str(data['dataset_id_list'])] + else: + dataset_ids = [str(id) for id in data['dataset_id_list']] + + if not dataset_ids: + raise ValueError("缺少必填字段: dataset_id 或 dataset_id_list") + + # 验证知识库权限 + knowledge_bases = [] + external_id_list = [] + for kb_id in dataset_ids: + knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first() + if not knowledge_base: + raise ValueError(f"知识库不存在: {kb_id}") + + if not self.check_knowledge_base_permission(knowledge_base, user, 'read'): + raise ValueError(f"无权访问知识库: {knowledge_base.name}") + + knowledge_bases.append(knowledge_base) + if knowledge_base.external_id: + external_id_list.append(str(knowledge_base.external_id)) + + # 创建metadata + metadata = { + 'model_id': data.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'), + 'dataset_id_list': dataset_ids, + 'dataset_external_id_list': external_id_list, + 'dataset_names': [kb.name for kb in knowledge_bases] + } + + # 设置标题 + title = data.get('title', 'New chat') + + # 创建用户问题记录 + question_record = ChatHistory.objects.create( + user=user, + knowledge_base=knowledge_bases[0], # 使用第一个知识库 + conversation_id=conversation_id, + title=title, + role='user', + content=data['question'], + metadata=metadata + ) + + return question_record, conversation_id, metadata, knowledge_bases, external_id_list + + except Exception as e: + logger.error(f"创建聊天记录失败: {str(e)}") + raise + + def get_conversation_detail(self, user, conversation_id): + """获取会话详情,供chat、gmail、feishu模块复用""" + try: + # 获取用户有权限的知识库ID + accessible_kb_ids = [ + kb.id for kb in KnowledgeBase.objects.all() + if self.check_knowledge_base_permission(kb, user, 'read') + ] + + # 查询会话记录 + messages = ChatHistory.objects.filter( + conversation_id=conversation_id, + is_deleted=False + ).filter( + Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids) + ).order_by('created_at') + + if not messages.exists(): + raise ValueError("对话不存在或无权限") + + # 获取知识库信息 + first_message = messages.first() + dataset_info = [] + if first_message and first_message.metadata and 'dataset_id_list' in first_message.metadata: + datasets = KnowledgeBase.objects.filter(id__in=first_message.metadata['dataset_id_list']) + accessible_datasets = [ + ds for ds in datasets if self.check_knowledge_base_permission(ds, user, 'read') + ] + dataset_info = [ + {'id': str(ds.id), 'name': ds.name, 'type': ds.type} + for ds in accessible_datasets + ] + + # 构建消息列表 + message_list = [ + { + 'id': str(msg.id), + 'parent_id': msg.parent_id, + 'role': msg.role, + 'content': msg.content, + 'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'metadata': msg.metadata + } + for msg in messages + ] + + return { + 'conversation_id': conversation_id, + 'datasets': dataset_info, + 'messages': message_list + } + + except Exception as e: + logger.error(f"获取会话详情失败: {str(e)}") + raise + + \ No newline at end of file diff --git a/apps/common/services/external_api_service.py b/apps/common/services/external_api_service.py index 7669a57..e816981 100644 --- a/apps/common/services/external_api_service.py +++ b/apps/common/services/external_api_service.py @@ -103,76 +103,84 @@ def call_split_api_multiple(files): try: url = f'{settings.API_BASE_URL}/api/dataset/document/split' - # 准备多文件上传数据 - files_data = {} - for i, file_obj in enumerate(files): - if hasattr(file_obj, 'seek'): - file_obj.seek(0) - - logger.info(f"准备上传文件 {i+1}/{len(files)}: {file_obj.name}, 大小: {file_obj.size}字节, 类型: {file_obj.content_type}") - - # 添加文件预览日志 - if hasattr(file_obj, 'read') and hasattr(file_obj, 'seek'): - content_preview = file_obj.read(100).decode('utf-8', errors='ignore') - logger.info(f"文件 {i+1} 内容预览: {content_preview}") - file_obj.seek(0) - - # 使用唯一的键名添加到files_data - files_data[f'file{i}'] = file_obj + # 准备请求数据 - 将所有文件作为 'file' 字段 + files_data = [('file', (file.name, file, file.content_type)) for file in files] - logger.info(f"调用分割API URL: {url}, 批量处理 {len(files_data)} 个文件") - logger.info(f"请求字段: {list(files_data.keys())}") + # 记录上传的文件信息 + for file in files: + logger.info(f"准备上传文件: {file.name}, 大小: {file.size}字节, 类型: {file.content_type}") + # 读取文件内容前100个字符进行记录 + if hasattr(file, 'read') and hasattr(file, 'seek'): + file.seek(0) + content_preview = file.read(100).decode('utf-8', errors='ignore') + logger.info(f"文件内容预览: {content_preview}") + file.seek(0) # 重置文件指针 - # 发送批量请求 - response = requests.post(url, files=files_data) + logger.info(f"调用分割API URL: {url}") + logger.info(f"上传文件数量: {len(files_data)}") + # 发送请求 + response = requests.post( + url, + files=files_data + ) + + # 记录请求头和响应信息 logger.info(f"请求头: {response.request.headers}") logger.info(f"响应状态码: {response.status_code}") if response.status_code != 200: logger.error(f"分割API返回错误状态码: {response.status_code}, 响应: {response.text}") return None - + + # 解析响应 result = response.json() logger.info(f"分割API响应详情: {result}") - logger.info(f"成功获取 {len(result.get('data', []))} 个文档结果") + # 如果数据为空,可能是API处理失败,尝试后备方案 if len(result.get('data', [])) == 0: - logger.warning("分割API返回的数据为空,使用后备方案") - # 为所有文件创建后备数据 + logger.warning("分割API返回的数据为空,尝试使用后备方案") fallback_data = { 'code': 200, - 'message': '成功(后备)', - 'data': [{ - 'name': file.name, - 'content': [{ - 'title': '文档内容', - 'content': '文件内容无法自动分割,请检查外部API。这是一个后备内容。' - }] - } for file in files] + 'message': '成功', + 'data': [ + { + 'name': file.name, + 'content': [ + { + 'title': '文档内容', + 'content': '文件内容无法自动分割,请检查外部API。这是一个后备内容。' + } + ] + } for file in files + ] } - logger.info(f"使用后备数据结构,为 {len(files)} 个文件生成数据") + logger.info("使用后备数据结构") return fallback_data - - return result + return result + except Exception as e: logger.error(f"调用分割API失败: {str(e)}") logger.error(traceback.format_exc()) - # 为所有文件创建后备响应 + # 创建后备响应 fallback_response = { 'code': 200, - 'message': '成功(后备)', - 'data': [{ - 'name': file.name if hasattr(file, 'name') else f'文件_{i}', - 'content': [{ - 'title': '文档内容', - 'content': '文件内容无法自动分割,请检查API连接。' - }] - } for i, file in enumerate(files)] + 'message': '成功', + 'data': [ + { + 'name': file.name, + 'content': [ + { + 'title': '文档内容', + 'content': '文件内容无法自动分割,请检查API连接。' + } + ] + } for file in files + ] } - logger.info(f"由于异常,返回后备响应,包含 {len(fallback_response['data'])} 个条目") + logger.info("由于异常,返回后备响应") return fallback_response def call_upload_api(external_id, doc_data): diff --git a/apps/common/services/notification_service.py b/apps/common/services/notification_service.py new file mode 100644 index 0000000..4aac112 --- /dev/null +++ b/apps/common/services/notification_service.py @@ -0,0 +1,43 @@ +# apps/common/services/notification_service.py +import logging +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer +from apps.notifications.models import Notification + +logger = logging.getLogger(__name__) + +class NotificationService: + def send_notification(self, user, title, content, notification_type, related_object_id, sender=None): + """发送通知并通过WebSocket推送""" + try: + notification = Notification.objects.create( + sender=sender, + receiver=user, + title=title, + content=content, + type=notification_type, + related_resource=related_object_id, + ) + + channel_layer = get_channel_layer() + async_to_sync(channel_layer.group_send)( + f"notification_user_{user.id}", + { + "type": "notification", + "data": { + "id": str(notification.id), + "title": notification.title, + "content": notification.content, + "type": notification.type, + "created_at": notification.created_at.isoformat(), + "sender": { + "id": str(notification.sender.id), + "name": notification.sender.name + } if notification.sender else None + } + } + ) + except Exception as e: + logger.error(f"发送通知失败: {str(e)}") + + \ No newline at end of file diff --git a/apps/common/services/permission_service.py b/apps/common/services/permission_service.py new file mode 100644 index 0000000..cfe3aa0 --- /dev/null +++ b/apps/common/services/permission_service.py @@ -0,0 +1,338 @@ +# apps/common/services/permission_service.py +import logging +from django.db import transaction +from django.db.models import Q +from django.utils import timezone +from rest_framework.exceptions import ValidationError +from apps.accounts.models import User +from apps.knowledge_base.models import KnowledgeBase +from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel + +logger = logging.getLogger(__name__) + +class PermissionService: + def can_manage_knowledge_base(self, user, knowledge_base): + """检查用户是否是知识库的创建者""" + return str(knowledge_base.user_id) == str(user.id) + + def check_extend_permission(self, permission, user): + """检查是否有权限延长权限有效期""" + knowledge_base = permission.knowledge_base + if knowledge_base.type == 'private': + return knowledge_base.user_id == user.id + if knowledge_base.type == 'leader': + return user.role == 'admin' + if knowledge_base.type == 'member': + return user.role == 'admin' or ( + user.role == 'leader' and user.department == knowledge_base.department + ) + return False + + def create_permission_request(self, user, validated_data, notification_service): + """创建权限申请并发送通知""" + knowledge_base = validated_data['knowledge_base'] + if str(knowledge_base.user_id) == str(user.id): + raise ValidationError({ + "code": 400, + "message": "您是此知识库的创建者,无需申请权限", + "data": None + }) + + approver = User.objects.get(id=knowledge_base.user_id) + requested_permissions = validated_data.get('permissions', {}) + expires_at = validated_data.get('expires_at') + + if not any([requested_permissions.get('can_read'), + requested_permissions.get('can_edit'), + requested_permissions.get('can_delete')]): + raise ValidationError("至少需要申请一种权限(读/改/删)") + + if not expires_at: + raise ValidationError("请指定权限到期时间") + + existing_request = Permission.objects.filter( + knowledge_base=knowledge_base, + applicant=user, + status='pending' + ).first() + if existing_request: + raise ValidationError("您已有一个待处理的权限申请") + + existing_permission = Permission.objects.filter( + knowledge_base=knowledge_base, + applicant=user, + status='approved', + expires_at__gt=timezone.now() + ).first() + if existing_permission: + raise ValidationError("您已有此知识库的访问权限") + + with transaction.atomic(): + permission = Permission.objects.create( + knowledge_base=knowledge_base, + applicant=user, + approver=approver, + permissions=requested_permissions, + expires_at=expires_at, + status='pending' + ) + + permission_types = [] + if requested_permissions.get('can_read'): + permission_types.append('读取') + if requested_permissions.get('can_edit'): + permission_types.append('编辑') + if requested_permissions.get('can_delete'): + permission_types.append('删除') + permission_str = '、'.join(permission_types) + + notification_service.send_notification( + user=approver, + title="新的权限申请", + content=f"用户 {user.name} 申请了知识库 '{knowledge_base.name}' 的{permission_str}权限", + notification_type="permission_request", + related_object_id=str(permission.id) + ) + + return permission + + def approve_permission(self, user, permission, response_message, notification_service): + """审批权限申请""" + if not self.can_manage_knowledge_base(user, permission.knowledge_base): + raise ValidationError({ + 'code': 403, + 'message': '只有知识库创建者可以审批此申请', + 'data': None + }) + + with transaction.atomic(): + permission.status = 'approved' + permission.approver = user + permission.response_message = response_message + permission.save() + + kb_permission = KBPermissionModel.objects.filter( + knowledge_base=permission.knowledge_base, + user=permission.applicant + ).first() + + if kb_permission: + kb_permission.can_read = permission.permissions.get('can_read', False) + kb_permission.can_edit = permission.permissions.get('can_edit', False) + kb_permission.can_delete = permission.permissions.get('can_delete', False) + kb_permission.granted_by = user + kb_permission.status = 'active' + kb_permission.expires_at = permission.expires_at + kb_permission.save() + logger.info(f"更新知识库权限记录: {kb_permission.id}") + else: + kb_permission = KBPermissionModel.objects.create( + knowledge_base=permission.knowledge_base, + user=permission.applicant, + can_read=permission.permissions.get('can_read', False), + can_edit=permission.permissions.get('can_edit', False), + can_delete=permission.permissions.get('can_delete', False), + granted_by=user, + status='active', + expires_at=permission.expires_at + ) + logger.info(f"创建新的知识库权限记录: {kb_permission.id}") + + notification_service.send_notification( + user=permission.applicant, + title="权限申请已通过", + content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已通过", + notification_type="permission_approved", + related_object_id=str(permission.id) + ) + + return permission + + def reject_permission(self, user, permission, response_message, notification_service): + """拒绝权限申请""" + if not self.can_manage_knowledge_base(user, permission.knowledge_base): + raise ValidationError({ + 'code': 403, + 'message': '只有知识库创建者可以审批此申请', + 'data': None + }) + + if permission.status != 'pending': + raise ValidationError({ + 'code': 400, + 'message': '该申请已被处理', + 'data': None + }) + + if not response_message: + raise ValidationError({ + 'code': 400, + 'message': '请填写拒绝原因', + 'data': None + }) + + with transaction.atomic(): + permission.status = 'rejected' + permission.approver = user + permission.response_message = response_message + permission.save() + + notification_service.send_notification( + user=permission.applicant, + title="权限申请已拒绝", + content=f"您对知识库 '{permission.knowledge_base.name}' 的权限申请已被拒绝\n拒绝原因:{response_message}", + notification_type="permission_rejected", + related_object_id=str(permission.id) + ) + + return permission + + def extend_permission(self, user, permission, new_expires_at, notification_service): + """延长权限有效期""" + if not self.check_extend_permission(permission, user): + raise ValidationError({ + "code": 403, + "message": "您没有权限延长此权限", + "data": None + }) + + if not new_expires_at: + raise ValidationError({ + "code": 400, + "message": "请设置新的过期时间", + "data": None + }) + + try: + new_expires_at = timezone.datetime.strptime(new_expires_at, '%Y-%m-%dT%H:%M:%SZ') + new_expires_at = timezone.make_aware(new_expires_at) + if new_expires_at <= timezone.now(): + raise ValidationError({ + "code": 400, + "message": "过期时间不能早于或等于当前时间", + "data": None + }) + except ValueError: + raise ValidationError({ + "code": 400, + "message": "过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)", + "data": None + }) + + with transaction.atomic(): + permission.expires_at = new_expires_at + permission.save() + + kb_permission = KBPermissionModel.objects.get( + knowledge_base=permission.knowledge_base, + user=permission.applicant + ) + kb_permission.expires_at = new_expires_at + kb_permission.save() + + notification_service.send_notification( + user=permission.applicant, + title="权限有效期延长", + content=f"您对知识库 '{permission.knowledge_base.name}' 的权限有效期已延长至 {new_expires_at.strftime('%Y-%m-%d %H:%M:%S')}", + notification_type="permission_extended", + related_object_id=str(permission.id) + ) + + return permission + + def update_user_permission(self, admin_user, user_id, knowledge_base_id, permissions, expires_at_str, notification_service): + """管理员更新用户权限""" + if admin_user.role != 'admin': + raise ValidationError({ + 'code': 403, + 'message': '只有管理员可以直接修改权限', + 'data': None + }) + + if not all([user_id, knowledge_base_id, permissions]): + raise ValidationError({ + 'code': 400, + 'message': '缺少必要参数', + 'data': None + }) + + required_permission_fields = ['can_read', 'can_edit', 'can_delete'] + if not all(field in permissions for field in required_permission_fields): + raise ValidationError({ + 'code': 400, + 'message': '权限参数格式错误,必须包含 can_read、can_edit、can_delete', + 'data': None + }) + + try: + user = User.objects.get(id=user_id) + knowledge_base = KnowledgeBase.objects.get(id=knowledge_base_id) + except User.DoesNotExist: + raise ValidationError({ + 'code': 404, + 'message': f'用户ID {user_id} 不存在', + 'data': None + }) + except KnowledgeBase.DoesNotExist: + raise ValidationError({ + 'code': 404, + 'message': f'知识库ID {knowledge_base_id} 不存在', + 'data': None + }) + + if knowledge_base.type == 'private' and str(knowledge_base.user_id) != str(user.id): + raise ValidationError({ + 'code': 403, + 'message': '不能修改其他用户的私有知识库权限', + 'data': None + }) + + if user.role == 'member' and permissions.get('can_delete'): + raise ValidationError({ + 'code': 400, + 'message': '普通成员不能获得删除权限', + 'data': None + }) + + expires_at = None + if expires_at_str: + try: + 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(): + raise ValidationError({ + 'code': 400, + 'message': '过期时间不能早于或等于当前时间', + 'data': None + }) + except ValueError: + raise ValidationError({ + 'code': 400, + 'message': '过期时间格式错误,应为 ISO 格式 (YYYY-MM-DDThh:mm:ssZ)', + 'data': None + }) + + 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': admin_user, + 'status': 'active', + 'expires_at': expires_at + } + ) + + notification_service.send_notification( + user=user, + title="知识库权限更新", + content=f"管理员已{created and '授予' or '更新'}您对知识库 '{knowledge_base.name}' 的权限", + notification_type="permission_updated", + related_object_id=str(permission.id) + ) + + return permission, created + \ No newline at end of file diff --git a/apps/permissions/serializers.py b/apps/permissions/serializers.py index e69de29..fd75f4e 100644 --- a/apps/permissions/serializers.py +++ b/apps/permissions/serializers.py @@ -0,0 +1,18 @@ +# apps/permissions/serializers.py +from rest_framework import serializers +from apps.permissions.models import Permission +from apps.knowledge_base.models import KnowledgeBase +from apps.accounts.models import User + +class PermissionSerializer(serializers.ModelSerializer): + knowledge_base = serializers.PrimaryKeyRelatedField(queryset=KnowledgeBase.objects.all()) + applicant = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), required=False) + approver = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), allow_null=True, required=False) + + class Meta: + model = Permission + fields = [ + 'id', 'knowledge_base', 'applicant', 'approver', 'permissions', + 'status', 'created_at', 'expires_at', 'response_message' + ] + read_only_fields = ['id', 'created_at', 'applicant', 'approver', 'status', 'response_message'] \ No newline at end of file diff --git a/apps/permissions/urls.py b/apps/permissions/urls.py index e69de29..b9953f7 100644 --- a/apps/permissions/urls.py +++ b/apps/permissions/urls.py @@ -0,0 +1,11 @@ +# apps/permissions/urls.py +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from apps.permissions.views import PermissionViewSet + +router = DefaultRouter() +router.register(r'', PermissionViewSet, basename='permission') + +urlpatterns = [ + path('', include(router.urls)), +] diff --git a/apps/permissions/views.py b/apps/permissions/views.py index 91ea44a..4b09e01 100644 --- a/apps/permissions/views.py +++ b/apps/permissions/views.py @@ -1,3 +1,480 @@ -from django.shortcuts import render +# apps/permissions/views.py +import logging +from django.db.models import Q +from django.utils import timezone +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.decorators import action +from apps.accounts.models import User +from apps.knowledge_base.models import KnowledgeBase +from apps.permissions.models import Permission, KnowledgeBasePermission as KBPermissionModel +from apps.permissions.serializers import PermissionSerializer +from apps.common.services.permission_service import PermissionService +from apps.common.services.notification_service import NotificationService -# Create your views here. +logger = logging.getLogger(__name__) + +class PermissionViewSet(viewsets.ModelViewSet): + serializer_class = PermissionSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + """获取权限申请列表:申请人或审批人是当前用户""" + user_id = str(self.request.user.id) + query = Q(applicant_id=user_id) | Q(approver_id=user_id) + 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.applicant.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)}") + import traceback + 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): + """创建权限申请""" + try: + permission_service = PermissionService() + notification_service = NotificationService() + permission = permission_service.create_permission_request( + self.request.user, serializer.validated_data, notification_service + ) + serializer.instance = permission + except Exception as e: + logger.error(f"创建权限申请失败: {str(e)}") + raise + + @action(detail=True, methods=['post']) + def approve(self, request, pk=None): + """批准权限申请""" + try: + permission = self.get_object() + permission_service = PermissionService() + notification_service = NotificationService() + response_message = request.data.get('response_message', '') + permission = permission_service.approve_permission( + request.user, permission, response_message, notification_service + ) + + return Response({ + 'code': 200, + 'message': '权限申请已批准', + 'data': None + }) + + except Permission.DoesNotExist: + return Response({ + 'code': 404, + 'message': '权限申请不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"处理权限申请失败: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'处理权限申请失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=True, methods=['post']) + def reject(self, request, pk=None): + """拒绝权限申请""" + try: + permission = self.get_object() + permission_service = PermissionService() + notification_service = NotificationService() + response_message = request.data.get('response_message') + permission = permission_service.reject_permission( + request.user, permission, response_message, notification_service + ) + + return Response({ + 'code': 200, + 'message': '权限申请已拒绝', + 'data': PermissionSerializer(permission).data + }) + + except Permission.DoesNotExist: + return Response({ + 'code': 404, + 'message': '权限申请不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"拒绝权限申请失败: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'拒绝权限申请失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=True, methods=['post']) + def extend(self, request, pk=None): + """延长权限有效期""" + try: + permission = self.get_object() + permission_service = PermissionService() + notification_service = NotificationService() + new_expires_at = request.data.get('expires_at') + permission = permission_service.extend_permission( + request.user, permission, new_expires_at, notification_service + ) + + return Response({ + 'code': 200, + 'message': '权限有效期延长成功', + 'data': PermissionSerializer(permission).data + }) + + except Permission.DoesNotExist: + return Response({ + 'code': 404, + 'message': '权限申请不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"延长权限有效期失败: {str(e)}") + import traceback + 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 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 = [ + { + '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 + } + for perm in permissions + ] + + 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)}") + import traceback + 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)}") + import traceback + 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: + permission_service = PermissionService() + notification_service = NotificationService() + permission, created = permission_service.update_user_permission( + request.user, + request.data.get('user_id'), + request.data.get('knowledge_base_id'), + request.data.get('permissions'), + request.data.get('expires_at'), + notification_service + ) + + return Response({ + 'code': 200, + 'message': f"{'创建' if created else '更新'}权限成功", + 'data': { + 'id': str(permission.id), + 'user': { + 'id': str(permission.user.id), + 'username': permission.user.username, + 'name': permission.user.name, + 'department': permission.user.department, + 'role': permission.user.role + }, + 'knowledge_base': { + 'id': str(permission.knowledge_base.id), + 'name': permission.knowledge_base.name, + 'type': permission.knowledge_base.type, + 'department': permission.knowledge_base.department, + 'group': permission.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)}") + import traceback + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'更新权限失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + diff --git a/daren_project/settings.py b/daren_project/settings.py index 80a4e18..b3ec797 100644 --- a/daren_project/settings.py +++ b/daren_project/settings.py @@ -171,4 +171,4 @@ AUTH_USER_MODEL = 'accounts.User' API_BASE_URL = 'http://81.69.223.133:48329' SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf' GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/' - +APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006' diff --git a/daren_project/urls.py b/daren_project/urls.py index 84dd0c5..88165ef 100644 --- a/daren_project/urls.py +++ b/daren_project/urls.py @@ -21,8 +21,8 @@ urlpatterns = [ path('admin/', admin.site.urls), path('api/auth/', include('apps.accounts.urls')), path('api/knowledge-bases/', include('apps.knowledge_base.urls')), - # path('api/chat/', include('apps.chat.urls')), - # path('api/permissions/', include('apps.permissions.urls')), + path('api/chat-history/', include('apps.chat.urls')), + path('api/permissions/', include('apps.permissions.urls')), # path('api/message/', include('apps.message.urls')), # path('api/gmail/', include('apps.gmail.urls')), # path('api/feishu/', include('apps.feishu.urls')),