wecom_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. """
  2. 企业微信API调用模块
  3. 支持客服消息的读取和发送功能
  4. 作者:开发工程师
  5. 日期:2025年01月
  6. """
  7. import time
  8. import logging
  9. import requests
  10. from typing import Dict, Optional
  11. from config import Config
  12. logger = logging.getLogger(__name__)
  13. class WeComAPI:
  14. """企业微信API调用类"""
  15. def __init__(self):
  16. self.corp_id = Config.CORP_ID
  17. self.app_secret = Config.APP_SECRET
  18. self.base_url = "https://qyapi.weixin.qq.com/cgi-bin"
  19. self._kf_access_token = None
  20. self._kf_token_expires_at = 0
  21. def get_kf_access_token(self) -> Optional[str]:
  22. """获取客服专用access_token"""
  23. # 检查token是否过期
  24. if self._kf_access_token and time.time() < self._kf_token_expires_at:
  25. return self._kf_access_token
  26. # 配置客服密钥
  27. secret = self.app_secret
  28. if not secret:
  29. logger.error("缺少APP_SECRET配置,无法获取客服access_token")
  30. return None
  31. url = f"{self.base_url}/gettoken"
  32. params = {
  33. 'corpid': self.corp_id,
  34. 'corpsecret': secret
  35. }
  36. try:
  37. response = requests.get(url, params=params, timeout=10)
  38. response.raise_for_status()
  39. data = response.json()
  40. if data.get('errcode') == 0:
  41. self._kf_access_token = data.get('access_token')
  42. expires_in = data.get('expires_in', 7200)
  43. self._kf_token_expires_at = time.time() + expires_in - 300 # 提前5分钟过期
  44. logger.info(f"获取客服access_token成功,有效期: {expires_in}秒")
  45. return self._kf_access_token
  46. else:
  47. logger.error(f"获取客服access_token失败: {data.get('errmsg')}")
  48. return None
  49. except Exception as e:
  50. logger.error(f"获取客服access_token异常: {e}")
  51. return None
  52. def sync_kf_messages(self, token: str, open_kfid: str = "", cursor: str = "", limit: int = 1000) -> Optional[Dict]:
  53. """
  54. 读取客服消息
  55. Args:
  56. token: 回调事件返回的token字段
  57. open_kfid: 指定拉取某个客服账号的消息
  58. cursor: 上一次调用时返回的next_cursor
  59. limit: 期望请求的数据量,默认1000
  60. Returns:
  61. 消息列表数据
  62. """
  63. access_token = self.get_kf_access_token()
  64. if not access_token:
  65. return None
  66. url = f"{self.base_url}/kf/sync_msg"
  67. params = {'access_token': access_token}
  68. data = {
  69. 'token': token,
  70. 'limit': limit,
  71. 'voice_format': 0 # 0-Amr 1-Silk
  72. }
  73. if cursor:
  74. data['cursor'] = cursor
  75. if open_kfid:
  76. data['open_kfid'] = open_kfid
  77. try:
  78. response = requests.post(url, params=params, json=data, timeout=10)
  79. response.raise_for_status()
  80. result = response.json()
  81. if result.get('errcode') == 0:
  82. logger.info(f"读取客服消息成功,获取到 {len(result.get('msg_list', []))} 条消息")
  83. return result
  84. else:
  85. logger.error(f"读取客服消息失败: {result.get('errmsg')}")
  86. return None
  87. except Exception as e:
  88. logger.error(f"读取客服消息异常: {e}")
  89. return None
  90. def send_kf_message(self, touser: str, open_kfid: str, msgtype: str, content: Dict, msgid: str = None) -> Optional[Dict]:
  91. """
  92. 发送客服消息
  93. Args:
  94. touser: 接收消息的客户UserID
  95. open_kfid: 发送消息的客服帐号ID
  96. msgtype: 消息类型 (text, image, voice, video, file, link, miniprogram, msgmenu, location)
  97. content: 消息内容
  98. msgid: 指定消息ID
  99. Returns:
  100. 发送结果
  101. """
  102. access_token = self.get_kf_access_token()
  103. if not access_token:
  104. return None
  105. url = f"{self.base_url}/kf/send_msg"
  106. params = {'access_token': access_token}
  107. data = {
  108. 'touser': touser,
  109. 'open_kfid': open_kfid,
  110. 'msgtype': msgtype
  111. }
  112. # 添加消息内容
  113. data[msgtype] = content
  114. if msgid:
  115. data['msgid'] = msgid
  116. try:
  117. response = requests.post(url, params=params, json=data, timeout=10)
  118. response.raise_for_status()
  119. result = response.json()
  120. if result.get('errcode') == 0:
  121. logger.info(f"发送客服消息成功,msgid: {result.get('msgid')}")
  122. return result
  123. else:
  124. logger.error(f"发送客服消息失败: {result.get('errmsg')}")
  125. return None
  126. except Exception as e:
  127. logger.error(f"发送客服消息异常: {e}")
  128. return None
  129. def send_kf_text_message(self, touser: str, open_kfid: str, content: str, msgid: str = None) -> Optional[Dict]:
  130. """发送文本消息"""
  131. text_content = {'content': content}
  132. return self.send_kf_message(touser, open_kfid, 'text', text_content, msgid)
  133. def send_kf_image_message(self, touser: str, open_kfid: str, media_id: str, msgid: str = None) -> Optional[Dict]:
  134. """发送图片消息"""
  135. image_content = {'media_id': media_id}
  136. return self.send_kf_message(touser, open_kfid, 'image', image_content, msgid)
  137. def send_kf_link_message(self, touser: str, open_kfid: str, title: str, desc: str, url: str, thumb_media_id: str, msgid: str = None) -> Optional[Dict]:
  138. """发送链接消息"""
  139. link_content = {
  140. 'title': title,
  141. 'desc': desc,
  142. 'url': url,
  143. 'thumb_media_id': thumb_media_id
  144. }
  145. return self.send_kf_message(touser, open_kfid, 'link', link_content, msgid)
  146. def send_event_response_message(self, code: str, content: str) -> Optional[Dict]:
  147. """
  148. 发送事件响应消息(如欢迎语、结束语)
  149. Args:
  150. code: 事件响应码
  151. msgtype: 消息类型 (text, msgmenu)
  152. content: 消息内容
  153. Returns:
  154. 发送结果
  155. """
  156. access_token = self.get_kf_access_token()
  157. if not access_token:
  158. return None
  159. url = f"{self.base_url}/kf/send_msg_on_event"
  160. params = {'access_token': access_token}
  161. data = {
  162. 'code': code,
  163. 'msgtype': 'text',
  164. 'text': {'content': content}
  165. }
  166. try:
  167. response = requests.post(url, params=params, json=data, timeout=10)
  168. response.raise_for_status()
  169. result = response.json()
  170. if result.get('errcode') == 0:
  171. logger.info(f"发送事件响应消息成功,code: {code}")
  172. return result
  173. else:
  174. logger.error(f"发送事件响应消息失败: {result.get('errmsg')}")
  175. return None
  176. except Exception as e:
  177. logger.error(f"发送事件响应消息异常: {e}")
  178. return None
  179. def get_service_session_state(self, external_userid: str, open_kfid: str) -> Optional[Dict]:
  180. """
  181. 获取会话状态
  182. Args:
  183. open_kfid: 客服帐号ID
  184. external_userid: 用户的external_userid
  185. Returns:
  186. 会话状态数据,包含以下字段:
  187. - service_state: 会话状态 (0: 未接待, 1: 由智能助手接待, 2: 接待池等待中, 3: 人工接待, 4: 用户已确认接待结束)
  188. - service_userid: 接待客服的userid (当 service_state 为3时返回)
  189. - service_session_id: 会话ID
  190. """
  191. access_token = self.get_kf_access_token()
  192. if not access_token:
  193. return None
  194. url = f"{self.base_url}/kf/service_state/get"
  195. params = {'access_token': access_token}
  196. data = {
  197. 'open_kfid': open_kfid,
  198. 'external_userid': external_userid
  199. }
  200. try:
  201. response = requests.post(url, params=params, json=data, timeout=10)
  202. response.raise_for_status()
  203. result = response.json()
  204. if result.get('errcode') == 0:
  205. logger.info(f"获取会话状态成功,状态: {result.get('service_state')}")
  206. return result
  207. else:
  208. logger.error(f"获取会话状态失败: {result.get('errmsg')}")
  209. return None
  210. except Exception as e:
  211. logger.error(f"获取会话状态异常: {e}")
  212. return None
  213. def update_service_session_state(self, external_userid: str, open_kfid: str, service_state: int, service_userid: str = None) -> Optional[Dict]:
  214. """
  215. 变更会话状态
  216. Args:
  217. open_kfid: 客服帐号ID
  218. external_userid: 用户的external_userid
  219. service_state: 要变更的会话状态 (0: 结束接待, 1: 开启由智能助手接待, 2: 进入接待池由人工接待)
  220. service_userid: 接待客服的userid (当service_state为3时必填)
  221. Returns:
  222. 变更结果
  223. """
  224. access_token = self.get_kf_access_token()
  225. if not access_token:
  226. return None
  227. url = f"{self.base_url}/kf/service_state/trans"
  228. params = {'access_token': access_token}
  229. data = {
  230. 'open_kfid': open_kfid,
  231. 'external_userid': external_userid,
  232. 'service_state': service_state
  233. }
  234. # 当状态为3(开启由人工接待)时,必须指定接待人
  235. if service_state == 3:
  236. if not service_userid:
  237. logger.error("变更会话状态失败: service_state为3时必须指定service_userid")
  238. return None
  239. data['service_userid'] = service_userid
  240. try:
  241. logger.info(f"变更会话状态请求: {data}")
  242. logger.info(f"变更会话状态请求: {url}")
  243. logger.info(f"变更会话状态请求: {params}")
  244. response = requests.post(url, params=params, json=data, timeout=10)
  245. response.raise_for_status()
  246. result = response.json()
  247. if result.get('errcode') == 0:
  248. logger.info(f"变更会话状态成功,状态: {service_state}")
  249. return result
  250. else:
  251. logger.error(f"变更会话状态失败: {result.get('errmsg')}")
  252. return None
  253. except Exception as e:
  254. logger.error(f"变更会话状态异常: {e}")
  255. return None
  256. # 全局API实例
  257. wecom_api = WeComAPI()