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
98 lines
3.0 KiB
Python
98 lines
3.0 KiB
Python
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)
|
|
|