feat: 会话编排建材化+阿语号码转ASCII+12小时话术跨层同步

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-29 17:59:45 +08:00
parent 874308c099
commit 9caf3c4501
8 changed files with 88 additions and 46 deletions
+32 -16
View File
@@ -10,6 +10,19 @@
"""
import re
# Unicode 十进制数字 → ASCII(阿语٠-٩/波斯语۰-۹), 中东买家号码 CRM 可直接拨。
# 挂在归一化链最前端(normalize_contacts 的值 / digits_only / scan 与
# latest 的捕获赋值): 存库、canon 去重比对、boundary_filter 客户回显豁免
# 两侧全部经此转成 ASCII——阿语消息与 ASCII 提取值比对口径一致,
# 同一号码不会被误判成"改号"触发更正邮件。
_DIGIT_TRANSLATE = str.maketrans(
"٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹", "01234567890123456789")
def _to_ascii_digits(value: str) -> str:
"""捕获源头归一: 阿语/波斯语数字转 ASCII(translate 对 ASCII 恒等)。"""
return value.translate(_DIGIT_TRANSLATE)
KEY_ALIASES = {
"phone": "phone", "手机": "phone", "电话": "phone", "电话号码": "phone",
"联系电话": "phone", "手机号": "phone", "手机号码": "phone",
@@ -38,7 +51,7 @@ def normalize_contacts(raw: dict | None) -> dict:
if name in key:
alias = target
break
val = str(v or "").strip()
val = _to_ascii_digits(str(v or "").strip())
if alias and val:
out[alias] = val
return out
@@ -50,8 +63,11 @@ def merge_contacts(old: dict | None, new: dict | None) -> dict:
def digits_only(s: str) -> str:
"""提取纯数字(split_validate 位数判断与后续 canon 复用)。"""
return re.sub(r"\D", "", s)
"""提取纯数字(split_validate 位数判断与后续 canon 复用)。
阿语/波斯语数字先转 ASCII——canon 与 boundary_filter 回显豁免两侧
都走本函数,天然保证同形比较(Unicode 正则的数字类本就匹配阿语数字,
位数判定不变;此处只统一"比对与存储的形态")。"""
return re.sub(r"\D", "", s.translate(_DIGIT_TRANSLATE))
def split_validate(contacts: dict) -> tuple[dict, dict]:
@@ -72,7 +88,7 @@ def split_validate(contacts: dict) -> tuple[dict, dict]:
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
INTL_PHONE_RE = re.compile(r"\+\d{6,15}")
# 邻接绑定:上下文词与数字必须在同一匹配内(仅空白衔接)——禁止"消息任意位置
# 有上下文词 + 任意位置有 7 位数字"的消息级误绑(S1:车架号/订单号/先令价格)
# 有上下文词 + 任意位置有 7 位数字"的消息级误绑(S1:单号/订单号/先令价格)
WHATSAPP_CTX_RE = re.compile(
r"(?:whats\s*app|wa\.?me|(?:^|\b)wa\s*[::])\D{0,12}(\+?\d[\d\s\-().]{5,17}\d)",
re.IGNORECASE,
@@ -129,7 +145,7 @@ CORRECTION_CTX_RE = re.compile(
def scan_user_messages(messages: list[dict]) -> dict:
"""S4: 扫描全部客户消息,返回高置信捕获(email / +国际号 / whatsapp·wa.me /
显式电话上下文邻接数字)。裸数字(价格/年份/里程/车架号)一律不收。"""
显式电话上下文邻接数字)。裸数字(价格/年份/数量/单号)一律不收。"""
found: dict[str, str] = {}
for msg in messages:
if msg.get("role") != "user":
@@ -142,30 +158,30 @@ def scan_user_messages(messages: list[dict]) -> dict:
if "phone" not in found:
sn = standalone_number(text)
if sn:
found["phone"] = sn
found["phone"] = _to_ascii_digits(sn)
if "phone" not in found:
hit = INTL_PHONE_RE.search(text)
if hit:
found["phone"] = hit.group(0)
found["phone"] = _to_ascii_digits(hit.group(0))
if "whatsapp" not in found:
hit = WHATSAPP_CTX_RE.search(text)
if hit:
found["whatsapp"] = hit.group(1)
found["whatsapp"] = _to_ascii_digits(hit.group(1))
if "phone" not in found:
hit = PHONE_CTX_RE.search(text)
if hit:
found["phone"] = hit.group(1)
found["phone"] = _to_ascii_digits(hit.group(1))
if "phone" not in found:
hit = PHONE_ADJACENCY_SCAN_RE.search(text)
if hit:
found["phone"] = hit.group(1)
found["phone"] = _to_ascii_digits(hit.group(1))
# 倒装属最低置信层: whatsapp/phone 任一已被更高置信源捕获即跳过,
# 不与之竞争(跨 key 门控故含 phone)。
if "whatsapp" not in found and "phone" not in found:
hit = NUMBER_FIRST_RE.search(text)
if hit:
ch = _number_first_channel(hit)
found[ch] = hit.group(1)
found[ch] = _to_ascii_digits(hit.group(1))
return found
@@ -211,21 +227,21 @@ def latest_stated_contacts(messages: list[dict]) -> dict:
latest["email"] = hit.group(0)
hit = WHATSAPP_CTX_RE.search(text)
if hit:
latest["whatsapp"] = hit.group(1)
latest["whatsapp"] = _to_ascii_digits(hit.group(1))
sn = standalone_number(text)
if sn:
latest["phone"] = sn
latest["phone"] = _to_ascii_digits(sn)
hit = PHONE_CTX_RE.search(text)
if hit:
latest["phone"] = hit.group(1)
latest["phone"] = _to_ascii_digits(hit.group(1))
elif CORRECTION_CTX_RE.search(text):
hit = INTL_PHONE_RE.search(text)
if hit:
latest["phone"] = hit.group(0)
latest["phone"] = _to_ascii_digits(hit.group(0))
hit = NUMBER_FIRST_RE.search(text)
if hit:
ch = _number_first_channel(hit)
latest[ch] = hit.group(1)
latest[ch] = _to_ascii_digits(hit.group(1))
return latest