feat: 判定树轮前接入+cc_reply消费+出口卫生+存量迁移
Task 3(cc-completion 计划最重集成项): - 轮前钩子 _cc_pre_turn: effective_raw 建成后、reminder 之前跑判定树, 建/重置/迁移 _cc(同 canon 保留状态,canon 变化即 RESET 重走); 与一次性计数写回同事务落库(条件扩为计数或 _cc 变更任一存在) - 轮末钩子 _cc_round_end: 四层联系方式合并后、D1 邮件前——双门控消费 cc_reply(仅 whatsapp 等待态;落定态仅开迟到 country 升级口,no_ip 耗除外),canon 变化 RESET,末次联系方式补建 pass - 出口卫生三封堵: cc_reply 消费后剥离/extractor 回传 _ 前缀键防御性 丢弃/入口 existing_info 滤 _ 复核 - 存量迁移: 旧 _cc_asked=True+裸 whatsapp 无 _cc → 一次性 fallback 态 - _contact_completion_note cc 分支删除,邮箱轻补解锁条件改 cc_settled(_cc.whatsapp);cc 墙门控改线 cc_due(_wa_cc) - 集成实证修两处数据流 bug: WHATSAPP_CTX_RE 间隙吞 + 号(显式国际 前缀被捕成裸号→误追问);同轮扫描合并方向反转(存量覆盖当轮新号, 轮前 canon 变化检测失效——与轮末最新自述优先同语义对齐) - 新增 10 项集成测试(affirm 确认/换号重走/迟到报国升级与忽略/ 卫生三封堵/存量迁移/confirmed 态忽略/同 canon 组归并/异号 phone 静默/纯 phone 无墙),282 项全绿 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+218
-67
@@ -12,7 +12,9 @@ from app.models.database import (
|
||||
Message, MessageRole, CustomerProfile, Evaluation, IntentLevel,
|
||||
LeadPushLog,
|
||||
)
|
||||
from app.services import email_service, lead_push_service
|
||||
from app.services import (
|
||||
cc_completion, email_service, ip_service, lead_push_service,
|
||||
)
|
||||
from app.services.ai_service import AIService
|
||||
from app.services.boundary_filter import (
|
||||
NO_CONTACT_ALREADY, NO_CONTACT_APP, NO_CONTACT_WEB, asks_contact,
|
||||
@@ -20,8 +22,8 @@ from app.services.boundary_filter import (
|
||||
violates_boundary,
|
||||
)
|
||||
from app.services.contact_pipeline import (
|
||||
canon, latest_stated_contacts, merge_contacts, normalize_contacts,
|
||||
resolve_by_recency, scan_user_messages, split_validate,
|
||||
canon, digits_only, latest_stated_contacts, merge_contacts,
|
||||
normalize_contacts, resolve_by_recency, scan_user_messages, split_validate,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -98,9 +100,11 @@ def _lang_note_for(msg: str) -> str:
|
||||
# Task 3(spec §4.1-2/§4.4, 2026-10-08): 缺失渠道轻补逐字模板——与 system.md
|
||||
# 联系方式引导策略节同源。注入的是"缺失渠道"(客户没给过的),与再索要墙的
|
||||
# 渠道精确化(Task 1)配套: 墙只拦已在手渠道的索要,这些话术恰好问缺失渠道。
|
||||
# CC 强制墙(2026-10-08 增补设计 §2)复用同源正文: _CC_ASK_* 是注入指引里
|
||||
# "请逐字使用这句"的正文;_CC_WALL_* 是墙的整段替换句(EN 加 "Got it — "
|
||||
# 衔接头,设计 §2 定稿文案;ZH 与正文同句)——两处逐字锚点不漂移。
|
||||
# CC 强制墙(2026-10-08 增补设计 §2)复用同源正文: _CC_ASK_* 是追问句正文;
|
||||
# _CC_WALL_* 是墙的整段替换句(EN 加 "Got it — " 衔接头;ZH 与正文同句)。
|
||||
# 2026-10-08 判定树设计 Task 3: cc 轻补注入分支已删(判定树接管),_CC_NOTE_*
|
||||
# 不再运行时注入——常量保留作 no_ip 路径/Task 4 回显模板的正文基准
|
||||
# (test_boundary_filter / test_system_prompt_dual_contact 仍锚定其正文)。
|
||||
_CC_ASK_EN = (
|
||||
"Could you send the full number with the country code, "
|
||||
"like +966 5x xxx xxxx? Just so our manager can reach you directly.")
|
||||
@@ -132,15 +136,14 @@ def _contact_completion_note(
|
||||
raw_info: dict | None, confirmed: dict | None, suspect: dict | None,
|
||||
lang: str = "en",
|
||||
) -> tuple[str, list[str]]:
|
||||
"""一次性缺失渠道轻补提醒(spec §4.2/§4.3/§4.3b)。纯函数,返回(注入文案,计数键)。
|
||||
"""邮箱轻补提醒(spec §4.2/§4.3b,2026-10-08 判定树设计收编)。
|
||||
纯函数,返回(注入文案,计数键)。
|
||||
|
||||
串行优先级(§4.3b——两提醒同轮并发时一次只注入一个,顺序固定):
|
||||
1. cc 优先: confirmed 号码渠道(whatsapp/phone)均有值且均不以 +/00 开头、
|
||||
_cc_asked 未标记 → 国家代码追问(引导重发完整号,走改号覆盖机制,
|
||||
管道零改动)
|
||||
2. email 轻补: (任一号码已带前缀 或 _cc_asked 已消耗)且 confirmed 含号码
|
||||
渠道、不含 email、_email_asked 未标记 → 邮箱轻补
|
||||
3. 其余(双渠道齐/仅 email/两计数均耗尽/confirmed 无号码渠道) → ("", [])
|
||||
cc 追问分支已删除——号码区号补全由轮前判定树钩子接管(_cc 状态机,
|
||||
_cc_pre_turn),确认提醒由 cc_due 驱动(Task 4 接)。本函数只管邮箱轻补,
|
||||
且串行解锁条件改为 cc_settled(_cc.whatsapp): 确认流未落定时注入邮箱
|
||||
轻补会让两个"逐字使用这句"同轮竞争(旧 §4.3b 场景①教训);落定即解锁。
|
||||
带 + 号客户无 _cc 状态 → cc_settled(None)=True,旧轻补行为不回归。
|
||||
suspect 仅为与调用处画像判定同签名,规则只看 confirmed(spec §4.2)。
|
||||
"""
|
||||
info = raw_info or {}
|
||||
@@ -148,14 +151,74 @@ def _contact_completion_note(
|
||||
if k in ("whatsapp", "phone") and v]
|
||||
if not numbers:
|
||||
return "", []
|
||||
all_bare = all(not v.startswith(("+", "00")) for v in numbers)
|
||||
if all_bare and not info.get("_cc_asked"):
|
||||
return (_CC_NOTE_ZH if lang == "zh" else _CC_NOTE_EN), ["_cc_asked"]
|
||||
if (not all_bare or info.get("_cc_asked")) \
|
||||
and "email" not in (confirmed or {}) \
|
||||
and not info.get("_email_asked"):
|
||||
return (_EMAIL_NOTE_ZH if lang == "zh" else _EMAIL_NOTE_EN), ["_email_asked"]
|
||||
return "", []
|
||||
if "email" in (confirmed or {}):
|
||||
return "", []
|
||||
if info.get("_email_asked"):
|
||||
return "", []
|
||||
if not cc_completion.cc_settled((info.get("_cc") or {}).get("whatsapp")):
|
||||
return "", [] # whatsapp 确认流未落定: 邮箱轻补串行等待
|
||||
return (_EMAIL_NOTE_ZH if lang == "zh" else _EMAIL_NOTE_EN), ["_email_asked"]
|
||||
|
||||
|
||||
def _cc_pre_turn(raw_info: dict, confirmed: dict, ip_iso: str | None) -> bool:
|
||||
"""轮前判定树钩子(spec §6-1/§3/§4): 对 confirmed 号码建/重置/迁移 _cc。
|
||||
|
||||
- 多渠道策略: 仅 whatsapp 组走确认流;phone 与 whatsapp 同 canon 时共享
|
||||
whatsapp 组状态(不建独立键),异号时 phone 组只记静默补全态
|
||||
(ip_proposed/naive/no_ip,不进确认流)——确认流的消费与门控
|
||||
(cc_due/cc_settled/轮末 transit)一律只看 _cc.whatsapp,phone 态
|
||||
仅供出站渲染(Task 6)。
|
||||
- canon 变化即重置重走判定树(旧补全永不跟随新号);同 canon 保留原状态
|
||||
(attempts 只被 canon 变化重置,spec §4)。
|
||||
- 显式 +/00 号码不建状态(无态=天然落定);prefix_normalized 静默规范化
|
||||
态(confirmed=true)照写,供出站渲染无标注输出。
|
||||
- 存量迁移(spec §4 尾): 旧 _cc_asked=True 且 whatsapp 裸号尚无 _cc 组 →
|
||||
一次性落 fallback 态(已问过不再追问,直通邮箱轻补);判定树算出的
|
||||
e164 建议保留(首封可拨)。_cc 组一旦存在即不再触发。
|
||||
就地更新 raw_info["_cc"],返回是否有变更(调用方与计数写回同事务落库)。
|
||||
"""
|
||||
before = raw_info.get("_cc")
|
||||
cc = dict(before or {})
|
||||
migrate = bool(raw_info.get("_cc_asked"))
|
||||
wa = str(confirmed.get("whatsapp") or "").strip()
|
||||
ph = str(confirmed.get("phone") or "").strip()
|
||||
|
||||
def _refresh(channel: str, value: str) -> None:
|
||||
existing = cc.get(channel)
|
||||
if existing is not None \
|
||||
and str(existing.get("canon") or "") == digits_only(value):
|
||||
return # 同 canon: 保留原状态(attempts 不重置)
|
||||
kind, state = cc_completion.classify(value, ip_iso)
|
||||
if state is None:
|
||||
cc.pop(channel, None) # 显式 +/00(或防御性垃圾输入): 无态即落定
|
||||
return
|
||||
st = dict(state)
|
||||
if (migrate and channel == "whatsapp" and existing is None
|
||||
and kind == cc_completion.NEED_CC):
|
||||
st["confirmed"] = False
|
||||
st["source"] = "fallback" # 存量迁移: 已问过,不再追问
|
||||
st.pop("suggest_ask_full", None)
|
||||
cc[channel] = st
|
||||
|
||||
if wa:
|
||||
_refresh("whatsapp", wa)
|
||||
else:
|
||||
cc.pop("whatsapp", None) # 号码不再在手: 状态清,防残留跟随
|
||||
if ph:
|
||||
wa_state = cc.get("whatsapp")
|
||||
if wa_state is not None \
|
||||
and str(wa_state.get("canon") or "") == digits_only(ph):
|
||||
cc.pop("phone", None) # 同 canon 组归并: phone 共享 whatsapp 状态
|
||||
else:
|
||||
_refresh("phone", ph)
|
||||
else:
|
||||
cc.pop("phone", None)
|
||||
|
||||
if cc:
|
||||
raw_info["_cc"] = cc
|
||||
else:
|
||||
raw_info.pop("_cc", None)
|
||||
return raw_info.get("_cc") != before
|
||||
|
||||
|
||||
# 方案B(13.6): 迂回话术轮换序号——连续破防时不至于复读同一句
|
||||
@@ -247,40 +310,64 @@ class ConversationManager:
|
||||
profile = await self._get_profile(conv.id)
|
||||
|
||||
# P0 同轮竞态消除: 当前消息即时正则扫描——仅并入本轮内存视图
|
||||
# (提醒/话术路由),不落库;持久化仍由轮末 extractor+scanner 负责,
|
||||
# 邮件触发时序不变。
|
||||
# (提醒/话术路由/轮前 cc 判定树),不落库;持久化仍由轮末
|
||||
# extractor+scanner 负责,邮件触发时序不变。
|
||||
# 合并方向(Task 3 修正): 当前消息捕获覆盖存量——客户当轮改号时,
|
||||
# 同轮视图必须看到新号(轮前判定树 canon 变化即重置依赖此方向;
|
||||
# 与轮末 Task 12 "最新显式自述优先"同语义)。存量只在当前消息
|
||||
# 未提及的渠道上补位。
|
||||
current_scan = scan_user_messages([{"role": "user", "content": content}])
|
||||
effective_raw = dict(profile.raw_info or {})
|
||||
if current_scan:
|
||||
effective_raw["联系方式"] = merge_contacts(
|
||||
current_scan, effective_raw.get("联系方式"))
|
||||
effective_raw.get("联系方式"), current_scan)
|
||||
|
||||
# 联系方式视图(轮前钩子/计数写回/再索要墙区共用;原在 reminder 之后
|
||||
# 计算,前移只为喂轮前钩子,输入相同取值不变)
|
||||
_contacts_now = normalize_contacts(effective_raw.get("联系方式"))
|
||||
_confirmed_now, _suspect_now = split_validate(_contacts_now)
|
||||
|
||||
# Task 3 轮前判定树钩子(spec §6-1;评审 Step 3b 钉死顺序: 必须在
|
||||
# _build_context_reminder 之前)——即时扫描并入裸号后同步跑判定树
|
||||
# (纯计算毫秒级)建/重置/迁移 _cc,当轮即可被墙门控/轮末消费/出站
|
||||
# 渲染读到;落库与下方一次性计数写回同事务。
|
||||
_ip_iso = ip_service.resolve_region_code(
|
||||
(conv.client_info or {}).get("ip"))
|
||||
_cc_changed = _cc_pre_turn(effective_raw, _confirmed_now, _ip_iso)
|
||||
_wa_cc = (effective_raw.get("_cc") or {}).get("whatsapp")
|
||||
|
||||
context_reminder = self._build_context_reminder(
|
||||
effective_raw, content, history, source=conv.source.value)
|
||||
if CONTACT_ASK_TRIGGER_RE.search(content or ""):
|
||||
logger.info("Contact-ask reminder fired (source=%s)", conv.source.value)
|
||||
|
||||
# Task 3(spec §4.3): 一次性计数写回——_build_context_reminder 是
|
||||
# staticmethod 收内存副本,计数键须由调用方在流式开始前显式落库;
|
||||
# 轮末 extractor 合并起点是 dict(profile.raw_info or {}),提前提交可存活。
|
||||
# 与 reminder 内部同参调用(上方纯函数),保证写回与注入永不漂移。
|
||||
_contacts_now = normalize_contacts(effective_raw.get("联系方式"))
|
||||
_confirmed_now, _suspect_now = split_validate(_contacts_now)
|
||||
# Task 3(spec §4.3/§6-1): 一次性计数写回 + _cc 状态同事务落库——
|
||||
# _build_context_reminder 是 staticmethod 收内存副本,计数键/状态须由
|
||||
# 调用方在流式开始前显式落库;轮末 extractor 合并起点是
|
||||
# dict(profile.raw_info or {}),提前提交可存活。与 reminder 内部同参
|
||||
# 调用(上方纯函数),保证写回与注入永不漂移;条件扩为"计数或 _cc
|
||||
# 变更任一存在"(评审 Step 3b)。
|
||||
_, completion_marks = _contact_completion_note(
|
||||
effective_raw, _confirmed_now, _suspect_now,
|
||||
"zh" if _CJK_RE.search(content or "") else "en")
|
||||
if completion_marks:
|
||||
if completion_marks or _cc_changed:
|
||||
raw = dict(profile.raw_info or {})
|
||||
for key in completion_marks:
|
||||
raw[key] = True
|
||||
if _cc_changed:
|
||||
if effective_raw.get("_cc"):
|
||||
raw["_cc"] = effective_raw["_cc"]
|
||||
else:
|
||||
raw.pop("_cc", None)
|
||||
profile.raw_info = raw
|
||||
self.db.add(profile)
|
||||
await self.db.commit()
|
||||
# warning 级(2026-10-08 第四轮验收): 生产曾现 2/5 裸号轮墙未介入的
|
||||
# 非确定性异常(同输入容器内链路复现正常)——本行生产可见,异常会话
|
||||
# 可立刻分辨"计数/注入是否发生",供观察期定位
|
||||
logger.warning("Contact completion note injected (conv=%s): %s",
|
||||
conv.id, completion_marks)
|
||||
# 可立刻分辨"计数/状态/注入是否发生",供观察期定位
|
||||
logger.warning("Contact completion state persisted (conv=%s): "
|
||||
"marks=%s cc_changed=%s",
|
||||
conv.id, completion_marks, _cc_changed)
|
||||
|
||||
# Enrich short user messages with context so AI doesn't lose track
|
||||
history_for_ai = list(history)
|
||||
@@ -297,11 +384,14 @@ class ConversationManager:
|
||||
# (confirmed/suspect 已在 Task 3 写回处算好,此处复用)
|
||||
has_contact = bool(_confirmed_now)
|
||||
_confirmed_channels = set(_confirmed_now)
|
||||
# CC 强制墙(2026-10-08 增补设计 §2): 本轮注入的是国家代码追问时,
|
||||
# 输出侧确定性强制。流式无法撤回 → 触发轮不逐句流出,整段缓冲到
|
||||
# 流末统一判 "country code" 在场与否(缓冲期间价格/零号码/再索要
|
||||
# 三墙照常逐句检查,优先级不变)。提示词层全部保留,墙只是兜底。
|
||||
cc_wall = completion_marks == ["_cc_asked"]
|
||||
# CC 强制墙(2026-10-08 cc 设计): 门控改线判定树——cc_due(_cc.whatsapp)
|
||||
# 等待态即触发(旧 `completion_marks == ["_cc_asked"]` 门控随 cc 轻补
|
||||
# 分支删除而消亡,_cc_asked 停写)。流式无法撤回 → 触发轮不逐句流出,
|
||||
# 整段缓冲到流末统一判 "country code" 在场与否(缓冲期间价格/零号码/
|
||||
# 再索要三墙照常逐句检查,优先级不变)。no_ip 态(无可回显号)沿用
|
||||
# 本套 ASK 模板与检测词;有建议国态的回显确认模板/e164 检测词由
|
||||
# Task 4 接管。
|
||||
cc_wall = cc_completion.cc_due(_wa_cc)
|
||||
|
||||
yield {"type": "stream_start"}
|
||||
emitted = ""
|
||||
@@ -398,13 +488,13 @@ 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 增补设计 §2): 触发轮缓冲全文在此判定——
|
||||
# 含 "country code"(中文轮认 "国家代码",与 _CC_NOTE_ZH 注入的逐字
|
||||
# 追问句同词)→ GLM 自发服从,缓冲原样流出,零干预;不含 → 整段替换
|
||||
# 为 CC 模板句正文。violated(价格/零号码墙)优先于本墙——沿用双墙
|
||||
# 裁决"更具体话术优先"惯例;_cc_asked 已在流前落库,一次性消耗不受
|
||||
# 替换影响。缓冲轮全部实义句被再索要墙丢弃时 cc_buffer 为空 → 同样
|
||||
# 走替换(模板句即本轮唯一实义输出,天然不触发下方兜底)。
|
||||
# CC 强制墙裁决(2026-10-08 cc 设计): 触发轮缓冲全文在此判定——
|
||||
# 含 "country code"(中文轮认 "国家代码",与 ASK 模板句同词)→ GLM
|
||||
# 自发服从,缓冲原样流出,零干预;不含 → 整段替换为 CC 模板句正文。
|
||||
# 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:
|
||||
@@ -450,7 +540,7 @@ class ConversationManager:
|
||||
# Task 12b: 轮前联系方式快照(extractor 深合并 + 覆盖 + 同号同步的比对基线)
|
||||
before_contacts = normalize_contacts((profile.raw_info or {}).get("联系方式"))
|
||||
|
||||
# _ 前缀内部键(_cc_asked 等一次性计数)不进 LLM 输入(Task 3 审查
|
||||
# _ 前缀内部键(_cc 状态机/_email_asked 计数)不进 LLM 输入(Task 3 审查
|
||||
# Minor 3 防御纵深——渲染/推送侧已过滤,此处防提示词侧意外回显)
|
||||
new_info = await self.ai.extract_info(
|
||||
full_history,
|
||||
@@ -458,6 +548,21 @@ class ConversationManager:
|
||||
if not str(k).startswith("_")},
|
||||
)
|
||||
logger.info("Extractor result: %s", new_info)
|
||||
# 出口卫生(spec §4 三封堵之②/③): cc_reply 是 extractor 的单轮瞬态
|
||||
# 分类,提取后即剥离——绝不进 raw_info 持久层(消费点在轮末钩子
|
||||
# _cc_round_end);同时防御性丢弃 extractor 回传的任何 `_` 前缀键,
|
||||
# 防 LLM 回显 _cc 覆盖 Python 管理的状态机。(入口侧封堵①: 上方
|
||||
# existing_info 已滤 `_`,复核通过)
|
||||
cc_reply = None
|
||||
if isinstance(new_info, dict):
|
||||
cc_reply = new_info.pop("cc_reply", None)
|
||||
stripped = {k: v for k, v in new_info.items()
|
||||
if not str(k).startswith("_")}
|
||||
if len(stripped) != len(new_info):
|
||||
logger.warning(
|
||||
"Dropped underscore keys from extractor output (conv=%s): %s",
|
||||
conv.id, [k for k in new_info if str(k).startswith("_")])
|
||||
new_info = stripped
|
||||
if new_info:
|
||||
raw = dict(profile.raw_info or {})
|
||||
for k, v in new_info.items():
|
||||
@@ -505,6 +610,11 @@ class ConversationManager:
|
||||
profile.raw_info = raw
|
||||
self.db.add(profile)
|
||||
|
||||
# Task 3 轮末 cc 钩子(spec §6-2): 联系方式四层合并完成后、D1 邮件
|
||||
# 触发前——消费 cc_reply 推进状态机,并为本轮才入管道的号码补建 _cc
|
||||
# (spec §5: 首封补全号/推送派生依赖已建好的 _cc)。
|
||||
await self._cc_round_end(conv, profile, cc_reply)
|
||||
|
||||
# Round 3: Evaluate (仅 ≥3 条客户消息才有意义, 前两轮必然 pending —— D7)
|
||||
user_msg_count = sum(1 for m in full_history if m["role"] == "user")
|
||||
evaluation_ran = user_msg_count >= 3
|
||||
@@ -590,6 +700,56 @@ class ConversationManager:
|
||||
seen.add(canon(str(v)))
|
||||
return seen
|
||||
|
||||
async def _cc_round_end(
|
||||
self, conv: Conversation, profile: CustomerProfile,
|
||||
cc_reply: dict | None,
|
||||
) -> None:
|
||||
"""轮末 cc 钩子(spec §6-2): 联系方式四层合并后、D1 邮件前调用。
|
||||
|
||||
- cc_reply 消费(双门控 Python 侧): 仅 whatsapp 组等待态(cc_due)消费
|
||||
——无回应/漏填也推进(attempts 随轮递增,达上限落定 fallback);
|
||||
落定态仅开迟到 country 升级口: 客户主动报国(非空 country)且
|
||||
source 非 no_ip 耗尽 → transit 重算 e164(客户主动给的信息不
|
||||
丢弃);迟到 affirm 不升级(对已标"未经确认"旧号的确认语义模糊,
|
||||
confirmed 态更不重算——客户改主意请重发完整号,走 canon 重置)。
|
||||
- canon 变化(换号/号消失)→ RESET 删组状态。
|
||||
- 补建 pass: 对最终联系方式重跑判定树(_cc_pre_turn)——本轮才经
|
||||
extractor 入管道的裸号/换号后的新号当场建状态,不下轮补课。
|
||||
"""
|
||||
original_cc = (profile.raw_info or {}).get("_cc")
|
||||
ip_iso = ip_service.resolve_region_code(
|
||||
(conv.client_info or {}).get("ip"))
|
||||
raw = dict(profile.raw_info or {})
|
||||
confirmed_end, _ = split_validate(normalize_contacts(raw.get("联系方式")))
|
||||
wa_canon_end = digits_only(str(confirmed_end.get("whatsapp") or ""))
|
||||
|
||||
cc_map = dict(raw.get("_cc") or {})
|
||||
state = cc_map.get("whatsapp")
|
||||
|
||||
reply = None
|
||||
if isinstance(cc_reply, dict):
|
||||
country = str(cc_reply.get("country") or "").strip()
|
||||
if cc_reply.get("affirm") is not None or country:
|
||||
if cc_completion.cc_due(state):
|
||||
reply = cc_reply
|
||||
elif (state is not None and not state.get("confirmed")
|
||||
and country and state.get("source") != "no_ip"):
|
||||
reply = cc_reply # 迟到 country 升级口
|
||||
|
||||
if state is not None:
|
||||
if not wa_canon_end or wa_canon_end != str(state.get("canon") or ""):
|
||||
cc_map.pop("whatsapp", None) # 换号/号消失 → RESET
|
||||
elif reply is not None or cc_completion.cc_due(state):
|
||||
cc_map["whatsapp"] = cc_completion.transit(state, reply)
|
||||
if cc_map:
|
||||
raw["_cc"] = cc_map
|
||||
else:
|
||||
raw.pop("_cc", None)
|
||||
_cc_pre_turn(raw, confirmed_end, ip_iso)
|
||||
if raw.get("_cc") != original_cc:
|
||||
profile.raw_info = raw
|
||||
self.db.add(profile)
|
||||
|
||||
async def _maybe_send_lead_email(
|
||||
self, conv: Conversation, profile: CustomerProfile, evaluation: dict,
|
||||
evaluated: bool = True,
|
||||
@@ -783,11 +943,12 @@ class ConversationManager:
|
||||
if val:
|
||||
parts.append(f"{key}: {val}")
|
||||
|
||||
# Task 3(spec §4.2/§4.3b): 缺失渠道一次性轻补——cc 优先、邮箱次之,串行
|
||||
# 注入。计数键(_marks)此处弃用: 本函数是 staticmethod 收内存副本,写回由
|
||||
# stream 调用方以同参调用纯函数取回并落库(纯函数保证两处判定不漂移)。
|
||||
# 计算前移到 confirmed 块之前: CC 追问轮 confirmed 块的确认句要让位。
|
||||
completion_note, completion_marks = _contact_completion_note(
|
||||
# Task 3(spec §4.2/§4.3b): 邮箱轻补注入(cc 分支已删,判定树接管号码
|
||||
# 区号补全——确认提醒由 cc_due 驱动,Task 4 接)。计数键(_marks)此处
|
||||
# 弃用: 本函数是 staticmethod 收内存副本,写回由 stream 调用方以同参
|
||||
# 调用纯函数取回并落库(纯函数保证两处判定不漂移)。计算前移到
|
||||
# confirmed 块之前: 未落定轮邮箱轻补不注入(cc_settled 串行)。
|
||||
completion_note, _marks = _contact_completion_note(
|
||||
raw_info, confirmed, suspect,
|
||||
lang="zh" if _CJK_RE.search(latest_msg or "") else "en")
|
||||
|
||||
@@ -802,19 +963,8 @@ class ConversationManager:
|
||||
parts.append(f"⚠️ 客户已提供联系方式({shown})。绝对禁止再索要客户已提供过"
|
||||
f"的联系方式;缺失渠道仅可按轻补指引提一次;号码缺国家代码"
|
||||
f"时可请客户重发完整号一次。")
|
||||
if completion_marks == ["_cc_asked"]:
|
||||
# 生产验收场景①修复(2026-10,三轮): CC 追问是对已在手渠道的重发
|
||||
# 请求,不在"缺失渠道"例外内——同轮两个"逐字使用这句"竞争时 GLM
|
||||
# 稳定服从更显眼的确认句,压制追问。本轮 confirmed 块让位: 唯一
|
||||
# 逐字锚点=下方轻补指引的追问句;禁数字回显堵住"让位后 GLM 自创
|
||||
# 确认句带出真实号码"的回归(墙的客户回显豁免会放行)
|
||||
parts.append("本轮不要使用确认句,请逐字使用下面这条国家代码追问句请客户"
|
||||
"重发带国家代码的完整号码(仅此一次,客户重发或明确不给后"
|
||||
"下一轮恢复正常)。回复中绝不写出号码数字——提到号码一律用 "
|
||||
"your WhatsApp 指代。")
|
||||
else:
|
||||
parts.append(f"需要回应时可直接逐字使用:\"{NO_CONTACT_ALREADY}\"(中文同理)。"
|
||||
f"确认收到后继续解答客户问题即可,不必急着结束对话。")
|
||||
parts.append(f"需要回应时可直接逐字使用:\"{NO_CONTACT_ALREADY}\"(中文同理)。"
|
||||
f"确认收到后继续解答客户问题即可,不必急着结束对话。")
|
||||
if boundary_note:
|
||||
parts.append(boundary_note)
|
||||
# Task 3 防自锁改写: 尾部补缺失渠道豁免(与上方改写同因)
|
||||
@@ -833,7 +983,8 @@ class ConversationManager:
|
||||
if boundary_note and not confirmed and not suspect:
|
||||
parts.append(boundary_note) # 未留资:边界提醒引导留资(职能边界话术自带)
|
||||
|
||||
# Task 3 轻补注入(计算已前移到 confirmed 块之前——CC 轮让位判定需要)
|
||||
# Task 3 轻补注入(计算已前移到 confirmed 块之前;cc_settled 未落定时
|
||||
# note 为空,天然不注入)
|
||||
if completion_note:
|
||||
parts.append(completion_note)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user