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
+8 -16
View File
@@ -14,8 +14,6 @@
保修期限是编造的公司政策承诺,一并墙级拦截,见 PRICE_PATTERNS 时间承诺组)。"""
import re
from app.services.contact_pipeline import digits_only
PRICE_PATTERNS = [
re.compile(r"\$\s?\d"),
re.compile(r"\d[\d,.]*\s*(usd\b|美元|块钱|万块?(?!\s*(公里|km)))", re.I),
@@ -215,22 +213,16 @@ NO_CONTACT_ALREADY = ("Got it — our manager will reach out to you at your "
"WhatsApp shortly with the full quote!")
def no_contact_value(sentence: str, user_history: list[dict] | None = None) -> bool:
def no_contact_value(sentence: str) -> bool:
"""零号码墙(v2): AI 输出不得含任何 ≥10 位数字串或邮箱。
唯一豁免: 该串出现在近期客户消息中(客户自己号码的礼貌回显)。
无值比对——年份/规格编号天然安全(已验证)。"""
for m in LONG_NUMBER_RE.finditer(sentence or ""):
c = digits_only(m.group(0))
if user_history and any(c in digits_only(msg.get("content") or "")
for msg in user_history if msg.get("role") == "user"):
continue # 客户回显豁免
回显豁免已移除(2026-10-08 增补设计 §3): 红线规定客户自己的号码/邮箱
也不回显(确认收到即可),旧豁免与红线矛盾且被 GLM 稳定利用回显号码;
AI 输出中的客户号码只可能来自回显,拦截无误杀面。阿语-印度数字由
Unicode \\d 原生匹配(转 ASCII 后同为 ≥10 位)。无值比对——年份/规格
编号天然安全(DN110/5000m 等已验证)。"""
if LONG_NUMBER_RE.search(sentence or ""):
return True
for m in ANY_EMAIL_RE.finditer(sentence or ""):
if user_history and any(m.group(0).lower() in (msg.get("content") or "").lower()
for msg in user_history if msg.get("role") == "user"):
continue
return True
return False
return bool(ANY_EMAIL_RE.search(sentence or ""))
# ---- Task 5b: 再索要墙(已留资时输出侧拦截) ----
+5 -6
View File
@@ -12,9 +12,8 @@ import re
# Unicode 十进制数字 → ASCII(阿语٠-٩/波斯语۰-۹), 中东买家号码 CRM 可直接拨。
# 挂在归一化链最前端(normalize_contacts 的值 / digits_only / scan 与
# latest 的捕获赋值): 存库、canon 去重比对、boundary_filter 客户回显豁免
# 两侧全部经此转成 ASCII——阿语消息与 ASCII 提取值比对口径一致,
# 同一号码不会被误判成"改号"触发更正邮件。
# latest 的捕获赋值): 存库、canon 去重比对全部经此转成 ASCII——阿语消息
# 与 ASCII 提取值比对口径一致,同一号码不会被误判成"改号"触发更正邮件。
_DIGIT_TRANSLATE = str.maketrans(
"٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹", "01234567890123456789")
@@ -82,9 +81,9 @@ def merge_contacts(old: dict | None, new: dict | None) -> dict:
def digits_only(s: str) -> str:
"""提取纯数字(split_validate 位数判断与后续 canon 复用)。
阿语/波斯语数字先转 ASCII——canon 与 boundary_filter 回显豁免两侧
都走本函数,天然保证同形比较(Unicode 正则的数字类本就匹配阿语数字,
位数判定不变;此处只统一"比对与存储的形态")。"""
阿语/波斯语数字先转 ASCII——canon 比对两侧都走本函数,天然保证同形
比较(Unicode 正则的数字类本就匹配阿语数字,位数判定不变;此处只统一
"比对与存储的形态"。boundary_filter 回显豁免移除后不再依赖本函数)。"""
return re.sub(r"\D", "", s.translate(_DIGIT_TRANSLATE))
+2 -2
View File
@@ -320,7 +320,7 @@ class ConversationManager:
dirty = sentence
violation = "boundary"
break
if no_contact_value(sentence, history_for_ai):
if no_contact_value(sentence):
violated = True
dirty = sentence
violation = "no_contact"
@@ -346,7 +346,7 @@ class ConversationManager:
violated = True
dirty = pending
violation = "boundary"
elif no_contact_value(pending, history_for_ai):
elif no_contact_value(pending):
violated = True
dirty = pending
violation = "no_contact"