feat: show thinking indicator during AI processing, accept non-numeric verification codes

This commit is contained in:
2026-07-11 22:54:06 +08:00
parent 1c7d0c1b00
commit fc4c5a175c
3 changed files with 69 additions and 10 deletions

View File

@@ -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 要简洁,把需要知道的信息浓缩成一句话,不需要分条列出

View File

@@ -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 发送成功,已标记已读")

View File

@@ -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"],