import traceback from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework import status, viewsets from rest_framework.decorators import action from apps.accounts.services.utils import validate_uuid_param from apps.gmail.services.goal_service import generate_recommended_reply, get_conversation_summary, get_last_message from .serializers import GmailCredentialSerializer, UserGoalSerializer, AutoReplyConfigSerializer from .services.gmail_service import GmailService from .models import GmailCredential, GmailConversation, GmailAttachment, UserGoal, ConversationSummary from django.shortcuts import get_object_or_404 import logging import os from django.conf import settings from django.core.files.storage import default_storage from django.core.files.base import ContentFile import json import base64 import threading from apps.feishu.services.auto_gmail_conversation_service import AutoGmailConversationService from django.utils import timezone from django.http import HttpResponse, Http404 from django.db.models import Q, Prefetch import uuid from apps.common.services.ai_service import AIService from apps.chat.models import ChatHistory # 配置日志记录器,用于记录视图操作的调试、警告和错误信息 logger = logging.getLogger(__name__) class GmailAuthInitiateView(APIView): """ API 视图,用于启动 Gmail OAuth2 认证流程。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def post(self, request): """ 处理 POST 请求,启动 Gmail OAuth2 认证并返回授权 URL。 支持两种方式提供客户端密钥: 1. 在请求体中提供client_secret_json字段 2. 上传名为client_secret_file的JSON文件 Args: request: Django REST Framework 请求对象,包含客户端密钥 JSON 数据或文件。 Returns: Response: 包含授权 URL 的 JSON 响应(成功时),或错误信息(失败时)。 Status Codes: 200: 成功生成授权 URL。 400: 请求数据无效。 500: 服务器内部错误(如认证服务失败)。 """ logger.debug(f"Received auth initiate request: {request.data}") # 检查是否是文件上传方式 client_secret_json = None if 'client_secret_file' in request.FILES: try: # 读取上传的JSON文件内容 client_secret_file = request.FILES['client_secret_file'] client_secret_json = json.loads(client_secret_file.read().decode('utf-8')) logger.info(f"从上传文件读取到客户端密钥JSON") except json.JSONDecodeError as e: logger.error(f"解析客户端密钥JSON文件失败: {str(e)}") return Response({ 'code': 400, 'message': f'无效的JSON文件格式: {str(e)}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) 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) # 如果不是文件上传,则尝试从请求数据中提取JSON if not client_secret_json: serializer = GmailCredentialSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): try: # 从请求数据中提取客户端密钥 JSON client_secret_json = serializer.validated_data['client_secret_json'] except Exception as e: logger.error(f"未提供客户端密钥JSON: {str(e)}") return Response({ 'code': 400, 'message': '请提供client_secret_json字段或上传client_secret_file文件', 'data': None }, status=status.HTTP_400_BAD_REQUEST) else: # 记录无效请求数据并返回错误响应 logger.warning(f"Invalid request data: {serializer.errors}") return Response({ 'code': 400, 'message': '请求数据无效,请提供client_secret_json字段或上传client_secret_file文件', 'data': serializer.errors }, status=status.HTTP_400_BAD_REQUEST) # 如果此时仍然没有client_secret_json,返回错误 if not client_secret_json: return Response({ 'code': 400, 'message': '请提供client_secret_json字段或上传client_secret_file文件', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 调用 GmailService 生成授权 URL auth_url = GmailService.initiate_authentication(request.user, client_secret_json) logger.info(f"Generated auth URL for user {request.user.id}") return Response({ 'code': 200, 'message': '成功生成授权URL', 'data': {'auth_url': auth_url} }, status=status.HTTP_200_OK) except Exception as e: # 记录错误并返回服务器错误响应 logger.error(f"Error initiating authentication for user {request.user.id}: {str(e)}") return Response({ 'code': 500, 'message': f'认证初始化错误:{str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GmailAuthCompleteView(APIView): """ API 视图,用于完成 Gmail OAuth2 认证流程。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def post(self, request): """ 处理 POST 请求,使用授权代码完成 Gmail OAuth2 认证并保存凭证。 支持两种方式提供客户端密钥: 1. 在请求体中提供client_secret_json字段 2. 上传名为client_secret_file的JSON文件 Args: request: Django REST Framework 请求对象,包含授权代码和客户端密钥 JSON 或文件。 Returns: Response: 包含已保存凭证数据的 JSON 响应(成功时),或错误信息(失败时)。 Status Codes: 201: 成功保存凭证。 400: 请求数据无效。 500: 服务器内部错误(如认证失败)。 """ logger.debug(f"Received auth complete request: {request.data}") # 检查是否是文件上传方式 client_secret_json = None if 'client_secret_file' in request.FILES: try: # 读取上传的JSON文件内容 client_secret_file = request.FILES['client_secret_file'] client_secret_json = json.loads(client_secret_file.read().decode('utf-8')) logger.info(f"从上传文件读取到客户端密钥JSON") except json.JSONDecodeError as e: logger.error(f"解析客户端密钥JSON文件失败: {str(e)}") return Response({ 'code': 400, 'message': f'无效的JSON文件格式: {str(e)}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) 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) # 获取授权码,无论是哪种方式都需要 auth_code = request.data.get('auth_code') if not auth_code: return Response({ 'code': 400, 'message': '必须提供授权码', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 如果不是文件上传,则尝试从请求数据中提取JSON if not client_secret_json: serializer = GmailCredentialSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): try: # 从请求数据中提取客户端密钥 JSON client_secret_json = serializer.validated_data['client_secret_json'] except Exception as e: logger.error(f"未提供客户端密钥JSON: {str(e)}") return Response({ 'code': 400, 'message': '请提供client_secret_json字段或上传client_secret_file文件', 'data': None }, status=status.HTTP_400_BAD_REQUEST) else: # 记录无效请求数据并返回错误响应 logger.warning(f"Invalid request data: {serializer.errors}") return Response({ 'code': 400, 'message': '请求数据无效,请提供client_secret_json字段或上传client_secret_file文件', 'data': serializer.errors }, status=status.HTTP_400_BAD_REQUEST) # 如果此时仍然没有client_secret_json,返回错误 if not client_secret_json: return Response({ 'code': 400, 'message': '请提供client_secret_json字段或上传client_secret_file文件', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 完成认证并保存凭证 credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json) # 序列化凭证数据以返回 return_serializer = GmailCredentialSerializer(credential, context={'request': request}) logger.info(f"Authentication completed for user {request.user.id}, email: {credential.email}") return Response({ 'code': 201, 'message': '认证完成并成功保存凭证', 'data': return_serializer.data }, status=status.HTTP_201_CREATED) except Exception as e: # 记录错误并返回服务器错误响应 logger.error(f"Error completing authentication for user {request.user.id}: {str(e)}") return Response({ 'code': 500, 'message': f'完成认证时发生错误:{str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GmailCredentialViewSet(viewsets.ModelViewSet): """ Gmail凭证管理视图集,提供对Gmail账户凭证的完整CRUD操作 此视图集替代了旧的GmailCredentialListView和GmailCredentialDetailView, 提供了更完整的功能和更统一的API接口。 支持以下操作: - list: 获取凭证列表 - create: 创建新凭证(初始化OAuth或完成OAuth) - retrieve: 获取单个凭证详情 - update: 完全更新凭证 - partial_update: 部分更新凭证 - destroy: 删除凭证 - set_default: 设置默认凭证(自定义操作) """ permission_classes = [IsAuthenticated] serializer_class = GmailCredentialSerializer def get_queryset(self): """获取当前用户的Gmail凭证""" return GmailCredential.objects.filter(user=self.request.user) def perform_create(self, serializer): """创建凭证时自动关联当前用户""" serializer.save(user=self.request.user) def list(self, request, *args, **kwargs): """ 列出用户的Gmail账号凭证 返回当前用户所有Gmail账号凭证的列表。 """ queryset = self.get_queryset() serializer = self.get_serializer(queryset, many=True) return Response({ 'code': 200, 'message': '获取Gmail账号列表成功', 'data': serializer.data }) def retrieve(self, request, *args, **kwargs): """ 获取特定Gmail凭证的详细信息 根据ID返回单个Gmail账号凭证的详细信息。 """ instance = self.get_object() serializer = self.get_serializer(instance) return Response({ 'code': 200, 'message': '成功获取凭证详情', 'data': serializer.data }) def create(self, request, *args, **kwargs): """ 创建Gmail账号凭证 - 初始化或完成OAuth授权 此接口有两个功能: 1. 如果未提供auth_code,则初始化OAuth并返回授权URL 2. 如果提供了auth_code,则完成OAuth并保存凭证 请求参数: - client_secret_json: Google Cloud项目的客户端密钥 - auth_code: [可选] 授权码,用于完成OAuth流程 """ try: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) auth_code = serializer.validated_data.get('auth_code') client_secret_json = serializer.validated_data.get('client_secret_json') if not auth_code: # 初始化OAuth auth_url = GmailService.initiate_authentication(request.user, client_secret_json) return Response({ 'code': 200, 'message': '授权URL生成成功', 'data': {'auth_url': auth_url} }) else: # 完成OAuth credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json) return Response({ 'code': 201, 'message': '授权成功', 'data': { 'id': credential.id, 'email': credential.email, 'is_valid': credential.is_valid } }, status=status.HTTP_201_CREATED) except Exception as e: logger.error(f"创建Gmail凭证失败: {str(e)}") logger.error(traceback.format_exc()) return Response({ 'code': 500, 'message': f'创建Gmail凭证失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def update(self, request, *args, **kwargs): """ 完全更新Gmail凭证信息 更新Gmail凭证的所有可编辑字段。 如果设置is_default=True,会自动将其他凭证设为非默认。 """ try: instance = self.get_object() serializer = self.get_serializer(instance, data=request.data) serializer.is_valid(raise_exception=True) # 如果设置为默认凭证,清除其他凭证的默认状态 if serializer.validated_data.get('is_default', False): GmailCredential.objects.filter(user=request.user).exclude(id=instance.id).update(is_default=False) serializer.save() return Response({ 'code': 200, 'message': '成功更新凭证', 'data': serializer.data }) except Exception as e: logger.error(f"更新Gmail凭证失败: {str(e)}") return Response({ 'code': 500, 'message': f'更新Gmail凭证失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def partial_update(self, request, *args, **kwargs): """ 部分更新Gmail凭证信息 更新Gmail凭证的部分字段。 如果设置is_default=True,会自动将其他凭证设为非默认。 """ try: instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) # 如果设置为默认凭证,清除其他凭证的默认状态 if serializer.validated_data.get('is_default', False): GmailCredential.objects.filter(user=request.user).exclude(id=instance.id).update(is_default=False) serializer.save() return Response({ 'code': 200, 'message': '成功更新凭证', 'data': serializer.data }) except Exception as e: logger.error(f"更新Gmail凭证失败: {str(e)}") return Response({ 'code': 500, 'message': f'更新Gmail凭证失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def destroy(self, request, *args, **kwargs): """ 删除Gmail账号凭证 根据ID删除指定的Gmail凭证。 """ try: instance = self.get_object() self.perform_destroy(instance) return Response({ 'code': 200, 'message': '删除Gmail账号成功', 'data': None }) except Exception as e: logger.error(f"删除Gmail凭证失败: {str(e)}") return Response({ 'code': 500, 'message': f'删除Gmail凭证失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @action(detail=True, methods=['post']) def set_default(self, request, pk=None): """设置默认Gmail账号""" try: credential = self.get_object() # 取消其他默认账号 GmailCredential.objects.filter( user=request.user, is_default=True ).update(is_default=False) # 设置当前账号为默认 credential.is_default = True credential.save() return Response({ 'code': 200, 'message': f'已将 {credential.email} 设为默认Gmail账号', 'data': None }) except Exception as e: logger.error(f"设置默认Gmail账号失败: {str(e)}") return Response({ 'code': 500, 'message': f'设置默认Gmail账号失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @action(detail=False, methods=['post']) def upload_client_secret(self, request): """ 通过上传客户端密钥JSON文件来创建Gmail凭证 请求参数: - client_secret_file: Google Cloud项目的客户端密钥JSON文件 - auth_code: [可选] 授权码,用于完成OAuth流程 """ try: # 检查是否上传了文件 if 'client_secret_file' not in request.FILES: return Response({ 'code': 400, 'message': '未找到客户端密钥文件,请使用client_secret_file字段上传', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 读取上传的JSON文件内容 client_secret_file = request.FILES['client_secret_file'] client_secret_json = json.loads(client_secret_file.read().decode('utf-8')) # 获取可选的授权码 auth_code = request.data.get('auth_code', '') if not auth_code: # 初始化OAuth # 调用GmailService生成授权URL auth_url = GmailService.initiate_authentication(request.user, client_secret_json) return Response({ 'code': 200, 'message': '授权URL生成成功', 'data': {'auth_url': auth_url} }) else: # 完成OAuth # 完成认证并保存凭证 credential = GmailService.complete_authentication(request.user, auth_code, client_secret_json) return Response({ 'code': 201, 'message': '授权成功', 'data': { 'id': credential.id, 'email': credential.email, 'is_valid': credential.is_valid } }, status=status.HTTP_201_CREATED) except json.JSONDecodeError as e: logger.error(f"解析客户端密钥JSON文件失败: {str(e)}") return Response({ 'code': 400, 'message': f'无效的JSON文件格式: {str(e)}', 'data': None }, status=status.HTTP_400_BAD_REQUEST) 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 GmailConversationView(APIView): """ API视图,用于获取和保存Gmail对话。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def post(self, request): """ 处理POST请求,获取Gmail对话并保存到聊天历史。 请求参数: user_email: 用户Gmail邮箱 influencer_email: 达人Gmail邮箱 kb_id: [可选] 知识库ID,不提供则使用默认知识库 返回: conversation_id: 创建的会话ID """ try: # 验证必填参数 user_email = request.data.get('user_email') influencer_email = request.data.get('influencer_email') if not user_email or not influencer_email: return Response({ 'code': 400, 'message': '缺少必填参数: user_email 或 influencer_email', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 可选参数 kb_id = request.data.get('kb_id') # 调用服务保存对话 conversation_id, error = GmailService.save_conversations_to_chat( request.user, user_email, influencer_email, kb_id ) if error: return Response({ 'code': 400, 'message': error, 'data': None }, status=status.HTTP_400_BAD_REQUEST) return Response({ 'code': 200, 'message': '获取Gmail对话成功', 'data': { 'conversation_id': conversation_id } }) except Exception as e: logger.error(f"获取Gmail对话失败: {str(e)}") return Response({ 'code': 500, 'message': f'获取Gmail对话失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get(self, request): """ 处理GET请求,获取用户的Gmail对话列表。 """ try: conversations = GmailConversation.objects.filter(user=request.user, is_active=True) data = [] for conversation in conversations: # 获取附件计数 attachments_count = GmailAttachment.objects.filter( conversation=conversation ).count() data.append({ 'id': str(conversation.id), 'conversation_id': conversation.conversation_id, 'user_email': conversation.user_email, 'influencer_email': conversation.influencer_email, 'title': conversation.title, 'last_sync_time': conversation.last_sync_time.strftime('%Y-%m-%d %H:%M:%S'), 'created_at': conversation.created_at.strftime('%Y-%m-%d %H:%M:%S'), 'attachments_count': attachments_count }) return Response({ 'code': 200, 'message': '获取对话列表成功', 'data': data }) 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) class GmailAttachmentListView(APIView): """ API视图,用于获取Gmail附件列表。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def get(self, request, conversation_id=None): """ 处理GET请求,获取指定对话的附件列表。 """ try: if conversation_id: # 获取指定对话的附件 conversation = get_object_or_404(GmailConversation, conversation_id=conversation_id, user=request.user) attachments = GmailAttachment.objects.filter(conversation=conversation) else: # 获取用户的所有附件 conversations = GmailConversation.objects.filter(user=request.user, is_active=True) attachments = GmailAttachment.objects.filter(conversation__in=conversations) data = [] for attachment in attachments: data.append({ 'id': str(attachment.id), 'conversation_id': attachment.conversation.conversation_id, 'filename': attachment.filename, 'content_type': attachment.content_type, 'size': attachment.size, 'sender_email': attachment.sender_email, 'created_at': attachment.created_at.strftime('%Y-%m-%d %H:%M:%S'), 'url': attachment.get_absolute_url() }) return Response({ 'code': 200, 'message': '获取附件列表成功', 'data': data }) 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) class GmailPubSubView(APIView): """ API视图,用于设置Gmail的Pub/Sub实时通知。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def post(self, request): """ 处理POST请求,为用户的Gmail账户设置Pub/Sub推送通知。 Args: request: Django REST Framework请求对象,包含Gmail邮箱信息。 Returns: Response: 设置结果的JSON响应。 Status Codes: 200: 成功设置Pub/Sub通知。 400: 请求数据无效。 404: 未找到指定Gmail凭证。 500: 服务器内部错误。 """ try: # 获取请求参数 email = request.data.get('email') if not email: return Response({ 'code': 400, 'message': '必须提供Gmail邮箱地址', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查用户是否有此Gmail账户的凭证 credential = GmailCredential.objects.filter(user=request.user, email=email).first() if not credential: return Response({ 'code': 404, 'message': f'未找到{email}的授权信息', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 设置Pub/Sub通知 success, error = GmailService.setup_gmail_push_notification(request.user, email) if success: return Response({ 'code': 200, 'message': f'已成功为{email}设置实时通知', 'data': None }, status=status.HTTP_200_OK) else: return Response({ 'code': 500, 'message': error, 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"设置Gmail Pub/Sub通知失败: {str(e)}") return Response({ 'code': 500, 'message': f'设置Gmail实时通知失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get(self, request): """ 处理GET请求,获取用户所有已设置Pub/Sub通知的Gmail账户。 这个方法目前仅返回用户的所有Gmail凭证,未来可以扩展为返回推送通知的详细状态。 Args: request: Django REST Framework请求对象。 Returns: Response: 包含Gmail账户列表的JSON响应。 Status Codes: 200: 成功返回账户列表。 """ # 获取用户所有Gmail凭证 credentials = request.user.gmail_credentials.filter(is_valid=True) # 构建响应数据 accounts = [] for cred in credentials: accounts.append({ 'id': cred.id, 'email': cred.email, 'is_default': cred.is_default }) return Response({ 'code': 200, 'message': '获取账户列表成功', 'data': {'accounts': accounts} }, status=status.HTTP_200_OK) class GmailSendEmailView(APIView): """ API视图,用于发送Gmail邮件(支持附件)。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def post(self, request): """ 处理POST请求,发送Gmail邮件。 请求应包含以下字段: - email: 发件人Gmail邮箱 - to: 收件人邮箱 - subject: 邮件主题 - body: 邮件正文 - attachments: 附件文件IDs列表 (可选) Args: request: Django REST Framework请求对象。 Returns: Response: 发送结果的JSON响应。 Status Codes: 200: 成功发送邮件。 400: 请求数据无效。 404: 未找到Gmail凭证。 500: 服务器内部错误。 """ try: # 获取请求参数 user_email = request.data.get('email') to_email = request.data.get('to') subject = request.data.get('subject') body = request.data.get('body') attachment_ids = request.data.get('attachments', []) # 验证必填字段 if not all([user_email, to_email, subject]): return Response({ 'code': 400, 'message': '缺少必要参数,请提供email、to和subject字段', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查是否有此Gmail账户的凭证 credential = GmailCredential.objects.filter( user=request.user, email=user_email, is_valid=True ).first() if not credential: return Response({ 'code': 404, 'message': f'未找到{user_email}的有效授权信息', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 处理附件 attachments = [] if attachment_ids and isinstance(attachment_ids, list): for file_id in attachment_ids: # 查找已上传的文件 file_obj = request.FILES.get(f'file_{file_id}') if file_obj: # 保存临时文件 tmp_path = os.path.join(settings.MEDIA_ROOT, 'tmp', f'{file_id}_{file_obj.name}') os.makedirs(os.path.dirname(tmp_path), exist_ok=True) with open(tmp_path, 'wb+') as destination: for chunk in file_obj.chunks(): destination.write(chunk) attachments.append({ 'path': tmp_path, 'filename': file_obj.name }) else: # 检查是否为已有的Gmail附件ID try: attachment = GmailAttachment.objects.get(id=file_id) if attachment.conversation.user_id == request.user.id: attachments.append({ 'path': attachment.file_path, 'filename': attachment.filename }) except (GmailAttachment.DoesNotExist, ValueError): logger.warning(f"无法找到附件: {file_id}") # 发送邮件 success, result = GmailService.send_email( request.user, user_email, to_email, subject, body or '', attachments ) if success: return Response({ 'code': 200, 'message': '邮件发送成功', 'data': {'message_id': result} }, status=status.HTTP_200_OK) else: return Response({ 'code': 500, 'message': result, 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"发送Gmail邮件失败: {str(e)}") return Response({ 'code': 500, 'message': f'发送Gmail邮件失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get(self, request): """ 处理GET请求,获取用户可用于发送邮件的Gmail账户列表。 Args: request: Django REST Framework请求对象。 Returns: Response: 包含Gmail账户列表的JSON响应。 Status Codes: 200: 成功返回账户列表。 """ # 获取用户所有可用Gmail凭证 credentials = request.user.gmail_credentials.filter(is_valid=True) # 构建响应数据 accounts = [] for cred in credentials: accounts.append({ 'id': cred.id, 'email': cred.email, 'is_default': cred.is_default }) return Response({ 'code': 200, 'message': '获取账户列表成功', 'data': {'accounts': accounts} }, status=status.HTTP_200_OK) class GmailWebhookView(APIView): """ API视图,用于接收Gmail Pub/Sub推送通知。 这个端点不需要认证,因为它由Google的Pub/Sub服务调用。 """ permission_classes = [] # 不需要认证 def post(self, request): """ 处理POST请求,接收Gmail Pub/Sub推送通知, 将推送通知交给Celery任务队列异步处理。 Args: request: Django REST Framework请求对象,包含Pub/Sub消息。 Returns: Response: 处理结果的JSON响应。 """ try: print("\n" + "="*100) print("[Gmail Webhook] 收到推送通知") print("="*100) # 打印请求时间和基本信息 current_time = timezone.now() print(f"接收时间: {current_time.strftime('%Y-%m-%d %H:%M:%S.%f')}") # 解析推送消息 message = request.data.get('message', {}) data = message.get('data', '') message_id = message.get('messageId', '') subscription = request.data.get('subscription', '') print(f"消息ID: {message_id}") print(f"订阅名称: {subscription}") if not data: print("[Gmail Webhook] 错误: 无效的推送消息格式,缺少data字段") return Response({ 'code': 400, 'message': '无效的推送消息格式', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查该消息是否已处理过 from .models import ProcessedPushNotification if ProcessedPushNotification.objects.filter(message_id=message_id).exists(): print(f"[Gmail Webhook] 通知 {message_id} 已处理过,跳过") return Response({ 'code': 200, 'message': '通知已处理过,跳过', 'data': None }) # 将消息提交到Celery任务队列进行异步处理 from .tasks import process_push_notification process_push_notification.delay(data, message_id, subscription) print("[Gmail Webhook] 已将推送通知提交到任务队列处理") print("="*100 + "\n") # 返回成功响应 return Response({ 'code': 200, 'message': '推送通知已接收并提交到任务队列', 'data': None }) except Exception as e: print(f"[Gmail Webhook] 处理推送通知失败: {str(e)}") logger.error(f"处理Gmail推送通知失败: {str(e)}") return Response({ 'code': 500, 'message': f'处理推送通知失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GmailConversationSummaryView(APIView): """ API视图,用于获取Gmail对话的总结。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def get(self, request, conversation_id=None): """ 处理GET请求,获取指定Gmail对话的总结。 Args: request: Django REST Framework请求对象。 conversation_id: 对话ID。如果不提供,则返回所有对话的摘要列表。 Returns: Response: 包含对话总结的JSON响应。 Status Codes: 200: 成功获取对话总结。 400: 请求参数无效。 404: 未找到指定对话。 500: 服务器内部错误。 """ try: # 如果提供了conversation_id,获取单个对话总结 if conversation_id: # 获取对话总结 summary, error = GmailService.get_conversation_summary(request.user, conversation_id) if error: return Response({ 'code': 400, 'message': error, 'data': None }, status=status.HTTP_400_BAD_REQUEST) return Response({ 'code': 200, 'message': '成功获取对话总结', 'data': { 'conversation_id': conversation_id, 'summary': summary } }) # 否则,获取所有对话的摘要列表 conversations = GmailConversation.objects.filter(user=request.user, is_active=True) results = [] for conversation in conversations: # 检查是否已经有缓存的总结 has_summary = (conversation.metadata and 'summary' in conversation.metadata and 'summary_updated_at' in conversation.metadata) results.append({ 'id': str(conversation.id), 'conversation_id': conversation.conversation_id, 'user_email': conversation.user_email, 'influencer_email': conversation.influencer_email, 'title': conversation.title, 'has_summary': has_summary, 'last_sync_time': conversation.last_sync_time.strftime('%Y-%m-%d %H:%M:%S'), }) return Response({ 'code': 200, 'message': '成功获取对话总结列表', 'data': results }) except Exception as e: logger.error(f"获取Gmail对话总结失败: {str(e)}") return Response({ 'code': 500, 'message': f'获取Gmail对话总结失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GmailGoalView(APIView): """ 用户目标API - 支持用户为每个对话设置不同的目标 """ permission_classes = [IsAuthenticated] def get(self, request, conversation_id=None): """ 获取用户的目标 如果提供了conversation_id,则获取该对话的活跃目标 如果没有提供conversation_id,则获取用户的所有活跃目标 """ try: if conversation_id: # 获取特定对话的活跃目标 goal = AutoGmailConversationService.get_active_goals_for_conversation( request.user, conversation_id ) if not goal: return Response({ 'code': 404, 'message': '未找到该对话的活跃目标', 'data': None }, status=status.HTTP_404_NOT_FOUND) serializer = UserGoalSerializer(goal) return Response({ 'code': 200, 'message': '获取目标成功', 'data': serializer.data }) else: # 获取用户的所有活跃目标 goals = UserGoal.objects.filter(user=request.user, is_active=True) # 准备响应数据,包括对话信息 result = [] for goal in goals: goal_data = UserGoalSerializer(goal).data result.append(goal_data) return Response({ 'code': 200, 'message': '获取目标列表成功', 'data': result }) 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 post(self, request): """创建对话目标""" try: # 获取请求参数 conversation_id = request.data.get('conversation_id') goal_description = request.data.get('goal_description') # 验证必要参数 if not conversation_id or not goal_description: return Response({ 'code': 400, 'message': '缺少必要参数: conversation_id 或 goal_description', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 查找对话 conversation = GmailConversation.objects.filter(conversation_id=conversation_id).first() if not conversation: return Response({ 'code': 404, 'message': '对话不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 检查权限 if conversation.user.id != request.user.id: return Response({ 'code': 403, 'message': '无权限访问此对话', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 查找现有目标 existing_goal = UserGoal.objects.filter( user=request.user, conversation=conversation, is_active=True ).first() if existing_goal: # 如果存在活跃目标,直接更新它 existing_goal.description = goal_description existing_goal.status = 'pending' # 重置状态 existing_goal.last_activity_time = timezone.now() existing_goal.metadata = existing_goal.metadata or {} existing_goal.metadata.update({ 'updated_at': timezone.now().isoformat(), 'influencer_email': conversation.influencer_email, 'user_email': conversation.user_email }) existing_goal.save() logger.info(f"用户 {request.user.username} 更新了对话 {conversation_id} 的目标") serializer = UserGoalSerializer(existing_goal) return Response({ 'code': 200, 'message': '目标更新成功', 'data': serializer.data }) else: # 创建新目标 goal = UserGoal.objects.create( user=request.user, conversation=conversation, description=goal_description, is_active=True, status='pending', metadata={ 'created_at': timezone.now().isoformat(), 'influencer_email': conversation.influencer_email, 'user_email': conversation.user_email } ) logger.info(f"用户 {request.user.username} 为对话 {conversation_id} 创建了新目标") serializer = UserGoalSerializer(goal) return Response({ 'code': 201, 'message': '目标创建成功', 'data': serializer.data }, status=status.HTTP_201_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) def delete(self, request, conversation_id): """停用对话目标""" try: # 获取该对话的活跃目标 goal = AutoGmailConversationService.get_active_goals_for_conversation( request.user, conversation_id ) if not goal: return Response({ 'code': 404, 'message': '未找到该对话的活跃目标', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 停用目标 goal.is_active = False goal.save() return Response({ 'code': 200, 'message': '目标已停用', 'data': None }) 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 SimpleRecommendedReplyView(APIView): """ 通过conversation_id一键获取目标、对话摘要和推荐回复 """ permission_classes = [IsAuthenticated] def post(self, request): """ 直接通过conversation_id获取推荐回复 请求参数: - conversation_id: 对话ID (必填) 响应: - 目标信息 - 对话摘要 - 达人最后发送的消息 - 推荐回复内容 """ try: # 获取请求参数 conversation_id = request.data.get('conversation_id') # 验证必填参数 if not conversation_id: return Response({ 'code': 400, 'message': '缺少必要参数: conversation_id', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证对话存在并且属于当前用户 # 直接通过conversation_id查找,不进行UUID验证 # Gmail对话ID可能是形如 gmail_ae51a9e8-ff35-4507-a63b-7d8091327e1e_44a2aaac 的格式 try: conversation = GmailConversation.objects.filter(conversation_id=conversation_id).first() if not conversation: return Response({ 'code': 404, 'message': '对话不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) if conversation.user != request.user: return Response({ 'code': 403, 'message': '无权限访问该对话', 'data': None }, status=status.HTTP_403_FORBIDDEN) 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) # 1. 获取对话的活跃目标 goal = UserGoal.objects.filter( user=request.user, conversation=conversation, is_active=True ).first() if not goal: return Response({ 'code': 404, 'message': '未找到该对话的活跃目标', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 2. 获取对话摘要 conversation_summary = get_conversation_summary(conversation_id) if not conversation_summary: conversation_summary = "无对话摘要" # 3. 获取最后一条达人消息 last_message = get_last_message(conversation_id) if not last_message: return Response({ 'code': 400, 'message': '对话中没有达人消息,无法生成回复', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 生成推荐回复 reply_content, error = generate_recommended_reply( request.user, goal.description, conversation_summary, last_message ) if error: return Response({ 'code': 500, 'message': f'生成推荐回复失败: {error}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 返回完整数据 return Response({ 'code': 200, 'message': '获取成功', 'data': { 'goal': { 'id': str(goal.id), 'description': goal.description, 'status': goal.status }, 'conversation_summary': conversation_summary, 'last_message': last_message, 'recommended_reply': reply_content } }) 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 GmailExportView(APIView): """ API视图,用于导出已回复的达人Gmail列表为Excel文件。 """ permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 def get(self, request, format=None): """ 处理GET请求,导出已回复的达人Gmail列表。 Query参数: - format: 导出格式,支持 'xlsx' 或 'csv',默认为 'xlsx' Returns: HttpResponse: 包含Excel文件的响应(成功时),或错误消息的JSON响应(失败时)。 """ try: # 获取导出格式参数 export_format = request.query_params.get('format', 'xlsx') if export_format not in ['xlsx', 'csv']: return Response({ 'code': 400, 'message': f'不支持的导出格式: {export_format},支持的格式有: xlsx, csv', 'data': None }, status=status.HTTP_400_BAD_REQUEST) logger.info(f"用户 {request.user.username} 请求导出已回复达人列表,格式: {export_format}") # 直接从具体文件导入而不依赖__init__.py from .services.export_service import GmailExportService file_path, error = GmailExportService.export_replied_influencers(request.user, format=export_format) if error: logger.error(f"导出失败: {error}") return Response({ 'code': 500, 'message': error, 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if not file_path or not os.path.exists(file_path): logger.error(f"导出文件不存在: {file_path}") return Response({ 'code': 500, 'message': '导出文件生成失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 读取生成的文件 with open(file_path, 'rb') as f: file_data = f.read() # 设置响应头,启用文件下载 response = HttpResponse( file_data, content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' if export_format == 'xlsx' else 'text/csv' ) filename = os.path.basename(file_path) response['Content-Disposition'] = f'attachment; filename="{filename}"' logger.info(f"成功导出文件: {filename}") return response except Exception as e: logger.error(f"导出过程中发生错误: {str(e)}") error_details = traceback.format_exc() return Response({ 'code': 500, 'message': f'导出失败: {str(e)}', 'data': { 'details': error_details[:500] } }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)