kfid路由窗口: 5类别映射企微客服链接(ROUTING_TARGETS配置化,弃用business_contacts个人名片)+ send_service_card新函数 + ContactCard支持service_url点击打开客服会话(行政→机票酒店前台/人力→共享服务/财务→报销服务台/法务→行政法务/物业→物业服务)

This commit is contained in:
Simon
2026-07-18 11:24:10 +08:00
parent 28bc4bda3b
commit 7886f2f523
3 changed files with 279 additions and 25 deletions
+243
View File
@@ -35,6 +35,61 @@ from app.services.ws_manager import manager as ws_manager
logger = logging.getLogger(__name__)
# =============================================================================
# 路由目标配置(v4.0 2026-07-18:企微客服 kfid 窗口,替代个人名片)
# =============================================================================
# 说明:用户提供的「公共咨询统一联系窗口」清单(企微客服 kfid 链接)。
# 优势:人员变动不影响路由(客服窗口由团队维护),卡片点击直达客服会话。
# 原 business_contacts 个人名片方案弃用(表保留,路由逻辑不再读表)。
#
# 映射规则:Dify 输出的 business_category(5 类别) → 清单中最贴近的服务窗口。
# - 行政(打印机/复印机/保洁/名片印刷/行政杂事)→ 机票酒店前台(行政前台受理)
# - 人力资源(工牌/考勤/入离职/社保)→ 人力资源共享服务咨询
# - 财务(报销/发票/工资/付款)→ 总部报销服务台
# - 法务(合同/协议/盖章/律师)→ 行政法务团队
# - 行政-物业(空调/灯/门禁卡/车位/物业维修)→ 物业服务
# =============================================================================
ROUTING_TARGETS: dict[str, dict[str, str]] = {
"行政": {
"service_name": "机票酒店前台",
"description": "受理和咨询快递、访客、失物招领及行政办公相关事宜",
"url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAV00zNJGfmuA0aG1c2qCDnQ",
},
"人力资源": {
"service_name": "人力资源共享服务咨询",
"description": "咨询和处理人力资源相关问题(工牌/考勤/入离职/社保公积金等)",
"url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAvSRL5i5b_Xia8vCmFc2gRw",
},
"财务": {
"service_name": "总部报销服务台",
"description": "咨询和处理总部报销系统相关问题",
"url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAG4HC1zWJtPuALPKl2X6jcw",
},
"法务": {
"service_name": "行政法务团队",
"description": "可咨询合同、合规、劳动争议、法律案件纠纷、外部检查、其他法律问题",
"url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAABF746pGAY5WZ0mOTP6kGKA",
},
"行政-物业": {
"service_name": "物业服务",
"description": "咨询和处理物业相关问题(空调/照明/门禁卡/车位/维修等)",
"url": "https://work.weixin.qq.com/nl/innerkfid/ikfCtcYBwAAUtkMyOToCZqe42ZBDupVEQ",
},
}
def get_routing_target(business_category: str) -> Optional[dict[str, str]]:
"""按业务类别获取路由目标窗口配置(kfid 客服链接)。
Args:
business_category: Dify 输出的业务类别(行政/人力资源/财务/法务/行政-物业)
Returns:
dict: {service_name, description, url};未配置返回 None
"""
return ROUTING_TARGETS.get(business_category)
# =============================================================================
# 共享 httpx 客户端(v4.0 P1-2:修复每次调用新建连接的泄漏问题)
# =============================================================================
@@ -240,6 +295,194 @@ async def get_contact_by_category(db, category: str) -> Optional[BusinessContact
# 名片三段式发送
# =============================================================================
async def send_service_card(
db,
conversation: Conversation,
employee_id: str,
target: dict[str, str],
reason: str,
business_category: str,
routing_confidence: float,
) -> None:
"""发送服务窗口卡片消息(v4.0 kfid 模式:路由文本 → contact_card → 系统提示)。
与 send_contact_card 的差异:
- 数据来源:ROUTING_TARGETS 配置(kfid 客服窗口),非 business_contacts 表
- extra_data.contact 构造为窗口信息(name=服务名, responsibility=描述, service_url=kfid链接)
- 前端 ContactCard 检测 service_url 存在时,点击改为打开 kfid 客服会话
Args:
db: 异步 DB session
conversation: 当前会话对象
employee_id: 员工企微 UserID
target: get_routing_target 返回的窗口配置 {service_name, description, url}
reason: 路由说明文本
business_category: 业务类别
routing_confidence: 路由置信度
"""
# 构造窗口名片数据(字段对齐前端 ContactCard 的 ContactInfo 接口)
contact_data: dict[str, Any] = {
"id": 0,
"name": target["service_name"],
"gender": "",
"department": "企业微信-通讯录-员工服务",
"position": "公共咨询服务窗口",
"responsibility": target["description"],
"extension": "",
"service_area": "",
"wecom_userid": "", # kfid 模式无个人 userid
"avatar_url": "",
"business_category": business_category,
"service_url": target["url"], # 前端检测此字段 → 点击打开链接
}
extra_data: dict[str, Any] = {
"contact": contact_data,
"routing_reason": reason,
"business_category": business_category,
"routing_confidence": routing_confidence,
}
# === 1. 路由说明文本消息 ===
routing_text_msg = Message(
conversation_id=conversation.id,
sender_type="ai",
sender_id="ai_bot",
sender_name="Duckula(达寇拉)",
content=reason,
msg_type="text",
is_read=True,
)
db.add(routing_text_msg)
await db.flush()
await ws_manager.broadcast_to_employees([employee_id], {
"type": "ai_reply",
"data": {
"message_id": str(routing_text_msg.id),
"conversation_id": str(conversation.id),
"sender_type": "ai",
"sender_id": "ai_bot",
"sender_name": "Duckula(达寇拉)",
"content": reason,
"msg_type": "text",
"is_guidance": False,
"ai_reply_count": conversation.ai_substantive_reply_count,
"can_call_agent": conversation.ai_substantive_reply_count >= 3,
"conversation_status": conversation.status,
},
})
try:
await ws_manager.broadcast({
"type": "new_message",
"data": {
"conversation_id": str(conversation.id),
"message_id": str(routing_text_msg.id),
"sender_type": "ai",
"sender_id": "ai_bot",
"sender_name": "Duckula(达寇拉)",
"content": reason,
"msg_type": "text",
},
})
except Exception as ws_err:
logger.warning(f"路由文本 WS 广播给坐席失败: {ws_err}")
# === 2. contact_card 窗口卡片消息 ===
service_card_msg = Message(
conversation_id=conversation.id,
sender_type="ai",
sender_id="ai_bot",
sender_name="Duckula(达寇拉)",
content=f"为您推荐{business_category}服务窗口:{target['service_name']}",
msg_type="contact_card",
extra_data=extra_data,
is_read=True,
)
db.add(service_card_msg)
await db.flush()
await ws_manager.broadcast_to_employees([employee_id], {
"type": "ai_reply",
"data": {
"message_id": str(service_card_msg.id),
"conversation_id": str(conversation.id),
"sender_type": "ai",
"sender_id": "ai_bot",
"sender_name": "Duckula(达寇拉)",
"content": f"为您推荐{business_category}服务窗口:{target['service_name']}",
"msg_type": "contact_card",
"extra_data": extra_data,
"is_guidance": False,
"ai_reply_count": conversation.ai_substantive_reply_count,
"can_call_agent": conversation.ai_substantive_reply_count >= 3,
"conversation_status": conversation.status,
},
})
try:
await ws_manager.broadcast({
"type": "new_message",
"data": {
"conversation_id": str(conversation.id),
"message_id": str(service_card_msg.id),
"sender_type": "ai",
"sender_id": "ai_bot",
"sender_name": "Duckula(达寇拉)",
"content": f"为您推荐{business_category}服务窗口:{target['service_name']}",
"msg_type": "contact_card",
"extra_data": extra_data,
},
})
except Exception as ws_err:
logger.warning(f"窗口卡片 WS 广播给坐席失败: {ws_err}")
# === 3. 系统提示消息 ===
hint_msg = Message(
conversation_id=conversation.id,
sender_type="system",
sender_id="system",
sender_name="系统",
content="点击上方卡片可直接联系服务窗口(企微客服会话)",
msg_type="system",
is_read=True,
)
db.add(hint_msg)
await db.flush()
await ws_manager.broadcast_to_employees([employee_id], {
"type": "ai_reply",
"data": {
"message_id": str(hint_msg.id),
"conversation_id": str(conversation.id),
"sender_type": "system",
"sender_id": "system",
"sender_name": "系统",
"content": "点击上方卡片可直接联系服务窗口(企微客服会话)",
"msg_type": "system",
"is_guidance": False,
"ai_reply_count": conversation.ai_substantive_reply_count,
"can_call_agent": conversation.ai_substantive_reply_count >= 3,
"conversation_status": conversation.status,
},
})
try:
await ws_manager.broadcast({
"type": "new_message",
"data": {
"conversation_id": str(conversation.id),
"message_id": str(hint_msg.id),
"sender_type": "system",
"sender_id": "system",
"sender_name": "系统",
"content": "点击上方卡片可直接联系服务窗口(企微客服会话)",
"msg_type": "system",
},
})
except Exception as ws_err:
logger.warning(f"系统提示 WS 广播给坐席失败: {ws_err}")
await db.commit()
async def send_contact_card(
db,
conversation: Conversation,