tencent_im.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. """
  2. 腾讯云IM API客户端
  3. 用于获取群聊消息历史,不在MongoDB中冗余存储群消息。
  4. API文档:https://cloud.tencent.com/document/product/269/2738
  5. """
  6. import httpx
  7. import random
  8. from typing import List, Optional
  9. from dotenv import load_dotenv
  10. import os
  11. from TLSSigAPIv2 import TLSSigAPIv2
  12. load_dotenv()
  13. # 腾讯云IM配置
  14. TENCENT_IM_APP_ID = os.getenv("TENCENT_IM_APP_ID")
  15. TENCENT_IM_ADMIN_ID = os.getenv("TENCENT_IM_ADMIN_ID", "administrator")
  16. TENCENT_IM_SECRET_KEY = os.getenv("TENCENT_IM_SECRET_KEY")
  17. class TencentIMClient:
  18. """腾讯云IM API客户端"""
  19. def __init__(self, app_id: str = None, admin_id: str = None, secret_key: str = None):
  20. self.app_id = app_id or TENCENT_IM_APP_ID
  21. self.admin_id = admin_id or TENCENT_IM_ADMIN_ID
  22. self.secret_key = secret_key or TENCENT_IM_SECRET_KEY
  23. if not all([self.app_id, self.admin_id, self.secret_key]):
  24. print("警告:腾讯云IM配置不完整,请检查环境变量")
  25. self.base_url = "https://console.tim.qq.com/v4"
  26. # 初始化 TLSSigAPIv2 实例
  27. if self.app_id and self.secret_key:
  28. self.sig_api = TLSSigAPIv2(int(self.app_id), self.secret_key)
  29. else:
  30. self.sig_api = None
  31. def _generate_usersig(self, expire: int = 86400 * 180) -> str:
  32. """
  33. 生成UserSig(使用腾讯云官方SDK)
  34. Args:
  35. expire: 有效期(秒),默认180天
  36. Returns:
  37. UserSig字符串
  38. """
  39. if not self.sig_api:
  40. raise ValueError("腾讯云IM配置不完整,无法生成UserSig")
  41. return self.sig_api.gen_sig(self.admin_id, expire)
  42. async def get_group_messages(
  43. self,
  44. group_id: str,
  45. req_msg_number: int = 20,
  46. req_msg_seq: int = None
  47. ) -> List[dict]:
  48. """
  49. 获取群消息历史
  50. API: group_open_http_svc/group_msg_get_simple
  51. Args:
  52. group_id: 群ID
  53. req_msg_number: 拉取消息数量(1-20)
  54. req_msg_seq: 翻页标识(上一页返回的最小MsgSeq)
  55. Returns:
  56. 消息列表,每条消息格式:
  57. {
  58. "From_Account": "user123",
  59. "MsgSeq": 12345,
  60. "MsgRandom": 12345,
  61. "MsgTimeStamp": 1609459200,
  62. "MsgBody": [
  63. {
  64. "MsgType": "TIMTextElem",
  65. "MsgContent": {"Text": "消息内容"}
  66. }
  67. ]
  68. }
  69. """
  70. if not self.app_id or not self.secret_key:
  71. print("腾讯云IM配置不完整,返回空消息列表")
  72. return []
  73. try:
  74. # 生成UserSig
  75. user_sig = self._generate_usersig()
  76. # 构造请求URL
  77. random_num = random.randint(0, 4294967295)
  78. url = (
  79. f"{self.base_url}/group_open_http_svc/group_msg_get_simple"
  80. f"?sdkappid={self.app_id}"
  81. f"&identifier={self.admin_id}"
  82. f"&usersig={user_sig}"
  83. f"&random={random_num}"
  84. f"&contenttype=json"
  85. )
  86. # 构造请求体
  87. payload = {
  88. "GroupId": group_id,
  89. "ReqMsgNumber": min(max(1, req_msg_number), 20), # 限制1-20
  90. }
  91. if req_msg_seq is not None:
  92. payload["ReqMsgSeq"] = req_msg_seq
  93. # 发送请求
  94. async with httpx.AsyncClient(timeout=10.0) as client:
  95. response = await client.post(url, json=payload)
  96. response.raise_for_status()
  97. result = response.json()
  98. # 检查返回状态
  99. if result.get("ActionStatus") == "OK":
  100. return result.get("RspMsgList", [])
  101. else:
  102. error_info = result.get("ErrorInfo", "未知错误")
  103. print(f"获取群消息失败: {error_info}")
  104. return []
  105. except Exception as e:
  106. print(f"调用腾讯云IM API失败: {e}")
  107. return []
  108. def parse_message_content(self, msg: dict) -> dict:
  109. """
  110. 解析消息体,提取内容(支持文本、图片、视频、分享)
  111. 过滤规则:
  112. - IsSystemMsg == 1: 系统消息 → 返回None
  113. - IsPlaceMsg != 0: 占位消息 → 返回None
  114. - From_Account == "@TIM#SYSTEM": 系统账号 → 返回None
  115. Args:
  116. msg: 消息对象(从get_group_messages返回)
  117. Returns:
  118. {
  119. "type": "text" | "image" | "video" | "share" | "mixed",
  120. "text": "文本内容",
  121. "images": ["url1", "url2"], # 图片URL列表
  122. "videos": [{"url": "...", "thumb": "..."}], # 视频信息
  123. "share_title": "分享标题"
  124. }
  125. 如果是系统消息,返回None
  126. """
  127. # 1. 过滤系统消息
  128. if msg.get("IsSystemMsg") == 1:
  129. return None
  130. # 2. 过滤占位消息
  131. if msg.get("IsPlaceMsg", 0) != 0:
  132. return None
  133. # 3. 过滤系统账号
  134. if msg.get("From_Account") == "@TIM#SYSTEM":
  135. return None
  136. # 4. 解析消息内容
  137. result = {
  138. "type": "text",
  139. "text": "",
  140. "images": [],
  141. "videos": [],
  142. "share_title": ""
  143. }
  144. msg_body = msg.get("MsgBody", [])
  145. if not msg_body:
  146. return None
  147. for body in msg_body:
  148. msg_type = body.get("MsgType")
  149. content = body.get("MsgContent", {})
  150. if msg_type == "TIMTextElem":
  151. # 文本消息
  152. text = content.get("Text", "")
  153. if text:
  154. result["text"] += text
  155. elif msg_type == "TIMImageElem":
  156. # 图片消息
  157. image_info = content.get("ImageInfoArray", [])
  158. if image_info:
  159. # 取第一个(原图)
  160. result["images"].append(image_info[0].get("URL", ""))
  161. result["type"] = "image" if not result["text"] else "mixed"
  162. elif msg_type == "TIMVideoFileElem":
  163. # 视频消息
  164. video_url = content.get("VideoUrl", "")
  165. thumb_url = content.get("ThumbUrl", "")
  166. if video_url:
  167. result["videos"].append({
  168. "url": video_url,
  169. "thumb": thumb_url
  170. })
  171. result["type"] = "video" if not result["text"] else "mixed"
  172. elif msg_type == "TIMCustomElem":
  173. # 自定义消息(链接分享等)
  174. data_str = content.get("Data", "")
  175. try:
  176. data = json.loads(data_str)
  177. title = data.get("title", "")
  178. if title:
  179. result["share_title"] = title
  180. result["type"] = "share" if not result["text"] else "mixed"
  181. except:
  182. pass
  183. # 如果没有任何有效内容,返回None
  184. if not result["text"] and not result["images"] and not result["videos"] and not result["share_title"]:
  185. return None
  186. return result
  187. def parse_message_text(self, msg: dict) -> str:
  188. """
  189. 解析消息体,提取文本内容(兼容旧接口)
  190. Args:
  191. msg: 消息对象(从get_group_messages返回)
  192. Returns:
  193. 文本内容(多个文本元素会拼接)
  194. """
  195. parsed = self.parse_message_content(msg)
  196. if not parsed:
  197. return ""
  198. return parsed.get("text", "")
  199. async def get_group_context(
  200. self,
  201. group_id: str,
  202. max_messages: int = 20,
  203. ai_account_map: dict = None
  204. ) -> List[dict]:
  205. """
  206. 获取群聊上下文(最近N条消息)
  207. Args:
  208. group_id: 群ID
  209. max_messages: 最多获取多少条消息
  210. ai_account_map: AI账号映射 {"6537328": "AI企业家", "6587745": "AI知心姐姐"}
  211. Returns:
  212. 消息列表,格式化后的结构:
  213. [
  214. {
  215. "user_id": "6485208", # 或 "[AI企业家]"
  216. "content": "消息文本",
  217. "images": ["url1"],
  218. "videos": [{"url": "...", "thumb": "..."}],
  219. "share_title": "分享标题",
  220. "timestamp": 1790827391,
  221. "msg_seq": 482
  222. },
  223. ...
  224. ]
  225. """
  226. if ai_account_map is None:
  227. ai_account_map = {
  228. "6537328": "AI企业家",
  229. "6587745": "AI知心姐姐"
  230. }
  231. raw_messages = await self.get_group_messages(group_id, max_messages)
  232. # 格式化消息(过滤掉系统消息)
  233. formatted = []
  234. for msg in raw_messages:
  235. parsed = self.parse_message_content(msg)
  236. if parsed: # 只保留有有效内容的消息
  237. user_id = msg.get("From_Account", "")
  238. # 如果是AI账号,标注为AI角色
  239. display_id = ai_account_map.get(user_id, user_id)
  240. if display_id != user_id:
  241. display_id = f"[{display_id}]"
  242. formatted.append({
  243. "user_id": display_id,
  244. "content": parsed.get("text", ""),
  245. "images": parsed.get("images", []),
  246. "videos": parsed.get("videos", []),
  247. "share_title": parsed.get("share_title", ""),
  248. "timestamp": msg.get("MsgTimeStamp", 0),
  249. "msg_seq": msg.get("MsgSeq", 0),
  250. })
  251. # 按时间升序排列(最早的在前)
  252. formatted.sort(key=lambda x: x["timestamp"])
  253. return formatted
  254. # 全局实例
  255. im_client = TencentIMClient()