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分块上传
This commit is contained in:
Simon
2026-07-13 02:17:03 +08:00
parent bea288e414
commit 449c6d4875
176 changed files with 46637 additions and 4805 deletions
+378 -44
View File
@@ -21,7 +21,7 @@ import {
shake,
getApprovalLinks,
getSoftwareDownloads,
detectApprovalIntent,
// v2.0: detectApprovalIntent 已移除(审批意图识别统一由后端处理)
leaveAsParticipant as leaveAsParticipantApi,
type UserInfo,
type ConversationInfo,
@@ -35,6 +35,8 @@ import {
type ParticipantItem,
} from '@/api/conversation'
import { useEmployeeStore } from '@/stores/employee'
// v2.0: 导入 WS 发送函数(用于选项选择回传)
import { sendWsMessage } from '@/composables/useH5WebSocket'
// --------------------------------------------------------------------------
// 本地缓存配置
@@ -186,6 +188,29 @@ export const useConversationStore = defineStore('conversation', () => {
/** 参与者面板是否展开 */
const participantPanelVisible = ref<boolean>(false)
// ==========================================================================
// v2.0 新增:动态推荐状态(侧边栏卡片推送)
// ==========================================================================
/**
* 动态推荐卡片列表(由后端 WS dynamic_recommend 推送)
* 做什么:存储 AI 推荐的审批/操作入口卡片,供 RightPanel 的 DynamicRecommend 组件渲染
* 为什么:审批卡片从聊天流移到侧边栏,与文字回复强关联但不打断对话
* 最多保留 3 张,超过时移除最旧的
*/
const dynamicRecommendations = ref<Array<{
recommend_id: string
card_type: string
title: string
description: string
approval_type?: string
confidence: number
message_id: string
conversation_id: string
}>>([])
/** 动态推荐未查看数量(用于 Badge 红点提示) */
const unreadRecommendCount = ref<number>(0)
// ==========================================================================
// 流式 AI 回复(打字机)临时状态 — 改造方案 A
// ==========================================================================
@@ -453,11 +478,9 @@ export const useConversationStore = defineStore('conversation', () => {
return
}
// 异步检测审批意图(不阻塞消息发送流程)
// 员工发消息 → 消息正常发送 → 异步检测审批意图 → 如果命中则在消息列表中插入审批卡片
checkApprovalIntent(content).catch((err) => {
console.warn('[Store] 审批意图检测异常:', err)
})
// v2.0 改造(2026-07-13):删除前端 checkApprovalIntent 异步调用
// 审批意图识别已统一由后端 process_h5_ai_reply → get_structured_reply 处理
// 结果通过 WS dynamic_recommend 推送到侧边栏,不再由前端独立调用
// ========================================================================
// 步骤1:乐观更新 - 立即添加临时消息到列表
@@ -743,7 +766,8 @@ export const useConversationStore = defineStore('conversation', () => {
// 如果已分配坐席,更新话术内容
if (data.assign_result === 'assigned' && data.assigned_agent_id) {
systemMsg.content = `${data.funny_phrase}\n\n🎉 坐席已为您服务,请稍候...`
// 2026-07-12 改造:去掉 🎉 emoji,文案与后端 funny_phrases.connected 保持一致
systemMsg.content = `${data.funny_phrase}\n\n坐席正在查看您的信息,请等待处理回复!`
// 更新会话状态
if (currentConversation.value) {
currentConversation.value.status = 'serving'
@@ -782,41 +806,10 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
/**
* 异步检测审批意图
* 调用后端 /approval/detect-intent 接口,后端先用关键词预过滤,
* 命中后调 Dify 做意图识别。如果检测到审批意图(is_approval_request === true),
* 在消息列表中插入一条审批卡片消息(不阻塞消息发送流程)。
*
* @param text 用户输入的消息文本
*/
async function checkApprovalIntent(text: string): Promise<void> {
try {
const response = await detectApprovalIntent(text)
console.log('[Store] 审批意图检测结果:', response)
if (response.is_approval_request) {
// 在消息列表中插入审批卡片消息(作为消息流内联卡片,不是弹窗)
const approvalCardMessage: Message = {
message_id: `approval_card_${Date.now()}`,
conversation_id: currentConversation.value?.conversation_id || '',
message_type: 'system',
msg_type: 'approval_card',
content: '',
sender_name: '',
created_at: new Date().toISOString(),
extra_data: {
approval_type: response.approval_type,
confidence: response.confidence,
},
}
messages.value.push(approvalCardMessage)
console.log('[Store] 已插入审批卡片消息, approval_type:', response.approval_type)
}
} catch (error) {
console.warn('[Store] 审批意图检测失败:', error)
}
}
// v2.0 改造(2026-07-13):checkApprovalIntent() 已删除
// 审批意图识别统一由后端 process_h5_ai_reply → get_structured_reply 处理
// 结果通过 WS dynamic_recommend 推送到侧边栏 DynamicRecommend 组件
// 前端不再独立调用 /approval/detect-intent 接口
/** 关闭审批卡片弹窗(兼容旧调用,新流程中审批卡片为内联消息,无需关闭) */
function closeApprovalCard(): void {
@@ -1072,6 +1065,7 @@ export const useConversationStore = defineStore('conversation', () => {
ai_reply_count: number
can_call_agent: boolean
conversation_status: string
extra_data?: Record<string, any> // v2.0: 结构化消息的 options/action
}): void {
if (currentConversation.value?.conversation_id !== data.conversation_id) return
@@ -1084,6 +1078,8 @@ export const useConversationStore = defineStore('conversation', () => {
sender_name: data.sender_name || 'Duckula(达寇拉)',
created_at: new Date().toISOString(),
status: 'sent',
// v2.0: 传递结构化数据(options 按钮列表、action 卡片信息)
extra_data: data.extra_data || undefined,
}
// 用真实消息替换占位气泡(若存在)
@@ -1158,8 +1154,148 @@ export const useConversationStore = defineStore('conversation', () => {
streamingAiMessageId.value = null
}
// ==========================================================================
// v2.0 新增:AI 思考指示器 + 动态推荐 + 选项回传
// ==========================================================================
/**
* 取消流式气泡(WS 断连时调用
* 处理 WS ai_thinking 消息(v2.0 新增
* 做什么:在消息列表中创建/更新一个"正在思考..."的 AI 占位气泡
* 为什么:blocking 模式下 Dify 返回需要 3-8 秒,需要即时反馈让用户知道 AI 在工作
*
* @param data - { conversation_id, status? }
* - status 未定义:首次思考指示,创建占位气泡显示"正在思考..."
* - status = "still_thinking"15 秒后更新为"仍在思考..."
*/
function handleAiThinking(data: { conversation_id: string; status?: string }): void {
if (currentConversation.value?.conversation_id !== data.conversation_id) return
// 如果已有占位气泡(streamingAiMessageId 或 thinking 气泡),更新文字
const thinkingId = streamingAiMessageId.value
const idx = thinkingId
? messages.value.findIndex(m => m.message_id === thinkingId)
: -1
const thinkingText = data.status === 'still_thinking'
? '仍在思考中,请稍候...'
: '正在思考...'
if (idx !== -1) {
// 更新已有占位气泡的文字
messages.value[idx].content = thinkingText
} else {
// 创建新的占位气泡
const tempId = `ai_thinking_${data.conversation_id}_${Date.now()}`
streamingAiMessageId.value = tempId
messages.value.push({
message_id: tempId,
conversation_id: data.conversation_id,
message_type: 'ai',
msg_type: 'text',
content: thinkingText,
sender_name: 'Duckula(达寇拉)',
created_at: new Date().toISOString(),
})
}
}
/**
* 处理 WS dynamic_recommend 消息(v2.0 新增)
* 做什么:将 AI 推荐的操作卡片数据存入 dynamicRecommendations
* RightPanel 的 DynamicRecommend 组件会响应式渲染
* 为什么:审批/操作入口从聊天流移到侧边栏,与文字回复强关联但不打断对话
*
* @param data - 推荐卡片数据 { recommend_id, card_type, title, description, ... }
*/
function handleDynamicRecommend(data: {
recommend_id: string
card_type: string
title: string
description: string
approval_type?: string
confidence: number
message_id: string
conversation_id: string
}): void {
// 添加到推荐列表(最多 3 张,超过时移除最旧的)
dynamicRecommendations.value.push(data)
if (dynamicRecommendations.value.length > 3) {
dynamicRecommendations.value.shift()
}
// 增加未读计数(用于 Badge 红点提示)
unreadRecommendCount.value += 1
console.log('[Store] 动态推荐已接收:', data.title, '未读:', unreadRecommendCount.value)
}
/**
* 清除未读推荐计数(用户查看侧边栏推荐时调用)
*/
function clearUnreadRecommend(): void {
unreadRecommendCount.value = 0
}
/**
* 移除指定的推荐卡片(用户点击 × 关闭时调用)
*/
function removeRecommend(recommendId: string): void {
const idx = dynamicRecommendations.value.findIndex(r => r.recommend_id === recommendId)
if (idx !== -1) {
dynamicRecommendations.value.splice(idx, 1)
}
}
/**
* 发送选项选择(v2.0 新增)
* 做什么:用户点击聊天气泡中的选项按钮后,通过 WS 发送 option_select 消息
* 后端接收后转化为 Dify user message,继续对话
* 为什么:实现交互式排查的闭环——AI 提问 → 用户点选项 → AI 继续推理
*
* @param optionValue - 选项的 value 字段(如 "has_code"
* @param optionLabel - 选项的 label 字段(如 "有错误代码"),作为用户消息显示
*/
function sendOptionSelect(optionValue: string, optionLabel: string): void {
const convId = currentConversation.value?.conversation_id
if (!convId) return
// 1. 在消息列表中显示用户的选择(作为员工消息)
const employeeStore = useEmployeeStore()
messages.value.push({
message_id: `option_select_${Date.now()}`,
conversation_id: convId,
message_type: 'employee',
msg_type: 'text',
content: optionLabel,
sender_name: employeeStore.employeeName || '我',
created_at: new Date().toISOString(),
status: 'sent',
})
// 2. 通过 WS 发送 option_select 消息(v2.0 已接入)
// 后端接收后转化为 Dify user message,继续对话
// WS 消息格式:{ type: "option_select", data: { conversation_id, option_value, option_label } }
const sent = sendWsMessage({
type: 'option_select',
data: {
conversation_id: convId,
option_value: optionValue,
option_label: optionLabel,
},
})
if (!sent) {
// WS 未连接,降级为普通消息发送(HTTP API)
console.warn('[Store] WS 未连接,选项选择降级为 HTTP 消息发送')
// 通过现有的 sendMessage API 发送,后端会走正常 AI 回复流程
sendMessage({ content: optionLabel, msg_type: 'text' }).catch((err: unknown) => {
console.error('[Store] 选项选择 HTTP 降级发送失败:', err)
})
}
console.log('[Store] 选项选择已发送:', optionValue, optionLabel)
}
/**
* 取消流式占位气泡
* 做什么:若 WS 在流式推送中途断开,移除未完成的占位气泡,
* 交由 3 秒轮询兜底重新拉取完整 AI 消息
* 为什么:避免断连后留下半成品打字气泡
@@ -1173,6 +1309,184 @@ export const useConversationStore = defineStore('conversation', () => {
streamingAiMessageId.value = null
}
// ==========================================================================
// 新增:排队/关闭机制 WS 事件处理
// ==========================================================================
/** 排队位置更新数据(来自 WS queue_position_update 事件) */
const queuePositionData = ref<{
position: number
segment: string
ahead_count: number
queue_priority: number
} | null>(null)
/** 坐席结单请求(来自 WS pending_close_request 事件) */
const pendingCloseRequest = ref<{
conversation_id: string
resolve_summary: string
agent_name: string
} | null>(null)
/** 会话已关闭信息(来自 WS conversation_resolved 事件) */
const resolvedInfo = ref<{
conversation_id: string
resolved_by: string
resolved_method: string
resolve_summary: string
} | null>(null)
/**
* 处理排队位置更新事件
* WS event: queue_position_update
*/
function handleQueuePositionUpdate(data: {
conversation_id: string
position: number
segment: string
ahead_count: number
queue_priority: number
}): void {
if (currentConversation.value?.conversation_id !== data.conversation_id) return
queuePositionData.value = {
position: data.position,
segment: data.segment,
ahead_count: data.ahead_count,
queue_priority: data.queue_priority,
}
}
/**
* 处理会话关闭事件
* WS event: conversation_resolved
*/
function handleConversationResolved(data: {
conversation_id: string
status: string
resolved_by: string
resolved_method: string
resolve_summary: string
}): void {
if (currentConversation.value?.conversation_id !== data.conversation_id) return
// 更新会话状态
if (currentConversation.value) {
currentConversation.value.status = 'resolved'
}
resolvedInfo.value = {
conversation_id: data.conversation_id,
resolved_by: data.resolved_by,
resolved_method: data.resolved_method,
resolve_summary: data.resolve_summary || '',
}
// 清除结单请求卡片
pendingCloseRequest.value = null
// 添加系统消息
const resolveMsg: Message = {
message_id: `resolve_${data.conversation_id}_${Date.now()}`,
conversation_id: data.conversation_id,
message_type: 'system',
msg_type: 'text',
content: getResolveMessageText(data.resolved_method),
sender_name: '系统',
created_at: new Date().toISOString(),
status: 'sent',
}
messages.value.push(resolveMsg)
}
/**
* 处理坐席结单请求
* WS event: pending_close_request
*/
function handlePendingCloseRequest(data: {
conversation_id: string
resolve_summary: string
agent_name: string
}): void {
if (currentConversation.value?.conversation_id !== data.conversation_id) return
// 更新会话状态为 pending_close
if (currentConversation.value) {
currentConversation.value.status = 'waiting' as any // pending_close 映射
}
pendingCloseRequest.value = {
conversation_id: data.conversation_id,
resolve_summary: data.resolve_summary || '',
agent_name: data.agent_name || '坐席',
}
// 添加系统消息提示
const systemMsg: Message = {
message_id: `pending_close_${data.conversation_id}_${Date.now()}`,
conversation_id: data.conversation_id,
message_type: 'system',
msg_type: 'text',
content: `📋 ${data.agent_name || '坐席'}发起了结单请求,请确认`,
sender_name: '系统',
created_at: new Date().toISOString(),
status: 'sent',
}
messages.value.push(systemMsg)
}
/**
* 处理诊断答题答案附加到上下文
* WS event: quiz_diagnostic_answer
*/
function handleQuizDiagnosticAnswer(data: {
question: string
selected_answer: string
conversation_id: string
}): void {
// 添加系统消息,告知用户诊断信息已传递
const systemMsg: Message = {
message_id: `quiz_diag_${data.conversation_id}_${Date.now()}`,
conversation_id: data.conversation_id,
message_type: 'system',
msg_type: 'text',
content: `📝 诊断信息已传递给坐席:${data.question}${data.selected_answer}`,
sender_name: '系统',
created_at: new Date().toISOString(),
status: 'sent',
}
messages.value.push(systemMsg)
}
/** 清除结单请求(确认或拒绝后调用) */
function clearPendingCloseRequest(): void {
pendingCloseRequest.value = null
}
/** 清除关闭信息(重开会话后调用) */
function clearResolvedInfo(): void {
resolvedInfo.value = null
queuePositionData.value = null
}
/**
* 根据关闭方式生成系统消息文本
*/
function getResolveMessageText(resolvedMethod: string): string {
if (resolvedMethod === 'ai_self') {
return '✅ 问题已解决,会话已关闭。24小时内可重新打开。'
}
if (resolvedMethod === 'agent_confirm') {
return '✅ 坐席已结单,会话已关闭。24小时内可重新打开。'
}
if (resolvedMethod === 'employee_initiative') {
return '✅ 会话已关闭。24小时内可重新打开。'
}
if (resolvedMethod === 'system_timeout') {
return '⏰ 会话因长时间无响应已自动关闭。24小时内可重新打开。'
}
return '✅ 会话已关闭。24小时内可重新打开。'
}
/**
* 初始化应用
* 1. 获取用户信息
@@ -1325,7 +1639,7 @@ export const useConversationStore = defineStore('conversation', () => {
stopPolling,
shakeAgent,
fetchApprovalLinks,
checkApprovalIntent,
// v2.0: checkApprovalIntent 已删除(审批意图识别统一由后端处理)
closeApprovalCard,
showApprovalCard,
fetchSoftwareDownloads,
@@ -1347,5 +1661,25 @@ export const useConversationStore = defineStore('conversation', () => {
handleAiReply,
handleAiReplyFailed,
cancelStreamingBubble,
// v2.0 新增:AI 思考指示器 + 动态推荐 + 选项回传
handleAiThinking,
handleDynamicRecommend,
clearUnreadRecommend,
removeRecommend,
sendOptionSelect,
dynamicRecommendations,
unreadRecommendCount,
// 排队/关闭机制 WS 事件处理
queuePositionData,
pendingCloseRequest,
resolvedInfo,
handleQueuePositionUpdate,
handleConversationResolved,
handlePendingCloseRequest,
handleQuizDiagnosticAnswer,
clearPendingCloseRequest,
clearResolvedInfo,
}
})