feat(admin): WallAdvanced 高级折叠区(正则/提醒迁移)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-14 19:20:58 +08:00
parent 79374a5c1d
commit 0553ccc32c
@@ -0,0 +1,336 @@
<!--
高级折叠区(Task 9):引擎级规则编辑——price/reask 正则行编辑器(双形态
行编辑:pattern + flags + comment)+ triggers/zero_number 单正则 +
reminders 六类话术,自 WallPane 原样迁入(复制不移动——WallPane 旧块
Task 10 才删,两处并存无冲突)。默认全部折叠;数据经 props.model 直改
reactive(与 ScenarioCard 同契约)。
-->
<template>
<div class="wall-advanced">
<ElAlert
type="warning"
:closable="false"
class="wa-warn"
title="引擎级规则——改错可能导致拦截失灵或 AI 行为异常"
description="以下正则与指令由引擎直接消费;不确定的不要动;改坏可用版本历史回滚。"
/>
<ElCollapse v-model="openSections" class="wa-collapse">
<!-- 正则列表区:价格墙 / 再索要墙(双形态行编辑器) -->
<ElCollapseItem
v-for="section in patternListSections"
:key="section.key"
:name="section.key"
:title="section.title"
>
<p class="section-note">{{ section.plain }}</p>
<p class="section-note">{{ section.note }}</p>
<div
v-for="(row, i) in section.rows.value"
:key="i"
class="regex-row"
>
<div class="regex-line">
<span class="row-index">{{ i + 1 }}</span>
<ElInput
v-model="row.pattern"
placeholder="正则表达式(Python re 语法)"
class="mono-input pattern-input"
/>
<ElSelect v-model="row.flags" class="flags-select" title="标志位">
<ElOption value="" label="无" />
<ElOption value="i" label="i 忽略大小写" />
</ElSelect>
<ElButton
:type="row.comment ? 'primary' : 'default'"
link
title="设计备注(comment,随词表保存但不参与编译)"
@click="row.showComment = !row.showComment"
>
备注{{ row.comment ? '●' : '○' }}
</ElButton>
<ElButton
type="danger"
link
title="删除此条"
@click="section.remove(i)"
>
删
</ElButton>
</div>
<ElInput
v-if="row.showComment"
v-model="row.comment"
type="textarea"
:autosize="{ minRows: 2, maxRows: 6 }"
placeholder="comment:设计说明(防误杀断言等),保存为条目 comment 字段"
class="comment-input"
/>
<p v-else-if="row.comment" class="comment-view mono">{{ row.comment }}</p>
</div>
<ElButton size="small" @click="section.add()">+ 添加一条</ElButton>
</ElCollapseItem>
<!-- 单正则区:triggers 三键 / zero_number 两键(合并为一个折叠项) -->
<ElCollapseItem name="singles" title="触发词与零号码规则(triggers / zero_number)">
<p class="section-note">识别边界话题/预算/索要联系方式,以及号码与邮箱。</p>
<div v-for="section in singlePatternSections" :key="section.key" class="wa-single">
<p class="wa-single-title">{{ section.title }}</p>
<p class="section-note">{{ section.note }}</p>
<div class="regex-row">
<div class="regex-line">
<ElInput
v-model="model.singles[section.key].pattern"
placeholder="正则表达式(Python re 语法)"
class="mono-input pattern-input"
/>
<ElSelect
v-model="model.singles[section.key].flags"
class="flags-select"
title="标志位"
>
<ElOption value="" label="无" />
<ElOption value="i" label="i 忽略大小写" />
</ElSelect>
</div>
<p
v-if="model.singles[section.key].comment"
class="comment-view mono"
>
{{ model.singles[section.key].comment }}
</p>
</div>
</div>
</ElCollapseItem>
<!-- reminders 六类话术 -->
<ElCollapseItem name="reminders" title="AI 提醒指令(reminders 六类)">
<p class="section-note">
按场景注入 AI 的系统指令文案;{shown}/{intl} 等占位符由系统回填。
</p>
<div v-for="(variants, cat) in REMINDER_VARIANTS" :key="cat" class="reminder-cat">
<p class="reminder-cat-title">{{ reminderCatLabel(cat) }}</p>
<div v-for="variant in variants" :key="variant" class="reminder-variant">
<span class="variant-label mono">{{ cat }}.{{ variant }}</span>
<ElInput
v-model="model.reminders[cat][variant]"
type="textarea"
:autosize="{ minRows: 2, maxRows: 8 }"
:placeholder="variantLabel(cat, variant)"
/>
</div>
</div>
</ElCollapseItem>
</ElCollapse>
</div>
</template>
<script setup lang="ts">
import { REMINDER_VARIANTS, type WallEditModel } from '../model'
defineOptions({ name: 'WallAdvanced' })
const props = defineProps<{
/** 词表编辑模型(直改 reactive——与 ScenarioCard 同契约) */
model: WallEditModel
}>()
// ---- 区块展开态(默认全部收起——引擎级规则按需展开) -------------------------------
const openSections = ref<string[]>([])
// ---- 正则列表区(价格墙 / 再索要墙)--------------------------------------------------
const patternListSections = [
{
key: 'price_patterns',
title: '价格识别正则(price_patterns)',
plain: '识别客户在谈价格的句式。',
note: '每条一行;flags 仅支持 i(忽略大小写);comment 为设计说明(不参与编译)。空 pattern 行保存时自动剔除。',
rows: toRef(() => props.model.priceRows),
add: () => props.model.priceRows.push(blankRegexRow()),
remove: (i: number) => props.model.priceRows.splice(i, 1)
},
{
key: 'reask_patterns',
title: '追问识别正则(reask_patterns)',
plain: '识别客户再次追问的句式。',
note: 'AI 输出侧防重复索要:命中且客户已留联系方式 → 静默丢弃该句(无替换文案)。',
rows: toRef(() => props.model.reaskRows),
add: () => props.model.reaskRows.push(blankRegexRow()),
remove: (i: number) => props.model.reaskRows.splice(i, 1)
}
]
function blankRegexRow() {
return { pattern: '', flags: '' as const, comment: '', showComment: true }
}
// ---- 单正则区(triggers / zero_number)----------------------------------------------
const singlePatternSections = [
{
key: 'boundary',
title: '边界触发器 triggers.boundary',
note: '客户侧触发词大表(价格/折扣/交期/样品等)——命中注入边界提醒话术。'
},
{
key: 'budget',
title: '预算触发器 triggers.budget',
note: '客户报预算触发词(与 reminders.boundary.budget 话术联动)。'
},
{
key: 'contact_ask',
title: '索要联系方式触发器 triggers.contact_ask',
note: '客户索要我方联系方式(最高意向信号)——命中注入对应 source 话术。'
},
{
key: 'long_number',
title: '零号码墙 zero_number.long_number',
note: '≥10 位数字串(号码红线)——命中整句替换为 no_contact 对应句。'
},
{
key: 'any_email',
title: '零号码墙 zero_number.any_email',
note: '邮箱地址(号码红线同款)——命中整句替换。'
}
] as const
// ---- 提醒标签 -------------------------------------------------------------------------
const REMINDER_CAT_LABELS: Record<string, string> = {
boundary: '职能边界',
contact_ask: '索要联系方式',
language_follow: '语言跟随',
lead_given: '已留线索',
cc_guide: '国家代码指引',
email_light: '邮箱轻补'
}
function reminderCatLabel(cat: string): string {
return REMINDER_CAT_LABELS[cat] ?? cat
}
function variantLabel(cat: string, variant: string): string {
const map: Record<string, string> = {
'boundary.budget': '客户报预算求推荐的边界提醒',
'boundary.normal': '常规价格/交期询问的边界提醒',
'contact_ask.app_already': 'App 会话且已留资',
'contact_ask.app': 'App 会话(无页面按钮)',
'contact_ask.web': 'Web 会话(指路页面按钮)'
}
return map[`${cat}.${variant}`] ?? variant
}
</script>
<style scoped lang="scss">
.wall-advanced {
min-width: 0;
}
.wa-warn {
margin-bottom: 12px;
}
.section-note {
margin: 0 0 10px;
font-size: 12px;
line-height: 1.6;
color: var(--art-gray-400);
}
// ---- 正则行 -------------------------------------------------------------------------
.regex-row {
margin-bottom: 8px;
}
.regex-line {
display: flex;
gap: 6px;
align-items: center;
.row-index {
width: 20px;
flex-shrink: 0;
font-size: 12px;
color: var(--art-gray-400);
text-align: right;
}
.pattern-input {
flex: 1;
min-width: 0;
}
.flags-select {
width: 130px;
flex-shrink: 0;
}
}
.comment-input {
margin: 4px 0 0 26px;
}
.comment-view {
margin: 4px 0 0 26px;
padding: 6px 8px;
font-size: 12px;
line-height: 1.6;
color: var(--art-gray-500);
background: var(--art-main-bg-color, var(--el-fill-color-lighter));
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
}
:deep(.mono-input textarea),
:deep(.mono-input input) {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 12px;
}
// ---- 单正则子块(五键合并展示) -------------------------------------------------------
.wa-single {
margin-bottom: 14px;
.wa-single-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
}
}
// ---- 提醒六类 -------------------------------------------------------------------------
.reminder-cat {
margin-bottom: 14px;
.reminder-cat-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
}
}
.reminder-variant {
display: flex;
gap: 8px;
margin-bottom: 8px;
.variant-label {
width: 190px;
flex-shrink: 0;
padding-top: 6px;
font-size: 12px;
color: var(--art-gray-500);
text-align: right;
}
}
</style>