kf_message_handler.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. import logging
  2. import time
  3. from typing import Dict
  4. from wecom_api import wecom_api
  5. from config import Config
  6. from cursor_manager import cursor_manager
  7. from rag_service import get_rag_service
  8. logger = logging.getLogger(__name__)
  9. WELCOME_MESSAGE = '''🎉 欢迎使用微信客服!
  10. 我是您的专属客服助手,很高兴为您服务!
  11. 🔹 有任何问题随时咨询
  12. 🔹 输入"帮助"查看功能菜单
  13. 🔹 我们提供7×24小时服务
  14. 请问有什么可以帮助您的吗?'''
  15. class KfMessageHandler:
  16. """微信客服消息处理器"""
  17. def __init__(self):
  18. self.processed_messages = set() # 内存中的消息去重缓存
  19. def process_kf_event(self, token: str, open_kfid: str) -> bool:
  20. """
  21. 处理客服事件,拉取并处理消息(优化版本)
  22. Args:
  23. token: 回调事件中的token
  24. open_kfid: 客服账号ID
  25. Returns:
  26. 处理是否成功
  27. """
  28. try:
  29. # 从持久化存储获取该客服账号的最后cursor
  30. cursor = cursor_manager.get_cursor(open_kfid)
  31. # 确定拉取限制:如果没有cursor(首次或丢失),只拉取最近1条消息作为保底
  32. limit = 1 if not cursor else 100
  33. logger.info(f"开始拉取客服消息 - OpenKfId: {open_kfid}, Cursor: {'有' if cursor else '无'}, Limit: {limit}")
  34. # 拉取消息
  35. result = wecom_api.sync_kf_messages(token, open_kfid, cursor, limit)
  36. if not result:
  37. logger.error("拉取客服消息失败")
  38. return False
  39. msg_list = result.get('msg_list', [])
  40. next_cursor = result.get('next_cursor', '')
  41. has_more = result.get('has_more', 0)
  42. logger.info(f"拉取到 {len(msg_list)} 条客服消息,next_cursor: {'有' if next_cursor else '无'}, has_more: {has_more}")
  43. # 处理每条消息(带去重)
  44. processed_count = 0
  45. new_messages = 0
  46. for msg in msg_list:
  47. msgid = msg.get('msgid', '')
  48. # 检查消息是否已处理(双重检查:内存缓存 + 数据库)
  49. if msgid in self.processed_messages or cursor_manager.is_message_processed(msgid):
  50. logger.debug(f"消息已处理,跳过: {msgid}")
  51. continue
  52. # 处理新消息
  53. if self.process_single_kf_message(msg):
  54. processed_count += 1
  55. new_messages += 1
  56. # 添加到内存缓存
  57. self.processed_messages.add(msgid)
  58. # 限制内存缓存大小
  59. if len(self.processed_messages) > 1000:
  60. # 清理一半的缓存
  61. self.processed_messages = set(list(self.processed_messages)[500:])
  62. # 保存cursor到持久化存储
  63. if next_cursor:
  64. cursor_manager.save_cursor(open_kfid, next_cursor, new_messages)
  65. logger.info(f"已保存新cursor - OpenKfId: {open_kfid}, 新消息数: {new_messages}")
  66. # 如果还有更多消息,继续拉取(但要避免无限循环)
  67. if has_more == 1 and new_messages > 0:
  68. logger.info("还有更多消息,继续拉取...")
  69. return self.process_kf_event(token, open_kfid)
  70. logger.info(f"客服消息处理完成 - 总拉取: {len(msg_list)}, 新处理: {processed_count}")
  71. return True
  72. except Exception as e:
  73. logger.error(f"处理客服事件异常: {e}")
  74. return False
  75. def process_single_kf_message(self, msg: Dict) -> bool:
  76. """
  77. 处理单条客服消息(优化版本)
  78. Args:
  79. msg: 消息数据
  80. Returns:
  81. 处理是否成功
  82. """
  83. try:
  84. msgid = msg.get('msgid', '')
  85. open_kfid = msg.get('open_kfid', '')
  86. external_userid = msg.get('external_userid', '')
  87. send_time = msg.get('send_time', 0)
  88. origin = msg.get('origin', 0) # 3-微信客户发送 4-系统推送事件 5-接待人员发送
  89. servicer_userid = msg.get('servicer_userid', '')
  90. msgtype = msg.get('msgtype', '')
  91. logger.info(f"处理客服消息 - msgid: {msgid}, 类型: {msgtype}, 来源: {origin}, 客户: {external_userid}")
  92. # 检查消息时效性(仅对客户发送的消息进行时效检查)
  93. current_time = int(time.time())
  94. message_age_seconds = current_time - send_time
  95. message_age_minutes = message_age_seconds / 60
  96. # 标记消息为已处理
  97. reply_sent = False
  98. # 只处理微信客户发送的消息
  99. if origin == 3 and external_userid:
  100. # 检查消息是否在10分钟内发送
  101. if message_age_minutes <= 10:
  102. reply_sent = self.handle_customer_message(msg)
  103. logger.info(f"消息已处理,消息年龄:{message_age_minutes:.1f}分钟")
  104. else:
  105. logger.info(f"消息超过10分钟时效限制,不予回复 - msgid: {msgid}, "
  106. f"消息时间: {send_time}, 当前时间: {current_time}, "
  107. f"消息年龄: {message_age_minutes:.1f}分钟")
  108. # 虽然不回复,但仍然标记为已处理,避免重复处理
  109. reply_sent = True
  110. elif origin == 4:
  111. # 处理系统事件
  112. self.handle_system_event(msg)
  113. elif origin == 5:
  114. # 接待人员发送的消息,记录日志
  115. logger.info(f"接待人员 {servicer_userid} 发送消息: {msgtype}")
  116. # 标记消息为已处理
  117. cursor_manager.mark_message_processed(
  118. msgid, open_kfid, external_userid, msgtype, origin, reply_sent
  119. )
  120. return True
  121. except Exception as e:
  122. logger.error(f"处理单条客服消息异常: {e}")
  123. return False
  124. def handle_customer_message(self, msg: Dict) -> bool:
  125. """
  126. 处理客户发送的消息
  127. Returns:
  128. 是否发送了回复
  129. """
  130. try:
  131. msgtype = msg.get('msgtype', '')
  132. external_userid = msg.get('external_userid', '')
  133. open_kfid = msg.get('open_kfid', '')
  134. reply_sent = False
  135. service_state = self.get_service_session_state(external_userid, open_kfid)
  136. if (service_state != 1 and service_state != 0):
  137. # 如果不是智能助手接待状态,则不处理
  138. logger.info(f"非智能助手接待状态,不处理消息 - open_kfid: {open_kfid}, 客户: {external_userid}")
  139. return False
  140. if msgtype == 'text':
  141. # 处理文本消息,使用智能回复生成回复内容
  142. text_data = msg.get('text', {})
  143. content = text_data.get('content', '')
  144. logger.info(f"收到客户文本消息: {content}")
  145. if '转人工' in content:
  146. # 转人工
  147. reply_content = "🤖 收到您的转人工请求,我会尽快转接人工服务。"
  148. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  149. self.update_service_session_state_to_service_pool(external_userid, open_kfid)
  150. else:
  151. # 生成智能回复
  152. reply_content = self.get_smart_reply(content)
  153. # 发送回复
  154. if reply_content:
  155. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  156. elif msgtype in ['image', 'voice', 'video', 'file']:
  157. # 处理多媒体消息,暂不处理,仅作简单回复收到消息
  158. replies = {
  159. 'image': '''📷 收到您发送的图片,请简单描述一下图片内容,我能更好地为您服务!''',
  160. 'voice': '''🎤 收到您的语音消息,感谢您的留言!''',
  161. 'video': '''🎬 收到您发送的视频,感谢分享!''',
  162. 'file': '''📎 收到您发送的文件,我会尽快查看处理。'''
  163. }
  164. reply_content = replies.get(msgtype, '收到您的消息,感谢分享!我会尽快为您处理。')
  165. if reply_content:
  166. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  167. elif msgtype == 'location':
  168. # 处理位置消息,暂不处理,仅作简单回复收到消息
  169. location = msg.get('location', {})
  170. name = location.get('name', '')
  171. address = location.get('address', '')
  172. reply_content = f"📍 收到您分享的位置:{name}\n地址:{address}\n\n感谢分享!如需导航或周边服务,请告诉我具体需求。"
  173. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  174. elif msgtype == 'link':
  175. # 处理链接消息,暂不处理,仅作简单回复收到消息
  176. link = msg.get('link', {})
  177. title = link.get('title', '')
  178. reply_content = f"🔗 收到您分享的链接:{title}\n\n感谢分享!我会查看相关内容。"
  179. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  180. elif msgtype == 'business_card':
  181. # 处理名片消息,暂不处理,仅作简单回复收到消息
  182. reply_content = "👤 收到您的名片,感谢分享联系方式!\n\n如有业务合作需求,我们会及时与您联系。"
  183. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  184. elif msgtype == 'miniprogram':
  185. # 处理小程序消息,暂不处理,仅作简单回复收到消息
  186. mini = msg.get('miniprogram', {})
  187. title = mini.get('title', '')
  188. reply_content = f"📱 收到您分享的小程序:{title}\n\n感谢分享!我会查看相关功能。"
  189. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  190. elif msgtype == 'channels_shop_product':
  191. # 处理视频号商品消息,暂不处理,仅作简单回复收到消息
  192. product = msg.get('channels_shop_product', {})
  193. title = product.get('title', '')
  194. price = product.get('sales_price', '')
  195. reply_content = f"🛍️ 收到您关注的商品:{title}\n价格:{price}分\n\n如需了解更多商品信息或购买咨询,请告诉我!"
  196. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  197. elif msgtype == 'channels_shop_order':
  198. # 处理视频号订单消息,暂不处理,仅作简单回复收到消息
  199. order = msg.get('channels_shop_order', {})
  200. order_id = order.get('order_id', '')
  201. state = order.get('state', '')
  202. reply_content = f"📦 收到您的订单信息:{order_id}\n状态:{state}\n\n如需查询订单详情或有其他问题,请随时联系我!"
  203. reply_sent = self.send_kf_reply(external_userid, open_kfid, reply_content)
  204. return reply_sent
  205. except Exception as e:
  206. logger.error(f"处理客户消息异常: {e}")
  207. return False
  208. def handle_system_event(self, msg: Dict) -> None:
  209. """处理系统事件"""
  210. try:
  211. event_data = msg.get('event', {})
  212. event_type = event_data.get('event_type', '')
  213. logger.info(f"处理系统事件: {event_type}")
  214. if event_type == 'enter_session':
  215. # 用户进入会话事件
  216. self.handle_enter_session_event(event_data)
  217. elif event_type == 'msg_send_fail':
  218. # 消息发送失败事件
  219. self.handle_send_fail_event(event_data)
  220. elif event_type == 'servicer_status_change':
  221. # 接待人员状态变更事件
  222. self.handle_servicer_status_change_event(event_data)
  223. elif event_type == 'session_status_change':
  224. # 会话状态变更事件
  225. self.handle_session_change_event(event_data)
  226. elif event_type == 'user_recall_msg':
  227. # 用户撤回消息事件
  228. self.handle_user_recall_event(event_data)
  229. elif event_type == 'servicer_recall_msg':
  230. # 接待人员撤回消息事件
  231. self.handle_servicer_recall_event(event_data)
  232. except Exception as e:
  233. logger.error(f"处理系统事件异常: {e}")
  234. def handle_enter_session_event(self, event_data: Dict) -> None:
  235. """处理用户进入会话事件"""
  236. try:
  237. external_userid = event_data.get('external_userid', '')
  238. open_kfid = event_data.get('open_kfid', '')
  239. scene = event_data.get('scene', '')
  240. welcome_code = event_data.get('welcome_code', '')
  241. logger.info(f"用户 {external_userid} 进入会话 - 场景: {scene} - 欢迎码: {welcome_code} - open_kfid: {open_kfid}")
  242. # 发送欢迎消息
  243. welcome_msg = WELCOME_MESSAGE
  244. # 使用事件响应消息接口发送欢迎语
  245. wecom_api.send_event_response_message(welcome_code, welcome_msg)
  246. except Exception as e:
  247. logger.error(f"处理进入会话事件异常: {e}")
  248. def handle_send_fail_event(self, event_data: Dict) -> None:
  249. """处理消息发送失败事件,暂不处理,仅做日志记录"""
  250. try:
  251. external_userid = event_data.get('external_userid', '')
  252. fail_msgid = event_data.get('fail_msgid', '')
  253. fail_type = event_data.get('fail_type', 0)
  254. fail_reasons = {
  255. 0: "未知原因",
  256. 1: "客服账号已删除",
  257. 2: "应用已关闭",
  258. 4: "会话已过期,超过48小时",
  259. 5: "会话已关闭",
  260. 6: "超过5条限制",
  261. 7: "未绑定视频号",
  262. 8: "主体未验证",
  263. 9: "未绑定视频号且主体未验证",
  264. 10: "用户拒收"
  265. }
  266. reason = fail_reasons.get(fail_type, "未知原因")
  267. logger.warning(f"消息发送失败 - 用户: {external_userid}, 消息ID: {fail_msgid}, 原因: {reason}")
  268. except Exception as e:
  269. logger.error(f"处理发送失败事件异常: {e}")
  270. def handle_servicer_status_change_event(self, event_data: Dict) -> None:
  271. """处理接待人员状态变更事件,暂不处理,仅做日志记录"""
  272. try:
  273. servicer_userid = event_data.get('servicer_userid', '')
  274. status = event_data.get('status', 0)
  275. open_kfid = event_data.get('open_kfid', '')
  276. logger.info(f"接待人员 {servicer_userid} 状态变更: {status}, open_kfid: {open_kfid}")
  277. except Exception as e:
  278. logger.error(f"处理接待人员状态变更事件异常: {e}")
  279. def handle_session_change_event(self, event_data: Dict) -> None:
  280. """处理会话状态变更,回复欢迎语或者结束语"""
  281. try:
  282. external_userid = event_data.get('external_userid', '')
  283. change_type = event_data.get('change_type', 0)
  284. msg_code = event_data.get('msg_code', '')
  285. change_types = {
  286. 1: "从接待池接入会话",
  287. 2: "转接会话",
  288. 3: "结束会话",
  289. 4: "重新接入已结束/已转接会话"
  290. }
  291. change_text = change_types.get(change_type, "未知变更")
  292. logger.info(f"会话状态变更 - 用户: {external_userid}, 变更: {change_text}")
  293. # 如果有消息码,可以发送相应的回复语或结束语
  294. if msg_code:
  295. if change_type == 1: # 接入会话
  296. response_msg = "您好!我是您的专属客服,很高兴为您服务!有什么可以帮助您的吗?"
  297. elif change_type == 3: # 结束会话
  298. response_msg = "感谢您的咨询!如有其他问题,欢迎随时联系我们。祝您生活愉快!"
  299. else:
  300. response_msg = None
  301. if response_msg:
  302. wecom_api.send_event_response_message(msg_code, response_msg)
  303. except Exception as e:
  304. logger.error(f"处理会话状态变更事件异常: {e}")
  305. def handle_user_recall_event(self, event_data: Dict) -> None:
  306. """处理用户撤回消息事件,暂不处理,仅做日志记录"""
  307. try:
  308. external_userid = event_data.get('external_userid', '')
  309. recall_msgid = event_data.get('recall_msgid', '')
  310. logger.info(f"用户 {external_userid} 撤回消息: {recall_msgid}")
  311. except Exception as e:
  312. logger.error(f"处理用户撤回消息事件异常: {e}")
  313. def handle_servicer_recall_event(self, event_data: Dict) -> None:
  314. """处理接待人员撤回消息事件,暂不处理,仅做日志记录"""
  315. try:
  316. external_userid = event_data.get('external_userid', '')
  317. servicer_userid = event_data.get('servicer_userid', '')
  318. recall_msgid = event_data.get('recall_msgid', '')
  319. logger.info(f"接待人员 {servicer_userid} 撤回消息: {recall_msgid}")
  320. except Exception as e:
  321. logger.error(f"处理接待人员撤回消息事件异常: {e}")
  322. def get_smart_reply(self, content: str) -> str:
  323. """智能回复,使用RAG服务获取回复内容,并记录token消耗情况和参考文档"""
  324. logger.info(f"智能客服回复开始")
  325. try:
  326. # 调用智能客服接口,获取智能回复内容和token消耗情况
  327. response = get_rag_service().query_service(content)
  328. if response.success:
  329. # 记录token使用情况
  330. if response.token_usage:
  331. logger.info(f"💰 本次查询Token使用: 输入{response.token_usage.prompt_tokens}, "
  332. f"输出{response.token_usage.completion_tokens}, "
  333. f"总计{response.token_usage.total_tokens}")
  334. # 记录参考文档
  335. if response.source_documents:
  336. logger.info(f"📚 参考文档: {[doc.doc_name for doc in response.source_documents]}")
  337. return response.content
  338. else:
  339. logger.error(f"RAG查询失败: {response.error_message}")
  340. return "抱歉,我暂时无法理解您的问题,请稍后再试或联系人工客服。"
  341. except Exception as e:
  342. logger.error(f"智能回复处理异常: {e}")
  343. return "抱歉,我暂时无法理解您的问题,请稍后再试或联系人工客服。"
  344. def send_kf_reply(self, external_userid: str, open_kfid: str, content: str) -> bool:
  345. """发送客服回复"""
  346. try:
  347. # 检查回复内容长度
  348. if len(content) > Config.KF_MAX_REPLY_LENGTH:
  349. content = content[:Config.KF_MAX_REPLY_LENGTH-10] + "...(内容过长已截断)"
  350. result = wecom_api.send_kf_text_message(external_userid, open_kfid, content)
  351. if result:
  352. logger.info(f"发送客服回复成功 - 用户: {external_userid}")
  353. return True
  354. else:
  355. logger.error(f"发送客服回复失败 - 用户: {external_userid}")
  356. return False
  357. except Exception as e:
  358. logger.error(f"发送客服回复异常: {e}")
  359. return False
  360. def get_service_session_state(self, external_userid: str, open_kfid: str) -> int:
  361. """获取服务会话状态"""
  362. try:
  363. result = wecom_api.get_service_session_state(external_userid, open_kfid)
  364. if result:
  365. logger.info(f"获取服务会话状态成功 - 用户: {external_userid}")
  366. logger.info(f"服务会话状态: {result}")
  367. return result.get('service_state', -1)
  368. else:
  369. logger.error(f"获取服务会话状态失败 - 用户: {external_userid}")
  370. return -1
  371. except Exception as e:
  372. logger.error(f"获取服务会话状态异常: {e}")
  373. return -1
  374. def update_service_session_state_to_service_pool(self, external_userid: str, open_kfid: str) -> bool:
  375. """更新服务会话状态"""
  376. try:
  377. result = wecom_api.update_service_session_state(external_userid, open_kfid, 2) # 2: 转接会话进入接待池,后续由人工接待
  378. if result:
  379. logger.info(f"更新服务会话状态成功 - 用户: {external_userid}")
  380. return True
  381. else:
  382. logger.error(f"更新服务会话状态失败 - 用户: {external_userid}")
  383. return False
  384. except Exception as e:
  385. logger.error(f"更新服务会话状态异常: {e}")
  386. return False
  387. # 全局客服消息处理器实例
  388. kf_message_handler = KfMessageHandler()