feat: 国家代码/邮箱双要状态提醒+一次性计数(串行优先级+防自锁禁令改写)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-08 12:04:10 +08:00
parent 1416ebf104
commit 2228ebe081
2 changed files with 188 additions and 6 deletions
+92 -6
View File
@@ -95,6 +95,60 @@ def _lang_note_for(msg: str) -> str:
return ""
# Task 3(spec §4.1-2/§4.4, 2026-10-08): 缺失渠道轻补逐字模板——与 system.md
# 联系方式引导策略节同源。注入的是"缺失渠道"(客户没给过的),与再索要墙的
# 渠道精确化(Task 1)配套: 墙只拦已在手渠道的索要,这些话术恰好问缺失渠道。
_CC_NOTE_EN = (
"⚠️ 轻补指引(系统指令,绝对不要向客户提及这段说明):客户留下的号码不含"
"国家代码,客户经理无法直接回拨。请逐字使用这句:\"Could you send the "
"full number with the country code, like +966 5x xxx xxxx? Just so our "
"manager can reach you directly.\" 仅此一次,客户不给就不再提。")
_CC_NOTE_ZH = (
"⚠️ 轻补指引(系统指令,绝对不要向客户提及这段说明):客户留下的号码不含"
"国家代码,客户经理无法直接回拨。请逐字使用这句:\"麻烦重发一下带国家代码"
"的完整号码呗(比如 +966 5x xxx xxxx)?客户经理直接加你。\" "
"仅此一次,客户不给就不再提。")
_EMAIL_NOTE_EN = (
"⚠️ 轻补指引(系统指令,绝对不要向客户提及这段说明):客户已留号码但还没"
"留邮箱。请逐字使用这句:\"Got it! And an email address would help too — "
"our manager will send the formal quotation PDF there.\" "
"仅此一次,客户不给就不再提。")
_EMAIL_NOTE_ZH = (
"⚠️ 轻补指引(系统指令,绝对不要向客户提及这段说明):客户已留号码但还没"
"留邮箱。请逐字使用这句:\"收到!邮箱也留一个呗?客户经理把正式报价单 PDF "
"发你邮箱。\" 仅此一次,客户不给就不再提。")
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)。纯函数,返回(注入文案,计数键)。
串行优先级(§4.3b——两提醒同轮并发时一次只注入一个,顺序固定):
1. cc 优先: confirmed 号码渠道(whatsapp/phone)均有值且均不以 +/00 开头、
_cc_asked 未标记 → 国家代码追问(引导重发完整号,走改号覆盖机制,
管道零改动)
2. email 轻补: (任一号码已带前缀 或 _cc_asked 已消耗)且 confirmed 含号码
渠道、不含 email、_email_asked 未标记 → 邮箱轻补
3. 其余(双渠道齐/仅 email/两计数均耗尽/confirmed 无号码渠道) → ("", [])
suspect 仅为与调用处画像判定同签名,规则只看 confirmed(spec §4.2)。
"""
info = raw_info or {}
numbers = [str(v).strip() for k, v in (confirmed or {}).items()
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 "", []
# 方案B(13.6): 迂回话术轮换序号——连续破防时不至于复读同一句
_boundary_seq = 0
@@ -197,6 +251,25 @@ class ConversationManager:
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)
_, completion_marks = _contact_completion_note(
effective_raw, _confirmed_now, _suspect_now,
"zh" if _CJK_RE.search(content or "") else "en")
if completion_marks:
raw = dict(profile.raw_info or {})
for key in completion_marks:
raw[key] = True
profile.raw_info = raw
self.db.add(profile)
await self.db.commit()
logger.info("Contact completion note injected (conv=%s): %s",
conv.id, completion_marks)
# Enrich short user messages with context so AI doesn't lose track
history_for_ai = list(history)
enriched = self._enrich_short_message(content, profile.raw_info or {}, history)
@@ -209,10 +282,9 @@ class ConversationManager:
# F6(D6联动): 客户已留联系方式时,收尾话术不得再索要——路由 contact 版
# 渠道精确化(2026-10-08): confirmed 渠道集合传给再索要墙——
# 索要缺失渠道(如已有 phone 问邮箱)放行,是 Task 3 邮箱轻补的前提
_confirmed_contacts, _ = split_validate(
normalize_contacts(effective_raw.get("联系方式")))
has_contact = bool(_confirmed_contacts)
_confirmed_channels = set(_confirmed_contacts)
# (confirmed/suspect 已在 Task 3 写回处算好,此处复用)
has_contact = bool(_confirmed_now)
_confirmed_channels = set(_confirmed_now)
yield {"type": "stream_start"}
emitted = ""
@@ -648,13 +720,18 @@ class ConversationManager:
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in confirmed.items())
# 逐字话术直接引用 NO_CONTACT_ALREADY 常量(Task 5 已 import)——
# 与零号码墙替换句同源,三处"经理主动联系"话术不漂移
parts.append(f"⚠️ 客户已提供联系方式({shown})。绝对禁止再索要任何联系方式!"
# Task 3 防自锁改写(spec 4.1-6): 原"绝对禁止再索要任何联系方式!"
# 会抑制缺失渠道轻补——只禁已提供过的渠道,缺失渠道放行一次
parts.append(f"⚠️ 客户已提供联系方式({shown})。绝对禁止再索要客户已提供过"
f"的联系方式;缺失渠道仅可按轻补指引提一次。"
f"需要回应时可直接逐字使用:\"{NO_CONTACT_ALREADY}\"(中文同理)。"
f"确认收到后继续解答客户问题即可,不必急着结束对话。")
if boundary_note:
parts.append(boundary_note)
# Task 3 防自锁改写: 尾部补缺失渠道豁免(与上方改写同因)
parts.append("客户已留过联系方式——边界请求直接告知'客户经理会把准确报价/"
"安排发到你 WhatsApp'即可,禁止再索要联系方式。")
"安排发到你 WhatsApp'即可,禁止再索要联系方式"
"(缺失渠道可按轻补指引提一次)。")
elif suspect:
shown = ", ".join(
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in suspect.items())
@@ -666,6 +743,15 @@ class ConversationManager:
if boundary_note and not confirmed and not suspect:
parts.append(boundary_note) # 未留资:边界提醒引导留资(职能边界话术自带)
# Task 3(spec §4.2/§4.3b): 缺失渠道一次性轻补——cc 优先、邮箱次之,串行
# 注入。计数键(_marks)此处弃用: 本函数是 staticmethod 收内存副本,写回由
# stream 调用方以同参调用纯函数取回并落库(纯函数保证两处判定不漂移)。
completion_note, _marks = _contact_completion_note(
raw_info, confirmed, suspect,
lang="zh" if _CJK_RE.search(latest_msg or "") else "en")
if completion_note:
parts.append(completion_note)
# 15.2: 索要我方联系方式提醒注入(镜像 boundary_note 的挂载位)。
# 互留提示仅限 web 来源且客户未留资(confirmed/suspect 都算"已留");
# App 的反向留资话术本身就是主话术,不再叠加