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

6.8 KiB
Raw Blame History

设计: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 为主;每步一次为限