Files
ai-kefu-jc/docs/superpowers/specs/2026-10-08-cc-ip-country-confirm-design.md
T

177 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计:号码区号补全(IP 推测 + 回显确认)— 终版 v2
- 日期:2026-10-08(v2 融合版:两轮独立审查 + 上游同日设计对照吸收 + 需求方三处优化)
- 前置:2026-10-08-dual-contact-and-country-code-design.md 及墙层增补——本设计
**替换**其中"请客户重发完整号"路径
- 对照:上游汽车项目同日设计 `D:\refactor\ai-kefu\docs\superpowers\specs\
2026-10-08-whatsapp-cc-completion-design.md`(吸收其 6 点,见 §8)
## 1. 目标
客户号码缺国家区号时:按 IP 推测 → 补全为 E.164 → **回显给客户确认**(顺带
暴露抄错的数字)→ 首封线索邮件即带补全号(零延迟、可拨、标注透明)。杜绝
发出拨不通的本地格式号码,同时不阻塞线索交付。
## 2. 架构(确定性优先,LLM 只做它擅长的)
| 职责 | 归属 | 理由 |
|---|---|---|
| 判定(有无码/建议国)、E.164 补全、状态机、邮件渲染 | **Python 确定性**(新模块 `cc_completion` + phonenumberslite) | 状态不可能被幻觉;可离线确定性测试;邮件触发不押在 LLM |
| 客户答语理解(肯定/否定/国名) | **extractor LLM**(schema 加 `cc_reply` 可选字段,单轮无状态分类) | "yeah that's right"/"naur"/"我在迪拜"正则极脆弱,恰是 LLM 强项;extractor 本每轮跑,零额外调用。**双门控**:extractor.md 声明"仅当客户在回应号码确认时填写 cc_reply";Python 侧仅在 `_cc` 等待态(未 confirmed 且 attempts 未耗尽)时消费——防止对无关问题的 yes 被误当确认;**漏填兜底**(extractor 未识别出客户在答确认)→ 等同无回应,attempts 递增走 fallback |
| 话术生成与确认句必出 | 提醒注入 + **既有墙骨架**(白名单放行/缓冲替换——四轮生产实测过的机制) | GLM 对反惯性指令 0/8 服从的血泪教训,确认回显必须墙保 |
## 3. 判定树(`cc_completion.classify(number, ip_iso)`)
```
号码
├─ 以 +/00 开头 ──────────────────► HAS_CC(现有流程直走,不动)
├─ 以 IP 国码开头且去码后剩余合法 ──► HAS_CC_NAKED → 静默规范化(无对话
│ 动作;此分支无确认防线,依赖剩余段对该国 is_valid 严格校验——碰撞需
│ "以X码开头+剩余恰为X国合法号"多重巧合,错误率极低,拨错有客户反馈兜底)
├─ phonenumbers region=IP国 解析合法 ─► NEED_CC,建议=IP 国(高置信)
└─ 均不匹配(VPN/罕见格式)─────────► NEED_CC,建议=naive(去前导0+IP码,回显靠客户纠错)
IP 无效/未知 → NEED_CC 无建议国 → 问"方便发带区号完整号吗"(1 轮为限)
```
## 4. 状态机(每 canon 组独立,存 `raw_info["_cc"]` 结构化 dict)
```python
raw_info["_cc"] = {
"whatsapp": {"e164": "+254712345678", "intl": "+254 712 345 678",
"confirmed": false, "attempts": 0,
"source": "ip_proposed", # ip_proposed|naive|ip_confirm|
# prefix_normalized|customer_stated|no_ip|fallback
"canon": "0712345678"}}
```
- **canon 组**:phone 与 whatsapp 同号(canon 相同)共享一个确认流,确认一次
(attempts 以组为计数主体);**不同号时只有 whatsapp 组走确认流**(主联系
渠道,对话永远只确认一个号),phone 组一律 `ip_proposed` 静默补全+标注、
不确认(第五轮自查简化)——**换号(canon 变化)即重置该组状态机重走判定树**
(结构性解决"改号残留":旧补全永不跟随新号)
- **source 语义钉死**:`prefix_normalized` → **confirmed=true**(码是客户自己
写的,首封**无**"未经确认"标注);`customer_stated`=客户报国名重算后确认;
attempts 耗尽落定时 source 改写为 `fallback`(标注保留"未经客户确认");
标注按 `(confirmed, source)` 分派而非仅 confirmed
- attempts 只被 canon 变化重置;国名重述不重置(防"无限换国名");no_ip 路径
上限 1 轮(attempts 上限口径与邮箱轻补解锁条件=「confirmed=true 或
attempts 耗尽或 source∈{prefix_normalized,fallback}」统一)
- **实现调和记录(Task 2 审查)**:no_ip **不在**终态 source 集内——新鲜 no_ip
态 cc_due=True 问一次、attempts≥1 落定(spec 原字面把 no_ip 列入落定集会
导致"永远不问",与本节"问 1 轮"自相矛盾,实现按正确语义调和)
- **迟到信息决策(Task 2 审查;Task 3 实现调和)**:落定态收到迟到 **country
(客户主动报国)→ 受控升级口:重算 e164 并保留信息(Task 6 出站用重算号),但
attempts 已过 cap → fallback 落定、不再回显**(原"再回显一轮"与 cap 语义冲突,
实现按保守调和);**no_ip 耗尽态排除在报国口外**(静默终态);迟到 **affirm
不升级**;混合答语(affirm+country 同填)走 affirm 分支 country 忽略
(Task 2 transit 既有 tie-break)。Task 6 渲染需防御 **fallback-无-e164**
组合(迁移分支可产出,有机状态机不产生)
- **回显确认轮**(NEED_CC 且有建议国):AI 注入逐字句 "…reach you at
{intl} — is that right?"(中英双语);**墙白名单**放行该精确号码
(`no_contact_value(allow_number=...)`,逐匹配段 digits_only 比对,只放
系统注入的那一个号);**确认句墙**(缓冲机制)保证必出
- 答语(下一轮 extractor 的 `cc_reply`):
- `affirm=true` → `confirmed=true, source=ip_confirm` → 落定
- `affirm=false, country=X` → `NAME_TO_REGION[X]`(中英映射表)转 ISO 重算
e164 → 再回显(attempts+1)
- `affirm=false` 无 country → 请重发完整号(新号到达=canon 变化自然重置)
- 无回应/不可解析 → attempts 随轮递增,**每渠道上限 2 轮** → 落定
`confirmed=false`(保持现有 e164 建议,不无限纠缠)
- **内部状态卫生(三通道封堵,上游对照吸收)**:
1. extractor 入口 `$existing_info` 过滤 `_` 前缀键(我们已有,复核)
2. **extractor 出口:消费 `cc_reply` 后即剥离**(瞬态分类非事实,不残留)
3. **extractor 出口:防御性丢弃任何回传的 `_` 前缀键**(防 LLM 回显 `_cc`
覆盖 Python 管理的状态机——上游发现的洞)
- **存量迁移**:升级部署时存量 `_cc_asked=True` 且号码裸的会话 → 映射为
`_cc{confirmed:false, source:fallback}`(已问过,不再追问,直通邮箱轻补)
## 4b. 提示词层改写(终审补——不改则回显机制与红线自相矛盾)
v2 核心是让 AI **回显补全号**,而现行提示词全线教的是"不回显+请重发"——
不改提示词,GLM 会拒写号、确认句墙轮轮整段替换(重演锚点竞争教训):
1. **system.md 五处禁令例外句**(:59/:175/:223/:229/引导策略节)从"号码缺国家
代码时可请客户重发完整号一次"改为"号码缺国家代码时,按系统提醒**回显补全
后的完整号码**请客户确认(仅系统提供的号码,禁止自编)"
2. **好差示例**(裸号组)改为回显确认形态;新增差例:"AI 自己拼号码/回显客户
裸号原文"——只允许回显系统注入的补全号
3. **conversation.py CC 轮让位句**("回复中绝不写出号码数字")改为"回复中
绝不写出号码数字——**唯一例外是系统提供的补全号原文**(逐字)"
4. **确认句墙检测词钉死**:缓冲裁决标准 = **系统注入的 e164(digits_only
等值)在场**(辅锚 "is that right"/"对吗"),替换现有 "country code" 检测
5. **测试迁移补项**:tests/test_system_prompt_dual_contact.py 全部断言按新
语义更新(五处例外句/好差示例/模板句同源)
6. **拆除/改造清单**(206beae..cbf234d 刚上线机制的处置):`_CC_ASK_EN/_ZH`
追问句 → 改为回显确认模板(`_CC_ECHO_*`,与 reminder/墙三方同源);
`_CC_WALL_*` 检测词替换(见 4);`_cc_asked` 计数 → 存量映射(§4)+
新代码不再写入;`_contact_completion_note` 的 cc 分支重写(判定树挂接)
## 5. 邮件/推送时序(需求方定案:首封即发补全号)
| 时点 | 发什么 | 标注 |
|---|---|---|
| 首封(裸号 confirmed 当轮,**零延迟**) | 判定树同步算出的 **e164 补全号**(ip_proposed/naive/prefix_normalized) | `confirmed=false` 时尾注 **"(区号按 IP 推测,未经客户确认)"**;`no_ip` 时裸号 + **"(缺国家区号)"** |
| 客户确认(号码未变) | **不补发**(首封已可拨;后续渲染自动去标注) | 状态升级透明 |
| 客户纠正/换号 | 现有**改号更正通道**(canon 变化自然触发) | 现有机制零新逻辑 |
- 渲染点:email_service(正文联系方式行 + 更正/回头客路径,签名加参取 `_cc`;
**邮件主题行的号码也用 e164、不加标注**——主题拥挤);
lead_push 的 contact 给干净 e164(契约要求"带国际区号且有效"),标注走
profile 派生(出站时由 `_cc` 计算,不落正常键——零 LLM 泄漏面)
- `profile_out` 先消费 `_cc` 生成补全 → 再过滤 `_` 键(顺序钉死)
- **面板同步**:cc-pending 期间 `ev.contact_info` 带未确认标注(防"邮件带
补全、后台显裸号"的分裂)
- 邮箱轻补串行:号码确认流落定(confirmed 或 attempts 耗尽)后才注入
## 6. 管道接入点与 IP 入口(终审钉死)
- **两个钩子**(锚点错了"首封即发补全号"会静默失效;第五轮自查优化——
判定树挪轮前,全链同轮完成,客户少等一轮):
1. **轮前**:即时扫描发现裸号后**同步跑判定树**(纯计算毫秒级)建 `_cc`
→ 本轮 reminder 即注入回显确认 → **同轮回显**;与一次性计数写回同事务落库
2. **轮末**:消费 `cc_reply` + 状态更新(确认落定/重算/换号重置)——在
`_maybe_send_lead_email` **之前**(conversation.py 约 :500-526 区间),
首封补全号与推送派生依赖已建好的 `_cc`
- `/session` 路由注入 `Request`,XFF 第一段合并进 `conv.client_info`
(**服务端 XFF 优先于客户端自报 ip**——WS 现状是客户端覆盖服务端、可伪造,
一并修正);chat_rest.py:107 只喂限流修不到数据流
- `ip_service` 新增 `resolve_region_code(ip) -> ISO`(iptocc 直透);
**修既有 bug**:`"AU": "加拿大"` 重复键(应为 CA)
- 面板同步载体:`ev.contact_info` 值尾注(canon() 纯数字提取,尾注不影响
24h 去重/更正判定)
## 7. 依赖与测试
- `requirements.txt` + `phonenumberslite`(~3MB、零传递依赖、离线、Apache 2.0)
- 测试(离线确定性 + 集成):
1. 判定树矩阵(号码形态 × IP 国,含静默规范化分支)
2. 状态机全转移(确认/否定/国名/换号重置/2 轮无回应/国名重述不重置/
同 canon 组共享)
3. 三通道卫生(入口过滤/出口剥离 cc_reply/丢弃 `_` 回显)
4. 邮件时序(首封带补全+标注、确认后不补发、改号走更正、no_ip 兜底)
5. 墙(白名单精确放行:同号放/异号拦;确认句缓冲替换)
6. 存量迁移;推送 contact 纯 e164;`NAME_TO_REGION` 映射
7. 提示词静态断言(4b-5):五处例外句新语义/回显好差示例/让位句例外/
_CC_ECHO 三方同源
8. 真实 LLM 验收(cc_reply 分类质量与门控、回显确认全流程、误判对照——
无关问题的 yes 不得触发确认)
- 既有测试迁移:D1 断言"confirmed 即发"若 fixture 用本地格式号 → 更新为
"confirmed 且判定树已同步跑"(行为等价,渲染值变化)
## 8. 与上游设计的对照结论
吸收:判定树静默规范化分支、`_cc` 结构化+canon 重置、extractor 答语分类、
出口卫生三封堵、同 canon 组共享、面板同步闸。
不采纳:上游"待确认暂缓发信"(违背零阻塞——以首封补全号+标注替代,兼得
及时性与可拨性);上游墙豁免参数(我们已有四轮实测的墙骨架+白名单)。
## 9. 风险
| 风险 | 等级 | 缓解 |
|---|---|---|
| IP 误判(VPN)致首封补全号错 | 中 | 标注"未经客户确认"透明 + 客户否定走重算/改号更正双通道 |
| naive 补全结构错 | 低 | 回显确认天然暴露;confirmed=false 标注兜底 |
| cc_reply 分类错误 | 低 | 最坏=多问一轮或保持未确认标注;attempts 上限防纠缠 |
| NAME_TO_REGION 覆盖不足 | 低 | 映射失败=未捕获,attempts 递增走兜底 |
| 墙白名单滥用扩大 | 低 | 逐段精确比对 + 评审断言锁死 |