feat: 会话编排建材化+阿语号码转ASCII+12小时话术跨层同步
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user