diff --git a/apps/gmail/README_GMAIL.md b/apps/gmail/README_GMAIL.md new file mode 100644 index 0000000..7dc7120 --- /dev/null +++ b/apps/gmail/README_GMAIL.md @@ -0,0 +1,468 @@ +# Gmail模块API接口文档 - Apifox测试版 + +## 1. 认证管理 + +### 1.1 启动OAuth授权 + +- **接口URL**: `/api/gmail/auth/initiate/` +- **请求方式**: `POST` +- **接口描述**: 生成Google OAuth授权URL,供用户访问并获取授权码 + +#### 请求参数 +```json +{ + "client_secret_json": "String (必填,二选一): Google OAuth客户端凭证(JSON格式字符串)", + "client_secret_file": "File (必填,二选一): 上传的客户端凭证JSON文件" +} +``` + +#### 响应参数 +```json +{ + "auth_url": "String: 用户需访问的授权URL", + "status": "String: 接口调用状态" +} +``` + +#### 接口作用 +生成Google OAuth授权链接,用户通过访问此链接授予应用访问Gmail账户的权限,并获取授权码用于后续步骤。 + +--- + +### 1.2 完成OAuth授权 + +- **接口URL**: `/api/gmail/auth/complete/` +- **请求方式**: `POST` +- **接口描述**: 使用授权码完成OAuth流程,获取并保存Gmail访问凭证 + +#### 请求参数 +```json +{ + "client_secret_json": "String (必填): Google OAuth客户端凭证(JSON格式)", + "auth_code": "String (必填): 用户从授权URL获得的授权码" +} +``` + +#### 响应参数 +```json +{ + "id": "Integer: 凭证ID", + "email": "String: 关联的Gmail邮箱地址", + "is_default": "Boolean: 是否为默认账户", + "created_at": "DateTime: 创建时间", + "status": "String: 操作状态" +} +``` + +#### 接口作用 +完成OAuth2.0授权流程,将用户的授权码转换为访问令牌,并将凭证保存到系统中,以便后续API调用。 + +--- + +### 1.3 获取Gmail凭证列表 + +- **接口URL**: `/api/gmail/credentials/` +- **请求方式**: `GET` +- **接口描述**: 获取系统中所有已授权的Gmail账户凭证列表 + +#### 响应参数 +```json +{ + "results": [ + { + "id": "Integer: 凭证ID", + "email": "String: Gmail邮箱地址", + "is_default": "Boolean: 是否为默认账户", + "created_at": "DateTime: 创建时间" + } + ], + "count": "Integer: 总记录数" +} +``` + +#### 接口作用 +列出系统中所有已授权的Gmail账户,便于用户查看和管理多个Gmail账户。 + +--- + +### 1.4 设置默认Gmail账户 + +- **接口URL**: `/api/gmail/credentials/{id}/set_default/` +- **请求方式**: `POST` +- **接口描述**: 将指定ID的Gmail账户设置为默认账户 + +#### 路径参数 +```json +{ + "id": "Integer (必填): 凭证ID" +} +``` + +#### 响应参数 +```json +{ + "status": "String: 操作状态", + "message": "String: 操作结果信息" +} +``` + +#### 接口作用 +设置系统默认使用的Gmail账户,当未指定账户时,系统操作将使用此默认账户。 + +--- + +## 2. 对话管理 + +### 2.1 创建Gmail对话 + +- **接口URL**: `/api/gmail/conversations/` +- **请求方式**: `POST` +- **接口描述**: 创建并保存用户与特定联系人的邮件对话 + +#### 请求参数 +```json +{ + "user_email": "String (必填): 用户Gmail邮箱", + "influencer_email": "String (必填): 对话方Gmail邮箱", + "kb_id": "Integer (可选): 关联的知识库ID" +} +``` + +#### 响应参数 +```json +{ + "conversation_id": "Integer: 创建的会话ID", + "user_email": "String: 用户Gmail邮箱", + "influencer_email": "String: 对话方Gmail邮箱", + "created_at": "DateTime: 创建时间", + "status": "String: 操作状态" +} +``` + +#### 接口作用 +创建并保存特定邮箱之间的对话关系,为后续邮件往来、自动回复和内容分析建立基础。 + +--- + +### 2.2 获取对话列表 + +- **接口URL**: `/api/gmail/conversations/` +- **请求方式**: `GET` +- **接口描述**: 获取所有保存的Gmail邮件对话 + +#### 查询参数 +```json +{ + "user_email": "String (可选): 筛选特定用户的对话", + "influencer_email": "String (可选): 筛选与特定联系人的对话" +} +``` + +#### 响应参数 +```json +{ + "results": [ + { + "id": "Integer: 对话ID", + "user_email": "String: 用户Gmail邮箱", + "influencer_email": "String: 对话方Gmail邮箱", + "last_message_time": "DateTime: 最后消息时间", + "message_count": "Integer: 消息数量" + } + ], + "count": "Integer: 总记录数" +} +``` + +#### 接口作用 +查询系统中保存的所有Gmail对话,可通过筛选参数查找特定用户或联系人的对话记录。 + +--- + +### 2.3 获取对话摘要 + +- **接口URL**: `/api/gmail/conversations/summary/{conversation_id}/` +- **请求方式**: `GET` +- **接口描述**: 获取特定对话的AI生成摘要信息 + +#### 路径参数 +```json +{ + "conversation_id": "Integer (必填): 对话ID" +} +``` + +#### 响应参数 +```json +{ + "conversation_id": "Integer: 对话ID", + "summary": "String: AI生成的对话摘要", + "key_points": ["String: 关键点1", "String: 关键点2"], + "sentiment": "String: 情感分析结果", + "last_updated": "DateTime: 最后更新时间" +} +``` + +#### 接口作用 +使用AI技术自动分析对话内容,生成对话摘要、提取关键点并进行情感分析,帮助用户快速了解对话内容和进展。 + +--- + +## 3. 邮件管理 + +### 3.1 发送邮件 + +- **接口URL**: `/api/gmail/send/` +- **请求方式**: `POST` +- **接口描述**: 通过指定Gmail账户发送邮件 + +#### 请求参数 +```json +{ + "email": "String (必填): 发件人Gmail邮箱", + "to": "String (必填): 收件人邮箱,多个收件人用逗号分隔", + "subject": "String (必填): 邮件主题", + "body": "String (必填): 邮件正文内容(支持HTML)", + "cc": "String (可选): 抄送邮箱,多个用逗号分隔", + "bcc": "String (可选): 密送邮箱,多个用逗号分隔", + "attachments": ["String: 附件ID1", "String: 附件ID2"] +} +``` + +#### 响应参数 +```json +{ + "message_id": "String: 发送的邮件ID", + "thread_id": "String: Gmail线程ID", + "status": "String: 发送状态", + "timestamp": "DateTime: 发送时间戳" +} +``` + +#### 接口作用 +使用已授权的Gmail账户发送邮件,支持HTML内容、抄送、密送和附件,返回发送状态和邮件ID。 + +--- + +### 3.2 获取附件列表 + +- **接口URL**: `/api/gmail/attachments/{conversation_id}/` +- **请求方式**: `GET` +- **接口描述**: 获取特定对话中的所有附件 + +#### 路径参数 +```json +{ + "conversation_id": "Integer (必填): 对话ID" +} +``` + +#### 响应参数 +```json +{ + "results": [ + { + "id": "String: 附件ID", + "filename": "String: 文件名", + "size": "Integer: 文件大小(字节)", + "mime_type": "String: MIME类型", + "message_id": "String: 所属邮件ID", + "created_at": "DateTime: 创建时间" + } + ], + "count": "Integer: 总记录数" +} +``` + +#### 接口作用 +检索特定对话中的所有邮件附件信息,包括文件名、大小和类型,便于管理和下载附件。 + +--- + +## 4. 推送通知 + +### 4.1 设置Gmail推送通知 + +- **接口URL**: `/api/gmail/notifications/setup/` +- **请求方式**: `POST` +- **接口描述**: 为指定Gmail账户设置实时邮件推送通知 + +#### 请求参数 +```json +{ + "email": "String (必填): 要监听的Gmail邮箱地址", + "topic_name": "String (可选): 自定义Pub/Sub主题名称" +} +``` + +#### 响应参数 +```json +{ + "status": "String: 设置状态", + "topic": "String: Pub/Sub主题名称", + "expiration": "DateTime: 通知过期时间", + "message": "String: 操作结果信息" +} +``` + +#### 接口作用 +使用Google Pub/Sub为Gmail账户设置实时通知,当有新邮件时,系统会接收到推送通知,从而实现邮件的实时处理。 + +--- + +### 4.2 接收Gmail推送通知 + +- **接口URL**: `/api/gmail/webhook/` +- **请求方式**: `POST` +- **接口描述**: 接收Google Pub/Sub推送的Gmail新邮件通知 + +#### 请求参数 +```json +{ + "message": { + "data": "String (必填): Base64编码的通知数据", + "attributes": "Object (可选): 消息属性" + }, + "subscription": "String (必填): 订阅名称" +} +``` + +#### 响应参数 +```json +{ + "status": "String: 处理状态", + "received": "Boolean: 是否成功接收" +} +``` + +#### 接口作用 +作为Webhook端点接收Google Pub/Sub推送的Gmail新邮件通知,解析通知内容并触发相应的处理逻辑,如自动回复和更新对话。 + +--- + +## 5. 自动回复功能 + +### 5.1 设置对话目标 + +- **接口URL**: `/api/gmail/goals/` +- **请求方式**: `POST` +- **接口描述**: 为特定对话设置AI自动回复的目标 + +#### 请求参数 +```json +{ + "conversation_id": "Integer (必填): 对话ID", + "goal_description": "String (必填): 目标描述文本", + "priority": "Integer (可选): 优先级(1-5)", + "auto_reply": "Boolean (可选): 是否启用自动回复" +} +``` + +#### 响应参数 +```json +{ + "id": "Integer: 目标ID", + "conversation_id": "Integer: 对话ID", + "goal_description": "String: 目标描述", + "priority": "Integer: 优先级", + "auto_reply": "Boolean: 是否启用自动回复", + "created_at": "DateTime: 创建时间", + "status": "String: 操作状态" +} +``` + +#### 接口作用 +为指定的Gmail对话设置AI处理目标,系统会根据目标生成推荐回复或自动回复邮件,帮助用户实现特定的沟通目标。 + +--- + +### 5.2 获取对话目标列表 + +- **接口URL**: `/api/gmail/goals/` +- **请求方式**: `GET` +- **接口描述**: 获取所有设置的对话目标 + +#### 查询参数 +```json +{ + "conversation_id": "Integer (可选): 筛选特定对话的目标", + "active": "Boolean (可选): 筛选活跃/非活跃目标" +} +``` + +#### 响应参数 +```json +{ + "results": [ + { + "id": "Integer: 目标ID", + "conversation_id": "Integer: 对话ID", + "goal_description": "String: 目标描述", + "priority": "Integer: 优先级", + "auto_reply": "Boolean: 是否启用自动回复", + "created_at": "DateTime: 创建时间" + } + ], + "count": "Integer: 总记录数" +} +``` + +#### 接口作用 +获取系统中设置的所有对话目标,可通过参数筛选特定对话的目标或活跃/非活跃目标。 + +--- + +### 5.3 获取推荐回复 + +- **接口URL**: `/api/gmail/recommended-reply/` +- **请求方式**: `POST` +- **接口描述**: 获取针对特定对话的AI推荐回复内容 + +#### 请求参数 +```json +{ + "conversation_id": "Integer (必填): 对话ID", + "last_message_id": "String (可选): 最后接收的邮件ID", + "custom_instructions": "String (可选): 自定义回复指示" +} +``` + +#### 响应参数 +```json +{ + "goal": { + "id": "Integer: 目标ID", + "description": "String: 目标描述" + }, + "conversation_summary": "String: 对话摘要", + "last_message": { + "sender": "String: 发送者", + "subject": "String: 主题", + "content": "String: 内容摘要" + }, + "recommended_reply": "String: AI推荐的回复内容", + "alternative_replies": ["String: 备选回复1", "String: 备选回复2"] +} +``` + +#### 接口作用 +基于对话历史、设定目标和最新邮件内容,使用AI生成推荐回复,帮助用户快速回应邮件并实现沟通目标。 + +--- + +## 使用说明 + +1. **授权流程**: + - 先调用"启动OAuth授权"接口获取授权URL + - 用户访问URL并获取授权码 + - 调用"完成OAuth授权"接口保存Gmail账户凭证 + +2. **实时通知设置**: + - 授权成功后调用"设置Gmail推送通知"接口 + - 确保webhook端点可从公网访问 + +3. **自动回复流程**: + - 创建对话并设置目标 + - 系统接收新邮件推送通知 + - 自动分析内容并生成回复 + - 根据设置决定是推荐回复还是自动回复 \ No newline at end of file diff --git a/apps/gmail/services/__init__.py b/apps/gmail/services/__init__.py index b28b04f..e69de29 100644 --- a/apps/gmail/services/__init__.py +++ b/apps/gmail/services/__init__.py @@ -1,3 +0,0 @@ - - - diff --git a/apps/gmail/services/export_service.py b/apps/gmail/services/export_service.py new file mode 100644 index 0000000..49bd131 --- /dev/null +++ b/apps/gmail/services/export_service.py @@ -0,0 +1,128 @@ +import os +import logging +import pandas as pd +from datetime import datetime +from django.db.models import Q, F +from django.utils import timezone +from apps.gmail.models import GmailConversation, GmailAttachment, ConversationSummary +from apps.chat.models import ChatHistory + +logger = logging.getLogger(__name__) + +class GmailExportService: + """ + Gmail导出服务,提供将达人回复导出为Excel的功能 + """ + + @staticmethod + def export_replied_influencers(user, format='xlsx'): + """ + 导出已回复的达人Gmail列表 + + Args: + user: 用户对象 + format: 导出格式, 默认为xlsx + + Returns: + tuple: (文件路径, 错误信息) + """ + try: + # 获取用户所有的Gmail对话 + conversations = GmailConversation.objects.filter( + user=user, + is_active=True + ) + + if not conversations.exists(): + return None, "未找到任何Gmail对话" + + # 准备导出数据 + export_data = [] + + for conversation in conversations: + # 查询达人是否有回复 + chat_history = ChatHistory.objects.filter( + user=user, + conversation_id=conversation.conversation_id, + metadata__contains={"from": conversation.influencer_email} + ).order_by('created_at') + + # 如果达人有回复,则添加到导出列表 + if chat_history.exists(): + # 获取第一条回复的时间 + first_reply = chat_history.first() + first_reply_time = first_reply.created_at.replace(tzinfo=None) if first_reply else None + + # 获取最后一条回复的时间 + last_reply = chat_history.last() + last_reply_time = last_reply.created_at.replace(tzinfo=None) if last_reply else None + + # 获取回复次数 + reply_count = chat_history.count() + + # 直接从数据库中获取对话摘要,不调用外部API + summary = "" + try: + # 先从ConversationSummary模型中查找 + conversation_summary = ConversationSummary.objects.filter( + conversation=conversation + ).first() + + if conversation_summary: + summary = conversation_summary.content + else: + # 如果没有摘要,尝试获取最新的几条消息内容 + recent_messages = ChatHistory.objects.filter( + user=user, + conversation_id=conversation.conversation_id + ).order_by('-created_at')[:5] + + if recent_messages: + summary = "最近消息: " + " | ".join([msg.content[:100] + ("..." if len(msg.content) > 100 else "") for msg in recent_messages]) + except Exception as e: + logger.error(f"获取对话摘要失败: {str(e)}") + summary = '无法获取摘要' + + # 构建导出记录 - 注意移除时区信息 + export_record = { + '用户邮箱': conversation.user_email, + '达人邮箱': conversation.influencer_email, + '对话标题': conversation.title, + '对话开始时间': conversation.created_at.replace(tzinfo=None), + '首次回复时间': first_reply_time, + '最近回复时间': last_reply_time, + '回复次数': reply_count, + '对话摘要': summary, + '对话ID': conversation.conversation_id + } + + export_data.append(export_record) + + if not export_data: + return None, "没有找到已回复的达人" + + # 创建DataFrame + df = pd.DataFrame(export_data) + + # 生成文件名和路径 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + file_name = f"influencer_replies_{timestamp}.{format}" + export_dir = os.path.join('media', 'exports') + os.makedirs(export_dir, exist_ok=True) + file_path = os.path.join(export_dir, file_name) + + # 根据格式导出 + if format == 'xlsx': + df.to_excel(file_path, index=False, engine='openpyxl') + elif format == 'csv': + df.to_csv(file_path, index=False, encoding='utf-8-sig') + else: + return None, f"不支持的导出格式: {format}" + + return file_path, None + + except Exception as e: + logger.error(f"导出已回复达人失败: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return None, f"导出失败: {str(e)}" \ No newline at end of file diff --git a/apps/gmail/urls.py b/apps/gmail/urls.py index 3c1047a..30d52f7 100644 --- a/apps/gmail/urls.py +++ b/apps/gmail/urls.py @@ -11,7 +11,8 @@ from .views import ( GmailConversationSummaryView, GmailGoalView, SimpleRecommendedReplyView, - GmailCredentialViewSet + GmailCredentialViewSet, + GmailExportView ) app_name = 'gmail' @@ -34,6 +35,7 @@ urlpatterns = [ path('conversations/summary//', GmailConversationSummaryView.as_view(), name='conversation_summary_detail'), path('goals/', GmailGoalView.as_view(), name='user_goals'), path('recommended-reply/', SimpleRecommendedReplyView.as_view(), name='recommended_reply'), + path('export/', GmailExportView.as_view(), name='export_replied_influencers'), # 包含Router生成的URL path('', include(router.urls)), ] \ No newline at end of file diff --git a/apps/gmail/views.py b/apps/gmail/views.py index a5cf10e..4efa10a 100644 --- a/apps/gmail/views.py +++ b/apps/gmail/views.py @@ -1393,3 +1393,79 @@ class SimpleRecommendedReplyView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +class GmailExportView(APIView): + """ + API视图,用于导出已回复的达人Gmail列表为Excel文件。 + """ + permission_classes = [IsAuthenticated] # 限制访问,仅允许已认证用户 + + def get(self, request, format=None): + """ + 处理GET请求,导出已回复的达人Gmail列表。 + + Query参数: + - format: 导出格式,支持 'xlsx' 或 'csv',默认为 'xlsx' + + Returns: + HttpResponse: 包含Excel文件的响应(成功时),或错误消息的JSON响应(失败时)。 + """ + try: + # 获取导出格式参数 + export_format = request.query_params.get('format', 'xlsx') + if export_format not in ['xlsx', 'csv']: + return Response({ + 'code': 400, + 'message': f'不支持的导出格式: {export_format},支持的格式有: xlsx, csv', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + logger.info(f"用户 {request.user.username} 请求导出已回复达人列表,格式: {export_format}") + + # 直接从具体文件导入而不依赖__init__.py + from .services.export_service import GmailExportService + file_path, error = GmailExportService.export_replied_influencers(request.user, format=export_format) + + if error: + logger.error(f"导出失败: {error}") + return Response({ + 'code': 500, + 'message': error, + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + if not file_path or not os.path.exists(file_path): + logger.error(f"导出文件不存在: {file_path}") + return Response({ + 'code': 500, + 'message': '导出文件生成失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # 读取生成的文件 + with open(file_path, 'rb') as f: + file_data = f.read() + + # 设置响应头,启用文件下载 + response = HttpResponse( + file_data, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' if export_format == 'xlsx' else 'text/csv' + ) + filename = os.path.basename(file_path) + response['Content-Disposition'] = f'attachment; filename="{filename}"' + + logger.info(f"成功导出文件: {filename}") + + return response + + except Exception as e: + logger.error(f"导出过程中发生错误: {str(e)}") + error_details = traceback.format_exc() + return Response({ + 'code': 500, + 'message': f'导出失败: {str(e)}', + 'data': { + 'details': error_details[:500] + } + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +