選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

wechat_channel.py 32KB

7ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
7ヶ月前
7ヶ月前
3ヶ月前
3ヶ月前
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