增加gmail模块

This commit is contained in:
wanjia 2025-05-13 18:36:06 +08:00
parent cee0b638e8
commit aeebe645bf
17 changed files with 1585 additions and 369 deletions

View 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'],
},
),
]

View File

@ -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}的目标"

View File

@ -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):
"""用户档案序列化器"""
@ -61,3 +61,18 @@ class PasswordChangeSerializer(serializers.Serializer):
if not user.check_password(value):
raise serializers.ValidationError("旧密码不正确")
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

View 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)

View File

@ -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/<str:pk>/', user_detail, name='user_detail'),
path('users/<str:pk>/update/', user_update, name='user_update'),
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'),
]

View File

@ -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__)
@ -586,3 +591,271 @@ def user_list(request):
'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)

View 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)}"

View File

@ -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=[

View File

@ -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'],
},
),
]

View File

@ -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),
),
]

View 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),
),
]

View 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',
},
),
]

View File

@ -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}"
@ -88,3 +91,20 @@ class GmailAttachment(models.Model):
class Meta:
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} 摘要"

View File

@ -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
# 处理每个历史变更
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', []))} 条历史变更记录")
# 提取所有消息ID
message_ids = set()
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:
# 检查不同类型的变更
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 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:
continue
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}")
# 将新邮件保存到聊天历史
GmailService._save_email_to_chat(
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
except Exception as notif_error:
logger.error(f"发送通知失败: {str(notif_error)}")
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(
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,
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'
}
user_email=credential.email,
influencer_email=email_data['from_email'],
conversation_id=conversation_id,
title=f"{email_data['from_email']} 的Gmail对话",
is_active=True
)
# 更新对话的同步时间
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(
# 保存邮件到新创建的对话
success = GmailService._save_email_to_chat(
user,
credential,
email_data['id'],
attachment['attachmentId'],
attachment['filename']
conversation,
email_data
)
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)
if success:
logger.info(f"成功保存邮件到新创建的对话")
saved = True
except Exception as create_error:
logger.error(f"创建新对话失败: {str(create_error)}")
# 场景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
)
# 更新聊天消息,添加附件信息
metadata = chat_message.metadata or {}
if 'attachments' not in metadata:
metadata['attachments'] = []
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
)
metadata['attachments'].append({
'id': str(gmail_attachment.id),
'filename': attachment['filename'],
'size': attachment['size'],
'mime_type': attachment['mimeType'],
'url': gmail_attachment.get_absolute_url()
})
# 保存邮件到新创建的对话
success = GmailService._save_email_to_chat(
user,
credential,
conversation,
email_data
)
chat_message.metadata = metadata
chat_message.save()
if success:
logger.info(f"成功保存邮件到新创建的对话")
saved = True
except Exception as create_error:
logger.error(f"创建新对话失败: {str(create_error)}")
return True
# 场景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

View File

@ -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/<str:conversation_id>/', 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/<str:conversation_id>/', GmailConversationSummaryView.as_view(), name='conversation_summary_detail'),
]

View File

@ -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附件列表
@ -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)
return Response({
'code': 200,
'message': '获取账户列表成功',
'data': {'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)
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)

View File

@ -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']