From d3ca3ef85fbb1f801f338848f7a21b261a1a770a Mon Sep 17 00:00:00 2001 From: wanjia Date: Thu, 17 Apr 2025 16:14:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=8E=88=E6=9D=83=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feishu/API_README.md | 335 +++++ feishu/README_feishu_ai_chat.md | 104 ++ feishu/feishu_ai_chat.py | 1189 +++++++++++++++++ gmail/attachments/test2.txt | 10 - gmail/client_secret.json | 1 - gmail/email_conversations.json | 106 +- gmail/email_conversations.txt | 92 +- gmail/gmail_read.py | 146 -- gmail/requirements.txt | 3 - gmail/storage.json | 2 +- gmail/test.txt | 1 - ..._2290c2f1-06fc-42ef-9c5f-234d6a2754b7.json | 1 + ..._a59c7c43-904a-4bab-9147-492137f8114f.json | 1 + role_based_system/urls.py | 9 +- user_management/consumers.py | 16 +- user_management/feishu_chat_views.py | 434 ++++++ user_management/gmail_integration.py | 172 ++- user_management/urls.py | 12 + user_management/views.py | 298 +++++ 19 files changed, 2693 insertions(+), 239 deletions(-) create mode 100644 feishu/API_README.md create mode 100644 feishu/README_feishu_ai_chat.md create mode 100644 feishu/feishu_ai_chat.py delete mode 100644 gmail/attachments/test2.txt delete mode 100644 gmail/client_secret.json delete mode 100644 gmail/gmail_read.py delete mode 100644 gmail/requirements.txt delete mode 100644 gmail/test.txt create mode 100644 gmail_tokens/gmail_token_2290c2f1-06fc-42ef-9c5f-234d6a2754b7.json create mode 100644 gmail_tokens/gmail_token_a59c7c43-904a-4bab-9147-492137f8114f.json create mode 100644 user_management/feishu_chat_views.py diff --git a/feishu/API_README.md b/feishu/API_README.md new file mode 100644 index 0000000..9752c27 --- /dev/null +++ b/feishu/API_README.md @@ -0,0 +1,335 @@ +# 飞书多维表格自动AI对话 API接口文档 + +本文档描述了飞书多维表格自动AI对话功能的REST API接口。 + +## 基本信息 + +- 基础URL: `/api/feishu/` +- 认证方式: JWT Token (需要在请求头中添加 `Authorization: Bearer `) +- 请求方式: POST/GET +- 返回格式: JSON + +## 注意事项 + +1. 请确保使用正确的URL路径:完整路径为 `/api/feishu/process-table/` 而非 `/api/v1/feishu/process-table/` +2. 飞书授权令牌有效期较短,为了避免授权问题,请在请求中提供正确的 `app_id` 和 `app_secret` 参数: + ```json + { + "table_id": "your_table_id", + "view_id": "your_view_id", + "app_id": "cli_a5c97daacb9e500d", + "app_secret": "fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y" + } + ``` +3. 查看运行日志中的错误信息,根据错误代码进行故障排除 + +## 接口列表 + +### 1. 处理飞书表格数据 + +从飞书多维表格读取数据,处理重复邮箱,可选择性地进行自动对话。 + +- **URL**: `/api/feishu/process-table/` +- **方法**: `POST` +- **权限**: 仅限组长角色 +- **请求参数**: + +| 参数名 | 类型 | 必需 | 描述 | +|--------|------|------|------| +| table_id | string | 是 | 表格ID | +| view_id | string | 是 | 视图ID | +| app_token | string | 否 | 飞书应用TOKEN | +| access_token | string | 否 | 用户访问令牌 | +| goal_template | string | 否 | 目标内容模板 | +| auto_chat | boolean | 否 | 是否自动执行AI对话 | +| turns | integer | 否 | 自动对话轮次 | + +- **请求示例**: + +```json +{ + "table_id": "tbl3oikG3F8YYtVA", + "view_id": "vewSOIsmxc", + "app_id": "cli_a5c97daacb9e500d", + "app_secret": "fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y", + "goal_template": "与达人{handle}(邮箱:{email})建立联系,最终目标是达成合作。", + "auto_chat": true, + "turns": 5 +} +``` + +- **成功响应**: + +```json +{ + "status": "success", + "records_count": 100, + "duplicate_emails_count": 5, + "processing_results": { + "total": 5, + "success": 4, + "failure": 1, + "details": [ + { + "email": "example1@gmail.com", + "handle": "example1", + "status": "success" + }, + // ...其他处理结果 + ] + }, + "chat_results": [ + { + "email": "example1@gmail.com", + "result": { + "status": "success", + "turns_completed": 5, + "goal_achieved": true, + "summary_status": "success", + "conversation_id": "feishu_ai_12345" + } + } + // ...其他自动对话结果 + ] +} +``` + +### 2. 执行自动对话 + +为指定邮箱的达人执行自动对话,支持真实邮件发送和接收。 + +- **URL**: `/api/feishu/auto-chat/` +- **方法**: `POST` +- **权限**: 仅限组长角色 +- **请求参数**: + +| 参数名 | 类型 | 必需 | 描述 | +|--------|------|------|------| +| email | string | 是 | 达人邮箱 | +| force_send | boolean | 否 | 是否强制发送新邮件(即使没有新回复) | +| subject | string | 否 | 邮件主题(仅当force_send=true时使用) | +| content | string | 否 | 邮件内容(仅当force_send=true时使用) | + +- **请求示例 (自动回复)**: + +```json +{ + "email": "example@gmail.com" +} +``` + +- **请求示例 (强制发送)**: + +```json +{ + "email": "example@gmail.com", + "force_send": true, + "subject": "关于合作细节的讨论", + "content": "您好,\n\n针对您提出的问题,我们可以提供以下方案...\n\n期待您的回复。\n\n祝好,\n运营团队" +} +``` + +- **成功响应**: + +```json +{ + "status": "success", + "email_sent": true, + "turns_completed": 1, + "goal_achieved": false, + "summary_status": "not_needed", + "conversation_id": "feishu_ai_12345" +} +``` + +- **待回复响应** (没有检测到新的达人回复): + +```json +{ + "status": "success", + "message": "没有新的达人回复,不需要回复", + "turns_completed": 0, + "goal_achieved": false, + "email_sent": false, + "conversation_id": "feishu_ai_12345" +} +``` + +### 3. 设置用户总目标 + +设置针对特定达人的对话总目标。 + +- **URL**: `/api/feishu/user-goal/` +- **方法**: `POST` +- **权限**: 仅限组长角色 +- **请求参数**: + +| 参数名 | 类型 | 必需 | 描述 | +|--------|------|------|------| +| email | string | 是 | 达人邮箱 | +| goal | string | 是 | 目标内容 | + +- **请求示例**: + +```json +{ + "email": "example@gmail.com", + "goal": "与达人建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。" +} +``` + +- **成功响应**: + +```json +{ + "status": "success", + "action": "create", + "goal": { + "id": "12345", + "content": "与达人建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。", + "created_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-01 12:00:00" + } +} +``` + +### 4. 获取用户总目标 + +获取当前用户的总目标设置。 + +- **URL**: `/api/feishu/user-goal/` +- **方法**: `GET` +- **权限**: 仅限组长角色 +- **请求参数**: 无 + +- **成功响应**: + +```json +{ + "status": "success", + "action": "retrieve", + "goal": { + "id": "12345", + "content": "与达人建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。", + "created_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-01 12:00:00" + } +} +``` + +### 5. 检查目标完成状态 + +检查与特定达人的对话是否已达成目标。 + +- **URL**: `/api/feishu/check-goal/` +- **方法**: `GET` +- **权限**: 仅限组长角色 +- **请求参数**: + +| 参数名 | 类型 | 必需 | 描述 | +|--------|------|------|------| +| email | string | 是 | 达人邮箱 | + +- **请求示例**: + +``` +GET /api/feishu/check-goal/?email=example@gmail.com +``` + +- **成功响应**: + +```json +{ + "status": "success", + "email": "example@gmail.com", + "goal_achieved": true, + "last_message_time": "2024-01-01 12:00:00", + "last_message": "好的,我们已经确认了合作细节。期待与您的成功合作![目标已达成]", + "summary": "本次对话主要讨论了合作条款和细节。达成了产品发布时间、价格区间、佣金比例等共识。双方已同意合作,下一步将签署正式合同。" +} +``` + +## 错误响应 + +所有接口在出错时会返回统一格式的错误信息: + +```json +{ + "error": "错误描述信息" +} +``` + +常见HTTP状态码: + +- 400: 请求参数错误 +- 403: 权限不足 +- 404: 资源不存在 +- 500: 服务器内部错误 + +## 使用示例 + +### 使用curl命令 + +```bash +# 获取授权令牌 +TOKEN=$(curl -s -X POST http://yourserver.com/api/auth/login/ -d '{"username":"admin","password":"yourpassword"}' -H "Content-Type: application/json" | jq -r '.token') + +# 处理飞书表格数据 +curl -X POST http://yourserver.com/api/feishu/process-table/ \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "table_id":"tbl3oikG3F8YYtVA", + "view_id":"vewSOIsmxc", + "app_id":"cli_a5c97daacb9e500d", + "app_secret":"fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y", + "auto_chat":true + }' + +# 检查目标完成状态 +curl -X GET "http://yourserver.com/api/feishu/check-goal/?email=example@gmail.com" \ + -H "Authorization: Bearer $TOKEN" +``` + +### 使用JavaScript + +```javascript +// 获取授权令牌 +async function getToken() { + const response = await fetch('http://yourserver.com/api/auth/login/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: 'admin', + password: 'yourpassword' + }), + }); + const data = await response.json(); + return data.token; +} + +// 处理飞书表格数据 +async function processFeishuTable() { + const token = await getToken(); + const response = await fetch('http://yourserver.com/api/feishu/process-table/', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + table_id: 'tbl3oikG3F8YYtVA', + view_id: 'vewSOIsmxc', + app_id: 'cli_a5c97daacb9e500d', + app_secret: 'fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y', + auto_chat: true + }), + }); + const data = await response.json(); + console.log(data); +} + +processFeishuTable(); +``` \ No newline at end of file diff --git a/feishu/README_feishu_ai_chat.md b/feishu/README_feishu_ai_chat.md new file mode 100644 index 0000000..47776f0 --- /dev/null +++ b/feishu/README_feishu_ai_chat.md @@ -0,0 +1,104 @@ +# 飞书多维表格自动AI对话工具 + +这是一个基于飞书多维表格数据的自动化AI对话工具,可以通过读取飞书表格数据,自动与达人进行实际的电子邮件对话。 + +## 主要功能 + +1. **读取飞书多维表格数据**:从指定的飞书表格获取达人信息 +2. **检测重复邮箱**:自动识别表格中的重复邮箱记录 +3. **创建知识库**:为每位达人自动创建知识库,存储历史对话内容 +4. **设置用户总目标**:针对每位达人设置对话目标 +5. **自动化AI对话**: + - 发送初始邮件给达人 + - 监听达人回复 + - 生成智能回复并通过Gmail发送 + - 检测目标达成状态 +6. **发送提醒**:当对话目标达成时,自动通知团队领导 + +## 核心优势 + +- **真实邮件互动**:通过Gmail实际发送和接收邮件,与达人进行真实互动 +- **智能回复生成**:使用DeepSeek AI生成专业、有针对性的回复 +- **目标导向**:所有对话都围绕预设目标进行,提高对话效率 +- **自动化处理**:减少人工干预,自动化处理大量达人沟通 +- **灵活控制**:支持强制发送模式,可随时人工介入 + +## 使用方法 + +### 1. 处理飞书表格 + +```bash +# 从飞书多维表格读取数据并处理重复邮箱 +python feishu_ai_chat.py process_table --table_id tbl3oikG3F8YYtVA --view_id vewSOIsmxc --app_id cli_a5c97daacb9e500d --app_secret fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y + +# 自动处理并启动对话 +python feishu_ai_chat.py process_table --table_id tbl3oikG3F8YYtVA --view_id vewSOIsmxc --auto_chat +``` + +### 2. 执行自动对话 + +```bash +# 检查新回复并回应 +python feishu_ai_chat.py auto_chat --email example@gmail.com + +# 强制发送新邮件 +python feishu_ai_chat.py auto_chat --email example@gmail.com --force_send --content "自定义邮件内容" +``` + +### 3. 设置用户总目标 + +```bash +python feishu_ai_chat.py set_goal --email example@gmail.com --goal "与达人建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。" +``` + +### 4. 检查目标完成状态 + +```bash +python feishu_ai_chat.py check_goal --email example@gmail.com +``` + +## API 使用方法 + +系统提供RESTful API接口,详见 [API_README.md](API_README.md)。 + +## 实际对话流程 + +1. **初始化阶段**: + - 系统读取飞书表格数据 + - 识别达人邮箱并创建知识库 + - 设置对话总目标 + +2. **首次对话**: + - 系统发送第一封邮件给达人 + - 邮件内容包含简短介绍和合作意向 + +3. **等待回复**: + - 系统监听达人回复 + - 当收到回复后,自动同步到知识库 + +4. **自动回复**: + - 分析达人回复内容 + - 生成智能回复 + - 通过Gmail发送回复邮件 + +5. **目标检测**: + - 持续检测对话是否达成预设目标 + - 当达成目标时发送通知 + +6. **对话总结**: + - 生成对话总结,包括关键点和后续行动项 + +## 注意事项 + +1. 使用前请确保已配置Gmail集成和正确的API密钥 +2. 达人的邮件回复可能需要一定时间,建议设置定时任务检查新回复 +3. 在自动对话过程中,可以随时通过强制发送模式介入对话 +4. 对话总目标应该明确具体,这将影响AI生成回复的质量 +5. 执行自动对话前,请确保飞书表格中的达人邮箱准确 + +## 技术说明 + +- 使用飞书开放平台API读取多维表格数据 +- 通过Gmail API实现邮件发送和接收 +- 利用DeepSeek API生成智能回复和对话总结 +- Django框架提供Web API接口 \ No newline at end of file diff --git a/feishu/feishu_ai_chat.py b/feishu/feishu_ai_chat.py new file mode 100644 index 0000000..9aa1187 --- /dev/null +++ b/feishu/feishu_ai_chat.py @@ -0,0 +1,1189 @@ +import os +import sys +import json +import logging +import traceback +import requests +from datetime import datetime +import django +from django.db import transaction +from django.contrib.auth import get_user_model +import time + +# 设置Django环境 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'role_based_system.settings') +django.setup() + +import lark_oapi as lark +from lark_oapi.api.bitable.v1 import * +from user_management.models import ( + FeishuCreator, KnowledgeBase, UserGoal, + GmailTalentMapping, ChatHistory, ConversationSummary +) +from django.conf import settings +from user_management.gmail_integration import GmailIntegration +from feishu.feishu import sync_to_knowledge_base + +logger = logging.getLogger(__name__) + +def get_tenant_access_token(app_id, app_secret): + """ + 获取飞书应用的tenant_access_token + """ + url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" + headers = { + "Content-Type": "application/json" + } + data = { + "app_id": app_id, + "app_secret": app_secret + } + + try: + response = requests.post(url, json=data, headers=headers) + response_json = response.json() + + if response.status_code == 200 and response_json.get("code") == 0: + return { + "status": "success", + "access_token": response_json.get("tenant_access_token"), + "expire": response_json.get("expire") + } + else: + logging.error(f"获取tenant_access_token失败: {response_json}") + return { + "status": "error", + "message": response_json.get("msg", "未知错误") + } + except Exception as e: + logging.error(f"获取tenant_access_token异常: {str(e)}") + return { + "status": "error", + "message": str(e) + } + +def fetch_table_records(app_token, table_id, view_id, access_token=None, app_id=None, app_secret=None): + """ + 从飞书多维表格获取记录 + + 参数: + app_token: 应用ID + table_id: 表格ID + view_id: 视图ID + access_token: 访问令牌(可选,如果提供则直接使用) + app_id: 应用ID(可选,用于自动获取token) + app_secret: 应用密钥(可选,用于自动获取token) + """ + # 如果没有提供access_token但提供了app_id和app_secret,则自动获取 + if not access_token and app_id and app_secret: + token_result = get_tenant_access_token(app_id, app_secret) + if token_result["status"] == "success": + access_token = token_result["access_token"] + else: + logging.error(f"无法获取访问令牌: {token_result['message']}") + return [] + + total_records = [] + page_token = None + page_size = 20 + + try: + # 初始化客户端 + client = lark.Client.builder() \ + .enable_set_token(True) \ + .log_level(lark.LogLevel.DEBUG) \ + .build() + + logger.info(f"开始从飞书表格获取数据: app_token={app_token}, table_id={table_id}, view_id={view_id}") + + # 尝试两种方法获取记录 + # 方法1: 使用list接口 + list_success = False + try: + while True: + try: + # 构造请求对象 + builder = ListAppTableRecordRequest.builder() \ + .app_token(app_token) \ + .table_id(table_id) \ + .page_size(page_size) \ + .view_id(view_id) + + # 如果有page_token,添加到请求中 + if page_token: + builder = builder.page_token(page_token) + + # 构建完整请求 + request = builder.build() + + logger.debug(f"发送list请求,page_token: {page_token}") + + # 发起请求 + option = lark.RequestOption.builder().user_access_token(access_token).build() + response = client.bitable.v1.app_table_record.list(request, option) + + if not response.success(): + logger.error(f"list请求失败: {response.code}, {response.msg}") + break + + # 获取当前页记录 + current_records = response.data.items + if not current_records: + logger.info("没有更多记录") + break + + total_records.extend(current_records) + list_success = True + + # 解析响应数据获取分页信息 + response_data = json.loads(response.raw.content) + has_more = response_data["data"].get("has_more", False) + total = response_data["data"].get("total", 0) + logger.info(f"list方法获取到 {len(current_records)} 条记录,当前总计: {len(total_records)}/{total} 条") + + # 获取下一页token + page_token = response_data["data"].get("page_token") + if not page_token or not has_more: + logger.info("list方法已获取所有数据") + break + + except Exception as e: + logger.error(f"list方法获取记录时出错: {str(e)}") + logger.error(traceback.format_exc()) + list_success = False + break + except Exception as e: + logger.error(f"list方法整体出错: {str(e)}") + list_success = False + + # 如果list方法失败,尝试search方法 + if not list_success or not total_records: + logger.info("尝试使用search方法获取数据...") + page_token = None + total_records = [] + + while True: + try: + # 构造search请求对象 + request = SearchAppTableRecordRequest.builder() \ + .app_token(app_token) \ + .table_id(table_id) \ + .page_size(page_size) + + # 如果有page_token,添加到请求中 + if page_token: + request = request.page_token(page_token) + + # 添加请求体 - 可以根据需要添加过滤条件 + request = request.request_body( + SearchAppTableRecordRequestBody.builder().build() + ).build() + + logger.debug(f"发送search请求,page_token: {page_token}") + + # 发起请求 + option = lark.RequestOption.builder().user_access_token(access_token).build() + response = client.bitable.v1.app_table_record.search(request, option) + + if not response.success(): + logger.error(f"search请求失败: {response.code}, {response.msg}") + break + + # 获取当前页记录 + current_records = response.data.items + if not current_records: + logger.info("search方法没有更多记录") + break + + total_records.extend(current_records) + + # 解析响应数据获取分页信息 + response_data = json.loads(response.raw.content) + has_more = response_data["data"].get("has_more", False) + total = response_data["data"].get("total", 0) + logger.info(f"search方法获取到 {len(current_records)} 条记录,当前总计: {len(total_records)}/{total} 条") + + # 获取下一页token + page_token = response_data["data"].get("page_token") + if not page_token or not has_more: + logger.info("search方法已获取所有数据") + break + + except Exception as e: + logger.error(f"search方法获取记录时出错: {str(e)}") + logger.error(traceback.format_exc()) + break + + logger.info(f"最终获取到 {len(total_records)} 条记录") + return total_records + + except Exception as e: + logger.error(f"获取飞书表格记录时出错: {str(e)}") + logger.error(traceback.format_exc()) + return [] + +def extract_field_value(field_value): + """ + 提取字段值 + + 参数: + field_value: 飞书返回的字段值 + + 返回: + 任意类型: 提取后的值 + """ + if isinstance(field_value, list): + if field_value and isinstance(field_value[0], dict): + return field_value[0].get('text', '') + elif isinstance(field_value, dict): + if 'text' in field_value: + return field_value['text'] + elif 'link' in field_value: + return field_value['link'] + elif 'link_record_ids' in field_value: + return '' + return field_value + +def find_duplicate_email_creators(records): + """ + 查找记录中重复邮箱的创作者 + + 参数: + records: 飞书记录列表 + + 返回: + dict: 以邮箱为键,记录列表为值的字典 + """ + email_map = {} + + for record in records: + fields = record.fields + email = extract_field_value(fields.get('邮箱', '')) + + if email: + if email not in email_map: + email_map[email] = [] + email_map[email].append(record) + + # 过滤出现次数>1的邮箱 + duplicate_emails = {email: records for email, records in email_map.items() if len(records) > 1} + + logger.info(f"发现 {len(duplicate_emails)} 个重复邮箱") + return duplicate_emails + +def create_or_update_knowledge_base(email, user=None): + """ + 为创作者创建或更新知识库 + + 参数: + email: 创作者邮箱 + user: 用户对象,默认为None(将选择一个组长) + + 返回: + tuple: (KnowledgeBase对象, 是否新创建) + """ + # 优先使用给定的用户,否则获取一个组长用户 + if not user: + User = get_user_model() + user = User.objects.filter(role='leader').first() + + if not user: + logger.error("未找到组长用户,无法创建知识库") + return None, False + + # 首先查找创作者 + creator = FeishuCreator.objects.filter(email=email).first() + + if not creator: + logger.error(f"找不到邮箱为 {email} 的创作者") + return None, False + + # 使用sync_to_knowledge_base函数创建知识库 + kb, created = sync_to_knowledge_base(creator_id=creator.id) + + if kb: + logger.info(f"邮箱 {email} 的知识库{'已创建' if created else '已存在'}: {kb.id}") + else: + logger.error(f"为邮箱 {email} 创建知识库失败") + + return kb, created + +def set_user_goal(user, email, goal_content): + """ + 设置用户总目标 + + 参数: + user: 用户对象 + email: 创作者邮箱 + goal_content: 目标内容 + + 返回: + dict: 包含操作结果和目标信息 + """ + try: + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(user) + + # 设置总目标 + result = gmail_integration.manage_user_goal(goal_content) + + if result['status'] == 'success': + logger.info(f"为用户 {user.username} 设置总目标成功: {result['action']}") + return result + else: + logger.error(f"为用户 {user.username} 设置总目标失败: {result.get('message', 'Unknown error')}") + return result + + except Exception as e: + logger.error(f"设置用户总目标时出错: {str(e)}") + logger.error(traceback.format_exc()) + return { + 'status': 'error', + 'message': str(e) + } + +def create_chat_with_ai(user, talent_email, goal_content): + """ + 创建与AI的聊天会话 + + 参数: + user: 用户对象 + talent_email: 达人邮箱 + goal_content: 目标内容 + + 返回: + dict: 操作结果 + """ + try: + # 1. 获取或创建知识库 + kb, kb_created = create_or_update_knowledge_base(talent_email, user) + if not kb: + return { + 'status': 'error', + 'message': f"为邮箱 {talent_email} 创建知识库失败" + } + + # 2. 设置用户总目标 + goal_result = set_user_goal(user, talent_email, goal_content) + if goal_result['status'] != 'success': + return goal_result + + # 3. 检查是否已有对应的Gmail映射关系 + mapping = GmailTalentMapping.objects.filter( + user=user, + talent_email=talent_email, + is_active=True + ).first() + + if not mapping: + # 创建映射关系 + mapping = GmailTalentMapping.objects.create( + user=user, + talent_email=talent_email, + knowledge_base=kb, + conversation_id=f"feishu_ai_{kb.id}", + is_active=True + ) + logger.info(f"创建新的Gmail映射: {talent_email} -> {kb.id}") + elif mapping.knowledge_base_id != kb.id: + # 更新现有映射关系 + mapping.knowledge_base = kb + mapping.save() + logger.info(f"更新Gmail映射: {talent_email} -> {kb.id}") + + return { + 'status': 'success', + 'action': 'create', + 'knowledge_base': { + 'id': str(kb.id), + 'name': kb.name, + 'created': kb_created + }, + 'goal': goal_result.get('goal'), + 'mapping': { + 'id': str(mapping.id), + 'conversation_id': mapping.conversation_id + } + } + + except Exception as e: + logger.error(f"创建AI聊天时出错: {str(e)}") + logger.error(traceback.format_exc()) + return { + 'status': 'error', + 'message': str(e) + } + +def process_duplicate_emails(duplicate_emails, goal_content=None): + """ + 处理重复邮箱记录 + + 参数: + duplicate_emails: 重复邮箱记录字典 + goal_content: 目标内容模板,可包含{email}和{handle}占位符 + + 返回: + dict: 处理结果统计 + """ + if not goal_content: + goal_content = "与达人{handle}建立联系并了解其账号情况,处理合作需求,最终目标是达成合作并签约。" + + User = get_user_model() + leader = User.objects.filter(role='leader').first() + + if not leader: + logger.error("未找到组长用户,无法处理重复邮箱") + return { + 'status': 'error', + 'message': "未找到组长用户" + } + + results = { + 'total': len(duplicate_emails), + 'success': 0, + 'failure': 0, + 'details': [] + } + + for email, records in duplicate_emails.items(): + try: + # 获取一个Handle作为示例 + handle = extract_field_value(records[0].fields.get('Handle', email.split('@')[0])) + + # 格式化目标内容 + formatted_goal = goal_content.format(email=email, handle=handle) + + # 创建AI聊天 + result = create_chat_with_ai(leader, email, formatted_goal) + + if result['status'] == 'success': + results['success'] += 1 + logger.info(f"成功为邮箱 {email} 创建AI聊天") + else: + results['failure'] += 1 + logger.error(f"为邮箱 {email} 创建AI聊天失败: {result.get('message', 'Unknown error')}") + + results['details'].append({ + 'email': email, + 'handle': handle, + 'status': result['status'], + 'message': result.get('message', '') + }) + + except Exception as e: + results['failure'] += 1 + logger.error(f"处理邮箱 {email} 时出错: {str(e)}") + logger.error(traceback.format_exc()) + results['details'].append({ + 'email': email, + 'status': 'error', + 'message': str(e) + }) + + return results + +def generate_ai_response(conversation_history, user_goal): + """ + 调用DeepSeek API生成AI响应 + + 参数: + conversation_history: 对话历史 + user_goal: 用户总目标 + + 返回: + str: AI响应内容 + """ + try: + # 使用有效的API密钥 + api_key = "sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf" + # 如果上面的密钥不正确,可以尝试从环境变量或数据库中获取 + # 从Django设置中获取密钥 + if hasattr(settings, 'DEEPSEEK_API_KEY') and settings.DEEPSEEK_API_KEY: + api_key = settings.DEEPSEEK_API_KEY + + url = "https://api.siliconflow.cn/v1/chat/completions" + + # 系统消息指定AI助手的角色和总目标 + system_message = { + "role": "system", + "content": f"""你是一位专业的电商客服和达人助手。你的任务是与达人进行对话,帮助实现以下总目标: + +{user_goal} + +你应该主动推进对话,引导达人朝着目标方向发展。每次回复应该简洁明了,专业且有帮助。 +如果你认为总目标已经达成,请在回复的最后一行添加标记: [目标已达成]""" + } + + messages = [system_message] + + # 添加对话历史,但限制消息数量避免超出token限制 + # 如果对话历史太长,可能需要进一步处理或分割 + if len(conversation_history) > 20: + # 选取关键消息:第一条、最后几条以及中间的一些重要消息 + selected_messages = ( + conversation_history[:2] + # 前两条 + conversation_history[len(conversation_history)//2-2:len(conversation_history)//2+2] + # 中间四条 + conversation_history[-12:] # 最后12条 + ) + messages.extend(selected_messages) + else: + messages.extend(conversation_history) + + # 构建API请求 + payload = { + "model": "deepseek-ai/DeepSeek-V3", + "messages": messages, + "stream": False, + "max_tokens": 1500, # 增加token上限以容纳完整回复 + "temperature": 0.3, # 降低随机性,使回复更加确定性 + "top_p": 0.9, + "top_k": 50, + "frequency_penalty": 0.5, + "presence_penalty": 0.2, + "n": 1, + "stop": [], + "response_format": { + "type": "text" + } + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + } + + logger.info("开始调用DeepSeek API生成对话回复") + response = requests.post(url, json=payload, headers=headers) + + if response.status_code != 200: + logger.error(f"DeepSeek API调用失败: {response.status_code}, {response.text}") + return None + + result = response.json() + logger.debug(f"DeepSeek API返回: {result}") + + # 提取回复内容 + if 'choices' in result and len(result['choices']) > 0: + reply = result['choices'][0]['message']['content'] + # 如果返回的内容为空,直接返回None + if not reply or reply.strip() == '': + logger.warning("DeepSeek API返回的回复内容为空") + return None + return reply + + logger.warning(f"DeepSeek API返回格式异常: {result}") + return None + + except Exception as e: + logger.error(f"调用DeepSeek API生成回复失败: {str(e)}") + logger.error(traceback.format_exc()) + return None + +def check_goal_achieved(response): + """ + 检查目标是否已达成 + + 参数: + response: AI的回复内容 + + 返回: + bool: 是否达成目标 + """ + if not response: + return False + + # 检查回复中是否包含目标达成标记 + goal_markers = [ + "[目标已达成]", + "【目标已达成】", + "目标已达成", + "已达成目标" + ] + + for marker in goal_markers: + if marker in response: + logger.info(f"检测到目标达成标记: {marker}") + return True + + return False + +def auto_chat_session(user, talent_email, max_turns=10): + """ + 执行自动聊天会话 + + 参数: + user: 用户对象 + talent_email: 达人邮箱 + max_turns: 最大对话轮次,默认10轮 + + 返回: + dict: 会话结果 + """ + try: + # 1. 获取用户目标 + gmail_integration = GmailIntegration(user) + goal_result = gmail_integration.manage_user_goal() + + if goal_result['status'] != 'success' or not goal_result.get('goal'): + logger.error(f"获取用户目标失败: {goal_result.get('message', 'No goal found')}") + return { + 'status': 'error', + 'message': "无法获取用户总目标" + } + + user_goal = goal_result['goal']['content'] + + # 2. 获取Gmail映射关系 + mapping = GmailTalentMapping.objects.filter( + user=user, + talent_email=talent_email, + is_active=True + ).first() + + if not mapping: + logger.error(f"找不到与邮箱 {talent_email} 的映射关系") + return { + 'status': 'error', + 'message': f"找不到与邮箱 {talent_email} 的映射关系" + } + + kb = mapping.knowledge_base + conversation_id = mapping.conversation_id + + # 3. 获取现有对话历史 + chat_messages = ChatHistory.objects.filter( + user=user, + knowledge_base=kb, + conversation_id=conversation_id, + is_deleted=False + ).order_by('created_at') + + conversation_history = [] + for msg in chat_messages: + conversation_history.append({ + "role": msg.role, + "content": msg.content + }) + + # 如果没有对话历史,添加一条系统消息作为开始 + if not conversation_history: + # 创建一条初始的系统消息 + system_msg = ChatHistory.objects.create( + user=user, + knowledge_base=kb, + conversation_id=conversation_id, + role='system', + content=f"与达人 {talent_email} 的对话开始。总目标: {user_goal}" + ) + + conversation_history.append({ + "role": "system", + "content": system_msg.content + }) + + # 发送第一封邮件来开始对话 + first_subject = "关于合作的洽谈" + first_content = f"您好,\n\n我是{user.username},我们正在寻找合适的达人合作伙伴,注意到您的账号非常适合我们的产品。\n\n请问您有兴趣了解更多关于我们合作的细节吗?\n\n期待您的回复。\n\n祝好,\n{user.username}" + + # 记录首次发送消息到对话历史 + initial_msg = ChatHistory.objects.create( + user=user, + knowledge_base=kb, + conversation_id=conversation_id, + role='assistant', + content=first_content + ) + + conversation_history.append({ + "role": "assistant", + "content": initial_msg.content + }) + + # 实际发送邮件 + email_result = gmail_integration.send_email( + to_email=talent_email, + subject=first_subject, + body=first_content, + conversation_id=conversation_id + ) + + if email_result['status'] != 'success': + logger.error(f"发送首次邮件失败: {email_result.get('message', 'Unknown error')}") + return { + 'status': 'error', + 'message': f"发送首次邮件失败: {email_result.get('message', 'Unknown error')}" + } + + logger.info(f"已发送首次邮件到 {talent_email}") + + # 首次邮件发送后,直接返回不进行后续对话,等待达人回复 + return { + 'status': 'success', + 'message': '已发送首次邮件,等待达人回复', + 'turns_completed': 1, + 'goal_achieved': False, + 'email_sent': True, + 'conversation_id': conversation_id + } + + # 4. 检查最新消息是否来自达人(user),如果是,才进行回复 + last_message = chat_messages.last() + if last_message and last_message.role == 'user': + # 有达人回复,生成AI回复并发送邮件 + # 生成AI回复 + ai_response = generate_ai_response(conversation_history, user_goal) + + if not ai_response: + logger.error("生成AI回复失败") + return { + 'status': 'error', + 'message': '生成AI回复失败' + } + + # 检查目标是否已达成 + goal_achieved = check_goal_achieved(ai_response) + + # 保存AI回复到对话历史 + ai_msg = ChatHistory.objects.create( + user=user, + knowledge_base=kb, + conversation_id=conversation_id, + role='assistant', + content=ai_response + ) + + # 使用最近的主题作为回复主题 + subject = "回复: " + (last_message.subject if hasattr(last_message, 'subject') and last_message.subject else "关于合作的洽谈") + + # 实际发送邮件 + email_result = gmail_integration.send_email( + to_email=talent_email, + subject=subject, + body=ai_response, + conversation_id=conversation_id + ) + + if email_result['status'] != 'success': + logger.error(f"发送邮件回复失败: {email_result.get('message', 'Unknown error')}") + return { + 'status': 'error', + 'message': f"发送邮件回复失败: {email_result.get('message', 'Unknown error')}" + } + + # 如果目标已达成,发送通知 + if goal_achieved: + send_goal_achieved_notification(user, talent_email, conversation_id) + # 生成对话总结 + summary_result = gmail_integration.generate_conversation_summary(talent_email) + summary_status = "success" if summary_result.get('status') == 'success' else "failed" + else: + summary_status = "not_needed" + + return { + 'status': 'success', + 'turns_completed': 1, + 'goal_achieved': goal_achieved, + 'summary_status': summary_status, + 'email_sent': True, + 'conversation_id': conversation_id + } + else: + # 没有新的达人回复,不需要回复 + return { + 'status': 'success', + 'message': '没有新的达人回复,不需要回复', + 'turns_completed': 0, + 'goal_achieved': False, + 'email_sent': False, + 'conversation_id': conversation_id + } + + except Exception as e: + logger.error(f"自动聊天会话出错: {str(e)}") + logger.error(traceback.format_exc()) + return { + 'status': 'error', + 'message': str(e) + } + +def simulate_user_reply(ai_response, turn_count): + """ + 模拟用户回复 + + 参数: + ai_response: AI的回复内容 + turn_count: 当前对话轮次 + + 返回: + str: 模拟的用户回复 + """ + # 根据对话轮次和AI回复生成不同的回复模板 + if turn_count == 0: + return "你好,很高兴认识你。我对你提到的合作很感兴趣,能详细说说你们公司的情况吗?" + + if "价格" in ai_response or "报价" in ai_response: + return "价格是我比较关注的,你们能提供什么样的价格方案?我希望能有一个灵活的合作模式。" + + if "合作模式" in ai_response or "合作方式" in ai_response: + return "这种合作模式听起来不错。我目前的粉丝群体主要是25-35岁的女性,她们对美妆和生活方式类产品比较感兴趣。你觉得这和你们的产品匹配吗?" + + if "产品" in ai_response: + return "产品听起来很不错。我想了解一下发货和售后是如何处理的?这对我的粉丝体验很重要。" + + if "合同" in ai_response or "协议" in ai_response: + return "我对合同条款没有太大问题,但希望能保持一定的创作自由度。什么时候可以开始合作?" + + # 默认回复 + return "这些信息很有帮助,谢谢。我需要再考虑一下,有什么其他你想告诉我的吗?" + +def send_goal_achieved_notification(user, talent_email, conversation_id): + """ + 发送目标达成通知 + + 参数: + user: 用户对象 + talent_email: 达人邮箱 + conversation_id: 对话ID + """ + try: + from user_management.models import Notification + + # 创建通知 + notification = Notification.objects.create( + type='system_notice', + title=f"目标已达成 - {talent_email}", + content=f"与达人 {talent_email} 的自动对话已达成总目标。请查看对话详情并进行后续处理。", + sender=user, # 这里发送者设为当前用户 + receiver=user, # 通知发给当前用户 + related_resource=conversation_id + ) + + logger.info(f"已创建目标达成通知: {notification.id}") + + except Exception as e: + logger.error(f"发送目标达成通知失败: {str(e)}") + logger.error(traceback.format_exc()) + +def print_help(): + """打印帮助信息""" + print("飞书多维表格自动AI对话工具") + print("=======================") + print("\n可用命令:") + print(" 1. 从飞书读取表格并处理重复邮箱:") + print(" python feishu_ai_chat.py process_table --app_token 应用TOKEN --table_id 表格ID --view_id 视图ID --access_token 访问令牌") + print() + print(" 2. 为指定邮箱执行自动对话:") + print(" python feishu_ai_chat.py auto_chat --email 达人邮箱 [--turns 对话轮数]") + print() + print(" 3. 设置用户总目标:") + print(" python feishu_ai_chat.py set_goal --email 达人邮箱 --goal \"总目标内容\"") + print() + print(" 4. 检查目标完成状态:") + print(" python feishu_ai_chat.py check_goal --email 达人邮箱") + print() + print(" 5. 帮助信息:") + print(" python feishu_ai_chat.py help") + print() + print("示例:") + print(" python feishu_ai_chat.py process_table --table_id tblcck2za8GZBliz --view_id vewSOIsmxc") + print(" python feishu_ai_chat.py auto_chat --email example@gmail.com --turns 5") + print() + +def handle_process_table(args): + """处理飞书表格命令""" + import argparse + parser = argparse.ArgumentParser(description='处理飞书表格') + parser.add_argument('--app_token', default="XYE6bMQUOaZ5y5svj4vcWohGnmg", help='飞书应用TOKEN') + parser.add_argument('--table_id', required=True, help='表格ID') + parser.add_argument('--view_id', required=True, help='视图ID') + parser.add_argument('--access_token', default=None, help='用户访问令牌') + parser.add_argument('--app_id', default="cli_a5c97daacb9e500d", help='应用ID') + parser.add_argument('--app_secret', default="fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y", help='应用密钥') + parser.add_argument('--goal_template', default="与达人{handle}(邮箱:{email})建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。", help='目标内容模板') + parser.add_argument('--auto_chat', action='store_true', help='是否自动执行AI对话') + parser.add_argument('--turns', type=int, default=5, help='自动对话轮次') + + params = parser.parse_args(args) + + # 从飞书表格获取记录 + records = fetch_table_records( + params.app_token, + params.table_id, + params.view_id, + params.access_token, + params.app_id, + params.app_secret + ) + + if not records: + logger.error("未获取到任何记录") + return + + # 查找重复邮箱的创作者 + duplicate_emails = find_duplicate_email_creators(records) + + if not duplicate_emails: + logger.info("未发现重复邮箱") + return + + logger.info(f"发现 {len(duplicate_emails)} 个重复邮箱,开始处理...") + + # 处理重复邮箱记录 + results = process_duplicate_emails(duplicate_emails, params.goal_template) + + # 打印处理结果 + logger.info(f"处理完成: 总计 {results['total']} 个邮箱,成功 {results['success']} 个,失败 {results['failure']} 个") + + # 如果需要自动对话 + if params.auto_chat: + # 获取组长用户 + User = get_user_model() + leader = User.objects.filter(role='leader').first() + + if not leader: + logger.error("未找到组长用户,无法进行自动对话") + return + + # 为每个成功创建的记录执行自动对话 + chat_results = [] + for detail in results['details']: + if detail['status'] == 'success': + email = detail['email'] + logger.info(f"开始为邮箱 {email} 执行自动对话...") + + chat_result = auto_chat_session(leader, email, max_turns=params.turns) + chat_results.append({ + 'email': email, + 'result': chat_result + }) + + logger.info(f"邮箱 {email} 自动对话完成: {chat_result['status']}") + + # 打印对话结果 + logger.info(f"自动对话完成: 总计 {len(chat_results)} 个对话") + for chat in chat_results: + result = chat['result'] + if result['status'] == 'success': + logger.info(f"邮箱 {chat['email']} 对话成功,轮次: {result['turns_completed']},目标达成: {result['goal_achieved']}") + else: + logger.info(f"邮箱 {chat['email']} 对话失败: {result.get('message', 'Unknown error')}") + +def handle_auto_chat(args): + """处理自动对话命令""" + import argparse + parser = argparse.ArgumentParser(description='执行自动对话') + parser.add_argument('--email', required=True, help='达人邮箱') + parser.add_argument('--force_send', action='store_true', help='是否强制发送新邮件') + parser.add_argument('--subject', help='邮件主题(仅当force_send=true时使用)') + parser.add_argument('--content', help='邮件内容(仅当force_send=true时使用)') + + params = parser.parse_args(args) + + # 获取组长用户 + User = get_user_model() + leader = User.objects.filter(role='leader').first() + + if not leader: + logger.error("未找到组长用户,无法进行自动对话") + return + + # 如果是强制发送模式 + if params.force_send: + if not params.content: + logger.error("当force_send=true时,必须提供content参数") + return + + try: + # 获取知识库映射 + mapping = GmailTalentMapping.objects.filter( + user=leader, + talent_email=params.email, + is_active=True + ).first() + + if not mapping: + logger.error(f"找不到与邮箱 {params.email} 的映射关系") + return + + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(leader) + + # 直接发送邮件 + mail_subject = params.subject if params.subject else "关于合作的洽谈" + mail_result = gmail_integration.send_email( + to_email=params.email, + subject=mail_subject, + body=params.content, + conversation_id=mapping.conversation_id + ) + + if mail_result['status'] != 'success': + logger.error(f"邮件发送失败: {mail_result.get('message', 'Unknown error')}") + return + + # 保存发送的内容到对话历史 + ChatHistory.objects.create( + user=leader, + knowledge_base=mapping.knowledge_base, + conversation_id=mapping.conversation_id, + role='assistant', + content=params.content + ) + + logger.info(f"已强制发送邮件到 {params.email}") + return { + 'status': 'success', + 'message': f"已强制发送邮件到 {params.email}", + 'email_sent': True, + 'conversation_id': mapping.conversation_id + } + + except Exception as e: + logger.error(f"强制发送邮件时出错: {str(e)}") + logger.error(traceback.format_exc()) + return { + 'status': 'error', + 'message': str(e) + } + + # 执行自动对话 + result = auto_chat_session(leader, params.email) + + # 打印结果 + if result['status'] == 'success': + if result.get('email_sent'): + logger.info(f"自动对话成功,已发送邮件到 {params.email}") + else: + logger.info(f"没有新的达人回复,不需要发送邮件") + else: + logger.error(f"自动对话失败: {result.get('message', 'Unknown error')}") + + return result + +def handle_set_goal(args): + """处理设置目标命令""" + import argparse + parser = argparse.ArgumentParser(description='设置用户总目标') + parser.add_argument('--email', required=True, help='达人邮箱') + parser.add_argument('--goal', required=True, help='目标内容') + + params = parser.parse_args(args) + + # 获取组长用户 + User = get_user_model() + leader = User.objects.filter(role='leader').first() + + if not leader: + logger.error("未找到组长用户,无法设置目标") + return + + # 设置总目标 + gmail_integration = GmailIntegration(leader) + result = gmail_integration.manage_user_goal(params.goal) + + # 打印结果 + if result['status'] == 'success': + logger.info(f"设置总目标成功: {result['action']}") + else: + logger.error(f"设置总目标失败: {result.get('message', 'Unknown error')}") + + return result + +def handle_check_goal(args): + """处理检查目标完成状态命令""" + import argparse + parser = argparse.ArgumentParser(description='检查目标完成状态') + parser.add_argument('--email', required=True, help='达人邮箱') + + params = parser.parse_args(args) + + # 获取组长用户 + User = get_user_model() + leader = User.objects.filter(role='leader').first() + + if not leader: + logger.error("未找到组长用户,无法检查目标") + return + + # 查找Gmail映射关系 + mapping = GmailTalentMapping.objects.filter( + user=leader, + talent_email=params.email, + is_active=True + ).first() + + if not mapping: + logger.error(f"找不到与邮箱 {params.email} 的映射关系") + return { + 'status': 'error', + 'message': f"找不到与邮箱 {params.email} 的映射关系" + } + + # 获取对话历史中最后的AI回复 + last_ai_message = ChatHistory.objects.filter( + user=leader, + knowledge_base=mapping.knowledge_base, + conversation_id=mapping.conversation_id, + role='assistant', + is_deleted=False + ).order_by('-created_at').first() + + if not last_ai_message: + logger.error(f"找不到与邮箱 {params.email} 的对话历史") + return { + 'status': 'error', + 'message': f"找不到与邮箱 {params.email} 的对话历史" + } + + # 检查目标是否已达成 + goal_achieved = check_goal_achieved(last_ai_message.content) + + # 获取对话总结 + summary = ConversationSummary.objects.filter( + user=leader, + talent_email=params.email, + is_active=True + ).order_by('-updated_at').first() + + result = { + 'status': 'success', + 'email': params.email, + 'goal_achieved': goal_achieved, + 'last_message_time': last_ai_message.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'summary': summary.summary if summary else None + } + + # 打印结果 + logger.info(f"目标状态检查结果:") + logger.info(f"邮箱: {params.email}") + logger.info(f"目标达成: {goal_achieved}") + logger.info(f"最后消息时间: {result['last_message_time']}") + + return result + +def main(): + """命令行入口函数""" + import sys + + if len(sys.argv) < 2 or sys.argv[1] == 'help': + print_help() + return + + command = sys.argv[1] + args = sys.argv[2:] + + if command == 'process_table': + handle_process_table(args) + elif command == 'auto_chat': + handle_auto_chat(args) + elif command == 'set_goal': + handle_set_goal(args) + elif command == 'check_goal': + handle_check_goal(args) + else: + print(f"未知命令: {command}") + print_help() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/gmail/attachments/test2.txt b/gmail/attachments/test2.txt deleted file mode 100644 index b16ac8e..0000000 --- a/gmail/attachments/test2.txt +++ /dev/null @@ -1,10 +0,0 @@ -本期节目内容简介 -在参加各类比赛时,肯定会遇到竞争对手。英语单词 rival、opponent、competitor 和 contestant 的含义相似,都可以用来指“与他人之间存在竞争关系的人或团队”。在本集《你问我答》节目中,我们将通过和体育比赛有关的实例来为大家阐释这四个近义词之间的区别和用法。 - -欢迎你加入并和我们一起讨论英语学习的方方面面。请通过微博“BBC英语教学”或邮件与我们取得联系。我们的邮箱地址是 questions.chinaelt@bbc.co.uk。 - -文字稿 -(关于台词的备注: 请注意这不是广播节目的逐字稿件。本文稿可能没有体现录制、编辑过程中对节目做出的改变。) - -Feifei -大家好,欢迎收听 BBC 英语教学的《你问我答》节目,我是冯菲菲。每集节目中,我们会回答大家在英语学习时遇到的一个问题。本集的问题来自 Adela。我们来听一下她的问题。 diff --git a/gmail/client_secret.json b/gmail/client_secret.json deleted file mode 100644 index a59f8b1..0000000 --- a/gmail/client_secret.json +++ /dev/null @@ -1 +0,0 @@ -{"installed":{"client_id":"266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com","project_id":"knowledge-454905","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost"]}} \ No newline at end of file diff --git a/gmail/email_conversations.json b/gmail/email_conversations.json index f270226..c298af2 100644 --- a/gmail/email_conversations.json +++ b/gmail/email_conversations.json @@ -34,7 +34,7 @@ "filename": "test2.txt", "mimeType": "text/plain", "size": 996, - "attachmentId": "ANGjdJ8O1uGve4uPqFSC2C4sMeC5jXJ3DGilhB1By705ZLGbOF30m6uITRtJHWsnB7yREKVslhYRdu4GKKvrrkWw-63ogqaZPGgi0WuSoB0OxIWXwbQSjaayUOPvc3P8y1g9A2mMAm5k0DkjH_LP0QMmulhXMg8ZgUcm4CZR7-HTBYztZSfWOoUeYkNugdzV5_2ax3GT34P5uaGHh3i4Ge6y-XDN-TDq3i1w9u_eTjZowoyVzjTj48uaQmzmFU36TQxg8ncovHk0sNZEygzCE1gbHbS1uM9N1tUHOOa6FWEpajHgz2aQwLh65SsMRqKe-LognFES-J_082IHddWs" + "attachmentId": "ANGjdJ-lpRprHQbSJhwdSR5-iiBiHvZfV_H59db3znGETs9vTjBU-egQng8f0Js7wX79HGTt7lKDhhlq95xbmfOUf81mCR9vLMu9QhNS1P4ph8oVc5aDSHUBQk6rj8Xqj8EmD-gL--bB2CjXNLuNZ-khuKcyABUUH7FKorP4UFebWKh9Phfy1Js_QhNeSy8wfcQuCiOQ78pYimuwA-dtbuDusRUmAX2VyM6SyJD1eR1hzeoUQoICZ7AJoxErf6whPdLN8qv1O_ZSeXjhbSkdeSwlScuSgXClHWUw8a8lDx_gWka7WfF2OtYdB4wItaYHFQcaRlK4qqES2IM-azOV" } ] }, @@ -117,5 +117,109 @@ "date": "2025-04-10 16:12:20", "body": "你好\r\n", "attachments": [] + }, + { + "id": "1961f18398a4567b", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 17:46:31", + "body": "实时\r\n", + "attachments": [] + }, + { + "id": "1961f19379ea6165", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 17:47:35", + "body": "111111\r\n", + "attachments": [] + }, + { + "id": "1961f1cf6e12b80c", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 17:51:41", + "body": "再来一次\r\n", + "attachments": [] + }, + { + "id": "1961f2673f0621b8", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 18:02:01", + "body": "坎坎坷坷\r\n", + "attachments": [] + }, + { + "id": "1961f2cd0d18aff6", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 18:08:31", + "body": "再来一次\r\n", + "attachments": [] + }, + { + "id": "1961f2e8aaac674e", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 18:10:54", + "body": "3234\r\n", + "attachments": [] + }, + { + "id": "1961f36448599caf", + "subject": "", + "from": "crush wds ", + "date": "2025-04-10 18:19:19", + "body": "4234\r\n", + "attachments": [] + }, + { + "id": "19622d0344d49679", + "subject": "", + "from": "crushwds@gmail.com", + "date": "2025-04-10 23:06:32", + "body": "0101 我是02", + "attachments": [] + }, + { + "id": "19622bfa19835a0a", + "subject": "", + "from": "crush wds ", + "date": "2025-04-11 10:48:14", + "body": "4.11\r\n", + "attachments": [] + }, + { + "id": "19622c0b5d9ea09f", + "subject": "", + "from": "crush wds ", + "date": "2025-04-11 10:49:24", + "body": "4.11 2\r\n", + "attachments": [] + }, + { + "id": "19622c17299a5432", + "subject": "", + "from": "crush wds ", + "date": "2025-04-11 10:50:13", + "body": "休息休息\r\n", + "attachments": [] + }, + { + "id": "19622c6f41eb9e5f", + "subject": "", + "from": "crush wds ", + "date": "2025-04-11 10:56:14", + "body": "反反复复\r\n", + "attachments": [] + }, + { + "id": "196236321d6dc082", + "subject": "", + "from": "crush wds ", + "date": "2025-04-11 13:46:49", + "body": "0202我是01\r\n", + "attachments": [] } ] \ No newline at end of file diff --git a/gmail/email_conversations.txt b/gmail/email_conversations.txt index 1b78942..074d880 100644 --- a/gmail/email_conversations.txt +++ b/gmail/email_conversations.txt @@ -1,5 +1,5 @@ ================================================== -记录时间: 2025-04-10 16:52:49 +记录时间: 2025-04-17 15:43:23 ================================================== 时间: 2025-03-27 12:04:29 @@ -105,3 +105,93 @@ yes 你好 -------------------------------------------------- +时间: 2025-04-10 17:46:31 +发件人: crush wds +主题: +内容: +实时 + +-------------------------------------------------- +时间: 2025-04-10 17:47:35 +发件人: crush wds +主题: +内容: +111111 + +-------------------------------------------------- +时间: 2025-04-10 17:51:41 +发件人: crush wds +主题: +内容: +再来一次 + +-------------------------------------------------- +时间: 2025-04-10 18:02:01 +发件人: crush wds +主题: +内容: +坎坎坷坷 + +-------------------------------------------------- +时间: 2025-04-10 18:08:31 +发件人: crush wds +主题: +内容: +再来一次 + +-------------------------------------------------- +时间: 2025-04-10 18:10:54 +发件人: crush wds +主题: +内容: +3234 + +-------------------------------------------------- +时间: 2025-04-10 18:19:19 +发件人: crush wds +主题: +内容: +4234 + +-------------------------------------------------- +时间: 2025-04-10 23:06:32 +发件人: crushwds@gmail.com +主题: +内容: +0101 我是02 +-------------------------------------------------- +时间: 2025-04-11 10:48:14 +发件人: crush wds +主题: +内容: +4.11 + +-------------------------------------------------- +时间: 2025-04-11 10:49:24 +发件人: crush wds +主题: +内容: +4.11 2 + +-------------------------------------------------- +时间: 2025-04-11 10:50:13 +发件人: crush wds +主题: +内容: +休息休息 + +-------------------------------------------------- +时间: 2025-04-11 10:56:14 +发件人: crush wds +主题: +内容: +反反复复 + +-------------------------------------------------- +时间: 2025-04-11 13:46:49 +发件人: crush wds +主题: +内容: +0202我是01 + +-------------------------------------------------- diff --git a/gmail/gmail_read.py b/gmail/gmail_read.py deleted file mode 100644 index c568f7f..0000000 --- a/gmail/gmail_read.py +++ /dev/null @@ -1,146 +0,0 @@ -''' -Reading GMAIL using Python - - Abhishek Chhibber -''' - -''' -This script does the following: -- Go to Gmal inbox -- Find and read all the unread messages -- Extract details (Date, Sender, Subject, Snippet, Body) and export them to a .csv file / DB -- Mark the messages as Read - so that they are not read again -''' - -''' -Before running this script, the user should get the authentication by following -the link: https://developers.google.com/gmail/api/quickstart/python -Also, credentials.json should be saved in the same directory as this file -''' - -# Importing required libraries -from apiclient import discovery -from apiclient import errors -from httplib2 import Http -from oauth2client import file, client, tools -import base64 -from bs4 import BeautifulSoup -import re -import time -import dateutil.parser as parser -from datetime import datetime -import datetime -import csv - - -# Creating a storage.JSON file with authentication details -SCOPES = 'https://www.googleapis.com/auth/gmail.modify' # we are using modify and not readonly, as we will be marking the messages Read -store = file.Storage('storage.json') -creds = store.get() -if not creds or creds.invalid: - flow = client.flow_from_clientsecrets('credentials.json', SCOPES) - creds = tools.run_flow(flow, store) -GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http())) - -user_id = 'me' -label_id_one = 'INBOX' -label_id_two = 'UNREAD' - -# Getting all the unread messages from Inbox -# labelIds can be changed accordingly -unread_msgs = GMAIL.users().messages().list(userId='me',labelIds=[label_id_one, label_id_two]).execute() - -# We get a dictonary. Now reading values for the key 'messages' -mssg_list = unread_msgs['messages'] - -print ("Total unread messages in inbox: ", str(len(mssg_list))) - -final_list = [ ] - - -for mssg in mssg_list: - temp_dict = { } - m_id = mssg['id'] # get id of individual message - message = GMAIL.users().messages().get(userId=user_id, id=m_id).execute() # fetch the message using API - payld = message['payload'] # get payload of the message - headr = payld['headers'] # get header of the payload - - - for one in headr: # getting the Subject - if one['name'] == 'Subject': - msg_subject = one['value'] - temp_dict['Subject'] = msg_subject - else: - pass - - - for two in headr: # getting the date - if two['name'] == 'Date': - msg_date = two['value'] - date_parse = (parser.parse(msg_date)) - m_date = (date_parse.date()) - temp_dict['Date'] = str(m_date) - else: - pass - - for three in headr: # getting the Sender - if three['name'] == 'From': - msg_from = three['value'] - temp_dict['Sender'] = msg_from - else: - pass - - temp_dict['Snippet'] = message['snippet'] # fetching message snippet - - - try: - - # Fetching message body - mssg_parts = payld['parts'] # fetching the message parts - part_one = mssg_parts[0] # fetching first element of the part - part_body = part_one['body'] # fetching body of the message - part_data = part_body['data'] # fetching data from the body - clean_one = part_data.replace("-","+") # decoding from Base64 to UTF-8 - clean_one = clean_one.replace("_","/") # decoding from Base64 to UTF-8 - clean_two = base64.b64decode (bytes(clean_one, 'UTF-8')) # decoding from Base64 to UTF-8 - soup = BeautifulSoup(clean_two , "lxml" ) - mssg_body = soup.body() - # mssg_body is a readible form of message body - # depending on the end user's requirements, it can be further cleaned - # using regex, beautiful soup, or any other method - temp_dict['Message_body'] = mssg_body - - except : - pass - - print (temp_dict) - final_list.append(temp_dict) # This will create a dictonary item in the final list - - # This will mark the messagea as read - GMAIL.users().messages().modify(userId=user_id, id=m_id,body={ 'removeLabelIds': ['UNREAD']}).execute() - - - - -print ("Total messaged retrived: ", str(len(final_list))) - -''' - -The final_list will have dictionary in the following format: - -{ 'Sender': '"email.com" ', - 'Subject': 'Lorem ipsum dolor sit ametLorem ipsum dolor sit amet', - 'Date': 'yyyy-mm-dd', - 'Snippet': 'Lorem ipsum dolor sit amet' - 'Message_body': 'Lorem ipsum dolor sit amet'} - - -The dictionary can be exported as a .csv or into a databse -''' - -#exporting the values as .csv -with open('CSV_NAME.csv', 'w', encoding='utf-8', newline = '') as csvfile: - fieldnames = ['Sender','Subject','Date','Snippet','Message_body'] - writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter = ',') - writer.writeheader() - for val in final_list: - writer.writerow(val) diff --git a/gmail/requirements.txt b/gmail/requirements.txt deleted file mode 100644 index 34aaede..0000000 --- a/gmail/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -google-api-python-client==1.7.8 -google-auth-httplib2==0.0.3 -google-auth-oauthlib==0.4.0 \ No newline at end of file diff --git a/gmail/storage.json b/gmail/storage.json index 8903fb2..d730417 100644 --- a/gmail/storage.json +++ b/gmail/storage.json @@ -1 +1 @@ -{"access_token": "ya29.a0AZYkNZga-tjDnp1lsXRohu1Tji-eVV88RaLnPjxr3HpYuBDW_6boys1aqnRnete1pT-E7ygZ5drpb0Hhbt9o15ryqbfeaKqS4HTDG_iIVvFn3npNNLSqIdvsf98burhBOnR-Nf6ty7xCsPLyFaO15bG2LybRgGL1mubVNMXSaCgYKAdQSARISFQHGX2MicVi2eoShd196_WeptFDUZg0175", "client_id": "266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eAXpVapw8WjjCgYIARAAGA4SNwF-L9Irm0iHkQzqzM7Hn39nctE-DOWKTsm89Ge3nG0bfdfqloRvLMiN4YWHEKcDpLdPIuZel0Q", "token_expiry": "2025-04-10T09:51:34Z", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "ya29.a0AZYkNZga-tjDnp1lsXRohu1Tji-eVV88RaLnPjxr3HpYuBDW_6boys1aqnRnete1pT-E7ygZ5drpb0Hhbt9o15ryqbfeaKqS4HTDG_iIVvFn3npNNLSqIdvsf98burhBOnR-Nf6ty7xCsPLyFaO15bG2LybRgGL1mubVNMXSaCgYKAdQSARISFQHGX2MicVi2eoShd196_WeptFDUZg0175", "expires_in": 3599, "refresh_token": "1//0eAXpVapw8WjjCgYIARAAGA4SNwF-L9Irm0iHkQzqzM7Hn39nctE-DOWKTsm89Ge3nG0bfdfqloRvLMiN4YWHEKcDpLdPIuZel0Q", "scope": "https://mail.google.com/", "token_type": "Bearer", "refresh_token_expires_in": 604799}, "scopes": ["https://mail.google.com/"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"} \ No newline at end of file +{"access_token": "ya29.a0AZYkNZj4ckWMF2gR1vQBVIrPGpcwFCOP_Ent8_epgJhS1d13ta5pdFpnMmrE3CnWmGiPL2cF83f7e4TKR7yVmW_sPaWmVGEnBVz_fIS2EbZrq27m9b6hBEVCNTHbTA_wMueySVfSxB-dewQdPIxMIG-cySbjmcOMM7xHlj-HaCgYKAd8SARYSFQHGX2MirZiNWqjAqxHPPwmxuaGUSA0175", "client_id": "266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eoW48WnH7cnKCgYIARAAGA4SNwF-L9Irq1-3Mr1DScClOG7KrCl5rBxxgTk3pnUATsv1nQo9x7wZczlih6oFkmaJRkCBwKQdAhM", "token_expiry": "2025-04-17T08:42:47Z", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "ya29.a0AZYkNZj4ckWMF2gR1vQBVIrPGpcwFCOP_Ent8_epgJhS1d13ta5pdFpnMmrE3CnWmGiPL2cF83f7e4TKR7yVmW_sPaWmVGEnBVz_fIS2EbZrq27m9b6hBEVCNTHbTA_wMueySVfSxB-dewQdPIxMIG-cySbjmcOMM7xHlj-HaCgYKAd8SARYSFQHGX2MirZiNWqjAqxHPPwmxuaGUSA0175", "expires_in": 3599, "refresh_token": "1//0eoW48WnH7cnKCgYIARAAGA4SNwF-L9Irq1-3Mr1DScClOG7KrCl5rBxxgTk3pnUATsv1nQo9x7wZczlih6oFkmaJRkCBwKQdAhM", "scope": "https://mail.google.com/", "token_type": "Bearer"}, "scopes": ["https://mail.google.com/"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"} \ No newline at end of file diff --git a/gmail/test.txt b/gmail/test.txt deleted file mode 100644 index e6076a0..0000000 --- a/gmail/test.txt +++ /dev/null @@ -1 +0,0 @@ -你好 diff --git a/gmail_tokens/gmail_token_2290c2f1-06fc-42ef-9c5f-234d6a2754b7.json b/gmail_tokens/gmail_token_2290c2f1-06fc-42ef-9c5f-234d6a2754b7.json new file mode 100644 index 0000000..fe43fa3 --- /dev/null +++ b/gmail_tokens/gmail_token_2290c2f1-06fc-42ef-9c5f-234d6a2754b7.json @@ -0,0 +1 @@ +{"access_token": "ya29.a0AZYkNZiIIeDZSsXkxaKmK66_GECsMkKxlXHMs5JSGuNUg0HM_-WEy0lM9iW4s_0m9-KOpvRpPfOytHgUjVmu-iS_CeAb7YjIkInauRkm1kWoi3YO-MpyE4k2E-VYN_EToxPQ7kOFgzmuzmgD7YzzrhkQUbYfwRR7avB23QIYaCgYKAX0SARYSFQHGX2Mi4OfTzGLIiHgiikzkvqlHQw0175", "client_id": "266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eNMbEHj3Tg2eCgYIARAAGA4SNwF-L9IrPoT7E3X2KyYx_ImbGl_en_kWItMvImasMtmWaX5QY1FFwWjv9qlP0aUOCmDatlQIn9M", "token_expiry": "2025-04-17T09:12:42Z", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "ya29.a0AZYkNZiIIeDZSsXkxaKmK66_GECsMkKxlXHMs5JSGuNUg0HM_-WEy0lM9iW4s_0m9-KOpvRpPfOytHgUjVmu-iS_CeAb7YjIkInauRkm1kWoi3YO-MpyE4k2E-VYN_EToxPQ7kOFgzmuzmgD7YzzrhkQUbYfwRR7avB23QIYaCgYKAX0SARYSFQHGX2Mi4OfTzGLIiHgiikzkvqlHQw0175", "expires_in": 3599, "refresh_token": "1//0eNMbEHj3Tg2eCgYIARAAGA4SNwF-L9IrPoT7E3X2KyYx_ImbGl_en_kWItMvImasMtmWaX5QY1FFwWjv9qlP0aUOCmDatlQIn9M", "scope": "https://mail.google.com/", "token_type": "Bearer"}, "scopes": ["https://mail.google.com/"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"} \ No newline at end of file diff --git a/gmail_tokens/gmail_token_a59c7c43-904a-4bab-9147-492137f8114f.json b/gmail_tokens/gmail_token_a59c7c43-904a-4bab-9147-492137f8114f.json new file mode 100644 index 0000000..bf82a73 --- /dev/null +++ b/gmail_tokens/gmail_token_a59c7c43-904a-4bab-9147-492137f8114f.json @@ -0,0 +1 @@ +{"access_token": "ya29.a0AZYkNZjT8BbLKF3B3a5jGq2Qb6uYUBT3honlyAqKAT9PO4QBtvffLXR6Dy7Uhfn0ECZAyo-APdhM29oEYH6mGyIilb8zwRhkagyz7g0SfaJ9TBR6OewrkAhLqxKomAFG3Zy1TuGKAJ2Ql_NpdYTnMMvBXlvSbJxxUqCvc6aPaCgYKAXsSARYSFQHGX2MiFkN-OJM7jUydE6eNoUjSaA0175", "client_id": "266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eEj3chLf7z1fCgYIARAAGA4SNwF-L9IrdLb5aMU_EPeZV23OAzUmbg73hwpBGGzr0sT6QCtnLJPsBuInx40cZX9C-WZeBbUaa8s", "token_expiry": "2025-04-17T09:00:48Z", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "ya29.a0AZYkNZjT8BbLKF3B3a5jGq2Qb6uYUBT3honlyAqKAT9PO4QBtvffLXR6Dy7Uhfn0ECZAyo-APdhM29oEYH6mGyIilb8zwRhkagyz7g0SfaJ9TBR6OewrkAhLqxKomAFG3Zy1TuGKAJ2Ql_NpdYTnMMvBXlvSbJxxUqCvc6aPaCgYKAXsSARYSFQHGX2MiFkN-OJM7jUydE6eNoUjSaA0175", "expires_in": 3599, "refresh_token": "1//0eEj3chLf7z1fCgYIARAAGA4SNwF-L9IrdLb5aMU_EPeZV23OAzUmbg73hwpBGGzr0sT6QCtnLJPsBuInx40cZX9C-WZeBbUaa8s", "scope": "https://mail.google.com/", "token_type": "Bearer"}, "scopes": ["https://mail.google.com/"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"} \ No newline at end of file diff --git a/role_based_system/urls.py b/role_based_system/urls.py index 1f8ff5f..5fd4f74 100644 --- a/role_based_system/urls.py +++ b/role_based_system/urls.py @@ -18,7 +18,8 @@ from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static -from user_management.views import gmail_webhook, feishu_sync_api, feishu_to_kb_api, check_creator_kb_api # 直接导入视图函数 +from user_management.views import gmail_webhook, feishu_sync_api, feishu_to_kb_api, check_creator_kb_api +from user_management.feishu_chat_views import process_feishu_table, run_auto_chat, feishu_user_goal, check_goal_status urlpatterns = [ # 管理后台 @@ -36,6 +37,12 @@ urlpatterns = [ path('api/feishu/to_kb', feishu_to_kb_api, name='feishu_to_kb_api'), path('api/feishu/check_kb', check_creator_kb_api, name='check_creator_kb_api'), + # 飞书AI聊天相关API - 直接注册 + path('api/feishu/process-table/', process_feishu_table, name='direct_process_feishu_table'), + path('api/feishu/auto-chat/', run_auto_chat, name='direct_run_auto_chat'), + path('api/feishu/user-goal/', feishu_user_goal, name='direct_feishu_user_goal'), + path('api/feishu/check-goal/', check_goal_status, name='direct_check_goal_status'), + # 媒体文件服务 *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), diff --git a/user_management/consumers.py b/user_management/consumers.py index f37ace2..c5b82a6 100644 --- a/user_management/consumers.py +++ b/user_management/consumers.py @@ -4,6 +4,7 @@ from channels.db import database_sync_to_async from channels.exceptions import StopConsumer import logging from rest_framework.authtoken.models import Token +from urllib.parse import parse_qs logger = logging.getLogger(__name__) @@ -11,19 +12,20 @@ class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): """建立WebSocket连接""" try: - # 获取token - headers = dict(self.scope['headers']) - auth_header = headers.get(b'authorization', b'').decode() + # 从URL参数中获取token + query_string = self.scope.get('query_string', b'').decode() + query_params = parse_qs(query_string) + token_key = query_params.get('token', [''])[0] - if not auth_header.startswith('Token '): + if not token_key: + logger.warning("WebSocket连接尝试,但没有提供token") await self.close() return - token_key = auth_header.split(' ')[1] - # 验证token self.user = await self.get_user_from_token(token_key) if not self.user: + logger.warning(f"WebSocket连接尝试,但token无效: {token_key}") await self.close() return @@ -34,8 +36,10 @@ class NotificationConsumer(AsyncWebsocketConsumer): self.channel_name ) await self.accept() + logger.info(f"用户 {self.user.username} WebSocket连接成功") except Exception as e: + logger.error(f"WebSocket连接错误: {str(e)}") await self.close() @database_sync_to_async diff --git a/user_management/feishu_chat_views.py b/user_management/feishu_chat_views.py new file mode 100644 index 0000000..e76c847 --- /dev/null +++ b/user_management/feishu_chat_views.py @@ -0,0 +1,434 @@ +import logging +import traceback +from rest_framework import status +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from user_management.models import ( + GmailTalentMapping, ChatHistory, ConversationSummary +) +from user_management.gmail_integration import GmailIntegration + +from feishu.feishu_ai_chat import ( + fetch_table_records, find_duplicate_email_creators, + process_duplicate_emails, auto_chat_session, + check_goal_achieved +) + +logger = logging.getLogger(__name__) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def process_feishu_table(request): + """ + 从飞书多维表格读取数据,处理重复邮箱 + + 请求参数: + table_id: 表格ID + view_id: 视图ID + app_token: 飞书应用TOKEN (可选) + access_token: 用户访问令牌 (可选) + app_id: 应用ID (可选) + app_secret: 应用密钥 (可选) + goal_template: 目标内容模板 (可选) + auto_chat: 是否自动执行AI对话 (可选) + turns: 自动对话轮次 (可选) + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + # 获取参数 + table_id = request.data.get("table_id", "tbl3oikG3F8YYtVA") # 默认表格ID + view_id = request.data.get("view_id", "vewSOIsmxc") # 默认视图ID + app_token = request.data.get("app_token", "XYE6bMQUOaZ5y5svj4vcWohGnmg") + access_token = request.data.get("access_token", "u-fK0HvbXVte.G2xzYs5oxV6k1nHu1glvFgG00l0Ma24VD") + app_id = request.data.get("app_id", "cli_a5c97daacb9e500d") + app_secret = request.data.get("app_secret", "fdVeOCLXmuIHZVmSV0VbJh9wd0Kq1o5y") + goal_template = request.data.get( + "goal_template", + "与达人{handle}(邮箱:{email})建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。" + ) + auto_chat = request.data.get("auto_chat", False) + turns = request.data.get("turns", 5) + + logger.info(f"处理飞书表格数据: table_id={table_id}, view_id={view_id}, app_id={app_id}") + + # 从飞书表格获取记录 + records = fetch_table_records( + app_token, + table_id, + view_id, + access_token, + app_id, + app_secret + ) + + if not records: + logger.warning("未获取到任何记录,可能是表格ID或视图ID不正确,或无权限访问") + + # 尝试使用SDK中的search方法直接获取 + try: + import lark_oapi as lark + from lark_oapi.api.bitable.v1 import ( + SearchAppTableRecordRequest, + SearchAppTableRecordRequestBody + ) + + # 创建client + client = lark.Client.builder() \ + .enable_set_token(True) \ + .log_level(lark.LogLevel.DEBUG) \ + .build() + + # 构造请求对象 + request = SearchAppTableRecordRequest.builder() \ + .app_token(app_token) \ + .table_id(table_id) \ + .page_size(20) \ + .request_body(SearchAppTableRecordRequestBody.builder().build()) \ + .build() + + # 发起请求 + option = lark.RequestOption.builder().user_access_token(access_token).build() + response = client.bitable.v1.app_table_record.search(request, option) + + if not response.success(): + logger.error(f"直接搜索请求失败: {response.code}, {response.msg}") + return Response( + {"message": f"未获取到任何记录,错误: {response.msg}"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 获取记录 + records = response.data.items + if not records: + return Response( + {"message": "未获取到任何记录"}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger.error(f"尝试直接搜索时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"message": f"未获取到任何记录,错误: {str(e)}"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 查找重复邮箱的创作者 + duplicate_emails = find_duplicate_email_creators(records) + + if not duplicate_emails: + return Response( + {"message": "未发现重复邮箱"}, + status=status.HTTP_200_OK + ) + + # 处理重复邮箱记录 + results = process_duplicate_emails(duplicate_emails, goal_template) + + # 如果需要自动对话 + chat_results = [] + if auto_chat and results['success'] > 0: + # 为每个成功创建的记录执行自动对话 + for detail in results['details']: + if detail['status'] == 'success': + email = detail['email'] + chat_result = auto_chat_session(request.user, email, max_turns=turns) + chat_results.append({ + 'email': email, + 'result': chat_result + }) + + # 返回处理结果 + return Response({ + 'status': 'success', + 'records_count': len(records), + 'duplicate_emails_count': len(duplicate_emails), + 'processing_results': results, + 'chat_results': chat_results + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"处理飞书表格时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def run_auto_chat(request): + """ + 为指定邮箱执行自动对话 + + 请求参数: + email: 达人邮箱 + force_send: 是否强制发送新邮件(即使没有新回复)(可选) + subject: 邮件主题(可选,仅当force_send=true时使用) + content: 邮件内容(可选,仅当force_send=true时使用) + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + # 获取参数 + email = request.data.get("email") + force_send = request.data.get("force_send", False) + subject = request.data.get("subject") + content = request.data.get("content") + + # 验证必要参数 + if not email: + return Response( + {"error": "缺少参数email"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 如果强制发送且没有提供内容 + if force_send and not content: + return Response( + {"error": "当force_send=true时,必须提供content参数"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 首先尝试同步最新邮件 + try: + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(request.user) + + # 同步最新邮件 + logger.info(f"正在同步与 {email} 的最新邮件...") + sync_result = gmail_integration.sync_talent_emails(email) + + if sync_result.get('status') == 'success': + logger.info(f"成功同步邮件: {sync_result.get('message', 'No message')}") + else: + logger.warning(f"同步邮件警告: {sync_result.get('message', 'Unknown warning')}") + + except Exception as e: + logger.error(f"同步邮件出错: {str(e)}") + logger.error(traceback.format_exc()) + # 仅记录错误,不中断流程 + + # 如果是强制发送模式 + if force_send: + try: + # 获取知识库映射 + mapping = GmailTalentMapping.objects.filter( + user=request.user, + talent_email=email, + is_active=True + ).first() + + if not mapping: + return Response( + {"error": f"找不到与邮箱 {email} 的映射关系"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 直接发送邮件 + mail_subject = subject if subject else "关于合作的洽谈" + mail_result = gmail_integration.send_email( + to_email=email, + subject=mail_subject, + body=content, + conversation_id=mapping.conversation_id + ) + + if mail_result['status'] != 'success': + return Response( + {"error": f"邮件发送失败: {mail_result.get('message', 'Unknown error')}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 保存发送的内容到对话历史 + ChatHistory.objects.create( + user=request.user, + knowledge_base=mapping.knowledge_base, + conversation_id=mapping.conversation_id, + role='assistant', + content=content + ) + + return Response({ + 'status': 'success', + 'message': f"已强制发送邮件到 {email}", + 'email_sent': True, + 'conversation_id': mapping.conversation_id + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"强制发送邮件时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # 执行自动对话 + result = auto_chat_session(request.user, email) + + # 返回结果 + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"执行自动对话时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +def feishu_user_goal(request): + """ + 设置或获取用户总目标 + + GET 请求: + 获取当前用户总目标 + + POST 请求参数: + email: 达人邮箱 + goal: 目标内容 + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + if request.method == 'GET': + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(request.user) + # 获取总目标 + result = gmail_integration.manage_user_goal() + return Response(result, status=status.HTTP_200_OK) + + elif request.method == 'POST': + # 获取参数 + email = request.data.get("email") + goal = request.data.get("goal") + + # 验证必要参数 + if not email: + return Response( + {"error": "缺少参数email"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not goal: + return Response( + {"error": "缺少参数goal"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 设置用户总目标 + gmail_integration = GmailIntegration(request.user) + result = gmail_integration.manage_user_goal(goal) + + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"管理用户总目标时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def check_goal_status(request): + """ + 检查目标完成状态 + + 请求参数: + email: 达人邮箱 + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + # 获取参数 + email = request.query_params.get("email") + + # 验证必要参数 + if not email: + return Response( + {"error": "缺少参数email"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 查找Gmail映射关系 + mapping = GmailTalentMapping.objects.filter( + user=request.user, + talent_email=email, + is_active=True + ).first() + + if not mapping: + return Response( + {"error": f"找不到与邮箱 {email} 的映射关系"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 获取对话历史中最后的AI回复 + last_ai_message = ChatHistory.objects.filter( + user=request.user, + knowledge_base=mapping.knowledge_base, + conversation_id=mapping.conversation_id, + role='assistant', + is_deleted=False + ).order_by('-created_at').first() + + if not last_ai_message: + return Response( + {"error": f"找不到与邮箱 {email} 的对话历史"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 检查目标是否已达成 + goal_achieved = check_goal_achieved(last_ai_message.content) + + # 获取对话总结 + summary = ConversationSummary.objects.filter( + user=request.user, + talent_email=email, + is_active=True + ).order_by('-updated_at').first() + + result = { + 'status': 'success', + 'email': email, + 'goal_achieved': goal_achieved, + 'last_message_time': last_ai_message.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'last_message': last_ai_message.content, + 'summary': summary.summary if summary else None + } + + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"检查目标状态时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file diff --git a/user_management/gmail_integration.py b/user_management/gmail_integration.py index 5d72614..986941d 100644 --- a/user_management/gmail_integration.py +++ b/user_management/gmail_integration.py @@ -18,6 +18,7 @@ from email import encoders import warnings import mimetypes import requests +import django.db.utils # 忽略oauth2client相关的所有警告 warnings.filterwarnings('ignore', message='file_cache is unavailable when using oauth2client >= 4.0.0 or google-auth') @@ -175,12 +176,23 @@ class GmailIntegration: try: # 确保是有效的JSON json_data = json.loads(self.client_secret) + # 强制设置redirect_uris为非浏览器模式,避免localhost连接拒绝问题 + for key in ['web', 'installed']: + if key in json_data and 'redirect_uris' in json_data[key]: + json_data[key]['redirect_uris'] = ['urn:ietf:wg:oauth:2.0:oob'] + logger.info("已强制设置redirect_uri为非浏览器模式") json.dump(json_data, f) except json.JSONDecodeError as e: logger.error(f"client_secret不是有效的JSON: {str(e)}") return False else: - json.dump(self.client_secret, f) + # 如果是字典,也进行相同处理 + client_secret_dict = dict(self.client_secret) + for key in ['web', 'installed']: + if key in client_secret_dict and 'redirect_uris' in client_secret_dict[key]: + client_secret_dict[key]['redirect_uris'] = ['urn:ietf:wg:oauth:2.0:oob'] + logger.info("已强制设置redirect_uri为非浏览器模式") + json.dump(client_secret_dict, f) logger.info(f"已将client_secret写入临时文件: {client_secret_path}") @@ -202,33 +214,11 @@ class GmailIntegration: logger.error("没有提供client_secret_json且找不到有效凭证") return False - # 提取重定向URI - redirect_uri = None - if isinstance(self.client_secret, dict): - for key in ['web', 'installed']: - if key in self.client_secret and 'redirect_uris' in self.client_secret[key]: - redirect_uri = self.client_secret[key]['redirect_uris'][0] - break - elif isinstance(self.client_secret, str): - try: - json_data = json.loads(self.client_secret) - for key in ['web', 'installed']: - if key in json_data and 'redirect_uris' in json_data[key]: - redirect_uri = json_data[key]['redirect_uris'][0] - break - except: - pass - - # 如果找不到重定向URI,使用默认值 - if not redirect_uri or redirect_uri == 'urn:ietf:wg:oauth:2.0:oob': - logger.info("使用非浏览器认证模式") - redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' - flow = client.flow_from_clientsecrets('client_secret.json', self.SCOPES) - flow.redirect_uri = redirect_uri - else: - logger.info(f"使用重定向URI: {redirect_uri}") - flow = client.flow_from_clientsecrets('client_secret.json', self.SCOPES) - flow.redirect_uri = redirect_uri + # 强制使用非浏览器认证模式,避免localhost连接问题 + logger.info("使用非浏览器认证模式") + redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' + flow = client.flow_from_clientsecrets('client_secret.json', self.SCOPES) + flow.redirect_uri = redirect_uri # 获取授权URL并抛出异常 auth_url = flow.step1_get_authorize_url() @@ -375,15 +365,7 @@ class GmailIntegration: return self._create_knowledge_base_basic(talent_email) def _create_knowledge_base_basic(self, talent_email): - """ - 使用基本方法创建知识库(当KnowledgeBaseViewSet创建失败时的后备方案) - - Args: - talent_email (str): 达人Gmail邮箱地址 - - Returns: - tuple: (knowledge_base, created) - 知识库对象和是否新创建的标志 - """ + """创建基础知识库,不处理映射关系""" try: # 根据达人邮箱生成一个唯一的标识名称 kb_name = f"Gmail-{talent_email.split('@')[0]}" @@ -410,13 +392,51 @@ class GmailIntegration: return existing_kb, False # 创建新知识库 - knowledge_base = KnowledgeBase.objects.create( - name=kb_name, - desc=f"与{talent_email}的Gmail邮件交流记录", - type="private", - user_id=self.user.id, - documents=[] - ) + try: + knowledge_base = KnowledgeBase.objects.create( + name=kb_name, + desc=f"与{talent_email}的Gmail邮件交流记录", + type="private", + user_id=self.user.id, + documents=[] + ) + except django.db.utils.IntegrityError as e: + # 处理名称重复的情况 + logger.warning(f"知识库名称'{kb_name}'已存在,尝试获取或创建带随机后缀的名称") + + # 先尝试查找已存在的知识库(不限制用户) + existing_kb = KnowledgeBase.objects.filter(name=kb_name).first() + + if existing_kb and str(existing_kb.user_id) == str(self.user.id): + # 如果存在且属于当前用户,直接使用 + logger.info(f"找到属于当前用户的知识库: {kb_name}") + + # 创建映射关系 + GmailTalentMapping.objects.update_or_create( + user=self.user, + talent_email=talent_email, + defaults={ + 'knowledge_base': existing_kb, + 'is_active': True + } + ) + + return existing_kb, False + else: + # 如果不存在或不属于当前用户,创建带随机后缀的新知识库 + import random + import string + suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=5)) + new_kb_name = f"{kb_name}-{suffix}" + + logger.info(f"创建带随机后缀的知识库: {new_kb_name}") + knowledge_base = KnowledgeBase.objects.create( + name=new_kb_name, + desc=f"与{talent_email}的Gmail邮件交流记录", + type="private", + user_id=self.user.id, + documents=[] + ) # 创建外部知识库 try: @@ -439,13 +459,37 @@ class GmailIntegration: is_active=True ) - logger.info(f"成功创建新知识库: {kb_name}, ID: {knowledge_base.id}") + logger.info(f"成功创建新知识库: {knowledge_base.name}, ID: {knowledge_base.id}") return knowledge_base, True except Exception as e: logger.error(f"创建或获取Gmail-达人知识库基本方法失败: {str(e)}") import traceback logger.error(traceback.format_exc()) + + # 尝试直接获取已存在的知识库作为最后手段 + try: + kb_name = f"Gmail-{talent_email.split('@')[0]}" + existing_kb = KnowledgeBase.objects.filter(name__startswith=kb_name).first() + + if existing_kb: + logger.info(f"在错误处理中找到可用的知识库: {existing_kb.name}") + + # 创建映射关系 + GmailTalentMapping.objects.update_or_create( + user=self.user, + talent_email=talent_email, + defaults={ + 'knowledge_base': existing_kb, + 'is_active': True + } + ) + + return existing_kb, False + except Exception as inner_e: + logger.error(f"错误处理中尝试获取知识库失败: {str(inner_e)}") + + # 如果所有尝试都失败,抛出异常 raise def get_conversations(self, talent_gmail): @@ -1991,13 +2035,24 @@ class GmailIntegration: try: # 确保是有效的JSON json_data = json.loads(self.client_secret) + # 强制设置redirect_uris为非浏览器模式,避免localhost连接拒绝问题 + for key in ['web', 'installed']: + if key in json_data and 'redirect_uris' in json_data[key]: + json_data[key]['redirect_uris'] = ['urn:ietf:wg:oauth:2.0:oob'] + logger.info("已强制设置redirect_uri为非浏览器模式") json.dump(json_data, f) except json.JSONDecodeError as e: logger.error(f"client_secret不是有效的JSON: {str(e)}") return False else: logger.info("client_secret是字典,直接写入文件") - json.dump(self.client_secret, f) + # 如果是字典,也进行相同处理 + client_secret_dict = dict(self.client_secret) + for key in ['web', 'installed']: + if key in client_secret_dict and 'redirect_uris' in client_secret_dict[key]: + client_secret_dict[key]['redirect_uris'] = ['urn:ietf:wg:oauth:2.0:oob'] + logger.info("已强制设置redirect_uri为非浏览器模式") + json.dump(client_secret_dict, f) logger.info(f"已将client_secret写入临时文件: {client_secret_path}") @@ -2012,28 +2067,9 @@ class GmailIntegration: logger.info(f"设置token存储: {self.token_storage_path}") store = file.Storage(self.token_storage_path) - # 提取重定向URI - redirect_uri = None - if isinstance(self.client_secret, dict): - for key in ['web', 'installed']: - if key in self.client_secret and 'redirect_uris' in self.client_secret[key]: - redirect_uri = self.client_secret[key]['redirect_uris'][0] - break - elif isinstance(self.client_secret, str): - try: - json_data = json.loads(self.client_secret) - for key in ['web', 'installed']: - if key in json_data and 'redirect_uris' in json_data[key]: - redirect_uri = json_data[key]['redirect_uris'][0] - break - except: - pass - - # 如果找不到重定向URI,使用默认值 - if not redirect_uri: - redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' - - logger.info(f"使用重定向URI: {redirect_uri}") + # 强制使用非浏览器认证模式 + redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' + logger.info(f"使用非浏览器认证模式: {redirect_uri}") # 从client_secret创建flow logger.info("从client_secret创建授权流程") diff --git a/user_management/urls.py b/user_management/urls.py index 07e1c8f..8e1b645 100644 --- a/user_management/urls.py +++ b/user_management/urls.py @@ -29,6 +29,12 @@ from .views import ( generate_conversation_summary, get_recommended_reply ) +from .feishu_chat_views import ( + process_feishu_table, + run_auto_chat, + feishu_user_goal, + check_goal_status +) # 创建路由器 router = DefaultRouter() @@ -74,4 +80,10 @@ urlpatterns = [ path('user-goal/', manage_user_goal, name='manage_user_goal'), path('conversation-summary/', generate_conversation_summary, name='generate_conversation_summary'), path('recommended-reply/', get_recommended_reply, name='get_recommended_reply'), + + # 飞书AI聊天相关API + path('feishu/process-table/', process_feishu_table, name='process_feishu_table'), + path('feishu/auto-chat/', run_auto_chat, name='run_auto_chat'), + path('feishu/user-goal/', feishu_user_goal, name='feishu_user_goal'), + path('feishu/check-goal/', check_goal_status, name='check_goal_status'), ] diff --git a/user_management/views.py b/user_management/views.py index 2d1c65c..41a4b3e 100644 --- a/user_management/views.py +++ b/user_management/views.py @@ -6916,3 +6916,301 @@ def check_creator_kb_api(request): 'data': None }, status=500) +from feishu.feishu_ai_chat import ( + fetch_table_records, find_duplicate_email_creators, + process_duplicate_emails, auto_chat_session, + handle_set_goal, handle_check_goal +) + +# 添加飞书多维表格AI对话接口 +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def process_feishu_table(request): + """ + 从飞书多维表格读取数据,处理重复邮箱 + + 请求参数: + table_id: 表格ID + view_id: 视图ID + app_token: 飞书应用TOKEN (可选) + access_token: 用户访问令牌 (可选) + goal_template: 目标内容模板 (可选) + auto_chat: 是否自动执行AI对话 (可选) + turns: 自动对话轮次 (可选) + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + # 获取参数 + table_id = request.data.get("table_id") + view_id = request.data.get("view_id") + app_token = request.data.get("app_token", "XYE6bMQUOaZ5y5svj4vcWohGnmg") + access_token = request.data.get("access_token", "u-ecM5BmzKx4uHz3sG0FouQSk1l9kxgl_3Xa00l5Ma24Jy") + goal_template = request.data.get( + "goal_template", + "与达人{handle}(邮箱:{email})建立联系并了解其账号情况,评估合作潜力,处理合作需求,最终目标是达成合作并签约。" + ) + auto_chat = request.data.get("auto_chat", False) + turns = request.data.get("turns", 5) + + # 验证必要参数 + if not table_id: + return Response( + {"error": "缺少参数table_id"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not view_id: + return Response( + {"error": "缺少参数view_id"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 从飞书表格获取记录 + records = fetch_table_records( + app_token, + table_id, + view_id, + access_token + ) + + if not records: + return Response( + {"message": "未获取到任何记录"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 查找重复邮箱的创作者 + duplicate_emails = find_duplicate_email_creators(records) + + if not duplicate_emails: + return Response( + {"message": "未发现重复邮箱"}, + status=status.HTTP_200_OK + ) + + # 处理重复邮箱记录 + results = process_duplicate_emails(duplicate_emails, goal_template) + + # 如果需要自动对话 + chat_results = [] + if auto_chat and results['success'] > 0: + # 为每个成功创建的记录执行自动对话 + for detail in results['details']: + if detail['status'] == 'success': + email = detail['email'] + chat_result = auto_chat_session(request.user, email, max_turns=turns) + chat_results.append({ + 'email': email, + 'result': chat_result + }) + + # 返回处理结果 + return Response({ + 'status': 'success', + 'records_count': len(records), + 'duplicate_emails_count': len(duplicate_emails), + 'processing_results': results, + 'chat_results': chat_results + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"处理飞书表格时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def run_auto_chat(request): + """ + 为指定邮箱执行自动对话 + + 请求参数: + email: 达人邮箱 + turns: 对话轮次 (可选) + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + # 获取参数 + email = request.data.get("email") + turns = request.data.get("turns", 5) + + # 验证必要参数 + if not email: + return Response( + {"error": "缺少参数email"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 执行自动对话 + result = auto_chat_session(request.user, email, max_turns=turns) + + # 返回结果 + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"执行自动对话时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +def feishu_user_goal(request): + """ + 设置或获取用户总目标 + + GET 请求: + 获取当前用户总目标 + + POST 请求参数: + email: 达人邮箱 + goal: 目标内容 + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + if request.method == 'GET': + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(request.user) + # 获取总目标 + result = gmail_integration.manage_user_goal() + return Response(result, status=status.HTTP_200_OK) + + elif request.method == 'POST': + # 获取参数 + email = request.data.get("email") + goal = request.data.get("goal") + + # 验证必要参数 + if not email: + return Response( + {"error": "缺少参数email"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not goal: + return Response( + {"error": "缺少参数goal"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 设置用户总目标 + gmail_integration = GmailIntegration(request.user) + result = gmail_integration.manage_user_goal(goal) + + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"管理用户总目标时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def check_goal_status(request): + """ + 检查目标完成状态 + + 请求参数: + email: 达人邮箱 + """ + try: + # 检查用户权限 - 只允许组长使用 + if request.user.role != 'leader': + return Response( + {"error": "只有组长角色的用户可以使用此功能"}, + status=status.HTTP_403_FORBIDDEN + ) + + # 获取参数 + email = request.query_params.get("email") + + # 验证必要参数 + if not email: + return Response( + {"error": "缺少参数email"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 查找Gmail映射关系 + mapping = GmailTalentMapping.objects.filter( + user=request.user, + talent_email=email, + is_active=True + ).first() + + if not mapping: + return Response( + {"error": f"找不到与邮箱 {email} 的映射关系"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 获取对话历史中最后的AI回复 + last_ai_message = ChatHistory.objects.filter( + user=request.user, + knowledge_base=mapping.knowledge_base, + conversation_id=mapping.conversation_id, + role='assistant', + is_deleted=False + ).order_by('-created_at').first() + + if not last_ai_message: + return Response( + {"error": f"找不到与邮箱 {email} 的对话历史"}, + status=status.HTTP_404_NOT_FOUND + ) + + # 导入检查函数 + from feishu.feishu_ai_chat import check_goal_achieved + + # 检查目标是否已达成 + goal_achieved = check_goal_achieved(last_ai_message.content) + + # 获取对话总结 + summary = ConversationSummary.objects.filter( + user=request.user, + talent_email=email, + is_active=True + ).order_by('-updated_at').first() + + result = { + 'status': 'success', + 'email': email, + 'goal_achieved': goal_achieved, + 'last_message_time': last_ai_message.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'last_message': last_ai_message.content, + 'summary': summary.summary if summary else None + } + + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"检查目标状态时出错: {str(e)}") + logger.error(traceback.format_exc()) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file