Compare commits

...

3 Commits

Author SHA1 Message Date
wanjia
7ffb0de74c 优化DeepSeek API调用,改进推荐回复功能 2025-04-11 15:22:42 +08:00
wanjia
45d49d4b4a 初始提交 - Gmail集成功能优化 2025-04-10 18:25:59 +08:00
wanjia
083971a94a gmail添加附件 2025-04-07 15:07:20 +08:00
23 changed files with 4338 additions and 47 deletions

View File

@ -0,0 +1,10 @@
本期节目内容简介
在参加各类比赛时,肯定会遇到竞争对手。英语单词 rival、opponent、competitor 和 contestant 的含义相似,都可以用来指“与他人之间存在竞争关系的人或团队”。在本集《你问我答》节目中,我们将通过和体育比赛有关的实例来为大家阐释这四个近义词之间的区别和用法。
欢迎你加入并和我们一起讨论英语学习的方方面面。请通过微博“BBC英语教学”或邮件与我们取得联系。我们的邮箱地址是 questions.chinaelt@bbc.co.uk。
文字稿
(关于台词的备注: 请注意这不是广播节目的逐字稿件。本文稿可能没有体现录制、编辑过程中对节目做出的改变。)
Feifei
大家好,欢迎收听 BBC 英语教学的《你问我答》节目,我是冯菲菲。每集节目中,我们会回答大家在英语学习时遇到的一个问题。本集的问题来自 Adela。我们来听一下她的问题。

View File

@ -1 +1 @@
{"installed":{"client_id":"240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com","project_id":"first-renderer-454910-c1","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"]}}
{"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"]}}

View File

@ -1,20 +1,121 @@
[
{
"id": "195d5c6094b7e85f",
"subject": "这是主题",
"from": "crush wds <ardonisierni@gmail.com>",
"date": "2025-03-27 12:04:29",
"body": "你好呀\r\n"
"body": "你好呀\r\n",
"attachments": []
},
{
"id": "195d5c72dbafcc13",
"subject": "",
"from": "wds crush <crushwds@gmail.com>",
"date": "2025-03-27 12:05:54",
"body": "你那里天气怎么样\r\n"
"body": "你那里天气怎么样\r\n",
"attachments": []
},
{
"id": "195d604a5ad5fabf",
"subject": "",
"from": "wds crush <crushwds@gmail.com>",
"date": "2025-03-27 13:13:03",
"body": "吃饭了吗\r\n"
"body": "吃饭了吗\r\n",
"attachments": []
},
{
"id": "1960eec066745682",
"subject": "Re: 这是主题",
"from": "wds crush <crushwds@gmail.com>",
"date": "2025-04-07 14:24:28",
"body": "测试附件内容\r\n\r\n\r\ncrush wds <ardonisierni@gmail.com> 于2025年3月27日周四 12:04写道\r\n\r\n> 你好呀\r\n>\r\n",
"attachments": [
{
"filename": "test2.txt",
"mimeType": "text/plain",
"size": 996,
"attachmentId": "ANGjdJ8O1uGve4uPqFSC2C4sMeC5jXJ3DGilhB1By705ZLGbOF30m6uITRtJHWsnB7yREKVslhYRdu4GKKvrrkWw-63ogqaZPGgi0WuSoB0OxIWXwbQSjaayUOPvc3P8y1g9A2mMAm5k0DkjH_LP0QMmulhXMg8ZgUcm4CZR7-HTBYztZSfWOoUeYkNugdzV5_2ax3GT34P5uaGHh3i4Ge6y-XDN-TDq3i1w9u_eTjZowoyVzjTj48uaQmzmFU36TQxg8ncovHk0sNZEygzCE1gbHbS1uM9N1tUHOOa6FWEpajHgz2aQwLh65SsMRqKe-LognFES-J_082IHddWs"
}
]
},
{
"id": "1961a1981e915eb3",
"subject": "",
"from": "crush wds <ardonisierni@gmail.com>",
"date": "2025-04-09 18:29:51",
"body": "测试角色1\r\n",
"attachments": []
},
{
"id": "1961a19cc997f933",
"subject": "",
"from": "wds crush <crushwds@gmail.com>",
"date": "2025-04-09 18:30:21",
"body": "测试角色2\r\n",
"attachments": []
},
{
"id": "1961de790e0b39e6",
"subject": "",
"from": "crushwds@gmail.com",
"date": "2025-04-10 00:13:57",
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
"attachments": []
},
{
"id": "1961dee46652dc86",
"subject": "",
"from": "crushwds@gmail.com",
"date": "2025-04-10 00:21:17",
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
"attachments": []
},
{
"id": "1961dfd7d36bf92c",
"subject": "",
"from": "crushwds@gmail.com",
"date": "2025-04-10 00:37:54",
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
"attachments": []
},
{
"id": "1961e39ad02da5f0",
"subject": "",
"from": "crushwds@gmail.com",
"date": "2025-04-10 01:43:38",
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
"attachments": []
},
{
"id": "1961df3bd554a4af",
"subject": "",
"from": "crush wds <ardonisierni@gmail.com>",
"date": "2025-04-10 12:27:03",
"body": "yes\r\n",
"attachments": []
},
{
"id": "1961e3944c8aab04",
"subject": "",
"from": "crush wds <ardonisierni@gmail.com>",
"date": "2025-04-10 13:42:58",
"body": "1\r\n",
"attachments": []
},
{
"id": "1961e39660feebd9",
"subject": "",
"from": "crush wds <ardonisierni@gmail.com>",
"date": "2025-04-10 13:43:07",
"body": "2\r\n",
"attachments": []
},
{
"id": "1961ec2028c16037",
"subject": "",
"from": "crush wds <ardonisierni@gmail.com>",
"date": "2025-04-10 16:12:20",
"body": "你好\r\n",
"attachments": []
}
]

View File

@ -1,5 +1,5 @@
==================================================
记录时间: 2025-03-27 13:15:18
记录时间: 2025-04-10 16:52:49
==================================================
时间: 2025-03-27 12:04:29
@ -23,3 +23,85 @@
吃饭了吗
--------------------------------------------------
时间: 2025-04-07 14:24:28
发件人: wds crush <crushwds@gmail.com>
主题: Re: 这是主题
内容:
测试附件内容
crush wds <ardonisierni@gmail.com> 于2025年3月27日周四 12:04写道
> 你好呀
>
附件:
- test2.txt (text/plain, 996 字节)
--------------------------------------------------
时间: 2025-04-09 18:29:51
发件人: crush wds <ardonisierni@gmail.com>
主题:
内容:
测试角色1
--------------------------------------------------
时间: 2025-04-09 18:30:21
发件人: wds crush <crushwds@gmail.com>
主题:
内容:
测试角色2
--------------------------------------------------
时间: 2025-04-10 00:13:57
发件人: crushwds@gmail.com
主题:
内容:
您好,关于我们之前讨论的产品,我想确认一些细节...
--------------------------------------------------
时间: 2025-04-10 00:21:17
发件人: crushwds@gmail.com
主题:
内容:
您好,关于我们之前讨论的产品,我想确认一些细节...
--------------------------------------------------
时间: 2025-04-10 00:37:54
发件人: crushwds@gmail.com
主题:
内容:
您好,关于我们之前讨论的产品,我想确认一些细节...
--------------------------------------------------
时间: 2025-04-10 01:43:38
发件人: crushwds@gmail.com
主题:
内容:
您好,关于我们之前讨论的产品,我想确认一些细节...
--------------------------------------------------
时间: 2025-04-10 12:27:03
发件人: crush wds <ardonisierni@gmail.com>
主题:
内容:
yes
--------------------------------------------------
时间: 2025-04-10 13:42:58
发件人: crush wds <ardonisierni@gmail.com>
主题:
内容:
1
--------------------------------------------------
时间: 2025-04-10 13:43:07
发件人: crush wds <ardonisierni@gmail.com>
主题:
内容:
2
--------------------------------------------------
时间: 2025-04-10 16:12:20
发件人: crush wds <ardonisierni@gmail.com>
主题:
内容:
你好
--------------------------------------------------

View File

@ -13,7 +13,7 @@ os.environ['HTTP_PROXY'] = 'http://127.0.0.1:7890'
os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
# Gmail API 认证
SCOPES = 'https://www.googleapis.com/auth/gmail.readonly'
SCOPES = ['https://mail.google.com/']
store = file.Storage('storage.json')
creds = store.get()
if not creds or creds.invalid:
@ -21,18 +21,47 @@ if not creds or creds.invalid:
creds = tools.run_flow(flow, store)
GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http()))
def download_attachment(message_id, attachment_id, filename):
"""下载邮件附件"""
try:
attachment = GMAIL.users().messages().attachments().get(
userId='me',
messageId=message_id,
id=attachment_id
).execute()
data = attachment['data']
file_data = base64.urlsafe_b64decode(data)
# 创建附件目录
if not os.path.exists('attachments'):
os.makedirs('attachments')
# 保存附件
filepath = os.path.join('attachments', filename)
with open(filepath, 'wb') as f:
f.write(file_data)
return filepath
except Exception as e:
print(f"Error downloading attachment: {str(e)}")
return None
def get_email_content(message):
"""提取邮件内容"""
try:
message_id = message['id'] # 获取邮件ID
payload = message['payload']
headers = payload['headers']
# 获取邮件基本信息
email_data = {
'id': message_id, # 保存邮件ID
'subject': '',
'from': '',
'date': '',
'body': ''
'body': '',
'attachments': [] # 新增附件列表
}
# 提取头部信息
@ -45,16 +74,37 @@ def get_email_content(message):
date = parser.parse(header['value'])
email_data['date'] = date.strftime('%Y-%m-%d %H:%M:%S')
# 提取邮件正文
if 'parts' in payload:
parts = payload['parts']
# 定义一个递归函数来处理所有部分和附件
def process_parts(parts):
for part in parts:
if part['mimeType'] == 'text/plain':
# 检查是否是附件
if 'filename' in part and part['filename']:
attachment = {
'filename': part['filename'],
'mimeType': part['mimeType'],
'size': part['body'].get('size', 0)
}
# 如果有附件内容数据可以获取附件ID
if 'attachmentId' in part['body']:
attachment['attachmentId'] = part['body']['attachmentId']
email_data['attachments'].append(attachment)
# 处理文本内容
if part['mimeType'] == 'text/plain' and not email_data['body']:
data = part['body'].get('data', '')
if data:
text = base64.urlsafe_b64decode(data).decode('utf-8')
email_data['body'] = text
break
# 递归处理多部分内容
if 'parts' in part:
process_parts(part['parts'])
# 处理邮件正文和附件
if 'parts' in payload:
process_parts(payload['parts'])
elif 'body' in payload:
data = payload['body'].get('data', '')
if data:
@ -123,6 +173,13 @@ def save_conversations(conversations, output_file):
f.write(f"主题: {msg['subject']}\n")
f.write("内容:\n")
f.write(f"{msg['body']}\n")
# 添加附件信息
if msg['attachments']:
f.write("\n附件:\n")
for att in msg['attachments']:
f.write(f" - {att['filename']} ({att['mimeType']}, {att['size']} 字节)\n")
f.write("-" * 50 + "\n")
print(f"对话记录已保存到: {output_file}")
@ -152,18 +209,39 @@ def main():
if conversations:
print(f"找到 {len(conversations)} 条对话记录")
# 保存对话记录(追加模式)
# 统计附件
total_attachments = 0
for msg in conversations:
total_attachments += len(msg['attachments'])
# 保存对话记录
save_conversations(conversations, output_file)
# 打印对话统计
print("\n对话统计:")
print(f"总消息数: {len(conversations)}")
print(f"总附件数: {total_attachments}")
senders = {}
for msg in conversations:
sender = msg['from']
senders[sender] = senders.get(sender, 0) + 1
for sender, count in senders.items():
print(f"{sender}: {count} 条消息")
# 提示用户是否下载附件
if total_attachments > 0:
download_choice = input(f"\n发现 {total_attachments} 个附件,是否下载? (y/n): ")
if download_choice.lower() == 'y':
print("\n开始下载附件...")
downloaded = 0
for msg in conversations:
for att in msg['attachments']:
if 'attachmentId' in att:
filepath = download_attachment(msg['id'], att['attachmentId'], att['filename'])
if filepath:
downloaded += 1
print(f"已下载: {att['filename']} -> {filepath}")
print(f"\n完成! 成功下载了 {downloaded}/{total_attachments} 个附件到 'attachments' 目录")
else:
print("未找到对话记录")

View File

@ -1 +1 @@
{"access_token": "ya29.a0AeXRPp7jfizXj6NfUavzABcDOEH-PD7TckkeCwQWFksLZwkFXqAaVe1gCeZZIWtP4hMD3F2-K7-hdSQAzCiyP7Z_72YPr_r3UDfflr6XOV7czJ937QknW_236kLJK1DDggpB4BFCB1cXtSJa6V4pbyez4lkka021mZWegOdGaCgYKATkSARISFQHGX2Mi7Ua3f90G-JLtvzu2YJgvoA0175", "client_id": "240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eUNnePCc4hEKCgYIARAAGA4SNwF-L9IrIRfxg1JwnMu0WHZAu-uCWT7cx1PncId3bPZr4x53Mo57xGXr2WjiF4szuSQQkeL4hNU", "token_expiry": "2025-03-27T06:13:12Z", "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.a0AeXRPp7jfizXj6NfUavzABcDOEH-PD7TckkeCwQWFksLZwkFXqAaVe1gCeZZIWtP4hMD3F2-K7-hdSQAzCiyP7Z_72YPr_r3UDfflr6XOV7czJ937QknW_236kLJK1DDggpB4BFCB1cXtSJa6V4pbyez4lkka021mZWegOdGaCgYKATkSARISFQHGX2Mi7Ua3f90G-JLtvzu2YJgvoA0175", "expires_in": 3599, "scope": "https://www.googleapis.com/auth/gmail.modify", "token_type": "Bearer", "refresh_token_expires_in": 599654}, "scopes": ["https://www.googleapis.com/auth/gmail.modify"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"}
{"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"}

View File

@ -1 +0,0 @@
{"token": "ya29.a0AeXRPp5MvarP9U4JZ3ZlV_xpIeHNL7IK1HEQLX6qhY-thS5IOFsdHrcNwbdNqkYrClFm4yGiMCXzWO-IilVbdSrDfEDw-Qfs7V9oTsZS2RYjbgcSR9rLUtZlL8ObnaljQie5VVzQZysHPZcMSdWiqabcQq2UsU5TNuOWo4J7_waCgYKAQESARISFQHGX2MiAR1O_HNYd0F6z0Rj-vSybA0177", "refresh_token": "1//0e1Usc30XxlXRCgYIARAAGA4SNwF-L9IrOOjEN7EkOSatDMHzTy0KaNdp_jf1XtLn7a5pFSljo3IKnrDdvVnxTO_FX5Asj_UDiAw", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/gmail.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2025-03-27T04:39:31.274041Z"}

View File

@ -0,0 +1,10 @@
本期节目内容简介
在参加各类比赛时,肯定会遇到竞争对手。英语单词 rival、opponent、competitor 和 contestant 的含义相似,都可以用来指“与他人之间存在竞争关系的人或团队”。在本集《你问我答》节目中,我们将通过和体育比赛有关的实例来为大家阐释这四个近义词之间的区别和用法。
欢迎你加入并和我们一起讨论英语学习的方方面面。请通过微博“BBC英语教学”或邮件与我们取得联系。我们的邮箱地址是 questions.chinaelt@bbc.co.uk。
文字稿
(关于台词的备注: 请注意这不是广播节目的逐字稿件。本文稿可能没有体现录制、编辑过程中对节目做出的改变。)
Feifei
大家好,欢迎收听 BBC 英语教学的《你问我答》节目,我是冯菲菲。每集节目中,我们会回答大家在英语学习时遇到的一个问题。本集的问题来自 Adela。我们来听一下她的问题。

View File

@ -0,0 +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"}

View File

@ -142,7 +142,7 @@ TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = False
USE_TZ = True # 将此项设置为True以启用时区支持
@ -294,3 +294,30 @@ REST_FRAMEWORK = {
'rest_framework.parsers.MultiPartParser'
],
}
# Gmail API配置
GOOGLE_CLOUD_PROJECT = 'knowledge-454905' # 更新为当前使用的项目ID
GMAIL_API_SCOPES = ['https://mail.google.com/']
GMAIL_TOPIC_NAME = 'gmail-watch-topic'
# Gmail webhook地址 (开发环境使用本机内网穿透地址)
GMAIL_WEBHOOK_URL = 'https://a7a4-116-227-35-74.ngrok-free.app/api/user/gmail/webhook/'
# 如果在生产环境,使用以下固定地址
# GMAIL_WEBHOOK_URL = 'https://你的域名/api/user/gmail/webhook/'
# 媒体文件目录
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# 时区设置
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True # 将此项设置为True以启用时区支持
# DeepSeek API配置
DEEPSEEK_API_KEY = "sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf"
# 注意这里需要更新为有效的DeepSeek API密钥

View File

@ -18,6 +18,7 @@ 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 # 直接导入视图函数
urlpatterns = [
# 管理后台
@ -26,6 +27,10 @@ urlpatterns = [
# API路由
path('api/', include('user_management.urls')),
# 专用Gmail Webhook路由 - 直接匹配根路径
path('api/user/gmail/webhook/', gmail_webhook, name='root_gmail_webhook'), # 修改为正确路径
path('gmail/webhook/', gmail_webhook, name='alt_gmail_webhook'), # 添加备用路径
# 媒体文件服务
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),

1
temp_client_secret.json Normal file
View File

@ -0,0 +1 @@
{"installed": {"client_id": "240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com", "project_id": "first-renderer-454910-c1", "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"]}}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
# 管理命令包

View File

@ -0,0 +1 @@
# Gmail管理命令

View File

@ -0,0 +1,80 @@
from django.core.management.base import BaseCommand, CommandError
from user_management.models import GmailCredential, User
from user_management.gmail_integration import GmailIntegration
import logging
import pickle
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '更新Gmail凭证中的邮箱信息'
def add_arguments(self, parser):
parser.add_argument('--email', type=str, help='指定用户邮箱')
parser.add_argument('--gmail', type=str, help='指定要设置的Gmail邮箱')
parser.add_argument('--all', action='store_true', help='更新所有凭证')
def handle(self, *args, **options):
email = options.get('email')
gmail = options.get('gmail')
update_all = options.get('all')
if update_all:
# 更新所有凭证
credentials = GmailCredential.objects.filter(is_active=True)
self.stdout.write(f"找到 {credentials.count()} 个活跃的Gmail凭证")
for credential in credentials:
self._update_credential(credential, gmail)
elif email:
# 更新指定用户的凭证
try:
user = User.objects.get(email=email)
credentials = GmailCredential.objects.filter(user=user, is_active=True)
if not credentials.exists():
raise CommandError(f"未找到用户 {email} 的Gmail凭证")
for credential in credentials:
self._update_credential(credential, gmail)
except User.DoesNotExist:
raise CommandError(f"未找到用户 {email}")
else:
self.stdout.write("请提供--email参数或--all参数")
def _update_credential(self, credential, gmail=None):
"""更新单个凭证"""
user = credential.user
self.stdout.write(f"正在更新用户 {user.email} 的Gmail凭证...")
if gmail:
# 如果指定了Gmail邮箱直接使用
credential.gmail_email = gmail
credential.save()
self.stdout.write(self.style.SUCCESS(f"已手动设置Gmail邮箱为: {gmail}"))
return
# 尝试使用API获取Gmail邮箱
try:
# 从凭证数据中恢复服务
creds = pickle.loads(credential.credentials)
if creds and not creds.invalid:
# 创建Gmail集成实例
integration = GmailIntegration(user=user)
integration.credentials = creds
# 尝试调用API获取用户资料
profile = integration.gmail_service.users().getProfile(userId='me').execute()
gmail_email = profile.get('emailAddress')
if gmail_email:
credential.gmail_email = gmail_email
credential.save()
self.stdout.write(self.style.SUCCESS(f"已更新Gmail邮箱为: {gmail_email}"))
else:
self.stdout.write(self.style.WARNING("无法从API获取Gmail邮箱"))
else:
self.stdout.write(self.style.ERROR("凭证无效,请重新授权"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"更新失败: {str(e)}"))

View File

@ -0,0 +1,71 @@
# Generated by Django 5.1.5 on 2025-04-09 15:16
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user_management', '0004_knowledgebasedocument'),
]
operations = [
migrations.CreateModel(
name='GmailAttachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('gmail_message_id', models.CharField(max_length=100, verbose_name='Gmail消息ID')),
('filename', models.CharField(max_length=255, verbose_name='文件名')),
('filepath', models.CharField(max_length=500, verbose_name='文件路径')),
('mimetype', models.CharField(max_length=100, verbose_name='MIME类型')),
('filesize', models.IntegerField(default=0, verbose_name='文件大小')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('chat_message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_attachments', to='user_management.chathistory')),
],
options={
'verbose_name': 'Gmail附件',
'verbose_name_plural': 'Gmail附件',
'db_table': 'gmail_attachments',
},
),
migrations.CreateModel(
name='GmailCredential',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('token_path', models.CharField(max_length=255, verbose_name='Token存储路径')),
('last_history_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='上次同步历史ID')),
('watch_expiration', models.DateTimeField(blank=True, null=True, verbose_name='监听过期时间')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_credentials', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Gmail认证凭据',
'verbose_name_plural': 'Gmail认证凭据',
'db_table': 'gmail_credentials',
},
),
migrations.CreateModel(
name='GmailTalentMapping',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('talent_email', models.EmailField(max_length=254, verbose_name='达人邮箱')),
('conversation_id', models.CharField(max_length=100, verbose_name='对话ID')),
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_mappings', to='user_management.knowledgebase')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_talent_mappings', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Gmail达人映射',
'verbose_name_plural': 'Gmail达人映射',
'db_table': 'gmail_talent_mappings',
'unique_together': {('user', 'talent_email')},
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-04-09 16:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user_management', '0005_gmailattachment_gmailcredential_gmailtalentmapping'),
]
operations = [
migrations.AddField(
model_name='gmailcredential',
name='credentials',
field=models.BinaryField(blank=True, null=True, verbose_name='序列化的凭证数据'),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 5.1.5 on 2025-04-10 09:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user_management', '0006_gmailcredential_credentials'),
]
operations = [
migrations.AlterModelOptions(
name='gmailcredential',
options={},
),
migrations.AlterField(
model_name='gmailcredential',
name='created_at',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='gmailcredential',
name='credentials',
field=models.BinaryField(default=b''),
preserve_default=False,
),
migrations.AlterField(
model_name='gmailcredential',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='gmailcredential',
name='last_history_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='gmailcredential',
name='token_path',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='gmailcredential',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='gmailcredential',
name='watch_expiration',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterModelTable(
name='gmailcredential',
table='gmail_credential',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-04-11 04:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user_management', '0007_alter_gmailcredential_options_and_more'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='auto_recommend_reply',
field=models.BooleanField(default=False, help_text='是否启用自动推荐回复功能'),
),
]

View File

@ -396,12 +396,13 @@ class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
department = models.CharField(max_length=100, blank=True, help_text="部门")
group = models.CharField(max_length=100, blank=True, help_text="小组")
auto_recommend_reply = models.BooleanField(default=False, help_text="是否启用自动推荐回复功能")
class Meta:
db_table = 'user_profiles'
def __str__(self):
return f"{self.user.username}'s profile"
return f"{self.user.username}的个人资料"
class KnowledgeBasePermission(models.Model):
"""知识库权限模型 - 实现知识库和用户的多对多关系"""
@ -707,3 +708,61 @@ class KnowledgeBaseDocument(models.Model):
def __str__(self):
return f"{self.knowledge_base.name} - {self.document_name}"
class GmailCredential(models.Model):
"""Gmail认证信息"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_credentials')
gmail_email = models.EmailField(max_length=255, null=True, blank=True, help_text="实际授权的Gmail账号可能与user.email不同")
credentials = models.BinaryField() # 序列化的凭证对象
token_path = models.CharField(max_length=255) # token存储路径
last_history_id = models.CharField(max_length=255, null=True, blank=True) # 最后处理的historyId
watch_expiration = models.DateTimeField(null=True, blank=True) # 监听过期时间
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
class Meta:
db_table = 'gmail_credential'
def __str__(self):
return f"{self.user.username}的Gmail认证"
class GmailTalentMapping(models.Model):
"""Gmail达人映射关系模型"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_talent_mappings')
talent_email = models.EmailField(verbose_name='达人邮箱')
knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name='gmail_mappings')
conversation_id = models.CharField(max_length=100, verbose_name='对话ID')
is_active = models.BooleanField(default=True, verbose_name='是否激活')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'gmail_talent_mappings'
unique_together = ['user', 'talent_email']
verbose_name = 'Gmail达人映射'
verbose_name_plural = 'Gmail达人映射'
def __str__(self):
return f"{self.user.username} - {self.talent_email}"
class GmailAttachment(models.Model):
"""Gmail附件模型"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
chat_message = models.ForeignKey(ChatHistory, on_delete=models.CASCADE, related_name='gmail_attachments')
gmail_message_id = models.CharField(max_length=100, verbose_name='Gmail消息ID')
filename = models.CharField(max_length=255, verbose_name='文件名')
filepath = models.CharField(max_length=500, verbose_name='文件路径')
mimetype = models.CharField(max_length=100, verbose_name='MIME类型')
filesize = models.IntegerField(default=0, verbose_name='文件大小')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'gmail_attachments'
verbose_name = 'Gmail附件'
verbose_name_plural = 'Gmail附件'
def __str__(self):
return f"{self.filename} ({self.gmail_message_id})"

View File

@ -13,7 +13,19 @@ from .views import (
RegisterView,
LoginView,
LogoutView,
ChatHistoryViewSet
ChatHistoryViewSet,
user_profile,
user_register,
setup_gmail_integration,
send_gmail_message,
gmail_webhook,
get_gmail_attachments,
download_gmail_attachment,
get_gmail_talents,
refresh_gmail_watch,
check_gmail_auth,
import_gmail_from_sender,
sync_talent_emails
)
# 创建路由器
@ -42,4 +54,16 @@ urlpatterns = [
path('users/<str:pk>/', user_detail, name='user-detail'),
path('users/<str:pk>/update/', user_update, name='user-update'),
path('users/<str:pk>/delete/', user_delete, name='user-delete'),
# Gmail集成API
path('gmail/setup/', setup_gmail_integration, name='setup_gmail_integration'),
path('gmail/send/', send_gmail_message, name='send_gmail_message'),
path('gmail/webhook/', gmail_webhook, name='gmail_webhook'),
path('gmail/attachments/', get_gmail_attachments, name='get_gmail_attachments'),
path('gmail/download/', download_gmail_attachment, name='download_gmail_attachment'),
path('gmail/talents/', get_gmail_talents, name='get_gmail_talents'),
path('gmail/refresh-watch/', refresh_gmail_watch, name='refresh_gmail_watch'),
path('gmail/check-auth/', check_gmail_auth, name='check_gmail_auth'),
path('gmail/import-from-sender/', import_gmail_from_sender, name='import_gmail_from_sender'),
path('gmail/sync-talent/', sync_talent_emails, name='sync_talent_emails'),
]

File diff suppressed because it is too large Load Diff