fix: 词表墙的四项审查修复(es 池降级/占位符白名单校验/键 allowlist/提取自检+防呆测试)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -101,7 +101,11 @@ def pivot_for(user_msg: str, has_contact: bool = False,
|
||||
wall: CompiledWall | None = None) -> str:
|
||||
w = wall or DEFAULT_WALL
|
||||
lang = _lang_pool(user_msg)
|
||||
pool = (w.pivots_contact if has_contact else w.pivots)[lang]
|
||||
pools = w.pivots_contact if has_contact else w.pivots
|
||||
# 缺池语言优雅降级(审查 I-1): 租户墙可无 es 池(auto 中英+$ 词表),西语
|
||||
# 消息按重音符路由 es 后回落 EN 池——与 _lang_pool 注释的罗曼语族降级
|
||||
# 取舍同源(宁可降级不可 KeyError 炸会话流)。
|
||||
pool = pools.get(lang) or pools["en"]
|
||||
return pool[0]
|
||||
|
||||
|
||||
@@ -110,7 +114,9 @@ def deflection_for(user_msg: str, rotate: int = 0,
|
||||
wall: CompiledWall | None = None) -> str:
|
||||
w = wall or DEFAULT_WALL
|
||||
lang = _lang_pool(user_msg)
|
||||
pool = (w.deflections_contact if has_contact else w.deflections)[lang]
|
||||
pools = w.deflections_contact if has_contact else w.deflections
|
||||
# 缺池语言优雅降级(审查 I-1): 同 pivot_for,回落 EN 池
|
||||
pool = pools.get(lang) or pools["en"]
|
||||
return pool[rotate % len(pool)]
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
seed_profiles/<slug>/wall.json(由 scripts/extract_config.py 从老项目源码
|
||||
AST 提取生成)是租户墙词表/话术的唯一数据源。compile_wall 把 JSON 编译为
|
||||
运行时对象: pattern 字符串 → re.Pattern,模板话术(含 {shown}/{intl} 等
|
||||
占位符)原样承载、由 conversation.py 运行期 .format() 回填。
|
||||
占位符)原样承载、由 conversation.py 运行期 .format() 回填;占位符仅允许
|
||||
TEMPLATE_PLACEHOLDERS 白名单键,编译期干跑校验(字面花括号/未知占位符
|
||||
显性失败,不留到运行期炸会话流)。
|
||||
|
||||
regex 条目两种形态(pattern 文本与源码逐字一致):
|
||||
- 纯字符串: 无标志位,re.compile(s)
|
||||
- {"pattern": "...", "flags": "i"}: flags 字符映射 re.IGNORECASE
|
||||
(提取脚本只产出 "i";comment 键为设计说明注释,编译时忽略)
|
||||
(提取脚本只产出 "i";comment 键为设计说明注释,编译时忽略;dict 条目
|
||||
出白名单 {pattern, flags, comment} 之外的键 → 显性拒绝,防键拼错静默放宽)
|
||||
|
||||
结构/regex 无效 → WallConfigError,信息含字段路径(坏 regex 含 "regex"
|
||||
字样与 re.error 原因)——配置错误在编译期显性失败,不流入运行期。
|
||||
@@ -69,6 +72,20 @@ _KNOWN_FIELDS = frozenset(
|
||||
"reask_patterns", "triggers", "reminders",
|
||||
"cc_templates"])
|
||||
|
||||
# regex 条目 dict 的合法键(审查 I-3): 提取脚本只产出 {pattern, flags,
|
||||
# comment};键拼错(如 "flagss")不再静默放宽标志位,而是编译期显性拒绝。
|
||||
_LEGAL_PATTERN_KEYS = frozenset({"pattern", "flags", "comment"})
|
||||
|
||||
# 模板占位符白名单(审查 I-2): conversation.py 运行期 .format() 回填的全部
|
||||
# 键名(grep 实证清单)。scripts/extract_config.py 反向引用此常量(单一来源),
|
||||
# 保证提取产出的占位符与引擎回填键名不漂移。字面花括号(如把中文"格式"
|
||||
# 写成 {格式})或未知占位符在编译期即拒绝——否则运行期 KeyError 直接炸
|
||||
# 客户会话流。
|
||||
TEMPLATE_PLACEHOLDERS = frozenset({
|
||||
"shown", "intl", "cc_script",
|
||||
"no_contact_web", "no_contact_app", "no_contact_already",
|
||||
})
|
||||
|
||||
|
||||
def _err(msg: str) -> WallConfigError:
|
||||
return WallConfigError(msg)
|
||||
@@ -79,6 +96,11 @@ def _compile_pattern(entry, field: str) -> re.Pattern:
|
||||
if isinstance(entry, str):
|
||||
pattern, flags = entry, 0
|
||||
elif isinstance(entry, dict):
|
||||
unknown = set(entry) - _LEGAL_PATTERN_KEYS
|
||||
if unknown:
|
||||
# 审查 I-3: 键拼错(如 "flagss")曾静默忽略→墙悄悄放宽,显性拒绝
|
||||
raise _err(f"{field}: 条目含未知键 {', '.join(sorted(unknown))}"
|
||||
f"(合法: {', '.join(sorted(_LEGAL_PATTERN_KEYS))}): {entry!r}")
|
||||
pattern = entry.get("pattern")
|
||||
flag_chars = str(entry.get("flags", ""))
|
||||
if not isinstance(pattern, str) or not pattern:
|
||||
@@ -116,8 +138,25 @@ def _compile_pool(cfg: dict, field: str) -> dict[str, tuple[str, ...]]:
|
||||
return out
|
||||
|
||||
|
||||
def _str_mapping(sub, label: str, required: tuple[str, ...]) -> dict[str, str]:
|
||||
"""一层 str 映射校验(no_contact 各键/reminders 各类变体/cc_templates 各组)。"""
|
||||
def _validate_template(template: str, label: str) -> None:
|
||||
"""编译期干跑 .format()(审查 I-2): 全白名单占位符代入空串,字面花括号/
|
||||
未知占位符/位置参数问题(原属运行期 KeyError/IndexError,炸客户会话流)
|
||||
前移为编译期 WallConfigError。纯文本模板(无占位符)天然通过。"""
|
||||
try:
|
||||
template.format(**{name: "" for name in TEMPLATE_PLACEHOLDERS})
|
||||
except (KeyError, IndexError, ValueError, AttributeError,
|
||||
TypeError) as exc:
|
||||
raise _err(f"{label}: 模板占位符非法(仅支持 "
|
||||
f"{', '.join(sorted(TEMPLATE_PLACEHOLDERS))}): "
|
||||
f"{template!r}: {exc}") from exc
|
||||
|
||||
|
||||
def _str_mapping(sub, label: str, required: tuple[str, ...],
|
||||
*, template: bool = False) -> dict[str, str]:
|
||||
"""一层 str 映射校验(no_contact 各键/reminders 各类变体/cc_templates 各组)。
|
||||
|
||||
template=True 时额外干跑 .format() 校验占位符(conversation.py 运行期
|
||||
回填的字段才需要;no_contact 为纯文本,不校验)。"""
|
||||
if not isinstance(sub, dict):
|
||||
raise _err(f"{label}: 必须为映射(缺类别或类型错误)")
|
||||
out: dict[str, str] = {}
|
||||
@@ -125,6 +164,8 @@ def _str_mapping(sub, label: str, required: tuple[str, ...]) -> dict[str, str]:
|
||||
val = sub.get(key)
|
||||
if not isinstance(val, str) or not val:
|
||||
raise _err(f"{label}.{key}: 缺非空字符串")
|
||||
if template:
|
||||
_validate_template(val, f"{label}.{key}")
|
||||
out[key] = val
|
||||
for key in sub:
|
||||
if key not in required:
|
||||
@@ -158,7 +199,7 @@ def compile_wall(cfg: dict) -> CompiledWall:
|
||||
if not isinstance(cfg["reminders"], dict):
|
||||
raise _err("reminders: 必须为映射")
|
||||
reminders = {cat: _str_mapping(cfg["reminders"].get(cat), f"reminders.{cat}",
|
||||
variants)
|
||||
variants, template=True)
|
||||
for cat, variants in _REMINDER_VARIANTS.items()}
|
||||
extra = set(cfg["reminders"]) - set(_REMINDER_VARIANTS)
|
||||
if extra:
|
||||
@@ -167,7 +208,8 @@ def compile_wall(cfg: dict) -> CompiledWall:
|
||||
if not isinstance(cfg["cc_templates"], dict):
|
||||
raise _err("cc_templates: 必须为映射")
|
||||
cc_templates = {group: _str_mapping(cfg["cc_templates"].get(group),
|
||||
f"cc_templates.{group}", variants)
|
||||
f"cc_templates.{group}", variants,
|
||||
template=True)
|
||||
for group, variants in _CC_TEMPLATE_VARIANTS.items()}
|
||||
extra = set(cfg["cc_templates"]) - set(_CC_TEMPLATE_VARIANTS)
|
||||
if extra:
|
||||
|
||||
Reference in New Issue
Block a user