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

120 lines
6.8 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 猜国 + 两段式号码确认 — 设计文档
- 日期:2026-10-08
- 前置:2026-10-08-dual-contact-and-country-code-design.md 及其墙层增补
(2026-10-08-cc-enforcement-wall-design.md)——本设计替换其中的
"请客户重发完整号"路径为更低摩擦的确认式流程
- 状态:已与需求方逐点确认(含"补全号发给客户确认"与"猜错直接要完整号")
## 1. 需求
客户裸号(无国家代码)时:
1. 用客户 IP 猜国家,**反问客户**"是不是在这个国家"(确认式,非索取式)
2. 客户答 Yes → 服务端补全号码 → **把补全号发给客户确认**("reach you at
+966 5x…— is that right?")→ 客户再 Yes → 完结,邮件/推送用补全号
3. 客户答 No(IP 猜错/VPN)→ **直接请客户给带码完整号**(一步到位,一次为限)
4. 任何一步卡住/不理睬 → 裸号照发(现状底线,不阻塞线索交付)
## 2. 资产盘点(实测)
| 能力 | 现状 | 结论 |
|---|---|---|
| WS 路由真实 IP | chat.py:24-27 已处理 X-Forwarded-For 第一段 | ✅ 复用 |
| REST 路由真实 IP | chat_rest.py:107 仅 request.client.host(反代下=127.0.0.1) | ⚠️ 前置补 XFF(一行) |
| IP→国家 | ip_service.resolve_country(iptocc,返回中文/ISO) | ✅ 复用(内部用 ISO code) |
| 国家→电话前缀 | 无 | ❌ 新建映射表 |
| CC 强制墙骨架 | conversation.py cc_wall 缓冲/替换/Important-2 closing | ✅ 复用(换文案) |
| 出站管道 | email_service + lead_push._build_payload | ✅ 挂补全点 |
## 3. 状态机(核心)
```
裸号在场且未处理
→ [_cc_pend] ①国家确认轮:"Are you in {英文国名}?"(墙保证必出)
├─ Yes(确定性判定)→ 生成补全号,存 _cc_full
│ → [_cc_conf] ②号码确认轮:"…reach you at {补全号} — is that right?"
│ (墙白名单放行该精确号码;确认句墙保证必出)
│ ├─ Yes → [_cc_done] 完结:出站用补全号(带标注)
│ └─ No → 请给完整号(一次)→ [_cc_done 或超限]
├─ No(猜错)→ 请给带码完整号(一次)→ 得到则 [_cc_done]
└─ 无法判定(长消息/不理睬)→ 下轮再给一次确认机会(计数)
→ 超限(确认机会共 2 次)→ 请完整号一次 → 仍无 → 裸号照发
```
状态键(raw_info,`_` 前缀族):`_cc_pend`(含猜的 ISO code)、`_cc_full`
(补全号字符串)、`_cc_done`(完结标记)、`_cc_tries`(已用确认机会数)。
## 4. 组件设计
### 4.1 国家前缀表(新建 `app/services/phone_prefix.py`)
- `CC_PREFIX: dict[str, str]`:ISO alpha-2 → 拨号前缀,覆盖 COUNTRY_NAMES
全量 + 主要外贸国(SA+966/AE+971/KE+254/NG+234/GH+233/EG+20/…约 40 国)
- `CC_EN_NAME: dict[str, str]`:ISO → 英文国名(对话展示用,海外客户读英文)
- `complete_number(raw: str, iso: str) -> str | None`:补全 = 前缀 + 去前导 0
(E.164 trunk 规则,绝大多数国家适用;意大利等特例返回 None → 走兜底);
已带 +/00 前缀的号直接返回原样(幂等)
- 未覆盖 ISO / 补全失败 → None → 调用方走"请完整号"兜底
### 4.2 Yes/No 判定器(conversation.py 模块级纯函数)
- `_cc_reply_intent(text) -> "yes" | "no" | None`:确定性关键词
(yes/yeah/yep/sure/correct/right/exactly/对/是的/没错/嗯 ↔ no/nope/not
really/wrong/不是/不对/错了)
- **适用条件**(防误判,调用方守卫):仅 `_cc_pend`/`_cc_conf` 的**紧邻下一轮**、
且客户消息**短**(≤30 字符,纯应答形态);长消息/含产品内容 → None(视为未
应答,转兜底)
- 与项目 doctrine 一致:判定不交给 GLM
### 4.3 提醒与墙(复用骨架,换文案)
- ①国家确认轮:reminder 注入逐字句(英文 "Are you in {EN}?" / 中文"你是在
{中文国名}吗?"),CC 墙检测词从 "country code" 改为 **本轮注入的英文国名
在场**(或中文国名),不在则替换——缓冲机制不变
- ②号码确认轮:reminder 注入逐字句含补全号;**墙白名单**:`no_contact_value`
新增参数 `allow_number: str | None`——数字串匹配(digits_only 归一后比对)
该精确号码则放行,其余照拦(安全边界不扩大:只放行系统注入的那一个号)
- 确认句墙同款缓冲:检测补全号在场,不在则替换
### 4.4 出站补全(email_service + lead_push_service)
- 消费点:渲染联系方式处(邮件 HTML/主题、push 的 contact.phone/whatsapp)
- 规则:`_cc_done` 且 `_cc_full` 在场 → 用 `_cc_full` 替换裸号展示,**带标注**
(如 `+966501234567(原号 0501234567,据 IP+客户确认为沙特,已补国家码)`)
- ⚠️ 推送侧交互:`_build_payload` 现过滤 `_` 前缀键——补全消费必须在过滤
**之前**(读 raw_info → 生成补全 contact → 过滤画像)
- DB 不改客户原文;聊天里仅②确认轮展示补全号(白名单放行)
### 4.5 REST 路由 XFF 前置修复
chat_rest.py:107 补 WS 同款 XFF 第一段逻辑(app 源客户 IP 真实化)
## 5. 防死缠与兜底链
- 确认机会共 **2 次**(①轮后未应答可再给一次);"请完整号"**1 次**
- 超限/全程不理睬 → 裸号照常 confirmed 发邮件/推送(现状行为,零阻塞)
- IP 无效(本地/未知)/ISO 无前缀/补全失败 → 直接走"请完整号"(跳过①)
- 与邮箱轻补的串行:号码确认流全部完结(_cc_done 或超限)后才可能注入邮箱
轻补(沿用 §4.3b 串行规则,判定条件相应更新)
## 6. 测试策略
1. 前缀表单测:补全(含去 0/幂等/特例 None)/英文国名/覆盖表
2. 判定器单测:yes/no 词表×短长消息×中英
3. 状态机集成(FakeAI 流程):①Yes→②Yes 全链(补全号落库、白名单放行、
出站带标注);①No→请完整号→给号完结;长消息→再给一次→超限裸号发;
IP 无效→跳①直接要完整号
4. 墙测试:白名单精确放行(同号放/异号拦/无参数照拦);确认句替换
5. 出站测试:邮件/push 的补全与标注;`_` 键过滤与消费顺序
6. 真实 LLM 验收:全流程三路径 + 误判对照(长消息含 Yes 不触发)
## 7. 风险
| 风险 | 等级 | 缓解 |
|---|---|---|
| IP 误判(VPN/出口) | 中 | ①确认式反问本身就是纠错机制;No→要完整号 |
| 长消息误判 yes/no | 中 | 短消息守卫 + 兜底链 + 出站标注透明 |
| 前缀表覆盖不足 | 低 | 未覆盖走旧兜底;表可持续补 |
| 墙白名单被滥用扩大 | 低 | 精确串匹配参数化,评审断言锁死"只放行注入号" |
| 对话轮次变多 | 低 | 全程 yes/no 为主;每步一次为限 |