daren/apps/gmail/views.py
2025-05-29 10:11:19 +08:00

1475 lines
58 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.user.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
from apps.user.authentication import CustomTokenAuthentication
# 配置日志记录器,用于记录视图操作的调试、警告和错误信息
logger = logging.getLogger(__name__)
class GmailAuthInitiateView(APIView):
"""
API 视图,用于启动 Gmail OAuth2 认证流程。
"""
permission_classes = [IsAuthenticated]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication]
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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)
# 查找现有目标
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.name} 更新了对话 {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.name} 为对话 {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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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]
authentication_classes = [CustomTokenAuthentication] # 限制访问,仅允许已认证用户
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.name} 请求导出已回复达人列表,格式: {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)