fix: 终审修复——墙执行侧接租户管道(端到端回归锁)+seed补email组+措辞与死依赖清理

C-1(必须修): 墙的执行侧没接租户管道——runtime.compiled_wall 全库只有
AIService 的 $DEFLECTION_POOL 一处消费,conversation.py 执行侧全走模块级
DEFAULT_WALL(jiancai 出厂),auto 租户会话实际跑的是 jc 墙。修复:
- manager 构造时 self._wall = runtime.compiled_wall(与 AIService 同源);
- 6 处过滤调用(violates_boundary/no_contact_value/asks_contact 的句内
  与 pending 尾巴两路)、CC 墙模板/替换句、no_contact 收尾选择、
  pivot/deflection、兜底句、contact_ask 触发日志判定全部传 wall=self._wall;
- _build_context_reminder/_contact_completion_note/_cc_confirm_note/
  _cc_script/_cc_wall_template/_lang_note_for 加 wall=None 尾参
  (None = 模块级默认墙,旧测试直调语义不变;流式侧 _contact_completion_note
  双胞调用同传 wall,写回与注入不漂移);
- 模块级 DEFAULT_WALL 视图常量(BOUNDARY_TRIGGER_RE/_CC_*/_EMAIL_NOTE_* 等)
  保留——boundary_filter 出厂默认与旧测试锚定,boundary_filter 模块级
  DEFAULT_WALL 照旧,conversation 会话流内一律租户墙;
- 端到端回归锁 tests/test_wall_execution_tenant.py(两租户真 runtime +
  脚本化假 AI): jc 独有 [€£] pattern 句在 jiancai 拦截替换(收尾=jc
  pivots 池)/auto 放行逐字流出;同一邮箱违规句两租户各自精确收到自己
  no_contact["web"](12 hours vs replies fast!);contact_ask 提醒注入
  各自墙文案互不渗漏。已验证模拟旧 DEFAULT_WALL 行为时三测全败。

I-1: 两租户 seed 补 email 组(以 brand_values.json 实际值为准:
auto=notify_email 环境值,jiancai=品牌邮箱——其 notify_email 记录是
notify@example.com 占位): auto {sender_name: XYH, notify_emails:
[18339827479@163.com]}, jiancai {sender_name: HESUPIPE, notify_emails:
[hesu@hesuu.com]};seed 存裸名,From 头渲染时外层再包【】。补测
test_seed_tenants_ship_email_group: env NOTIFY_EMAIL patch 成异值证明
seed 槽胜出。

M-1/M-2: README/CLAUDE.md "platform.db 热加载"改为"文件级 registry_reload
热加载(Phase 2 升级为 platform.db)";requirements.txt 删 alembic
(全库零引用,grep 实证唯一命中即该行)。

M-3/M-4(挂账): CLAUDE.md Phase 2 清理单补两项——/health 分级健康
(Phase 4 部署批次)、PromptProvider.from_dir FileNotFoundError 带路径上下文。

全量回归: 463 passed(459 基线 + 4 新增),根目录与 tests/ 子目录同数。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 00:18:54 +08:00
parent 0bf492c1e4
commit 9ad02a778f
9 changed files with 274 additions and 63 deletions
+107 -60
View File
@@ -25,6 +25,7 @@ from app.engine.services.contact_pipeline import (
canon, digits_only, latest_stated_contacts, merge_contacts,
normalize_contacts, resolve_by_recency, scan_user_messages, split_validate,
)
from app.engine.wall_schema import CompiledWall
if TYPE_CHECKING:
from app.platform.registry import TenantRuntime
@@ -71,6 +72,8 @@ def context_seed_map(config_data: dict | None) -> dict:
# - 原则: 误触发低害(多一条提醒),漏触发高危(红线失守),词表偏宽;上线后漏词案例回流补词
# Task 4 词表数据化: 三组触发正则的数据源 = DEFAULT_WALL(jiancai 出厂)
# .triggers 编译产物(seed_profiles/jiancai/wall.json),上方精修说明随数据留存
# C-1(终审): 本组模块常量降级为"默认墙视图"——供直调/旧测试按名锚定;
# 运行期触发词查找一律走租户墙(manager 的 self._wall.triggers)
BOUNDARY_TRIGGER_RE = DEFAULT_WALL.triggers["boundary"]
# 预算路径路由(评审#9): 命中 BOUNDARY 触发词且含预算词 → 走预算 playbook
@@ -97,11 +100,14 @@ LANG_NOTE_ZH = DEFAULT_WALL.reminders["language_follow"]["zh"]
LANG_NOTE_ES = DEFAULT_WALL.reminders["language_follow"]["es"]
def _lang_note_for(msg: str) -> str:
def _lang_note_for(msg: str, wall: CompiledWall | None = None) -> str:
# C-1(终审): wall=None = 模块级默认墙(LANG_NOTE_* 常量即该默认视图,
# 旧测试锚定);manager 一律传租户墙
w = wall if wall is not None else DEFAULT_WALL
if _CJK_RE.search(msg or ""):
return LANG_NOTE_ZH
return w.reminders["language_follow"]["zh"]
if _ES_FEATURE_RE.search(msg or ""):
return LANG_NOTE_ES
return w.reminders["language_follow"]["es"]
return ""
@@ -157,45 +163,60 @@ def _cc_intl(wa_state: dict | None) -> str:
return str((wa_state or {}).get("intl") or cc_completion.format_intl(e164))
def _cc_script(wa_state: dict | None, lang: str) -> str:
def _cc_script(wa_state: dict | None, lang: str,
wall: CompiledWall | None = None) -> str:
"""CC 等待态本轮逐字话术(Task 4, spec §4;P3 扩 ES): 有 e164 → 回显
确认句(intl 由 Python 算好填入,GLM 不拼号);no_ip → 追问句
(_CC_ASK_* 服务)。reminder 注入/墙替换/测试断言三方同源,统一走本函数。"""
(_CC_ASK_* 服务)。reminder 注入/墙替换/测试断言三方同源,统一走本函数。
C-1(终审): wall=None = 模块级默认墙(_CC_ECHO_*/_CC_ASK_* 常量即该
默认视图);manager 一律传租户墙。"""
w = wall if wall is not None else DEFAULT_WALL
if str((wa_state or {}).get("e164") or ""):
intl = _cc_intl(wa_state)
echo = {"zh": _CC_ECHO_ZH, "es": _CC_ECHO_ES}.get(lang, _CC_ECHO_EN)
return echo.format(intl=intl)
return {"zh": _CC_ASK_ZH, "es": _CC_ASK_ES}.get(lang, _CC_ASK_EN)
echo = w.cc_templates["echo"]
return {"zh": echo["zh"], "es": echo["es"]}.get(
lang, echo["en"]).format(intl=intl)
ask = w.cc_templates["ask"]
return {"zh": ask["zh"], "es": ask["es"]}.get(lang, ask["en"])
def _cc_wall_template(wa_state: dict | None, lang: str) -> str:
def _cc_wall_template(wa_state: dict | None, lang: str,
wall: CompiledWall | None = None) -> str:
"""CC 触发轮墙整段替换句: 回显轮 = ECHO 模板(自带 "Got it —" 衔接头);
no_ip 轮 = _CC_WALL_*(ASK 追问句 + 衔接头,沿用 Task 3 形态;P3 扩 ES)。"""
no_ip 轮 = _CC_WALL_*(ASK 追问句 + 衔接头,沿用 Task 3 形态;P3 扩 ES)。
C-1(终审): wall=None = 模块级默认墙(_CC_WALL_* 常量即该默认视图);
manager 一律传租户墙。"""
if str((wa_state or {}).get("e164") or ""):
return _cc_script(wa_state, lang)
return {"zh": _CC_WALL_ZH, "es": _CC_WALL_ES}.get(lang, _CC_WALL_EN)
return _cc_script(wa_state, lang, wall=wall)
w = wall if wall is not None else DEFAULT_WALL
tmpl = w.cc_templates["wall"]
return {"zh": tmpl["zh"], "es": tmpl["es"]}.get(lang, tmpl["en"])
def _cc_confirm_note(wa_state: dict | None, lang: str) -> str:
def _cc_confirm_note(wa_state: dict | None, lang: str,
wall: CompiledWall | None = None) -> str:
"""cc 等待态确认句注入(Task 4, spec §4/§4b): cc_due 才注入,与邮箱轻补
cc_settled 串行互斥(两个"逐字使用这句"不同轮竞争)。外壳仿轻补指引:
⚠️ 系统指令声明 + 逐字话术 + 客户答复后致谢不追问。回显分支带零号码
唯一例外(4b-3 让位语义重建: 仅系统提供的补全号原文可写出)。
Task 4 词表数据化: 指引话术数据源 = DEFAULT_WALL.reminders.cc_guide
({intl}/{cc_script} 占位由本函数 .format() 回填,值逐字等价)。"""
Task 4 词表数据化: 指引话术数据源 = 墙 reminders.cc_guide
({intl}/{cc_script} 占位由本函数 .format() 回填,值逐字等价)。
C-1(终审): wall=None = 模块级默认墙;manager 一律传租户墙。"""
if not wa_state or not cc_completion.cc_due(wa_state):
return ""
w = wall if wall is not None else DEFAULT_WALL
if str(wa_state.get("e164") or ""):
intl = _cc_intl(wa_state)
return DEFAULT_WALL.reminders["cc_guide"]["echo"].format(
intl=intl, cc_script=_cc_script(wa_state, lang))
return DEFAULT_WALL.reminders["cc_guide"]["ask"].format(
cc_script=_cc_script(wa_state, lang))
return w.reminders["cc_guide"]["echo"].format(
intl=intl, cc_script=_cc_script(wa_state, lang, wall=wall))
return w.reminders["cc_guide"]["ask"].format(
cc_script=_cc_script(wa_state, lang, wall=wall))
def _contact_completion_note(
raw_info: dict | None, confirmed: dict | None, suspect: dict | None,
lang: str = "en", email_ask: bool = True,
wall: CompiledWall | None = None,
) -> tuple[str, list[str]]:
"""邮箱轻补提醒(spec §4.2/§4.3b,2026-10-08 判定树设计收编)。
纯函数,返回(注入文案,计数键)。
@@ -208,6 +229,9 @@ def _contact_completion_note(
suspect 仅为与调用处画像判定同签名,规则只看 confirmed(spec §4.2)。
email_ask=False(Task 9: contact.email_light_ask 开关)→ 恒空,不注入
也不产计数键。
C-1(终审): wall=None = 模块级默认墙(_EMAIL_NOTE_* 常量即该默认视图);
manager 一律传租户墙(流式调用处与本函数在 reminder 内的调用同参,
写回与注入永不漂移)。
"""
if not email_ask:
return "", []
@@ -222,7 +246,9 @@ def _contact_completion_note(
return "", []
if not cc_completion.cc_settled((info.get("_cc") or {}).get("whatsapp")):
return "", [] # whatsapp 确认流未落定: 邮箱轻补串行等待
return (_EMAIL_NOTE_ZH if lang == "zh" else _EMAIL_NOTE_EN), ["_email_asked"]
w = wall if wall is not None else DEFAULT_WALL
return ((w.reminders["email_light"]["zh"] if lang == "zh"
else w.reminders["email_light"]["en"]), ["_email_asked"])
def _cc_pre_turn(raw_info: dict, confirmed: dict, ip_iso: str | None) -> bool:
@@ -369,6 +395,11 @@ class ConversationManager:
# 的 PromptProvider + compiled_wall 组装($DEFLECTION_POOL 单一事实源
# 注入)。Task 9: AI 凭据/config 全部经 runtime.config.data 接线。
self.runtime = runtime
# C-1(终审审查): 执行侧墙接线——过滤/替换/提醒全走租户编译墙,
# 与 AIService 的 $DEFLECTION_POOL 同源(runtime.compiled_wall);
# 模块级 DEFAULT_WALL 仅供无租户上下文的直调默认(boundary_filter
# 出厂视图 + 本模块旧测试锚定),会话流内一律本墙。
self._wall = runtime.compiled_wall
self.ai = ai_service or AIService(
prompts=runtime.prompts,
wall=runtime.compiled_wall,
@@ -499,8 +530,8 @@ class ConversationManager:
context_reminder = self._build_context_reminder(
effective_raw, content, history, source=conv.source.value,
contact_flags=self._contact_flags)
if CONTACT_ASK_TRIGGER_RE.search(content or ""):
contact_flags=self._contact_flags, wall=self._wall)
if self._wall.triggers["contact_ask"].search(content or ""):
logger.info("Contact-ask reminder fired (source=%s)", conv.source.value)
# Task 3(spec §4.3/§6-1): 一次性计数写回 + _cc 状态同事务落库——
@@ -512,7 +543,7 @@ class ConversationManager:
_, completion_marks = _contact_completion_note(
effective_raw, _confirmed_now, _suspect_now,
"zh" if _CJK_RE.search(content or "") else "en",
email_ask=self._contact_flags["email_light_ask"])
email_ask=self._contact_flags["email_light_ask"], wall=self._wall)
if completion_marks or _cc_changed:
raw = dict(profile.raw_info or {})
for key in completion_marks:
@@ -577,12 +608,13 @@ class ConversationManager:
pending += token
complete, pending = split_sentences(pending)
for sentence in complete:
if violates_boundary(sentence):
if violates_boundary(sentence, wall=self._wall):
violated = True
dirty = sentence
violation = "boundary"
break
if no_contact_value(sentence, allow_number=cc_allow_number):
if no_contact_value(sentence, allow_number=cc_allow_number,
wall=self._wall):
violated = True
dirty = sentence
violation = "no_contact"
@@ -590,7 +622,8 @@ class ConversationManager:
# Task 5b 再索要墙: 已留资时索要句静默丢弃(丢句不掐断,
# 其余句子照常流出——软于两墙的掐断+替换);
# 渠道精确化: 只丢索要已在手渠道的句子
if has_contact and asks_contact(sentence, _confirmed_channels):
if has_contact and asks_contact(sentence, _confirmed_channels,
wall=self._wall):
logger.warning("Re-ask wall dropped sentence (conv=%s): %r",
conv.id, sentence[:60])
dropped_ask = True
@@ -604,15 +637,17 @@ class ConversationManager:
if violated:
break # 提前掐断生成,省 token
if not violated and pending:
if violates_boundary(pending):
if violates_boundary(pending, wall=self._wall):
violated = True
dirty = pending
violation = "boundary"
elif no_contact_value(pending, allow_number=cc_allow_number):
elif no_contact_value(pending, allow_number=cc_allow_number,
wall=self._wall):
violated = True
dirty = pending
violation = "no_contact"
elif has_contact and asks_contact(pending, _confirmed_channels):
elif has_contact and asks_contact(pending, _confirmed_channels,
wall=self._wall):
# Task 5b 再索要墙: 尾巴是索要句 → 静默丢弃(不流出不掐断)
logger.warning("Re-ask wall dropped tail (conv=%s): %r",
conv.id, pending[:60])
@@ -636,21 +671,26 @@ class ConversationManager:
# NO_CONTACT_ALREADY——GLM 有 3/3 回显倾向,该路径概率高,
# 客户该收到确认/追问而非"稍后联系"虚假承诺;
# 价格墙路径(violation=="boundary")不变
closing = _cc_wall_template(_wa_cc, _cc_lang_of(content))
closing = _cc_wall_template(_wa_cc, _cc_lang_of(content),
wall=self._wall)
else:
# Task 9 收口: 视图直取 DEFAULT_WALL.no_contact(单一
# 事实源),不再经 boundary_filter 的模块级常量转引
closing = (DEFAULT_WALL.no_contact["already"] if has_contact
else (DEFAULT_WALL.no_contact["app"]
# Task 9 收口: 视图直取墙 no_contact(单一事实源),
# 不经 boundary_filter 的模块级常量转引;C-1(终审):
# 取租户墙——替换文案各自来自自己的池
_w = self._wall
closing = (_w.no_contact["already"] if has_contact
else (_w.no_contact["app"]
if conv.source.value == "app"
else DEFAULT_WALL.no_contact["web"]))
else _w.no_contact["web"]))
else:
logger.warning(
"Boundary filter intercepted sentence (conv=%s): %r | tail=%r",
conv.id, dirty[:80], (pending or "")[:40])
closing = (pivot_for(content, has_contact) if emitted
closing = (pivot_for(content, has_contact, wall=self._wall)
if emitted
else deflection_for(content, _boundary_rotate(),
has_contact=has_contact))
has_contact=has_contact,
wall=self._wall))
# Minor-5(审查修复): 已流出前缀与替换句之间补一个空格分隔
# ("...xxxx? Got it —"——英文衔接无空格会粘连);CC/触发轮缓冲
# 未流出,emitted 恒空,天然不受影响。空格随 closing 一起流出,
@@ -690,7 +730,8 @@ class ConversationManager:
logger.warning(
"CC enforcement wall replaced reply (conv=%s): %r",
conv.id, cc_full[:60])
cc_template = _cc_wall_template(_wa_cc, cc_lang)
cc_template = _cc_wall_template(_wa_cc, cc_lang,
wall=self._wall)
for i in range(0, len(cc_template), 40):
yield {"type": "stream_chunk",
"content": cc_template[i:i + 40]}
@@ -702,7 +743,7 @@ class ConversationManager:
if not violated and dropped_ask and not emitted.strip():
logger.warning("Re-ask wall dropped entire reply (conv=%s), "
"falling back to closing", conv.id)
fallback = DEFAULT_WALL.no_contact["already"]
fallback = self._wall.no_contact["already"]
for i in range(0, len(fallback), 40):
yield {"type": "stream_chunk",
"content": fallback[i:i + 40]}
@@ -1141,24 +1182,29 @@ class ConversationManager:
def _build_context_reminder(raw_info: dict, latest_msg: str,
history: list[dict] | None = None,
source: str = "",
contact_flags: dict | None = None) -> str:
contact_flags: dict | None = None,
wall: CompiledWall | None = None) -> str:
"""Build a context reminder from extracted profile info.
Task 4 词表数据化: 六类轮中提醒话术(boundary/contact_ask/
language_follow/lead_given/cc_guide/email_light)的数据源 =
DEFAULT_WALL.reminders(seed_profiles/jiancai/wall.json);
墙 reminders(seed_profiles/<slug>/wall.json);
{shown}/{no_contact_web}/{no_contact_already} 占位由本函数
.format() 回填,值与数据化前逐字等价。
contact_flags(Task 9): contact 行为开关——cc_completion=False 跳过
cc 确认指引,email_light_ask=False 跳过邮箱轻补;None = 全开
(直调本函数的旧测试语义)。"""
(直调本函数的旧测试语义)。
wall(C-1 终审): 租户编译墙(触发词/提醒话术/no_contact 全走它);
None = 模块级默认墙(直调/旧测试语义——BOUNDARY_TRIGGER_RE 等
模块常量即该默认视图)。"""
w = wall if wall is not None else DEFAULT_WALL
# 职能边界提醒(评审#7: 前置到早退之前,首条消息画像为空也要生效)
boundary_note = ""
if BOUNDARY_TRIGGER_RE.search(latest_msg or ""):
if BUDGET_TRIGGER_RE.search(latest_msg or ""):
if w.triggers["boundary"].search(latest_msg or ""):
if w.triggers["budget"].search(latest_msg or ""):
# 评审#9: 预算求推荐 → 预算路径(推荐+反问+引导),不一刀切要号
boundary_note = DEFAULT_WALL.reminders["boundary"]["budget"]
boundary_note = w.reminders["boundary"]["budget"]
else:
boundary_note = DEFAULT_WALL.reminders["boundary"]["normal"]
boundary_note = w.reminders["boundary"]["normal"]
# 联系方式状态前移(供 boundary/contact 提醒共用判定)
contacts_all = normalize_contacts(raw_info.get("联系方式"))
@@ -1169,23 +1215,23 @@ class ConversationManager:
# 冒烟实证: 抽象指令("用App话术")GLM 会无视并背 prompt 里更显眼的 web 脚本——
# 提醒必须携带该分支的逐字话术,不让模型自己挑。
contact_note = ""
if CONTACT_ASK_TRIGGER_RE.search(latest_msg or ""):
if w.triggers["contact_ask"].search(latest_msg or ""):
if source == "app":
if confirmed or suspect:
# 已留资: 索要我方联系方式 → 逐字"经理主动联系"版,
# 与"禁止再索要"提醒不再打架
# Task 3 收尾: 第三处禁令同步改写(禁令集完整)——与前两处
# 同语义,只禁已提供过的渠道,缺失渠道放行轻补一次
contact_note = DEFAULT_WALL.reminders["contact_ask"]["app_already"]
contact_note = w.reminders["contact_ask"]["app_already"]
else:
contact_note = DEFAULT_WALL.reminders["contact_ask"]["app"]
contact_note = w.reminders["contact_ask"]["app"]
else:
contact_note = DEFAULT_WALL.reminders["contact_ask"]["web"].format(
no_contact_web=DEFAULT_WALL.no_contact["web"])
contact_note = w.reminders["contact_ask"]["web"].format(
no_contact_web=w.no_contact["web"])
# 语言跟随提醒(Task 11): 与 boundary/contact 提醒同挂载位——
# 首条消息画像为空也要生效(中文客户开场即漂英文是实测重灾区)
lang_note = _lang_note_for(latest_msg)
lang_note = _lang_note_for(latest_msg, wall=wall)
if not raw_info:
# 空画像:仅返回边界/联系/语言提醒(可能为空串)
@@ -1213,7 +1259,7 @@ class ConversationManager:
completion_note, _marks = _contact_completion_note(
raw_info, confirmed, suspect,
lang="zh" if _CJK_RE.search(latest_msg or "") else "en",
email_ask=flags["email_light_ask"])
email_ask=flags["email_light_ask"], wall=wall)
# Task 4(spec §4 回显确认轮): cc 等待态注入确认句指引——有 e164 用
# 回显模板(逐字 intl),no_ip 用 ASK 追问句;与邮箱轻补 cc_settled
@@ -1221,30 +1267,31 @@ class ConversationManager:
# attempts 管理(轮末 transit 递增),提醒每轮等待态都在场。
# P3: 语言路由扩 ES(_cc_lang_of),西语轮注入西语模板。
cc_note = (_cc_confirm_note(
(raw_info.get("_cc") or {}).get("whatsapp"), _cc_lang_of(latest_msg))
(raw_info.get("_cc") or {}).get("whatsapp"), _cc_lang_of(latest_msg),
wall=wall)
if flags["cc_completion"] else "")
# 联系方式专项(D6) 与 职能边界(D6联动)
if confirmed:
shown = ", ".join(
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in confirmed.items())
# 逐字话术直接引用 DEFAULT_WALL.no_contact["already"](Task 9 收口:
# 逐字话术直接引用墙 no_contact["already"](Task 9 收口:
# 视图直取单一事实源)——与零号码墙替换句同源,三处"经理主动联系"
# 话术不漂移
# Task 3 防自锁改写(spec 4.1-6): 原"绝对禁止再索要任何联系方式!"
# 会抑制缺失渠道轻补——只禁已提供过的渠道,缺失渠道放行一次
parts.append(DEFAULT_WALL.reminders["lead_given"]["forbidden"].format(
parts.append(w.reminders["lead_given"]["forbidden"].format(
shown=shown))
parts.append(DEFAULT_WALL.reminders["lead_given"]["respond_script"].format(
no_contact_already=DEFAULT_WALL.no_contact["already"]))
parts.append(w.reminders["lead_given"]["respond_script"].format(
no_contact_already=w.no_contact["already"]))
if boundary_note:
parts.append(boundary_note)
# Task 3 防自锁改写: 尾部补缺失渠道豁免(与上方改写同因)
parts.append(DEFAULT_WALL.reminders["lead_given"]["boundary_tail"])
parts.append(w.reminders["lead_given"]["boundary_tail"])
elif suspect:
shown = ", ".join(
f"{k}: {' '.join(str(v).split())[:60]}" for k, v in suspect.items())
parts.append(DEFAULT_WALL.reminders["lead_given"]["suspect"].format(
parts.append(w.reminders["lead_given"]["suspect"].format(
shown=shown))
if boundary_note:
parts.append(boundary_note)