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 6568b35..cf95579 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 @@ -26,23 +26,37 @@ | CC 强制墙骨架 | conversation.py cc_wall 缓冲/替换/Important-2 closing | ✅ 复用(换文案) | | 出站管道 | email_service + lead_push._build_payload | ✅ 挂补全点 | -## 3. 状态机(核心) +## 3. 状态机(核心,含审查修订) ``` -裸号在场且未处理 - → [_cc_pend] ①国家确认轮:"Are you in {英文国名}?"(墙保证必出) - ├─ Yes(确定性判定)→ 生成补全号,存 _cc_full +裸号在场且未处理(无 _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_done 或超限] - ├─ No(猜错)→ 请给带码完整号(一次)→ 得到则 [_cc_done] - └─ 无法判定(长消息/不理睬)→ 下轮再给一次确认机会(计数) - → 超限(确认机会共 2 次)→ 请完整号一次 → 仍无 → 裸号照发 + │ ├─ Yes → [终态 _cc_done] 客户亲口确认 + │ └─ No / 无法判定(一次重试后仍无)→ 请给完整号一次[_cc_asked_full] + │ → 给了(带前缀)→ [终态 _cc_done,用客户号无标注] + ├─ No(猜错)→ 请给带码完整号一次[_cc_asked_full] → 得到则 [终态 _cc_done] + └─ 无法判定 → ①重试一次 → 仍无 → 请完整号一次 → 仍无 → [超限终态 _cc_done] + 旁路(审查补充):流中任意轮 confirmed 号码已带 +/00 前缀(客户主动给完整号) + → 直接 [终态 _cc_done](用客户号,无标注),①②全退 ``` -状态键(raw_info,`_` 前缀族):`_cc_pend`(含猜的 ISO code)、`_cc_full` -(补全号字符串)、`_cc_done`(完结标记)、`_cc_tries`(已用确认机会数)。 +**终态语义(审查修订)**:`_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. 组件设计 @@ -70,24 +84,38 @@ - ①国家确认轮:reminder 注入逐字句(英文 "Are you in {EN}?" / 中文"你是在 {中文国名}吗?"),CC 墙检测词从 "country code" 改为 **本轮注入的英文国名 - 在场**(或中文国名),不在则替换——缓冲机制不变 + 在场**(中文轮检测中文国名),不在则替换——缓冲机制不变 - ②号码确认轮:reminder 注入逐字句含补全号;**墙白名单**:`no_contact_value` - 新增参数 `allow_number: str | None`——数字串匹配(digits_only 归一后比对) - 该精确号码则放行,其余照拦(安全边界不扩大:只放行系统注入的那一个号) + 新增参数 `allow_number: str | None`——**对句中每个 ≥10 位数字匹配段逐一 + digits_only 归一比对**(非整句 substring,防"允许号+另一串数字"同句混放), + 全部命中白名单才放行该段,其余照拦(安全边界:只放行系统注入的那一个号) - 确认句墙同款缓冲:检测补全号在场,不在则替换 -### 4.4 出站补全(email_service + lead_push_service) +### 4.4 出站补全与时序(审查修订——核心架构决策) -- 消费点:渲染联系方式处(邮件 HTML/主题、push 的 contact.phone/whatsapp) -- 规则:`_cc_done` 且 `_cc_full` 在场 → 用 `_cc_full` 替换裸号展示,**带标注** - (如 `+966501234567(原号 0501234567,据 IP+客户确认为沙特,已补国家码)`) -- ⚠️ 推送侧交互:`_build_payload` 现过滤 `_` 前缀键——补全消费必须在过滤 - **之前**(读 raw_info → 生成补全 contact → 过滤画像) +**时序冲突与决策**:裸号 confirmed 当轮即触发 D1 首封 lead 邮件(零阻塞底线), +而①②确认流要到 N+1 轮后才完结。决策:**首封不延迟(裸号照发),确认完结 +(_cc_done 且有 _cc_full)时补发一封更正邮件**(复用现有 correction 通道, +占每会话 3 封额度)——客户经理先拿裸号线索(及时性不破),随后收带补全号 ++标注的更正(信息升级)。 + +- **邮件**:更正邮件渲染 `_cc_full` 补全号 + 标注(`+966501234567(原号 + 0501234567,据 IP+客户确认为沙特,已补国家码)`) +- **推送(UniService,当前关闭)**:contact.phone/whatsapp 给**干净补全号** + (E.164,无标注——对端契约要求"带国际区号且有效",中文标注串会被整条拒收, + 见 docs/AI_INQUIRY_PUSH_API.md);标注写 profile 的正常键(如 + `号码补全说明`,随画像进 CRM);推送时机=确认完结后的下一轮自然触发(或 + correction 推送,与邮件对齐,v1 只做自然轮) +- ⚠️ 消费顺序:`_build_payload` 先读 raw_info 生成补全 contact → 再过滤 `_` 键 - DB 不改客户原文;聊天里仅②确认轮展示补全号(白名单放行) -### 4.5 REST 路由 XFF 前置修复 +### 4.5 IP 入口修复(审查修订——位置改 /session) -chat_rest.py:107 补 WS 同款 XFF 第一段逻辑(app 源客户 IP 真实化) +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. 防死缠与兜底链