fix: 零号码墙回显豁免收窄——AI输出客户号码/邮箱一律拦截

增补设计 §3(需求方决策): 旧"礼貌回显"豁免与"不回显"红线矛盾,且被
GLM 稳定利用回显号码 3/3——AI 输出中的客户号码只可能来自回显,拦截
无误杀面。

- boundary_filter.no_contact_value: 移除 user_history 参数与豁免分支,
  ≥10 位数字串(含阿语数字形态,Unicode \d 原生匹配)/邮箱一律拦;
  digits_only 导入随之清理
- conversation.py 两处调用点同步去参(句子循环 + pending 尾巴)
- contact_pipeline.py 两处注释里指向已移除豁免的表述同步修正
- 测试: 旧豁免测试(echo_exempt/arabic_echo_exemption_consistent)按新
  语义改写为一律拦;新增邮箱回显拦截/规格形态(DN110/5000m/中文)放行;
  流式集成 2 项——客户完整号/邮箱被原样回显 → 整段替换 NO_CONTACT_ALREADY

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-08 13:39:59 +08:00
parent 5f124e691b
commit 41b2ce081f
5 changed files with 84 additions and 35 deletions
+44
View File
@@ -1179,3 +1179,47 @@ def test_cc_wall_chinese_template(async_db, monkeypatch):
assert tail == _CC_WALL_ZH
assert "国家代码" in tail
asyncio.run(manager.db.close())
# ---- 回显豁免收窄(2026-10-08 增补设计 §3: 零号码墙输出侧一律拦) ----
def test_no_contact_wall_blocks_exact_customer_number_echo(async_db, monkeypatch):
"""客户自己的号码被 AI 原样回显 → 拦截整段替换(旧豁免放行此形态——
GLM 稳定利用它回显号码,与"不回显"红线矛盾)。带前缀号构造,避开 CC
触发轮,隔离验证本规则。"""
from app.services.boundary_filter import NO_CONTACT_ALREADY
class ExactEchoAI(FakeAI):
async def stream_chat(self, messages, context=""):
for token in ("Got it — +966501234567! ",
"Our manager will contact you soon."):
yield token
(manager, conv), _ = _setup(async_db, monkeypatch,
ai=ExactEchoAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(manager, conv.session_id, "my whatsapp is +966501234567"))
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert "+966501234567" not in tail # 回显零残留
assert tail == NO_CONTACT_ALREADY # 已留资 → 确认话术替换
asyncio.run(manager.db.close())
def test_no_contact_wall_blocks_customer_email_echo(async_db, monkeypatch):
"""客户邮箱被 AI 回显 → 同规则拦截(确认收到即可,不复述地址)。"""
from app.services.boundary_filter import NO_CONTACT_ALREADY
class EmailEchoAI(FakeAI):
async def stream_chat(self, messages, context=""):
for token in ("Got it, a@b.com — ", "quote goes there soon!"):
yield token
(manager, conv), _ = _setup(async_db, monkeypatch,
ai=EmailEchoAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(manager, conv.session_id, "my email is a@b.com"))
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert "a@b.com" not in tail
assert tail == NO_CONTACT_ALREADY
asyncio.run(manager.db.close())