增加gmail模块
This commit is contained in:
parent
cee0b638e8
commit
aeebe645bf
33
apps/accounts/migrations/0005_usergoal.py
Normal file
33
apps/accounts/migrations/0005_usergoal.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 09:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0004_delete_usergoal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserGoal',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('description', models.TextField(verbose_name='目标描述')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '用户目标',
|
||||||
|
'verbose_name_plural': '用户目标',
|
||||||
|
'db_table': 'user_goals',
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -98,3 +98,21 @@ class UserProfile(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username}的个人资料"
|
return f"{self.user.username}的个人资料"
|
||||||
|
|
||||||
|
class UserGoal(models.Model):
|
||||||
|
"""用户目标模型 - 存储用户设定的沟通或销售目标"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='goals')
|
||||||
|
description = models.TextField(verbose_name='目标描述')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name='是否激活')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'user_goals'
|
||||||
|
verbose_name = '用户目标'
|
||||||
|
verbose_name_plural = '用户目标'
|
||||||
|
ordering = ['-updated_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}的目标"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from apps.accounts.models import User, UserProfile
|
from apps.accounts.models import User, UserProfile, UserGoal
|
||||||
|
|
||||||
class UserProfileSerializer(serializers.ModelSerializer):
|
class UserProfileSerializer(serializers.ModelSerializer):
|
||||||
"""用户档案序列化器"""
|
"""用户档案序列化器"""
|
||||||
@ -60,4 +60,19 @@ class PasswordChangeSerializer(serializers.Serializer):
|
|||||||
user = self.context['request'].user
|
user = self.context['request'].user
|
||||||
if not user.check_password(value):
|
if not user.check_password(value):
|
||||||
raise serializers.ValidationError("旧密码不正确")
|
raise serializers.ValidationError("旧密码不正确")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class UserGoalSerializer(serializers.ModelSerializer):
|
||||||
|
"""用户目标序列化器"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserGoal
|
||||||
|
fields = ['id', 'description', 'created_at', 'updated_at', 'is_active']
|
||||||
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""创建新目标时自动关联当前用户"""
|
||||||
|
user = self.context['request'].user
|
||||||
|
goal = UserGoal.objects.create(user=user, **validated_data)
|
||||||
|
return goal
|
102
apps/accounts/services/goal_service.py
Normal file
102
apps/accounts/services/goal_service.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
from datetime import datetime
|
||||||
|
from apps.accounts.models import UserGoal
|
||||||
|
from apps.gmail.models import GmailConversation, ConversationSummary
|
||||||
|
from apps.chat.models import ChatHistory
|
||||||
|
from apps.common.services.ai_service import AIService
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_active_goal(user):
|
||||||
|
"""
|
||||||
|
获取用户最新的活跃目标
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: 用户对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserGoal: 用户目标对象或None
|
||||||
|
"""
|
||||||
|
return UserGoal.objects.filter(user=user, is_active=True).order_by('-updated_at').first()
|
||||||
|
|
||||||
|
def get_conversation_summary(conversation_id):
|
||||||
|
"""
|
||||||
|
获取对话摘要
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_id: 对话ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 摘要内容或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 先检查持久化存储的摘要
|
||||||
|
try:
|
||||||
|
conversation = GmailConversation.objects.get(conversation_id=conversation_id)
|
||||||
|
summary = ConversationSummary.objects.get(conversation=conversation)
|
||||||
|
return summary.content
|
||||||
|
except (GmailConversation.DoesNotExist, ConversationSummary.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果没有持久化的摘要,尝试生成简单摘要
|
||||||
|
chat_history = ChatHistory.objects.filter(conversation_id=conversation_id).order_by('-created_at')[:5]
|
||||||
|
if not chat_history:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 生成简单摘要(最近几条消息)
|
||||||
|
messages = []
|
||||||
|
for msg in chat_history:
|
||||||
|
if len(messages) < 3: # 只取最新的3条
|
||||||
|
role = "用户" if msg.role == "user" else "达人"
|
||||||
|
content = msg.content
|
||||||
|
if len(content) > 100:
|
||||||
|
content = content[:100] + "..."
|
||||||
|
messages.append(f"{role}: {content}")
|
||||||
|
|
||||||
|
if messages:
|
||||||
|
return "最近对话: " + " | ".join(reversed(messages))
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取对话摘要失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_last_message(conversation_id):
|
||||||
|
"""
|
||||||
|
获取对话中最后一条对方发送的消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_id: 对话ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 最后一条消息内容或None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取对话中最后一条对方(达人)发送的消息
|
||||||
|
last_message = ChatHistory.objects.filter(
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
role='assistant' # 达人的消息
|
||||||
|
).order_by('-created_at').first()
|
||||||
|
|
||||||
|
if last_message:
|
||||||
|
return last_message.content
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取最后一条消息失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def generate_recommended_reply(user, goal_description, conversation_summary, last_message):
|
||||||
|
"""
|
||||||
|
根据用户目标、对话摘要和最后一条消息生成推荐话术
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: 用户对象
|
||||||
|
goal_description: 用户目标描述
|
||||||
|
conversation_summary: 对话摘要
|
||||||
|
last_message: 达人最后发送的消息内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (推荐话术内容, 错误信息)
|
||||||
|
"""
|
||||||
|
# 直接调用AIService生成回复
|
||||||
|
return AIService.generate_email_reply(goal_description, conversation_summary, last_message)
|
@ -4,6 +4,7 @@ from apps.accounts.views import (
|
|||||||
LoginView, RegisterView, LogoutView, user_profile, change_password,
|
LoginView, RegisterView, LogoutView, user_profile, change_password,
|
||||||
user_detail, user_update, user_delete, verify_token, user_list
|
user_detail, user_update, user_delete, verify_token, user_list
|
||||||
)
|
)
|
||||||
|
from .views import UserGoalView, UserGoalDetailView, RecommendedReplyView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('login/', LoginView.as_view(), name='login'),
|
path('login/', LoginView.as_view(), name='login'),
|
||||||
@ -16,4 +17,7 @@ urlpatterns = [
|
|||||||
path('users/<str:pk>/', user_detail, name='user_detail'),
|
path('users/<str:pk>/', user_detail, name='user_detail'),
|
||||||
path('users/<str:pk>/update/', user_update, name='user_update'),
|
path('users/<str:pk>/update/', user_update, name='user_update'),
|
||||||
path('users/<str:pk>/delete/', user_delete, name='user_delete'),
|
path('users/<str:pk>/delete/', user_delete, name='user_delete'),
|
||||||
|
path('goals/', UserGoalView.as_view(), name='user_goals'),
|
||||||
|
path('goals/<str:goal_id>/', UserGoalDetailView.as_view(), name='user_goal_detail'),
|
||||||
|
path('recommended-reply/', RecommendedReplyView.as_view(), name='recommended_reply'),
|
||||||
]
|
]
|
||||||
|
@ -15,13 +15,18 @@ from django.shortcuts import get_object_or_404
|
|||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from apps.accounts.models import User
|
from apps.accounts.models import User, UserGoal
|
||||||
from apps.accounts.services.auth_service import (
|
from apps.accounts.services.auth_service import (
|
||||||
authenticate_user, create_user, generate_token, delete_token
|
authenticate_user, create_user, generate_token, delete_token
|
||||||
)
|
)
|
||||||
from apps.accounts.services.utils import (
|
from apps.accounts.services.utils import (
|
||||||
convert_to_uuid, format_user_response, validate_uuid_param
|
convert_to_uuid, format_user_response, validate_uuid_param
|
||||||
)
|
)
|
||||||
|
from apps.accounts.services.goal_service import (
|
||||||
|
generate_recommended_reply, get_active_goal, get_conversation_summary,
|
||||||
|
get_last_message
|
||||||
|
)
|
||||||
|
from .serializers import UserGoalSerializer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -585,4 +590,272 @@ def user_list(request):
|
|||||||
'message': f'获取用户列表失败: {str(e)}',
|
'message': f'获取用户列表失败: {str(e)}',
|
||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
class UserGoalView(APIView):
|
||||||
|
"""
|
||||||
|
用户目标管理API
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""获取当前用户的所有目标"""
|
||||||
|
try:
|
||||||
|
goals = UserGoal.objects.filter(user=request.user, is_active=True)
|
||||||
|
serializer = UserGoalSerializer(goals, many=True)
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取目标列表成功',
|
||||||
|
'data': serializer.data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取用户目标失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取用户目标失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""创建新的用户目标"""
|
||||||
|
try:
|
||||||
|
serializer = UserGoalSerializer(data=request.data, context={'request': request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({
|
||||||
|
'code': 201,
|
||||||
|
'message': '目标创建成功',
|
||||||
|
'data': serializer.data
|
||||||
|
}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '创建目标失败',
|
||||||
|
'data': serializer.errors
|
||||||
|
}, 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 UserGoalDetailView(APIView):
|
||||||
|
"""
|
||||||
|
用户目标详情API
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_object(self, goal_id, user):
|
||||||
|
"""获取指定的用户目标"""
|
||||||
|
try:
|
||||||
|
# 验证UUID格式
|
||||||
|
uuid_obj, error_response = validate_uuid_param(goal_id)
|
||||||
|
if error_response:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return UserGoal.objects.get(id=uuid_obj, user=user)
|
||||||
|
except UserGoal.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get(self, request, goal_id):
|
||||||
|
"""获取单个目标详情"""
|
||||||
|
try:
|
||||||
|
goal = self.get_object(goal_id, request.user)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
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 put(self, request, goal_id):
|
||||||
|
"""更新目标信息"""
|
||||||
|
try:
|
||||||
|
goal = self.get_object(goal_id, request.user)
|
||||||
|
if not goal:
|
||||||
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': '目标不存在',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
serializer = UserGoalSerializer(goal, data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '目标更新成功',
|
||||||
|
'data': serializer.data
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '更新目标失败',
|
||||||
|
'data': serializer.errors
|
||||||
|
}, 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)
|
||||||
|
|
||||||
|
def delete(self, request, goal_id):
|
||||||
|
"""删除目标"""
|
||||||
|
try:
|
||||||
|
goal = self.get_object(goal_id, request.user)
|
||||||
|
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 RecommendedReplyView(APIView):
|
||||||
|
"""
|
||||||
|
基于用户目标生成推荐回复话术的API
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
生成推荐回复话术
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
- goal_id: 目标ID (可选,如不提供则使用用户当前活跃目标)
|
||||||
|
- conversation_id: 对话ID (可选,用于获取对话摘要和最后一条消息)
|
||||||
|
- conversation_summary: 对话摘要 (可选,如提供了conversation_id则优先使用对话ID获取摘要)
|
||||||
|
- last_message: 达人最后发送的消息内容 (可选,如提供了conversation_id则可以自动获取)
|
||||||
|
|
||||||
|
响应:
|
||||||
|
- 推荐的回复话术
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取请求参数
|
||||||
|
goal_id = request.data.get('goal_id')
|
||||||
|
conversation_id = request.data.get('conversation_id')
|
||||||
|
conversation_summary = request.data.get('conversation_summary', '')
|
||||||
|
last_message = request.data.get('last_message')
|
||||||
|
|
||||||
|
# 如果提供了对话ID,尝试获取对话摘要和最后一条消息
|
||||||
|
if conversation_id:
|
||||||
|
# 获取对话摘要
|
||||||
|
stored_summary = get_conversation_summary(conversation_id)
|
||||||
|
if stored_summary:
|
||||||
|
conversation_summary = stored_summary
|
||||||
|
|
||||||
|
# 如果没有提供last_message,尝试从对话中获取
|
||||||
|
if not last_message:
|
||||||
|
last_message = get_last_message(conversation_id)
|
||||||
|
|
||||||
|
# 验证必填参数
|
||||||
|
if not last_message:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '缺少必要参数: last_message,且无法从对话ID自动获取最后一条消息',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 获取用户目标
|
||||||
|
if goal_id:
|
||||||
|
# 如果提供了目标ID,则获取该目标
|
||||||
|
uuid_obj, error_response = validate_uuid_param(goal_id)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
try:
|
||||||
|
goal = UserGoal.objects.get(id=uuid_obj, user=request.user, is_active=True)
|
||||||
|
goal_description = goal.description
|
||||||
|
except UserGoal.DoesNotExist:
|
||||||
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': '目标不存在',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
else:
|
||||||
|
# 否则使用用户最近的活跃目标
|
||||||
|
goal = get_active_goal(request.user)
|
||||||
|
|
||||||
|
if not goal:
|
||||||
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': '未找到活跃目标,请先设置目标',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
goal_description = goal.description
|
||||||
|
|
||||||
|
# 生成推荐回复
|
||||||
|
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),
|
||||||
|
'goal_description': goal_description,
|
||||||
|
'recommended_reply': reply_content,
|
||||||
|
'conversation_id': conversation_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
202
apps/common/services/ai_service.py
Normal file
202
apps/common/services/ai_service.py
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from django.conf import settings
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class AIService:
|
||||||
|
"""
|
||||||
|
通用AI服务类,提供对不同AI模型的统一调用接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def call_silicon_cloud_api(messages, model="deepseek-ai/DeepSeek-V3", max_tokens=512, temperature=0.7):
|
||||||
|
"""
|
||||||
|
调用SiliconCloud API
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: 消息列表,格式为[{"role": "user", "content": "..."}, ...]
|
||||||
|
model: 使用的模型名称,默认为DeepSeek-V3
|
||||||
|
max_tokens: 最大生成token数
|
||||||
|
temperature: 温度参数,控制创造性
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (生成内容, 错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取API密钥
|
||||||
|
api_key = getattr(settings, 'SILICON_CLOUD_API_KEY', '')
|
||||||
|
if not api_key:
|
||||||
|
return None, "未配置Silicon Cloud API密钥"
|
||||||
|
|
||||||
|
url = "https://api.siliconflow.cn/v1/chat/completions"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"stream": False,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
"top_p": 0.7,
|
||||||
|
"top_k": 50,
|
||||||
|
"frequency_penalty": 0.5,
|
||||||
|
"n": 1,
|
||||||
|
"stop": [],
|
||||||
|
"messages": messages
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设置代理(如果配置了)
|
||||||
|
proxies = None
|
||||||
|
proxy_url = getattr(settings, 'PROXY_URL', None)
|
||||||
|
if proxy_url:
|
||||||
|
proxies = {
|
||||||
|
"http": proxy_url,
|
||||||
|
"https": proxy_url
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
logger.info(f"正在调用SiliconCloud API,模型: {model}")
|
||||||
|
response = requests.post(url, json=payload, headers=headers, proxies=proxies)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"API请求失败,状态码: {response.status_code},响应: {response.text}")
|
||||||
|
return None, f"API请求失败,状态码: {response.status_code}"
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
# 从API响应中提取内容
|
||||||
|
if 'choices' in result and result['choices']:
|
||||||
|
content = result['choices'][0]['message']['content'].strip()
|
||||||
|
return content, None
|
||||||
|
else:
|
||||||
|
logger.error(f"API响应格式错误: {result}")
|
||||||
|
return None, "API响应格式错误,无法提取内容"
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"API请求异常: {str(e)}")
|
||||||
|
return None, f"API请求异常: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"调用AI服务失败: {str(e)}")
|
||||||
|
return None, f"调用AI服务失败: {str(e)}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_email_reply(goal_description, conversation_summary, last_message):
|
||||||
|
"""
|
||||||
|
生成邮件回复话术
|
||||||
|
|
||||||
|
Args:
|
||||||
|
goal_description: 用户目标描述
|
||||||
|
conversation_summary: 对话摘要
|
||||||
|
last_message: 达人最后发送的消息内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (推荐话术内容, 错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证必要参数
|
||||||
|
if not goal_description or not last_message:
|
||||||
|
return None, "缺少必要参数:目标描述或最后消息"
|
||||||
|
|
||||||
|
# 准备提示信息
|
||||||
|
prompt = f"""
|
||||||
|
请为用户提供一条回复达人邮件的推荐话术,帮助用户实现销售目标。
|
||||||
|
|
||||||
|
用户目标:{goal_description}
|
||||||
|
|
||||||
|
对话摘要:{conversation_summary or '无对话摘要'}
|
||||||
|
|
||||||
|
达人最后发送的消息:
|
||||||
|
{last_message}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 回复应当专业礼貌,并围绕用户目标展开
|
||||||
|
2. 提供有说服力的话术,推动促成合作
|
||||||
|
3. 不超过200字
|
||||||
|
4. 直接给出回复内容,不要包含任何额外解释
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "你是一名专业的邮件回复助手,你的任务是生成有效的销售话术,帮助用户实现销售目标。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": prompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 调用通用API方法
|
||||||
|
return AIService.call_silicon_cloud_api(
|
||||||
|
messages,
|
||||||
|
model="Pro/deepseek-ai/DeepSeek-R1",
|
||||||
|
max_tokens=512,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成回复话术失败: {str(e)}")
|
||||||
|
return None, f"生成回复话术失败: {str(e)}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_conversation_summary(conversation_history):
|
||||||
|
"""
|
||||||
|
生成对话摘要
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_history: 对话历史记录列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (摘要内容, 错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not conversation_history:
|
||||||
|
return None, "无对话历史,无法生成摘要"
|
||||||
|
|
||||||
|
# 构造对话历史文本
|
||||||
|
conversation_text = "\n\n".join([
|
||||||
|
f"**{'用户' if msg.get('is_from_user', False) else '达人'}**: {msg.get('content', '')}"
|
||||||
|
for msg in conversation_history
|
||||||
|
])
|
||||||
|
|
||||||
|
# 准备提示信息
|
||||||
|
prompt = f"""
|
||||||
|
请对以下用户与达人之间的对话内容进行总结:
|
||||||
|
|
||||||
|
{conversation_text}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 总结应包含双方主要讨论的话题和关键点
|
||||||
|
2. 特别关注产品详情、价格谈判、合作意向等商务要点
|
||||||
|
3. 简明扼要,不超过200字
|
||||||
|
4. 直接给出总结内容,不要包含任何额外解释
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "你是一名专业的对话总结助手,擅长提取商务沟通中的关键信息。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": prompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 调用通用API方法
|
||||||
|
return AIService.call_silicon_cloud_api(
|
||||||
|
messages,
|
||||||
|
model="Pro/deepseek-ai/DeepSeek-R1",
|
||||||
|
max_tokens=512,
|
||||||
|
temperature=0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成对话摘要失败: {str(e)}")
|
||||||
|
return None, f"生成对话摘要失败: {str(e)}"
|
@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-12 06:56
|
# Generated by Django 5.2 on 2025-05-13 06:43
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@ -14,6 +15,44 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailConversation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
||||||
|
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
||||||
|
('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
|
||||||
|
('title', models.CharField(default='Gmail对话', help_text='对话标题', max_length=100)),
|
||||||
|
('last_sync_time', models.DateTimeField(default=django.utils.timezone.now, help_text='最后同步时间')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-updated_at'],
|
||||||
|
'unique_together': {('user', 'user_email', 'influencer_email')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailAttachment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
|
||||||
|
('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
|
||||||
|
('filename', models.CharField(help_text='原始文件名', max_length=255)),
|
||||||
|
('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)),
|
||||||
|
('content_type', models.CharField(help_text='MIME类型', max_length=100)),
|
||||||
|
('size', models.IntegerField(default=0, help_text='文件大小(字节)')),
|
||||||
|
('sender_email', models.EmailField(help_text='发送者邮箱', max_length=254)),
|
||||||
|
('chat_message_id', models.CharField(blank=True, help_text='关联到ChatHistory的消息ID', max_length=100, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='gmail.gmailconversation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='GmailCredential',
|
name='GmailCredential',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-12 08:22
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('gmail', '0001_initial'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GmailConversation',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('user_email', models.EmailField(help_text='用户Gmail邮箱', max_length=254)),
|
|
||||||
('influencer_email', models.EmailField(help_text='达人Gmail邮箱', max_length=254)),
|
|
||||||
('conversation_id', models.CharField(help_text='关联到chat_history的会话ID', max_length=100, unique=True)),
|
|
||||||
('title', models.CharField(default='Gmail对话', help_text='对话标题', max_length=100)),
|
|
||||||
('last_sync_time', models.DateTimeField(default=django.utils.timezone.now, help_text='最后同步时间')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('is_active', models.BooleanField(default=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_conversations', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-updated_at'],
|
|
||||||
'unique_together': {('user', 'user_email', 'influencer_email')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GmailAttachment',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('email_message_id', models.CharField(help_text='Gmail邮件ID', max_length=100)),
|
|
||||||
('attachment_id', models.CharField(help_text='Gmail附件ID', max_length=100)),
|
|
||||||
('filename', models.CharField(help_text='原始文件名', max_length=255)),
|
|
||||||
('file_path', models.CharField(help_text='保存在服务器上的路径', max_length=255)),
|
|
||||||
('content_type', models.CharField(help_text='MIME类型', max_length=100)),
|
|
||||||
('size', models.IntegerField(default=0, help_text='文件大小(字节)')),
|
|
||||||
('sender_email', models.EmailField(help_text='发送者邮箱', max_length=254)),
|
|
||||||
('chat_message_id', models.CharField(blank=True, help_text='关联到ChatHistory的消息ID', max_length=100, null=True)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='gmail.gmailconversation')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 08:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='last_history_id',
|
||||||
|
field=models.CharField(blank=True, help_text='Last processed Gmail history ID', max_length=50, null=True),
|
||||||
|
),
|
||||||
|
]
|
18
apps/gmail/migrations/0003_gmailconversation_metadata.py
Normal file
18
apps/gmail/migrations/0003_gmailconversation_metadata.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 09:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0002_gmailcredential_last_history_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gmailconversation',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, default=dict, help_text='存储额外信息,如已处理的消息ID等', null=True),
|
||||||
|
),
|
||||||
|
]
|
31
apps/gmail/migrations/0004_conversationsummary.py
Normal file
31
apps/gmail/migrations/0004_conversationsummary.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-13 10:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gmail', '0003_gmailconversation_metadata'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ConversationSummary',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('content', models.TextField(verbose_name='摘要内容')),
|
||||||
|
('last_message_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='最后处理的消息ID或ChatHistory的ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('conversation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='summary', to='gmail.gmailconversation')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Gmail对话摘要',
|
||||||
|
'verbose_name_plural': 'Gmail对话摘要',
|
||||||
|
'db_table': 'gmail_conversation_summaries',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -3,6 +3,7 @@ from apps.accounts.models import User
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
import uuid
|
||||||
|
|
||||||
class GmailCredential(models.Model):
|
class GmailCredential(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_credentials')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_credentials')
|
||||||
@ -12,6 +13,7 @@ class GmailCredential(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
is_valid = models.BooleanField(default=True, help_text="Whether the credential is valid")
|
is_valid = models.BooleanField(default=True, help_text="Whether the credential is valid")
|
||||||
|
last_history_id = models.CharField(max_length=50, blank=True, null=True, help_text="Last processed Gmail history ID")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('user', 'email')
|
unique_together = ('user', 'email')
|
||||||
@ -53,6 +55,7 @@ class GmailConversation(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True, null=True, help_text="存储额外信息,如已处理的消息ID等")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username}: {self.user_email} - {self.influencer_email}"
|
return f"{self.user.username}: {self.user_email} - {self.influencer_email}"
|
||||||
@ -87,4 +90,21 @@ class GmailAttachment(models.Model):
|
|||||||
return f"/media/gmail_attachments/{os.path.basename(self.file_path)}"
|
return f"/media/gmail_attachments/{os.path.basename(self.file_path)}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
class ConversationSummary(models.Model):
|
||||||
|
"""Gmail对话摘要模型,用于持久化存储对话摘要"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
conversation = models.OneToOneField('GmailConversation', on_delete=models.CASCADE, related_name='summary')
|
||||||
|
content = models.TextField(verbose_name='摘要内容')
|
||||||
|
last_message_id = models.CharField(max_length=255, verbose_name='最后处理的消息ID或ChatHistory的ID', null=True, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'gmail_conversation_summaries'
|
||||||
|
verbose_name = 'Gmail对话摘要'
|
||||||
|
verbose_name_plural = 'Gmail对话摘要'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"对话 {self.conversation.id} 摘要"
|
@ -13,19 +13,20 @@ from googleapiclient.errors import HttpError
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from ..models import GmailCredential, GmailConversation, GmailAttachment
|
from ..models import GmailCredential, GmailConversation, GmailAttachment, ConversationSummary
|
||||||
from apps.chat.models import ChatHistory
|
from apps.chat.models import ChatHistory
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
import requests
|
import requests
|
||||||
from google.cloud import pubsub_v1
|
|
||||||
from apps.common.services.notification_service import NotificationService
|
from apps.common.services.notification_service import NotificationService
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
from apps.accounts.models import User
|
||||||
|
from apps.common.services.ai_service import AIService
|
||||||
|
import traceback
|
||||||
|
|
||||||
# 配置日志记录器
|
# 配置日志记录器
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -45,9 +46,9 @@ class GmailService:
|
|||||||
TOKEN_DIR = os.path.join(settings.BASE_DIR, 'gmail_tokens')
|
TOKEN_DIR = os.path.join(settings.BASE_DIR, 'gmail_tokens')
|
||||||
# 附件存储目录
|
# 附件存储目录
|
||||||
ATTACHMENT_DIR = os.path.join(settings.BASE_DIR, 'media', 'gmail_attachments')
|
ATTACHMENT_DIR = os.path.join(settings.BASE_DIR, 'media', 'gmail_attachments')
|
||||||
# Gmail 监听 Pub/Sub 主题和订阅
|
# Gmail 监听 Pub/Sub 主题和订阅(仅后缀)
|
||||||
PUBSUB_TOPIC = getattr(settings, 'GMAIL_PUBSUB_TOPIC', 'projects/{project_id}/topics/gmail-notifications')
|
PUBSUB_TOPIC = getattr(settings, 'GMAIL_PUBSUB_TOPIC', 'gmail-notifications')
|
||||||
PUBSUB_SUBSCRIPTION = getattr(settings, 'GMAIL_PUBSUB_SUBSCRIPTION', 'projects/{project_id}/subscriptions/gmail-notifications-sub')
|
PUBSUB_SUBSCRIPTION = getattr(settings, 'GMAIL_PUBSUB_SUBSCRIPTION', 'gmail-notifications-sub')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def initiate_authentication(user, client_secret_json):
|
def initiate_authentication(user, client_secret_json):
|
||||||
@ -177,15 +178,32 @@ class GmailService:
|
|||||||
Exception: 如果凭证无效或创建服务失败。
|
Exception: 如果凭证无效或创建服务失败。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"获取Gmail服务实例,用户: {credential.user.username}, 邮箱: {credential.email}")
|
||||||
|
|
||||||
# 从数据库凭证中获取 Google API 凭证对象
|
# 从数据库凭证中获取 Google API 凭证对象
|
||||||
credentials = credential.get_credentials()
|
credentials = credential.get_credentials()
|
||||||
|
|
||||||
|
# 检查凭证是否需要刷新
|
||||||
|
if credentials.expired:
|
||||||
|
logger.info(f"OAuth凭证已过期,尝试刷新...")
|
||||||
|
|
||||||
# 创建 Gmail API 服务,代理通过环境变量自动应用
|
# 创建 Gmail API 服务,代理通过环境变量自动应用
|
||||||
return build('gmail', 'v1', credentials=credentials)
|
service = build('gmail', 'v1', credentials=credentials)
|
||||||
|
logger.info(f"成功创建Gmail服务实例")
|
||||||
|
|
||||||
|
return service
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 记录错误并抛出
|
logger.error(f"创建Gmail服务失败: {str(e)}")
|
||||||
logger.error(f"Error creating Gmail service: {str(e)}")
|
|
||||||
|
# 如果是凭证刷新失败,标记凭证为无效
|
||||||
|
if 'invalid_grant' in str(e).lower() or '401' in str(e) or 'token has been expired or revoked' in str(e).lower():
|
||||||
|
logger.error(f"OAuth凭证刷新失败,标记凭证为无效")
|
||||||
|
credential.is_valid = False
|
||||||
|
credential.save()
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -581,30 +599,50 @@ class GmailService:
|
|||||||
# 获取Gmail服务
|
# 获取Gmail服务
|
||||||
service = GmailService.get_service(credential)
|
service = GmailService.get_service(credential)
|
||||||
|
|
||||||
|
# 获取最新的历史ID
|
||||||
|
try:
|
||||||
|
profile = service.users().getProfile(userId='me').execute()
|
||||||
|
latest_history_id = profile.get('historyId')
|
||||||
|
if latest_history_id:
|
||||||
|
# 保存最新历史ID到凭证
|
||||||
|
credential.last_history_id = latest_history_id
|
||||||
|
credential.save()
|
||||||
|
logger.info(f"已更新{user_email}的历史ID: {latest_history_id}")
|
||||||
|
except Exception as history_error:
|
||||||
|
logger.error(f"获取历史ID失败: {str(history_error)}")
|
||||||
|
|
||||||
# 设置Pub/Sub主题和订阅名称
|
# 设置Pub/Sub主题和订阅名称
|
||||||
project_id = getattr(settings, 'GOOGLE_CLOUD_PROJECT_ID', '')
|
project_id = getattr(settings, 'GOOGLE_CLOUD_PROJECT_ID', '')
|
||||||
if not project_id:
|
if not project_id:
|
||||||
return False, "未配置Google Cloud项目ID"
|
return False, "未配置Google Cloud项目ID"
|
||||||
|
|
||||||
topic = topic_name or GmailService.PUBSUB_TOPIC.format(project_id=project_id)
|
# 完整的主题格式必须是:projects/{project_id}/topics/{topic_name}
|
||||||
subscription = subscription_name or GmailService.PUBSUB_SUBSCRIPTION.format(project_id=project_id)
|
# 使用已经在Google Cloud Console中创建的主题:gmail-watch-topic
|
||||||
|
full_topic_name = f'projects/{project_id}/topics/gmail-watch-topic'
|
||||||
|
logger.info(f"使用主题名称: {full_topic_name}")
|
||||||
|
|
||||||
# 为Gmail账户启用推送通知
|
# 为Gmail账户启用推送通知
|
||||||
request = {
|
request = {
|
||||||
'labelIds': ['INBOX'],
|
'labelIds': ['INBOX'],
|
||||||
'topicName': topic
|
'topicName': full_topic_name
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 先停止现有的监听
|
# 先停止现有的监听
|
||||||
service.users().stop(userId='me').execute()
|
service.users().stop(userId='me').execute()
|
||||||
logger.info(f"已停止现有的监听: {user_email}")
|
logger.info(f"已停止现有的监听: {user_email}")
|
||||||
except:
|
except Exception as stop_error:
|
||||||
pass
|
logger.warning(f"停止现有监听失败(可能无监听): {str(stop_error)}")
|
||||||
|
|
||||||
# 启动新的监听
|
# 启动新的监听
|
||||||
service.users().watch(userId='me', body=request).execute()
|
watch_response = service.users().watch(userId='me', body=request).execute()
|
||||||
logger.info(f"已为 {user_email} 设置Gmail推送通知,主题: {topic}")
|
logger.info(f"已为 {user_email} 设置Gmail推送通知,主题: {full_topic_name}, 响应: {watch_response}")
|
||||||
|
|
||||||
|
# 如果响应中包含historyId,保存它
|
||||||
|
if 'historyId' in watch_response:
|
||||||
|
credential.last_history_id = watch_response['historyId']
|
||||||
|
credential.save()
|
||||||
|
logger.info(f"已从watch响应更新{user_email}的历史ID: {watch_response['historyId']}")
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@ -613,113 +651,33 @@ class GmailService:
|
|||||||
return False, f"设置Gmail推送通知失败: {str(e)}"
|
return False, f"设置Gmail推送通知失败: {str(e)}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def start_pubsub_listener(user_id=None):
|
def process_new_emails(user, credential, history_id=None):
|
||||||
"""
|
|
||||||
启动Pub/Sub监听器,监听Gmail新消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id: 指定要监听的用户ID (可选,如果不指定则监听所有用户)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
project_id = getattr(settings, 'GOOGLE_CLOUD_PROJECT_ID', '')
|
|
||||||
if not project_id:
|
|
||||||
logger.error("未配置Google Cloud项目ID,无法启动Gmail监听")
|
|
||||||
return
|
|
||||||
|
|
||||||
subscription_name = GmailService.PUBSUB_SUBSCRIPTION.format(project_id=project_id)
|
|
||||||
|
|
||||||
# 创建订阅者客户端
|
|
||||||
subscriber = pubsub_v1.SubscriberClient()
|
|
||||||
subscription_path = subscriber.subscription_path(project_id, subscription_name.split('/')[-1])
|
|
||||||
|
|
||||||
def callback(message):
|
|
||||||
"""处理接收到的Pub/Sub消息"""
|
|
||||||
try:
|
|
||||||
# 解析消息数据
|
|
||||||
data = json.loads(message.data.decode('utf-8'))
|
|
||||||
logger.info(f"接收到Gmail推送通知: {data}")
|
|
||||||
|
|
||||||
# 确认消息已处理
|
|
||||||
message.ack()
|
|
||||||
|
|
||||||
# 获取关键信息
|
|
||||||
email_address = data.get('emailAddress')
|
|
||||||
history_id = data.get('historyId')
|
|
||||||
|
|
||||||
if not email_address or not history_id:
|
|
||||||
logger.error("推送通知缺少必要信息")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 获取用户凭证
|
|
||||||
query = GmailCredential.objects.filter(email=email_address)
|
|
||||||
if user_id:
|
|
||||||
query = query.filter(user_id=user_id)
|
|
||||||
|
|
||||||
credential = query.first()
|
|
||||||
if not credential:
|
|
||||||
logger.error(f"未找到匹配的Gmail凭证: {email_address}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 处理新收到的邮件
|
|
||||||
GmailService.process_new_emails(credential.user, credential, history_id)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理Gmail推送通知失败: {str(e)}")
|
|
||||||
# 确认消息,避免重复处理
|
|
||||||
message.ack()
|
|
||||||
|
|
||||||
# 设置订阅流
|
|
||||||
streaming_pull_future = subscriber.subscribe(subscription_path, callback=callback)
|
|
||||||
logger.info(f"Gmail Pub/Sub监听器已启动: {subscription_path}")
|
|
||||||
|
|
||||||
# 保持监听状态
|
|
||||||
try:
|
|
||||||
streaming_pull_future.result()
|
|
||||||
except Exception as e:
|
|
||||||
streaming_pull_future.cancel()
|
|
||||||
logger.error(f"Gmail Pub/Sub监听器中断: {str(e)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"启动Gmail Pub/Sub监听器失败: {str(e)}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def start_pubsub_listener_thread(user_id=None):
|
|
||||||
"""在后台线程中启动Pub/Sub监听器"""
|
|
||||||
t = threading.Thread(target=GmailService.start_pubsub_listener, args=(user_id,))
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
return t
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_new_emails(user, credential, history_id):
|
|
||||||
"""
|
"""
|
||||||
处理新收到的邮件
|
处理新收到的邮件
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user: 用户对象
|
user: 用户对象
|
||||||
credential: Gmail凭证对象
|
credential: Gmail凭证对象
|
||||||
history_id: Gmail历史记录ID
|
history_id: Gmail历史记录ID (可选,如果不提供则使用凭证中的last_history_id)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 如果没有提供history_id,使用凭证中的last_history_id
|
||||||
|
if not history_id and credential.last_history_id:
|
||||||
|
history_id = credential.last_history_id
|
||||||
|
logger.info(f"使用凭证中保存的历史ID: {history_id}")
|
||||||
|
|
||||||
|
if not history_id:
|
||||||
|
logger.error(f"缺少历史ID,无法处理新邮件")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"开始处理Gmail新邮件,用户: {user.username}, 邮箱: {credential.email}, 历史ID: {history_id}")
|
||||||
|
|
||||||
# 获取Gmail服务
|
# 获取Gmail服务
|
||||||
service = GmailService.get_service(credential)
|
service = GmailService.get_service(credential)
|
||||||
|
|
||||||
# 获取历史记录变更
|
|
||||||
history_results = service.users().history().list(
|
|
||||||
userId='me',
|
|
||||||
startHistoryId=history_id,
|
|
||||||
historyTypes=['messageAdded']
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
if 'history' not in history_results:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 获取活跃对话
|
# 获取活跃对话
|
||||||
active_conversations = GmailConversation.objects.filter(
|
active_conversations = GmailConversation.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
@ -732,37 +690,172 @@ class GmailService:
|
|||||||
logger.info(f"用户 {user.username} 没有活跃的Gmail对话")
|
logger.info(f"用户 {user.username} 没有活跃的Gmail对话")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 处理每个历史变更
|
logger.info(f"找到 {len(influencer_emails)} 个活跃的Gmail对话")
|
||||||
for history in history_results.get('history', []):
|
|
||||||
for message_added in history.get('messagesAdded', []):
|
# 方法1: 通过历史记录获取变更
|
||||||
message_id = message_added.get('message', {}).get('id')
|
try:
|
||||||
if not message_id:
|
logger.info(f"通过历史记录获取变更...")
|
||||||
continue
|
# 获取历史记录变更,包含所有相关变更类型
|
||||||
|
history_results = service.users().history().list(
|
||||||
# 获取完整邮件内容
|
userId='me',
|
||||||
message = service.users().messages().get(userId='me', id=message_id).execute()
|
startHistoryId=history_id,
|
||||||
email_data = GmailService._parse_email_content(message)
|
historyTypes=['messageAdded', 'messageDeleted', 'labelAdded', 'labelRemoved']
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# 保存最新的historyId(如果有)
|
||||||
|
if 'historyId' in history_results:
|
||||||
|
new_history_id = history_results['historyId']
|
||||||
|
credential.last_history_id = new_history_id
|
||||||
|
credential.save()
|
||||||
|
logger.info(f"已更新最新历史ID: {new_history_id}")
|
||||||
|
|
||||||
|
# 处理历史记录
|
||||||
|
processed_by_history = False
|
||||||
|
if 'history' in history_results and history_results['history']:
|
||||||
|
logger.info(f"找到 {len(history_results.get('history', []))} 条历史变更记录")
|
||||||
|
|
||||||
if not email_data:
|
# 提取所有消息ID
|
||||||
continue
|
message_ids = set()
|
||||||
|
for history in history_results.get('history', []):
|
||||||
|
# 检查不同类型的变更
|
||||||
|
for messages_key in ['messagesAdded', 'labelAdded', 'labelRemoved']:
|
||||||
|
for message_item in history.get(messages_key, []):
|
||||||
|
if 'message' in message_item and 'id' in message_item['message']:
|
||||||
|
message_ids.add(message_item['message']['id'])
|
||||||
|
|
||||||
|
if message_ids:
|
||||||
|
logger.info(f"从历史记录中找到 {len(message_ids)} 个消息ID")
|
||||||
|
processed_by_history = True
|
||||||
|
# 处理每个消息
|
||||||
|
for message_id in message_ids:
|
||||||
|
GmailService._process_single_message(service, user, credential, message_id, active_conversations, influencer_emails)
|
||||||
|
else:
|
||||||
|
logger.info(f"未找到历史变更记录")
|
||||||
|
|
||||||
|
# 方法2: 如果历史记录没有变更,直接查询收件箱最近邮件
|
||||||
|
if not processed_by_history:
|
||||||
|
logger.info(f"未通过历史记录找到变更,尝试直接查询最近邮件...")
|
||||||
|
# 查询最近的10封邮件
|
||||||
|
results = service.users().messages().list(
|
||||||
|
userId='me',
|
||||||
|
maxResults=10,
|
||||||
|
labelIds=['INBOX']
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
messages = results.get('messages', [])
|
||||||
|
if not messages:
|
||||||
|
logger.info(f"未找到任何收件箱邮件")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"找到 {len(messages)} 封收件箱邮件,检查最近的邮件")
|
||||||
|
|
||||||
|
# 检查所有邮件,不限制处理数量
|
||||||
|
processed_message_ids = []
|
||||||
|
saved_count = 0
|
||||||
|
|
||||||
|
# 从conversation的metadata中获取已处理的消息ID
|
||||||
|
already_processed_ids = set()
|
||||||
|
for conversation in active_conversations:
|
||||||
|
if conversation.metadata and 'last_processed_messages' in conversation.metadata:
|
||||||
|
already_processed_ids.update(conversation.metadata.get('last_processed_messages', []))
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
message_id = msg['id']
|
||||||
|
|
||||||
# 检查是否是来自达人的邮件
|
# 避免重复处理
|
||||||
if email_data['from_email'] in influencer_emails:
|
if message_id in already_processed_ids:
|
||||||
# 查找相关对话
|
logger.info(f"邮件ID: {message_id} 已处理过,跳过")
|
||||||
conversation = active_conversations.filter(
|
continue
|
||||||
influencer_email=email_data['from_email']
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if conversation:
|
|
||||||
# 将新邮件保存到聊天历史
|
|
||||||
GmailService._save_email_to_chat(
|
|
||||||
user,
|
|
||||||
credential,
|
|
||||||
conversation,
|
|
||||||
email_data
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发送通知
|
processed_message_ids.append(message_id)
|
||||||
|
logger.info(f"处理新发现的邮件ID: {message_id}")
|
||||||
|
|
||||||
|
if GmailService._process_single_message(service, user, credential, message_id, active_conversations, influencer_emails):
|
||||||
|
saved_count += 1
|
||||||
|
|
||||||
|
# 更新最近处理的消息ID到所有活跃对话
|
||||||
|
if processed_message_ids:
|
||||||
|
for conversation in active_conversations:
|
||||||
|
metadata = conversation.metadata or {}
|
||||||
|
|
||||||
|
# 保留之前处理过的ID,加上新处理的ID
|
||||||
|
old_ids = metadata.get('last_processed_messages', [])
|
||||||
|
# 只保留最近的20个ID,避免列表过长
|
||||||
|
new_ids = (processed_message_ids + old_ids)[:20]
|
||||||
|
|
||||||
|
metadata['last_processed_messages'] = new_ids
|
||||||
|
conversation.metadata = metadata
|
||||||
|
conversation.save()
|
||||||
|
|
||||||
|
logger.info(f"更新了 {len(processed_message_ids)} 个新处理的邮件ID,保存了 {saved_count} 封邮件")
|
||||||
|
|
||||||
|
except HttpError as e:
|
||||||
|
if e.resp.status == 404:
|
||||||
|
# 历史ID可能无效,尝试获取当前ID并更新
|
||||||
|
logger.warning(f"历史ID {history_id} 无效,尝试获取当前ID")
|
||||||
|
try:
|
||||||
|
profile = service.users().getProfile(userId='me').execute()
|
||||||
|
new_history_id = profile.get('historyId')
|
||||||
|
if new_history_id:
|
||||||
|
credential.last_history_id = new_history_id
|
||||||
|
credential.save()
|
||||||
|
logger.info(f"已更新为新的历史ID: {new_history_id}")
|
||||||
|
# 尝试使用方法2直接获取邮件
|
||||||
|
logger.info(f"尝试直接获取最近邮件...")
|
||||||
|
GmailService.process_new_emails(user, credential, new_history_id)
|
||||||
|
except Exception as profile_error:
|
||||||
|
logger.error(f"获取新历史ID失败: {str(profile_error)}")
|
||||||
|
else:
|
||||||
|
logger.error(f"获取历史变更失败: {str(e)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理Gmail新消息失败: {str(e)}")
|
||||||
|
# 记录堆栈跟踪以便更好地诊断问题
|
||||||
|
import traceback
|
||||||
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_single_message(service, user, credential, message_id, active_conversations, influencer_emails):
|
||||||
|
"""处理单个邮件消息"""
|
||||||
|
try:
|
||||||
|
# 获取完整邮件内容
|
||||||
|
message = service.users().messages().get(userId='me', id=message_id).execute()
|
||||||
|
email_data = GmailService._parse_email_content(message)
|
||||||
|
|
||||||
|
if not email_data:
|
||||||
|
logger.warning(f"无法解析邮件内容: {message_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"邮件信息: 发件人={email_data['from_email']}, 收件人={email_data['to_email']}, 主题={email_data['subject']}")
|
||||||
|
|
||||||
|
saved = False
|
||||||
|
|
||||||
|
# 场景1: 来自达人的邮件 - 寻找匹配的对话记录
|
||||||
|
if email_data['from_email'] in influencer_emails:
|
||||||
|
logger.info(f"找到来自达人 {email_data['from_email']} 的邮件")
|
||||||
|
|
||||||
|
# 查找相关对话
|
||||||
|
conversation = active_conversations.filter(
|
||||||
|
influencer_email=email_data['from_email']
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if conversation:
|
||||||
|
logger.info(f"找到匹配的对话记录: ID={conversation.id}, 会话ID={conversation.conversation_id}")
|
||||||
|
|
||||||
|
# 将新邮件保存到聊天历史
|
||||||
|
success = GmailService._save_email_to_chat(
|
||||||
|
user,
|
||||||
|
credential,
|
||||||
|
conversation,
|
||||||
|
email_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"成功保存邮件到聊天历史")
|
||||||
|
saved = True
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
try:
|
||||||
NotificationService().send_notification(
|
NotificationService().send_notification(
|
||||||
user=user,
|
user=user,
|
||||||
title="收到新邮件",
|
title="收到新邮件",
|
||||||
@ -770,115 +863,101 @@ class GmailService:
|
|||||||
notification_type="gmail",
|
notification_type="gmail",
|
||||||
related_object_id=conversation.conversation_id
|
related_object_id=conversation.conversation_id
|
||||||
)
|
)
|
||||||
|
except Exception as notif_error:
|
||||||
logger.info(f"已处理来自 {email_data['from_email']} 的新邮件")
|
logger.error(f"发送通知失败: {str(notif_error)}")
|
||||||
|
else:
|
||||||
except Exception as e:
|
logger.error(f"保存邮件到聊天历史失败")
|
||||||
logger.error(f"处理Gmail新消息失败: {str(e)}")
|
else:
|
||||||
|
# 找不到对话记录,创建新的
|
||||||
@staticmethod
|
logger.info(f"未找到与 {email_data['from_email']} 的对话记录,创建新对话")
|
||||||
def _save_email_to_chat(user, credential, conversation, email_data):
|
try:
|
||||||
"""
|
conversation_id = f"gmail_{user.id}_{str(uuid.uuid4())[:8]}"
|
||||||
保存一封邮件到聊天历史
|
conversation = GmailConversation.objects.create(
|
||||||
|
user=user,
|
||||||
Args:
|
user_email=credential.email,
|
||||||
user: 用户对象
|
influencer_email=email_data['from_email'],
|
||||||
credential: Gmail凭证对象
|
conversation_id=conversation_id,
|
||||||
conversation: Gmail对话对象
|
title=f"与 {email_data['from_email']} 的Gmail对话",
|
||||||
email_data: 邮件数据
|
is_active=True
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 成功标志
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 查找关联的知识库
|
|
||||||
first_message = ChatHistory.objects.filter(
|
|
||||||
conversation_id=conversation.conversation_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not first_message:
|
|
||||||
knowledge_base = KnowledgeBase.objects.filter(user_id=user.id, type='private').first()
|
|
||||||
if not knowledge_base:
|
|
||||||
logger.error("未找到默认知识库")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
knowledge_base = first_message.knowledge_base
|
|
||||||
|
|
||||||
# 确定发送者角色 (user 或 assistant)
|
|
||||||
is_from_user = email_data['from_email'].lower() == credential.email.lower()
|
|
||||||
role = 'user' if is_from_user else 'assistant'
|
|
||||||
|
|
||||||
# 准备内容文本
|
|
||||||
content = f"主题: {email_data['subject']}\n\n{email_data['body']}"
|
|
||||||
|
|
||||||
# 创建聊天消息
|
|
||||||
chat_message = ChatHistory.objects.create(
|
|
||||||
user=user,
|
|
||||||
knowledge_base=knowledge_base,
|
|
||||||
conversation_id=conversation.conversation_id,
|
|
||||||
title=conversation.title,
|
|
||||||
role=role,
|
|
||||||
content=content,
|
|
||||||
metadata={
|
|
||||||
'gmail_message_id': email_data['id'],
|
|
||||||
'from': email_data['from'],
|
|
||||||
'to': email_data['to'],
|
|
||||||
'date': email_data['date'],
|
|
||||||
'source': 'gmail'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新对话的同步时间
|
|
||||||
conversation.last_sync_time = timezone.now()
|
|
||||||
conversation.save()
|
|
||||||
|
|
||||||
# 处理附件
|
|
||||||
if email_data['attachments']:
|
|
||||||
for attachment in email_data['attachments']:
|
|
||||||
if 'attachmentId' in attachment:
|
|
||||||
# 下载附件
|
|
||||||
file_path = GmailService.download_attachment(
|
|
||||||
user,
|
|
||||||
credential,
|
|
||||||
email_data['id'],
|
|
||||||
attachment['attachmentId'],
|
|
||||||
attachment['filename']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if file_path:
|
# 保存邮件到新创建的对话
|
||||||
# 保存附件记录
|
success = GmailService._save_email_to_chat(
|
||||||
gmail_attachment = GmailAttachment.objects.create(
|
user,
|
||||||
conversation=conversation,
|
credential,
|
||||||
email_message_id=email_data['id'],
|
conversation,
|
||||||
attachment_id=attachment['attachmentId'],
|
email_data
|
||||||
filename=attachment['filename'],
|
)
|
||||||
file_path=file_path,
|
|
||||||
content_type=attachment['mimeType'],
|
if success:
|
||||||
size=attachment['size'],
|
logger.info(f"成功保存邮件到新创建的对话")
|
||||||
sender_email=email_data['from_email'],
|
saved = True
|
||||||
chat_message_id=str(chat_message.id)
|
except Exception as create_error:
|
||||||
)
|
logger.error(f"创建新对话失败: {str(create_error)}")
|
||||||
|
|
||||||
# 更新聊天消息,添加附件信息
|
|
||||||
metadata = chat_message.metadata or {}
|
|
||||||
if 'attachments' not in metadata:
|
|
||||||
metadata['attachments'] = []
|
|
||||||
|
|
||||||
metadata['attachments'].append({
|
|
||||||
'id': str(gmail_attachment.id),
|
|
||||||
'filename': attachment['filename'],
|
|
||||||
'size': attachment['size'],
|
|
||||||
'mime_type': attachment['mimeType'],
|
|
||||||
'url': gmail_attachment.get_absolute_url()
|
|
||||||
})
|
|
||||||
|
|
||||||
chat_message.metadata = metadata
|
|
||||||
chat_message.save()
|
|
||||||
|
|
||||||
return True
|
# 场景2: 发送给达人的邮件 - 寻找匹配的对话记录
|
||||||
|
elif email_data['to_email'] in influencer_emails:
|
||||||
|
logger.info(f"这是发送给达人 {email_data['to_email']} 的邮件")
|
||||||
|
|
||||||
|
# 查找相关对话
|
||||||
|
conversation = active_conversations.filter(
|
||||||
|
influencer_email=email_data['to_email']
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if conversation:
|
||||||
|
logger.info(f"找到匹配的对话记录: ID={conversation.id}, 会话ID={conversation.conversation_id}")
|
||||||
|
|
||||||
|
# 将新邮件保存到聊天历史
|
||||||
|
success = GmailService._save_email_to_chat(
|
||||||
|
user,
|
||||||
|
credential,
|
||||||
|
conversation,
|
||||||
|
email_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"成功保存邮件到聊天历史")
|
||||||
|
saved = True
|
||||||
|
else:
|
||||||
|
logger.error(f"保存邮件到聊天历史失败")
|
||||||
|
else:
|
||||||
|
# 找不到对话记录,创建新的
|
||||||
|
logger.info(f"未找到与 {email_data['to_email']} 的对话记录,创建新对话")
|
||||||
|
try:
|
||||||
|
conversation_id = f"gmail_{user.id}_{str(uuid.uuid4())[:8]}"
|
||||||
|
conversation = GmailConversation.objects.create(
|
||||||
|
user=user,
|
||||||
|
user_email=credential.email,
|
||||||
|
influencer_email=email_data['to_email'],
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
title=f"与 {email_data['to_email']} 的Gmail对话",
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存邮件到新创建的对话
|
||||||
|
success = GmailService._save_email_to_chat(
|
||||||
|
user,
|
||||||
|
credential,
|
||||||
|
conversation,
|
||||||
|
email_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"成功保存邮件到新创建的对话")
|
||||||
|
saved = True
|
||||||
|
except Exception as create_error:
|
||||||
|
logger.error(f"创建新对话失败: {str(create_error)}")
|
||||||
|
|
||||||
|
# 场景3: 其他邮件 - 不保存非达人相关邮件
|
||||||
|
else:
|
||||||
|
logger.info(f"邮件 {email_data['from_email']} → {email_data['to_email']} 与跟踪的达人对话无关,不保存")
|
||||||
|
|
||||||
|
return saved
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存Gmail新邮件到聊天记录失败: {str(e)}")
|
logger.error(f"处理邮件 {message_id} 时出错: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"错误详情: {traceback.format_exc()}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1080,5 +1159,197 @@ class GmailService:
|
|||||||
logger.error(f"发送Gmail邮件失败: {str(e)}")
|
logger.error(f"发送Gmail邮件失败: {str(e)}")
|
||||||
return False, f"发送Gmail邮件失败: {str(e)}"
|
return False, f"发送Gmail邮件失败: {str(e)}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _save_email_to_chat(user, credential, conversation, email_data):
|
||||||
|
"""
|
||||||
|
保存一封邮件到聊天历史
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: 用户对象
|
||||||
|
credential: Gmail凭证对象
|
||||||
|
conversation: Gmail对话对象
|
||||||
|
email_data: 邮件数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 成功标志
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 查找关联的知识库
|
||||||
|
first_message = ChatHistory.objects.filter(
|
||||||
|
conversation_id=conversation.conversation_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not first_message:
|
||||||
|
knowledge_base = KnowledgeBase.objects.filter(user_id=user.id, type='private').first()
|
||||||
|
if not knowledge_base:
|
||||||
|
logger.error("未找到默认知识库")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
knowledge_base = first_message.knowledge_base
|
||||||
|
|
||||||
|
# 确定发送者角色 (user 或 assistant)
|
||||||
|
is_from_user = email_data['from_email'].lower() == credential.email.lower()
|
||||||
|
role = 'user' if is_from_user else 'assistant'
|
||||||
|
|
||||||
|
# 准备内容文本
|
||||||
|
content = f"主题: {email_data['subject']}\n\n{email_data['body']}"
|
||||||
|
|
||||||
|
# 创建聊天消息
|
||||||
|
chat_message = ChatHistory.objects.create(
|
||||||
|
user=user,
|
||||||
|
knowledge_base=knowledge_base,
|
||||||
|
conversation_id=conversation.conversation_id,
|
||||||
|
title=conversation.title,
|
||||||
|
role=role,
|
||||||
|
content=content,
|
||||||
|
metadata={
|
||||||
|
'gmail_message_id': email_data['id'],
|
||||||
|
'from': email_data['from'],
|
||||||
|
'to': email_data['to'],
|
||||||
|
'date': email_data['date'],
|
||||||
|
'source': 'gmail'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新对话的同步时间
|
||||||
|
conversation.last_sync_time = timezone.now()
|
||||||
|
conversation.save()
|
||||||
|
|
||||||
|
# 处理附件
|
||||||
|
if email_data['attachments']:
|
||||||
|
for attachment in email_data['attachments']:
|
||||||
|
if 'attachmentId' in attachment:
|
||||||
|
# 下载附件
|
||||||
|
file_path = GmailService.download_attachment(
|
||||||
|
user,
|
||||||
|
credential,
|
||||||
|
email_data['id'],
|
||||||
|
attachment['attachmentId'],
|
||||||
|
attachment['filename']
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
# 保存附件记录
|
||||||
|
gmail_attachment = GmailAttachment.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
email_message_id=email_data['id'],
|
||||||
|
attachment_id=attachment['attachmentId'],
|
||||||
|
filename=attachment['filename'],
|
||||||
|
file_path=file_path,
|
||||||
|
content_type=attachment['mimeType'],
|
||||||
|
size=attachment['size'],
|
||||||
|
sender_email=email_data['from_email'],
|
||||||
|
chat_message_id=str(chat_message.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新聊天消息,添加附件信息
|
||||||
|
metadata = chat_message.metadata or {}
|
||||||
|
if 'attachments' not in metadata:
|
||||||
|
metadata['attachments'] = []
|
||||||
|
|
||||||
|
metadata['attachments'].append({
|
||||||
|
'id': str(gmail_attachment.id),
|
||||||
|
'filename': attachment['filename'],
|
||||||
|
'size': attachment['size'],
|
||||||
|
'mime_type': attachment['mimeType'],
|
||||||
|
'url': gmail_attachment.get_absolute_url()
|
||||||
|
})
|
||||||
|
|
||||||
|
chat_message.metadata = metadata
|
||||||
|
chat_message.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存Gmail新邮件到聊天记录失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_conversation_summary(user, conversation_id):
|
||||||
|
"""
|
||||||
|
获取Gmail对话摘要
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): 用户对象
|
||||||
|
conversation_id (str): 对话ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (摘要内容, 错误信息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 查询对话
|
||||||
|
try:
|
||||||
|
conversation = GmailConversation.objects.get(conversation_id=conversation_id)
|
||||||
|
except GmailConversation.DoesNotExist:
|
||||||
|
return None, "对话不存在"
|
||||||
|
|
||||||
|
# 检查访问权限
|
||||||
|
if str(conversation.user.id) != str(user.id):
|
||||||
|
return None, "无权访问此对话"
|
||||||
|
|
||||||
|
# 获取最新的聊天历史ID
|
||||||
|
latest_chat = ChatHistory.objects.filter(
|
||||||
|
conversation_id=conversation_id
|
||||||
|
).order_by('-created_at').first()
|
||||||
|
|
||||||
|
if not latest_chat:
|
||||||
|
return None, "对话中没有消息记录"
|
||||||
|
|
||||||
|
# 检查摘要是否已存在且是最新的
|
||||||
|
try:
|
||||||
|
summary = ConversationSummary.objects.get(conversation=conversation)
|
||||||
|
# 如果摘要存在且已包含最新消息,直接返回
|
||||||
|
if summary.last_message_id == str(latest_chat.id):
|
||||||
|
return summary.content, None
|
||||||
|
|
||||||
|
except ConversationSummary.DoesNotExist:
|
||||||
|
# 如果摘要不存在,则创建
|
||||||
|
summary = None
|
||||||
|
|
||||||
|
# 获取对话历史
|
||||||
|
chat_history = ChatHistory.objects.filter(
|
||||||
|
conversation_id=conversation_id
|
||||||
|
).order_by('created_at')
|
||||||
|
|
||||||
|
if not chat_history:
|
||||||
|
return None, "对话中没有消息记录"
|
||||||
|
|
||||||
|
# 构造对话历史记录列表
|
||||||
|
conversation_history = []
|
||||||
|
for chat in chat_history:
|
||||||
|
conversation_history.append({
|
||||||
|
'content': chat.content,
|
||||||
|
'is_from_user': chat.role == 'user',
|
||||||
|
'timestamp': chat.created_at.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
# 使用AI服务生成摘要
|
||||||
|
summary_content, error = AIService.generate_conversation_summary(conversation_history)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return None, f"生成摘要失败: {error}"
|
||||||
|
|
||||||
|
# 持久化保存摘要
|
||||||
|
if summary:
|
||||||
|
# 更新现有摘要
|
||||||
|
summary.content = summary_content
|
||||||
|
summary.last_message_id = str(latest_chat.id)
|
||||||
|
summary.save()
|
||||||
|
else:
|
||||||
|
# 创建新摘要
|
||||||
|
summary = ConversationSummary.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
content=summary_content,
|
||||||
|
last_message_id=str(latest_chat.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return summary_content, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"获取对话摘要失败: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return None, error_msg
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -7,8 +7,9 @@ from .views import (
|
|||||||
GmailConversationView,
|
GmailConversationView,
|
||||||
GmailAttachmentListView,
|
GmailAttachmentListView,
|
||||||
GmailPubSubView,
|
GmailPubSubView,
|
||||||
GmailNotificationStartView,
|
GmailSendEmailView,
|
||||||
GmailSendEmailView
|
GmailWebhookView,
|
||||||
|
GmailConversationSummaryView
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = 'gmail'
|
app_name = 'gmail'
|
||||||
@ -22,6 +23,8 @@ urlpatterns = [
|
|||||||
path('attachments/', GmailAttachmentListView.as_view(), name='attachment_list'),
|
path('attachments/', GmailAttachmentListView.as_view(), name='attachment_list'),
|
||||||
path('attachments/<str:conversation_id>/', GmailAttachmentListView.as_view(), name='attachment_list_by_conversation'),
|
path('attachments/<str:conversation_id>/', GmailAttachmentListView.as_view(), name='attachment_list_by_conversation'),
|
||||||
path('notifications/setup/', GmailPubSubView.as_view(), name='pubsub_setup'),
|
path('notifications/setup/', GmailPubSubView.as_view(), name='pubsub_setup'),
|
||||||
path('notifications/start/', GmailNotificationStartView.as_view(), name='notification_start'),
|
|
||||||
path('send/', GmailSendEmailView.as_view(), name='send_email'),
|
path('send/', GmailSendEmailView.as_view(), name='send_email'),
|
||||||
|
path('webhook/', GmailWebhookView.as_view(), name='webhook'),
|
||||||
|
path('conversations/summary/', GmailConversationSummaryView.as_view(), name='conversation_summary_list'),
|
||||||
|
path('conversations/summary/<str:conversation_id>/', GmailConversationSummaryView.as_view(), name='conversation_summary_detail'),
|
||||||
]
|
]
|
@ -11,6 +11,9 @@ import os
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import threading
|
||||||
|
|
||||||
# 配置日志记录器,用于记录视图操作的调试、警告和错误信息
|
# 配置日志记录器,用于记录视图操作的调试、警告和错误信息
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -45,14 +48,26 @@ class GmailAuthInitiateView(APIView):
|
|||||||
# 调用 GmailService 生成授权 URL
|
# 调用 GmailService 生成授权 URL
|
||||||
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
|
auth_url = GmailService.initiate_authentication(request.user, client_secret_json)
|
||||||
logger.info(f"Generated auth URL for user {request.user.id}")
|
logger.info(f"Generated auth URL for user {request.user.id}")
|
||||||
return Response({'auth_url': auth_url}, status=status.HTTP_200_OK)
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '成功生成授权URL',
|
||||||
|
'data': {'auth_url': auth_url}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 记录错误并返回服务器错误响应
|
# 记录错误并返回服务器错误响应
|
||||||
logger.error(f"Error initiating authentication for user {request.user.id}: {str(e)}")
|
logger.error(f"Error initiating authentication for user {request.user.id}: {str(e)}")
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'认证初始化错误:{str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
# 记录无效请求数据并返回错误响应
|
# 记录无效请求数据并返回错误响应
|
||||||
logger.warning(f"Invalid request data: {serializer.errors}")
|
logger.warning(f"Invalid request data: {serializer.errors}")
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '请求数据无效',
|
||||||
|
'data': serializer.errors
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class GmailAuthCompleteView(APIView):
|
class GmailAuthCompleteView(APIView):
|
||||||
@ -88,14 +103,26 @@ class GmailAuthCompleteView(APIView):
|
|||||||
# 序列化凭证数据以返回
|
# 序列化凭证数据以返回
|
||||||
serializer = GmailCredentialSerializer(credential, context={'request': request})
|
serializer = GmailCredentialSerializer(credential, context={'request': request})
|
||||||
logger.info(f"Authentication completed for user {request.user.id}, email: {credential.email}")
|
logger.info(f"Authentication completed for user {request.user.id}, email: {credential.email}")
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response({
|
||||||
|
'code': 201,
|
||||||
|
'message': '认证完成并成功保存凭证',
|
||||||
|
'data': serializer.data
|
||||||
|
}, status=status.HTTP_201_CREATED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 记录错误并返回服务器错误响应
|
# 记录错误并返回服务器错误响应
|
||||||
logger.error(f"Error completing authentication for user {request.user.id}: {str(e)}")
|
logger.error(f"Error completing authentication for user {request.user.id}: {str(e)}")
|
||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'完成认证时发生错误:{str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
# 记录无效请求数据并返回错误响应
|
# 记录无效请求数据并返回错误响应
|
||||||
logger.warning(f"Invalid request data: {serializer.errors}")
|
logger.warning(f"Invalid request data: {serializer.errors}")
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '请求数据无效',
|
||||||
|
'data': serializer.errors
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class GmailCredentialListView(APIView):
|
class GmailCredentialListView(APIView):
|
||||||
@ -121,7 +148,11 @@ class GmailCredentialListView(APIView):
|
|||||||
credentials = request.user.gmail_credentials.all()
|
credentials = request.user.gmail_credentials.all()
|
||||||
# 序列化凭证数据
|
# 序列化凭证数据
|
||||||
serializer = GmailCredentialSerializer(credentials, many=True, context={'request': request})
|
serializer = GmailCredentialSerializer(credentials, many=True, context={'request': request})
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '成功获取凭证列表',
|
||||||
|
'data': serializer.data
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class GmailCredentialDetailView(APIView):
|
class GmailCredentialDetailView(APIView):
|
||||||
@ -148,7 +179,11 @@ class GmailCredentialDetailView(APIView):
|
|||||||
# 获取用户拥有的指定凭证,未找到则返回 404
|
# 获取用户拥有的指定凭证,未找到则返回 404
|
||||||
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
||||||
serializer = GmailCredentialSerializer(credential, context={'request': request})
|
serializer = GmailCredentialSerializer(credential, context={'request': request})
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '成功获取凭证详情',
|
||||||
|
'data': serializer.data
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request, pk):
|
def patch(self, request, pk):
|
||||||
"""
|
"""
|
||||||
@ -174,9 +209,17 @@ class GmailCredentialDetailView(APIView):
|
|||||||
if serializer.validated_data.get('is_default', False):
|
if serializer.validated_data.get('is_default', False):
|
||||||
GmailCredential.objects.filter(user=request.user).exclude(id=credential.id).update(is_default=False)
|
GmailCredential.objects.filter(user=request.user).exclude(id=credential.id).update(is_default=False)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '成功更新凭证',
|
||||||
|
'data': serializer.data
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
# 返回无效数据错误
|
# 返回无效数据错误
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '请求数据无效',
|
||||||
|
'data': serializer.errors
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request, pk):
|
def delete(self, request, pk):
|
||||||
"""
|
"""
|
||||||
@ -196,7 +239,12 @@ class GmailCredentialDetailView(APIView):
|
|||||||
# 获取并删除用户拥有的指定凭证
|
# 获取并删除用户拥有的指定凭证
|
||||||
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
credential = get_object_or_404(GmailCredential, pk=pk, user=request.user)
|
||||||
credential.delete()
|
credential.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response({
|
||||||
|
'code': 204,
|
||||||
|
'message': '凭证已成功删除',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class GmailConversationView(APIView):
|
class GmailConversationView(APIView):
|
||||||
"""
|
"""
|
||||||
@ -301,6 +349,7 @@ class GmailConversationView(APIView):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class GmailAttachmentListView(APIView):
|
class GmailAttachmentListView(APIView):
|
||||||
"""
|
"""
|
||||||
API视图,用于获取Gmail附件列表。
|
API视图,用于获取Gmail附件列表。
|
||||||
@ -348,7 +397,7 @@ class GmailAttachmentListView(APIView):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class GmailPubSubView(APIView):
|
class GmailPubSubView(APIView):
|
||||||
"""
|
"""
|
||||||
API视图,用于设置Gmail的Pub/Sub实时通知。
|
API视图,用于设置Gmail的Pub/Sub实时通知。
|
||||||
@ -376,24 +425,44 @@ class GmailPubSubView(APIView):
|
|||||||
email = request.data.get('email')
|
email = request.data.get('email')
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
return Response({'error': '必须提供Gmail邮箱地址'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '必须提供Gmail邮箱地址',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 检查用户是否有此Gmail账户的凭证
|
# 检查用户是否有此Gmail账户的凭证
|
||||||
credential = GmailCredential.objects.filter(user=request.user, email=email).first()
|
credential = GmailCredential.objects.filter(user=request.user, email=email).first()
|
||||||
if not credential:
|
if not credential:
|
||||||
return Response({'error': f'未找到{email}的授权信息'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': f'未找到{email}的授权信息',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# 设置Pub/Sub通知
|
# 设置Pub/Sub通知
|
||||||
success, error = GmailService.setup_gmail_push_notification(request.user, email)
|
success, error = GmailService.setup_gmail_push_notification(request.user, email)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return Response({'message': f'已成功为{email}设置实时通知'}, status=status.HTTP_200_OK)
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': f'已成功为{email}设置实时通知',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return Response({'error': error}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': error,
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"设置Gmail Pub/Sub通知失败: {str(e)}")
|
logger.error(f"设置Gmail Pub/Sub通知失败: {str(e)}")
|
||||||
return Response({'error': f'设置Gmail实时通知失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'设置Gmail实时通知失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
@ -422,41 +491,12 @@ class GmailPubSubView(APIView):
|
|||||||
'is_default': cred.is_default
|
'is_default': cred.is_default
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response({'accounts': accounts}, status=status.HTTP_200_OK)
|
return Response({
|
||||||
|
'code': 200,
|
||||||
class GmailNotificationStartView(APIView):
|
'message': '获取账户列表成功',
|
||||||
"""
|
'data': {'accounts': accounts}
|
||||||
API视图,用于启动Gmail Pub/Sub监听器。
|
}, status=status.HTTP_200_OK)
|
||||||
通常由系统管理员或后台任务调用,而非普通用户。
|
|
||||||
"""
|
|
||||||
permission_classes = [IsAuthenticated] # 可根据需要更改为更严格的权限
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
"""
|
|
||||||
处理POST请求,启动Gmail Pub/Sub监听器。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: Django REST Framework请求对象。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response: 启动结果的JSON响应。
|
|
||||||
|
|
||||||
Status Codes:
|
|
||||||
200: 成功启动监听器。
|
|
||||||
500: 服务器内部错误。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 可选:指定要监听的用户ID
|
|
||||||
user_id = request.data.get('user_id')
|
|
||||||
|
|
||||||
# 在后台线程中启动监听器
|
|
||||||
thread = GmailService.start_pubsub_listener_thread(user_id)
|
|
||||||
|
|
||||||
return Response({'message': '已成功启动Gmail实时通知监听器'}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"启动Gmail Pub/Sub监听器失败: {str(e)}")
|
|
||||||
return Response({'error': f'启动Gmail实时通知监听器失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
class GmailSendEmailView(APIView):
|
class GmailSendEmailView(APIView):
|
||||||
"""
|
"""
|
||||||
@ -498,7 +538,9 @@ class GmailSendEmailView(APIView):
|
|||||||
# 验证必填字段
|
# 验证必填字段
|
||||||
if not all([user_email, to_email, subject]):
|
if not all([user_email, to_email, subject]):
|
||||||
return Response({
|
return Response({
|
||||||
'error': '缺少必要参数,请提供email、to和subject字段'
|
'code': 400,
|
||||||
|
'message': '缺少必要参数,请提供email、to和subject字段',
|
||||||
|
'data': None
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 检查是否有此Gmail账户的凭证
|
# 检查是否有此Gmail账户的凭证
|
||||||
@ -510,7 +552,9 @@ class GmailSendEmailView(APIView):
|
|||||||
|
|
||||||
if not credential:
|
if not credential:
|
||||||
return Response({
|
return Response({
|
||||||
'error': f'未找到{user_email}的有效授权信息'
|
'code': 404,
|
||||||
|
'message': f'未找到{user_email}的有效授权信息',
|
||||||
|
'data': None
|
||||||
}, status=status.HTTP_404_NOT_FOUND)
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# 处理附件
|
# 处理附件
|
||||||
@ -556,18 +600,23 @@ class GmailSendEmailView(APIView):
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
return Response({
|
return Response({
|
||||||
|
'code': 200,
|
||||||
'message': '邮件发送成功',
|
'message': '邮件发送成功',
|
||||||
'message_id': result
|
'data': {'message_id': result}
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return Response({
|
return Response({
|
||||||
'error': result
|
'code': 500,
|
||||||
|
'message': result,
|
||||||
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"发送Gmail邮件失败: {str(e)}")
|
logger.error(f"发送Gmail邮件失败: {str(e)}")
|
||||||
return Response({
|
return Response({
|
||||||
'error': f'发送Gmail邮件失败: {str(e)}'
|
'code': 500,
|
||||||
|
'message': f'发送Gmail邮件失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
@ -595,4 +644,177 @@ class GmailSendEmailView(APIView):
|
|||||||
'is_default': cred.is_default
|
'is_default': cred.is_default
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response({'accounts': accounts}, status=status.HTTP_200_OK)
|
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推送通知。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Django REST Framework请求对象,包含Pub/Sub消息。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 接收结果的JSON响应。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"收到Gmail推送通知: {request.data}")
|
||||||
|
|
||||||
|
# 解析推送消息
|
||||||
|
message = request.data.get('message', {})
|
||||||
|
data = message.get('data', '')
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '无效的推送消息格式',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Base64解码消息数据
|
||||||
|
try:
|
||||||
|
decoded_data = json.loads(base64.b64decode(data).decode('utf-8'))
|
||||||
|
logger.info(f"解码后的推送数据: {decoded_data}")
|
||||||
|
|
||||||
|
# 处理Gmail通知
|
||||||
|
email_address = decoded_data.get('emailAddress')
|
||||||
|
history_id = decoded_data.get('historyId')
|
||||||
|
|
||||||
|
if not email_address:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '推送数据缺少邮箱地址',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 查找对应的Gmail凭证
|
||||||
|
credential = GmailCredential.objects.filter(email=email_address, is_valid=True).first()
|
||||||
|
if credential:
|
||||||
|
# 即使没有history_id,也尝试处理,因为我们现在有了备用机制
|
||||||
|
if not history_id:
|
||||||
|
logger.warning(f"推送通知中没有historyId,将使用凭证中保存的历史ID")
|
||||||
|
|
||||||
|
# 启动后台任务处理新邮件
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=GmailService.process_new_emails,
|
||||||
|
args=(credential.user, credential, history_id)
|
||||||
|
)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
else:
|
||||||
|
logger.warning(f"收到推送通知,但未找到对应的Gmail凭证: {email_address}")
|
||||||
|
|
||||||
|
# 确认接收
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '成功接收推送通知',
|
||||||
|
'data': None
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"解析推送数据失败: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': f'解析推送数据失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pymysql
|
import pymysql
|
||||||
|
import os
|
||||||
pymysql.install_as_MySQLdb()
|
pymysql.install_as_MySQLdb()
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
@ -27,7 +28,7 @@ SECRET_KEY = 'django-insecure-aie+z75u&tnnx8@g!2ie+q)qhq1!eg&ob!c1(e1vr!eclh+xv6
|
|||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '02bf-180-159-100-165.ngrok-free.app']
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@ -175,7 +176,7 @@ AUTH_USER_MODEL = 'accounts.User'
|
|||||||
|
|
||||||
API_BASE_URL = 'http://81.69.223.133:48329'
|
API_BASE_URL = 'http://81.69.223.133:48329'
|
||||||
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
|
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'
|
||||||
GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/'
|
GMAIL_WEBHOOK_URL = 'https://02bf-180-159-100-165.ngrok-free.app/api/gmail/webhook/'
|
||||||
APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006'
|
APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006'
|
||||||
|
|
||||||
|
|
||||||
@ -187,9 +188,10 @@ PROXY_URL = 'http://127.0.0.1:7890'
|
|||||||
|
|
||||||
|
|
||||||
# Gmail Pub/Sub相关设置
|
# Gmail Pub/Sub相关设置
|
||||||
GOOGLE_CLOUD_PROJECT_ID = 'your-project-id' # 替换为您的Google Cloud项目ID
|
GOOGLE_CLOUD_PROJECT_ID = 'knowledge-454905' # 替换为您的Google Cloud项目ID
|
||||||
GMAIL_PUBSUB_TOPIC = 'projects/{project_id}/topics/gmail-notifications'
|
# 主题名称
|
||||||
GMAIL_PUBSUB_SUBSCRIPTION = 'projects/{project_id}/subscriptions/gmail-notifications-sub'
|
GMAIL_PUBSUB_TOPIC = 'gmail-watch-topic'
|
||||||
|
|
||||||
# 设置允许使用Google Pub/Sub的应用列表
|
# 设置允许使用Google Pub/Sub的应用列表
|
||||||
INSTALLED_APPS += ['google.cloud.pubsub']
|
INSTALLED_APPS += ['google.cloud.pubsub']
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user