feat: 出站补全渲染——首封e164+标注/主题行/推送派生/canon双保险/面板尾注

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-08 19:51:46 +08:00
parent 9d16235454
commit 7df2e4ae1e
8 changed files with 524 additions and 37 deletions
+42 -8
View File
@@ -6,6 +6,7 @@ from email.mime.text import MIMEText
import aiosmtplib
from app.config import settings
from app.services.cc_completion import cc_state_for, outbound_e164, outbound_number
logger = logging.getLogger(__name__)
@@ -33,6 +34,29 @@ def _contact_rows(contacts: dict) -> str:
)
def _cc_views(contacts: dict, cc_info: dict | None) -> tuple[dict, dict]:
"""Task 6(spec §5)出站双视图:_cc 感知的正文/主题两套联系方式值。
正文视图(body):phone/whatsapp 有态 → e164 + 按 confirmed 标注;
无 e164 → 裸号 +(缺国家区号);canon 不匹配的残留态在 cc_state_for
内判 None 回退原文。主题视图(subj):同状态一律纯 e164 无标注
(主题拥挤)。其余渠道(email/wechat)原样。
"""
body: dict = {}
subj: dict = {}
for k, v in (contacts or {}).items():
if not v:
continue
if k in ("phone", "whatsapp"):
state = cc_state_for(cc_info, k, str(v))
body[k] = outbound_number(str(v), state)
subj[k] = outbound_e164(str(v), state)
else:
body[k] = str(v)
subj[k] = str(v)
return body, subj
async def _send(msg: MIMEMultipart) -> bool:
if not all([settings.SMTP_HOST, settings.SMTP_USER, settings.NOTIFY_EMAIL]):
logger.warning("SMTP not configured, skipping email")
@@ -56,16 +80,19 @@ async def send_lead_email(
reasoning: str,
conversation_messages: list[dict],
intent_level: str = "",
cc_info: dict | None = None,
) -> bool:
"""cc_info = raw_info["_cc"](Task 6, spec §5):正文联系方式行渲染
补全号+标注、主题行号码用 e164 无标注;缺省(无 _cc/显式 + 号)行为不变。"""
tag = TAG_LABELS.get(intent_level, "未评估")
contacts = dict(contact_info or {})
contacts, subj_contacts = _cc_views(contact_info, cc_info)
country = (customer_profile or {}).get("目标国家", "")
if not country:
country = (customer_profile or {}).get("target_country", "未知")
msg = MIMEMultipart("alternative")
contact_parts = [
f"{CONTACT_LABELS.get(k, k)}: {_flat(v)}" for k, v in contacts.items() if v
f"{CONTACT_LABELS.get(k, k)}: {_flat(v)}" for k, v in subj_contacts.items()
]
subject = f"[新询盘·{tag}] {_flat(country)}"
if contact_parts:
@@ -101,10 +128,13 @@ async def send_lead_email(
return await _send(msg)
async def send_returning_lead_email(contact_info: dict) -> bool:
"""D5: 24h 内同联系方式再访,只发一行轻提醒。"""
async def send_returning_lead_email(
contact_info: dict, cc_info: dict | None = None,
) -> bool:
"""D5: 24h 内同联系方式再访,只发一行轻提醒。主题行号码走 e164(Task 6)。"""
_body, subj_contacts = _cc_views(contact_info, cc_info)
contact_parts = [
f"{CONTACT_LABELS.get(k, k)}: {_flat(v)}" for k, v in (contact_info or {}).items() if v
f"{CONTACT_LABELS.get(k, k)}: {_flat(v)}" for k, v in subj_contacts.items()
]
msg = MIMEMultipart("alternative")
msg["Subject"] = f"[线索回访] {' | '.join(contact_parts) or '已知客户'}再次来访"
@@ -116,14 +146,18 @@ async def send_returning_lead_email(contact_info: dict) -> bool:
return await _send(msg)
async def send_correction_email(contact_info: dict, count: int = 1) -> bool:
"""D4: 联系方式变化(改号/新增/无提示换值)后的轻量更正邮件,每会话上限3封。"""
async def send_correction_email(
contact_info: dict, count: int = 1, cc_info: dict | None = None,
) -> bool:
"""D4: 联系方式变化(改号/新增/无提示换值)后的轻量更正邮件,每会话上限3封。
正文联系方式行渲染补全号+标注(Task 6, spec §5 更正路径)。"""
body_contacts, _subj = _cc_views(contact_info, cc_info)
msg = MIMEMultipart("alternative")
msg["Subject"] = f"[询盘·更正{count}] 客户联系方式已更新(以本封为准)"
msg["From"] = f"【HESUPIPE】 <{settings.SMTP_USER}>"
msg["To"] = settings.NOTIFY_EMAIL
msg.attach(MIMEText(
f"<html><body><p>这是本会话第 {count} 次更正(上限 3 次),请以本封为准:</p>"
f"<table>{_contact_rows(contact_info)}</table>"
f"<table>{_contact_rows(body_contacts)}</table>"
f"</body></html>", "html", "utf-8"))
return await _send(msg)