from flask_restful import Resource, reqparse from flask import jsonify,request from bridge.context import ContextType import requests,json from wechat import gewe_chat from voice.ali.ali_voice import AliVoice from common import utils,redis_helper,memory,kafka_helper from common.log import logger import openai import xml.etree.ElementTree as ET import os from voice import audio_convert class MessagesResource(Resource): def __init__(self): self.parser = reqparse.RequestParser() def post(self): msg = request.get_json() logger.info(f"收到微信回调消息: {msg}") type_name =msg.get("TypeName") app_id = msg.get("Appid") # token_id = "f828cb3c-1039-489f-b9ae-7494d1778a15" token_id=get_token_id_by_app_id(app_id) if token_id=="": logger.warning('找不到登录信息,不处理') return jsonify({"message": "收到微信回调消息"}) if type_name=='AddMsg': wxid = msg.get("Wxid") msg_data = msg.get("Data") msg_type = msg_data.get("MsgType",None) from_wxid = msg_data["FromUserName"]["string"] to_wxid = msg_data["ToUserName"]["string"] handlers = { 1: handle_text, 3: handle_image, 34: handle_voice, 49: handle_xml, 37: handle_add_friend_notice } handler = handlers.get(msg_type) if handler: return handler(token_id,app_id, wxid,msg_data,from_wxid, to_wxid) else: logger.warning(f"微信回调消息类型 {msg_type} 未处理") elif type_name=='ModContacts': ''' 好友通过验证及好友资料变更的通知消息 ''' msg_data = msg.get("Data") handle_mod_contacts(token_id,app_id,msg_data) elif type_name=="Offline": ''' 掉线通知 ''' elif type_name=="DelContacts": ''' 删除好友通知 退出群聊 ''' else: logger.warning(f"未知消息类型") return jsonify({"message": "收到微信回调消息"}) def handle_text(token_id,app_id, wxid,msg_data,from_wxid, to_wxid): ''' 文本消息 ''' msg_content=msg_data["Content"]["string"] if wxid == from_wxid: #手动发送消息 logger.info("Active message sending detected") gewe_chat.wxchat.save_contacts_brief_to_cache(token_id,app_id,wxid,[to_wxid]) callback_to_user=msg_data["ToUserName"]["string"] input_wx_content_dialogue_message=[{"type": "text", "text": msg_content}] input_message=utils.dialogue_message(from_wxid,to_wxid,input_wx_content_dialogue_message) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) else: callback_to_user=msg_data["FromUserName"]["string"] hash_key = f'__AI_OPS_WX__:MESSAGES:{wxid}:{callback_to_user}' prompt={"role": "user", "content": [{ "type": "text", "text": msg_content }]} messages_to_send=get_messages_from_cache(hash_key, prompt) # 收到的对话 input_wx_content_dialogue_message=[{"type": "text", "text": msg_content}] input_message=utils.dialogue_message(callback_to_user,wxid,input_wx_content_dialogue_message) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) cache_data = memory.USER_INTERACTIVE_CACHE.get(wxid) if cache_data and cache_data.get('interactive'): messages_to_send=[{"role": "user", "content": msg_content}] res=fast_gpt_api(messages_to_send,f'{wxid}-{callback_to_user}') reply_content=res["choices"][0]["message"]["content"] description = '' userSelectOptions = [] if isinstance(reply_content, list) and any(item.get("type") == "interactive" for item in reply_content): for item in reply_content: if item["type"] == "interactive" and item["interactive"]["type"] == "userSelect": params = item["interactive"]["params"] description = params.get("description") userSelectOptions = params.get("userSelectOptions", []) values_string = "\n".join(option["value"] for option in userSelectOptions) if description is not None: memory.USER_INTERACTIVE_CACHE[wxid] = { "interactive":True } reply_content=description + '------------------------------\n'+values_string elif isinstance(reply_content, list) and any(item.get("type") == "text" for item in reply_content): memory.USER_INTERACTIVE_CACHE[wxid] = { "interactive":False } text='' for item in reply_content: if item["type"] == "text": text=item["text"]["content"] if text=='': # 去除上次上一轮对话再次请求 cache_messages_str=redis_helper.redis_helper.get_hash_field(hash_key,"data") cache_messages = json.loads(cache_messages_str) if cache_messages_str else [] if len(cache_messages) >= 3: cache_messages = cache_messages[:-3] redis_helper.redis_helper.update_hash_field(hash_key,"data",json.dumps(cache_messages,ensure_ascii=False)) messages_to_send=get_messages_from_cache(hash_key, prompt) res=fast_gpt_api(messages_to_send,f'{wxid}-{callback_to_user}') reply_content=res["choices"][0]["message"]["content"] else: reply_content=text else: memory.USER_INTERACTIVE_CACHE[wxid] = { "interactive":False } reply_content=res["choices"][0]["message"]["content"] # print(f'token_id {token_id}') # print(f'app_id {app_id}') # print(f'touser: {callback_to_user}') # # print(f'towxuser:{towxuser}') # print(reply_content) gewe_chat.wxchat.post_text(token_id,app_id,callback_to_user,reply_content) get_messages_from_cache(hash_key, {"role": "assistant", "content": reply_content}) # 回复的对话 input_wx_content_dialogue_message=[{"type": "text", "text": reply_content}] input_message=utils.dialogue_message(wxid,callback_to_user,input_wx_content_dialogue_message,True) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) def handle_image(token_id,app_id, wxid,msg_data,from_wxid, to_wxid): ''' 图片消息 ''' msg_content=msg_data["Content"]["string"] callback_to_user=from_wxid hash_key = f'__AI_OPS_WX__:MESSAGES:{wxid}:{callback_to_user}' wx_img_url=gewe_chat.wxchat.download_image_msg(token_id,app_id,msg_content) oss_access_key_id="LTAI5tRTG6pLhTpKACJYoPR5" oss_access_key_secret="E7dMzeeMxq4VQvLg7Tq7uKf3XWpYfN" oss_endpoint="http://oss-cn-shanghai.aliyuncs.com" oss_bucket_name="cow-agent" oss_prefix="cow" img_url=utils.upload_oss(oss_access_key_id, oss_access_key_secret, oss_endpoint, oss_bucket_name, wx_img_url, oss_prefix) prompt={ "role": "user", "content": [{ "type": "image_url", "image_url": {"url": img_url} }] } get_messages_from_cache(hash_key, prompt) gewe_chat.wxchat.post_text(token_id,app_id,callback_to_user,'已经上传了图片,有什么可以为您服务') logger.info(f"上传图片 URL: {img_url}") wx_content_dialogue_message=[{"type": "image_url", "image_url": {"url": img_url}}] input_message=utils.dialogue_message(wxid,callback_to_user,wx_content_dialogue_message) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) def handle_voice(token_id,app_id, wxid,msg_data,from_wxid, to_wxid): ''' 语音消息 ''' callback_to_user=from_wxid msg_content=msg_data["Content"]["string"] msg_id=msg_data["MsgId"] file_url=gewe_chat.wxchat.download_audio_msg(token_id,app_id,msg_id,msg_content) react_silk_path=utils.save_to_local_from_url(file_url) react_wav_path = os.path.splitext(react_silk_path)[0] + ".wav" audio_convert.any_to_wav(react_silk_path,react_wav_path) react_voice_text=AliVoice().voiceToText(react_wav_path) os.remove(react_silk_path) os.remove(react_wav_path) hash_key = f'__AI_OPS_WX__:MESSAGES:{wxid}:{callback_to_user}' messages=get_messages_from_cache(hash_key, {"role": "user", "content": react_voice_text}) ai_res=fast_gpt_api(messages,f'{wxid}-{callback_to_user}') ai_res_content=ai_res["choices"][0]["message"]["content"] voice_during,voice_url=utils.wx_voice(ai_res_content) ret,ret_msg,res=gewe_chat.wxchat.post_voice(token_id,app_id,callback_to_user,voice_url,voice_during) # 删除临时文件 if ret==200: get_messages_from_cache(hash_key, {"role": "assistant", "content": ai_res_content}) logger.info((f'{wxid} 向 {callback_to_user} 发送语音文本【{ai_res_content}】{ret_msg}')) # 构造对话消息并发送到 Kafka input_wx_content_dialogue_message = [{"type": "text", "text": ai_res_content}] input_message = utils.dialogue_message(wxid, callback_to_user, input_wx_content_dialogue_message) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s", input_message) else: logger.warning((f'{wxid} 向 {callback_to_user} 发送语音文本【{ai_res_content}】{ret_msg}')) def handle_xml(token_id,app_id, wxid,msg_data,from_wxid, to_wxid): ''' 处理xml ''' msg_content_xml=msg_data["Content"]["string"] root = ET.fromstring(msg_content_xml) type_value = root.find(".//appmsg/type").text handlers = { 57: handle_xml_reference, } handler = handlers.get(type_value) if handler: return handler(token_id,app_id, wxid,msg_data,from_wxid, to_wxid) else: print(f"xml消息 {type_value} 未解析") def handle_xml_reference(token_id,app_id, wxid,msg_data,from_wxid, to_wxid): ''' 判断此类消息的逻辑:$.Data.MsgType=49 并且 解析$.Data.Content.string中的xml msg.appmsg.type=57 ''' callback_to_user=from_wxid hash_key = f'__AI_OPS_WX__:MESSAGES:{wxid}:{callback_to_user}' msg_content= msg_data["PushContent"] prompt={"role": "user", "content": [{ "type": "text", "text": msg_content }]} # 收到的对话 messages_to_send=get_messages_from_cache(hash_key, prompt) input_wx_content_dialogue_message=[{"type": "text", "text": msg_content}] input_message=utils.dialogue_message(callback_to_user,wxid,input_wx_content_dialogue_message) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) # 回复的对话 res=fast_gpt_api(messages_to_send,f'{wxid}-{callback_to_user}') reply_content=res["choices"][0]["message"]["content"] input_wx_content_dialogue_message=[{"type": "text", "text": reply_content}] input_message=utils.dialogue_message(wxid,callback_to_user,input_wx_content_dialogue_message,True) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) get_messages_from_cache(hash_key, {"role": "assistant", "content": reply_content}) gewe_chat.wxchat.post_text(token_id,app_id,callback_to_user,reply_content) def handle_add_friend_notice(token_id,app_id, wxid,msg_data,from_wxid, to_wxid): ''' 好友添加请求通知 ''' logger.info('好友添加请求通知') msg_content_xml=msg_data["Content"]["string"] root = ET.fromstring(msg_content_xml) msg_content = root.attrib.get('content', None) v3= root.attrib.get('encryptusername', None) v4= root.attrib.get('ticket', None) scene=root.attrib.get('scene', None) to_contact_wxid=root.attrib.get('fromusername', None) wxid=msg_data["ToUserName"]["string"] # 自动同意好友 # print(v3) # print(v4) # print(scene) # print(msg_content) # 操作类型,2添加好友 3同意好友 4拒绝好友 #option=2 option=3 reply_add_contact_contact="亲,我是你的美丽顾问" ret,ret_msg=gewe_chat.wxchat.add_contacts(token_id,app_id,scene,option,v3,v4,reply_add_contact_contact) if ret==200: logger.info('自动添加好友成功') # 好友发送的文字 hash_key = f'__AI_OPS_WX__:MESSAGES:{wxid}:{to_contact_wxid}' prompt={"role": "user", "content": [{"type": "text","text": msg_content}]} messages_to_send=get_messages_from_cache(hash_key, prompt) input_wx_content_dialogue_message=[{"type": "text", "text": msg_content}] input_message=utils.dialogue_message(to_contact_wxid,wxid,input_wx_content_dialogue_message) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) callback_to_user=to_contact_wxid res=fast_gpt_api(messages_to_send,f'{wxid}-{callback_to_user}') reply_content=res["choices"][0]["message"]["content"] #保存好友信息 gewe_chat.wxchat.save_contacts_brief_to_cache(token_id,app_id, wxid,[to_contact_wxid]) # 保存到缓存 get_messages_from_cache(hash_key, {"role": "assistant", "content": reply_content}) # 发送信息 gewe_chat.wxchat.post_text(token_id,app_id, to_contact_wxid,reply_content) # 发送到kafka input_wx_content_dialogue_message=[{"type": "text", "text": reply_content}] input_message=utils.dialogue_message(wxid,to_contact_wxid,input_wx_content_dialogue_message,True) kafka_helper.kafka_client.produce_message(input_message) logger.info("发送对话 %s",input_message) else: logger.warning("添加好友失败") def handle_mod_contacts(token_id,app_id,msg_data): ''' 好友通过验证及好友资料变更的通知消息 ''' logger.info('好友通过验证及好友资料变更的通知消息') contact_wxid = msg_data["UserName"]["string"] keys=redis_helper.redis_helper.client.keys('__AI_OPS_WX__:LOGININFO:*') wxid="" for k in keys: key=k.decode('utf-8') hash_key=f"__AI_OPS_WX__:LOGININFO:{key}" cache_app_id=redis_helper.redis_helper.get_hash_field(hash_key,"appId") if app_id==cache_app_id: wxid=redis_helper.redis_helper.get_hash_field(hash_key,"wxid") break if wxid!="": hash_key=f"__AI_OPS_WX__:CONTACTS_BRIEF:{wxid}" cache_str=redis_helper.redis_helper.get_hash_field(hash_key,"data") cache = json.loads(cache_str) if cache_str else [] for c in cache: if c["userName"]==contact_wxid: c["nickName"] = msg_data["NickName"] c["snsBgImg"] = msg_data["SnsBgimgId"] c["smallHeadImgUrl"] = msg_data["SmallHeadImgUrl"] c["signature"]= msg_data["Signature"] # 更新缓存,如果有修改过的话 if cache: updated_cache_str = json.dumps(cache) redis_helper.redis_helper.set_hash_field(hash_key, "data", updated_cache_str) def get_messages_from_cache(hash_key,object:object)->object: messages=redis_helper.redis_helper.get_hash(hash_key) wxid=hash_key.split(':')[-1] if not messages: messages=[{"role": "system", "content": ""}] messages.append(object) redis_helper.redis_helper.set_hash(hash_key,{"data":json.dumps(messages,ensure_ascii=False)},3600) else: messages_str=redis_helper.redis_helper.get_hash_field(hash_key,"data") messages = json.loads(messages_str) if messages_str else [] #判断是否含有图片 last_message = messages[-1] content = last_message.get("content", []) if isinstance(content, list) and content: last_content_type = content[-1].get("type") if last_content_type == 'image_url': content.append(object['content'][0]) messages[-1]['content']=content else: messages.append(object) else: messages.append(object) redis_helper.redis_helper.set_hash(hash_key,{"data":json.dumps(messages,ensure_ascii=False)},3600) return messages def fast_gpt_api(messages:list,session_id:str): #api_key="sk-tdi7u0zuLsR0JpPMGBeFZxymOpL0zoFVafX8EEEvEakIDAGQ22NyQ6w" api_key="sk-uJDBdKmJVb2cmfldGOvlIY6Qx0AzqWMPD3lS1IzgQYzHNOXv9SKNI" api_url = "http://106.15.182.218:3000/api/v1/chat/completions" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {api_key}" } data={ "model": "", "messages":messages, "chatId": session_id, "detail": True } print(json.dumps(data,ensure_ascii=False)) logger.info("[CHATGPT] 请求={}".format(json.dumps(data, ensure_ascii=False))) response = requests.post(url=api_url, headers=headers, data=json.dumps(data), timeout=600) response.raise_for_status() response_data = response.json() logger.info("[CHATGPT] 响应={}".format(json.dumps(response_data, separators=(',', ':'),ensure_ascii=False))) print(response_data) return response_data def get_token_id_by_app_id(app_id: str) -> str: # 使用 SCAN 避免一次性返回所有的匹配键,逐步扫描 cursor = 0 while True: cursor, login_keys = redis_helper.redis_helper.client.scan(cursor, match='__AI_OPS_WX__:LOGININFO:*') # 批量获取所有键的 hash 数据 for k in login_keys: r = redis_helper.redis_helper.get_hash(k) if r.get("appId") == app_id: return r.get("tokenId", "") # 如果游标为 0,则表示扫描完成 if cursor == 0: break return ""