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
+64 -5
View File
@@ -57,15 +57,15 @@ def _setup(async_db, monkeypatch, source=ConversationSource.widget, ai=None):
sent = []
async def fake_lead(customer_profile, contact_info, reasoning,
conversation_messages, intent_level=""):
conversation_messages, intent_level="", cc_info=None):
sent.append(("lead", dict(contact_info), intent_level))
return True
async def fake_nudge(contact_info):
async def fake_nudge(contact_info, cc_info=None):
sent.append(("nudge", dict(contact_info), ""))
return True
async def fake_fix(contact_info, count=1):
async def fake_fix(contact_info, count=1, cc_info=None):
sent.append(("fix", dict(contact_info), ""))
return True
@@ -276,7 +276,9 @@ def test_oscillation_does_not_burn_correction_quota(async_db, monkeypatch):
select(Evaluation).where(Evaluation.conversation_id == conv.id))).scalar_one()
ev = asyncio.run(check())
assert ev.correction_count == 2
assert ev.contact_info == {"phone": "18325698745", "whatsapp": "18325698745"}
# Task 6: 末轮裸号轮末补建 _cc(no_ip,确认流未走完) → 快照带等待态尾注
assert ev.contact_info == {"phone": "18325698745(待确认补全号)",
"whatsapp": "18325698745(待确认补全号)"}
asyncio.run(manager.db.close())
@@ -304,7 +306,9 @@ def test_standalone_number_overrides_uncooperative_extractor(async_db, monkeypat
assert kinds == ["lead", "fix", "fix"], f"实际: {kinds}"
assert sent[2][1]["phone"] == "15236541128" # 更正2 邮件内容就是裸号
assert ev.correction_count == 2
assert ev.contact_info == {"phone": "15236541128", "whatsapp": "15236541128"}
# Task 6: 末轮裸号轮末补建 _cc(确认流未走完) → 快照带等待态尾注
assert ev.contact_info == {"phone": "15236541128(待确认补全号)",
"whatsapp": "15236541128(待确认补全号)"}
asyncio.run(manager.db.close())
@@ -1714,3 +1718,58 @@ def test_violated_closing_separated_by_space(async_db, monkeypatch):
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert tail == streamed # 落库=流出
asyncio.run(manager.db.close())
# ---- Task 6: 出站渲染集成(面板尾注/确认不补发/换号走更正/跨会话 e164 去重) ----
def test_cc_first_email_panel_tail_and_no_resend_on_confirm(async_db, monkeypatch):
"""spec §5 邮件时序集成: 首封快照 ev.contact_info 存裸号原文+等待态尾注
(待确认补全号)(面板同步,e164 走派生不落本体——防每轮误判换号);
客户确认(号码未变)不补发更正;换号(canon 变化)才走现有更正通道。"""
_sa(monkeypatch)
ai = _RecAI(extract=[{}, {"cc_reply": {"affirm": True, "country": None}},
{}, {}])
(manager, conv), sent = _setup(async_db, monkeypatch, ai=ai,
source=ConversationSource.standalone)
async def get_ev():
return (await manager.db.execute(
select(Evaluation).where(Evaluation.conversation_id == conv.id))
).scalar_one()
asyncio.run(_send(manager, conv.session_id, "my whatsapp is 0501234567"))
assert [s[0] for s in sent] == ["lead"]
ev = asyncio.run(get_ev())
# 面板尾注: 首封时确认流未落定(attempts=1 < 2),快照值带等待态标注;
# 尾注全非数字,不影响后续 canon 比对
assert ev.contact_info["whatsapp"] == "0501234567(待确认补全号)"
asyncio.run(_send(manager, conv.session_id, "yes that's right"))
asyncio.run(_send(manager, conv.session_id, "and the fittings?"))
assert [s[0] for s in sent] == ["lead"] # 确认落定(号码未变)不补发
asyncio.run(_send(manager, conv.session_id, "my new whatsapp is 0512345678"))
assert [s[0] for s in sent] == ["lead", "fix"] # 换号 → 现有更正通道
ev = asyncio.run(get_ev())
assert ev.contact_info["whatsapp"] == "0512345678(待确认补全号)"
asyncio.run(manager.db.close())
def test_cross_session_dedup_merges_cc_e164_canon(async_db, monkeypatch):
"""Task 6 canon 双保险(跨会话 24h 去重): A 会话留裸号(首封带补全号发信),
B 会话同号带 + 重访 → 去重集并入 A 的 _cc.e164 canon → 回访轻提醒,
不退化为全量询盘。"""
_sa(monkeypatch)
ai1 = _RecAI(extract=[{}])
(m1, c1), sent1 = _setup(async_db, monkeypatch, ai=ai1,
source=ConversationSource.standalone)
asyncio.run(_send(m1, c1.session_id, "my whatsapp is 0501234567"))
assert [s[0] for s in sent1] == ["lead"]
asyncio.run(m1.db.close())
ai2 = _RecAI(extract=[{}])
(m2, c2), sent2 = _setup(async_db, monkeypatch, ai=ai2,
source=ConversationSource.standalone)
asyncio.run(_send(m2, c2.session_id, "my whatsapp is +966501234567"))
assert [s[0] for s in sent2] == ["nudge"] # e164 形态同号 → 仍判回访
asyncio.run(m2.db.close())