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

156 lines
9.9 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 | /session 的 client_info 来自请求体(自报);:107 仅喂限流 | ⚠️ /session 注入 Request 合并 XFF(§4.5) |
| IP→国家 | ip_service.resolve_country(中文/ISO 混合,另有 AU 重复键 bug) | ⚠️ 新增 resolve_iso 直返 ISO + 修 bug(§4.5) |
| 国家→电话前缀 | 无 | ❌ 新建映射表 |
| CC 强制墙骨架 | conversation.py cc_wall 缓冲/替换/Important-2 closing | ✅ 复用(换文案) |
| 出站管道 | email_service + lead_push._build_payload | ✅ 挂补全点 |
## 3. 状态机(核心,含审查修订)
```
裸号在场且未处理(无 _cc_done)
→ [_cc_pend=ISO] ①国家确认轮:"Are you in {英文国名}?"(墙保证必出)
├─ Yes → 生成补全号(4.1),存 _cc_full
│ → [_cc_conf] ②号码确认轮:"…reach you at {补全号} — is that right?"
│ (墙白名单放行该精确号码;确认句墙保证必出)
│ ├─ Yes → [终态 _cc_done] 客户亲口确认
│ └─ No / 无法判定(②仅此一次,不重试)→ 请给完整号一次[_cc_asked_full]
│ → 给了(带前缀)→ [终态 _cc_done,用客户号无标注]
├─ No(猜错)→ 请给带码完整号一次[_cc_asked_full] → 得到则 [终态 _cc_done]
└─ 无法判定 → ①重试一次 → 仍无 → 请完整号一次 → 仍无 → [超限终态 _cc_done]
旁路(审查补充):流中任意轮 confirmed 号码已带 +/00 前缀(客户主动给完整号)
→ 直接 [终态 _cc_done](用客户号,无标注),①②全退
```
**终态语义(审查修订)**:`_cc_done` 是唯一终态键(值为 True);区分两种完结:
- 有 `_cc_full`(或客户号带前缀)→ 出站用补全/完整号
- 无 `_cc_full`(超限路径)→ 出站用裸号(现状底线)
**状态键族**:`_cc_pend`(ISO code)、`_cc_full`(补全号)、`_cc_conf`(②进行中)、
`_cc_asked_full`(请完整号已用一次)、`_cc_tries`(①确认机会计数,上限 2)、
`_cc_done`(终态)。写回机制沿用现有(值可为字符串/整数,非仅 True)。
**预算口径(审查修订)**:①确认机会 2 次(_cc_tries);②确认 1 次,不判定或
No 后不再重复②,直接进"请完整号一次";"请完整号"全会话共 1 次。
**存量迁移(审查补充)**:升级部署时存量 `_cc_asked=True` 且号码仍裸的会话,
新状态机视其为**超限终态**(等价 `_cc_done`,无 `_cc_full`)——不违背已向客户
承诺的"仅此一次",直接进入邮箱轻补串行。
## 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`——**对句中每个 ≥10 位数字匹配段逐一
digits_only 归一比对**(非整句 substring,防"允许号+另一串数字"同句混放),
全部命中白名单才放行该段,其余照拦(安全边界:只放行系统注入的那一个号)
- 确认句墙同款缓冲:检测补全号在场,不在则替换
### 4.4 出站补全与时序(审查修订——核心架构决策)
**时序冲突与决策**:裸号 confirmed 当轮即触发 D1 首封 lead 邮件(零阻塞底线),
而①②确认流要到 N+1 轮后才完结。决策:**首封不延迟(裸号照发),确认完结
(_cc_done 且有 _cc_full)时补发一封更正邮件**(复用现有 correction 通道,
占每会话 3 封额度)——客户经理先拿裸号线索(及时性不破),随后收带补全号
+标注的更正(信息升级)。
- **邮件**:更正邮件渲染 `_cc_full` 补全号 + 标注(`+966501234567(原号
0501234567,据 IP+客户确认为沙特,已补国家码)`)——**标注由 `_cc_full`+
当前裸号在出站渲染时派生,不落 raw_info 正常键**(正常键会进三处 LLM 输入
路径被 GLM 当客户信息看见——泄漏前科教训,钉死为零泄漏面)
- **更正邮件触发机制(复审落笔说明)**:现有 correction 触发条件是 canon 变化
(Yes→Yes 路径 canon 不变、永不自然触发)——实现必须以"本轮 `_cc_done` 与
`_cc_full` **同时新出现**"为强制触发事件独立插入;额度耗尽
(correction_count≥3)时跳过;`ev.contact_info` 快照同步更新为补全号
- **推送(UniService,当前关闭)**:contact.phone/whatsapp 给**干净补全号**
(E.164,无标注——对端契约要求"带国际区号且有效",中文标注串会被整条拒收);
**v1 明确不做** correction 推送(邮件主通道已完整;推送启用后靠自然轮/
届时再补)
- ⚠️ 消费顺序:`_build_payload` 先读 raw_info 生成补全 contact → 再过滤 `_` 键
- DB 不改客户原文;聊天里仅②确认轮展示补全号(白名单放行)
### 4.5 IP 入口修复(审查修订——位置改 /session)
REST 的 client_info 来自 `/session` 请求体(客户端自报),chat_rest.py:107 的
IP 只喂限流——修不到特性数据流。修复:`/session` 路由注入 `Request`,XFF
第一段合并进 conv.client_info(与 WS 路由同款逻辑);另修 ip_service.py 既有
bug:`"AU": "加拿大"` 重复键吞掉了加拿大(应为 CA),并新增
`resolve_iso(ip) -> str`(iptocc 直返 ISO,跳过中文表反查的脆弱性)。
## 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. (复审补项)存量 `_cc_asked` 等价终态不重复问;旁路转移(流中给完整号→
①②全退);更正邮件补发时机(_cc_done+_cc_full 新出现触发)与额度耗尽
跳过;推送 contact 为纯 E.164 断言
## 7. 风险
| 风险 | 等级 | 缓解 |
|---|---|---|
| IP 误判(VPN/出口) | 中 | ①确认式反问本身就是纠错机制;No→要完整号 |
| 长消息误判 yes/no | 中 | 短消息守卫 + 兜底链 + 出站标注透明 |
| 前缀表覆盖不足 | 低 | 未覆盖走旧兜底;表可持续补 |
| 墙白名单被滥用扩大 | 低 | 精确串匹配参数化,评审断言锁死"只放行注入号" |
| 对话轮次变多 | 低 | 全程 yes/no 为主;每步一次为限 |