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

9.9 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 /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 为主;每步一次为限