621 lines
21 KiB
Markdown
621 lines
21 KiB
Markdown
|
|
# 员工结束会话功能 - 技术方案
|
|||
|
|
|
|||
|
|
> **版本**: v1.2
|
|||
|
|
> **日期**: 2026-07-30
|
|||
|
|
> **关联PRD**: PRD-REQ-会话-001-员工结束会话-v1.2
|
|||
|
|
> **状态**: 待评审
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、概述
|
|||
|
|
|
|||
|
|
本文档描述 v1.2 调整的技术实现方案。**核心变更**:
|
|||
|
|
1. 操作按钮从 5 态(hidden/disabled/active/urgent/waiting)扩展为 **6 态**(移除 hidden / 恢复 end / 新增 reopen)
|
|||
|
|
2. 新增 **4 种引导语**渲染逻辑
|
|||
|
|
3. store 新增 `showHeaderExitBtn` 字段,**控制顶部退出按钮仅 AI 场景显示**
|
|||
|
|
4. 双入口(顶部退出 + 操作按钮 end)**场景互斥**,从根源避免双入口混乱
|
|||
|
|
5. 24h 重新打开按钮实现
|
|||
|
|
|
|||
|
|
> 沿用 v1.1:排队进度底部消息胶囊、右栏移除 queue section、清理 api/queue 和 api/quiz。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、技术架构
|
|||
|
|
|
|||
|
|
### 2.1 现有代码结构(v1.2 基准)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
frontend-h5/src/components/chat/
|
|||
|
|
├── InputBar.vue # 输入栏(含操作按钮 6 态 + 引导语)— v1.2 改造
|
|||
|
|
├── ChatPanel.vue # 对话区(含顶部退出按钮 v-show)— v1.2 改造
|
|||
|
|
├── MessageBubble.vue # 消息气泡(含"会话已关闭"系统消息样式)
|
|||
|
|
├── EvaluationDialog.vue # 满意度评价弹窗
|
|||
|
|
├── ResolveConfirmCard.vue # 坐席结单确认卡片
|
|||
|
|
└── ... 其他
|
|||
|
|
|
|||
|
|
frontend-h5/src/stores/
|
|||
|
|
└── conversation.ts # 新增 showHeaderExitBtn 字段 + reopenSession action
|
|||
|
|
|
|||
|
|
frontend-h5/src/components/assistant/
|
|||
|
|
├── RightPanel.vue # 右栏(v1.1 已移除 queue section)— v1.2 沿用
|
|||
|
|
├── QueueWaiting.vue # 标记弃用,保留文件以备未来恢复
|
|||
|
|
└── ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 关键现有代码(InputBar.vue:194-243 v1.1 现状)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
type CallAgentState = 'hidden' | 'disabled' | 'active' | 'urgent' | 'waiting'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**v1.2 改造**:扩展为 6 态,详细见 §3.1。
|
|||
|
|
|
|||
|
|
### 2.3 关键现有代码(ChatPanel.vue:59-65 顶部退出按钮)
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<button class="chat-panel__exit-btn" title="结束会话" @click="handleExitWithEvaluation">
|
|||
|
|
<svg ...>...</svg>
|
|||
|
|
</button>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**v1.2 改造**:增加 `v-show="store.showHeaderExitBtn"`,详见 §3.5。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、方案设计
|
|||
|
|
|
|||
|
|
### 3.1 操作按钮 6 态状态机(v1.2 核心变更)
|
|||
|
|
|
|||
|
|
**位置**:InputBar.vue 控件区第一层(行 78-88)
|
|||
|
|
|
|||
|
|
**6 态定义**:
|
|||
|
|
|
|||
|
|
| 状态值 | 触发条件 | 按钮文案 | 按钮图标 | 样式 | 可点击 |
|
|||
|
|
|--------|---------|---------|---------|------|--------|
|
|||
|
|
| `disabled` | 无会话 / AI<3 轮 / 坐席离线 / 会话过期 | 人工坐席 | 🔒 | 灰色禁用 | ❌ |
|
|||
|
|
| `active` | AI≥3 轮 / 无紧急词 | 人工坐席 | 🎧 | 绿色描边 | ✅ |
|
|||
|
|
| `urgent` | 检测到紧急关键词 | 人工坐席 | 🚨 | 红色脉冲 | ✅ |
|
|||
|
|
| `waiting` | 排队中 | 排队等待 | ⏳ | 橙色描边 | ✅(取消排队)|
|
|||
|
|
| `end` 🆕 | 坐席已接入(serving) | 结束咨询 | 📴 | 红色填充 | ✅(弹评价)|
|
|||
|
|
| `reopen` 🆕 | 会话已关闭 + 24h 内 | 重新打开 | 🔄 | 蓝色填充 | ✅(reopen API)|
|
|||
|
|
|
|||
|
|
**v1.2 移除**:
|
|||
|
|
- ❌ `hidden` 态 — 完全移除,所有场景都有按钮呈现
|
|||
|
|
|
|||
|
|
**优先级顺序**(v1.2 修订):
|
|||
|
|
```
|
|||
|
|
end > reopen > waiting > active/urgent > disabled
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 状态流转图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌── (打开应用 / 重新进入)
|
|||
|
|
↓
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ disabled │ ← 无会话
|
|||
|
|
│ "先说问题" │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│ 用户输入 + AI ≥3 轮
|
|||
|
|
↓
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ active │ ── 检测到紧急词 ──→ urgent
|
|||
|
|
│ "人工坐席" │ │
|
|||
|
|
└────────┬─────────┘ │
|
|||
|
|
│ 点击 / 紧急关键词 │
|
|||
|
|
↓ ↓
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ waiting │ ←─────────────│ urgent │
|
|||
|
|
│ "排队等待" │ │ "人工坐席" │
|
|||
|
|
└────────┬─────────┘ └──────────────────┘
|
|||
|
|
│ 坐席接听
|
|||
|
|
↓
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ end 🆕 │
|
|||
|
|
│ "结束咨询" │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│ 坐席挂断 / 用户点结束
|
|||
|
|
↓ (close + 评价)
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ reopen 🆕 │ ← 会话已关闭 + 24h 内
|
|||
|
|
│ "重新打开" │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│ 点击 reopen
|
|||
|
|
↓
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ active │ (回到正常会话流程)
|
|||
|
|
└──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 store 新增字段(v1.2 关键)
|
|||
|
|
|
|||
|
|
**文件**:`stores/conversation.ts`
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 新增:顶部退出按钮是否可见(仅 AI 会话显示)
|
|||
|
|
const showHeaderExitBtn = computed<boolean>(() => {
|
|||
|
|
const conv = currentConversation.value
|
|||
|
|
if (!conv) return false // 无会话:隐藏
|
|||
|
|
if (conv.status === 'resolved') return false // 会话已关闭:隐藏
|
|||
|
|
|
|||
|
|
// 人工咨询启动后(waiting / serving):隐藏
|
|||
|
|
const isHumanActive =
|
|||
|
|
conv.status === 'waiting' ||
|
|||
|
|
conv.status === 'serving' ||
|
|||
|
|
!!queuePositionData.value // 排队中
|
|||
|
|
if (isHumanActive) return false
|
|||
|
|
|
|||
|
|
// AI 对话中:显示
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 新增:会话是否在 24h 重新打开窗口内
|
|||
|
|
const canReopen = computed<boolean>(() => {
|
|||
|
|
const conv = currentConversation.value
|
|||
|
|
if (!conv || conv.status !== 'resolved') return false
|
|||
|
|
if (!conv.resolved_at) return false
|
|||
|
|
const elapsed = Date.now() - new Date(conv.resolved_at).getTime()
|
|||
|
|
return elapsed < 24 * 60 * 60 * 1000 // 24h 内
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 新增:重新打开会话 action(调用 closing.ts 已有的 reopenConversation)
|
|||
|
|
async function reopenCurrentConversation(): Promise<void> {
|
|||
|
|
const conv = currentConversation.value
|
|||
|
|
if (!conv || !canReopen.value) return
|
|||
|
|
const result = await reopenConversation(conv.conversation_id)
|
|||
|
|
// 更新 store,触发 UI 切回 active 态
|
|||
|
|
if (result) {
|
|||
|
|
// ... 复用现有 openConversation 流程
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 组件改动
|
|||
|
|
|
|||
|
|
#### 3.4.1 InputBar.vue
|
|||
|
|
|
|||
|
|
**改动 1:状态类型扩展**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// v1.1
|
|||
|
|
type CallAgentState = 'hidden' | 'disabled' | 'active' | 'urgent' | 'waiting'
|
|||
|
|
|
|||
|
|
// v1.2
|
|||
|
|
type CallAgentState = 'disabled' | 'active' | 'urgent' | 'waiting' | 'end' | 'reopen'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 2:状态计算逻辑**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const callAgentState = computed<CallAgentState>(() => {
|
|||
|
|
const conv = store.currentConversation
|
|||
|
|
if (!conv) return 'disabled' // v1.2: 无会话 = disabled(不再 hidden)
|
|||
|
|
|
|||
|
|
// 🆕 会话已关闭 + 24h 内 → reopen
|
|||
|
|
if (conv.status === 'resolved' && store.canReopen) return 'reopen'
|
|||
|
|
|
|||
|
|
// 🆕 坐席已接入 → end
|
|||
|
|
if (conv.status === 'serving') return 'end'
|
|||
|
|
|
|||
|
|
// 🆕 顶部按钮可见性也通过 store.showHeaderExitBtn 暴露给父组件
|
|||
|
|
|
|||
|
|
if (conv.status === 'waiting') return 'waiting'
|
|||
|
|
|
|||
|
|
// 紧急关键词检测
|
|||
|
|
if (checkUrgentKeywords()) return 'urgent'
|
|||
|
|
|
|||
|
|
// AI ≥3 轮 / canCallAgent
|
|||
|
|
if (store.canCallAgent) return 'active'
|
|||
|
|
|
|||
|
|
// 兜底:disabled(含 AI<3 轮、坐席离线、过期等场景)
|
|||
|
|
return 'disabled'
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 3:按钮文案 + 图标扩展**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const callAgentBtnText = computed(() => {
|
|||
|
|
if (callAgentState.value === 'waiting') return '排队等待'
|
|||
|
|
if (callAgentState.value === 'end') return '结束咨询' // 🆕
|
|||
|
|
if (callAgentState.value === 'reopen') return '重新打开' // 🆕
|
|||
|
|
return '人工坐席'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const callAgentBtnIcon = computed(() => {
|
|||
|
|
if (callAgentState.value === 'waiting') return '⏳'
|
|||
|
|
if (callAgentState.value === 'urgent') return '🚨'
|
|||
|
|
if (callAgentState.value === 'active') return '🎧'
|
|||
|
|
if (callAgentState.value === 'end') return '📴' // 🆕
|
|||
|
|
if (callAgentState.value === 'reopen') return '🔄' // 🆕
|
|||
|
|
return '🔒'
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 4:按钮样式扩展**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const callAgentBtnClass = computed(() => ({
|
|||
|
|
'call-agent-btn--disabled': callAgentState.value === 'disabled',
|
|||
|
|
'call-agent-btn--active': callAgentState.value === 'active',
|
|||
|
|
'call-agent-btn--urgent': callAgentState.value === 'urgent',
|
|||
|
|
'call-agent-btn--waiting': callAgentState.value === 'waiting',
|
|||
|
|
'call-agent-btn--end': callAgentState.value === 'end', // 🆕
|
|||
|
|
'call-agent-btn--reopen': callAgentState.value === 'reopen', // 🆕
|
|||
|
|
}))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 5:点击行为扩展(end / reopen 分支)**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function handleCallAgent(): void {
|
|||
|
|
if (callAgentState.value === 'disabled') return
|
|||
|
|
|
|||
|
|
// 🆕 end 态:触发结束会话流程(与顶部按钮共用 emit)
|
|||
|
|
if (callAgentState.value === 'end') {
|
|||
|
|
emit('end-conversation')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🆕 reopen 态:调用 reopen API
|
|||
|
|
if (callAgentState.value === 'reopen') {
|
|||
|
|
store.reopenCurrentConversation()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (callAgentState.value === 'waiting') {
|
|||
|
|
emit('cancel-queue')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// active / urgent → 呼叫坐席
|
|||
|
|
emit('call-agent')
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 6:emit 扩展**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
(e: 'call-agent'): void
|
|||
|
|
(e: 'cancel-queue'): void
|
|||
|
|
(e: 'end-conversation'): void // 🆕(操作按钮 end 态专用,复用 handleExitWithEvaluation)
|
|||
|
|
}>()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 7:新增引导语渲染**
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<!-- 操作按钮 + 引导语 -->
|
|||
|
|
<div class="input-bar__controls">
|
|||
|
|
<button
|
|||
|
|
v-if="callAgentState !== 'reopen'" <!-- 🆕 reopen 态不显示"人工坐席"图标按钮 -->
|
|||
|
|
class="call-agent-btn"
|
|||
|
|
:class="callAgentBtnClass"
|
|||
|
|
...
|
|||
|
|
>
|
|||
|
|
<span class="call-agent-btn__icon">{{ callAgentBtnIcon }}</span>
|
|||
|
|
<span class="call-agent-btn__text">{{ callAgentBtnText }}</span>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<!-- 🆕 引导语:4 种场景 -->
|
|||
|
|
<div v-if="guideText" class="input-bar__guide" :class="guideClass">
|
|||
|
|
{{ guideText }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
// 🆕 引导语计算
|
|||
|
|
const guideText = computed<string | null>(() => {
|
|||
|
|
const conv = store.currentConversation
|
|||
|
|
const state = callAgentState.value
|
|||
|
|
|
|||
|
|
// 仅 disabled 态显示引导语(active/urgent/waiting/end/reopen 不显示)
|
|||
|
|
// 注意:这里 state 是 CallAgentState 按钮态(含 'active'),不是会话 status
|
|||
|
|
if (state !== 'disabled') return null
|
|||
|
|
|
|||
|
|
// 场景1:无会话
|
|||
|
|
if (!conv) return '💡 先描述一下你遇到的问题,AI 助手会先帮你看看'
|
|||
|
|
|
|||
|
|
// 场景2:AI <3 轮(用 canCallAgent 反推)
|
|||
|
|
// v1.2 P1 Bug 修复(QA 第 1 轮 2026-07-30):原代码误用 'active',
|
|||
|
|
// 但 ConversationInfo.status 取值为 'ai_handling' | 'waiting' | 'serving' | 'resolved'
|
|||
|
|
// (见 src/frontend-h5/src/api/conversation.ts:44),
|
|||
|
|
// 导致引导语场景 2 永远不显示。正确应为 'ai_handling'。
|
|||
|
|
if (conv.status === 'ai_handling' && !store.canCallAgent) {
|
|||
|
|
return '请继续描述您的问题或需求'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 场景3:坐席离线
|
|||
|
|
if (!store.agentOnline) {
|
|||
|
|
return '⚠️ 坐席当前离线,建议先用 AI 解答;如紧急可刷新重试'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 场景4:会话过期(>24h)
|
|||
|
|
if (conv.status === 'resolved' && !store.canReopen) {
|
|||
|
|
return '⏰ 上一会话已过期,开始新对话吧'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const guideClass = computed(() => ({
|
|||
|
|
'input-bar__guide--warn': guideText.value?.includes('⚠️'),
|
|||
|
|
}))
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 8:样式新增(end / reopen)**
|
|||
|
|
|
|||
|
|
```scss
|
|||
|
|
// 🆕 end 态:红色填充(最显眼的"危险/结束"语义)
|
|||
|
|
.call-agent-btn--end {
|
|||
|
|
border-color: var(--color-danger);
|
|||
|
|
background: var(--color-danger);
|
|||
|
|
color: #fff;
|
|||
|
|
font-weight: 700;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background: #dc2626;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
transform: scale(0.96);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🆕 reopen 态:蓝色填充(中性"恢复"语义)
|
|||
|
|
.call-agent-btn--reopen {
|
|||
|
|
border-color: var(--info, #1989fa);
|
|||
|
|
background: var(--info-light, #ecf5ff);
|
|||
|
|
color: var(--info, #1989fa);
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background: var(--info, #1989fa);
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
transform: scale(0.96);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.4.2 ChatPanel.vue(顶部按钮可见性)
|
|||
|
|
|
|||
|
|
**改动 1:顶部按钮 v-show 绑定**
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<!-- v1.1:始终显示 -->
|
|||
|
|
<button class="chat-panel__exit-btn" @click="handleExitWithEvaluation">
|
|||
|
|
|
|||
|
|
<!-- v1.2:仅 AI 场景显示 -->
|
|||
|
|
<button
|
|||
|
|
v-show="store.showHeaderExitBtn"
|
|||
|
|
class="chat-panel__exit-btn"
|
|||
|
|
title="结束会话(不再发送提醒)"
|
|||
|
|
@click="handleExitWithEvaluation"
|
|||
|
|
>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 2:处理 end 态的 emit**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// InputBar @end-conversation 事件处理
|
|||
|
|
async function handleEndConversation(): Promise<void> {
|
|||
|
|
// 复用现有 handleExitWithEvaluation 逻辑
|
|||
|
|
await handleExitWithEvaluation()
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 3:模板绑定 InputBar 新事件**
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<InputBar
|
|||
|
|
@call-agent="handleDirectCall"
|
|||
|
|
@cancel-queue="handleCancelQueue"
|
|||
|
|
@end-conversation="handleEndConversation" <!-- 🆕 -->
|
|||
|
|
/>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动 4:顶部按钮 tooltip / 弹窗文案**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 顶部按钮点击后弹窗文案调整
|
|||
|
|
const showExitConfirm = ref<boolean>(false)
|
|||
|
|
|
|||
|
|
function handleExitWithEvaluation(): void {
|
|||
|
|
// 弹窗文案强化"不再发送提醒"
|
|||
|
|
showExitConfirm.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// van-dialog message:
|
|||
|
|
message: "退出后会话记录会清空,当前咨询进度将丢失,将不再发送未回复提醒。"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.4.3 QueueCapsule.vue(v1.1 已规划,v1.2 沿用)
|
|||
|
|
|
|||
|
|
沿用 v1.1 设计,此处略。详见 v1.1 技术方案。
|
|||
|
|
|
|||
|
|
### 3.5 顶部按钮可见性规则(v1.2 关键)
|
|||
|
|
|
|||
|
|
| 场景 | `showHeaderExitBtn` | 说明 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 无会话 | `false` | 用户还没开始 |
|
|||
|
|
| AI 对话(active / urgent / disabled) | `true` | AI 场景专属可用 |
|
|||
|
|
| 坐席离线 | `true` | 仍属 AI 场景 |
|
|||
|
|
| 排队中(waiting) | `false` | 操作按钮 waiting 接管 |
|
|||
|
|
| 坐席服务中(serving) | `false` | 操作按钮 end 接管 |
|
|||
|
|
| 会话已关闭(reopen / disabled 过期) | `false` | 都不需要顶部按钮 |
|
|||
|
|
|
|||
|
|
**store 计算逻辑**:见 §3.3。
|
|||
|
|
|
|||
|
|
### 3.6 引导语设计
|
|||
|
|
|
|||
|
|
| 场景 | 引导语 | 颜色 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 无会话 | 💡 先描述一下你遇到的问题,AI 助手会先帮你看看 | 默认灰 |
|
|||
|
|
| AI <3 轮 | 请继续描述您的问题或需求 | 默认灰 |
|
|||
|
|
| 坐席离线 | ⚠️ 坐席当前离线,建议先用 AI 解答;如紧急可刷新重试 | 橙色(warn) |
|
|||
|
|
| 会话过期(>24h) | ⏰ 上一会话已过期,开始新对话吧 | 默认灰 |
|
|||
|
|
|
|||
|
|
**实现细节**:仅 `disabled` 态显示,其他态不显示。详见 §3.4.1 改动 7。
|
|||
|
|
|
|||
|
|
### 3.7 重新打开按钮实现
|
|||
|
|
|
|||
|
|
**API**:`api/closing.ts:119` 的 `reopenConversation` 已存在,v1.2 直接复用。
|
|||
|
|
|
|||
|
|
**前端流程**:
|
|||
|
|
```
|
|||
|
|
点击"🔄 重新打开"按钮
|
|||
|
|
↓
|
|||
|
|
调用 store.reopenCurrentConversation()
|
|||
|
|
↓
|
|||
|
|
POST /h5/conversations/current/reopen
|
|||
|
|
↓
|
|||
|
|
后端创建新会话并关联原会话ID
|
|||
|
|
↓
|
|||
|
|
前端更新 store.currentConversation
|
|||
|
|
↓
|
|||
|
|
按钮态自动切回 active / waiting / serving
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**24h 边界判断**:
|
|||
|
|
- 前端:基于 `conv.resolved_at` 计算 elapsed,与 24h 比较
|
|||
|
|
- 后端:reopen API 做权威校验(防止客户端改时间绕过)
|
|||
|
|
- 双保险:即使前端误判,后端也会拒绝 24h 外的 reopen
|
|||
|
|
|
|||
|
|
### 3.8 双入口防抖/互斥(v1.2 关键)
|
|||
|
|
|
|||
|
|
**场景互斥保证**(不需要 debounce):
|
|||
|
|
- 顶部按钮 + end 态按钮由 `store.showHeaderExitBtn` 和 `callAgentState` 联合控制
|
|||
|
|
- 任何时候两者**最多一个可点击**
|
|||
|
|
- 不存在"同时存在两个结束入口"的 UI 状态
|
|||
|
|
|
|||
|
|
**async handler 三件套**(防抖 + 同步 store + try/finally 重置):
|
|||
|
|
- 顶部按钮 `handleExitWithEvaluation`:沿用 ChatPanel.vue:403 已有修复
|
|||
|
|
- 操作按钮 end 态:复用 `handleEndConversation` → `handleExitWithEvaluation`,共享防抖逻辑
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 双重错误信息(参见 memory 里 BUG-用户-003 修复)
|
|||
|
|
async function handleExitWithEvaluation(): Promise<void> {
|
|||
|
|
if (isExiting.value) return // 防抖
|
|||
|
|
isExiting.value = true
|
|||
|
|
try {
|
|||
|
|
// ... close + 评价 + 关闭
|
|||
|
|
} catch (e: any) {
|
|||
|
|
// catch 兜底优先显示后端真实 message
|
|||
|
|
showToast(e?.message || '退出失败,请稍后重试')
|
|||
|
|
} finally {
|
|||
|
|
isExiting.value = false // try/finally 重置
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、API 需求
|
|||
|
|
|
|||
|
|
### 4.1 现有 API(已存在)
|
|||
|
|
|
|||
|
|
| API | 说明 |
|
|||
|
|
|-----|------|
|
|||
|
|
| `POST /h5/conversations/current/close` | 员工主动关闭会话 |
|
|||
|
|
| `POST /api/conversations/{id}/evaluation` | 提交满意度评价 |
|
|||
|
|
| `POST /h5/conversations/current/reopen` | 24h 内重开(v1.2 复用)|
|
|||
|
|
|
|||
|
|
### 4.2 需新增/确认
|
|||
|
|
|
|||
|
|
| API | 说明 |
|
|||
|
|
|-----|------|
|
|||
|
|
| WebSocket 推送 | 排队状态变更通知(waiting → serving → resolved)|
|
|||
|
|
| `conv.resolved_at` 字段 | 后端返回会话关闭时间,前端计算 24h 边界 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、非技术变更
|
|||
|
|
|
|||
|
|
### 5.1 前端样式新增
|
|||
|
|
|
|||
|
|
```scss
|
|||
|
|
// InputBar 新增样式(见 §3.4.1 改动 8)
|
|||
|
|
.call-agent-btn--end { ... }
|
|||
|
|
.call-agent-btn--reopen { ... }
|
|||
|
|
|
|||
|
|
// ChatPanel 顶部按钮 v-show + tooltip(见 §3.4.2 改动 1)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 文案修订
|
|||
|
|
|
|||
|
|
| 位置 | v1.1 | v1.2 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 顶部按钮 tooltip | "结束会话" | "结束会话(不再发送提醒)" |
|
|||
|
|
| 退出确认弹窗 | "退出后会话记录会清空..." | "...将不再发送未回复提醒。" |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、风险与依赖
|
|||
|
|
|
|||
|
|
| 风险 | 影响 | 缓解措施 |
|
|||
|
|
|------|------|---------|
|
|||
|
|
| R1:双入口虽然场景互斥,但文案相似 | 用户可能误操作 | 顶部按钮 tooltip 强调"不再发送提醒",end 按钮文案"结束咨询"明确"挂断" |
|
|||
|
|
| R2:`showHeaderExitBtn` 计算错误 | 顶部按钮在错误场景显示 | 单元测试覆盖所有 9 种场景 |
|
|||
|
|
| R3:24h 边界依赖客户端时钟 | 改时间绕过 | 后端 reopen API 权威校验 |
|
|||
|
|
| R4:end 态 + 顶部按钮同时误触发 | 重复 close 请求 | `isExiting` 防抖 + store 同步(沿用 BUG-用户-003 修复) |
|
|||
|
|
| R5:引导语渲染与按钮态不同步 | UI 状态不一致 | 引导语渲染条件与 `callAgentState` 共享同一 computed |
|
|||
|
|
|
|||
|
|
**依赖**:
|
|||
|
|
- D1:WebSocket `queue_position_update` 事件已实现
|
|||
|
|
- D2:`store.agentOnline` 字段已存在
|
|||
|
|
- D3:EvaluationDialog 组件已实现
|
|||
|
|
- D4:reopenConversation API 已在 `closing.ts:119` 实现
|
|||
|
|
- D5:`conv.resolved_at` 字段后端返回(需确认)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、验收标准
|
|||
|
|
|
|||
|
|
| # | 验收条件 |
|
|||
|
|
|---|---------|
|
|||
|
|
| AC1 | 6 态按钮正确切换(disabled/active/urgent/waiting/end/reopen) |
|
|||
|
|
| AC2 | 4 种引导语按场景正确显示 |
|
|||
|
|
| AC3 | 顶部按钮仅 AI 场景显示(waiting/serving 时隐藏) |
|
|||
|
|
| AC4 | 操作按钮 end 态仅 serving 时显示,点击触发评价弹窗 |
|
|||
|
|
| AC5 | 「重新打开」按钮在 24h 内可见,点击调用 reopen API |
|
|||
|
|
| AC6 | 引导语与按钮态同步,无错位 |
|
|||
|
|
| AC7 | 双入口场景互斥(任何时候只有一个结束入口可用) |
|
|||
|
|
| AC8 | 评价提交后窗口自动关闭 |
|
|||
|
|
| AC9 | 重新进入应用显示全新/重开会话 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、实施计划
|
|||
|
|
|
|||
|
|
| 阶段 | 任务 | 工时 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 1 | store 新增 showHeaderExitBtn + canReopen + reopenCurrentConversation | 1h |
|
|||
|
|
| 2 | InputBar.vue 6 态扩展 + 引导语 + end/reopen 样式 | 2h |
|
|||
|
|
| 3 | ChatPanel.vue 顶部按钮 v-show + 文案修订 + end-conversation 处理 | 1h |
|
|||
|
|
| 4 | 单元测试(状态机 + showHeaderExitBtn + canReopen) | 2h |
|
|||
|
|
| 5 | 联调测试 + 视觉回归 | 2h |
|
|||
|
|
| 6 | 部署 + 灰度 | 1h |
|
|||
|
|
| **合计** | | **9h** |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、变更记录(v1.1 → v1.2)
|
|||
|
|
|
|||
|
|
| 变更项 | v1.1 技术方案 | v1.2 技术方案 |
|
|||
|
|
|--------|---------------|---------------|
|
|||
|
|
| 状态机 | 5 态(含 hidden) | **6 态(移除 hidden / 恢复 end / 新增 reopen)** |
|
|||
|
|
| 引导语 | 无 | **新增 4 种引导语计算逻辑** |
|
|||
|
|
| store 新字段 | 无 | **`showHeaderExitBtn` + `canReopen` + `reopenCurrentConversation`** |
|
|||
|
|
| 顶部按钮 | 始终显示 | **`v-show="store.showHeaderExitBtn"` 仅 AI 场景显示** |
|
|||
|
|
| 顶部按钮文案 | 无 tooltip | **加 tooltip「结束会话(不再发送提醒)」** |
|
|||
|
|
| 退出确认弹窗 | "退出后会话记录会清空..." | **"...将不再发送未回复提醒。"** |
|
|||
|
|
| 操作按钮 end 态 | 已移除 | **恢复(红色填充)** |
|
|||
|
|
| 重新打开按钮 | 无 | **新增(蓝色填充)** |
|
|||
|
|
| 24h 边界 | 无 | **前端基于 `resolved_at` 计算 + 后端权威校验** |
|
|||
|
|
| 双入口防抖 | 无显式防抖 | **`isExiting` 共享 + 场景互斥(无需 debounce)** |
|