diff --git a/src/ai_client.py b/src/ai_client.py index ced66b3..afcc1f6 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -15,10 +15,21 @@ SYSTEM_PROMPT = """你是一个邮件摘要助手。请分析邮件内容并以 "priority": "high/medium/low", "action_required": true/false, "action_items": ["待办事项1", "待办事项2"], - "key_points": ["关键要点1", "关键要点2"] + "key_points": ["关键要点1", "关键要点2"], + "can_reply": true/false, + "reply_suggestions": [ + {"text": "回复内容", "is_simple": true/false}, + {"text": "回复内容", "is_simple": true/false}, + {"text": "回复内容", "is_simple": true/false} + ] } -其中 subject 字段不能照抄原邮件主题,必须是你总结生成的简短标题。 +其中: +- subject 不能照抄原邮件主题,必须是你总结生成的简短标题 +- can_reply 为 false 表示无需回复或不适合建议回复(此时忽略 reply_suggestions) +- is_simple 为 true 表示纯确认性回复(如"好的""收到"),用户选择后可直发 +- is_simple 为 false 表示涉及实质内容,需要用户确认再发送 +- 每条回复控制在 50 字以内 只返回 JSON,不要包含任何其他文字。""" @@ -35,46 +46,6 @@ def summarize_email(ai_cfg: AIConfig, recipient: str, subject: str, sender: str, "response_format": {"type": "json_object"}, } - headers = { - "Authorization": f"Bearer {ai_cfg.api_key}", - "Content-Type": "application/json", - } - - return _call_deepseek(ai_cfg, payload) - - -REPLY_PROMPT = """你是一个邮件回复助手。根据以下邮件内容生成3个建议回复。 - -返回 JSON: -{ - "can_reply": true/false, - "suggestions": [ - {"text": "回复内容", "is_simple": true/false}, - {"text": "回复内容", "is_simple": true/false}, - {"text": "回复内容", "is_simple": true/false} - ] -} - -- can_reply 为 false 时表示无需回复或不适合自动建议(此时忽略 suggestions) -- is_simple 为 true 表示纯确认性回复(如"好的""收到""明白"),用户选择后可直接发送 -- is_simple 为 false 表示涉及实质内容,需要用户确认后再发送 -- 每条回复控制在 50 字以内 -只返回 JSON,不要包含任何其他文字。""" - - -@retry() -def generate_reply_suggestions(ai_cfg: AIConfig, sender: str, subject: str, body: str) -> dict: - content = f"发件人: {sender}\n主题: {subject}\n正文:\n{body[:4000]}" - - payload = { - "model": ai_cfg.model, - "messages": [ - {"role": "system", "content": REPLY_PROMPT}, - {"role": "user", "content": content}, - ], - "response_format": {"type": "json_object"}, - } - return _call_deepseek(ai_cfg, payload) diff --git a/src/tg_bot.py b/src/tg_bot.py index 6eff8dd..a2b340f 100644 --- a/src/tg_bot.py +++ b/src/tg_bot.py @@ -2,7 +2,7 @@ import json import logging import requests from src.config import TelegramConfig, Config -from src.ai_client import generate_reply_suggestions +# reply suggestions come embedded in summary_data from summarize_email from src.smtp_client import send_reply from src.retry import retry @@ -21,11 +21,12 @@ _conversations: dict[int, dict] = {} @retry() def send_summary(tg_cfg: TelegramConfig, chat_id: str, summary_text: str, summary_data: dict, original_body: str, account_idx: int) -> int: + can_reply = summary_data.get("can_reply", True) result = _tg_req(tg_cfg, "sendMessage", { "chat_id": chat_id, "text": summary_text, "parse_mode": "MarkdownV2", - "reply_markup": _summary_keyboard(), + "reply_markup": _summary_keyboard(can_reply), }) msg_id = result["result"]["message_id"] _email_contexts[msg_id] = { @@ -35,6 +36,7 @@ def send_summary(tg_cfg: TelegramConfig, chat_id: str, summary_text: str, "account_idx": account_idx, "sender": summary_data.get("sender", ""), "subject": summary_data.get("subject", ""), + "can_reply": can_reply, } return msg_id @@ -108,32 +110,28 @@ CALLBACK_CONFIRM_REPLY = "cr" CALLBACK_CANCEL = "c" -def _summary_keyboard() -> dict: - return { - "inline_keyboard": [ - [ - {"text": "查看原文", "callback_data": CALLBACK_VIEW_ORIG}, - {"text": "回复", "callback_data": CALLBACK_REPLY}, - ], - [ - {"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}, - ], - ] - } +def _summary_keyboard(can_reply: bool = True) -> dict: + kb = [ + [ + {"text": "查看原文", "callback_data": CALLBACK_VIEW_ORIG}, + {"text": "回复", "callback_data": CALLBACK_REPLY}, + ], + ] + if can_reply: + kb.append([{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}]) + return {"inline_keyboard": kb} -def _orig_keyboard() -> dict: - return { - "inline_keyboard": [ - [ - {"text": "返回摘要", "callback_data": CALLBACK_VIEW_SUMM}, - {"text": "回复", "callback_data": CALLBACK_REPLY}, - ], - [ - {"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}, - ], - ] - } +def _orig_keyboard(can_reply: bool = True) -> dict: + kb = [ + [ + {"text": "返回摘要", "callback_data": CALLBACK_VIEW_SUMM}, + {"text": "回复", "callback_data": CALLBACK_REPLY}, + ], + ] + if can_reply: + kb.append([{"text": "AI回复", "callback_data": CALLBACK_AI_REPLY}]) + return {"inline_keyboard": kb} def _ai_reply_keyboard(msg_id: int, suggestions: list) -> dict: @@ -191,57 +189,52 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict): ctx = _email_contexts.get(msg_id) if action == CALLBACK_VIEW_ORIG and ctx: + can_reply = ctx.get("can_reply", True) text = f"*📄 原文 \\- {_escape(ctx['subject'])}*\n\n{_escape(ctx['original_body'][:3500])}" _tg_req(tg_cfg, "editMessageText", { "chat_id": chat_id, "message_id": msg_id, "text": text, "parse_mode": "MarkdownV2", - "reply_markup": _orig_keyboard(), + "reply_markup": _orig_keyboard(can_reply), }) elif action == CALLBACK_VIEW_SUMM and ctx: + can_reply = ctx.get("can_reply", True) _tg_req(tg_cfg, "editMessageText", { "chat_id": chat_id, "message_id": msg_id, "text": ctx["summary_text"], "parse_mode": "MarkdownV2", - "reply_markup": _summary_keyboard(), + "reply_markup": _summary_keyboard(can_reply), }) elif action == CALLBACK_REPLY and ctx: _conversations[chat_id] = {"state": "awaiting_reply", "msg_id": msg_id} + can_reply = ctx.get("can_reply", True) _tg_req(tg_cfg, "editMessageText", { "chat_id": chat_id, "message_id": msg_id, "text": ctx["summary_text"], "parse_mode": "MarkdownV2", - "reply_markup": _summary_keyboard(), + "reply_markup": _summary_keyboard(can_reply), }) send_text(tg_cfg, str(chat_id), "请输入你的回复内容:") elif action == CALLBACK_AI_REPLY and ctx: - try: - acct = cfg.email_accounts[ctx["account_idx"]] - suggestions = generate_reply_suggestions( - cfg.ai, ctx["sender"], ctx["subject"], ctx["original_body"], - ) - except Exception as e: - send_text(tg_cfg, str(chat_id), f"AI 生成回复失败: {e}") - return - - if not suggestions.get("can_reply") or not suggestions.get("suggestions"): + suggestions = ctx["summary_data"].get("reply_suggestions", []) + if not suggestions: send_text(tg_cfg, str(chat_id), "此邮件无需回复,已隐藏 AI 回复功能。") return _conversations[chat_id] = { "state": "ai_reply_selection", "msg_id": msg_id, - "ai_suggestions": suggestions["suggestions"], + "ai_suggestions": suggestions, } text = "*AI 建议回复,请选择:*\n\n" - for i, s in enumerate(suggestions["suggestions"], 1): + for i, s in enumerate(suggestions, 1): tag = "✅ 可直接发送" if s.get("is_simple") else "📝 需确认" text += f"{i}\\. {_escape(s['text'])} _{tag}_\n" _tg_req(tg_cfg, "editMessageText", { "chat_id": chat_id, "message_id": msg_id, "text": text, "parse_mode": "MarkdownV2", - "reply_markup": _ai_reply_keyboard(msg_id, suggestions["suggestions"]), + "reply_markup": _ai_reply_keyboard(msg_id, suggestions), }) elif action == CALLBACK_SELECT_REPLY and ctx: @@ -282,10 +275,11 @@ def _handle_callback(tg_cfg: TelegramConfig, cfg: Config, cb: dict): elif action == CALLBACK_CANCEL: _conversations.pop(chat_id, None) if ctx: + can_reply = ctx.get("can_reply", True) _tg_req(tg_cfg, "editMessageText", { "chat_id": chat_id, "message_id": msg_id, "text": ctx["summary_text"], "parse_mode": "MarkdownV2", - "reply_markup": _summary_keyboard(), + "reply_markup": _summary_keyboard(can_reply), })