Files
ai-kefu-jc/docs/2026-08-18-contact-driven-lead-email-changelog.md
T

142 lines
11 KiB
Markdown
Raw Normal View History

# 改动文档:联系方式驱动的询盘邮件(批次 1)
- **日期**:2026-08-17 ~ 2026-08-18
- **分支**:`chany_dev_dev`(基于 `ee03e8d`,全部改动在工作区,尚未提交)
- **状态**:77 项测试全绿(基线 17 项),本地真实 GLM + SMTP 冒烟通过,用户实测验收通过
- **实施档案**:[superpowers/plans/2026-08-17-contact-driven-lead-email.md](superpowers/plans/2026-08-17-contact-driven-lead-email.md)(决策 D1-D13、12 个任务的全过程记录)
---
## 1. 背景与解决的问题
| # | 线上问题 | 根因(代码实证) |
|---|---|---|
| 1 | 大量**无联系方式**的询盘邮件推送至销售邮箱 | widget/embedded/standalone 来源的会话结束条件没有联系方式守卫(仅 app 来源有);GLM 评估不遵守"无联系方式不给过"的 prompt 规则 |
| 2 | 有意向客户没留联系方式,**无法触达** | 一判定 interested 就结束会话并锁死客户(ended 后输入框禁用),连补联系方式的机会都没有 |
| 3 | 客户**已给的联系方式被 AI 丢失/重复索要** | extractor 某轮返回空联系方式时浅合并整层覆盖(数据丢失);上下文提醒不含"禁止再索要"指令 |
**修复后的一句话**:联系方式是发询盘邮件的唯一条件——管道确定性检测到即发;没有就永不发、永不结束会话;已给的绝不丢失、绝不重复索要。
---
## 2. 核心架构
```
客户每条消息
├─ AI 回复(system.md 话术:价值交换要号 → 先聊后引 WhatsApp 兜底)
├─ 统一联系方式管道(app/services/contact_pipeline.py,纯函数)
│ 源1: extractor 提取(多语言 LLM)
│ 源2: 高置信正则(邻接绑定:裸数字一律不收,防价格/年份/车架号误捕)
│ 源3: 独立裸号(整条消息仅 ≥9 位数字 = 最显式自述)
│ → key 归一化 → 深合并(新值覆盖) → 最新显式自述覆盖 → 最近提及裁决
│ → 格式校验分级:confirmed(可触发)/ suspect(仅用于提醒)
├─ 邮件触发器(ConversationManager 内聚,路由层零邮件逻辑)
│ confirmed 首次出现 → [新询盘·标签];24h 跨会话同值 → [线索回访]轻提醒
│ 联系方式变化 → [询盘·更正N](每会话≤3封);达上限后锁邮件不锁快照
├─ 定向提醒:confirmed→"绝对禁止再索要";suspect→"礼貌确认一次"(值经消毒防提示词注入)
├─ 评估前置:<3 条客户消息跳过 GLM 评估(省 1/3 调用),意向仅作邮件标签
├─ 每轮评估落库:interested+无联系方式 = 卡单记录(admin 可筛人工兜底)
└─ 会话永不自动结束(决策 A)——关闭留给后续超时清理
```
---
## 3. 文件清单
### 新增(5 个文件,1010 行)
| 文件 | 行数 | 职责 |
|---|---|---|
| `app/services/contact_pipeline.py` | 200 | 统一联系方式管道:`normalize_contacts`(中英文 key 别名归一)、`merge_contacts`(深合并,堵浅合并抹数据)、`split_validate`(confirmed/suspect 分级;微信 ≥4 字符)、`scan_user_messages`(高置信正则扫描,全量客户消息历史)、`latest_stated_contacts`(最新显式自述,唯一可覆盖 extractor 回传的来源)、`standalone_number`(独立裸号 ≥9 位)、`resolve_by_recency`(最近提及裁决:杀 extractor 历史振荡、压幻觉值)、`canon`(去重规范形) |
| `tests/test_contact_pipeline.py` | 253 | 管道 34 项单测(含误报反例:价格/年份/里程/VIN/订单号) |
| `tests/test_lead_email_trigger.py` | 417 | 触发器/结束守卫/提醒/更正链 25 项集成测试(FakeAI 桩,含用户实测 bug 的完整复刻用例) |
| `tests/test_email_rendering.py` | 66 | 邮件渲染 3 项(标签、HTML 转义、内部 key 过滤、RFC2047 扁平化) |
| `scripts/baseline_stats.py` | 74 | 上线前后对比基线统计(只读单次脚本) |
### 修改(9 个文件,+323 / −329)
| 文件 | 改动摘要 |
|---|---|
| `app/services/conversation.py` | 核心重构。**新增**:管道接线(深合并修复、正则辅源、覆盖、裁决)、`_maybe_send_lead_email` 触发器、`_get_or_init_evaluation`/`_recently_emailed_canons` 辅助、评估前置、每轮评估落库(卡单标记)、定向提醒与短消息增强的过滤/消毒。**删除**:自动结束逻辑与 `_finalize`(决策 A)、`process_message` 死代码(83 行)、`_has_real_contact`、旧 `ended&&interested→发邮件` 路径的全部依赖 |
| `app/services/email_service.py` | 全量重写。全字段 `html.escape`(D12 防注入);主题意向标签 `[新询盘·意向强/待确认/无意向/未评估]` + 空白折叠加固;中文标签联系方式列;新增 `send_returning_lead_email`([线索回访])与 `send_correction_email(contact_info, count)`([询盘·更正N]);"邮件为触发时快照"提示 |
| `app/api/routes/chat.py` | 拆除 `_try_send_email` 整个函数、email 相关导入与 ended 触发块;WS 连接生命周期保持 HEAD 语义(ended 后关闭连接) |
| `app/api/routes/chat_rest.py` | 拆除 lazy-import 触发块;docstring 更新;App 响应契约字段不变(`ended` 恒为 `false`,见 §8 知会事项) |
| `app/models/database.py` | Evaluation 表新增 `correction_count INT DEFAULT 0` |
| `app/main.py` | `_migrate` 新增 PRAGMA 幂等迁移(实测二次运行 no-op) |
| `app/prompts/system.md` | ①价值交换固定模板(只承诺"完整报价单",禁具体价格/折扣/时限);②先聊后引(聊天内最多要两次,才推 WhatsApp 按钮);③拿到联系方式后确认一句继续解答,不急着收尾;④系统提醒配合(已提供→禁再要;疑似→礼貌确认一次) |
| `app/prompts/extractor.md` | 客户最后提供的号码覆盖旧值;单独数字串也是号码 |
| `tests/test_chat_rest_message.py` | 删除 2 个钉死旧触发路径的测试(被 manager 层新测试取代),保留 reply/404/429 |
---
## 4. 行为对照
| 场景 | 改前 | 改后 |
|---|---|---|
| widget 无联系方式 + GLM 误判 interested | 结束会话锁死客户 + 发无联系方式邮件 | 零邮件;会话永不结束,AI 按话术继续争取 |
| 第 1 条消息就带联系方式 | 等判定结束才发 | 立即发 `[新询盘·未评估]` |
| 联系方式被 extractor 空轮抹掉 | 数据丢失 | 深合并保留 |
| AI 重复索要已给的联系方式 | 常发生 | 定向提醒禁止;suspect 值礼貌确认一次 |
| 换号(说/不说"更正") | 依赖 extractor(实测常丢) | 最新显式自述 / 独立裸号 / 最近提及,确定性翻新 |
| 同号镜像两个 key(手机+WhatsApp) | 更正后新旧混排 | 同步翻新,邮件/快照均为最新 |
| extractor 在历史号码间振荡 | 每次翻转烧一封更正邮件 | 最近提及裁决熄灭(实测复刻用例锁死) |
| 同号码 24h 内再开新会话 | 每会话一封全量 | 一行 `[线索回访]` 轻提醒 |
| 更正次数 | —(新能力) | 每会话 ≤3 封 `[更正N]`;达上限锁邮件不锁快照(admin 永远最新) |
| 会话结束 | contact+interested+≥3轮即结束锁死 | **永不自动结束**(决策 A) |
| GLM 评估调用 | 每条消息 | <3 条客户消息跳过 |
| 邮件 HTML | 客户可控文本不转义 | 全转义 + 主题行加固 |
---
## 5. 关键决策记录(产品拍板存档)
| 决策 | 内容 |
|---|---|
| D1 | 联系方式 = 发邮件唯一条件(不看消息数、不看意向评级) |
| D2 | 意向降级为邮件标签;评估跳过时标签为 [未评估](与客户端事件的 pending 语义分离) |
| D4 | 同会话全量邮件 1 封 + 更正邮件 ≤3 封;锁邮件不锁快照 |
| D5 | 24h 跨会话同值去重 → 轻提醒 |
| D6 | confirmed→禁止再索要 / suspect→礼貌确认(含提示词注入消毒) |
| D7 | 评估前置(<3 轮跳过) |
| D8 | interested+无联系方式 → 卡单记录(admin 人工兜底数据基础) |
| D9 | 旧路由层触发路径整体拆除(与新触发器同批部署,杜绝双发窗口) |
| 决策 A(2026-08-18) | 会话永不自动结束(邮件解耦后自动结束只剩伤害:锁死没问完的客户) |
| 更正上限(2026-08-18) | 1 → 3 次(防轰炸与漏最终号的折中) |
## 6. 实测驱动的修复链(2026-08-18 用户实测发现的 4 个 bug)
| 用户反馈 | 定位 | 修复(任务号) |
|---|---|---|
| "wrong number, use X" 不触发更正 | extractor 回传旧值 + 扫描器首中/填空无法翻新 | 最新显式自述覆盖(Task 12) |
| 更正邮件里手机/WhatsApp 新旧混排 | extractor 镜像同号进两个 key,只翻新命中 key | 同号同步(12b)→ 升级为最近提及裁决(12c) |
| 更正配额被烧光、末封邮件显示旧号 | extractor 在历史号码间振荡,每次翻转触发虚假更正 | 最近提及裁决(12c) |
| 直接发裸号不触发更正 | 裸号唯一捕获源是概率性的 extractor | 独立裸号确定性捕获,≥9 位下限排除价格/年份(12d) |
---
## 7. 部署注意事项 ⚠️
1. **整体上线,禁止部署中间态**:新触发器与旧路径拆除必须同一批次(D9,中间态存在双发邮件窗口)。
2. **迁移自动执行**:启动时 `_migrate` 自动为旧库追加 `correction_count` 列,无需手工 SQL。
3. **跑生产基线**:部署机上执行 `PYTHONIOENCODING=utf-8 python scripts/baseline_stats.py`,覆盖 `docs/superpowers/plans/2026-08-17-baseline.txt`(当前为开发库占位数据)。
4. **SMTP 必须配置**:首触邮件即时发送;未配置时每条消息都会重试并记日志。
5. **上线后观测**(对照基线):①新邮件 100% 带联系方式;②全量邮件量 vs 基线上界;③[更正]/[回访] 占比;④admin 卡单数量。
## 8. 已知限制与残余风险(台账摘录)
| 项 | 状态 |
|---|---|
| App 端 `ended` 恒为 false(决策 A) | App 若有"对话结束"闭环 UI 需调整;widget/独立页零影响 |
| 会话只生不死(无超时清理) | 数据卫生问题,功能无害;超时清理排健康轨,**上线后尽早做** |
| 第 1 轮 extractor 幻觉出从未提及的号码 → 首封邮件混排 | 双低概率条件叠加;whatsapp 侧仍真实可联;出现再做 12e |
| 中文无冒号给号句式("我手机号是X")无确定性捕获 | extractor 兜底;实测命中率待观察,低则补中文语境模式 |
| 号码变体击穿 24h 去重 / 撤回已给联系方式 / 客户离开页面后无法找回 | best-effort / 接受 / 物理死局(火力已前置到"人还在"时) |
| ADMIN_PASSWORD 仍为默认 admin123 | H1 暂缓(用户决策),**强烈建议尽快在 .env 修改** |
| 发送成功后落库失败的毫秒级窗口 | at-least-once 有意选择(漏线索比重复邮件贵) |
## 9. 未包含(后续批次,见实施档案 §6)
- **批次 2**:widget 会话持久化(localStorage+48h 过期,解决刷新丢会话/重复线索)、前端历史渲染(独立页 loadHistory 空函数)、WS 限流 + 消息长度上限
- **批次 3**:admin"缺联系方式线索"筛选(基于卡单记录)、UI 留资卡片
- **健康轨**:删 Redis、SQLite 备份+WAL、GLM 超时重试、admin 分页修复、国家映射(加拿大笔误)、超时清理会话、删除无调用方的 `AIService.chat`、ADMIN_PASSWORD