diff --git a/docs/superpowers/plans/2026-10-08-cc-completion.md b/docs/superpowers/plans/2026-10-08-cc-completion.md new file mode 100644 index 0000000..acf1dfb --- /dev/null +++ b/docs/superpowers/plans/2026-10-08-cc-completion.md @@ -0,0 +1,164 @@ +# 号码区号补全(IP 推测 + 回显确认 + 首封补全号)实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 客户号码缺国家区号时,按 IP + phonenumberslite 判定补全,AI 回显补全号请客户确认,首封线索邮件即带补全号(零延迟、标注透明)。 + +**Architecture:** 确定性判定树与状态机(新模块 cc_completion + `_cc` 结构化状态,canon 组换号重置)+ extractor LLM 答语分类(cc_reply 双门控)+ 既有墙骨架改造(确认句墙检测 e164 在场、白名单放行注入号)+ 出站先消费后过滤。 + +**Tech Stack:** phonenumberslite(新增依赖)/ FastAPI / pytest(LLM mock) + +**Spec(唯一依据,六轮打磨):** `docs/superpowers/specs/2026-10-08-cc-ip-country-confirm-design.md` +**约定:** 每任务全量 pytest 绿才 commit(当前基线 256);中文一律 Read/Edit 工具写;行号可能漂移,动笔先读代码核实锚点。 + +--- + +### Task 1: 基础层——phonenumberslite + ip_service 扩展 + REST XFF + +**Files:** +- Modify: `requirements.txt`(+phonenumberslite)、`app/services/ip_service.py`、`app/api/routes/chat_rest.py`(/session 路由) +- Test: `tests/test_ip_service.py`(新建) + +- [ ] **Step 1: 依赖** `pip install phonenumberslite` + requirements.txt 加行 +- [ ] **Step 2: 失败测试**(新建 tests/test_ip_service.py): +```python +def test_resolve_region_code(): + from app.services.ip_service import resolve_region_code + assert resolve_region_code("104.28.41.169") in ("HK", "SG", None) # 公网IP返回ISO或None,绝不返回中文/"本地" + assert resolve_region_code("") is None + assert resolve_region_code("127.0.0.1") is None + +def test_country_names_ca_bug_fixed(): + from app.services import ip_service + assert ip_service.COUNTRY_NAMES.get("CA") == "加拿大" + assert ip_service.COUNTRY_NAMES.get("AU") == "澳大利亚" +``` +- [ ] **Step 3: 跑红** → **Step 4: 实现**:ip_service 加 `resolve_region_code(ip) -> str | None`(iptocc 的 country_code 直透,"本地/未知/--"归 None);修字典 `"AU": "加拿大"` 重复键(CA 缺失) +- [ ] **Step 5: REST XFF**:/session 路由注入 `Request`,XFF 第一段**服务端优先**合并进 client_info(WS 的 chat.py:60 客户端覆盖服务端可伪造——一并改为服务端优先,两处注释说明) +- [ ] **Step 6: 全量绿 + commit** `feat: 基础层——phonenumberslite/resolve_region_code/CA字典bug/REST会话XFF服务端优先` + +--- + +### Task 2: 核心模块 cc_completion(判定树 + 状态机,纯函数,TDD 重头) + +**Files:** +- Create: `app/services/cc_completion.py` +- Test: `tests/test_cc_completion.py`(新建) + +- [ ] **Step 1: 失败测试矩阵**(spec §3/§4 全转移,先写全再实现): + +```python +# 判定树矩阵(spec §3) +def test_classify_tree(): + from app.services.cc_completion import classify, HAS_CC, NEED_CC + assert classify("+966501234567", "SA")[0] == HAS_CC # a: +前缀 + assert classify("00966501234567", "SA")[0] == HAS_CC # a: 00前缀 + r = classify("966501234567", "SA") # b: IP码开头静默规范化 + assert r[0] == HAS_CC and r[1]["e164"] == "+966501234567" and r[1]["source"] == "prefix_normalized" and r[1]["confirmed"] is True + r = classify("0501234567", "SA") # c: IP国解析合法 + assert r[0] == NEED_CC and r[1]["source"] == "ip_proposed" and r[1]["e164"] == "+966501234567" + r = classify("0501234567", "KE") # d: 均不匹配→naive + assert r[0] == NEED_CC and r[1]["source"] == "naive" + r = classify("0501234567", None) # e: 无IP + assert r[0] == NEED_CC and r[1].get("suggest_ask_full") is True + +# 状态机(spec §4): canon重置/attempt递增/affirm/国名重算/耗尽fallback/漏填兜底 +def test_transit_full_lifecycle(): ... # 按spec §4逐转移实现 +def test_name_to_region(): ... # 中英国名→ISO(覆盖 COUNTRY_NAMES 全集+常见英文) +def test_format_intl(): ... # e164→分组展示 "+254 712 345 678" +``` + +- [ ] **Step 2: 跑红** → **Step 3: 实现 cc_completion.py**: + - `classify(number, ip_iso) -> (kind, cc_dict)`:四分支判定树(b 分支=剥 IP 国码前缀(`country_code_for_region`)+ 剩余段 `parse(num, region)+is_valid_number` 严格校验;c=`parse` IP 国合法;d=naive 去前导0+IP 码;e=无建议国) + - `NAME_TO_REGION`、`format_intl(e164)`(phonenumbers `format_number(International)`) + - `transit(cc_state, reply, new_canon)`:状态机纯函数(affirm→ip_confirm 落定;country 重算再回显 attempts+1;canon 变化→重置;attempts≥2→source=fallback 落定;无 reply→attempts+1) + - `cc_due(cc_state)`:等待态判定(reminder 消费条件) + - `cc_settled(cc_state)`:落定判定(邮箱轻补解锁条件,spec §4 统一口径) +- [ ] **Step 4: 跑绿矩阵** → **Step 5: 全量绿 + commit** `feat: cc_completion判定树+状态机纯函数` + +--- + +### Task 3: 管道接入(轮前判定/轮末消费/出口卫生/存量迁移) + +**Files:** +- Modify: `app/services/conversation.py`(轮前即时扫描后、轮末合并区、`_contact_completion_note` 改造)、`app/services/ai_service.py`(复核入口过滤) +- Test: `tests/test_lead_email_trigger.py` + +- [ ] **Step 1: 轮前钩子**(spec §6-1):即时扫描建 effective_raw 后**同步跑判定树**——裸号且无 `_cc`(或 canon 变化)→ 建 `_cc`(仅 whatsapp 组确认流;phone 组 ip_proposed 静默)→ 与一次性计数写回同事务落库。`_contact_completion_note` 的 cc 分支删除(其 email 分支保留),cc 提醒改由本钩子产出(Task 4 接) +- [ ] **Step 2: 轮末钩子**(spec §6-2):四层合并后、D1 邮件前——消费 `cc_reply`(仅 `cc_due` 等待态,双门控 Python 侧)→ `transit` 状态更新落库;**剥离 cc_reply**(瞬态);**丢弃 extractor 回传的任何 `_` 前缀键**(出口卫生 3) +- [ ] **Step 3: 存量迁移**(spec §4 尾):读到旧 `_cc_asked=True` 且号码裸 → 一次性改写为 `_cc{confirmed:false, source:fallback}`(不追问直通轻补) +- [ ] **Step 4: 集成测试**(FakeAI 流程):裸号轮当轮 `_cc` 已建(轮前钩子);cc_reply affirm→落定;canon 换号→重置重走;attempts 耗尽→fallback;出口无 `_` 残留/cc_reply 剥离;旧 `_cc_asked` 会话不重复问 +- [ ] **Step 5: 全量绿(既有 CC 相关断言按拆除清单同步迁移)+ commit** `feat: 判定树轮前接入+cc_reply消费+出口卫生+存量迁移` + +--- + +### Task 4: 提醒与墙改造(回显确认模板/确认句墙/白名单) + +**Files:** +- Modify: `app/services/conversation.py`(reminder 注入/墙区传参/让位句)、`app/services/boundary_filter.py`(no_contact_value 加 allow_number) +- Test: `tests/test_boundary_filter.py`、`tests/test_lead_email_trigger.py` + +- [ ] **Step 1: 失败测试**:白名单(同号放/异号拦/无参照拦/多号同句仅白名单号放);确认句墙(检测词=注入 e164 digits 等值在场,含辅锚 "is that right"/"对吗";不在则整段替换 `_CC_ECHO_*`) +- [ ] **Step 2: 实现**: + - `no_contact_value(sentence, allow_number=None)`:逐 `LONG_NUMBER_RE` 匹配段 digits_only 归一比对,**全部段**命中 allow_number 才放行该句(混入异号照拦) + - 新模板 `_CC_ECHO_EN/_ZH`("…reach you at {intl} — is that right?" 中英,**与 reminder/墙/断言三方同源**——沿用 `_CC_ASK_*` 拼装式结构改造) + - reminder:`cc_due` 时注入回显句(intl 由 Python 算好,不让 GLM 拼号);确认句墙(缓冲)裁决词从 "country code" 改为注入 e164 在场;conversation 墙区调用传 `allow_number=等待态的 e164` + - 让位句改写(4b-3):"回复中绝不写出号码数字——**唯一例外是系统提供的补全号原文(逐字)**" +- [ ] **Step 3: 跑绿 + 全量绿 + commit** `feat: 回显确认模板+确认句墙e164检测+白名单逐段放行` + +--- + +### Task 5: 提示词层(system.md/extractor.md 改写 + 拆除清单 + 断言迁移) + +**Files:** +- Modify: `app/prompts/system.md`、`app/prompts/extractor.md` +- Test: `tests/test_system_prompt_dual_contact.py`、`tests/test_extractor_evaluator_prompts.py` + +- [ ] **Step 1: 失败断言**(新语义):五处例外句含"回显补全后的完整号码";好差示例含回显组+自编号差例;extractor.md 含 cc_reply 字段说明与双门控声明 +- [ ] **Step 2: 改 system.md**(4b-1/2):五处禁令例外句(:59/:175/:223/:229/引导策略节,行号先 grep 核实)改"回显确认"语义;好差示例两改一增(裸号组改回显形态;增"AI 自编号码"差例) +- [ ] **Step 3: 改 extractor.md**(4b/§2 双门控):加 `cc_reply` 可选字段(`{"affirm": true}` / `{"affirm": false, "country": "..."}`)+ "仅当客户在回应号码确认时填写" +- [ ] **Step 4: 断言全量迁移**(test_system_prompt_dual_contact 四断言按新语义重写;同源断言锚 `_CC_ECHO_*` 正文) +- [ ] **Step 5: 全量绿 + commit** `feat: 提示词回显确认语义+extractor cc_reply字段+断言迁移` + +--- + +### Task 6: 出站渲染(首封补全号/标注/主题行/推送/面板) + +**Files:** +- Modify: `app/services/email_service.py`、`app/services/lead_push_service.py`、`app/services/conversation.py`(ev.contact_info 尾注 + 邮箱轻补条件) +- Test: `tests/test_email_rendering.py`、`tests/test_lead_push_service.py` + +- [ ] **Step 1: 失败测试**:首封渲染 e164+`(区号按 IP 推测,未经客户确认)`(confirmed=false 时);prefix_normalized **无**标注;主题行 e164 无标注;no_ip 裸号+`(缺国家区号)`;确认后(号码未变)**不补发**;换号走现有更正;push contact 纯 e164 + profile 含派生标注键;`_` 键过滤前消费 +- [ ] **Step 2: 实现**:email_service 渲染 `_cc`(正文/主题/更正/回头客,签名加参);lead_push `profile_out` 先消费 `_cc`(contact 用 e164、标注进 `号码补全说明` 派生键)再过滤 `_`;`ev.contact_info` 等待态尾注;`_contact_completion_note` 的 email 分支解锁条件改 `cc_settled` +- [ ] **Step 3: 跑绿 + 全量绿 + commit** `feat: 出站补全渲染——首封e164+标注/主题行/推送派生/面板尾注` + +--- + +### Task 7: 全量回归 + 部署 + 真实 LLM 验收 + +- [ ] **Step 1: 全量 pytest + 既有测试迁移清点**(D1"confirmed 即发"断言 fixture 本地格式号→补全渲染值更新;回显豁免旧断言残留清零;CLAUDE.md 测试基线数更新) +- [ ] **Step 2: 部署**(CLAUDE.md 发版流程;镜像含新依赖自动装) +- [ ] **Step 3: 生产验收**(真实 GLM,测试号+真实公网 IP 场景): + ①裸号(`0501234567`)→ **同轮** AI 回显 "+966 50 123 4567 — is that right?"(墙保+白名单放行)→ 客户 "yes" → 确认落定不再追问 + ②验收 ① 变体:客户 "no, I'm in Egypt" → 重算 egypt 码再回显 → yes → 落定 + ③客户答非所问(聊产品)→ 2 轮后不再纠缠;首封邮件检查:带 e164+标注 + ④误判对照:确认等待期客户对无关问题答 yes → **不得**触发确认 + ⑤邮箱-only 客户零干扰;带 + 号客户零干扰(旁路) + 验收后清理测试会话 +- [ ] **Step 4: 观察期提醒**(breach_rate 号码回显观测已在线)+ commit 收尾文档 + +--- + +## 拆除清单(Task 3-5 执行时对照,防漏拆) + +| 旧机制(206beae..cbf234d) | 处置 | +|---|---| +| `_CC_ASK_EN/_ZH` 追问句 | 改造为 `_CC_ECHO_*` 回显模板(同源结构保留) | +| `_CC_WALL_*` 检测 "country code" | Task 4 换 e164 在场检测 | +| `_cc_asked` 计数写入 | 停写;存量映射(Task 3 Step 3) | +| `_contact_completion_note` cc 分支 | 删除(判定树接管);email 分支保留改条件 | +| system.md 五处"重发完整号"例外 | Task 5 改回显语义 | + +## 回滚 + +git revert 对应 commit 重发版;判定树/渲染均为独立模块,单任务可回退。