| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- """
- 企业微信API调用模块
- 支持客服消息的读取和发送功能
- 作者:开发工程师
- 日期:2025年01月
- """
- import time
- import logging
- import requests
- from typing import Dict, Optional
- from config import Config
- logger = logging.getLogger(__name__)
- class WeComAPI:
- """企业微信API调用类"""
-
- def __init__(self):
- self.corp_id = Config.CORP_ID
- self.app_secret = Config.APP_SECRET
- self.base_url = "https://qyapi.weixin.qq.com/cgi-bin"
- self._kf_access_token = None
- self._kf_token_expires_at = 0
-
-
- def get_kf_access_token(self) -> Optional[str]:
- """获取客服专用access_token"""
- # 检查token是否过期
- if self._kf_access_token and time.time() < self._kf_token_expires_at:
- return self._kf_access_token
-
- # 配置客服密钥
- secret = self.app_secret
- if not secret:
- logger.error("缺少APP_SECRET配置,无法获取客服access_token")
- return None
-
- url = f"{self.base_url}/gettoken"
- params = {
- 'corpid': self.corp_id,
- 'corpsecret': secret
- }
-
- try:
- response = requests.get(url, params=params, timeout=10)
- response.raise_for_status()
- data = response.json()
-
- if data.get('errcode') == 0:
- self._kf_access_token = data.get('access_token')
- expires_in = data.get('expires_in', 7200)
- self._kf_token_expires_at = time.time() + expires_in - 300 # 提前5分钟过期
- logger.info(f"获取客服access_token成功,有效期: {expires_in}秒")
- return self._kf_access_token
- else:
- logger.error(f"获取客服access_token失败: {data.get('errmsg')}")
- return None
-
- except Exception as e:
- logger.error(f"获取客服access_token异常: {e}")
- return None
-
- def sync_kf_messages(self, token: str, open_kfid: str = "", cursor: str = "", limit: int = 1000) -> Optional[Dict]:
- """
- 读取客服消息
-
- Args:
- token: 回调事件返回的token字段
- open_kfid: 指定拉取某个客服账号的消息
- cursor: 上一次调用时返回的next_cursor
- limit: 期望请求的数据量,默认1000
-
- Returns:
- 消息列表数据
- """
- access_token = self.get_kf_access_token()
- if not access_token:
- return None
-
- url = f"{self.base_url}/kf/sync_msg"
- params = {'access_token': access_token}
-
- data = {
- 'token': token,
- 'limit': limit,
- 'voice_format': 0 # 0-Amr 1-Silk
- }
-
- if cursor:
- data['cursor'] = cursor
- if open_kfid:
- data['open_kfid'] = open_kfid
-
- try:
- response = requests.post(url, params=params, json=data, timeout=10)
- response.raise_for_status()
- result = response.json()
-
- if result.get('errcode') == 0:
- logger.info(f"读取客服消息成功,获取到 {len(result.get('msg_list', []))} 条消息")
- return result
- else:
- logger.error(f"读取客服消息失败: {result.get('errmsg')}")
- return None
-
- except Exception as e:
- logger.error(f"读取客服消息异常: {e}")
- return None
-
- def send_kf_message(self, touser: str, open_kfid: str, msgtype: str, content: Dict, msgid: str = None) -> Optional[Dict]:
- """
- 发送客服消息
-
- Args:
- touser: 接收消息的客户UserID
- open_kfid: 发送消息的客服帐号ID
- msgtype: 消息类型 (text, image, voice, video, file, link, miniprogram, msgmenu, location)
- content: 消息内容
- msgid: 指定消息ID
-
- Returns:
- 发送结果
- """
- access_token = self.get_kf_access_token()
- if not access_token:
- return None
-
- url = f"{self.base_url}/kf/send_msg"
- params = {'access_token': access_token}
-
- data = {
- 'touser': touser,
- 'open_kfid': open_kfid,
- 'msgtype': msgtype
- }
-
- # 添加消息内容
- data[msgtype] = content
-
- if msgid:
- data['msgid'] = msgid
-
- try:
- response = requests.post(url, params=params, json=data, timeout=10)
- response.raise_for_status()
- result = response.json()
-
- if result.get('errcode') == 0:
- logger.info(f"发送客服消息成功,msgid: {result.get('msgid')}")
- return result
- else:
- logger.error(f"发送客服消息失败: {result.get('errmsg')}")
- return None
-
- except Exception as e:
- logger.error(f"发送客服消息异常: {e}")
- return None
-
- def send_kf_text_message(self, touser: str, open_kfid: str, content: str, msgid: str = None) -> Optional[Dict]:
- """发送文本消息"""
- text_content = {'content': content}
- return self.send_kf_message(touser, open_kfid, 'text', text_content, msgid)
-
- def send_kf_image_message(self, touser: str, open_kfid: str, media_id: str, msgid: str = None) -> Optional[Dict]:
- """发送图片消息"""
- image_content = {'media_id': media_id}
- return self.send_kf_message(touser, open_kfid, 'image', image_content, msgid)
-
- 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]:
- """发送链接消息"""
- link_content = {
- 'title': title,
- 'desc': desc,
- 'url': url,
- 'thumb_media_id': thumb_media_id
- }
- return self.send_kf_message(touser, open_kfid, 'link', link_content, msgid)
-
- def send_event_response_message(self, code: str, content: str) -> Optional[Dict]:
- """
- 发送事件响应消息(如欢迎语、结束语)
-
- Args:
- code: 事件响应码
- msgtype: 消息类型 (text, msgmenu)
- content: 消息内容
-
- Returns:
- 发送结果
- """
- access_token = self.get_kf_access_token()
- if not access_token:
- return None
-
- url = f"{self.base_url}/kf/send_msg_on_event"
- params = {'access_token': access_token}
-
- data = {
- 'code': code,
- 'msgtype': 'text',
- 'text': {'content': content}
- }
-
- try:
- response = requests.post(url, params=params, json=data, timeout=10)
- response.raise_for_status()
- result = response.json()
-
- if result.get('errcode') == 0:
- logger.info(f"发送事件响应消息成功,code: {code}")
- return result
- else:
- logger.error(f"发送事件响应消息失败: {result.get('errmsg')}")
- return None
-
- except Exception as e:
- logger.error(f"发送事件响应消息异常: {e}")
- return None
-
- def get_service_session_state(self, external_userid: str, open_kfid: str) -> Optional[Dict]:
- """
- 获取会话状态
-
- Args:
- open_kfid: 客服帐号ID
- external_userid: 用户的external_userid
-
- Returns:
- 会话状态数据,包含以下字段:
- - service_state: 会话状态 (0: 未接待, 1: 由智能助手接待, 2: 接待池等待中, 3: 人工接待, 4: 用户已确认接待结束)
- - service_userid: 接待客服的userid (当 service_state 为3时返回)
- - service_session_id: 会话ID
- """
- access_token = self.get_kf_access_token()
- if not access_token:
- return None
-
- url = f"{self.base_url}/kf/service_state/get"
- params = {'access_token': access_token}
-
- data = {
- 'open_kfid': open_kfid,
- 'external_userid': external_userid
- }
-
- try:
- response = requests.post(url, params=params, json=data, timeout=10)
- response.raise_for_status()
- result = response.json()
-
- if result.get('errcode') == 0:
- logger.info(f"获取会话状态成功,状态: {result.get('service_state')}")
- return result
- else:
- logger.error(f"获取会话状态失败: {result.get('errmsg')}")
- return None
-
- except Exception as e:
- logger.error(f"获取会话状态异常: {e}")
- return None
-
- def update_service_session_state(self, external_userid: str, open_kfid: str, service_state: int, service_userid: str = None) -> Optional[Dict]:
- """
- 变更会话状态
-
- Args:
- open_kfid: 客服帐号ID
- external_userid: 用户的external_userid
- service_state: 要变更的会话状态 (0: 结束接待, 1: 开启由智能助手接待, 2: 进入接待池由人工接待)
- service_userid: 接待客服的userid (当service_state为3时必填)
-
- Returns:
- 变更结果
- """
- access_token = self.get_kf_access_token()
- if not access_token:
- return None
-
- url = f"{self.base_url}/kf/service_state/trans"
- params = {'access_token': access_token}
-
- data = {
- 'open_kfid': open_kfid,
- 'external_userid': external_userid,
- 'service_state': service_state
- }
-
- # 当状态为3(开启由人工接待)时,必须指定接待人
- if service_state == 3:
- if not service_userid:
- logger.error("变更会话状态失败: service_state为3时必须指定service_userid")
- return None
- data['service_userid'] = service_userid
-
- try:
- logger.info(f"变更会话状态请求: {data}")
- logger.info(f"变更会话状态请求: {url}")
- logger.info(f"变更会话状态请求: {params}")
- response = requests.post(url, params=params, json=data, timeout=10)
- response.raise_for_status()
- result = response.json()
-
- if result.get('errcode') == 0:
- logger.info(f"变更会话状态成功,状态: {service_state}")
- return result
- else:
- logger.error(f"变更会话状态失败: {result.get('errmsg')}")
- return None
-
- except Exception as e:
- logger.error(f"变更会话状态异常: {e}")
- return None
-
- # 全局API实例
- wecom_api = WeComAPI()
|