You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

wechat_channel.py 33KB

8 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
4 달 전
8 달 전
8 달 전
4 달 전
4 달 전
4 달 전
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. # encoding:utf-8
  2. """
  3. wechat channel
  4. """
  5. import io
  6. import json
  7. import os
  8. import threading
  9. import time
  10. import requests
  11. from bridge.context import *
  12. from bridge.reply import *
  13. from channel.chat_channel import ChatChannel
  14. from channel import chat_channel
  15. from channel.wechat.wechat_message import *
  16. from common.expired_dict import ExpiredDict
  17. from common.log import logger
  18. from common.singleton import singleton
  19. from common.time_check import time_checker
  20. from common.utils import convert_webp_to_png
  21. from config import conf, get_appdata_dir
  22. from lib import itchat
  23. from lib.itchat.content import *
  24. from urllib.parse import urlparse
  25. import threading
  26. from common import kafka_helper, redis_helper
  27. from confluent_kafka import Consumer, KafkaException
  28. import json,time,re
  29. import pickle
  30. from datetime import datetime
  31. import oss2
  32. # from common.kafka_client import KafkaClient
  33. @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE, ATTACHMENT, SHARING])
  34. def handler_single_msg(msg):
  35. try:
  36. cmsg = WechatMessage(msg, False)
  37. except NotImplementedError as e:
  38. logger.debug("[WX]single message {} skipped: {}".format(msg["MsgId"], e))
  39. return None
  40. WechatChannel().handle_single(cmsg)
  41. return None
  42. @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE, ATTACHMENT, SHARING], isGroupChat=True)
  43. def handler_group_msg(msg):
  44. try:
  45. cmsg = WechatMessage(msg, True)
  46. except NotImplementedError as e:
  47. logger.debug("[WX]group message {} skipped: {}".format(msg["MsgId"], e))
  48. return None
  49. WechatChannel().handle_group(cmsg)
  50. return None
  51. def _check(func):
  52. def wrapper(self, cmsg: ChatMessage):
  53. msgId = cmsg.msg_id
  54. if msgId in self.receivedMsgs:
  55. logger.info("Wechat message {} already received, ignore".format(msgId))
  56. return
  57. self.receivedMsgs[msgId] = True
  58. create_time = cmsg.create_time # 消息时间戳
  59. if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息
  60. logger.debug("[WX]history message {} skipped".format(msgId))
  61. return
  62. if cmsg.my_msg and not cmsg.is_group:
  63. logger.debug("[WX]my message {} skipped".format(msgId))
  64. return
  65. return func(self, cmsg)
  66. return wrapper
  67. # 可用的二维码生成接口
  68. # https://api.qrserver.com/v1/create-qr-code/?size=400×400&data=https://www.abc.com
  69. # https://api.isoyu.com/qr/?m=1&e=L&p=20&url=https://www.abc.com
  70. def qrCallback(uuid, status, qrcode):
  71. # logger.debug("qrCallback: {} {}".format(uuid,status))
  72. if status == "0":
  73. try:
  74. from PIL import Image
  75. img = Image.open(io.BytesIO(qrcode))
  76. _thread = threading.Thread(target=img.show, args=("QRCode",))
  77. _thread.setDaemon(True)
  78. _thread.start()
  79. except Exception as e:
  80. pass
  81. import qrcode
  82. url = f"https://login.weixin.qq.com/l/{uuid}"
  83. qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url)
  84. qr_api2 = "https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url)
  85. qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url)
  86. qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url)
  87. print("You can also scan QRCode in any website below:")
  88. print(qr_api3)
  89. print(qr_api4)
  90. print(qr_api2)
  91. print(qr_api1)
  92. _send_qr_code([qr_api3, qr_api4, qr_api2, qr_api1])
  93. qr = qrcode.QRCode(border=1)
  94. qr.add_data(url)
  95. qr.make(fit=True)
  96. qr.print_ascii(invert=True)
  97. @singleton
  98. class WechatChannel(ChatChannel):
  99. NOT_SUPPORT_REPLYTYPE = []
  100. def __init__(self):
  101. super().__init__()
  102. self.receivedMsgs = ExpiredDict(conf().get("expires_in_seconds", 3600))
  103. self.auto_login_times = 0
  104. def startup(self):
  105. try:
  106. itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
  107. # login by scan QRCode
  108. hotReload = conf().get("hot_reload", False)
  109. status_path = os.path.join(get_appdata_dir(), "itchat.pkl")
  110. # with open(status_path, 'rb') as file:
  111. # data = pickle.load(file)
  112. # logger.info(data)
  113. itchat.auto_login(
  114. enableCmdQR=2,
  115. hotReload=hotReload,
  116. statusStorageDir=status_path,
  117. qrCallback=qrCallback,
  118. exitCallback=self.exitCallback,
  119. loginCallback=self.loginCallback
  120. )
  121. self.user_id = itchat.instance.storageClass.userName
  122. self.name = itchat.instance.storageClass.nickName
  123. logger.info("Wechat login success, user_id: {}, nickname: {}".format(self.user_id, self.name))
  124. # 创建一个线程来运行 consume_messages
  125. # kafka_thread = threading.Thread(target=consume_messages, args=('47.116.67.214:9092', 'ai-test-group', topic,self.name))
  126. # kafka_client=KafkaClient()
  127. # kafka_thread = threading.Thread(target=consume_wx_messages, args=('47.116.67.214:9092', 'ai-test-group', topic,self.name))
  128. kafka_thread = threading.Thread(target=kafka_helper.kafka_client.consume_messages, args=(wx_messages_process_callback, self.name))
  129. kafka_thread.start()
  130. logger.info("启动kafka")
  131. # 好友定时同步
  132. agent_nickname=self.name
  133. friend_thread =threading.Thread(target=hourly_change_save_friends, args=(agent_nickname,))
  134. friend_thread.start()
  135. # 立刻同步
  136. agent_info=fetch_agent_info(agent_nickname)
  137. agent_tel=agent_info.get("agent_tel",None)
  138. # friends=itchat.get_contact(update=True)[1:]
  139. friends=itchat.get_friends(update=True)[1:]
  140. # logger.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
  141. # logger.info(friends)
  142. # logger.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
  143. save_friends_to_redis(agent_tel,agent_nickname, friends)
  144. logger.info("启动好友同步")
  145. # start message listener
  146. logger.info("启动itchat")
  147. itchat.run()
  148. # 运行kafka
  149. #
  150. except Exception as e:
  151. logger.exception(e)
  152. def exitCallback(self):
  153. print('主动退出')
  154. try:
  155. from common.linkai_client import chat_client
  156. if chat_client.client_id and conf().get("use_linkai"):
  157. print('退出')
  158. _send_logout()
  159. time.sleep(2)
  160. self.auto_login_times += 1
  161. if self.auto_login_times < 100:
  162. chat_channel.handler_pool._shutdown = False
  163. self.startup()
  164. except Exception as e:
  165. pass
  166. def loginCallback(self):
  167. logger.debug("Login success")
  168. print('登录成功')
  169. # 同步
  170. _send_login_success()
  171. # handle_* 系列函数处理收到的消息后构造Context,然后传入produce函数中处理Context和发送回复
  172. # Context包含了消息的所有信息,包括以下属性
  173. # type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
  174. # content 消息内容,如果是TEXT类型,content就是文本内容,如果是VOICE类型,content就是语音文件名,如果是IMAGE_CREATE类型,content就是图片生成命令
  175. # kwargs 附加参数字典,包含以下的key:
  176. # session_id: 会话id
  177. # isgroup: 是否是群聊
  178. # receiver: 需要回复的对象
  179. # msg: ChatMessage消息对象
  180. # origin_ctype: 原始消息类型,语音转文字后,私聊时如果匹配前缀失败,会根据初始消息是否是语音来放宽触发规则
  181. # desire_rtype: 希望回复类型,默认是文本回复,设置为ReplyType.VOICE是语音回复
  182. @time_checker
  183. @_check
  184. def handle_single(self, cmsg: ChatMessage):
  185. # filter system message
  186. if cmsg.other_user_id in ["weixin"]:
  187. return
  188. if cmsg.ctype == ContextType.VOICE:
  189. if conf().get("speech_recognition") != True:
  190. return
  191. logger.debug("[WX]receive voice msg: {}".format(cmsg.content))
  192. elif cmsg.ctype == ContextType.IMAGE:
  193. logger.debug("[WX]receive image msg: {}".format(cmsg.content))
  194. # print(cmsg.content)
  195. file_path = cmsg.content
  196. logger.info(f"on_handle_context: 获取到图片路径 {file_path}")
  197. oss_access_key_id="LTAI5tRTG6pLhTpKACJYoPR5"
  198. oss_access_key_secret="E7dMzeeMxq4VQvLg7Tq7uKf3XWpYfN"
  199. oss_endpoint="http://oss-cn-shanghai.aliyuncs.com"
  200. oss_bucket_name="cow-agent"
  201. oss_image_url = upload_oss(oss_access_key_id, oss_access_key_secret, oss_endpoint, oss_bucket_name, file_path, f'cow/{os.path.basename(file_path)}')
  202. print(f"oss_image_url:{oss_image_url}")
  203. input_content = oss_image_url
  204. input_from_user_nickname = cmsg.from_user_nickname
  205. input_to_user_nickname = cmsg.to_user_nickname
  206. input_wx_content_dialogue_message=[{"type": "image_url", "image_url": {"url": input_content}}]
  207. input_message=dialogue_message(input_from_user_nickname,input_to_user_nickname,input_wx_content_dialogue_message)
  208. kafka_helper.kafka_client.produce_message(input_message)
  209. logger.info("发送对话 %s",input_message)
  210. elif cmsg.ctype == ContextType.PATPAT:
  211. logger.debug("[WX]receive patpat msg: {}".format(cmsg.content))
  212. elif cmsg.ctype == ContextType.TEXT:
  213. logger.debug("[WX]receive text msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
  214. # content = cmsg.content # 消息内容
  215. # from_user_nickname = cmsg.from_user_nickname # 发送方昵称
  216. # to_user_nickname = cmsg.to_user_nickname # 接收方昵称
  217. # wx_content_dialogue_message=[{"type": "text", "text": content}]
  218. # message=dialogue_message(from_user_nickname,to_user_nickname,wx_content_dialogue_message)
  219. # kafka_helper.kafka_client.produce_message(message)
  220. # logger.info("发送对话 %s", json.dumps(message, ensure_ascii=False))
  221. input_content = cmsg.content
  222. input_from_user_nickname = cmsg.from_user_nickname
  223. input_to_user_nickname = cmsg.to_user_nickname
  224. input_wx_content_dialogue_message=[{"type": "text", "text": input_content}]
  225. input_message=dialogue_message(input_from_user_nickname,input_to_user_nickname,input_wx_content_dialogue_message)
  226. kafka_helper.kafka_client.produce_message(input_message)
  227. logger.info("发送对话 %s",input_message)
  228. else:
  229. logger.debug("[WX]receive msg: {}, cmsg={}".format(cmsg.content, cmsg))
  230. context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg)
  231. if context:
  232. self.produce(context)
  233. @time_checker
  234. @_check
  235. def handle_group(self, cmsg: ChatMessage):
  236. if cmsg.ctype == ContextType.VOICE:
  237. if conf().get("group_speech_recognition") != True:
  238. return
  239. logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content))
  240. elif cmsg.ctype == ContextType.IMAGE:
  241. logger.debug("[WX]receive image for group msg: {}".format(cmsg.content))
  242. elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.ACCEPT_FRIEND, ContextType.EXIT_GROUP]:
  243. logger.debug("[WX]receive note msg: {}".format(cmsg.content))
  244. elif cmsg.ctype == ContextType.TEXT:
  245. # logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
  246. pass
  247. elif cmsg.ctype == ContextType.FILE:
  248. logger.debug(f"[WX]receive attachment msg, file_name={cmsg.content}")
  249. else:
  250. logger.debug("[WX]receive group msg: {}".format(cmsg.content))
  251. context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg, no_need_at=conf().get("no_need_at", False))
  252. if context:
  253. self.produce(context)
  254. # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
  255. def send(self, reply: Reply, context: Context):
  256. receiver = context["receiver"]
  257. if reply.type == ReplyType.TEXT:
  258. itchat.send(reply.content, toUserName=receiver)
  259. logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
  260. # logger.info(context)
  261. # logger.info(context["msg"])
  262. # // 发送kafka
  263. # msg=context["msg"]
  264. msg: ChatMessage = context["msg"]
  265. # content=msg["content"]
  266. is_group=msg.is_group
  267. if not is_group:
  268. # print(f'响应:{content}')
  269. # 用户输入
  270. # input_content=msg.content
  271. # input_from_user_nickname=msg.from_user_nickname
  272. # input_to_user_nickname=msg.to_user_nickname
  273. # input_wx_content_dialogue_message=[{"type": "text", "text": input_content}]
  274. # input_message=dialogue_message(input_from_user_nickname,input_to_user_nickname,input_wx_content_dialogue_message)
  275. # kafka_helper.kafka_client.produce_message(input_message)
  276. # logger.info("发送对话 %s", json.dumps(input_message, separators=(',', ':'), ensure_ascii=False))
  277. # 响应用户
  278. output_content=reply.content
  279. output_from_user_nickname=msg.to_user_nickname # 回复翻转
  280. output_to_user_nickname=msg.from_user_nickname # 回复翻转
  281. output_wx_content_dialogue_message=[{"type": "text", "text": output_content}]
  282. output_message=dialogue_message(output_from_user_nickname,output_to_user_nickname,output_wx_content_dialogue_message)
  283. kafka_helper.kafka_client.produce_message(output_message)
  284. logger.info("发送对话 %s", output_message)
  285. elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
  286. itchat.send(reply.content, toUserName=receiver)
  287. logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
  288. elif reply.type == ReplyType.VOICE:
  289. itchat.send_file(reply.content, toUserName=receiver)
  290. logger.info("[WX] sendFile={}, receiver={}".format(reply.content, receiver))
  291. elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
  292. img_url = reply.content
  293. logger.debug(f"[WX] start download image, img_url={img_url}")
  294. pic_res = requests.get(img_url, stream=True)
  295. image_storage = io.BytesIO()
  296. size = 0
  297. for block in pic_res.iter_content(1024):
  298. size += len(block)
  299. image_storage.write(block)
  300. logger.info(f"[WX] download image success, size={size}, img_url={img_url}")
  301. image_storage.seek(0)
  302. if ".webp" in img_url:
  303. try:
  304. image_storage = convert_webp_to_png(image_storage)
  305. except Exception as e:
  306. logger.error(f"Failed to convert image: {e}")
  307. return
  308. itchat.send_image(image_storage, toUserName=receiver)
  309. logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver))
  310. elif reply.type == ReplyType.IMAGE: # 从文件读取图片
  311. image_storage = reply.content
  312. image_storage.seek(0)
  313. itchat.send_image(image_storage, toUserName=receiver)
  314. logger.info("[WX] sendImage, receiver={}".format(receiver))
  315. elif reply.type == ReplyType.FILE: # 新增文件回复类型
  316. file_storage = reply.content
  317. itchat.send_file(file_storage, toUserName=receiver)
  318. logger.info("[WX] sendFile, receiver={}".format(receiver))
  319. # msg: ChatMessage = context["msg"]
  320. # # content=msg["content"]
  321. # is_group=msg.is_group
  322. # if not is_group:
  323. # # print(f'响应:{content}')
  324. # # 用户输入
  325. # # input_content=msg.content
  326. # # input_from_user_nickname=msg.from_user_nickname
  327. # # input_to_user_nickname=msg.to_user_nickname
  328. # # input_wx_content_dialogue_message=[{"type": "text", "text": input_content}]
  329. # # input_message=dialogue_message(input_from_user_nickname,input_to_user_nickname,input_wx_content_dialogue_message)
  330. # # kafka_helper.kafka_client.produce_message(input_message)
  331. # # logger.info("发送对话 %s", json.dumps(input_message, separators=(',', ':'), ensure_ascii=False))
  332. # # 响应用户
  333. # output_content=reply.content
  334. # output_from_user_nickname=msg.to_user_nickname # 回复翻转
  335. # output_to_user_nickname=msg.from_user_nickname # 回复翻转
  336. # output_wx_content_dialogue_message=[{"type": "file", "text": output_content}]
  337. # output_message=dialogue_message(output_from_user_nickname,output_to_user_nickname,output_wx_content_dialogue_message)
  338. # kafka_helper.kafka_client.produce_message(output_message)
  339. # logger.info("发送对话 %s", output_message)
  340. elif reply.type == ReplyType.VIDEO: # 新增视频回复类型
  341. video_storage = reply.content
  342. itchat.send_video(video_storage, toUserName=receiver)
  343. logger.info("[WX] sendFile, receiver={}".format(receiver))
  344. elif reply.type == ReplyType.VIDEO_URL: # 新增视频URL回复类型
  345. video_url = reply.content
  346. logger.debug(f"[WX] start download video, video_url={video_url}")
  347. video_res = requests.get(video_url, stream=True)
  348. video_storage = io.BytesIO()
  349. size = 0
  350. for block in video_res.iter_content(1024):
  351. size += len(block)
  352. video_storage.write(block)
  353. logger.info(f"[WX] download video success, size={size}, video_url={video_url}")
  354. video_storage.seek(0)
  355. itchat.send_video(video_storage, toUserName=receiver)
  356. logger.info("[WX] sendVideo url={}, receiver={}".format(video_url, receiver))
  357. def _send_login_success():
  358. try:
  359. from common.linkai_client import chat_client
  360. if chat_client.client_id:
  361. chat_client.send_login_success()
  362. except Exception as e:
  363. pass
  364. def _send_logout():
  365. try:
  366. from common.linkai_client import chat_client
  367. if chat_client.client_id:
  368. chat_client.send_logout()
  369. except Exception as e:
  370. pass
  371. def _send_qr_code(qrcode_list: list):
  372. try:
  373. from common.linkai_client import chat_client
  374. if chat_client.client_id:
  375. chat_client.send_qrcode(qrcode_list)
  376. except Exception as e:
  377. pass
  378. def clean_json_string(json_str):
  379. # 删除所有控制字符(非打印字符),包括换行符、回车符等
  380. return re.sub(r'[\x00-\x1f\x7f]', '', json_str)
  381. def save_friends_to_redis(agent_tel,agent_nickname, friends):
  382. contact_list = []
  383. for friend in friends:
  384. friend_data = {
  385. "UserName": friend.UserName,
  386. "NickName": friend.NickName,
  387. "Signature": friend.Signature,
  388. "Province": friend.Province,
  389. "City": friend.City,
  390. "Sex": str(friend.Sex), # 性别可转换为字符串存储
  391. "Alias": friend.Alias
  392. }
  393. contact_list.append(friend_data) # 将每个朋友的信息加入到列表中
  394. agent_contact_list = {
  395. "AgentTel":agent_tel,
  396. "agent_nick_name": agent_nickname,
  397. "contact_list": contact_list # 将朋友列表添加到字典中
  398. }
  399. # 将联系人信息保存到 Redis,使用一个合适的 key
  400. hash_key = f"__AI_OPS_WX__:CONTACTLIST"
  401. redis_helper.redis_helper.update_hash_field(hash_key, agent_tel, json.dumps(agent_contact_list, ensure_ascii=False)) # 设置有效期为 600 秒
  402. def hourly_change_save_friends(agent_nickname):
  403. last_hour = datetime.now().hour # 获取当前小时
  404. while True:
  405. current_hour = datetime.now().hour
  406. if current_hour != last_hour: # 检测小时是否变化
  407. friends=itchat.get_friends(update=True)[1:]
  408. agent_info=fetch_agent_info(agent_nickname)
  409. agent_tel=agent_info.get("agent_tel",None)
  410. save_friends_to_redis(agent_tel,agent_nickname, friends)
  411. last_hour = current_hour
  412. time.sleep(1) # 每秒检查一次
  413. def wx_messages_process_callback(user_nickname,message):
  414. """
  415. 处理消费到的 Kafka 消息(基础示例)
  416. :param message: Kafka 消费到的消息内容
  417. """
  418. # print(user_nickname)
  419. # print(f"Processing message: {message}")
  420. # return True
  421. msg_content= message
  422. cleaned_content = clean_json_string(msg_content)
  423. content=json.loads(cleaned_content)
  424. data = content.get("data", {})
  425. msg_type_data=data.get("msg_type",None)
  426. content_data = data.get("content",{})
  427. agent_nickname_data=content_data.get("agent_nickname",None)
  428. agent_tel=content_data.get("agent_tel",None)
  429. if user_nickname == agent_nickname_data and msg_type_data=='group-sending':
  430. friends=itchat.get_friends(update=True)[1:]
  431. contact_list_content_data=content_data.get("contact_list",None)
  432. # 更新好友缓存
  433. save_friends_to_redis(agent_tel,agent_nickname_data,friends)
  434. # 遍历要群发的好友
  435. for contact in contact_list_content_data:
  436. nickname = contact.get("nickname",None)
  437. if(nickname not in [friend['NickName'] for friend in friends]):
  438. logger.warning(f'微信中没有 {nickname} 的昵称,将不会发送消息')
  439. for friend in friends:
  440. if friend.get("NickName",None) == nickname:
  441. wx_content_list=content_data.get("wx_content",[])
  442. for wx_content in wx_content_list:
  443. # 处理文件
  444. if wx_content.get("type",None) == 'text':
  445. wx_content_text=wx_content.get("text",None)
  446. itchat.send(wx_content_text, toUserName=friend.get("UserName",None))
  447. logger.info(f"{user_nickname} 向 {nickname} 发送文字【 {wx_content_text} 】")
  448. # // 发送kafka
  449. wx_content_dialogue_message=[{"type": "text", "text": wx_content_text}]
  450. message=dialogue_message(agent_nickname_data,friend.get("NickName",None),wx_content_dialogue_message)
  451. kafka_helper.kafka_client.produce_message(message)
  452. logger.info("发送对话 %s",message)
  453. time.sleep(3)
  454. # 处理图片
  455. elif wx_content.get("type",None) == 'image_url':
  456. print('发送图片')
  457. image_url= wx_content.get("image_url",{})
  458. url=image_url.get("url",None)
  459. # 网络图片
  460. logger.debug(f"[WX] start download image, img_url={url}")
  461. pic_res = requests.get(url, stream=True)
  462. image_storage = io.BytesIO()
  463. size = 0
  464. for block in pic_res.iter_content(1024):
  465. size += len(block)
  466. image_storage.write(block)
  467. logger.info(f"[WX] download image success, size={size}, img_url={url}")
  468. image_storage.seek(0)
  469. if ".webp" in url:
  470. try:
  471. image_storage = convert_webp_to_png(image_storage)
  472. except Exception as e:
  473. logger.error(f"Failed to convert image: {e}")
  474. return
  475. itchat.send_image(image_storage, toUserName=friend.get("UserName",None))
  476. logger.info(f"{user_nickname} 向 {nickname} 发送图片【 {url} 】")
  477. # // 发送kafka
  478. wx_content_dialogue_message=[{"type": "image_url", "image_url": {"url": url}}]
  479. message=dialogue_message(agent_nickname_data,friend.get("NickName",None),wx_content_dialogue_message)
  480. kafka_helper.kafka_client.produce_message(message)
  481. logger.info("发送对话 %s",message)
  482. time.sleep(3)
  483. #处理文件
  484. elif wx_content.get("type",None) == 'file':
  485. print('处理文件')
  486. file_url= wx_content.get("file_url",{})
  487. url=file_url.get("url",None)
  488. # 提取路径部分
  489. parsed_url = urlparse(url).path
  490. # 获取文件名和扩展名
  491. filename = os.path.basename(parsed_url) # 文件名(包含扩展名)
  492. name, ext = os.path.splitext(filename) # 分离文件名和扩展名
  493. if ext in ['.pdf']:
  494. print('处理PDF文件')
  495. tmp_file_path=save_to_local_from_url(url)
  496. itchat.send_file(tmp_file_path, toUserName=friend.get("UserName",None))
  497. logger.info(f'删除本地{ext}文件: {tmp_file_path}')
  498. os.remove(tmp_file_path)
  499. logger.info(f"{user_nickname} 向 {nickname} 发送 {ext} 文件【 {url} 】")
  500. # // 发送kafka
  501. wx_content_dialogue_message=[{"type": "file", "file_url": {"url": url}}]
  502. message=dialogue_message(agent_nickname_data,friend.get("NickName",None),wx_content_dialogue_message)
  503. kafka_helper.kafka_client.produce_message(message)
  504. logger.info("发送对话 %s",message)
  505. time.sleep(3)
  506. elif ext in ['.mp4']:
  507. print('处理MP4文件')
  508. tmp_file_path=save_to_local_from_url(url)
  509. itchat.send_file(tmp_file_path, toUserName=friend.get("UserName",None))
  510. logger.info(f'删除本地{ext}文件: {tmp_file_path}')
  511. os.remove(tmp_file_path)
  512. logger.info(f"{user_nickname} 向 {nickname} 发送 {ext} 文件【 {url} 】")
  513. # // 发送kafka
  514. wx_content_dialogue_message=[{"type": "file", "file_url": {"url": url}}]
  515. message=dialogue_message(agent_nickname_data,friend.get("NickName",None),wx_content_dialogue_message)
  516. kafka_helper.kafka_client.produce_message(message)
  517. logger.info("发送对话 %s",message)
  518. time.sleep(3)
  519. else:
  520. logger.warning(f'暂不支持 {ext} 文件的处理')
  521. return True
  522. else:
  523. return False
  524. def dialogue_message(nickname_from,nickname_to,wx_content):
  525. """
  526. 构造消息的 JSON 数据
  527. :param contents: list,包含多个消息内容,每个内容为字典,如:
  528. [{"type": "text", "text": "AAAAAAA"},
  529. {"type": "image_url", "image_url": {"url": "https://AAAAA.jpg"}},
  530. {"type":"file","file_url":{"url":"https://AAAAA.pdf"}}
  531. ]
  532. :return: JSON 字符串
  533. """
  534. # 获取当前时间戳,精确到毫秒
  535. current_timestamp = int(time.time() * 1000)
  536. # 获取当前时间,格式化为 "YYYY-MM-DD HH:MM:SS"
  537. current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  538. # 构造 JSON 数据
  539. data = {
  540. "messageId": str(current_timestamp),
  541. "topic": "topic.aiops.wx",
  542. "time": current_time,
  543. "data": {
  544. "msg_type": "dialogue",
  545. "content": {
  546. "nickname_from": nickname_from,
  547. "nickname_to": nickname_to,
  548. "wx_content":wx_content
  549. }
  550. }
  551. }
  552. return json.dumps(data, separators=(',', ':'), ensure_ascii=False)
  553. def fetch_agent_info(agent_nickname):
  554. if os.environ.get('environment', 'default')=='default':
  555. return {
  556. "agent_nickname": agent_nickname,
  557. "agent_tel": "19200137635"
  558. }
  559. aiops_api=conf().get("aiops_api")
  560. # 定义请求URL
  561. url = f"{aiops_api}/business/Agent/smartinfobyname"
  562. # 定义请求头
  563. headers = {
  564. "accept": "*/*",
  565. "Content-Type": "application/json"
  566. }
  567. # 定义请求数据
  568. data = {
  569. "smartName": agent_nickname
  570. }
  571. try:
  572. # 发送POST请求
  573. response = requests.post(url, headers=headers, data=json.dumps(data))
  574. # 确认响应状态码
  575. if response.status_code == 200:
  576. response_data = response.json()
  577. if response_data.get("code") == 200:
  578. # 提取 smartName 和 smartPhone
  579. data = response_data.get("data", {})
  580. return {
  581. "agent_nickname": data.get("smartName"),
  582. "agent_tel": data.get("smartPhone")
  583. }
  584. else:
  585. logger.error(f"API 返回错误信息: {response_data.get('msg')}")
  586. return None
  587. else:
  588. logger.error(f"请求失败,状态码:{response.status_code}")
  589. return None
  590. except Exception as e:
  591. logger.error(f"请求出错: {e}")
  592. return None
  593. def save_to_local_from_url(url):
  594. '''
  595. 从url保存到本地tmp目录
  596. '''
  597. parsed_url = urlparse(url)
  598. # 从 URL 提取文件名
  599. filename = os.path.basename(parsed_url.path)
  600. # tmp_dir = os.path(__file__) # 获取系统临时目录
  601. # print(tmp_dir)
  602. tmp_file_path = os.path.join(os.getcwd(),'tmp', filename) # 拼接完整路径
  603. # 检查是否存在同名文件
  604. if os.path.exists(tmp_file_path):
  605. logger.info(f"文件已存在,将覆盖:{tmp_file_path}")
  606. # 下载文件并保存到临时目录
  607. response = requests.get(url, stream=True)
  608. with open(tmp_file_path, 'wb') as f:
  609. for chunk in response.iter_content(chunk_size=1024):
  610. if chunk: # 检查是否有内容
  611. f.write(chunk)
  612. return tmp_file_path
  613. def upload_oss(access_key_id, access_key_secret, endpoint, bucket_name, local_file_path, oss_file_name, expiration_days=7):
  614. """
  615. 上传文件到阿里云OSS并设置生命周期规则,同时返回文件的公共访问地址。
  616. :param access_key_id: 阿里云AccessKey ID
  617. :param access_key_secret: 阿里云AccessKey Secret
  618. :param endpoint: OSS区域对应的Endpoint
  619. :param bucket_name: OSS中的Bucket名称
  620. :param local_file_path: 本地文件路径
  621. :param oss_file_name: OSS中的文件存储路径
  622. :param expiration_days: 文件保存天数,默认7天后删除
  623. :return: 文件的公共访问地址
  624. """
  625. # 创建Bucket实例
  626. auth = oss2.Auth(access_key_id, access_key_secret)
  627. bucket = oss2.Bucket(auth, endpoint, bucket_name)
  628. ### 1. 设置生命周期规则 ###
  629. rule_id = f'delete_after_{expiration_days}_days' # 规则ID
  630. prefix = oss_file_name.split('/')[0] + '/' # 设置规则应用的前缀为文件所在目录
  631. # 定义生命周期规则
  632. rule = oss2.models.LifecycleRule(rule_id, prefix, status=oss2.models.LifecycleRule.ENABLED,
  633. expiration=oss2.models.LifecycleExpiration(days=expiration_days))
  634. # 设置Bucket的生命周期
  635. lifecycle = oss2.models.BucketLifecycle([rule])
  636. bucket.put_bucket_lifecycle(lifecycle)
  637. print(f"已设置生命周期规则:文件将在{expiration_days}天后自动删除")
  638. ### 2. 上传文件到OSS ###
  639. bucket.put_object_from_file(oss_file_name, local_file_path)
  640. ### 3. 构建公共访问URL ###
  641. file_url = f"http://{bucket_name}.{endpoint.replace('http://', '')}/{oss_file_name}"
  642. print(f"文件上传成功,公共访问地址:{file_url}")
  643. return file_url