|
@@ -0,0 +1,832 @@
|
|
|
|
|
+{
|
|
|
|
|
+ "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",
|
|
|
|
|
+ "<div align=\"center\">\n",
|
|
|
|
|
+ " <strong>📬 EmailDigestAgent — 让 AI 帮你读完收件箱</strong><br>\n",
|
|
|
|
|
+ " HelloAgents · Datawhale 社区共创\n",
|
|
|
|
|
+ "</div>"
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ "metadata": {
|
|
|
|
|
+ "kernelspec": {
|
|
|
|
|
+ "display_name": ".venv (3.14.2.final.0)",
|
|
|
|
|
+ "language": "python",
|
|
|
|
|
+ "name": "python3"
|
|
|
|
|
+ },
|
|
|
|
|
+ "language_info": {
|
|
|
|
|
+ "codemirror_mode": {
|
|
|
|
|
+ "name": "ipython",
|
|
|
|
|
+ "version": 3
|
|
|
|
|
+ },
|
|
|
|
|
+ "file_extension": ".py",
|
|
|
|
|
+ "mimetype": "text/x-python",
|
|
|
|
|
+ "name": "python",
|
|
|
|
|
+ "nbconvert_exporter": "python",
|
|
|
|
|
+ "pygments_lexer": "ipython3",
|
|
|
|
|
+ "version": "3.14.2"
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ "nbformat": 4,
|
|
|
|
|
+ "nbformat_minor": 4
|
|
|
|
|
+}
|