feat: 回显确认模板+确认句墙e164检测+白名单逐段放行+no_ip专属
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+112
-26
@@ -121,6 +121,14 @@ _CC_NOTE_ZH = (
|
||||
"⚠️ 轻补指引(系统指令,绝对不要向客户提及这段说明):客户留下的号码不含"
|
||||
"国家代码,客户经理无法直接回拨。请逐字使用这句:\"" + _CC_ASK_ZH + "\" "
|
||||
"仅此一次,客户不给就不再提。")
|
||||
# Task 4(spec §4 回显确认轮/§4b): 判定树给出 e164 建议的等待态轮用回显确认
|
||||
# 模板——{intl} 由 Python format_intl 算好填入(reminder/墙替换/测试断言三方
|
||||
# 同源引用本常量),GLM 只逐字复述不拼号。no_ip 轮(无可回显号)仍走 _CC_ASK_*
|
||||
# (上方保留不拆),两条路径按 _cc.whatsapp 的 e164 在场分流。
|
||||
_CC_ECHO_EN = (
|
||||
"Got it — just to confirm: our manager will reach you at {intl} — is that right?")
|
||||
_CC_ECHO_ZH = (
|
||||
"收到——跟您确认下:客户经理会通过 {intl} 联系您,这个号码对吗?")
|
||||
_EMAIL_NOTE_EN = (
|
||||
"⚠️ 轻补指引(系统指令,绝对不要向客户提及这段说明):客户已留号码但还没"
|
||||
"留邮箱。请逐字使用这句:\"Got it! And an email address would help too — "
|
||||
@@ -132,6 +140,55 @@ _EMAIL_NOTE_ZH = (
|
||||
"发你邮箱。\" 仅此一次,客户不给就不再提。")
|
||||
|
||||
|
||||
def _cc_intl(wa_state: dict | None) -> str:
|
||||
"""等待态的分组展示号: intl 键缺失时按 e164 防御重算(存量/迁移数据)。"""
|
||||
e164 = str((wa_state or {}).get("e164") or "")
|
||||
if not e164:
|
||||
return ""
|
||||
return str((wa_state or {}).get("intl") or cc_completion.format_intl(e164))
|
||||
|
||||
|
||||
def _cc_script(wa_state: dict | None, lang: str) -> str:
|
||||
"""CC 等待态本轮逐字话术(Task 4, spec §4): 有 e164 → 回显确认句
|
||||
(intl 由 Python 算好填入,GLM 不拼号);no_ip → 追问句(_CC_ASK_* 服务)。
|
||||
reminder 注入/墙替换/测试断言三方同源,统一走本函数。"""
|
||||
if str((wa_state or {}).get("e164") or ""):
|
||||
intl = _cc_intl(wa_state)
|
||||
return (_CC_ECHO_ZH if lang == "zh" else _CC_ECHO_EN).format(intl=intl)
|
||||
return _CC_ASK_ZH if lang == "zh" else _CC_ASK_EN
|
||||
|
||||
|
||||
def _cc_wall_template(wa_state: dict | None, lang: str) -> str:
|
||||
"""CC 触发轮墙整段替换句: 回显轮 = ECHO 模板(自带 "Got it —" 衔接头);
|
||||
no_ip 轮 = _CC_WALL_*(ASK 追问句 + 衔接头,沿用 Task 3 形态)。"""
|
||||
if str((wa_state or {}).get("e164") or ""):
|
||||
return _cc_script(wa_state, lang)
|
||||
return _CC_WALL_ZH if lang == "zh" else _CC_WALL_EN
|
||||
|
||||
|
||||
def _cc_confirm_note(wa_state: dict | None, lang: str) -> str:
|
||||
"""cc 等待态确认句注入(Task 4, spec §4/§4b): cc_due 才注入,与邮箱轻补
|
||||
cc_settled 串行互斥(两个"逐字使用这句"不同轮竞争)。外壳仿轻补指引:
|
||||
⚠️ 系统指令声明 + 逐字话术 + 客户答复后致谢不追问。回显分支带零号码
|
||||
唯一例外(4b-3 让位语义重建: 仅系统提供的补全号原文可写出)。"""
|
||||
if not wa_state or not cc_completion.cc_due(wa_state):
|
||||
return ""
|
||||
if str(wa_state.get("e164") or ""):
|
||||
intl = _cc_intl(wa_state)
|
||||
return (
|
||||
"⚠️ 号码确认指引(系统指令,绝对不要向客户提及这段说明):客户留下"
|
||||
"的号码不含国家代码,系统已按 IP 推测补全为 " + intl +
|
||||
"。请逐字使用这句:\"" + _cc_script(wa_state, lang) + "\" "
|
||||
"本轮回复绝不写出任何号码数字——唯一例外是上面系统提供的补全号"
|
||||
"原文(逐字照抄,禁止自编或改动数字)。客户答复确认或纠正后,"
|
||||
"致谢即可,不再追问号码。")
|
||||
return (
|
||||
"⚠️ 号码确认指引(系统指令,绝对不要向客户提及这段说明):客户留下"
|
||||
"的号码不含国家代码,且系统无法按 IP 推测补全。请逐字使用这句:\""
|
||||
+ _cc_script(wa_state, lang) + "\" 仅此一次,客户不给就不再提。"
|
||||
"客户答复后致谢即可,不再追问号码。")
|
||||
|
||||
|
||||
def _contact_completion_note(
|
||||
raw_info: dict | None, confirmed: dict | None, suspect: dict | None,
|
||||
lang: str = "en",
|
||||
@@ -387,11 +444,14 @@ class ConversationManager:
|
||||
# CC 强制墙(2026-10-08 cc 设计): 门控改线判定树——cc_due(_cc.whatsapp)
|
||||
# 等待态即触发(旧 `completion_marks == ["_cc_asked"]` 门控随 cc 轻补
|
||||
# 分支删除而消亡,_cc_asked 停写)。流式无法撤回 → 触发轮不逐句流出,
|
||||
# 整段缓冲到流末统一判 "country code" 在场与否(缓冲期间价格/零号码/
|
||||
# 再索要三墙照常逐句检查,优先级不变)。no_ip 态(无可回显号)沿用
|
||||
# 本套 ASK 模板与检测词;有建议国态的回显确认模板/e164 检测词由
|
||||
# Task 4 接管。
|
||||
# 整段缓冲到流末统一裁决(缓冲期间价格/零号码/再索要三墙照常逐句
|
||||
# 检查,优先级不变)。Task 4(spec §4/§4b): 回显轮(等待态有 e164)裁决
|
||||
# 锚 = 注入 e164 的 digits 等值在场,替换句 = ECHO 模板;no_ip 轮
|
||||
# (无可回显号)沿用 ASK 模板与 "country code" 问句短语锚。
|
||||
cc_wall = cc_completion.cc_due(_wa_cc)
|
||||
# Task 4 白名单传参(spec §4): 零号码墙逐段放行系统注入的补全号——
|
||||
# 仅回显轮(等待态有 e164)传,其余轮 None 保持原行为(全拦)
|
||||
cc_allow_number = ((_wa_cc or {}).get("e164") if cc_wall else None)
|
||||
|
||||
yield {"type": "stream_start"}
|
||||
emitted = ""
|
||||
@@ -413,7 +473,7 @@ class ConversationManager:
|
||||
dirty = sentence
|
||||
violation = "boundary"
|
||||
break
|
||||
if no_contact_value(sentence):
|
||||
if no_contact_value(sentence, allow_number=cc_allow_number):
|
||||
violated = True
|
||||
dirty = sentence
|
||||
violation = "no_contact"
|
||||
@@ -439,7 +499,7 @@ class ConversationManager:
|
||||
violated = True
|
||||
dirty = pending
|
||||
violation = "boundary"
|
||||
elif no_contact_value(pending):
|
||||
elif no_contact_value(pending, allow_number=cc_allow_number):
|
||||
violated = True
|
||||
dirty = pending
|
||||
violation = "no_contact"
|
||||
@@ -462,12 +522,13 @@ class ConversationManager:
|
||||
"No-contact wall replaced sentence (conv=%s): %r",
|
||||
conv.id, dirty[:60])
|
||||
if cc_wall:
|
||||
# Important-2(审查修复): CC 触发轮回显被拦 → closing 改用
|
||||
# CC 模板句而非 NO_CONTACT_ALREADY——GLM 有 3/3 回显倾向,
|
||||
# 该路径概率高,客户该收到追问而非"稍后联系"虚假承诺;
|
||||
# Important-2(审查修复) + Task 4: CC 触发轮回显被拦 →
|
||||
# closing 改用本轮 CC 模板(回显轮 ECHO/no_ip 轮 ASK)而非
|
||||
# NO_CONTACT_ALREADY——GLM 有 3/3 回显倾向,该路径概率高,
|
||||
# 客户该收到确认/追问而非"稍后联系"虚假承诺;
|
||||
# 价格墙路径(violation=="boundary")不变
|
||||
closing = (_CC_WALL_ZH if _CJK_RE.search(content or "")
|
||||
else _CC_WALL_EN)
|
||||
closing = _cc_wall_template(
|
||||
_wa_cc, "zh" if _CJK_RE.search(content or "") else "en")
|
||||
else:
|
||||
closing = (NO_CONTACT_ALREADY if has_contact
|
||||
else (NO_CONTACT_APP if conv.source.value == "app"
|
||||
@@ -488,16 +549,27 @@ class ConversationManager:
|
||||
for i in range(0, len(closing), 40):
|
||||
yield {"type": "stream_chunk", "content": closing[i:i + 40]}
|
||||
emitted += closing
|
||||
# CC 强制墙裁决(2026-10-08 cc 设计): 触发轮缓冲全文在此判定——
|
||||
# 含 "country code"(中文轮认 "国家代码",与 ASK 模板句同词)→ GLM
|
||||
# 自发服从,缓冲原样流出,零干预;不含 → 整段替换为 CC 模板句正文。
|
||||
# violated(价格/零号码墙)优先于本墙——沿用双墙裁决"更具体话术优先"
|
||||
# 惯例;_cc 状态已在流前落库,不受替换影响。缓冲轮全部实义句被再索要
|
||||
# 墙丢弃时 cc_buffer 为空 → 同样走替换(模板句即本轮唯一实义输出,
|
||||
# 天然不触发下方兜底)。
|
||||
# CC 强制墙裁决(2026-10-08 cc 设计;Task 4 改造 spec §4b-4): 触发轮
|
||||
# 缓冲全文在此判定——回显轮(等待态有 e164)裁决锚 = **注入 e164 的
|
||||
# digits 等值在场**(GLM 任意分组形态 "+966 50 123 4567"/"+966501234567"
|
||||
# 均判在场;辅锚 "is that right"/"对吗" 随模板正文,不作独立判据——
|
||||
# 号码在场才是回显的承重语义);no_ip 轮无可回显号,沿用问句短语锚
|
||||
# ("country code"/"国家代码",与 _CC_ASK_* 模板句同词)。在场 → GLM
|
||||
# 自发服从,缓冲原样流出零干预;不在场 → 整段替换为对应 ECHO/ASK
|
||||
# 模板。violated(价格/零号码墙)优先于本墙——沿用双墙裁决"更具体
|
||||
# 话术优先"惯例;_cc 状态已在流前落库,不受替换影响。缓冲轮全部
|
||||
# 实义句被再索要墙丢弃时 cc_buffer 为空 → 同样走替换(模板句即
|
||||
# 本轮唯一实义输出,天然不触发下方兜底)。
|
||||
if cc_wall and not violated:
|
||||
cc_full = "".join(cc_buffer)
|
||||
if "country code" in cc_full.lower() or "国家代码" in cc_full:
|
||||
cc_lang = "zh" if _CJK_RE.search(content or "") else "en"
|
||||
wa_e164 = str((_wa_cc or {}).get("e164") or "")
|
||||
if wa_e164:
|
||||
obeyed = digits_only(wa_e164) in digits_only(cc_full)
|
||||
else:
|
||||
obeyed = ("country code" in cc_full.lower()
|
||||
or "国家代码" in cc_full)
|
||||
if obeyed:
|
||||
for i in range(0, len(cc_full), 40):
|
||||
yield {"type": "stream_chunk", "content": cc_full[i:i + 40]}
|
||||
emitted = cc_full
|
||||
@@ -505,8 +577,7 @@ class ConversationManager:
|
||||
logger.warning(
|
||||
"CC enforcement wall replaced reply (conv=%s): %r",
|
||||
conv.id, cc_full[:60])
|
||||
cc_template = (_CC_WALL_ZH if _CJK_RE.search(content or "")
|
||||
else _CC_WALL_EN)
|
||||
cc_template = _cc_wall_template(_wa_cc, cc_lang)
|
||||
for i in range(0, len(cc_template), 40):
|
||||
yield {"type": "stream_chunk",
|
||||
"content": cc_template[i:i + 40]}
|
||||
@@ -914,7 +985,8 @@ class ConversationManager:
|
||||
"WhatsApp shortly with the full quote!\"(中文同理)。"
|
||||
"绝对禁止输出任何号码或邮箱,也绝对禁止再索要客户已提供过"
|
||||
"的联系方式(缺失渠道可按轻补指引提一次;号码缺国家代码时"
|
||||
"可请客户重发完整号一次)。")
|
||||
"按系统提醒回显补全后的完整号码请客户确认——仅系统提供的"
|
||||
"号码,禁止自编)。")
|
||||
else:
|
||||
contact_note = ("⚠️ 客户正在索要我方联系方式(最高意向信号)。当前为 App 会话,"
|
||||
"没有页面按钮——禁止使用任何 tap/button 按钮话术!请逐字使用这句:"
|
||||
@@ -952,6 +1024,14 @@ class ConversationManager:
|
||||
raw_info, confirmed, suspect,
|
||||
lang="zh" if _CJK_RE.search(latest_msg or "") else "en")
|
||||
|
||||
# Task 4(spec §4 回显确认轮): cc 等待态注入确认句指引——有 e164 用
|
||||
# 回显模板(逐字 intl),no_ip 用 ASK 追问句;与邮箱轻补 cc_settled
|
||||
# 串行互斥,两个"逐字使用这句"永不同轮。无计数键: 轮次上限由状态机
|
||||
# attempts 管理(轮末 transit 递增),提醒每轮等待态都在场。
|
||||
cc_note = _cc_confirm_note(
|
||||
(raw_info.get("_cc") or {}).get("whatsapp"),
|
||||
"zh" if _CJK_RE.search(latest_msg or "") else "en")
|
||||
|
||||
# 联系方式专项(D6) 与 职能边界(D6联动)
|
||||
if confirmed:
|
||||
shown = ", ".join(
|
||||
@@ -961,8 +1041,9 @@ class ConversationManager:
|
||||
# Task 3 防自锁改写(spec 4.1-6): 原"绝对禁止再索要任何联系方式!"
|
||||
# 会抑制缺失渠道轻补——只禁已提供过的渠道,缺失渠道放行一次
|
||||
parts.append(f"⚠️ 客户已提供联系方式({shown})。绝对禁止再索要客户已提供过"
|
||||
f"的联系方式;缺失渠道仅可按轻补指引提一次;号码缺国家代码"
|
||||
f"时可请客户重发完整号一次。")
|
||||
f"的联系方式;缺失渠道仅可按轻补指引提一次;号码缺国家代码时"
|
||||
f"按系统提醒回显补全后的完整号码请客户确认(仅系统提供的"
|
||||
f"号码,禁止自编)。")
|
||||
parts.append(f"需要回应时可直接逐字使用:\"{NO_CONTACT_ALREADY}\"(中文同理)。"
|
||||
f"确认收到后继续解答客户问题即可,不必急着结束对话。")
|
||||
if boundary_note:
|
||||
@@ -970,8 +1051,9 @@ class ConversationManager:
|
||||
# Task 3 防自锁改写: 尾部补缺失渠道豁免(与上方改写同因)
|
||||
parts.append("客户已留过联系方式——边界请求直接告知'客户经理会把准确报价/"
|
||||
"安排发到你 WhatsApp'即可,禁止再索要联系方式"
|
||||
"(缺失渠道可按轻补指引提一次;号码缺国家代码时可请客户"
|
||||
"重发完整号一次)。")
|
||||
"(缺失渠道可按轻补指引提一次;号码缺国家代码时按系统"
|
||||
"提醒回显补全后的完整号码请客户确认——仅系统提供的号码,"
|
||||
"禁止自编)。")
|
||||
elif suspect:
|
||||
shown = ", ".join(
|
||||
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in suspect.items())
|
||||
@@ -983,6 +1065,10 @@ class ConversationManager:
|
||||
if boundary_note and not confirmed and not suspect:
|
||||
parts.append(boundary_note) # 未留资:边界提醒引导留资(职能边界话术自带)
|
||||
|
||||
# Task 4 cc 确认指引注入(与下方邮箱轻补互斥——cc_due vs cc_settled)
|
||||
if cc_note:
|
||||
parts.append(cc_note)
|
||||
|
||||
# Task 3 轻补注入(计算已前移到 confirmed 块之前;cc_settled 未落定时
|
||||
# note 为空,天然不注入)
|
||||
if completion_note:
|
||||
|
||||
Reference in New Issue
Block a user