Files
ai-kefu-jc/docs/superpowers/plans/2026-10-08-cc-completion.md
T
浅念 9cc24aeaec docs: 计划全部任务勾选完成
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-08 21:29:11 +08:00

16 KiB
Raw Blame History

号码区号补全(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):

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 全转移,先写全再实现):

# 判定树矩阵(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 消费条件);None(无 _cc,号本就带 +/00)→ 非等待(邮箱轻补不回归)
    • cc_settled(cc_state):落定判定(邮箱轻补解锁条件,spec §4 统一口径);None → True
    • 测试矩阵点名两条易漏转移(评审补):国名重述不重置 attempts;no_ip 上限 1 轮
  • 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 3b(评审补):计数写回块(:263-283 if completion_marks:)条件扩为"计数或 _cc 新建任一存在";轮前钩子在 _build_context_reminder(:258)之前执行(写明顺序)

  • Step 4: 集成测试(FakeAI 流程):裸号轮当轮 _cc 已建(轮前钩子);cc_reply affirm→落定;canon 换号→重置重走;attempts 耗尽→fallback;出口无 _ 残留/cc_reply 剥离;旧 _cc_asked 会话不重复问;非等待态收到 cc_reply → Python 侧忽略(双门控确定性半边);canon 组归并(phone/whatsapp 同号确认一次、异号仅 whatsapp 走确认流——管道侧逻辑)

  • 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_* 拼装式结构改造)
    • no_ip 路径专属(评审补):IP 无效无可回显——保留一条 ask 形态模板 (_CC_ASK_* 原句保留不拆除,服务 no_ip 轮"方便发带区号完整号吗"1 轮为限); 该类轮墙裁决锚 = 问句短语("full number with country code"/"带国家代码")在场, 与 e164 在场锚并列;补一条 no_ip 管道测试(防静默失能)
    • 三处门控改线(评审补):旧 completion_marks == ["_cc_asked"] 触发点 (cc_wall :304 / 让位句 :805-814 / cc 提醒注入 :837)全部改线到 cc_due; conversation :755/:802-824 三处 Python 注入提醒里的"可请客户重发完整号一次" 短语同步收敛为回显语义(防同场竞争降低首过命中率)
    • 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,评审修正——grep 全量改写而非固定清单): grep "重发完整号/号码缺国家代码" 的全部命中处(实测 :7/:59/:175/:223/:229

    • 引导策略节 :198-200 + 好差示例 :162-166)改"回显确认"语义;特别两处高权重锚: ① :7 语言规则模板句清单成员(旧追问句→回显确认句);② :56 红线"绝不能在回复里 写出号码"加例外"唯一例外=系统提供的补全号原文(逐字)"——不改则 GLM 拒写号、 墙轮轮替换(锚点竞争教训);好差示例两改一增(裸号组改回显形态;增"AI 自编号码"差例)。 Step 1 断言按 grep 命中计数而非固定五处
  • Step 3: 改 extractor.md(4b/§2 双门控):加 cc_reply 可选字段({"affirm": true} / {"affirm": false, "country": "..."})+ "仅当客户在回应号码确认时填写"

  • Step 3b: 语言规则放开(需求方确认 2026-10-08):system.md :7 的"模板句 只有中英两套原版、绝不翻译成第三种语言"改为"默认英语;模板句跟随客户语言 自然表达——系统提醒提供的逐字话术优先用其语言版本,无则按客户语言自然 翻译(保持意思与礼貌度)";"翻译是违规"示例反向修正;CLAUDE.md 差异表同步。 风险记录:REASK 墙仍中英覆盖,放开后"已留资重复索要"的小语种形态靠 reminder 确认式压制——记入观察期(后续按需扩 REASK 句式)。真实验收补 西语客户留资一轮(Task 7 验收⑥)

  • 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 含派生标注键;_ 键过滤前消费;跨会话 24h 去重(评审补)——同客户新会话再留同一裸号,canon 对比集须并入 _cc.e164 的 canon,回访轻提醒不退化为全量询盘

  • Step 2: 实现:email_service 渲染 _cc(正文/主题/更正/回头客,签名加参);lead_push profile_out 先消费 _cc(contact 用 e164、标注进 号码补全说明 派生键)再过滤 _;ev.contact_info 等待态尾注;_contact_completion_note 的 email 分支解锁条件改 cc_settled;canon 比对口径钉死(评审补):ev.contact_info 存裸号原文+尾注、e164 走派生,且同会话更正判定(:635 old_canons)与跨会话去重集(:577-591)并入 _cc.e164 的 canon——防 e164 替换本体造成"每轮误判换号→更正连发"与"回访退化"

  • Step 3: 跑绿 + 全量绿 + commit feat: 出站补全渲染——首封e164+标注/主题行/推送派生/面板尾注


Task 7: 全量回归 + 部署 + 真实 LLM 验收

  • Step 1: 全量 pytest + 既有测试迁移清点(D1"confirmed 即发"断言 fixture 本地格式号→补全渲染值更新;回显豁免旧断言残留清零;CLAUDE.md 测试基线数更新)
  • Step 2: 部署(CLAUDE.md 发版流程;镜像含新依赖自动装)
  • Step 3: 真实 LLM 验收(⚠️ 全部在本地环境——需求方明令禁止生产测试,防发邮件污染线上数据): 本地起 uvicorn + 本地 .env(真实智谱 Key 已有;SMTP 留占位→发送失败但触发链路在 本地日志/DB 可查;PUSH_ENABLED=false)。IP 场景:本地 IP 为内网(resolve→None→ no_ip 路径天然覆盖);带 IP 场景用本地 .env 之外临时 client_info 自报公网 IP 模拟 (或仅靠单测矩阵覆盖判定树分支,真实验收聚焦对话行为)。场景清单: ①裸号(0501234567)→ 同轮 AI 回显 "+966 50 123 4567 — is that right?"(墙保+白名单放行)→ 客户 "yes" → 确认落定不再追问 ②验收 ① 变体:客户 "no, I'm in Egypt" → 重算 egypt 码再回显 → yes → 落定 ③客户答非所问(聊产品)→ 2 轮后不再纠缠;首封邮件检查:带 e164+标注 ④误判对照:确认等待期客户对无关问题答 yes → 不得触发确认 ⑤邮箱-only 客户零干扰;带 + 号客户零干扰(旁路) ⑥西语客户全流程(语言规则放开验证:AI 用西语回显确认+翻译质量+墙 e164 锚 语言无关不受影响) 验收后清理本地测试会话;生产部署后仅做只读冒烟(/health+浏览页面,不对话不留资)
  • Step 4: 观察期提醒(breach_rate 号码回显观测已在线)+ commit 收尾文档

拆除清单(Task 3-5 执行时对照,防漏拆)

旧机制(206beae..cbf234d) 处置
_CC_ASK_EN/_ZH 追问句 保留不拆(服务 no_ip 轮);新增 _CC_ECHO_* 承接回显场景
_CC_WALL_* 检测 "country code" Task 4 改:回显轮=e164 在场,no_ip 轮=问句短语锚(保留)
_cc_asked 计数写入 停写;存量映射(Task 3 Step 3)
_contact_completion_note cc 分支 删除(判定树接管);email 分支保留改条件
system.md 五处"重发完整号"例外 Task 5 改回显语义

回滚

git revert 对应 commit 重发版;判定树/渲染均为独立模块,单任务可回退。