feat: 出站补全渲染——首封e164+标注/主题行/推送派生/canon双保险/面板尾注
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -282,6 +282,49 @@ def _cc_pre_turn(raw_info: dict, confirmed: dict, ip_iso: str | None) -> bool:
|
||||
return raw_info.get("_cc") != before
|
||||
|
||||
|
||||
# Task 6(spec §5 面板同步): 等待确认期间的联系方式值尾注——防"邮件带补全、
|
||||
# 后台显裸号"的分裂。尾注全为非数字字符,canon() 纯数字提取不受影响
|
||||
# (24h 去重/更正判定照常)。
|
||||
_CC_PENDING_TAIL = "(待确认补全号)"
|
||||
|
||||
|
||||
def _panel_contact_info(confirmed: dict, cc_map: dict | None) -> dict:
|
||||
"""ev.contact_info 存储视图(Task 6 评审钉死): 裸号原文 + 等待态尾注,
|
||||
e164 走派生不落本体——e164 替换本体会让"存储值 vs 每轮裸号确认值"
|
||||
永远不同 canon,退化成每轮误判换号→更正连发/回访退化。
|
||||
只标 whatsapp 确认流覆盖的号码(canon 与 _cc.whatsapp 一致,含同号
|
||||
phone 镜像): phone 独立静默态不进确认流、attempts 永不推进,cc_due
|
||||
恒真,若照标会永久滞留且语义失真。"""
|
||||
wa = (cc_map or {}).get("whatsapp")
|
||||
wa_canon = str((wa or {}).get("canon") or "")
|
||||
pending = bool(wa_canon) and cc_completion.cc_due(wa)
|
||||
out: dict = {}
|
||||
for k, v in (confirmed or {}).items():
|
||||
if (pending and k in ("phone", "whatsapp")
|
||||
and digits_only(str(v)) == wa_canon):
|
||||
out[k] = f"{v}{_CC_PENDING_TAIL}"
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _channel_canons(confirmed: dict, cc_map: dict | None) -> dict[str, set[str]]:
|
||||
"""canon 口径双保险(Task 6 评审钉死): 每渠道候选集 = 裸号 canon ∪
|
||||
_cc.e164 的 canon——同号换形态(裸号↔+前缀完整形态)不误判换号
|
||||
(更正连发)、回访不退化成全量询盘。只扩"新值侧": 若把当前 _cc 的
|
||||
e164 canon 并入旧值/历史集,换号后新号恰为前缀完整形态时会自我满足,
|
||||
真更正被吞。"""
|
||||
out: dict[str, set[str]] = {}
|
||||
for k, v in (confirmed or {}).items():
|
||||
cands = {canon(str(v))}
|
||||
state = cc_completion.cc_state_for(cc_map, k, str(v))
|
||||
e164 = str((state or {}).get("e164") or "")
|
||||
if e164:
|
||||
cands.add(canon(e164))
|
||||
out[k] = cands
|
||||
return out
|
||||
|
||||
|
||||
# 方案B(13.6): 迂回话术轮换序号——连续破防时不至于复读同一句
|
||||
_boundary_seq = 0
|
||||
|
||||
@@ -720,6 +763,7 @@ class ConversationManager:
|
||||
# 评估落库(D8 泛化): 每轮评估后刷新意向;email_sent/correction_count
|
||||
# 永不触碰。有意向无联系方式 = 卡单(admin 可筛);已发过邮件的
|
||||
# contact_info 由触发器更正分支管理,这里不覆盖。
|
||||
# Task 6: 存储视图走面板尾注(等待态标注,e164 派生不落本体)
|
||||
if user_msg_count >= 3:
|
||||
ev = await self._get_or_init_evaluation(conv.id)
|
||||
ev.intent_level = IntentLevel(evaluation.get("intent_level", "pending"))
|
||||
@@ -727,7 +771,8 @@ class ConversationManager:
|
||||
if not confirmed_now:
|
||||
ev.contact_info = {}
|
||||
elif not ev.email_sent:
|
||||
ev.contact_info = confirmed_now
|
||||
ev.contact_info = _panel_contact_info(
|
||||
confirmed_now, (profile.raw_info or {}).get("_cc"))
|
||||
self.db.add(ev)
|
||||
|
||||
# 决策A(2026-08-18): 自动结束移除——会话永不因评估结果结束;
|
||||
@@ -761,18 +806,29 @@ class ConversationManager:
|
||||
|
||||
async def _recently_emailed_canons(self) -> set[str]:
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
# 只取需要的列;🔴 禁止加 LIMIT——完整 24h 窗口才正确
|
||||
# 只取需要的列;🔴 禁止加 LIMIT——完整 24h 窗口才正确。
|
||||
# Task 6 双保险: join 画像取历史会话的 _cc——各渠道 e164 的 canon
|
||||
# 一并入集(快照存裸号原文+尾注,客户以 + 前缀完整形态回访同号时
|
||||
# 仍要命中,否则回访退化成全量询盘)。
|
||||
result = await self.db.execute(
|
||||
select(Evaluation.contact_info).where(
|
||||
select(Evaluation.contact_info, CustomerProfile.raw_info)
|
||||
.outerjoin(
|
||||
CustomerProfile,
|
||||
CustomerProfile.conversation_id == Evaluation.conversation_id)
|
||||
.where(
|
||||
Evaluation.email_sent.is_(True),
|
||||
Evaluation.evaluated_at >= cutoff,
|
||||
)
|
||||
)
|
||||
seen: set[str] = set()
|
||||
for (contact_info,) in result:
|
||||
for contact_info, raw_info in result:
|
||||
for v in (contact_info or {}).values():
|
||||
if v:
|
||||
seen.add(canon(str(v)))
|
||||
for state in ((raw_info or {}).get("_cc") or {}).values():
|
||||
e164 = str((state or {}).get("e164") or "")
|
||||
if e164:
|
||||
seen.add(canon(e164))
|
||||
return seen
|
||||
|
||||
async def _cc_round_end(
|
||||
@@ -840,16 +896,22 @@ class ConversationManager:
|
||||
confirmed, _suspect = split_validate(contacts_all)
|
||||
if not confirmed:
|
||||
return
|
||||
# Task 6(spec §5): 出站渲染取 _cc(首封补全号/标注/主题行/推送派生);
|
||||
# 轮末钩子已保证 _cc 与最终联系方式同 canon 同步
|
||||
cc_map = (profile.raw_info or {}).get("_cc") or {}
|
||||
ev = await self._get_or_init_evaluation(conv.id)
|
||||
|
||||
if not ev.email_sent:
|
||||
# S3: 任一值已见于24h内其他会话 → 轻提醒;否则全量
|
||||
# S3: 任一值已见于24h内其他会话 → 轻提醒;否则全量。
|
||||
# Task 6 双保险: 比对候选集含 _cc.e164 的 canon(见 _channel_canons)
|
||||
seen = await self._recently_emailed_canons()
|
||||
if any(canon(str(v)) in seen for v in confirmed.values()):
|
||||
if any(cands & seen
|
||||
for cands in _channel_canons(confirmed, cc_map).values()):
|
||||
await self._collect_push(
|
||||
pending_pushes, ev, conv, profile, confirmed,
|
||||
"returning", evaluation, evaluated)
|
||||
sent = await email_service.send_returning_lead_email(confirmed)
|
||||
sent = await email_service.send_returning_lead_email(
|
||||
confirmed, cc_info=cc_map)
|
||||
else:
|
||||
await self._collect_push(
|
||||
pending_pushes, ev, conv, profile, confirmed,
|
||||
@@ -861,27 +923,32 @@ class ConversationManager:
|
||||
reasoning=evaluation.get("reasoning", ""),
|
||||
conversation_messages=messages,
|
||||
intent_level=evaluation.get("intent_level", "") if evaluated else "",
|
||||
cc_info=cc_map,
|
||||
)
|
||||
if sent:
|
||||
ev.email_sent = True
|
||||
ev.contact_info = confirmed
|
||||
ev.contact_info = _panel_contact_info(confirmed, cc_map)
|
||||
self.db.add(ev)
|
||||
else:
|
||||
old_canons = {canon(str(v)) for v in (ev.contact_info or {}).values()}
|
||||
if any(canon(str(v)) not in old_canons for v in confirmed.values()):
|
||||
# Task 6 双保险: 新值候选集含 _cc.e164 的 canon——确认落定
|
||||
# (号码未变)不补发更正;真换号(两组候选都不在旧集)才走更正
|
||||
if any(not (cands & old_canons)
|
||||
for cands in _channel_canons(confirmed, cc_map).values()):
|
||||
if ev.correction_count < 3:
|
||||
count = ev.correction_count + 1
|
||||
await self._collect_push(
|
||||
pending_pushes, ev, conv, profile, confirmed,
|
||||
"correction", evaluation, evaluated)
|
||||
if await email_service.send_correction_email(confirmed, count=count):
|
||||
if await email_service.send_correction_email(
|
||||
confirmed, count=count, cc_info=cc_map):
|
||||
ev.correction_count = count
|
||||
ev.contact_info = confirmed
|
||||
ev.contact_info = _panel_contact_info(confirmed, cc_map)
|
||||
self.db.add(ev)
|
||||
else:
|
||||
# D4'(2026-08-18): 锁邮件不锁快照 —— 上限已到,
|
||||
# admin 仍要看到客户最新号码
|
||||
ev.contact_info = confirmed
|
||||
ev.contact_info = _panel_contact_info(confirmed, cc_map)
|
||||
self.db.add(ev)
|
||||
|
||||
async def _collect_push(
|
||||
|
||||
Reference in New Issue
Block a user