{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 📬 邮件智能摘要助手(EmailDigestAgent)\n", "## 基于 HelloAgents 框架的邮件智能分类、摘要与日报生成系统\n", "\n", "**核心流程:** 获取邮件(IMAP) → 智能分类 → AI 摘要 → 日报生成\n", "\n", "**支持邮箱:** QQ邮箱 / 163 / 126 / Gmail / Outlook — 任何支持 IMAP 的邮箱\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第1部分:环境配置\n", "\n", "### 1.1 安装依赖\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 安装依赖(如已安装可跳过)\n", "# hello-agents 框架 + 基础工具,imaplib 是 Python 标准库无需安装\n", "# !pip install -q hello-agents python-dotenv rich\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.2 导入库并配置 LLM\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import json\n", "import re\n", "import email\n", "import imaplib\n", "from email.header import decode_header\n", "from datetime import datetime, timedelta, timezone\n", "from typing import Dict, Any, List, Optional\n", "from dataclasses import dataclass, field\n", "from collections import Counter\n", "\n", "# HelloAgents\n", "from hello_agents import SimpleAgent, HelloAgentsLLM\n", "from hello_agents.tools import Tool\n", "\n", "# Utils\n", "from dotenv import load_dotenv\n", "from rich.console import Console\n", "from rich.table import Table\n", "from rich.panel import Panel\n", "from rich.markdown import Markdown\n", "\n", "console = Console()\n", "load_dotenv(override=True)\n", "\n", "# Init LLM\n", "llm = HelloAgentsLLM()\n", "model_id = os.getenv(\"LLM_MODEL_ID\", \"gpt-4o-mini\")\n", "print(\"✅ 环境配置完成\")\n", "print(f\" LLM 模型: {model_id}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第2部分:模拟邮件数据\n", "\n", "内置 10 封覆盖不同场景的模拟邮件,用于演示模式。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ========================================\n", "# 模拟邮件数据集(10封覆盖不同场景)\n", "# ========================================\n", "\n", "DEMO_EMAILS = [\n", " {\n", " \"id\": \"1\",\n", " \"from\": \"boss@company.com\",\n", " \"subject\": \"紧急:请今天下班前确认Q3预算方案\",\n", " \"date\": \"2026-07-02 08:30:00\",\n", " \"body\": \"各位部门负责人,附件是Q3预算方案初稿。请各位于今天下班前完成审核并回复确认。如有修改意见,请在明天上午的预算会议上提出。未及时确认的部门将按现有方案执行。\\n\\n附件:Q3_Budget_v2.xlsx\"\n", " },\n", " {\n", " \"id\": \"2\",\n", " \"from\": \"client@vip-corp.com\",\n", " \"subject\": \"Re: 合同条款第5条和第8条的修改意见\",\n", " \"date\": \"2026-07-02 09:15:00\",\n", " \"body\": \"你好,我们法务团队审核了合同草案,对第5条(付款条款)和第8条(违约责任)有较大异议。特别是违约金的设定比例需要调整。请尽快安排一次线上沟通,我们的法务希望本周内解决这个问题。\"\n", " },\n", " {\n", " \"id\": \"3\",\n", " \"from\": \"hr@company.com\",\n", " \"subject\": \"【通知】年度绩效评估系统本周五关闭\",\n", " \"date\": \"2026-07-02 10:00:00\",\n", " \"body\": \"各位同事,本年度绩效自评和互评系统将于本周五(7月4日)17:00正式关闭。目前系统显示你还有2位同事的互评未完成。请尽快登录系统完成评估,逾期将影响年度绩效结果。\\n\\n评估系统链接:http://hr.company.com/performance\"\n", " },\n", " {\n", " \"id\": \"4\",\n", " \"from\": \"github-notifications@github.com\",\n", " \"subject\": \"[hello-agents] New PR #156: Add EmailDigestAgent project\",\n", " \"date\": \"2026-07-02 10:30:00\",\n", " \"body\": \"Pull Request #156 opened by WHS.\\n\\nTitle: Add EmailDigestAgent project\\nBranch: feature/email-digest to main\\nFiles changed: 5\\n\\nView on GitHub: https://github.com/datawhalechina/hello-agents/pull/156\"\n", " },\n", " {\n", " \"id\": \"5\",\n", " \"from\": \"newsletter@techweekly.com\",\n", " \"subject\": \"Tech Weekly #234: AI Agent 最新趋势与工具盘点\",\n", " \"date\": \"2026-07-02 11:00:00\",\n", " \"body\": \"本周精选:\\n1. OpenAI 发布新一代 Agent 框架\\n2. LangChain v0.3 重大更新\\n3. 深度解析:多智能体协作的最佳实践\\n4. 开源推荐:5个值得关注的 Agent 项目\\n\\n点击阅读全文:https://techweekly.com/234\"\n", " },\n", " {\n", " \"id\": \"6\",\n", " \"from\": \"li.team@company.com\",\n", " \"subject\": \"下午3点的项目例会议程更新\",\n", " \"date\": \"2026-07-02 12:00:00\",\n", " \"body\": \"Hi team, 今天下午3点的项目例会议程有更新:\\n\\n1. Sprint 回顾(15min)\\n2. Bug 修复进展讨论(20min)\\n3. 新需求评审——用户权限模块(25min)\\n\\n请提前查阅 Jira board 准备更新。\"\n", " },\n", " {\n", " \"id\": \"7\",\n", " \"from\": \"marketing@online-shop.com\",\n", " \"subject\": \"年中大促最后一天!全场5折起\",\n", " \"date\": \"2026-07-02 13:00:00\",\n", " \"body\": \"年中狂欢倒计时!最后24小时!\\n\\n全场商品5折起\\n满299包邮\\n满599送精美礼品\\n\\n立即抢购:https://shop.example.com/sale\\n退订请回复TD\"\n", " },\n", " {\n", " \"id\": \"8\",\n", " \"from\": \"noreply@calendar.google.com\",\n", " \"subject\": \"提醒:明天上午10:00 季度评审会议\",\n", " \"date\": \"2026-07-02 14:00:00\",\n", " \"body\": \"日历提醒:\\n\\n事件:季度评审会议\\n时间:2026年7月3日 10:00-12:00\\n地点:3楼会议室A / Zoom\\n参与者:全体部门负责人\\n\\n请提前准备汇报材料。\"\n", " },\n", " {\n", " \"id\": \"9\",\n", " \"from\": \"spam-bot@random-mail.xyz\",\n", " \"subject\": \"Congratulations! You've won a FREE iPhone!\",\n", " \"date\": \"2026-07-02 15:00:00\",\n", " \"body\": \"Dear Winner,\\n\\nYour email address has been selected in our annual lottery! You have won a brand new iPhone 16 Pro Max!\\n\\nTo claim your prize, click: http://suspicious-link.xyz/claim\\n\\nHurry! This offer expires in 24 hours!\"\n", " },\n", " {\n", " \"id\": \"10\",\n", " \"from\": \"team-lead@company.com\",\n", " \"subject\": \"关于新同事入职培训和 mentor 安排\",\n", " \"date\": \"2026-07-02 16:30:00\",\n", " \"body\": \"大家下午好,下周一我们团队有新同事加入(前端开发,2年经验)。需要安排以下事项:\\n\\n1. 入职培训计划(HR 已发邮件)\\n2. 指定一位 mentor(建议资深前端同事)\\n3. 准备开发环境和新手引导文档\\n\\n请今天下班前在群里确认谁能担任 mentor!\"\n", " }\n", "]\n", "\n", "print(f\"✅ 已加载 {len(DEMO_EMAILS)} 封模拟邮件\")\n", "\n", "preview = \"\\n\".join([\n", " f\" {i+1}. [{e['from']}] {e['subject'][:50]}...\"\n", " for i, e in enumerate(DEMO_EMAILS)\n", "])\n", "console.print(Panel.fit(preview, title=\"📧 模拟邮件列表\", style=\"blue\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第3部分:工具定义\n", "\n", "定义两个核心工具:\n", "- **EmailFetchTool**:通过 IMAP 协议获取邮件(支持 QQ/163/126/Gmail/Outlook),或使用模拟数据\n", "- **EmailDigestTool**:用 LLM 对邮件列表进行分类和摘要" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ========================================\n", "# 工具1: 邮件获取工具(IMAP 通用版)\n", "# ========================================\n", "\n", "def _decode_mime_header(header_value: str) -> str:\n", " if not header_value:\n", " return \"\"\n", " parts = decode_header(header_value)\n", " decoded = \"\"\n", " for text, charset in parts:\n", " if isinstance(text, bytes):\n", " try:\n", " decoded += text.decode(charset or 'utf-8', errors='replace')\n", " except (LookupError, UnicodeDecodeError):\n", " decoded += text.decode('utf-8', errors='replace')\n", " else:\n", " decoded += str(text)\n", " return decoded\n", "\n", "\n", "def _parse_email_body(msg) -> str:\n", " body = \"\"\n", " if msg.is_multipart():\n", " for part in msg.walk():\n", " content_type = part.get_content_type()\n", " disposition = str(part.get(\"Content-Disposition\", \"\"))\n", " if content_type == \"text/plain\" and \"attachment\" not in disposition:\n", " payload = part.get_payload(decode=True)\n", " if payload:\n", " charset = part.get_content_charset() or 'utf-8'\n", " try:\n", " body = payload.decode(charset, errors='replace')\n", " except (LookupError, UnicodeDecodeError):\n", " body = payload.decode('utf-8', errors='replace')\n", " break\n", " if not body:\n", " for part in msg.walk():\n", " if (part.get_content_type() == \"text/html\"\n", " and \"attachment\" not in str(part.get(\"Content-Disposition\", \"\"))):\n", " payload = part.get_payload(decode=True)\n", " if payload:\n", " charset = part.get_content_charset() or 'utf-8'\n", " try:\n", " body = payload.decode(charset, errors='replace')\n", " body = re.sub(r'<[^>]+>', '', body)\n", " body = re.sub(r'\\s+', ' ', body).strip()\n", " except Exception:\n", " body = payload.decode('utf-8', errors='replace')\n", " break\n", " else:\n", " payload = msg.get_payload(decode=True)\n", " if payload:\n", " charset = msg.get_content_charset() or 'utf-8'\n", " try:\n", " body = payload.decode(charset, errors='replace')\n", " except Exception:\n", " body = payload.decode('utf-8', errors='replace')\n", " return body[:3000]\n", "\n", "\n", "class EmailFetchTool(Tool):\n", " def __init__(self, use_demo: bool = True, demo_emails=None):\n", " super().__init__(\n", " name=\"email_fetch\",\n", " description=\"获取收件箱中的未读邮件\"\n", " )\n", " self.use_demo = use_demo\n", " self.demo_emails = demo_emails or DEMO_EMAILS\n", "\n", " def _read_imap_config(self) -> tuple:\n", " server = os.getenv(\"IMAP_SERVER\") or \"\"\n", " port = int(os.getenv(\"IMAP_PORT\") or \"0\")\n", " username = os.getenv(\"IMAP_USERNAME\") or \"\"\n", " password = os.getenv(\"IMAP_PASSWORD\") or \"\"\n", " if not (username and password and server):\n", " try:\n", " with open(\"config/email_config.json\", \"r\", encoding=\"utf-8\") as f:\n", " cfg = json.load(f)\n", " imap_cfg = cfg.get(\"imap\", {})\n", " server = server or imap_cfg.get(\"server\", \"imap.qq.com\")\n", " port = port or imap_cfg.get(\"port\", 993)\n", " username = username or imap_cfg.get(\"username\", \"\")\n", " password = password or imap_cfg.get(\"password\", \"\")\n", " except FileNotFoundError:\n", " pass\n", " return server or \"imap.qq.com\", port or 993, username, password\n", "\n", " def _fetch_via_imap(self, max_emails: int = 50, hours: int = 24) -> list:\n", " server, port, username, password = self._read_imap_config()\n", " if not username or not password:\n", " raise ValueError(\"未配置邮箱信息\")\n", "\n", " console.print(f\"[dim] 连接 {server}:{port} 用户: {username}...[/dim]\")\n", " mail = imaplib.IMAP4_SSL(server, port)\n", " mail.login(username, password)\n", "\n", " # 选择收件箱 — 依次尝试: 无引号, 带引号, readonly\n", " select_status = None\n", " for name, rdonly in [(\"INBOX\", False), ('\"INBOX\"', False), (\"INBOX\", True)]:\n", " select_status, select_data = mail.select(name, readonly=rdonly)\n", " console.print(f\"[dim] select({name!r}, readonly={rdonly}) -> {select_status!r}[/dim]\")\n", " if select_status == \"OK\":\n", " break\n", " if select_status != \"OK\":\n", " raise ValueError(f\"无法选择 INBOX (状态: {select_status!r})\")\n", "\n", " since_date = (datetime.now() - timedelta(hours=hours)).strftime(\"%d-%b-%Y\")\n", " search_criteria = f'(UNSEEN SINCE {since_date})'\n", " status, message_ids = mail.search(None, search_criteria)\n", " if status != \"OK\":\n", " mail.logout()\n", " return []\n", "\n", " ids = message_ids[0].split()\n", " ids = list(reversed(ids))[:max_emails]\n", " emails = []\n", " for msg_id in ids:\n", " try:\n", " status, msg_data = mail.fetch(msg_id, \"(RFC822)\")\n", " if status != \"OK\":\n", " continue\n", " raw_email = msg_data[0][1]\n", " msg = email.message_from_bytes(raw_email)\n", " msg_id_str = msg_id.decode() if isinstance(msg_id, bytes) else msg_id\n", " emails.append({\n", " \"id\": msg_id_str,\n", " \"from\": _decode_mime_header(msg.get(\"From\", \"\")),\n", " \"subject\": _decode_mime_header(msg.get(\"Subject\", \"(无主题)\")),\n", " \"date\": msg.get(\"Date\", \"\"),\n", " \"body\": _parse_email_body(msg)\n", " })\n", " except Exception as e:\n", " console.print(f\"[yellow] ⚠️ 邮件解析失败: {e}[/yellow]\")\n", " continue\n", "\n", " mail.logout()\n", " console.print(f\"[dim] 已断开连接[/dim]\")\n", " return emails\n", "\n", " def run(self, hours: int = 24, max_emails: int = 50) -> str:\n", " if self.use_demo:\n", " emails = self.demo_emails[:max_emails]\n", " else:\n", " try:\n", " emails = self._fetch_via_imap(max_emails=max_emails, hours=hours)\n", " except imaplib.IMAP4.error as e:\n", " console.print(f\"[red]❌ IMAP 连接失败: {e}[/red]\")\n", " raise RuntimeError(f\"IMAP 连接失败: {e}\") from e\n", " except ValueError:\n", " raise\n", " except Exception as e:\n", " console.print(f\"[red]❌ 未知错误: {e}[/red]\")\n", " raise\n", "\n", " if not emails:\n", " return json.dumps({\"message\": \"没有新的未读邮件\", \"emails\": []}, ensure_ascii=False, indent=2)\n", " return json.dumps({\"count\": len(emails), \"fetch_time\": datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"), \"emails\": emails}, ensure_ascii=False, indent=2)\n", "\n", " def get_parameters(self):\n", " from hello_agents.tools import ToolParameter\n", " return []\n", "\n", "print(\"✅ EmailFetchTool defined (IMAP version)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ========================================\n", "# 工具2: 邮件摘要与分类工具(LLM驱动)\n", "# ========================================\n", "\n", "class EmailDigestTool(Tool):\n", " \"\"\"用 LLM 对邮件列表进行分类、摘要和优先级判断\"\"\"\n", "\n", " CATEGORIES = [\"工作\", \"客户\", \"个人\", \"通知\", \"促销\", \"垃圾\"]\n", " PRIORITIES = [\"高\", \"中\", \"低\"]\n", "\n", " def __init__(self, llm=None, use_demo: bool = True, demo_emails=None):\n", " super().__init__(\n", " name=\"email_fetch\",\n", " description=\"获取收件箱中的未读邮件。支持演示模式和IMAP真实接入模式。\"\n", " )\n", " self.llm = llm\n", " self.use_demo = use_demo\n", " self.demo_emails = demo_emails or DEMO_EMAILS\n", "\n", " def _build_prompt(self, emails_json: str, count: int) -> str:\n", " cats = \", \".join(self.CATEGORIES)\n", " prios = \", \".join(self.PRIORITIES)\n", " return f\"\"\"你是一个专业的邮件分析助手。请分析以下邮件列表并返回结构化结果。\n", "\n", "邮件数据(JSON格式):\n", "{emails_json}\n", "\n", "分析规则:\n", "1. 分类 - 从 [{cats}] 中选择最合适的一类\n", "2. 优先级 - 从 [{prios}] 中选择:\n", " 高: 需要立即处理(老板指令、客户需求、合同、截止日期)\n", " 中: 需要处理但不紧急(项目讨论、同事沟通、HR通知)\n", " 低: 可忽略(新闻订阅、促销广告、系统通知、垃圾邮件)\n", "3. 一句话摘要 - 用中文提炼核心内容,包含关键动作或截止时间\n", "4. 关键行动 - 若需要回复或行动,用1-2句话说明;否则填\"无\"\n", "\n", "严格按以下JSON格式返回(只返回JSON,不加其他文字):\n", "{{\n", " \"analyzed_at\": \"当前时间\",\n", " \"total\": {count},\n", " \"items\": [\n", " {{\n", " \"id\": \"邮件ID\",\n", " \"from\": \"发件人\",\n", " \"subject\": \"主题\",\n", " \"category\": \"分类\",\n", " \"priority\": \"优先级\",\n", " \"summary\": \"一句话摘要\",\n", " \"action\": \"关键行动\"\n", " }}\n", " ]\n", "}}\"\"\"\n", "\n", " def run(self, emails_json: str) -> str:\n", " try:\n", " data = json.loads(emails_json)\n", " count = data.get(\"count\", len(data.get(\"emails\", [])))\n", " prompt = self._build_prompt(emails_json, count)\n", " response = self.llm.invoke([{\"role\": \"user\", \"content\": prompt}])\n", " result = response.content.strip()\n", " \n", " if result.startswith(\"```\"):\n", " result = re.sub(r'^```(?:json)?\\s*', '', result)\n", " result = re.sub(r'\\s*```$', '', result)\n", "\n", " parsed = json.loads(result)\n", " return json.dumps(parsed, ensure_ascii=False, indent=2)\n", "\n", " except json.JSONDecodeError as e:\n", " return json.dumps({\n", " \"error\": \"LLM 返回格式异常\",\n", " \"detail\": str(e)\n", " }, ensure_ascii=False, indent=2)\n", " except Exception as e:\n", " return json.dumps({\n", " \"error\": \"分析过程中出错\",\n", " \"detail\": str(e)\n", " }, ensure_ascii=False, indent=2)\n", " def get_parameters(self):\n", " from hello_agents.tools import ToolParameter\n", " return []\n", "\n", "\n", "print(\"✅ EmailDigestTool defined\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第4部分:日报生成 Pipeline\n", "\n", "编排完整流程:获取 → 分类摘要 → 统计 → 日报生成 → 保存文件" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ========================================\n", "# 邮件日报生成 Pipeline\n", "# ========================================\n", "\n", "@dataclass\n", "class EmailDigestPipeline:\n", " \"\"\"邮件日报 Pipeline: 获取(IMAP/演示) -> LLM分类摘要 -> Markdown日报\"\"\"\n", "\n", " llm: any\n", " use_demo: bool = True\n", " fetch_tool: EmailFetchTool = field(init=False)\n", " digest_tool: EmailDigestTool = field(init=False)\n", "\n", " def __post_init__(self):\n", " self.fetch_tool = EmailFetchTool(use_demo=self.use_demo)\n", " self.digest_tool = EmailDigestTool(llm=self.llm)\n", "\n", " def run(self, hours: int = 24, max_emails: int = 50) -> str:\n", " \"\"\"执行完整 Pipeline,返回 Markdown 日报\"\"\"\n", " mode = \"演示数据\" if self.use_demo else \"IMAP 真实邮箱\"\n", " console.print(Panel.fit(\n", " f\"模式: {mode} | 时间: 最近{hours}h | 上限: {max_emails}封\",\n", " title=\"📬 EmailDigestAgent\", style=\"cyan\"\n", " ))\n", "\n", " # Step 1: Fetch\n", " console.print(\"\\n📥 [1/4] 获取邮件...\", style=\"cyan\")\n", " raw = self.fetch_tool.run(hours=hours, max_emails=max_emails)\n", " data = json.loads(raw)\n", " count = data.get(\"count\", len(data.get(\"emails\", [])))\n", " if count == 0:\n", " return self._empty_report()\n", " console.print(f\" ✅ {count} 封邮件\")\n", "\n", " # Step 2: LLM digest\n", " console.print(\"\\n🧠 [2/4] LLM 分类与摘要...\", style=\"cyan\")\n", " digest_raw = self.digest_tool.run(raw)\n", " digest = json.loads(digest_raw)\n", " items = digest.get(\"items\", [])\n", " if \"error\" in digest:\n", " console.print(f\" ⚠️ {digest['error']}\")\n", " console.print(f\" ✅ 已分析 {len(items)} 封\")\n", "\n", " # Step 3: Stats\n", " console.print(\"\\n📊 [3/4] 统计汇总...\", style=\"cyan\")\n", " stats = self._compute_stats(items)\n", " console.print(f\" 🔴高:{stats['priorities'].get('高',0)} 🟡中:{stats['priorities'].get('中',0)} 🟢低:{stats['priorities'].get('低',0)}\")\n", "\n", " # Step 4: Report\n", " console.print(\"\\n📝 [4/4] 生成日报...\", style=\"cyan\")\n", " report = self._generate_report(stats, items)\n", "\n", " os.makedirs(\"output\", exist_ok=True)\n", " path = f\"output/email_digest_{datetime.now().strftime('%Y%m%d')}.md\"\n", " with open(path, 'w', encoding='utf-8') as f:\n", " f.write(report)\n", " console.print(f\"\\n📄 已保存: {path}\", style=\"bold green\")\n", " return report\n", "\n", " def _compute_stats(self, items):\n", " cats = Counter(i.get(\"category\", \"other\") for i in items)\n", " prios = Counter(i.get(\"priority\", \"中\") for i in items)\n", " actions = sum(1 for i in items if i.get(\"action\", \"无\") != \"无\")\n", " return {\"total\": len(items), \"categories\": dict(cats),\n", " \"priorities\": dict(prios), \"need_action\": actions}\n", "\n", " def _generate_report(self, stats, items):\n", " now = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n", " src = \"演示数据\" if self.use_demo else \"IMAP 收件箱\"\n", " order = {\"高\": 0, \"中\": 1, \"低\": 2}\n", " sorted_items = sorted(items, key=lambda x: order.get(x.get(\"priority\", \"中\"), 1))\n", "\n", " lines = [\n", " f\"# 📬 邮件日报\",\n", " f\"**生成时间:** {now} | **来源:** {src}\",\n", " f\"---\",\n", " f\"## 📊 概览\",\n", " f\"| 指标 | 数值 |\",\n", " f\"|------|------|\",\n", " f\"| 总计 | {stats['total']} 封 |\",\n", " f\"| 🔴 高优先级 | {stats['priorities'].get('高', 0)} |\",\n", " f\"| 🟡 中优先级 | {stats['priorities'].get('中', 0)} |\",\n", " f\"| 🟢 低优先级 | {stats['priorities'].get('低', 0)} |\",\n", " f\"| 📋 需行动 | {stats['need_action']} |\",\n", " f\"\",\n", " f\"### 分类分布\",\n", " ]\n", " for cat, cnt in stats['categories'].items():\n", " lines.append(f\"- **{cat}**:{cnt} 封 {'█' * cnt}\")\n", " lines.append(f\"---\")\n", "\n", " for pri, label in [(\"高\", \"🔴 高优先级 — 立即处理\"),\n", " (\"中\", \"🟡 中优先级 — 今天处理\"),\n", " (\"低\", \"🟢 低优先级 — 可忽略\")]:\n", " pitems = [i for i in sorted_items if i.get(\"priority\") == pri]\n", " if not pitems:\n", " continue\n", " lines.append(f\"## {label}\")\n", " lines.append(f\"| # | 发件人 | 主题 | 类型 | 一句话摘要 |\")\n", " lines.append(f\"|---|--------|------|------|-----------|\")\n", " for idx, item in enumerate(pitems, 1):\n", " fr = item.get(\"from\", \"\")\n", " subj = item.get(\"subject\", \"\")\n", " cat = item.get(\"category\", \"-\")\n", " summ = item.get(\"summary\", \"-\")\n", " lines.append(f\"| {idx} | {fr} | {subj} | {cat} | {summ} |\")\n", " lines.append(\"\")\n", " if pri in (\"高\", \"中\"):\n", " acts = [i for i in pitems if i.get(\"action\", \"无\") != \"无\"]\n", " if acts:\n", " lines.append(\"**📋 行动项:**\")\n", " for a in acts:\n", " lines.append(f\"- [{a.get('category','')}] **{a.get('subject','')}** — {a.get('action','')}\")\n", " lines.append(\"\")\n", "\n", " lines.extend([\"---\", \"*EmailDigestAgent · HelloAgents*\"])\n", " return \"\\n\".join(lines)\n", "\n", " def _empty_report(self):\n", " now = datetime.now().strftime(\"%Y-%m-%d %H:%M\")\n", " return (f\"# 📬 邮件日报\\n\\n**{now}**\\n\\n## 🎉 收件箱干净!\\n\\n\"\n", " f\"当前没有未读邮件。\\n\\n---\\n*EmailDigestAgent · HelloAgents*\")\n", "\n", "print(\"✅ EmailDigestPipeline defined\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第5部分:功能演示\n", "\n", "### 5.1 演示模式:生成邮件日报" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 创建 Pipeline(演示模式)并运行\n", "pipeline = EmailDigestPipeline(llm=llm, use_demo=True)\n", "report = pipeline.run(hours=24, max_emails=20)\n", "\n", "# 渲染日报\n", "console.print(\"\\n\" + \"=\" * 60)\n", "console.print(\"📬 邮件日报\", style=\"bold cyan\")\n", "console.print(\"=\" * 60 + \"\\n\")\n", "console.print(Markdown(report))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.2 查看保存的日报文件" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import glob\n", "reports = sorted(glob.glob(\"output/email_digest_*.md\"), reverse=True)\n", "if reports:\n", " print(f\"Latest: {reports[0]}\")\n", " with open(reports[0], 'r', encoding='utf-8') as f:\n", " console.print(Markdown(f.read()))\n", "else:\n", " print(\"No reports yet. Run the pipeline first.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.3 数据统计可视化" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 分类和优先级统计\n", "emails_data = json.loads(pipeline.fetch_tool.run(hours=24, max_emails=20))\n", "digest_data = json.loads(pipeline.digest_tool.run(json.dumps(emails_data)))\n", "items = digest_data.get(\"items\", [])\n", "\n", "table = Table(title=\"📊 邮件分析统计\")\n", "table.add_column(\"指标\", style=\"cyan\")\n", "table.add_column(\"数值\", style=\"white\")\n", "table.add_column(\"说明\", style=\"dim\")\n", "table.add_row(\"总数\", str(len(items)), \"\")\n", "table.add_row(\"需立即处理(高)\", str(sum(1 for i in items if i.get(\"priority\")==\"高\")), \"🔴\")\n", "table.add_row(\"今天处理(中)\", str(sum(1 for i in items if i.get(\"priority\")==\"中\")), \"🟡\")\n", "table.add_row(\"可延后(低)\", str(sum(1 for i in items if i.get(\"priority\")==\"低\")), \"🟢\")\n", "table.add_row(\"需要行动\", str(sum(1 for i in items if i.get(\"action\",\"无\")!=\"无\")), \"📋\")\n", "console.print(table)\n", "\n", "console.print(\"\\n📂 分类分布:\")\n", "cats = Counter(i.get(\"category\",\"other\") for i in items)\n", "for cat, cnt in cats.most_common():\n", " bar = \"█\" * (cnt * 3)\n", " console.print(f\" {cat:8s} {cnt:2d} {bar}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第6部分:真实邮箱接入(IMAP 通用协议)\n", "\n", "支持 **QQ邮箱 / 163 / 126 / Gmail / Outlook** 等所有支持 IMAP 的邮箱。\n", "\n", "### 各邮箱 IMAP 配置速查" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"=\" * 60)\n", "print(\"📧 真实邮箱接入指南\")\n", "print(\"=\" * 60)\n", "print()\n", "print(\"【各邮箱 IMAP 配置】\")\n", "print()\n", "print(\" QQ邮箱:\")\n", "print(\" server: imap.qq.com port: 993\")\n", "print(\" 步骤: QQ邮箱网页版 -> 设置 -> 账户 -> 开启IMAP/SMTP服务\")\n", "print(\" -> 验证密保 -> 获取授权码 (注意不是QQ密码)\")\n", "print()\n", "print(\" 网易163:\")\n", "print(\" server: imap.163.com port: 993\")\n", "print(\" 步骤: 网易邮箱网页版 -> 设置 -> POP3/SMTP/IMAP -> 开启IMAP\")\n", "print(\" -> 获取授权码\")\n", "print()\n", "print(\" 网易126:\")\n", "print(\" server: imap.126.com port: 993\")\n", "print(\" 步骤: 同163邮箱\")\n", "print()\n", "print(\" Gmail:\")\n", "print(\" server: imap.gmail.com port: 993\")\n", "print(\" 步骤: Google账户 -> 安全性 -> 两步验证 -> 应用专用密码\")\n", "print()\n", "print(\" Outlook/Hotmail:\")\n", "print(\" server: outlook.office365.com port: 993\")\n", "print(\" 步骤: Microsoft账户 -> 安全性 -> 应用密码\")\n", "print()\n", "print(\"【配置方式二选一】\")\n", "print()\n", "print(\" 方式1 (推荐) - 编辑 .env 文件:\")\n", "print(\" IMAP_SERVER=imap.qq.com\")\n", "print(\" IMAP_PORT=993\")\n", "print(\" IMAP_USERNAME=your_email@qq.com\")\n", "print(\" IMAP_PASSWORD=你的授权码\")\n", "print()\n", "print(\" 方式2 - 编辑 config/email_config.json:\")\n", "print(\" 修改 imap 段中的 server / port / username / password\")\n", "print()\n", "print(\"【运行真实模式】\")\n", "print(\" pipeline = EmailDigestPipeline(llm=llm, use_demo=False)\")\n", "print(\" report = pipeline.run(hours=24, max_emails=50)\")\n", "print(\" console.print(Markdown(report))\")\n", "print()\n", "print(\"=\" * 60)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 真实模式演示\n", "\n", "请先按上面指南完成配置后再运行。连接失败时会直接报错并展示原因,方便排查。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ========================================\n", "# 真实 IMAP 模式(取消注释以运行)\n", "# ========================================\n", "#\n", "# 配置好 .env 中的 IMAP_SERVER/IMAP_PORT/IMAP_USERNAME/IMAP_PASSWORD 后运行:\n", "#\n", "# pipeline_real = EmailDigestPipeline(llm=llm, use_demo=False)\n", "# report = pipeline_real.run(hours=24, max_emails=50)\n", "# console.print(Markdown(report))\n", "\n", "print(\"💡 取消上面代码的注释,配置好 .env 中的 IMAP 信息后即可运行真实模式。\")\n", "print(\"如果 IMAP 连接失败,会直接报错并展示原因,方便排查。\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 第7部分:项目总结\n", "\n", "### ✅ 实现功能\n", "\n", "- **邮件获取**:支持 IMAP 协议(QQ/163/126/Gmail/Outlook)+ 模拟数据双模式,真实模式连接失败会直接报错\n", "- **MIME 解码**:正确处理中文主题、发件人名、多 part 邮件和 HTML 邮件\n", "- **LLM 分类**:6 种类型(工作/客户/个人/通知/促销/垃圾)+ 3 级优先级\n", "- **AI 摘要**:语义理解邮件内容,生成一句话中文摘要 + 关键行动项\n", "- **日报生成**:Markdown 日报,按优先级分区,含统计概览和行动清单\n", "- **文件持久化**:自动保存到 output/ 目录\n", "\n", "### 🎯 设计亮点\n", "\n", "- **真实痛点**:收件箱过载是每个职场人的日常困扰\n", "- **IMAP 通用**:不再绑定 Gmail,国内邮箱原生支持\n", "- **严格报错**:真实模式下 IMAP 连接失败会直接抛出异常,方便排查\n", "- **Pipeline 清晰**:获取 -> 分类/摘要 -> 日报,四步职责分明\n", "\n", "### 🔮 未来方向\n", "\n", "- 定时任务(每天早上 8:00 自动运行)\n", "- 日报推送至微信/钉钉\n", "- 周报/月报趋势统计\n", "\n", "---\n", "\n", "