大模型应用:一个基于AI大模型的自动邮件简报系统 - Flask + HTML 方案

项目概述

这是一个简化的邮件自动收取和总结系统,使用 Flask 作为后端,简单的 HTML 页面作为前端,无需 Docker,部署简单。
在这里插入图片描述

项目结构

email-digest-simple/
├── app.py
├── config.py
├── email_fetcher.py
├── glm_client.py
├── digest_generator.py
├── models.py
├── templates/
│   ├── base.html
│   ├── index.html
│   └── digest.html
├── static/
│   └── style.css
├── data/
│   └── emails.db
├── .env
├── requirements.txt
└── README.md

核心代码实现

1. 配置文件

config.py

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

class Config:
    # 邮箱配置
    EMAIL_HOST = os.getenv('EMAIL_HOST', 'imap.gmail.com')
    EMAIL_PORT = int(os.getenv('EMAIL_PORT', '993'))
    EMAIL_ADDRESS = os.getenv('EMAIL_ADDRESS')
    EMAIL_PASSWORD = os.getenv('EMAIL_PASSWORD')
    
    # GLM配置
    GLM_API_KEY = os.getenv('GLM_API_KEY')
    GLM_MODEL = os.getenv('GLM_MODEL', 'glm-4')
    GLM_BASE_URL = os.getenv('GLM_BASE_URL', 'https://open.bigmodel.cn/api/paas/v4')
    
    # 应用配置
    SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production')
    DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
    
    # 调度配置
    CHECK_INTERVAL_MINUTES = int(os.getenv('CHECK_INTERVAL_MINUTES', '30'))
    MAX_EMAILS_PER_RUN = int(os.getenv('MAX_EMAILS_PER_RUN', '20'))
    
    # 数据库
    DATABASE_PATH = BASE_DIR / 'data' / 'emails.db'
    DATABASE_PATH.parent.mkdir(exist_ok=True)

2. 数据模型

models.py

import sqlite3
from datetime import datetime
import json
from config import Config

class Database:
    def __init__(self):
        self.db_path = Config.DATABASE_PATH
        self.init_database()
    
    def get_connection(self):
        return sqlite3.connect(self.db_path)
    
    def init_database(self):
        conn = self.get_connection()
        cursor = conn.cursor()
        
        # 创建邮件表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS emails (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email_id TEXT UNIQUE,
                subject TEXT,
                sender TEXT,
                recipients TEXT,
                date TEXT,
                body TEXT,
                body_html TEXT,
                summary TEXT,
                processed BOOLEAN DEFAULT 0,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        # 创建简报表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS digests (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                date TEXT,
                content TEXT,
                email_count INTEGER,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        conn.commit()
        conn.close()
    
    def save_email(self, email_data):
        conn = self.get_connection()
        cursor = conn.cursor()
        
        cursor.execute('''
            INSERT OR REPLACE INTO emails 
            (email_id, subject, sender, recipients, date, body, body_html, summary, processed, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ''', (
            email_data['email_id'],
            email_data['subject'],
            email_data['sender'],
            json.dumps(email_data['recipients']),
            email_data['date'].isoformat(),
            email_data['body'],
            email_data['body_html'],
            email_data['summary'],
            email_data['processed'],
            datetime.now().isoformat()
        ))
        
        conn.commit()
        conn.close()
    
    def get_processed_email_ids(self):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('SELECT email_id FROM emails WHERE processed = 1')
        results = cursor.fetchall()
        conn.close()
        return {row[0] for row in results}
    
    def get_latest_digest(self):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT id, date, content, email_count, created_at 
            FROM digests 
            ORDER BY date DESC 
            LIMIT 1
        ''')
        result = cursor.fetchone()
        conn.close()
        
        if result:
            return {
                'id': result[0],
                'date': result[1],
                'content': json.loads(result[2]),
                'email_count': result[3],
                'created_at': result[4]
            }
        return None
    
    def save_digest(self, digest_data):
        conn = self.get_connection()
        cursor = conn.cursor()
        
        cursor.execute('''
            INSERT INTO digests (date, content, email_count)
            VALUES (?, ?, ?)
        ''', (
            digest_data['date'].isoformat(),
            json.dumps(digest_data['content'], ensure_ascii=False),
            digest_data['email_count']
        ))
        
        conn.commit()
        conn.close()
    
    def get_all_emails(self, limit=50):
        conn = self.get_connection()
        cursor = conn.cursor()
        cursor.execute('''
            SELECT id, subject, sender, date, summary, processed
            FROM emails
            ORDER BY date DESC
            LIMIT ?
        ''', (limit,))
        results = cursor.fetchall()
        conn.close()
        
        emails = []
        for row in results:
            emails.append({
                'id': row[0],
                'subject': row[1],
                'sender': row[2],
                'date': row[3],
                'summary': row[4],
                'processed': bool(row[5])
            })
        return emails

3. 邮件获取器

email_fetcher.py

import imaplib
import email
from email.header import decode_header
from datetime import datetime, timedelta
import logging

from config import Config
from models import Database

logger = logging.getLogger(__name__)

class EmailFetcher:
    def __init__(self):
        self.db = Database()
        
    def connect(self):
        """连接到IMAP服务器"""
        try:
            self.imap_server = imaplib.IMAP4_SSL(Config.EMAIL_HOST, Config.EMAIL_PORT)
            self.imap_server.login(Config.EMAIL_ADDRESS, Config.EMAIL_PASSWORD)
            logger.info(f"成功连接到邮箱: {Config.EMAIL_ADDRESS}")
            return True
        except Exception as e:
            logger.error(f"连接邮箱失败: {e}")
            return False
            
    def disconnect(self):
        """断开连接"""
        if hasattr(self, 'imap_server'):
            try:
                self.imap_server.logout()
            except:
                pass
                
    def _decode_mime_words(self, s):
        """解码MIME编码的字符串"""
        if not s:
            return ""
        decoded_fragments = decode_header(s)
        fragments = []
        for fragment, encoding in decoded_fragments:
            if isinstance(fragment, bytes):
                if encoding:
                    fragment = fragment.decode(encoding)
                else:
                    fragment = fragment.decode('utf-8', errors='ignore')
            fragments.append(fragment)
        return ''.join(fragments)
        
    def _get_email_body(self, msg):
        """提取邮件正文"""
        body = ""
        body_html = ""
        
        if msg.is_multipart():
            for part in msg.walk():
                content_type = part.get_content_type()
                content_disposition = str(part.get("Content-Disposition"))
                
                if "attachment" not in content_disposition:
                    charset = part.get_content_charset() or 'utf-8'
                    try:
                        payload = part.get_payload(decode=True)
                        if payload:
                            text = payload.decode(charset, errors='ignore')
                            if content_type == "text/plain":
                                body += text
                            elif content_type == "text/html":
                                body_html += text
                    except Exception as e:
                        logger.warning(f"解析邮件正文失败: {e}")
        else:
            charset = msg.get_content_charset() or 'utf-8'
            try:
                payload = msg.get_payload(decode=True)
                if payload:
                    body = payload.decode(charset, errors='ignore')
            except Exception as e:
                logger.warning(f"解析邮件正文失败: {e}")
                
        return body, body_html
        
    def fetch_new_emails(self, since_days=1):
        """获取新的未处理邮件"""
        if not self.connect():
            return []
            
        try:
            self.imap_server.select('INBOX')
            
            # 计算日期范围
            since_date = (datetime.now() - timedelta(days=since_days)).strftime("%d-%b-%Y")
            search_criteria = f'(SINCE "{since_date}")'
            
            status, messages = self.imap_server.search(None, search_criteria)
            if status != 'OK':
                logger.error("搜索邮件失败")
                return []
                
            email_ids = messages[0].split()
            logger.info(f"找到 {len(email_ids)} 封邮件")
            
            # 限制处理数量
            email_ids = email_ids[-Config.MAX_EMAILS_PER_RUN:] if email_ids else []
            
            # 获取已处理的邮件ID
            processed_ids = self.db.get_processed_email_ids()
            
            new_emails = []
            for email_id in email_ids:
                email_id_str = email_id.decode('utf-8')
                if email_id_str in processed_ids:
                    continue
                    
                try:
                    status, msg_data = self.imap_server.fetch(email_id, '(RFC822)')
                    if status != 'OK':
                        continue
                        
                    msg = email.message_from_bytes(msg_data[0][1])
                    
                    subject = self._decode_mime_words(msg.get("Subject", ""))
                    sender = self._decode_mime_words(msg.get("From", ""))
                    recipients = [self._decode_mime_words(r) for r in msg.get_all("To", [])]
                    date_str = msg.get("Date", "")
                    
                    try:
                        email_date = email.utils.parsedate_to_datetime(date_str)
                    except:
                        email_date = datetime.now()
                    
                    body, body_html = self._get_email_body(msg)
                    
                    email_data = {
                        'email_id': email_id_str,
                        'subject': subject[:200],
                        'sender': sender[:100],
                        'recipients': recipients[:5],
                        'date': email_date,
                        'body': body[:10000],
                        'body_html': body_html[:10000] if body_html else None,
                        'summary': None,
                        'processed': False
                    }
                    
                    new_emails.append(email_data)
                    
                except Exception as e:
                    logger.error(f"解析邮件 {email_id_str} 失败: {e}")
                    continue
                    
            return new_emails
            
        except Exception as e:
            logger.error(f"获取邮件失败: {e}")
            return []
        finally:
            self.disconnect()

4. GLM客户端

glm_client.py

import requests
import json
import logging

from config import Config

logger = logging.getLogger(__name__)

class GLMClient:
    def __init__(self):
        self.api_key = Config.GLM_API_KEY
        self.base_url = Config.GLM_BASE_URL
        self.model = Config.GLM_MODEL
        
    def summarize_email(self, email_content, subject=""):
        """使用GLM模型总结邮件内容"""
        if not self.api_key:
            logger.error("GLM API key 未配置")
            return None
            
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        
        # 构建提示词
        prompt = f"""
        请为以下邮件生成简洁的中文摘要,突出重点信息:
        
        邮件主题:{subject}
        邮件内容:{email_content}
        
        要求:
        1. 摘要控制在100字以内
        2. 突出关键信息和行动项
        3. 语言简洁明了
        """
        
        payload = {
            "model": self.model,
            "messages": [
                {"role": "user", "content": prompt}
            ],
            "temperature": 0.3,
            "max_tokens": 150
        }
        
        try:
            response = requests.post(
                f"{self.base_url}/chat/completions",
                headers=headers,
                json=payload,
                timeout=30
            )
            
            if response.status_code == 200:
                result = response.json()
                summary = result['choices'][0]['message']['content'].strip()
                return summary
            else:
                error_text = response.text
                logger.error(f"GLM API 调用失败: {response.status_code} - {error_text}")
                return None
                
        except Exception as e:
            logger.error(f"GLM API 调用异常: {e}")
            return None
            
    def batch_summarize(self, emails):
        """批量总结邮件"""
        for email_data in emails:
            if email_data['body'].strip():
                summary = self.summarize_email(email_data['body'], email_data['subject'])
                email_data['summary'] = summary or "总结失败"
                email_data['processed'] = True
            else:
                email_data['summary'] = "邮件内容为空"
                email_data['processed'] = True
                
        return emails

5. 简报生成器

digest_generator.py

from datetime import datetime
import json

class DigestGenerator:
    def generate_digest_content(self, emails):
        """生成简报内容"""
        digest_data = {
            "generated_at": datetime.now().isoformat(),
            "email_count": len(emails),
            "emails": []
        }
        
        for email in emails:
            digest_data["emails"].append({
                "subject": email['subject'],
                "sender": email['sender'],
                "date": email['date'].isoformat(),
                "summary": email['summary']
            })
            
        return digest_data
        
    def create_digest(self, emails):
        """创建简报记录"""
        content = self.generate_digest_content(emails)
        digest = {
            'date': datetime.now(),
            'content': content,
            'email_count': len(emails)
        }
        return digest

6. 主应用

app.py

import os
import threading
import time
from datetime import datetime, timedelta
import logging
from flask import Flask, render_template, request, jsonify, redirect, url_for
from apscheduler.schedulers.background import BackgroundScheduler

from config import Config
from email_fetcher import EmailFetcher
from glm_client import GLMClient
from digest_generator import DigestGenerator
from models import Database

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# 加载环境变量
from dotenv import load_dotenv
load_dotenv()

app = Flask(__name__)
app.config.from_object(Config)

# 初始化组件
db = Database()
scheduler = BackgroundScheduler()
email_fetcher = EmailFetcher()
glm_client = GLMClient()
digest_generator = DigestGenerator()

def process_emails():
    """处理邮件并生成简报"""
    logger = logging.getLogger(__name__)
    logger.info("开始处理邮件...")
    
    # 获取新邮件
    new_emails = email_fetcher.fetch_new_emails(since_days=1)
    
    if not new_emails:
        logger.info("没有新邮件需要处理")
        return
        
    logger.info(f"发现 {len(new_emails)} 封新邮件,开始生成摘要...")
    
    # 生成摘要
    summarized_emails = glm_client.batch_summarize(new_emails)
    
    # 保存到数据库
    for email_data in summarized_emails:
        db.save_email(email_data)
    
    logger.info(f"成功保存 {len(summarized_emails)} 封邮件")
    
    # 生成简报
    digest = digest_generator.create_digest(summarized_emails)
    db.save_digest(digest)
    
    logger.info(f"简报生成完成")

@app.route('/')
def index():
    """首页 - 显示最新简报"""
    latest_digest = db.get_latest_digest()
    return render_template('index.html', digest=latest_digest)

@app.route('/emails')
def emails():
    """邮件列表页面"""
    all_emails = db.get_all_emails(limit=100)
    return render_template('digest.html', emails=all_emails)

@app.route('/trigger', methods=['POST'])
def trigger_processing():
    """手动触发邮件处理"""
    try:
        process_emails()
        return jsonify({'success': True, 'message': '邮件处理完成'})
    except Exception as e:
        return jsonify({'success': False, 'message': f'处理失败: {str(e)}'})

@app.route('/health')
def health():
    """健康检查"""
    return jsonify({
        'status': 'healthy',
        'scheduler_running': scheduler.running if scheduler else False,
        'current_time': datetime.now().isoformat()
    })

def start_scheduler():
    """启动定时任务"""
    if not scheduler.running:
        scheduler.add_job(
            func=process_emails,
            trigger="interval",
            minutes=Config.CHECK_INTERVAL_MINUTES,
            id='email_processing'
        )
        scheduler.start()
        logging.info(f"定时任务已启动,每 {Config.CHECK_INTERVAL_MINUTES} 分钟检查一次")

if __name__ == '__main__':
    # 启动定时任务
    start_scheduler()
    
    try:
        app.run(
            host='0.0.0.0',
            port=5000,
            debug=Config.DEBUG
        )
    except KeyboardInterrupt:
        logging.info("收到中断信号,正在停止...")
    finally:
        if scheduler.running:
            scheduler.shutdown()

7. HTML 模板

templates/base.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}邮件简报系统{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <header>
            <h1>📧 邮件简报系统</h1>
            <nav>
                <a href="{{ url_for('index') }}" class="{% if request.endpoint == 'index' %}active{% endif %}">最新简报</a>
                <a href="{{ url_for('emails') }}" class="{% if request.endpoint == 'emails' %}active{% endif %}">邮件列表</a>
            </nav>
        </header>
        
        <main>
            {% block content %}{% endblock %}
        </main>
        
        <footer>
            <p>&copy; 2024 邮件简报系统</p>
        </footer>
    </div>
    
    <script>
        // 手动触发处理
        function triggerProcessing() {
            const button = document.getElementById('trigger-btn');
            button.disabled = true;
            button.textContent = '处理中...';
            
            fetch('/trigger', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                }
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    alert('邮件处理完成!');
                    location.reload();
                } else {
                    alert('处理失败: ' + data.message);
                    button.disabled = false;
                    button.textContent = '🔄 手动处理邮件';
                }
            })
            .catch(error => {
                alert('请求失败: ' + error.message);
                button.disabled = false;
                button.textContent = '🔄 手动处理邮件';
            });
        }
    </script>
</body>
</html>

templates/index.html

{% extends "base.html" %}

{% block content %}
<div class="dashboard">
    <div class="controls">
        <button id="trigger-btn" onclick="triggerProcessing()" class="btn btn-primary">
            🔄 手动处理邮件
        </button>
        <button onclick="location.reload()" class="btn btn-secondary">
            📥 刷新数据
        </button>
    </div>
    
    {% if digest %}
    <div class="digest-card">
        <div class="digest-header">
            <h2>📅 {{ digest.date[:10] }}</h2>
            <p class="email-count">📧 {{ digest.email_count }} 封邮件</p>
        </div>
        
        <div class="email-list">
            {% for email in digest.content.emails %}
            <div class="email-item">
                <div class="email-header">
                    <h3>{{ email.subject }}</h3>
                    <span class="sender">{{ email.sender }}</span>
                </div>
                <div class="email-summary">
                    <p>{{ email.summary or '暂无摘要' }}</p>
                </div>
                <div class="email-time">
                    {{ email.date[11:16] }}
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
    {% else %}
    <div class="no-data">
        <h3>暂无邮件简报</h3>
        <p>系统会自动处理新邮件,或点击上方按钮手动处理</p>
    </div>
    {% endif %}
    
    <div class="system-info">
        <h4>系统信息</h4>
        <div class="info-grid">
            <div class="info-item">
                <strong>检查间隔:</strong> {{ config.CHECK_INTERVAL_MINUTES }} 分钟
            </div>
            <div class="info-item">
                <strong>最大处理数:</strong> {{ config.MAX_EMAILS_PER_RUN }} 封/次
            </div>
            <div class="info-item">
                <strong>邮箱地址:</strong> {{ config.EMAIL_ADDRESS }}
            </div>
        </div>
    </div>
</div>
{% endblock %}

templates/digest.html

{% extends "base.html" %}

{% block content %}
<div class="emails-page">
    <h2>邮件列表 (共 {{ emails|length }} 封)</h2>
    
    <div class="email-list">
        {% for email in emails %}
        <div class="email-item {% if email.processed %}processed{% endif %}">
            <div class="email-header">
                <h3>{{ email.subject }}</h3>
                <span class="sender">{{ email.sender }}</span>
            </div>
            <div class="email-summary">
                <p>{{ email.summary or '暂无摘要' }}</p>
            </div>
            <div class="email-meta">
                <span class="date">{{ email.date[:10] }} {{ email.date[11:16] }}</span>
                <span class="status {% if email.processed %}success{% else %}warning{% endif %}">
                    {{ '已处理' if email.processed else '未处理' }}
                </span>
            </div>
        </div>
        {% endfor %}
    </div>
</div>
{% endblock %}

8. CSS 样式

static/style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f5f7fa;
    color: #333;
    line-height: 1.6;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 30px;
    padding-bottom: 20px;
    border-bottom: 2px solid #e0e0e0;
}

header h1 {
    color: #2c3e50;
    font-size: 2.5rem;
}

nav a {
    margin-left: 20px;
    text-decoration: none;
    color: #666;
    padding: 8px 16px;
    border-radius: 4px;
    transition: all 0.3s ease;
}

nav a:hover {
    color: #3498db;
    background-color: #f8f9fa;
}

nav a.active {
    color: #3498db;
    background-color: #e3f2fd;
    font-weight: bold;
}

.btn {
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 14px;
    font-weight: bold;
    transition: all 0.3s ease;
    margin-right: 10px;
}

.btn-primary {
    background-color: #3498db;
    color: white;
}

.btn-primary:hover:not(:disabled) {
    background-color: #2980b9;
}

.btn-secondary {
    background-color: #95a5a6;
    color: white;
}

.btn-secondary:hover {
    background-color: #7f8c8d;
}

.btn:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.controls {
    margin-bottom: 30px;
    display: flex;
    gap: 10px;
}

.digest-card {
    background: white;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    padding: 25px;
    margin-bottom: 30px;
}

.digest-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 25px;
    padding-bottom: 15px;
    border-bottom: 1px solid #eee;
}

.digest-header h2 {
    color: #2c3e50;
    font-size: 2rem;
}

.email-count {
    color: #666;
    font-size: 1.2rem;
}

.email-item {
    padding: 20px 0;
    border-bottom: 1px solid #eee;
    position: relative;
}

.email-item:last-child {
    border-bottom: none;
}

.email-item.processed {
    background-color: #f8f9fa;
}

.email-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 12px;
}

.email-header h3 {
    color: #2c3e50;
    font-size: 1.3rem;
    font-weight: 500;
}

.sender {
    color: #666;
    font-size: 0.9rem;
}

.email-summary {
    color: #555;
    margin-bottom: 12px;
    line-height: 1.5;
}

.email-summary p {
    margin: 0;
}

.email-time, .date {
    color: #999;
    font-size: 0.85rem;
}

.email-meta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 8px;
}

.status {
    font-size: 0.8rem;
    padding: 4px 8px;
    border-radius: 12px;
    font-weight: bold;
}

.status.success {
    background-color: #d4edda;
    color: #155724;
}

.status.warning {
    background-color: #fff3cd;
    color: #856404;
}

.no-data {
    text-align: center;
    padding: 60px 20px;
    background: white;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.no-data h3 {
    color: #666;
    margin-bottom: 15px;
}

.system-info {
    background: white;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    padding: 20px;
}

.system-info h4 {
    margin-bottom: 15px;
    color: #2c3e50;
}

.info-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 15px;
}

.info-item {
    padding: 10px;
    background-color: #f8f9fa;
    border-radius: 5px;
}

.info-item strong {
    color: #666;
    margin-right: 5px;
}

footer {
    margin-top: 40px;
    text-align: center;
    color: #666;
    padding-top: 20px;
    border-top: 1px solid #eee;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .container {
        padding: 15px;
    }
    
    header {
        flex-direction: column;
        align-items: flex-start;
    }
    
    nav {
        margin-top: 15px;
    }
    
    nav a {
        margin-left: 0;
        margin-right: 15px;
    }
    
    .controls {
        flex-direction: column;
    }
    
    .btn {
        width: 100%;
        margin-right: 0;
        margin-bottom: 10px;
    }
    
    .digest-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 10px;
    }
    
    .email-header {
        flex-direction: column;
        gap: 5px;
    }
    
    .email-meta {
        flex-direction: column;
        align-items: flex-start;
        gap: 5px;
    }
}

9. 依赖文件

requirements.txt

Flask==2.3.3
python-dotenv==1.0.0
requests==2.31.0
APScheduler==3.10.4

10. 环境变量配置

.env 示例

# 邮箱配置
EMAIL_HOST=imap.gmail.com
EMAIL_PORT=993
EMAIL_ADDRESS=your_email@gmail.com
EMAIL_PASSWORD=your_app_password

# GLM配置
GLM_API_KEY=your_glm_api_key
GLM_MODEL=glm-4

# 应用配置
SECRET_KEY=your-very-secret-key-change-this-in-production
DEBUG=False

# 调度配置
CHECK_INTERVAL_MINUTES=30
MAX_EMAILS_PER_RUN=20

部署说明

1. 环境准备

# 克隆或创建项目目录
mkdir email-digest-simple
cd email-digest-simple

# 创建虚拟环境
python -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate  # Windows

# 安装依赖
pip install -r requirements.txt

2. 配置环境变量

# 复制环境变量模板
cp .env.example .env  # 如果有示例文件
# 或者直接创建 .env 文件

# 编辑 .env 文件
nano .env

重要配置说明:

  • EMAIL_PASSWORD: 对于 Gmail,需要在 Google 账户设置中启用"两步验证",然后生成"应用专用密码"
  • GLM_API_KEY: 从 智谱AI开放平台 获取 API 密钥

3. 运行应用

# 开发模式(自动重载)
python app.py

# 生产模式
# 安装 gunicorn
pip install gunicorn

# 使用 gunicorn 运行
gunicorn -w 4 -b 0.0.0.0:5000 app:app

4. 系统服务部署(Linux)

创建 systemd 服务文件:

sudo nano /etc/systemd/system/email-digest.service

服务配置内容:

[Unit]
Description=Email Digest Simple Service
After=network.target

[Service]
Type=simple
User=your-username
WorkingDirectory=/path/to/email-digest-simple
Environment=PATH=/path/to/email-digest-simple/venv/bin
ExecStart=/path/to/email-digest-simple/venv/bin/python app.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl enable email-digest.service
sudo systemctl start email-digest.service
sudo systemctl status email-digest.service

5. Nginx 反向代理(可选)

如果需要通过域名访问,可以配置 Nginx:

server {
    listen 80;
    server_name your-domain.com;
    
    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

6. 访问应用

  • 本地访问: http://localhost:5000
  • 服务器访问: http://your-server-ip:5000
  • 健康检查: http://your-server-ip:5000/health

7. 功能说明

  1. 自动处理: 系统会按照配置的时间间隔自动检查新邮件并生成简报
  2. 手动处理: 在网页上点击"手动处理邮件"按钮可以立即处理
  3. 查看简报: 首页显示最新的邮件简报
  4. 邮件列表: 可以查看所有已处理的邮件详情
  5. 系统信息: 显示当前的配置信息和处理状态

8. 安全注意事项

  1. 密码安全: 不要将 .env 文件提交到版本控制系统
  2. 生产环境: 将 DEBUG 设置为 False
  3. 防火墙: 如果在服务器上运行,确保只开放必要的端口
  4. 定期备份: 备份 data/emails.db 数据库文件

这个版本使用 Flask 和简单的 HTML/CSS 实现了完整的功能,部署简单,维护方便,非常适合个人使用或小团队部署。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐