feat: add email notification channel for chat replies
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
2026-03-03 14:22:56 +08:00
parent 484f1f6be4
commit 31c74e661e
3 changed files with 171 additions and 13 deletions

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
import asyncio
import smtplib
import time
from datetime import datetime
from email.header import Header
from email.mime.text import MIMEText
from typing import Any
from .config import (
EMAIL_NOTIFY_ENABLED,
SMTP_HOST,
SMTP_PASSWORD,
SMTP_PORT,
SMTP_TO,
SMTP_USER,
)
_last_push: dict[tuple[str, str], tuple[str, str, float]] = {}
def _truncate(text: str, max_len: int = 220) -> str:
t = str(text or "").strip()
if len(t) <= max_len:
return t
return f"{t[:max_len]}..."
def _send_mail(subject: str, body: str) -> tuple[bool, str]:
if not EMAIL_NOTIFY_ENABLED:
return False, "email_disabled"
if not SMTP_HOST or not SMTP_USER or not SMTP_PASSWORD or not SMTP_TO:
return False, "email_config_incomplete"
try:
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = f"{Header('千牛AI通知', 'utf-8').encode()} <{SMTP_USER}>"
msg["To"] = SMTP_TO
if int(SMTP_PORT) == 465:
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=10) as server:
server.login(SMTP_USER, SMTP_PASSWORD)
server.sendmail(SMTP_USER, [SMTP_TO], msg.as_string())
else:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASSWORD)
server.sendmail(SMTP_USER, [SMTP_TO], msg.as_string())
return True, "ok"
except Exception as e:
return False, str(e)
async def push_chat_to_email(
*,
customer_name: str,
customer_id: str,
acc_id: str,
customer_msg: str,
reply_msg: str,
goods_name: str = "",
recent_dialogue: list[dict[str, Any]] | None = None,
) -> tuple[bool, str]:
if not EMAIL_NOTIFY_ENABLED:
return False, "email_disabled"
key = (str(customer_id or ""), str(acc_id or ""))
now = time.time()
last = _last_push.get(key)
cm = str(customer_msg or "")
rm = str(reply_msg or "")
if last:
lcm, lrm, lts = last
if lcm == cm and lrm == rm and (now - lts) < 30:
return False, "dedup_30s"
_last_push[key] = (cm, rm, now)
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
subject = f"[千牛AI]{acc_id or '店铺'}-{customer_name or customer_id or '客户'}"
lines: list[str] = [f"时间: {ts}"]
lines.append(f"店铺: {acc_id or '未知店铺'}")
lines.append(f"客户: {customer_name or '-'} ({customer_id or '-'})")
if goods_name:
lines.append(f"商品: {_truncate(goods_name, 120)}")
lines.append("")
for item in (recent_dialogue or [])[-8:]:
role = "客户" if str(item.get("role", "")) == "user" else "客服"
txt = _truncate(str(item.get("text", "") or ""), 180)
if txt:
lines.append(f"{role}: {txt}")
lines.append(f"客户: {_truncate(cm, 220)}")
lines.append(f"客服: {_truncate(rm, 220)}")
body = "\n".join(lines)
return await asyncio.to_thread(_send_mail, subject, body)