diff --git a/src/ai_client.py b/src/ai_client.py index 2bba533..aa5e0ad 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -31,7 +31,7 @@ SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以 其中: - subject 不能照抄原邮件主题,必须是你总结生成的简短标题 - category: 邮件类型。推广邮件(营销、促销、广告、 newsletter 推广等)标记为 "promotion";验证码邮件标记为 "notification";普通邮件标记为 "normal" -- verification_code: 仅当 category 为 "notification" 且邮件包含验证码时填写,只填纯数字验证码(如「123456」「888888」),不包含平台名。非验证码邮件留空字符串 +- verification_code: 仅当 category 为 "notification" 且邮件包含验证码时填写,提取完整的验证码字符串(纯数字、字母数字混合等均可,如「123456」「abc-789」「701383」)。非验证码邮件留空字符串 - app_name: 仅当 category 为 "notification" 且邮件包含验证码时填写,填写发送验证码的应用/平台名称(如「GitHub」「微信」「Kraken」)。非验证码邮件留空字符串 - links: 从邮件正文中提取用户需要点击的链接。只提取有意义的、用户可能需要操作的链接(如确认链接、登录链接、下载链接、账单链接、退订链接等),不要提取、追踪像素、邮件头中的无关链接。每条包含简短描述文字和完整 URL。最多 3 条。没有需要点击的链接时为空数组 - summary 要简洁,把需要知道的信息浓缩成一句话,不需要分条列出 diff --git a/src/summarizer.py b/src/summarizer.py index d01901a..3be2bad 100644 --- a/src/summarizer.py +++ b/src/summarizer.py @@ -3,7 +3,7 @@ from typing import Generator, Optional from src.config import Config from src.email_client import fetch_unseen_emails, mark_as_seen, Email from src.ai_client import summarize_email -from src.tg_bot import send_summary, format_summary +from src.tg_bot import send_summary, send_thinking, edit_message, format_summary logger = logging.getLogger(__name__) @@ -23,6 +23,10 @@ def poll_accounts(cfg: Config) -> Generator[tuple[int, Email], None, None]: def ai_process(cfg: Config, acct_idx: int, mail: Email) -> Optional[dict]: logger.info(f" 正在摘要: {mail.subject}") + # 先发思考中消息 + thinking_msg_id = send_thinking(cfg.telegram, cfg.telegram.chat_id) + logger.info(f" 思考中消息已发送: msg_id={thinking_msg_id}") + summary = summarize_email(cfg.ai, mail.recipient, mail.subject, mail.sender, mail.body, mail.account_email) summary["recipient"] = mail.recipient if "@" not in summary.get("sender", ""): @@ -38,18 +42,44 @@ def ai_process(cfg: Config, acct_idx: int, mail: Email) -> Optional[dict]: "acct_idx": acct_idx, "account_email": mail.account_email, "uid": mail.uid, + "thinking_msg_id": thinking_msg_id, } def tg_send_and_mark(cfg: Config, info: dict): acct = cfg.email_accounts[info["acct_idx"]] - send_summary(cfg.telegram, cfg.telegram.chat_id, - info["text"], info["data"], - info["original_body"], info["acct_idx"], - original_sender=info.get("original_sender", ""), - original_recipient=info.get("original_recipient", ""), - original_reply_to=info.get("original_reply_to", ""), - account_email=info.get("account_email", "")) - # 只有 send_summary 成功返回(未抛异常)才标记已读 + thinking_msg_id = info.get("thinking_msg_id") + if thinking_msg_id: + # 编辑思考中消息为实际摘要 + from src.tg_bot import _summary_keyboard + can_reply = info["data"].get("can_reply", True) + is_promotion = info["data"].get("category") == "promotion" + links = info["data"].get("links", []) + edit_message(cfg.telegram, cfg.telegram.chat_id, thinking_msg_id, + info["text"], _summary_keyboard(can_reply, is_promotion, links)) + # 保存上下文用于回调 + from src.tg_bot import _email_contexts + _email_contexts[thinking_msg_id] = { + "summary_text": info["text"], + "summary_data": info["data"], + "original_body": info["original_body"][:10000], + "original_sender": info.get("original_sender", ""), + "original_recipient": info.get("original_recipient", ""), + "original_reply_to": info.get("original_reply_to", ""), + "account_email": info.get("account_email", ""), + "sender": info["data"].get("sender", ""), + "subject": info["data"].get("subject", ""), + "can_reply": can_reply, + } + else: + # 兜底:没有思考中消息就正常发送 + send_summary(cfg.telegram, cfg.telegram.chat_id, + info["text"], info["data"], + info["original_body"], info["acct_idx"], + original_sender=info.get("original_sender", ""), + original_recipient=info.get("original_recipient", ""), + original_reply_to=info.get("original_reply_to", ""), + account_email=info.get("account_email", "")) + # 只有发送成功才标记已读 mark_as_seen(acct, [info["uid"]]) logger.info(f" TG 发送成功,已标记已读") diff --git a/src/tg_bot.py b/src/tg_bot.py index b0db106..dc3fde8 100644 --- a/src/tg_bot.py +++ b/src/tg_bot.py @@ -57,6 +57,25 @@ def send_text(tg_cfg: TelegramConfig, chat_id: str, text: str, return result["result"]["message_id"] +def send_thinking(tg_cfg: TelegramConfig, chat_id: str) -> int: + result = _tg_req(tg_cfg, "sendMessage", { + "chat_id": chat_id, + "text": "💭 思考中...", + }) + return result["result"]["message_id"] + + +def edit_message(tg_cfg: TelegramConfig, chat_id: str, msg_id: int, + text: str, reply_markup: dict = None): + payload = { + "chat_id": chat_id, "message_id": msg_id, + "text": text, "parse_mode": "MarkdownV2", + } + if reply_markup: + payload["reply_markup"] = reply_markup + _tg_req(tg_cfg, "editMessageText", payload) + + def delete_message(tg_cfg: TelegramConfig, chat_id: str, msg_id: int): try: _tg_req(tg_cfg, "deleteMessage", { @@ -325,6 +344,11 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict): if not conv or conv.get("summary_msg_id") != msg_id: return logger.info(" 换一批 (已有 %d 条历史)", len(conv.get("all_suggestions", []))) + # 先显示思考中 + _tg_req(tg_cfg, "editMessageText", { + "chat_id": chat_id, "message_id": msg_id, + "text": "💭 思考中...", "parse_mode": None, + }) try: new_suggestions = generate_more_replies( cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"], @@ -365,6 +389,11 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict): logger.info(" 取消, 删除提示消息") elif action == CALLBACK_PROMO_DETAIL and ctx: + # 先显示思考中 + _tg_req(tg_cfg, "editMessageText", { + "chat_id": chat_id, "message_id": msg_id, + "text": "💭 思考中...", "parse_mode": None, + }) try: details = expand_promo_detail( cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"],