tencent_im.py 10 KB

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