2026-10-08 10:56:59 +08:00
# 联系方式双要 + 国家代码完整化 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** AI 索要联系方式升级为"WhatsApp(带国家代码)+ 邮箱分步双要",同时再索要墙精确化为按渠道放行缺失渠道的补充索要。
**Architecture:** 三层闭环——system.md 话术教行为;conversation.py 状态感知提醒(一次性计数 `_cc_asked` /`_email_asked` ,号码闭环优先于邮箱);boundary_filter.asks_contact 渠道精确化。推送端补 `_` 前缀过滤防内部状态泄漏。
**Tech Stack:** 既有栈(FastAPI/pytest, LLM 全 mock) + 生产真实验收
**设计文档:** `docs/superpowers/specs/2026-10-08-dual-contact-and-country-code-design.md` (本计划的唯一依据,含全部决策)
**约定:** 每任务 pytest 全绿才 commit;中文进文件用 Read/Edit 工具(环境坑见 CLAUDE.md)。
---
### Task 1: 再索要墙渠道精确化(TDD)
**Files:**
- Modify: `app/services/boundary_filter.py:276-278` ( asks_contact)、`app/services/conversation.py:210` (拆 confirmed)、`:239/:258` (两处调用传参)
- Test: `tests/test_boundary_filter.py`
- [ ] **Step 1: 写失败测试** (追加到 test_boundary_filter.py,先看现有 REASK 测试的导入风格)
```python
def test_reask_wall_channel_precision ():
"""渠道精确化(2026-10-08 spec §4.4): 索要已在手的渠道才丢,
索要缺失渠道放行;泛渠道词只要手上有任何号码渠道即丢(宁丢勿放)。"""
wa = { "whatsapp" } # 已有 WhatsApp
em = { "email" } # 只有邮箱
# 精确渠道: 已在手的丢
assert asks_contact ( "What's your WhatsApp number?" , wa ) is True
assert asks_contact ( "留个邮箱吧" , em ) is True
# 精确渠道: 缺失的放行(双要核心场景)
assert asks_contact ( "And an email address would help too" , wa ) is False
assert asks_contact ( "你WhatsApp多少?" , em ) is False
# 泛渠道词: 手上有任何号码渠道(whatsapp/phone/wechat)即丢
assert asks_contact ( "What's your number?" , wa ) is True
assert asks_contact ( "留个电话呗" , wa ) is True
# 泛渠道词: 只有邮箱时放行
assert asks_contact ( "What's your number?" , em ) is False
# 精确+泛同句共存: 任一命中判丢
assert asks_contact ( "leave your number or email" , wa ) is True
# 无参调用: 原行为(命中即丢)不变
assert asks_contact ( "What's your WhatsApp number?" ) is True
assert asks_contact ( "hello there" ) is False
```
- [ ] **Step 2: 跑红** `python -m pytest tests/test_boundary_filter.py::test_reask_wall_channel_precision -v` → TypeError( asks_contact 不收第二参)
2026-10-08 11:08:03 +08:00
- [ ] **Step 3: 实现** 。boundary_filter.py 把 asks_contact 替换为(保持 REASK_PATTERNS 不动,新增渠道映射层)。
⚠️ 分支顺序是评审修正版(plan review Issue 1:初版顺序会让 Step 1 的两条断言必挂——"泛词+仅邮箱"和"精确+泛同句"两个分支被兜底吞掉)。判定顺序固定:①在手精确词→丢;②泛词+有号码渠道→丢;③命中但句中无任何渠道词→保守丢;④其余→放行:
2026-10-08 10:56:59 +08:00
```python
# 渠道精确化(2026-10-08 spec §4.4): REASK 模式命中后按渠道词分辨——
# 索要已在手的渠道才丢,索要缺失渠道放行(双要补邮箱场景);泛渠道词
2026-10-08 11:08:03 +08:00
# 只要手上有任何号码渠道即丢(宁丢勿放);精确+泛同句共存时按①②自然
# 实现任一命中判丢。中文泛词不用 \b(CJK 无词边界,"电话"前 \b 永不成立)。
_PRECISE_CHANNEL_RE = re . compile ( r "whatsapp|e-?mail|邮箱|微信|wechat" , re . I )
_GENERIC_CONTACT_RE = re . compile ( r "\b(?:number|phone|contact)\b|电话|联系方式|号码" , re . I )
def _precise_channels_in ( s : str ) -> set :
out = set ()
for m in _PRECISE_CHANNEL_RE . finditer ( s ):
w = m . group ( 0 ) . lower ()
out . add ( "email" if w in ( "e-mail" , "email" , "邮箱" )
else "wechat" if w in ( "微信" , "wechat" ) else "whatsapp" )
return out
2026-10-08 10:56:59 +08:00
def asks_contact ( sentence : str , confirmed_channels : set | None = None ) -> bool :
"""输出句是否在索要联系方式(已留资时用于丢弃)。
confirmed_channels=None → 原行为(命中即丢,向后兼容);
2026-10-08 11:08:03 +08:00
传入已确认渠道集合 → 渠道精确化(①②③④判定,见上注释)。"""
2026-10-08 10:56:59 +08:00
s = sentence or ""
2026-10-08 11:08:03 +08:00
hit = any ( p . search ( s ) for p in REASK_PATTERNS )
if confirmed_channels is None or not hit :
return hit
has_number = bool ( confirmed_channels & { "whatsapp" , "phone" , "wechat" })
precise = _precise_channels_in ( s )
has_generic = bool ( _GENERIC_CONTACT_RE . search ( s ))
if precise & confirmed_channels : # ① 句中有在手精确渠道词
return True
if has_number and has_generic : # ② 泛词 + 手上有号码渠道
return True
if not precise and not has_generic : # ③ 命中但无渠道词 → 保守丢
return True
return False # ④ 精确词全缺失/泛词但无号码 → 放行
2026-10-08 10:56:59 +08:00
```
- [ ] **Step 4: 跑绿** (该测试 + 现有全部 REASK 测试不回归)
- [ ] **Step 5: conversation.py 接线** 。`:210` 处把 `has_contact = bool(split_validate(...))` 的结果拆出 confirmed 渠道集合(先读该处上下文确认变量形态),供 `:239` 、`:258` 两个调用点用:
```python
_confirmed_contacts , _ = split_validate ( ... ) # 复用既有调用,不再只取 bool
has_contact = bool ( _confirmed_contacts )
_confirmed_channels = set ( _confirmed_contacts ) # {"whatsapp","phone","email","wechat"}
# 两处调用改为:
if has_contact and asks_contact ( sentence , _confirmed_channels ):
...
elif has_contact and asks_contact ( pending , _confirmed_channels ):
```
2026-10-08 11:08:03 +08:00
- [ ] **Step 5b: 更新两处被新语义打破的既有测试** ( plan review Issue 2):
`tests/test_lead_email_trigger.py` 的 `test_reask_wall_drops_ask_sentence_when_contact` 与
`test_reask_wall_all_dropped_falls_back_to_closing` (场景=客户留 phone、AI 问
"Got your WhatsApp?"、期望丢句)——新语义下 WhatsApp 是缺失渠道 → 放行。
改法:测试场景改为 AI 问**已在手的渠道**(如 "Got your phone number?")保持
丢句语义;同时各补一条"有 phone 在手问 WhatsApp → 放行并流出"的断言
(钉死 spec 未明说的语义决定:号码渠道间互视为可补全)。
2026-10-08 10:56:59 +08:00
- [ ] **Step 6: 全量绿 + commit**
```bash
python -m pytest -q && git add -A && git commit -m "feat: 再索要墙渠道精确化——索要缺失渠道放行(双要前置)
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
---
### Task 2: 推送 `_` 前缀过滤(TDD)
**Files:**
- Modify: `app/services/lead_push_service.py:89`
- Test: `tests/test_lead_push_service.py`
- [ ] **Step 1: 失败测试** (追加;参考同文件现有 _build_payload 相关测试的构造方式)
```python
def test_build_payload_filters_internal_underscore_keys ():
"""内部状态键(_cc_asked/_email_asked)不得泄进推送画像(spec §4.3)。"""
# 构造含 _ 前缀键的 profile,走 _build_payload,断言产物无 _ 键
# (具体 fixture 构造参考同文件现有 payload 测试;核心断言:)
# assert all(not str(k).startswith("_") for k in payload["profile"])
```
(实现者按现有测试风格补全 fixture;断言目标即注释所示。)
- [ ] **Step 2: 跑红**
- [ ] **Step 3: 实现** 。lead_push_service.py:89 改:
```python
# _ 前缀内部状态(一次性计数等)不进推送画像(spec §4.3——渲染侧
# 两处有过滤,推送端曾整体拷贝泄漏)
profile_out = { k : v for k , v in ( raw_info or {}) . items ()
if not str ( k ) . startswith ( "_" )}
```
- [ ] **Step 4: 全量绿 + commit**
```bash
python -m pytest -q && git add -A && git commit -m "fix: 推送画像过滤_前缀内部键(防CRM泄漏)
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
---
### Task 3: 状态提醒注入 + 一次性计数(TDD,核心任务)
**Files:**
- Modify: `app/services/conversation.py` ( _build_context_reminder 注入区 + stream 方法写回计数)
- Test: `tests/test_lead_email_trigger.py` (复用其 manager 流程设施做集成测试)
**设计要点(实现者必读):**
- 新增模块级函数 `_contact_completion_note(raw_info, confirmed, suspect) -> tuple[str, list[str]]`
—— 返回 (注入文案, 需落库的计数键列表);纯函数便于单测。
- 串行优先级(spec §4.3b):**号码闭环优先**——confirmed 号码均不以 `+` /`00`
开头且 `_cc_asked` 未标记 → 注入国家代码追问;仅当号码已带前缀**或**
`_cc_asked` 已标记,且 confirmed 含号码渠道不含 email 且 `_email_asked`
未标记 → 注入邮箱轻补。一次最多返回一条。
- 注入文案(逐字模板,中英双语随 latest_msg 语言选择——参考 `_lang_note_for`
的实现形态):
- cc 追问英文:`"Could you send the full number with the country code, like +966 5x xxx xxxx? Just so our manager can reach you directly."` (中文版:`"麻烦重发一下带国家代码的完整号码呗(比如 +966 5x xxx xxxx)?客户经理直接加你。"` )
- email 轻补英文:`"Got it! And an email address would help too — our manager will send the formal quotation PDF there."` (中文版:`"收到!邮箱也留一个呗?客户经理把正式报价单 PDF 发你邮箱。"` )
- 注入格式仿既有 contact_note: `⚠️` 前缀系统指令 + 逐字话术 + "仅此一次,客户不给就不再提"指引
- 计数写回:stream 方法在组装 reminder 前调 `_contact_completion_note` ,
reminder 尾部拼接文案;**流式开始前**把返回的计数键写进
`profile.raw_info` 并随本轮事务提交(staticmethod 收到的是内存副本,
写回必须在调用方——spec §4.3 实现约束)。
- [ ] **Step 1: 失败单测** (追加到 test_lead_email_trigger.py,参考其现有 _build_context_reminder 直调风格)
```python
def test_contact_completion_note_serial_priority ():
"""spec §4.3b: 裸号+无邮箱(主场景)只注入国家代码追问;号码闭环后才可能
注入邮箱轻补;各计数消耗后不再注入。"""
from app.services.conversation import _contact_completion_note
wa_bare = { "whatsapp" : "0501234567" } # 裸号(无+/00前缀)
wa_full = { "whatsapp" : "+966501234567" }
wa_email = { "whatsapp" : "+966501234567" , "email" : "a@b.com" }
# 1) 裸号+无邮箱 → cc追问,计数["_cc_asked"]
note , marks = _contact_completion_note ({ "联系方式" : wa_bare }, wa_bare , {})
assert "country code" in note and marks == [ "_cc_asked" ]
# 2) _cc_asked已消耗+仍裸号无邮箱 → 转邮箱轻补
note , marks = _contact_completion_note (
{ "联系方式" : wa_bare , "_cc_asked" : True }, wa_bare , {})
assert "email" in note and marks == [ "_email_asked" ]
# 3) 完整号+无邮箱 → 直接邮箱轻补
note , marks = _contact_completion_note ({ "联系方式" : wa_full }, wa_full , {})
assert "email" in note and marks == [ "_email_asked" ]
# 4) 双渠道齐 → 不注入
note , marks = _contact_completion_note ({ "联系方式" : wa_email }, wa_email , {})
assert note == "" and marks == []
# 5) email-only → 不注入(不硬要WhatsApp)
note , marks = _contact_completion_note (
{ "联系方式" : { "email" : "a@b.com" }}, { "email" : "a@b.com" }, {})
assert note == "" and marks == []
# 6) 计数均已消耗 → 不再注入(防死缠)
note , marks = _contact_completion_note (
{ "联系方式" : wa_bare , "_cc_asked" : True , "_email_asked" : True },
wa_bare , {})
assert note == "" and marks == []
```
- [ ] **Step 2: 跑红**
- [ ] **Step 3: 实现 `_contact_completion_note`** (纯函数:按上述串行优先级;文案含中英两版,语言随参数选——若直调测试传不了 latest_msg,函数签名加 `lang="en"` 参数,中文由调用方传)
- [ ] **Step 4: 跑绿单测**
- [ ] **Step 5: 接线 _build_context_reminder** (有画像路径 parts 拼接处追加 note;空画像早退路径不注入——没联系方式谈不上补全)+ stream 方法流式前写回计数键
2026-10-08 11:12:06 +08:00
- [ ] **Step 5b: 改写 conversation.py 的两处硬禁令提醒句** ( plan review Issue 3——防自锁不只 system.md 一处;复审补正:是两句不是一句):
- `:646` (索要我方联系方式确认路径)"绝对禁止再索要任何联系方式!" → 改为
"绝对禁止再索要客户**已提供过**的联系方式;缺失渠道仅可按轻补指引提一次。"
- `:651-652` ( confirmed + boundary_note 分支,**"已留资+当轮问价"最常见组合
就走这里**)"…禁止再索要联系方式。" → 尾部补"(缺失渠道可按轻补指引提一次)"。
⚠️ 断言因果(复审确认):test_lead_email_trigger.py `:475` 断言的
"禁止再索要联系方式"子串实际来自 `:651` ——只改 `:646` 时三处断言都不会挂
(空转),改了 `:651` 后 `:475/:881` 才需要同步更新(届时按现有清单执行)。
2026-10-08 11:08:03 +08:00
- [ ] **Step 6: 集成测试** (spec §6-6 计数持久化)——用同文件现有 manager 流程测试风格(CaptureAI 捕获注入 context)构造两轮对话:**第一轮正向断言**(context 含追问句原文——防"写了计数却没拼进 reminder"的接线失误)+ 第一轮后查 DB `raw_info` 含 `_cc_asked` 、第二轮 reminder 不再含追问句
2026-10-08 10:56:59 +08:00
- [ ] **Step 7: 全量绿 + commit**
```bash
python -m pytest -q && git add -A && git commit -m "feat: 国家代码/邮箱双要状态提醒+一次性计数(串行优先级)
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
---
### Task 4: system.md 话术升级(7 处,静态断言先行)
**Files:**
- Modify: `app/prompts/system.md` (先通读全文再动笔)
- Test: `tests/test_extractor_evaluator_prompts.py` 或新建(静态断言风格)
- [ ] **Step 1: 写失败静态断言**
```python
def test_system_md_dual_contact_wording ():
src = open ( "app/prompts/system.md" , encoding = "utf-8" ) . read ()
# 国家代码引导(spec §4.1-1)
assert "with the country code" in src
# 分步双要+不坚持(§4.1-2/3)
assert "formal quotation PDF" in src and "轻补" in src
# 追问话术(§4.1-4)
assert "send the full number with the country code" in src
# 好差示例新增2组(§4.1-5)
assert src . count ( "国家代码" ) >= 2
# 全局禁令改写(§4.1-6——防自锁)
assert "已在手的渠道" in src and "缺失渠道" in src
# 模板句枚举新增(§4.1-7)
assert "国家代码追问句" in src and "邮箱轻补句" in src
```
2026-10-08 11:08:03 +08:00
- [ ] **Step 2: 跑红** → **Step 3: 改 system.md 七处** (话术全文见 spec §4.1;⚠️ 第 6 处"已留资时不用任何索要句"改写是防自锁关键,逐字改为"已留资时不再索要**已在手的渠道**(改确认式);**缺失渠道**可按【联系方式引导策略】轻补一次")。
⚠️ **plan review Issue 3 补充——防自锁是四处不是一处** :除 L7 外,system.md
还有三处同款硬禁令必须同步改写(否则更显眼的老禁令抑制新行为):
2026-10-08 11:12:06 +08:00
- 红线节(约 :59)"绝不再索要联系方式"→ 补"(缺失渠道可轻补一次)"
2026-10-08 11:08:03 +08:00
- 系统提醒配合节(约 :201)同款表述同步
2026-10-08 11:12:06 +08:00
- 对话流程节(约 :166)"拿到联系方式后…别再提联系方式"→ 补"(缺失渠道轻补一次除外)"
- [ ] **Step 4: 跑绿 + 全量绿** (现有 system.md 文案断言若冲突同步更新)
2026-10-08 10:56:59 +08:00
- [ ] **Step 5: 模板句过墙断言** (spec §6-4,防自锁报警线):
```python
def test_new_templates_pass_reask_wall ():
"""新模板句逐字原文在所有渠道状态下均不命中 asks_contact(spec §6-4):
墙只拦意译索要,不得拦系统注入话术——若扩REASK模式或改措辞致命中,此断言报警。"""
from app.services.boundary_filter import asks_contact
cc_en = ( "Could you send the full number with the country code, "
"like +966 5x xxx xxxx? Just so our manager can reach you directly." )
em_en = ( "Got it! And an email address would help too — our manager "
"will send the formal quotation PDF there." )
for channels in ( None , set (), { "whatsapp" }, { "email" },
{ "whatsapp" , "phone" }, { "whatsapp" , "email" }):
for s in ( cc_en , em_en ):
assert not asks_contact ( s , channels ), ( s , channels )
```
- [ ] **Step 6: 全量绿 + commit**
```bash
python -m pytest -q && git add -A && git commit -m "feat: system.md双要话术+国家代码引导+全局禁令改写(防自锁)
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
---
### Task 5: 全量回归 + 生产部署 + 真实 LLM 验收
- [ ] **Step 1: 全量测试 + 全局残扫** (无意外残留)
```bash
python -m pytest -q
```
- [ ] **Step 2: 部署** (照 CLAUDE.md 发版流程:push → 服务器 pull + build + up → health 验证)
- [ ] **Step 3: 生产三场景验收** ( spec §6-8,用测试号):
① 给裸号 `0501234567` → AI 礼貌请其重发完整号(**仅一次**);
② 留 WhatsApp → AI 轻补邮箱一次;
③ 只留邮箱 → 不被纠缠要 WhatsApp
(验收后 admin 删除测试会话;检查推送日志无 `_` 前缀键泄漏)
- [ ] **Step 4: commit 验收记录** (如有文档更新)
---
## 回滚
任一任务出问题:`git revert <commit>` 重发版(发版流程同上);话术类改动可单独回滚 Task 4 的 commit。