Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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