Files
wecom_it_smart_desk/docs/02-技术文档/技术方案-REQ-会话-001-员工结束会话-v1.2.archive.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

21 KiB
Raw Blame History

员工结束会话功能 - 技术方案

版本: 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 现状)

type CallAgentState = 'hidden' | 'disabled' | 'active' | 'urgent' | 'waiting'

v1.2 改造:扩展为 6 态,详细见 §3.1。

2.3 关键现有代码(ChatPanel.vue:59-65 顶部退出按钮)

<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

// 新增:顶部退出按钮是否可见(仅 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:状态类型扩展

// v1.1
type CallAgentState = 'hidden' | 'disabled' | 'active' | 'urgent' | 'waiting'

// v1.2
type CallAgentState = 'disabled' | 'active' | 'urgent' | 'waiting' | 'end' | 'reopen'

改动 2:状态计算逻辑

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:按钮文案 + 图标扩展

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:按钮样式扩展

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 分支)

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')
}

改动 6emit 扩展

const emit = defineEmits<{
  (e: 'call-agent'): void
  (e: 'cancel-queue'): void
  (e: 'end-conversation'): void   // 🆕(操作按钮 end 态专用,复用 handleExitWithEvaluation
}>()

改动 7:新增引导语渲染

<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 助手会先帮你看看'

  // 场景2AI <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

// 🆕 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 绑定

<!-- 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

// InputBar @end-conversation 事件处理
async function handleEndConversation(): Promise<void> {
  // 复用现有 handleExitWithEvaluation 逻辑
  await handleExitWithEvaluation()
}

改动 3:模板绑定 InputBar 新事件

<InputBar
  @call-agent="handleDirectCall"
  @cancel-queue="handleCancelQueue"
  @end-conversation="handleEndConversation"   <!-- 🆕 -->
/>

改动 4:顶部按钮 tooltip / 弹窗文案

// 顶部按钮点击后弹窗文案调整
const showExitConfirm = ref<boolean>(false)

function handleExitWithEvaluation(): void {
  // 弹窗文案强化"不再发送提醒"
  showExitConfirm.value = true
}

// van-dialog message:
message: "退出后会话记录会清空,当前咨询进度将丢失,将不再发送未回复提醒。"

3.4.3 QueueCapsule.vuev1.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 重新打开按钮实现

APIapi/closing.ts:119reopenConversation 已存在,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.showHeaderExitBtncallAgentState 联合控制
  • 任何时候两者最多一个可点击
  • 不存在"同时存在两个结束入口"的 UI 状态

async handler 三件套(防抖 + 同步 store + try/finally 重置):

  • 顶部按钮 handleExitWithEvaluation:沿用 ChatPanel.vue:403 已有修复
  • 操作按钮 end 态:复用 handleEndConversationhandleExitWithEvaluation,共享防抖逻辑
// 双重错误信息(参见 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 前端样式新增

// 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 按钮文案"结束咨询"明确"挂断"
R2showHeaderExitBtn 计算错误 顶部按钮在错误场景显示 单元测试覆盖所有 9 种场景
R324h 边界依赖客户端时钟 改时间绕过 后端 reopen API 权威校验
R4:end 态 + 顶部按钮同时误触发 重复 close 请求 isExiting 防抖 + store 同步(沿用 BUG-用户-003 修复)
R5:引导语渲染与按钮态不同步 UI 状态不一致 引导语渲染条件与 callAgentState 共享同一 computed

依赖

  • D1WebSocket queue_position_update 事件已实现
  • D2store.agentOnline 字段已存在
  • D3EvaluationDialog 组件已实现
  • D4reopenConversation API 已在 closing.ts:119 实现
  • D5conv.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)