Files
ai-kefu-jc/docs/2026-08-18-contact-driven-lead-email-changelog.md
T
2026-09-29 14:15:12 +08:00

142 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 改动文档:联系方式驱动的询盘邮件(批次 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