From 9d2e2b224dad5a7de255b7d3fc24de1485f22481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 8 Oct 2026 15:42:28 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=8C=BA=E5=8F=B7=E8=A1=A5=E5=85=A8spe?= =?UTF-8?q?c=20v2=E7=BB=88=E7=89=88=E2=80=94=E2=80=94=E8=9E=8D=E5=90=88pho?= =?UTF-8?q?nenumberslite=E5=88=A4=E5=AE=9A=E6=A0=91/=E9=A6=96=E5=B0=81?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E5=8F=B7/=E7=BB=93=E6=9E=84=E5=8C=96=5Fcc+ca?= =?UTF-8?q?non/extractor=E7=AD=94=E8=AF=AD=E5=88=86=E7=B1=BB/=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E5=8D=AB=E7=94=9F/=E4=B8=8A=E6=B8=B8=E5=AF=B9?= =?UTF-8?q?=E7=85=A7=E7=BB=93=E8=AE=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- ...2026-10-08-cc-ip-country-confirm-design.md | 224 ++++++++---------- 1 file changed, 95 insertions(+), 129 deletions(-) diff --git a/docs/superpowers/specs/2026-10-08-cc-ip-country-confirm-design.md b/docs/superpowers/specs/2026-10-08-cc-ip-country-confirm-design.md index ec097cc..8bfef7f 100644 --- a/docs/superpowers/specs/2026-10-08-cc-ip-country-confirm-design.md +++ b/docs/superpowers/specs/2026-10-08-cc-ip-country-confirm-design.md @@ -1,155 +1,121 @@ -# 设计:IP 猜国 + 两段式号码确认 — 设计文档 +# 设计:号码区号补全(IP 推测 + 回显确认)— 终版 v2 -- 日期:2026-10-08 -- 前置:2026-10-08-dual-contact-and-country-code-design.md 及其墙层增补 - (2026-10-08-cc-enforcement-wall-design.md)——本设计替换其中的 - "请客户重发完整号"路径为更低摩擦的确认式流程 -- 状态:已与需求方逐点确认(含"补全号发给客户确认"与"猜错直接要完整号") +- 日期: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. 需求 +## 1. 目标 -客户裸号(无国家代码)时: -1. 用客户 IP 猜国家,**反问客户**"是不是在这个国家"(确认式,非索取式) -2. 客户答 Yes → 服务端补全号码 → **把补全号发给客户确认**("reach you at - +966 5x…— is that right?")→ 客户再 Yes → 完结,邮件/推送用补全号 -3. 客户答 No(IP 猜错/VPN)→ **直接请客户给带码完整号**(一步到位,一次为限) -4. 任何一步卡住/不理睬 → 裸号照发(现状底线,不阻塞线索交付) +客户号码缺国家区号时:按 IP 推测 → 补全为 E.164 → **回显给客户确认**(顺带 +暴露抄错的数字)→ 首封线索邮件即带补全号(零延迟、可拨、标注透明)。杜绝 +发出拨不通的本地格式号码,同时不阻塞线索交付。 -## 2. 资产盘点(实测) +## 2. 架构(确定性优先,LLM 只做它擅长的) -| 能力 | 现状 | 结论 | +| 职责 | 归属 | 理由 | |---|---|---| -| 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 | ✅ 挂补全点 | +| 判定(有无码/建议国)、E.164 补全、状态机、邮件渲染 | **Python 确定性**(新模块 `cc_completion` + phonenumberslite) | 状态不可能被幻觉;可离线确定性测试;邮件触发不押在 LLM | +| 客户答语理解(肯定/否定/国名) | **extractor LLM**(schema 加 `cc_reply` 可选字段,单轮无状态分类) | "yeah that's right"/"naur"/"我在迪拜"正则极脆弱,恰是 LLM 强项;extractor 本每轮跑,零额外调用 | +| 话术生成与确认句必出 | 提醒注入 + **既有墙骨架**(白名单放行/缓冲替换——四轮生产实测过的机制) | GLM 对反惯性指令 0/8 服从的血泪教训,确认回显必须墙保 | -## 3. 状态机(核心,含审查修订) +## 3. 判定树(`cc_completion.classify(number, ip_iso)`) ``` -裸号在场且未处理(无 _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](用客户号,无标注),①②全退 +号码 + ├─ 以 +/00 开头 ──────────────────► HAS_CC(现有流程直走,不动) + ├─ 以 IP 国码开头且去码后剩余合法 ──► HAS_CC_NAKED → 静默规范化(无对话动作) + ├─ phonenumbers region=IP国 解析合法 ─► NEED_CC,建议=IP 国(高置信) + └─ 均不匹配(VPN/罕见格式)─────────► NEED_CC,建议=naive(去前导0+IP码,回显靠客户纠错) + IP 无效/未知 → NEED_CC 无建议国 → 问"方便发带区号完整号吗"(1 轮为限) ``` -**终态语义(审查修订)**:`_cc_done` 是唯一终态键(值为 True);区分两种完结: -- 有 `_cc_full`(或客户号带前缀)→ 出站用补全/完整号 -- 无 `_cc_full`(超限路径)→ 出站用裸号(现状底线) +## 4. 状态机(每 canon 组独立,存 `raw_info["_cc"]` 结构化 dict) -**状态键族**:`_cc_pend`(ISO code)、`_cc_full`(补全号)、`_cc_conf`(②进行中)、 -`_cc_asked_full`(请完整号已用一次)、`_cc_tries`(①确认机会计数,上限 2)、 -`_cc_done`(终态)。写回机制沿用现有(值可为字符串/整数,非仅 True)。 +```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"}} +``` -**预算口径(审查修订)**:①确认机会 2 次(_cc_tries);②确认 1 次,不判定或 -No 后不再重复②,直接进"请完整号一次";"请完整号"全会话共 1 次。 +- **canon 组**:phone 与 whatsapp 同号(canon 相同)共享一个确认流,确认一次; + 不同号各自独立状态——**换号(canon 变化)即重置该组状态机重走判定树** + (结构性解决"改号残留":旧补全永不跟随新号) +- attempts 只被 canon 变化重置;国名重述不重置(防"无限换国名") +- **回显确认轮**(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}`(已问过,不再追问,直通邮箱轻补) -**存量迁移(审查补充)**:升级部署时存量 `_cc_asked=True` 且号码仍裸的会话, -新状态机视其为**超限终态**(等价 `_cc_done`,无 `_cc_full`)——不违背已向客户 -承诺的"仅此一次",直接进入邮箱轻补串行。 +## 5. 邮件/推送时序(需求方定案:首封即发补全号) -## 4. 组件设计 +| 时点 | 发什么 | 标注 | +|---|---|---| +| 首封(裸号 confirmed 当轮,**零延迟**) | 判定树同步算出的 **e164 补全号**(ip_proposed/naive/prefix_normalized) | `confirmed=false` 时尾注 **"(区号按 IP 推测,未经客户确认)"**;`no_ip` 时裸号 + **"(缺国家区号)"** | +| 客户确认(号码未变) | **不补发**(首封已可拨;后续渲染自动去标注) | 状态升级透明 | +| 客户纠正/换号 | 现有**改号更正通道**(canon 变化自然触发) | 现有机制零新逻辑 | -### 4.1 国家前缀表(新建 `app/services/phone_prefix.py`) +- 渲染点:email_service(正文联系方式行 + 更正/回头客路径,签名加参取 `_cc`); + lead_push 的 contact 给干净 e164(契约要求"带国际区号且有效"),标注走 + profile 派生(出站时由 `_cc` 计算,不落正常键——零 LLM 泄漏面) +- `profile_out` 先消费 `_cc` 生成补全 → 再过滤 `_` 键(顺序钉死) +- **面板同步**:cc-pending 期间 `ev.contact_info` 带未确认标注(防"邮件带 + 补全、后台显裸号"的分裂) +- 邮箱轻补串行:号码确认流落定(confirmed 或 attempts 耗尽)后才注入 -- `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 → 调用方走"请完整号"兜底 +## 6. IP 入口与基础修复 -### 4.2 Yes/No 判定器(conversation.py 模块级纯函数) +- `/session` 路由注入 `Request`,XFF 第一段合并进 `conv.client_info`(WS 路由 + 已有同款;chat_rest.py:107 只喂限流修不到数据流) +- `ip_service` 新增 `resolve_region_code(ip) -> ISO`(iptocc 直透); + **修既有 bug**:`"AU": "加拿大"` 重复键(应为 CA) -- `_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 +## 7. 依赖与测试 -### 4.3 提醒与墙(复用骨架,换文案) +- `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. 真实 LLM 验收(cc_reply 分类质量、回显确认全流程、误判对照) +- 既有测试迁移:D1 断言"confirmed 即发"若 fixture 用本地格式号 → 更新为 + "confirmed 且判定树已同步跑"(行为等价,渲染值变化) -- ①国家确认轮:reminder 注入逐字句(英文 "Are you in {EN}?" / 中文"你是在 - {中文国名}吗?"),CC 墙检测词从 "country code" 改为 **本轮注入的英文国名 - 在场**(中文轮检测中文国名),不在则替换——缓冲机制不变 -- ②号码确认轮:reminder 注入逐字句含补全号;**墙白名单**:`no_contact_value` - 新增参数 `allow_number: str | None`——**对句中每个 ≥10 位数字匹配段逐一 - digits_only 归一比对**(非整句 substring,防"允许号+另一串数字"同句混放), - 全部命中白名单才放行该段,其余照拦(安全边界:只放行系统注入的那一个号) -- 确认句墙同款缓冲:检测补全号在场,不在则替换 +## 8. 与上游设计的对照结论 -### 4.4 出站补全与时序(审查修订——核心架构决策) +吸收:判定树静默规范化分支、`_cc` 结构化+canon 重置、extractor 答语分类、 +出口卫生三封堵、同 canon 组共享、面板同步闸。 +不采纳:上游"待确认暂缓发信"(违背零阻塞——以首封补全号+标注替代,兼得 +及时性与可拨性);上游墙豁免参数(我们已有四轮实测的墙骨架+白名单)。 -**时序冲突与决策**:裸号 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. 风险 +## 9. 风险 | 风险 | 等级 | 缓解 | |---|---|---| -| IP 误判(VPN/出口) | 中 | ①确认式反问本身就是纠错机制;No→要完整号 | -| 长消息误判 yes/no | 中 | 短消息守卫 + 兜底链 + 出站标注透明 | -| 前缀表覆盖不足 | 低 | 未覆盖走旧兜底;表可持续补 | -| 墙白名单被滥用扩大 | 低 | 精确串匹配参数化,评审断言锁死"只放行注入号" | -| 对话轮次变多 | 低 | 全程 yes/no 为主;每步一次为限 | +| IP 误判(VPN)致首封补全号错 | 中 | 标注"未经客户确认"透明 + 客户否定走重算/改号更正双通道 | +| naive 补全结构错 | 低 | 回显确认天然暴露;confirmed=false 标注兜底 | +| cc_reply 分类错误 | 低 | 最坏=多问一轮或保持未确认标注;attempts 上限防纠缠 | +| NAME_TO_REGION 覆盖不足 | 低 | 映射失败=未捕获,attempts 递增走兜底 | +| 墙白名单滥用扩大 | 低 | 逐段精确比对 + 评审断言锁死 |