From aeebe645bf1810925cbf47115f59f72661769fc4 Mon Sep 17 00:00:00 2001 From: wanjia Date: Tue, 13 May 2025 18:36:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0gmail=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/accounts/migrations/0005_usergoal.py | 33 + apps/accounts/models.py | 18 + apps/accounts/serializers.py | 19 +- apps/accounts/services/goal_service.py | 102 +++ apps/accounts/urls.py | 4 + apps/accounts/views.py | 275 ++++++- apps/common/services/ai_service.py | 202 +++++ apps/gmail/migrations/0001_initial.py | 41 +- .../0002_gmailconversation_gmailattachment.py | 55 -- .../0002_gmailcredential_last_history_id.py | 18 + .../0003_gmailconversation_metadata.py | 18 + .../migrations/0004_conversationsummary.py | 31 + apps/gmail/models.py | 22 +- apps/gmail/services/gmail_service.py | 755 ++++++++++++------ apps/gmail/urls.py | 9 +- apps/gmail/views.py | 338 ++++++-- daren_project/settings.py | 14 +- 17 files changed, 1585 insertions(+), 369 deletions(-) create mode 100644 apps/accounts/migrations/0005_usergoal.py create mode 100644 apps/accounts/services/goal_service.py create mode 100644 apps/common/services/ai_service.py delete mode 100644 apps/gmail/migrations/0002_gmailconversation_gmailattachment.py create mode 100644 apps/gmail/migrations/0002_gmailcredential_last_history_id.py create mode 100644 apps/gmail/migrations/0003_gmailconversation_metadata.py create mode 100644 apps/gmail/migrations/0004_conversationsummary.py diff --git a/apps/accounts/migrations/0005_usergoal.py b/apps/accounts/migrations/0005_usergoal.py new file mode 100644 index 0000000..fe40ea4 --- /dev/null +++ b/apps/accounts/migrations/0005_usergoal.py @@ -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'], + }, + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index 74734b4..1eb5748 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -98,3 +98,21 @@ class UserProfile(models.Model): def __str__(self): 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}的目标" diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 35344d7..1e929c1 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from apps.accounts.models import User, UserProfile +from apps.accounts.models import User, UserProfile, UserGoal class UserProfileSerializer(serializers.ModelSerializer): """用户档案序列化器""" @@ -60,4 +60,19 @@ class PasswordChangeSerializer(serializers.Serializer): user = self.context['request'].user if not user.check_password(value): raise serializers.ValidationError("旧密码不正确") - return value \ No newline at end of file + 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 \ No newline at end of file diff --git a/apps/accounts/services/goal_service.py b/apps/accounts/services/goal_service.py new file mode 100644 index 0000000..f59b042 --- /dev/null +++ b/apps/accounts/services/goal_service.py @@ -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) \ No newline at end of file diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 4ef0565..38538e0 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -4,6 +4,7 @@ from apps.accounts.views import ( LoginView, RegisterView, LogoutView, user_profile, change_password, user_detail, user_update, user_delete, verify_token, user_list ) +from .views import UserGoalView, UserGoalDetailView, RecommendedReplyView urlpatterns = [ path('login/', LoginView.as_view(), name='login'), @@ -16,4 +17,7 @@ urlpatterns = [ path('users//', user_detail, name='user_detail'), path('users//update/', user_update, name='user_update'), path('users//delete/', user_delete, name='user_delete'), + path('goals/', UserGoalView.as_view(), name='user_goals'), + path('goals//', UserGoalDetailView.as_view(), name='user_goal_detail'), + path('recommended-reply/', RecommendedReplyView.as_view(), name='recommended_reply'), ] diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f326223..86305eb 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -15,13 +15,18 @@ from django.shortcuts import get_object_or_404 import uuid import logging import traceback -from apps.accounts.models import User +from apps.accounts.models import User, UserGoal from apps.accounts.services.auth_service import ( authenticate_user, create_user, generate_token, delete_token ) from apps.accounts.services.utils import ( 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__) @@ -585,4 +590,272 @@ def user_list(request): 'message': f'获取用户列表失败: {str(e)}', 'data': None }, 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) \ No newline at end of file diff --git a/apps/common/services/ai_service.py b/apps/common/services/ai_service.py new file mode 100644 index 0000000..452eb7e --- /dev/null +++ b/apps/common/services/ai_service.py @@ -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)}" \ No newline at end of file diff --git a/apps/gmail/migrations/0001_initial.py b/apps/gmail/migrations/0001_initial.py index a7dcff2..d241c1f 100644 --- a/apps/gmail/migrations/0001_initial.py +++ b/apps/gmail/migrations/0001_initial.py @@ -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.utils.timezone from django.conf import settings from django.db import migrations, models @@ -14,6 +15,44 @@ class Migration(migrations.Migration): ] 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( name='GmailCredential', fields=[ diff --git a/apps/gmail/migrations/0002_gmailconversation_gmailattachment.py b/apps/gmail/migrations/0002_gmailconversation_gmailattachment.py deleted file mode 100644 index 0707fc7..0000000 --- a/apps/gmail/migrations/0002_gmailconversation_gmailattachment.py +++ /dev/null @@ -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'], - }, - ), - ] diff --git a/apps/gmail/migrations/0002_gmailcredential_last_history_id.py b/apps/gmail/migrations/0002_gmailcredential_last_history_id.py new file mode 100644 index 0000000..79fe075 --- /dev/null +++ b/apps/gmail/migrations/0002_gmailcredential_last_history_id.py @@ -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), + ), + ] diff --git a/apps/gmail/migrations/0003_gmailconversation_metadata.py b/apps/gmail/migrations/0003_gmailconversation_metadata.py new file mode 100644 index 0000000..3c2ac0b --- /dev/null +++ b/apps/gmail/migrations/0003_gmailconversation_metadata.py @@ -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), + ), + ] diff --git a/apps/gmail/migrations/0004_conversationsummary.py b/apps/gmail/migrations/0004_conversationsummary.py new file mode 100644 index 0000000..6f05b3f --- /dev/null +++ b/apps/gmail/migrations/0004_conversationsummary.py @@ -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', + }, + ), + ] diff --git a/apps/gmail/models.py b/apps/gmail/models.py index 058f363..036475a 100644 --- a/apps/gmail/models.py +++ b/apps/gmail/models.py @@ -3,6 +3,7 @@ from apps.accounts.models import User import json import os from django.utils import timezone +import uuid class GmailCredential(models.Model): 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) updated_at = models.DateTimeField(auto_now=True) 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: unique_together = ('user', 'email') @@ -53,6 +55,7 @@ class GmailConversation(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) is_active = models.BooleanField(default=True) + metadata = models.JSONField(default=dict, blank=True, null=True, help_text="存储额外信息,如已处理的消息ID等") def __str__(self): 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)}" class Meta: - ordering = ['-created_at'] \ No newline at end of file + 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} 摘要" \ No newline at end of file diff --git a/apps/gmail/services/gmail_service.py b/apps/gmail/services/gmail_service.py index 2a60b10..30d3786 100644 --- a/apps/gmail/services/gmail_service.py +++ b/apps/gmail/services/gmail_service.py @@ -13,19 +13,20 @@ from googleapiclient.errors import HttpError from django.conf import settings from django.utils import timezone 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.knowledge_base.models import KnowledgeBase import requests -from google.cloud import pubsub_v1 from apps.common.services.notification_service import NotificationService import threading -import time from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication from email.header import Header import mimetypes +from apps.accounts.models import User +from apps.common.services.ai_service import AIService +import traceback # 配置日志记录器 logger = logging.getLogger(__name__) @@ -45,9 +46,9 @@ class GmailService: TOKEN_DIR = os.path.join(settings.BASE_DIR, 'gmail_tokens') # 附件存储目录 ATTACHMENT_DIR = os.path.join(settings.BASE_DIR, 'media', 'gmail_attachments') - # Gmail 监听 Pub/Sub 主题和订阅 - PUBSUB_TOPIC = getattr(settings, 'GMAIL_PUBSUB_TOPIC', 'projects/{project_id}/topics/gmail-notifications') - PUBSUB_SUBSCRIPTION = getattr(settings, 'GMAIL_PUBSUB_SUBSCRIPTION', 'projects/{project_id}/subscriptions/gmail-notifications-sub') + # Gmail 监听 Pub/Sub 主题和订阅(仅后缀) + PUBSUB_TOPIC = getattr(settings, 'GMAIL_PUBSUB_TOPIC', 'gmail-notifications') + PUBSUB_SUBSCRIPTION = getattr(settings, 'GMAIL_PUBSUB_SUBSCRIPTION', 'gmail-notifications-sub') @staticmethod def initiate_authentication(user, client_secret_json): @@ -177,15 +178,32 @@ class GmailService: Exception: 如果凭证无效或创建服务失败。 """ try: + logger.info(f"获取Gmail服务实例,用户: {credential.user.username}, 邮箱: {credential.email}") + # 从数据库凭证中获取 Google API 凭证对象 credentials = credential.get_credentials() + # 检查凭证是否需要刷新 + if credentials.expired: + logger.info(f"OAuth凭证已过期,尝试刷新...") + # 创建 Gmail API 服务,代理通过环境变量自动应用 - return build('gmail', 'v1', credentials=credentials) + service = build('gmail', 'v1', credentials=credentials) + logger.info(f"成功创建Gmail服务实例") + + return service except Exception as e: - # 记录错误并抛出 - logger.error(f"Error creating Gmail service: {str(e)}") + logger.error(f"创建Gmail服务失败: {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 @staticmethod @@ -581,30 +599,50 @@ class GmailService: # 获取Gmail服务 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主题和订阅名称 project_id = getattr(settings, 'GOOGLE_CLOUD_PROJECT_ID', '') if not project_id: return False, "未配置Google Cloud项目ID" - - topic = topic_name or GmailService.PUBSUB_TOPIC.format(project_id=project_id) - subscription = subscription_name or GmailService.PUBSUB_SUBSCRIPTION.format(project_id=project_id) + + # 完整的主题格式必须是:projects/{project_id}/topics/{topic_name} + # 使用已经在Google Cloud Console中创建的主题:gmail-watch-topic + full_topic_name = f'projects/{project_id}/topics/gmail-watch-topic' + logger.info(f"使用主题名称: {full_topic_name}") # 为Gmail账户启用推送通知 request = { 'labelIds': ['INBOX'], - 'topicName': topic + 'topicName': full_topic_name } try: # 先停止现有的监听 service.users().stop(userId='me').execute() logger.info(f"已停止现有的监听: {user_email}") - except: - pass + except Exception as stop_error: + logger.warning(f"停止现有监听失败(可能无监听): {str(stop_error)}") # 启动新的监听 - service.users().watch(userId='me', body=request).execute() - logger.info(f"已为 {user_email} 设置Gmail推送通知,主题: {topic}") + watch_response = service.users().watch(userId='me', body=request).execute() + 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 @@ -613,113 +651,33 @@ class GmailService: return False, f"设置Gmail推送通知失败: {str(e)}" @staticmethod - def start_pubsub_listener(user_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): + def process_new_emails(user, credential, history_id=None): """ 处理新收到的邮件 Args: user: 用户对象 credential: Gmail凭证对象 - history_id: Gmail历史记录ID + history_id: Gmail历史记录ID (可选,如果不提供则使用凭证中的last_history_id) Returns: None """ 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服务 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( user=user, @@ -732,37 +690,172 @@ class GmailService: logger.info(f"用户 {user.username} 没有活跃的Gmail对话") return - # 处理每个历史变更 - for history in history_results.get('history', []): - for message_added in history.get('messagesAdded', []): - message_id = message_added.get('message', {}).get('id') - if not message_id: - continue - - # 获取完整邮件内容 - message = service.users().messages().get(userId='me', id=message_id).execute() - email_data = GmailService._parse_email_content(message) + logger.info(f"找到 {len(influencer_emails)} 个活跃的Gmail对话") + + # 方法1: 通过历史记录获取变更 + try: + logger.info(f"通过历史记录获取变更...") + # 获取历史记录变更,包含所有相关变更类型 + history_results = service.users().history().list( + userId='me', + startHistoryId=history_id, + 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: - continue + # 提取所有消息ID + 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: - # 查找相关对话 - conversation = active_conversations.filter( - influencer_email=email_data['from_email'] - ).first() - - if conversation: - # 将新邮件保存到聊天历史 - GmailService._save_email_to_chat( - user, - credential, - conversation, - email_data - ) + # 避免重复处理 + if message_id in already_processed_ids: + logger.info(f"邮件ID: {message_id} 已处理过,跳过") + continue - # 发送通知 + 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( user=user, title="收到新邮件", @@ -770,115 +863,101 @@ class GmailService: notification_type="gmail", related_object_id=conversation.conversation_id ) - - logger.info(f"已处理来自 {email_data['from_email']} 的新邮件") - - except Exception as e: - logger.error(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'] + except Exception as notif_error: + logger.error(f"发送通知失败: {str(notif_error)}") + else: + logger.error(f"保存邮件到聊天历史失败") + else: + # 找不到对话记录,创建新的 + logger.info(f"未找到与 {email_data['from_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['from_email'], + conversation_id=conversation_id, + title=f"与 {email_data['from_email']} 的Gmail对话", + is_active=True ) - 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() + # 保存邮件到新创建的对话 + 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)}") - 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: - logger.error(f"保存Gmail新邮件到聊天记录失败: {str(e)}") + logger.error(f"处理邮件 {message_id} 时出错: {str(e)}") + import traceback + logger.error(f"错误详情: {traceback.format_exc()}") return False @staticmethod @@ -1080,5 +1159,197 @@ class GmailService: logger.error(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 + \ No newline at end of file diff --git a/apps/gmail/urls.py b/apps/gmail/urls.py index 64ec33d..6c04318 100644 --- a/apps/gmail/urls.py +++ b/apps/gmail/urls.py @@ -7,8 +7,9 @@ from .views import ( GmailConversationView, GmailAttachmentListView, GmailPubSubView, - GmailNotificationStartView, - GmailSendEmailView + GmailSendEmailView, + GmailWebhookView, + GmailConversationSummaryView ) app_name = 'gmail' @@ -22,6 +23,8 @@ urlpatterns = [ path('attachments/', GmailAttachmentListView.as_view(), name='attachment_list'), path('attachments//', GmailAttachmentListView.as_view(), name='attachment_list_by_conversation'), 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('webhook/', GmailWebhookView.as_view(), name='webhook'), + path('conversations/summary/', GmailConversationSummaryView.as_view(), name='conversation_summary_list'), + path('conversations/summary//', GmailConversationSummaryView.as_view(), name='conversation_summary_detail'), ] \ No newline at end of file diff --git a/apps/gmail/views.py b/apps/gmail/views.py index 0fd7f26..add1074 100644 --- a/apps/gmail/views.py +++ b/apps/gmail/views.py @@ -11,6 +11,9 @@ import os from django.conf import settings from django.core.files.storage import default_storage from django.core.files.base import ContentFile +import json +import base64 +import threading # 配置日志记录器,用于记录视图操作的调试、警告和错误信息 logger = logging.getLogger(__name__) @@ -45,14 +48,26 @@ class GmailAuthInitiateView(APIView): # 调用 GmailService 生成授权 URL auth_url = GmailService.initiate_authentication(request.user, client_secret_json) logger.info(f"Generated auth URL for user {request.user.id}") - return Response({'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: # 记录错误并返回服务器错误响应 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}") - 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): @@ -88,14 +103,26 @@ class GmailAuthCompleteView(APIView): # 序列化凭证数据以返回 serializer = GmailCredentialSerializer(credential, context={'request': request}) 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: # 记录错误并返回服务器错误响应 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}") - 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): @@ -121,7 +148,11 @@ class GmailCredentialListView(APIView): credentials = request.user.gmail_credentials.all() # 序列化凭证数据 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): @@ -148,7 +179,11 @@ class GmailCredentialDetailView(APIView): # 获取用户拥有的指定凭证,未找到则返回 404 credential = get_object_or_404(GmailCredential, pk=pk, user=request.user) 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): """ @@ -174,9 +209,17 @@ class GmailCredentialDetailView(APIView): if serializer.validated_data.get('is_default', False): GmailCredential.objects.filter(user=request.user).exclude(id=credential.id).update(is_default=False) 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): """ @@ -196,7 +239,12 @@ class GmailCredentialDetailView(APIView): # 获取并删除用户拥有的指定凭证 credential = get_object_or_404(GmailCredential, pk=pk, user=request.user) 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): """ @@ -301,6 +349,7 @@ class GmailConversationView(APIView): 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + class GmailAttachmentListView(APIView): """ API视图,用于获取Gmail附件列表。 @@ -348,7 +397,7 @@ class GmailAttachmentListView(APIView): 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - + class GmailPubSubView(APIView): """ API视图,用于设置Gmail的Pub/Sub实时通知。 @@ -376,24 +425,44 @@ class GmailPubSubView(APIView): email = request.data.get('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账户的凭证 credential = GmailCredential.objects.filter(user=request.user, email=email).first() 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通知 success, error = GmailService.setup_gmail_push_notification(request.user, email) 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: - 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: 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): """ @@ -422,41 +491,12 @@ class GmailPubSubView(APIView): 'is_default': cred.is_default }) - return Response({'accounts': accounts}, status=status.HTTP_200_OK) - -class GmailNotificationStartView(APIView): - """ - API视图,用于启动Gmail Pub/Sub监听器。 - 通常由系统管理员或后台任务调用,而非普通用户。 - """ - 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) + return Response({ + 'code': 200, + 'message': '获取账户列表成功', + 'data': {'accounts': accounts} + }, status=status.HTTP_200_OK) + class GmailSendEmailView(APIView): """ @@ -498,7 +538,9 @@ class GmailSendEmailView(APIView): # 验证必填字段 if not all([user_email, to_email, subject]): return Response({ - 'error': '缺少必要参数,请提供email、to和subject字段' + 'code': 400, + 'message': '缺少必要参数,请提供email、to和subject字段', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查是否有此Gmail账户的凭证 @@ -510,7 +552,9 @@ class GmailSendEmailView(APIView): if not credential: return Response({ - 'error': f'未找到{user_email}的有效授权信息' + 'code': 404, + 'message': f'未找到{user_email}的有效授权信息', + 'data': None }, status=status.HTTP_404_NOT_FOUND) # 处理附件 @@ -556,18 +600,23 @@ class GmailSendEmailView(APIView): if success: return Response({ + 'code': 200, 'message': '邮件发送成功', - 'message_id': result + 'data': {'message_id': result} }, status=status.HTTP_200_OK) else: return Response({ - 'error': result + 'code': 500, + 'message': result, + 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except Exception as e: logger.error(f"发送Gmail邮件失败: {str(e)}") return Response({ - 'error': f'发送Gmail邮件失败: {str(e)}' + 'code': 500, + 'message': f'发送Gmail邮件失败: {str(e)}', + 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get(self, request): @@ -595,4 +644,177 @@ class GmailSendEmailView(APIView): '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) + diff --git a/daren_project/settings.py b/daren_project/settings.py index f942e5b..5ad3ebd 100644 --- a/daren_project/settings.py +++ b/daren_project/settings.py @@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ from pathlib import Path import pymysql +import os pymysql.install_as_MySQLdb() # 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! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost', '127.0.0.1', '02bf-180-159-100-165.ngrok-free.app'] # Application definition @@ -175,7 +176,7 @@ AUTH_USER_MODEL = 'accounts.User' API_BASE_URL = 'http://81.69.223.133:48329' SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf' -GMAIL_WEBHOOK_URL = 'https://27b3-180-159-100-165.ngrok-free.app/api/user/gmail/webhook/' +GMAIL_WEBHOOK_URL = 'https://02bf-180-159-100-165.ngrok-free.app/api/gmail/webhook/' APPLICATION_ID = 'd5d11efa-ea9a-11ef-9933-0242ac120006' @@ -187,9 +188,10 @@ PROXY_URL = 'http://127.0.0.1:7890' # Gmail Pub/Sub相关设置 -GOOGLE_CLOUD_PROJECT_ID = 'your-project-id' # 替换为您的Google Cloud项目ID -GMAIL_PUBSUB_TOPIC = 'projects/{project_id}/topics/gmail-notifications' -GMAIL_PUBSUB_SUBSCRIPTION = 'projects/{project_id}/subscriptions/gmail-notifications-sub' +GOOGLE_CLOUD_PROJECT_ID = 'knowledge-454905' # 替换为您的Google Cloud项目ID +# 主题名称 +GMAIL_PUBSUB_TOPIC = 'gmail-watch-topic' # 设置允许使用Google Pub/Sub的应用列表 -INSTALLED_APPS += ['google.cloud.pubsub'] \ No newline at end of file +INSTALLED_APPS += ['google.cloud.pubsub'] +