Files
wecom_it_smart_desk/frontend-admin/src/components/exclusion/ExclusionRuleForm.vue
T
Simon 449c6d4875 feat: 2026-07-12~13 全量更新 - AI对话链路改造+H5 v4/v5+坐席端v5+上下文感知诊断+知识库迭代3
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS

## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS

## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)

## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code

## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)

## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过

## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务

## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记已实施
- 新增架构图/时序图/类图(mermaid)

## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
2026-07-13 02:17:03 +08:00

397 lines
14 KiB
Vue
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.
<!--
=============================================================================
企微IT智能服务台 排除规则新建/编辑表单对话框
=============================================================================
说明
- 支持 create / edit 两种模式
- 匹配方式切换时匹配条件输入框动态变化
keyword 逗号分隔关键词输入
regex 正则表达式输入
intent 意图标识输入
category 问题分类输入
- 命中动作 transfer_human / transfer_human_with_context
转人工提示语为必填prompt_transfer 时选填silent_transfer 时隐藏
- 优先级 P0最高 P3最低默认 P2
- match_scope 多选默认 ai_auto_reply
-->
<template>
<el-dialog
:model-value="visible"
:title="mode === 'create' ? '新建排除规则' : '编辑排除规则'"
width="640px"
destroy-on-close
@update:model-value="$emit('update:visible', $event)"
@open="handleOpen"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="exclusion-form"
>
<!-- 规则名称 -->
<el-form-item label="规则名称" prop="rule_name">
<el-input
v-model="formData.rule_name"
placeholder="请输入规则名称,如:投诉类消息转人工"
maxlength="200"
show-word-limit
/>
</el-form-item>
<!-- 规则描述 -->
<el-form-item label="规则描述">
<el-input
v-model="formData.rule_description"
type="textarea"
:rows="2"
placeholder="可选,描述规则用途和命中场景"
maxlength="500"
/>
</el-form-item>
<!-- 优先级 + 匹配方式 -->
<div class="form-row">
<el-form-item label="优先级" prop="priority" class="form-col">
<el-select v-model="formData.priority" style="width: 100%">
<el-option label="P0(最高)" value="P0" />
<el-option label="P1(高)" value="P1" />
<el-option label="P2(中)" value="P2" />
<el-option label="P3(低)" value="P3" />
</el-select>
</el-form-item>
<el-form-item label="匹配方式" prop="match_type" class="form-col">
<el-select v-model="formData.match_type" style="width: 100%" @change="handleMatchTypeChange">
<el-option label="关键词" value="keyword" />
<el-option label="正则表达式" value="regex" />
<el-option label="意图识别" value="intent" />
<el-option label="分类匹配" value="category" />
</el-select>
</el-form-item>
</div>
<!-- 匹配条件 根据匹配方式动态展示 -->
<el-form-item label="匹配条件" prop="match_condition">
<!-- 关键词逗号分隔 -->
<template v-if="formData.match_type === 'keyword'">
<el-input
v-model="formData.match_condition"
type="textarea"
:rows="2"
placeholder="多个关键词用逗号分隔,如:投诉,举报,差评,维权"
/>
<div class="form-hint">多个关键词用英文逗号分隔消息中包含任一关键词即命中不区分大小写</div>
</template>
<!-- 正则表达式 -->
<template v-else-if="formData.match_type === 'regex'">
<el-input
v-model="formData.match_condition"
type="textarea"
:rows="2"
placeholder="输入正则表达式,如:^.*(投诉|举报).*$"
/>
<div class="form-hint">使用 Python re 语法忽略大小写和多行模式复杂正则设有 2 秒超时保护</div>
</template>
<!-- 意图识别 -->
<template v-else-if="formData.match_type === 'intent'">
<el-input
v-model="formData.match_condition"
placeholder="输入意图标识,如:complaint、refund_request"
/>
<div class="form-hint">
系统通过 AI 意图识别模型判断消息意图匹配此标识即命中常用意图complaint / refund_request / cancellation / escalation
</div>
</template>
<!-- 分类匹配 -->
<template v-else-if="formData.match_type === 'category'">
<el-input
v-model="formData.match_condition"
placeholder="输入问题分类名称,如:网络故障、账号异常"
/>
<div class="form-hint">
匹配会话最近一次智能分诊的问题分类若无分诊记录则不命中
</div>
</template>
</el-form-item>
<!-- 匹配范围 -->
<el-form-item label="匹配范围">
<el-select
v-model="formData.match_scope"
multiple
style="width: 100%"
placeholder="选择生效场景"
>
<el-option label="AI自动回复" value="ai_auto_reply" />
<el-option label="AI降级回复" value="ai_fallback" />
</el-select>
<div class="form-hint">选择该排除规则在哪些 AI 回复场景中生效</div>
</el-form-item>
<!-- 命中动作 -->
<el-form-item label="命中后动作" prop="action_type">
<el-radio-group v-model="formData.action_type">
<el-radio value="transfer_human">转人工</el-radio>
<el-radio value="transfer_human_with_context">转人工带上下文</el-radio>
<el-radio value="prompt_transfer">提示转人工</el-radio>
<el-radio value="silent_transfer">静默转人工</el-radio>
</el-radio-group>
</el-form-item>
<!-- 转人工提示语 条件显示 -->
<el-form-item
v-if="formData.action_type !== 'silent_transfer'"
:label="transferMessageLabel"
:prop="formData.action_type === 'transfer_human' || formData.action_type === 'transfer_human_with_context' ? 'transfer_message' : undefined"
>
<el-input
v-model="formData.transfer_message"
type="textarea"
:rows="2"
:placeholder="transferMessagePlaceholder"
maxlength="500"
/>
<div class="form-hint">{{ transferMessageHint }}</div>
</el-form-item>
</el-form>
<!-- 对话框底部 -->
<template #footer>
<el-button @click="$emit('update:visible', false)">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">
{{ mode === 'create' ? '创建' : '保存' }}
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import { ref, reactive, computed, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import {
createExclusionRule,
updateExclusionRule,
} from '@/api/exclusion'
import type { ExclusionRule, ExclusionRuleFormData } from '@/api/exclusion'
// ==========================================================================
// Props & Emits
// ==========================================================================
const props = defineProps<{
visible: boolean
mode: 'create' | 'edit'
rule: ExclusionRule | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
// ==========================================================================
// 表单引用
// ==========================================================================
const formRef = ref<FormInstance>()
const submitting = ref<boolean>(false)
// ==========================================================================
// 表单数据
// ==========================================================================
const formData = reactive<ExclusionRuleFormData>({
rule_name: '',
rule_description: '',
priority: 'P2',
match_type: 'keyword',
match_condition: '',
match_scope: ['ai_auto_reply'],
action_type: 'transfer_human',
transfer_message: '',
})
// ==========================================================================
// 计算属性
// ==========================================================================
/** 转人工提示语标签 */
const transferMessageLabel = computed<string>(() => {
if (formData.action_type === 'prompt_transfer') {
return '提示文案'
}
return '转人工提示语'
})
/** 转人工提示语 placeholder */
const transferMessagePlaceholder = computed<string>(() => {
if (formData.action_type === 'prompt_transfer') {
return '如:检测到您的问题可能需要人工协助,是否转接人工客服?'
}
return '如:您的问题已转接人工客服,请稍候'
})
/** 转人工提示语 hint */
const transferMessageHint = computed<string>(() => {
if (formData.action_type === 'prompt_transfer') {
return '用户看到的提示消息,不会自动转人工'
}
return '命中后发送给用户的转接提示消息'
})
// ==========================================================================
// 表单校验规则
// ==========================================================================
const formRules = reactive<FormRules>({
rule_name: [
{ required: true, message: '请输入规则名称', trigger: 'blur' },
{ min: 1, max: 200, message: '规则名称长度 1-200 字符', trigger: 'blur' },
],
priority: [
{ required: true, message: '请选择优先级', trigger: 'change' },
],
match_type: [
{ required: true, message: '请选择匹配方式', trigger: 'change' },
],
match_condition: [
{ required: true, message: '请输入匹配条件', trigger: 'blur' },
],
action_type: [
{ required: true, message: '请选择命中后动作', trigger: 'change' },
],
transfer_message: [
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
// transfer_human 和 transfer_human_with_context 时必填
if (
(formData.action_type === 'transfer_human' ||
formData.action_type === 'transfer_human_with_context') &&
(!value || !value.trim())
) {
callback(new Error('转人工动作需填写提示语'))
} else {
callback()
}
},
trigger: 'blur',
},
],
})
// ==========================================================================
// 对话框打开 — 初始化表单数据
// ==========================================================================
function handleOpen(): void {
if (props.mode === 'edit' && props.rule) {
// 编辑模式:填充已有数据
formData.rule_name = props.rule.rule_name
formData.rule_description = props.rule.rule_description || ''
formData.priority = props.rule.priority
formData.match_type = props.rule.match_type
formData.match_condition = props.rule.match_condition
formData.match_scope = props.rule.match_scope?.length ? [...props.rule.match_scope] : ['ai_auto_reply']
formData.action_type = props.rule.action_type
formData.transfer_message = props.rule.transfer_message || ''
} else {
// 新建模式:重置为默认值
formData.rule_name = ''
formData.rule_description = ''
formData.priority = 'P2'
formData.match_type = 'keyword'
formData.match_condition = ''
formData.match_scope = ['ai_auto_reply']
formData.action_type = 'transfer_human'
formData.transfer_message = ''
}
// 清除校验状态
nextTick(() => {
formRef.value?.clearValidate()
})
}
// ==========================================================================
// 匹配方式切换 — 清空匹配条件
// ==========================================================================
function handleMatchTypeChange(): void {
formData.match_condition = ''
nextTick(() => {
formRef.value?.clearValidate('match_condition')
})
}
// ==========================================================================
// 提交表单
// ==========================================================================
async function handleSubmit(): Promise<void> {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch {
return // 校验未通过
}
submitting.value = true
try {
// 构建提交数据
const submitData: ExclusionRuleFormData = {
rule_name: formData.rule_name.trim(),
rule_description: formData.rule_description?.trim() || undefined,
priority: formData.priority,
match_type: formData.match_type,
match_condition: formData.match_condition.trim(),
match_scope: formData.match_scope,
action_type: formData.action_type,
transfer_message: formData.transfer_message?.trim() || undefined,
}
if (props.mode === 'create') {
await createExclusionRule(submitData)
ElMessage.success('规则创建成功')
} else {
await updateExclusionRule(props.rule!.id, submitData)
ElMessage.success('规则更新成功')
}
emit('success')
} catch (error: any) {
// Axios 拦截器已统一弹窗,此处仅记录日志
console.error('提交排除规则失败:', error)
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.exclusion-form {
max-height: 60vh;
overflow-y: auto;
padding-right: 8px;
}
/* 双列表单行 */
.form-row {
display: flex;
gap: 16px;
}
.form-col {
flex: 1;
}
/* 表单提示文字 */
.form-hint {
font-size: 12px;
color: var(--text-muted, #909399);
line-height: 1.5;
margin-top: 4px;
}
</style>