v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化
This commit is contained in:
+121
-1
@@ -18,11 +18,16 @@
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
from app.services.ws_manager import manager as ws_manager
|
||||
from app.services.cache_service import cache_service
|
||||
from app.database import _get_session_factory
|
||||
from app.models.conversation import Conversation
|
||||
from app.models.message import Message
|
||||
from app.tasks.h5_ai_task import process_h5_ai_reply
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -212,6 +217,91 @@ async def websocket_endpoint(
|
||||
# H5员工 WebSocket 端点
|
||||
# ==========================================================================
|
||||
|
||||
async def _handle_option_select(
|
||||
conversation_id: str,
|
||||
employee_id: str,
|
||||
option_label: str,
|
||||
):
|
||||
"""处理员工点击 AI 选项按钮的后端逻辑(v2.0 新增)。
|
||||
|
||||
做什么:
|
||||
1. 在 DB 中存储员工的选项选择为一条 employee 消息
|
||||
2. 广播该消息给坐席端(让坐席看到员工选了什么)
|
||||
3. 触发 process_h5_ai_reply() → Dify 接收选项文本作为用户消息 → 返回下一轮 AI 回复
|
||||
|
||||
为什么:前端 sendOptionSelect() 通过 WS 发送 option_select 消息,
|
||||
后端必须接收并触发 AI 回复,否则用户点击选项后无响应。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
employee_id: 员工企微 UserID
|
||||
option_label: 选项的显示文本(如"企微密码"),作为用户消息发给 Dify
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
async with _get_session_factory()() as db:
|
||||
# 1. 查找会话(获取 dify_conversation_id 用于多轮上下文)
|
||||
conversation = await db.get(Conversation, conversation_id)
|
||||
if not conversation:
|
||||
logger.warning(f"option_select: 会话不存在 {conversation_id}")
|
||||
return
|
||||
|
||||
# 2. 存储员工消息(选项选择作为文本消息)
|
||||
emp_msg = Message(
|
||||
conversation_id=conversation_id,
|
||||
sender_type="employee",
|
||||
sender_id=employee_id,
|
||||
sender_name="", # 前端会从 employeeStore 补全
|
||||
content=option_label,
|
||||
msg_type="text",
|
||||
is_read=False,
|
||||
)
|
||||
db.add(emp_msg)
|
||||
await db.flush()
|
||||
|
||||
# 更新会话时间
|
||||
conversation.updated_at = datetime.now()
|
||||
db.add(conversation)
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
|
||||
# 3. 广播给坐席端(让坐席看到员工的选择)
|
||||
try:
|
||||
await ws_manager.broadcast({
|
||||
"type": "new_message",
|
||||
"data": {
|
||||
"conversation_id": str(conversation_id),
|
||||
"message_id": str(emp_msg.id),
|
||||
"sender_type": "employee",
|
||||
"sender_id": employee_id,
|
||||
"content": option_label,
|
||||
"msg_type": "text",
|
||||
},
|
||||
})
|
||||
except Exception as ws_err:
|
||||
logger.warning(f"option_select: WS 广播坐席失败: {ws_err}")
|
||||
|
||||
# 4. 触发 AI 回复(异步后台任务,不阻塞)
|
||||
# dify_conversation_id 从 conversation 对象获取(保持多轮上下文)
|
||||
asyncio.create_task(
|
||||
process_h5_ai_reply(
|
||||
conversation_id=conversation_id,
|
||||
employee_id=employee_id,
|
||||
content=option_label,
|
||||
dify_conversation_id=conversation.dify_conversation_id,
|
||||
msg_type="text",
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
f"option_select 已触发 AI 回复: conv={conversation_id}, "
|
||||
f"option={option_label}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"option_select 处理异常: {e}", exc_info=True)
|
||||
|
||||
|
||||
@router.websocket("/ws/h5/{employee_id}")
|
||||
async def h5_websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
@@ -314,7 +404,7 @@ async def h5_websocket_endpoint(
|
||||
|
||||
try:
|
||||
# 消息接收循环
|
||||
# H5员工端目前只发送心跳 ping,不需要发送 typing 等事件
|
||||
# H5员工端发送心跳 ping 和 option_select(选项按钮点击)
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
|
||||
@@ -323,6 +413,36 @@ async def h5_websocket_endpoint(
|
||||
await websocket.send_json({"type": "pong"})
|
||||
logger.debug(f"H5 WebSocket 心跳: employee_id={employee_id}")
|
||||
|
||||
# v2.0: 处理选项按钮点击(option_select)
|
||||
# 做什么:员工点击 AI 结构化消息中的选项按钮后,前端通过 WS 发送 option_select
|
||||
# 后端接收后触发 AI 回复流程(与普通发消息等效),实现交互式排查闭环
|
||||
elif data.get("type") == "option_select":
|
||||
option_data = data.get("data", {})
|
||||
conv_id = option_data.get("conversation_id")
|
||||
option_label = option_data.get("option_label", "")
|
||||
option_value = option_data.get("option_value", "")
|
||||
|
||||
if conv_id and option_label:
|
||||
logger.info(
|
||||
f"H5 WS option_select: employee={employee_id}, "
|
||||
f"conv={conv_id}, option={option_value}"
|
||||
)
|
||||
# 异步触发 AI 回复(不阻塞 WS 循环)
|
||||
# process_h5_ai_reply 内部创建独立 DB session,
|
||||
# dify_conversation_id 传 None 时会从 conversation 对象回退读取
|
||||
asyncio.create_task(
|
||||
_handle_option_select(
|
||||
conversation_id=conv_id,
|
||||
employee_id=employee_id,
|
||||
option_label=option_label,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"H5 WS option_select 数据不完整: employee={employee_id}, "
|
||||
f"conv_id={conv_id}, label={option_label}"
|
||||
)
|
||||
|
||||
else:
|
||||
logger.debug(
|
||||
f"H5 WebSocket 收到未知消息: employee_id={employee_id}, "
|
||||
|
||||
Reference in New Issue
Block a user