From 0ccddd8c6314eac1bd63c98613f19da810e357eb Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Thu, 11 Dec 2025 22:09:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=AC=AC=E5=9B=9B=E9=98=B6=E6=AE=B5=20-=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 添加任务完成邮件模板 (templates/email/task_complete.html) 2. 在email_service.py中添加: - task_notify_enabled 字段支持 - send_task_complete_email() 函数,支持附件大小限制和分批发送 - send_task_complete_email_async() 异步发送函数 - MAX_ATTACHMENT_SIZE 常量 (10MB) 3. 更新app.py: - 邮件设置API支持task_notify_enabled字段 - 截图回调中集成任务完成邮件发送 4. 更新admin.html: - 添加"启用任务完成通知"开关 - 更新loadEmailSettings/updateEmailSettings函数 附件超过10MB时会自动分两封邮件发送(通知+截图),作为容错机制 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app.py | 31 +++- email_service.py | 248 ++++++++++++++++++++++++++++- templates/admin.html | 11 ++ templates/email/task_complete.html | 89 +++++++++++ 4 files changed, 373 insertions(+), 6 deletions(-) create mode 100644 templates/email/task_complete.html diff --git a/app.py b/app.py index e7e6d20..7a5d7c0 100755 --- a/app.py +++ b/app.py @@ -2487,11 +2487,11 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc # 重置账号状态 account.is_running = False account.status = "未开始" - + # 先清除任务状态(这样to_dict()不会包含detail_status) if account_id in task_status: del task_status[account_id] - + # 然后发送更新 socketio.emit('account_update', account.to_dict(), room=f'user_{user_id}') @@ -2517,6 +2517,29 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc source=source ) + # 发送任务完成邮件通知 + try: + user_info = database.get_user_by_id(user_id) + if user_info and user_info.get('email'): + screenshot_path = None + if result and result.get('success') and result.get('filename'): + screenshot_path = os.path.join(SCREENSHOTS_DIR, result['filename']) + + account_name = account.remark if account.remark else account.username + email_service.send_task_complete_email_async( + user_id=user_id, + email=user_info['email'], + username=user_info['username'], + account_name=account_name, + browse_type=browse_type, + total_items=browse_result.get('total_items', 0), + total_attachments=browse_result.get('total_attachments', 0), + screenshot_path=screenshot_path, + log_callback=lambda msg: log_to_client(msg, user_id, account_id) + ) + except Exception as email_error: + logger.warning(f"发送任务完成邮件失败: {email_error}") + except Exception as e: logger.error(f"截图回调出错: {e}") @@ -3716,12 +3739,14 @@ def update_email_settings_api(): failover_enabled = data.get('failover_enabled', True) register_verify_enabled = data.get('register_verify_enabled') base_url = data.get('base_url') + task_notify_enabled = data.get('task_notify_enabled') email_service.update_email_settings( enabled=enabled, failover_enabled=failover_enabled, register_verify_enabled=register_verify_enabled, - base_url=base_url + base_url=base_url, + task_notify_enabled=task_notify_enabled ) return jsonify({'success': True}) except Exception as e: diff --git a/email_service.py b/email_service.py index d4fef49..c6bf9f3 100644 --- a/email_service.py +++ b/email_service.py @@ -55,6 +55,11 @@ RATE_LIMIT_BIND = 60 # 绑定邮件: 1分钟 QUEUE_WORKERS = int(os.environ.get('EMAIL_QUEUE_WORKERS', '2')) QUEUE_MAX_SIZE = int(os.environ.get('EMAIL_QUEUE_MAX_SIZE', '100')) +# 附件大小限制(字节) +# 大多数邮件服务商限制单封邮件附件大小为10-25MB +# 为安全起见,设置为10MB,超过则分批发送 +MAX_ATTACHMENT_SIZE = int(os.environ.get('EMAIL_MAX_ATTACHMENT_SIZE', str(10 * 1024 * 1024))) # 10MB + # ============ 数据库操作 ============ @@ -108,6 +113,7 @@ def init_email_tables(): enabled INTEGER DEFAULT 0, failover_enabled INTEGER DEFAULT 1, register_verify_enabled INTEGER DEFAULT 0, + task_notify_enabled INTEGER DEFAULT 0, base_url TEXT DEFAULT '', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) @@ -201,9 +207,15 @@ def get_email_settings() -> Dict[str, Any]: except: cursor.execute("ALTER TABLE email_settings ADD COLUMN base_url TEXT DEFAULT ''") conn.commit() + try: + cursor.execute("SELECT task_notify_enabled FROM email_settings LIMIT 1") + except: + cursor.execute("ALTER TABLE email_settings ADD COLUMN task_notify_enabled INTEGER DEFAULT 0") + conn.commit() cursor.execute(""" - SELECT enabled, failover_enabled, register_verify_enabled, base_url, updated_at + SELECT enabled, failover_enabled, register_verify_enabled, base_url, + task_notify_enabled, updated_at FROM email_settings WHERE id = 1 """) row = cursor.fetchone() @@ -213,13 +225,15 @@ def get_email_settings() -> Dict[str, Any]: 'failover_enabled': bool(row[1]), 'register_verify_enabled': bool(row[2]) if row[2] is not None else False, 'base_url': row[3] or '', - 'updated_at': row[4] + 'task_notify_enabled': bool(row[4]) if row[4] is not None else False, + 'updated_at': row[5] } return { 'enabled': False, 'failover_enabled': True, 'register_verify_enabled': False, 'base_url': '', + 'task_notify_enabled': False, 'updated_at': None } @@ -228,7 +242,8 @@ def update_email_settings( enabled: bool, failover_enabled: bool, register_verify_enabled: bool = None, - base_url: str = None + base_url: str = None, + task_notify_enabled: bool = None ) -> bool: """更新全局邮件设置""" with db_pool.get_db() as conn: @@ -246,6 +261,10 @@ def update_email_settings( updates.append('base_url = ?') params.append(base_url) + if task_notify_enabled is not None: + updates.append('task_notify_enabled = ?') + params.append(int(task_notify_enabled)) + cursor.execute(f""" UPDATE email_settings SET {', '.join(updates)} @@ -1567,6 +1586,229 @@ def create_zip_attachment(files: List[Dict[str, Any]], zip_filename: str = 'scre } +# ============ 任务完成通知邮件 ============ + +def send_task_complete_email( + user_id: int, + email: str, + username: str, + account_name: str, + browse_type: str, + total_items: int, + total_attachments: int, + screenshot_path: str = None, + log_callback: Callable = None +) -> Dict[str, Any]: + """ + 发送任务完成通知邮件(支持附件大小限制,超过则分批发送) + + Args: + user_id: 用户ID + email: 收件人邮箱 + username: 用户名 + account_name: 账号名称 + browse_type: 浏览类型 + total_items: 浏览条目数 + total_attachments: 附件数量 + screenshot_path: 截图文件路径 + log_callback: 日志回调函数 + + Returns: + {'success': bool, 'error': str, 'emails_sent': int} + """ + # 检查邮件功能是否启用 + settings = get_email_settings() + if not settings.get('enabled', False): + return {'success': False, 'error': '邮件功能未启用', 'emails_sent': 0} + + if not settings.get('task_notify_enabled', False): + return {'success': False, 'error': '任务通知功能未启用', 'emails_sent': 0} + + if not email: + return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0} + + # 获取完成时间 + complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # 读取截图文件 + screenshot_data = None + screenshot_filename = None + if screenshot_path and os.path.exists(screenshot_path): + try: + with open(screenshot_path, 'rb') as f: + screenshot_data = f.read() + screenshot_filename = os.path.basename(screenshot_path) + except Exception as e: + if log_callback: + log_callback(f"[邮件] 读取截图文件失败: {e}") + + # 读取邮件模板 + template_path = os.path.join(os.path.dirname(__file__), 'templates', 'email', 'task_complete.html') + try: + with open(template_path, 'r', encoding='utf-8') as f: + html_template = f.read() + except FileNotFoundError: + html_template = """ + + +

任务完成通知

+

您好,{{ username }}!

+

您的浏览任务已完成。

+

账号:{{ account_name }}

+

浏览类型:{{ browse_type }}

+

浏览条目:{{ total_items }} 条

+

附件数量:{{ total_attachments }} 个

+

完成时间:{{ complete_time }}

+ + + """ + + # 准备发送 + emails_sent = 0 + last_error = '' + + # 检查附件大小,决定是否需要分批发送 + if screenshot_data and len(screenshot_data) > MAX_ATTACHMENT_SIZE: + # 附件超过限制,不压缩直接发送(大图片压缩效果不大) + # 这种情况很少见,但作为容错处理 + batch_info = '(由于文件较大,截图将单独发送)' + + # 先发送不带附件的通知邮件 + html_body = html_template.replace('{{ username }}', username) + html_body = html_body.replace('{{ account_name }}', account_name) + html_body = html_body.replace('{{ browse_type }}', browse_type) + html_body = html_body.replace('{{ total_items }}', str(total_items)) + html_body = html_body.replace('{{ total_attachments }}', str(total_attachments)) + html_body = html_body.replace('{{ complete_time }}', complete_time) + html_body = html_body.replace('{{ batch_info }}', batch_info) + + text_body = f""" +您好,{username}! + +您的浏览任务已完成。 + +账号:{account_name} +浏览类型:{browse_type} +浏览条目:{total_items} 条 +附件数量:{total_attachments} 个 +完成时间:{complete_time} + +截图将在下一封邮件中发送。 +""" + + result = send_email( + to_email=email, + subject=f'【知识管理平台】任务完成 - {account_name}', + body=text_body, + html_body=html_body, + email_type=EMAIL_TYPE_TASK_COMPLETE, + user_id=user_id, + log_callback=log_callback + ) + + if result['success']: + emails_sent += 1 + if log_callback: + log_callback(f"[邮件] 任务通知已发送") + else: + last_error = result['error'] + + # 单独发送截图附件 + attachment = [{'filename': screenshot_filename, 'data': screenshot_data}] + result2 = send_email( + to_email=email, + subject=f'【知识管理平台】任务截图 - {account_name}', + body=f'这是 {account_name} 的任务截图。', + attachments=attachment, + email_type=EMAIL_TYPE_TASK_COMPLETE, + user_id=user_id, + log_callback=log_callback + ) + + if result2['success']: + emails_sent += 1 + if log_callback: + log_callback(f"[邮件] 截图附件已发送") + else: + last_error = result2['error'] + + else: + # 正常情况:附件大小在限制内,一次性发送 + batch_info = '' + attachments = None + + if screenshot_data: + attachments = [{'filename': screenshot_filename, 'data': screenshot_data}] + + html_body = html_template.replace('{{ username }}', username) + html_body = html_body.replace('{{ account_name }}', account_name) + html_body = html_body.replace('{{ browse_type }}', browse_type) + html_body = html_body.replace('{{ total_items }}', str(total_items)) + html_body = html_body.replace('{{ total_attachments }}', str(total_attachments)) + html_body = html_body.replace('{{ complete_time }}', complete_time) + html_body = html_body.replace('{{ batch_info }}', batch_info) + + text_body = f""" +您好,{username}! + +您的浏览任务已完成。 + +账号:{account_name} +浏览类型:{browse_type} +浏览条目:{total_items} 条 +附件数量:{total_attachments} 个 +完成时间:{complete_time} + +{'截图已附在邮件中。' if screenshot_data else ''} +""" + + result = send_email( + to_email=email, + subject=f'【知识管理平台】任务完成 - {account_name}', + body=text_body, + html_body=html_body, + attachments=attachments, + email_type=EMAIL_TYPE_TASK_COMPLETE, + user_id=user_id, + log_callback=log_callback + ) + + if result['success']: + emails_sent += 1 + if log_callback: + log_callback(f"[邮件] 任务通知已发送") + else: + last_error = result['error'] + + return { + 'success': emails_sent > 0, + 'error': last_error if emails_sent == 0 else '', + 'emails_sent': emails_sent + } + + +def send_task_complete_email_async( + user_id: int, + email: str, + username: str, + account_name: str, + browse_type: str, + total_items: int, + total_attachments: int, + screenshot_path: str = None, + log_callback: Callable = None +): + """异步发送任务完成通知邮件""" + import threading + thread = threading.Thread( + target=send_task_complete_email, + args=(user_id, email, username, account_name, browse_type, + total_items, total_attachments, screenshot_path, log_callback), + daemon=True + ) + thread.start() + + # ============ 初始化 ============ def init_email_service(): diff --git a/templates/admin.html b/templates/admin.html index a408858..883ccc7 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1233,6 +1233,15 @@ 开启后,新用户注册需通过邮箱验证才能激活账号(优先级高于自动审核) +
+ +
+ 开启后,定时任务完成时将发送邮件通知给用户(用户需已设置邮箱) +
+
@@ -2797,6 +2806,7 @@ document.getElementById('emailEnabled').checked = data.enabled; document.getElementById('failoverEnabled').checked = data.failover_enabled; document.getElementById('registerVerifyEnabled').checked = data.register_verify_enabled || false; + document.getElementById('taskNotifyEnabled').checked = data.task_notify_enabled || false; document.getElementById('baseUrl').value = data.base_url || ''; } } catch (error) { @@ -2814,6 +2824,7 @@ enabled: document.getElementById('emailEnabled').checked, failover_enabled: document.getElementById('failoverEnabled').checked, register_verify_enabled: document.getElementById('registerVerifyEnabled').checked, + task_notify_enabled: document.getElementById('taskNotifyEnabled').checked, base_url: document.getElementById('baseUrl').value.trim() }) }); diff --git a/templates/email/task_complete.html b/templates/email/task_complete.html new file mode 100644 index 0000000..3a2fa08 --- /dev/null +++ b/templates/email/task_complete.html @@ -0,0 +1,89 @@ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+

知识管理平台

+

任务完成通知

+
+

+ 您好,{{ username }}! +

+

+ 您的浏览任务已完成,以下是任务详情: +

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
账号:{{ account_name }}
浏览类型:{{ browse_type }}
浏览条目:{{ total_items }} 条
附件数量:{{ total_attachments }} 个
完成时间:{{ complete_time }}
+
+ + +
+

+ 截图已附在邮件中{{ batch_info }} +

+
+ +

+ 如有任何问题,请登录平台查看详情。 +

+
+

+ 此邮件由系统自动发送,请勿直接回复。 +

+

+ © 知识管理平台 +

+
+
+ +