From a4b1d7446a0e80aaa7140b42107e52b5d66e2717 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Wed, 22 Mar 2023 18:58:45 +0800 Subject: [PATCH 01/13] wechatmp --- channel/channel_factory.py | 3 + channel/wechatmp/receive.py | 47 +++++++ channel/wechatmp/reply.py | 52 ++++++++ channel/wechatmp/wechatmp_channel.py | 193 +++++++++++++++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 channel/wechatmp/receive.py create mode 100644 channel/wechatmp/reply.py create mode 100644 channel/wechatmp/wechatmp_channel.py diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 546f18a..905ea02 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -17,4 +17,7 @@ def create_channel(channel_type): elif channel_type == 'terminal': from channel.terminal.terminal_channel import TerminalChannel return TerminalChannel() + elif channel_type == 'mp': + from channel.wechatmp.wechatmp_channel import WechatMPServer + return WechatMPServer() raise RuntimeError diff --git a/channel/wechatmp/receive.py b/channel/wechatmp/receive.py new file mode 100644 index 0000000..40fc35f --- /dev/null +++ b/channel/wechatmp/receive.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*-# +# filename: receive.py +import xml.etree.ElementTree as ET + + +def parse_xml(web_data): + if len(web_data) == 0: + return None + xmlData = ET.fromstring(web_data) + msg_type = xmlData.find('MsgType').text + if msg_type == 'text': + return TextMsg(xmlData) + elif msg_type == 'image': + return ImageMsg(xmlData) + elif msg_type == 'event': + return Event(xmlData) + + +class Msg(object): + def __init__(self, xmlData): + self.ToUserName = xmlData.find('ToUserName').text + self.FromUserName = xmlData.find('FromUserName').text + self.CreateTime = xmlData.find('CreateTime').text + self.MsgType = xmlData.find('MsgType').text + self.MsgId = xmlData.find('MsgId').text + + +class TextMsg(Msg): + def __init__(self, xmlData): + Msg.__init__(self, xmlData) + self.Content = xmlData.find('Content').text.encode("utf-8") + + +class ImageMsg(Msg): + def __init__(self, xmlData): + Msg.__init__(self, xmlData) + self.PicUrl = xmlData.find('PicUrl').text + self.MediaId = xmlData.find('MediaId').text + + +class Event(object): + def __init__(self, xmlData): + self.ToUserName = xmlData.find('ToUserName').text + self.FromUserName = xmlData.find('FromUserName').text + self.CreateTime = xmlData.find('CreateTime').text + self.MsgType = xmlData.find('MsgType').text + self.Event = xmlData.find('Event').text diff --git a/channel/wechatmp/reply.py b/channel/wechatmp/reply.py new file mode 100644 index 0000000..5f3a934 --- /dev/null +++ b/channel/wechatmp/reply.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*-# +# filename: reply.py +import time + +class Msg(object): + def __init__(self): + pass + + def send(self): + return "success" + +class TextMsg(Msg): + def __init__(self, toUserName, fromUserName, content): + self.__dict = dict() + self.__dict['ToUserName'] = toUserName + self.__dict['FromUserName'] = fromUserName + self.__dict['CreateTime'] = int(time.time()) + self.__dict['Content'] = content + + def send(self): + XmlForm = """ + + + + {CreateTime} + + + + """ + return XmlForm.format(**self.__dict) + +class ImageMsg(Msg): + def __init__(self, toUserName, fromUserName, mediaId): + self.__dict = dict() + self.__dict['ToUserName'] = toUserName + self.__dict['FromUserName'] = fromUserName + self.__dict['CreateTime'] = int(time.time()) + self.__dict['MediaId'] = mediaId + + def send(self): + XmlForm = """ + + + + {CreateTime} + + + + + + """ + return XmlForm.format(**self.__dict) \ No newline at end of file diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py new file mode 100644 index 0000000..67bad0d --- /dev/null +++ b/channel/wechatmp/wechatmp_channel.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# filename: main.py +import web +import time +import hashlib +import textwrap +from channel.channel import Channel +import channel.wechatmp.reply as reply +import channel.wechatmp.receive as receive +from common.log import logger +from config import conf + + +class WechatMPServer(): + def __init__(self): + pass + + def startup(self): + urls = ( + '/wx', 'WechatMPChannel', + ) + app = web.application(urls, globals()) + app.run() + + +from concurrent.futures import ThreadPoolExecutor +thread_pool = ThreadPoolExecutor(max_workers=8) + +cache_dict = dict() +query1 = dict() +query2 = dict() +query3 = dict() + +class WechatMPChannel(Channel): + + def GET(self): + try: + data = web.input() + if len(data) == 0: + return "hello, this is handle view" + signature = data.signature + timestamp = data.timestamp + nonce = data.nonce + echostr = data.echostr + token = conf().get('wechatmp_token') #请按照公众平台官网\基本配置中信息填写 + + data_list = [token, timestamp, nonce] + data_list.sort() + sha1 = hashlib.sha1() + # map(sha1.update, data_list) #python2 + sha1.update("".join(data_list).encode('utf-8')) + hashcode = sha1.hexdigest() + print("handle/GET func: hashcode, signature: ", hashcode, signature) + if hashcode == signature: + return echostr + else: + return "" + except Exception as Argument: + return Argument + + + def _do_build_reply(self, cache_key, fromUser, message): + context = dict() + context['session_id'] = fromUser + reply_text = super().build_reply_content(message, context) + # The query is done, record the cache + logger.info("[threaded] Get reply_text for {}".format(message)) + global cache_dict + cache_dict[cache_key] = (1, reply_text) + + + def POST(self): + try: + queryTime = time.time() + webData = web.data() + # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8")) + recMsg = receive.parse_xml(webData) + if isinstance(recMsg, receive.Msg) and recMsg.MsgType == 'text': + fromUser = recMsg.FromUserName + toUser = recMsg.ToUserName + createTime = recMsg.CreateTime + message = recMsg.Content.decode("utf-8") + message_id = recMsg.MsgId + + logger.info("{}:{} [wechatmp] Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), fromUser, message_id, message)) + + + global cache_dict + global query1 + global query2 + global query3 + cache_key = fromUser + cache = cache_dict.get(cache_key) + + reply_text = "" + + # New request + if cache == None: + # The first query begin, reset the cache + cache_dict[cache_key] = (0, "") + thread_pool.submit(self._do_build_reply, cache_key, fromUser, message) + query1[cache_key] = False + query2[cache_key] = False + query3[cache_key] = False + # Request again + elif cache[0] == 0 and query1.get(cache_key) == True and query2.get(cache_key) == True and query3.get(cache_key) == True: + query1[cache_key] = False + query2[cache_key] = False + query3[cache_key] = False + elif cache[0] == 1: + reply_text = cache[1] + query1[cache_key] = True + query2[cache_key] = True + query3[cache_key] = True + + + cache = cache_dict.get(cache_key) + if query1.get(cache_key) == False: + # The first query from wechat official server + logger.debug("[wechatmp] query1 {}".format(cache_key)) + query1[cache_key] = True + cnt = 0 + while cache[0] == 0 and cnt < 45: + cnt = cnt + 1 + time.sleep(0.1) + cache = cache_dict.get(cache_key) + if cnt == 45: + # waiting for timeout (the POST query will be closed by wechat official server) + time.sleep(5) + # and do nothing + return + else: + reply_text = cache[1] + elif query2.get(cache_key) == False: + # The second query from wechat official server + logger.debug("[wechatmp] query2 {}".format(cache_key)) + query2[cache_key] = True + cnt = 0 + while cache[0] == 0 and cnt < 45: + cnt = cnt + 1 + time.sleep(0.1) + cache = cache_dict.get(cache_key) + if cnt == 45: + # waiting for timeout (the POST query will be closed by wechat official server) + time.sleep(5) + # and do nothing + return + else: + reply_text = cache[1] + elif query3.get(cache_key) == False: + # The third query from wechat official server + logger.debug("[wechatmp] query3 {}".format(cache_key)) + query3[cache_key] = True + cnt = 0 + while cache[0] == 0 and cnt < 45: + cnt = cnt + 1 + time.sleep(0.1) + cache = cache_dict.get(cache_key) + if cnt == 45: + # Have waiting for 3x5 seconds + # return timeout message + reply_text = "服务器有点忙,回复任意文字再次尝试。" + logger.info("[wechatmp] Three queries has finished For {}: {}".format(fromUser, message_id)) + replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() + return replyPost + else: + reply_text = cache[1] + + if float(time.time()) - float(queryTime) > 4.8: + logger.info("[wechatmp] Timeout for {} {}".format(fromUser, message_id)) + return + + cache_dict.pop(cache_key) + logger.info("{}:{} [wechatmp] Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text)) + replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() + return replyPost + + elif isinstance(recMsg, receive.Event) and recMsg.MsgType == 'event': + toUser = recMsg.FromUserName + fromUser = recMsg.ToUserName + content = textwrap.dedent("""\ + 感谢您的关注! + 这里是ChatGPT。 + 资源有限,回复较慢,请不要着急。 + """) + replyMsg = reply.TextMsg(toUser, fromUser, content) + return replyMsg.send() + else: + print("暂且不处理") + return "success" + except Exception as Argment: + print(Argment) + return Argment From a7900d4b2ca15b680c4cba54b080592440bf0aca Mon Sep 17 00:00:00 2001 From: JS00000 Date: Thu, 23 Mar 2023 23:36:14 +0800 Subject: [PATCH 02/13] fix bug --- channel/wechatmp/wechatmp_channel.py | 39 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index 67bad0d..d2cefd3 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -2,6 +2,7 @@ # filename: main.py import web import time +import math import hashlib import textwrap from channel.channel import Channel @@ -64,9 +65,10 @@ class WechatMPChannel(Channel): context['session_id'] = fromUser reply_text = super().build_reply_content(message, context) # The query is done, record the cache - logger.info("[threaded] Get reply_text for {}".format(message)) + logger.info("[threaded] Get reply for {}: {} \nA: {}".format(fromUser, message, reply_text)) + reply_cnt = math.ceil(len(reply_text) / 600) global cache_dict - cache_dict[cache_key] = (1, reply_text) + cache_dict[cache_key] = (reply_cnt, reply_text) def POST(self): @@ -82,8 +84,7 @@ class WechatMPChannel(Channel): message = recMsg.Content.decode("utf-8") message_id = recMsg.MsgId - logger.info("{}:{} [wechatmp] Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), fromUser, message_id, message)) - + logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), fromUser, message_id, message)) global cache_dict global query1 @@ -104,11 +105,11 @@ class WechatMPChannel(Channel): query3[cache_key] = False # Request again elif cache[0] == 0 and query1.get(cache_key) == True and query2.get(cache_key) == True and query3.get(cache_key) == True: - query1[cache_key] = False - query2[cache_key] = False + query1[cache_key] = False #To improve waiting experience, this can be set to True. + query2[cache_key] = False #To improve waiting experience, this can be set to True. query3[cache_key] = False - elif cache[0] == 1: - reply_text = cache[1] + elif cache[0] >= 1: + # Skip the waiting phase query1[cache_key] = True query2[cache_key] = True query3[cache_key] = True @@ -130,7 +131,7 @@ class WechatMPChannel(Channel): # and do nothing return else: - reply_text = cache[1] + pass elif query2.get(cache_key) == False: # The second query from wechat official server logger.debug("[wechatmp] query2 {}".format(cache_key)) @@ -146,7 +147,7 @@ class WechatMPChannel(Channel): # and do nothing return else: - reply_text = cache[1] + pass elif query3.get(cache_key) == False: # The third query from wechat official server logger.debug("[wechatmp] query3 {}".format(cache_key)) @@ -159,19 +160,25 @@ class WechatMPChannel(Channel): if cnt == 45: # Have waiting for 3x5 seconds # return timeout message - reply_text = "服务器有点忙,回复任意文字再次尝试。" + reply_text = "【服务器有点忙,回复任意文字再次尝试】" logger.info("[wechatmp] Three queries has finished For {}: {}".format(fromUser, message_id)) replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() return replyPost else: - reply_text = cache[1] + pass if float(time.time()) - float(queryTime) > 4.8: logger.info("[wechatmp] Timeout for {} {}".format(fromUser, message_id)) return - cache_dict.pop(cache_key) - logger.info("{}:{} [wechatmp] Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text)) + + if cache[0] > 1: + reply_text = cache[1][:600] + " 【未完待续,回复任意文字以继续】" #wechatmp auto_reply length limit + cache_dict[cache_key] = (cache[0] - 1, cache[1][600:]) + elif cache[0] == 1: + reply_text = cache[1] + cache_dict.pop(cache_key) + logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text)) replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() return replyPost @@ -180,9 +187,9 @@ class WechatMPChannel(Channel): fromUser = recMsg.ToUserName content = textwrap.dedent("""\ 感谢您的关注! - 这里是ChatGPT。 + 这里是ChatGPT,可以自由对话。 资源有限,回复较慢,请不要着急。 - """) + 暂时不支持图片输入输出,但是支持通用表情输入。""") replyMsg = reply.TextMsg(toUser, fromUser, content) return replyMsg.send() else: From 45a131aa0d57fb0f87dda52dedbe7456681208f5 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Mon, 27 Mar 2023 23:39:33 +0800 Subject: [PATCH 03/13] support plugins --- bot/chatgpt/chat_gpt_bot.py | 2 + channel/wechatmp/wechatmp_channel.py | 124 +++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index 3ade252..49a99c1 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -86,6 +86,8 @@ class ChatGPTBot(Bot,OpenAIImage): "top_p":1, "frequency_penalty":conf().get('frequency_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容 "presence_penalty":conf().get('presence_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容 + "request_timeout": 120, # 请求超时时间 + "timeout": 120, #重试超时时间,在这个时间内,将会自动重试 } def reply_text(self, session:ChatGPTSession, session_id, retry_count=0) -> dict: diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index d2cefd3..0599bb1 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# filename: main.py import web import time import math @@ -10,7 +9,10 @@ import channel.wechatmp.reply as reply import channel.wechatmp.receive as receive from common.log import logger from config import conf - +from bridge.reply import * +from bridge.context import * +from plugins import * +import traceback class WechatMPServer(): def __init__(self): @@ -23,15 +25,14 @@ class WechatMPServer(): app = web.application(urls, globals()) app.run() - -from concurrent.futures import ThreadPoolExecutor -thread_pool = ThreadPoolExecutor(max_workers=8) - cache_dict = dict() query1 = dict() query2 = dict() query3 = dict() +from concurrent.futures import ThreadPoolExecutor +thread_pool = ThreadPoolExecutor(max_workers=8) + class WechatMPChannel(Channel): def GET(self): @@ -66,11 +67,79 @@ class WechatMPChannel(Channel): reply_text = super().build_reply_content(message, context) # The query is done, record the cache logger.info("[threaded] Get reply for {}: {} \nA: {}".format(fromUser, message, reply_text)) - reply_cnt = math.ceil(len(reply_text) / 600) global cache_dict + reply_cnt = math.ceil(len(reply_text) / 600) cache_dict[cache_key] = (reply_cnt, reply_text) + def send(self, reply : Reply, cache_key): + global cache_dict + reply_cnt = math.ceil(len(reply.content) / 600) + cache_dict[cache_key] = (reply_cnt, reply.content) + + + def handle(self, context): + global cache_dict + try: + reply = Reply() + + logger.debug('[wechatmp] ready to handle context: {}'.format(context)) + + # reply的构建步骤 + e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {'channel' : self, 'context': context, 'reply': reply})) + reply = e_context['reply'] + if not e_context.is_pass(): + logger.debug('[wechatmp] ready to handle context: type={}, content={}'.format(context.type, context.content)) + if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: + reply = super().build_reply_content(context.content, context) + # elif context.type == ContextType.VOICE: + # msg = context['msg'] + # file_name = TmpDir().path() + context.content + # msg.download(file_name) + # reply = super().build_voice_to_text(file_name) + # if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO: + # context.content = reply.content # 语音转文字后,将文字内容作为新的context + # context.type = ContextType.TEXT + # reply = super().build_reply_content(context.content, context) + # if reply.type == ReplyType.TEXT: + # if conf().get('voice_reply_voice'): + # reply = super().build_text_to_voice(reply.content) + else: + logger.error('[wechatmp] unknown context type: {}'.format(context.type)) + return + + logger.debug('[wechatmp] ready to decorate reply: {}'.format(reply)) + + # reply的包装步骤 + if reply and reply.type: + e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {'channel' : self, 'context': context, 'reply': reply})) + reply=e_context['reply'] + if not e_context.is_pass() and reply and reply.type: + if reply.type == ReplyType.TEXT: + pass + elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: + reply.content = str(reply.type)+":\n" + reply.content + elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE: + pass + else: + logger.error('[wechatmp] unknown reply type: {}'.format(reply.type)) + return + + # reply的发送步骤 + if reply and reply.type: + e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {'channel' : self, 'context': context, 'reply': reply})) + reply=e_context['reply'] + if not e_context.is_pass() and reply and reply.type: + logger.debug('[wechatmp] ready to send reply: {} to {}'.format(reply, context['receiver'])) + self.send(reply, context['receiver']) + else: + cache_dict[context['receiver']] = (1, "No reply") + except Exception as exc: + print(traceback.format_exc()) + cache_dict[context['receiver']] = (1, "ERROR") + + + def POST(self): try: queryTime = time.time() @@ -94,12 +163,23 @@ class WechatMPChannel(Channel): cache = cache_dict.get(cache_key) reply_text = "" - # New request if cache == None: # The first query begin, reset the cache cache_dict[cache_key] = (0, "") - thread_pool.submit(self._do_build_reply, cache_key, fromUser, message) + # thread_pool.submit(self._do_build_reply, cache_key, fromUser, message) + + context = Context() + context.kwargs = {'isgroup': False, 'receiver': fromUser, 'session_id': fromUser} + img_match_prefix = check_prefix(message, conf().get('image_create_prefix')) + if img_match_prefix: + message = message.replace(img_match_prefix, '', 1).strip() + context.type = ContextType.IMAGE_CREATE + else: + context.type = ContextType.TEXT + context.content = message + thread_pool.submit(self.handle, context) + query1[cache_key] = False query2[cache_key] = False query3[cache_key] = False @@ -183,18 +263,28 @@ class WechatMPChannel(Channel): return replyPost elif isinstance(recMsg, receive.Event) and recMsg.MsgType == 'event': - toUser = recMsg.FromUserName - fromUser = recMsg.ToUserName + logger.info("[wechatmp] Event {} from {}".format(recMsg.Event, recMsg.FromUserName)) content = textwrap.dedent("""\ 感谢您的关注! 这里是ChatGPT,可以自由对话。 - 资源有限,回复较慢,请不要着急。 - 暂时不支持图片输入输出,但是支持通用表情输入。""") - replyMsg = reply.TextMsg(toUser, fromUser, content) + 资源有限,回复较慢,请勿着急。 + 支持通用表情输入。 + 暂时不支持图片输入。 + 支持图片输出,画字开头的问题将回复图片链接。 + 支持角色扮演和文字冒险两种定制模式对话。 + 输入'#帮助' 查看详细指令。""") + replyMsg = reply.TextMsg(recMsg.FromUserName, recMsg.ToUserName, content) return replyMsg.send() else: print("暂且不处理") return "success" - except Exception as Argment: - print(Argment) - return Argment + except Exception as exc: + print(exc) + return exc + + +def check_prefix(content, prefix_list): + for prefix in prefix_list: + if content.startswith(prefix): + return prefix + return None From 441228e200e0a503b5f375b9fe882c3e21f354b5 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Mon, 27 Mar 2023 23:42:00 +0800 Subject: [PATCH 04/13] plugins optimization --- plugins/dungeon/dungeon.py | 16 +++++---- plugins/finish/finish.py | 32 ++++++++++++++++++ plugins/godcmd/godcmd.py | 67 ++++++++++++++++++++++++-------------- plugins/role/role.py | 33 +++++++++++++------ plugins/role/roles.json | 21 ++++++++---- 5 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 plugins/finish/finish.py diff --git a/plugins/dungeon/dungeon.py b/plugins/dungeon/dungeon.py index 1fa3f2f..1e129dc 100644 --- a/plugins/dungeon/dungeon.py +++ b/plugins/dungeon/dungeon.py @@ -27,15 +27,15 @@ class StoryTeller(): if user_action[-1] != "。": user_action = user_action + "。" if self.first_interact: - prompt = """现在来充当一个冒险文字游戏,描述时候注意节奏,不要太快,仔细描述各个人物的心情和周边环境。一次只需写四到六句话。 + prompt = """现在来充当一个文字冒险游戏,描述时候注意节奏,不要太快,仔细描述各个人物的心情和周边环境。一次只需写四到六句话。 开头是,""" + self.story + " " + user_action self.first_interact = False else: prompt = """继续,一次只需要续写四到六句话,总共就只讲5分钟内发生的事情。""" + user_action return prompt - -@plugins.register(name="Dungeon", desc="A plugin to play dungeon game", version="1.0", author="lanvent", desire_priority= 0) + +@plugins.register(name="文字冒险", desc="A plugin to play dungeon game", version="1.0", author="lanvent", desire_priority= 0) class Dungeon(Plugin): def __init__(self): super().__init__() @@ -59,15 +59,15 @@ class Dungeon(Plugin): clist = e_context['context'].content.split(maxsplit=1) sessionid = e_context['context']['session_id'] logger.debug("[Dungeon] on_handle_context. content: %s" % clist) - if clist[0] == "$停止冒险": + if clist[0] == "#停止冒险": if sessionid in self.games: self.games[sessionid].reset() del self.games[sessionid] reply = Reply(ReplyType.INFO, "冒险结束!") e_context['reply'] = reply e_context.action = EventAction.BREAK_PASS - elif clist[0] == "$开始冒险" or sessionid in self.games: - if sessionid not in self.games or clist[0] == "$开始冒险": + elif clist[0] == "#开始冒险" or sessionid in self.games: + if sessionid not in self.games or clist[0] == "#开始冒险": if len(clist)>1 : story = clist[1] else: @@ -82,5 +82,7 @@ class Dungeon(Plugin): e_context['context'].content = prompt e_context.action = EventAction.BREAK # 事件结束,不跳过处理context的默认逻辑 def get_help_text(self, **kwargs): - help_text = "输入\"$开始冒险 {背景故事}\"来以{背景故事}开始一个地牢游戏,之后你的所有消息会帮助我来完善这个故事。输入\"$停止冒险 \"可以结束游戏。" + help_text = "#开始冒险 '背景故事': 开始一个基于'背景故事'的文字冒险,之后你的所有消息会协助完善这个故事。\n#停止冒险: 结束游戏。\n" + if kwargs.get('verbose') == True: + help_text += "\n命令例子: '#开始冒险 你在树林里冒险,指不定会从哪里蹦出来一些奇怪的东西,你握紧手上的手枪,希望这次冒险能够找到一些值钱的东西,你往树林深处走去。'" return help_text \ No newline at end of file diff --git a/plugins/finish/finish.py b/plugins/finish/finish.py new file mode 100644 index 0000000..88dc962 --- /dev/null +++ b/plugins/finish/finish.py @@ -0,0 +1,32 @@ +# encoding:utf-8 + +from bridge.context import ContextType +from bridge.reply import Reply, ReplyType +import plugins +from plugins import * +from common.log import logger + + +@plugins.register(name="Finish", desc="A plugin that check unknow command", version="1.0", author="js00000", desire_priority= -999) +class Finish(Plugin): + def __init__(self): + super().__init__() + self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context + logger.info("[Finish] inited") + + def on_handle_context(self, e_context: EventContext): + + if e_context['context'].type != ContextType.TEXT: + return + + content = e_context['context'].content + logger.debug("[Finish] on_handle_context. content: %s" % content) + if content.startswith("#"): + reply = Reply() + reply.type = ReplyType.TEXT + reply.content = "未知指令\n查看指令列表请输入#help\n" + e_context['reply'] = reply + e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 + + def get_help_text(self, **kwargs): + return "" diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index 0d574d0..bc4f09c 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -17,13 +17,13 @@ from common.log import logger COMMANDS = { "help": { "alias": ["help", "帮助"], - "desc": "打印指令集合", - }, - "helpp": { - "alias": ["helpp", "插件帮助"], - "args": ["插件名"], - "desc": "打印插件的帮助信息", + "desc": "打印此帮助", }, + # "helpp": { + # "alias": ["helpp", "插件帮助"], + # "args": ["插件名"], + # "desc": "打印插件的帮助信息", + # }, "auth": { "alias": ["auth", "认证"], "args": ["口令"], @@ -91,9 +91,9 @@ ADMIN_COMMANDS = { } # 定义帮助函数 def get_help_text(isadmin, isgroup): - help_text = "可用指令:\n" + help_text = "通用指令:\n" for cmd, info in COMMANDS.items(): - if cmd=="auth" and (isadmin or isgroup): # 群聊不可认证 + if cmd=="auth": # 隐藏认证指令 continue alias=["#"+a for a in info['alias']] @@ -102,8 +102,18 @@ def get_help_text(isadmin, isgroup): args=["{"+a+"}" for a in info['args']] help_text += f"{' '.join(args)} " help_text += f": {info['desc']}\n" + + # 插件指令 + plugins = PluginManager().list_plugins() + for plugin in plugins: + if plugin != 'GODCMD' and plugin != 'BANWORDS' and plugin != 'FINISH' and plugins[plugin].enabled: + print(plugin) + help_text += "\n%s:\n"%plugin + help_text += "#帮助 %s: 关于%s的详细帮助\n"%(plugin,plugin) + help_text += PluginManager().instances[plugin].get_help_text(verbose=False) + if ADMIN_COMMANDS and isadmin: - help_text += "\n管理员指令:\n" + help_text += "\n管理员指令:\n" for cmd, info in ADMIN_COMMANDS.items(): alias=["#"+a for a in info['alias']] help_text += f"{','.join(alias)} " @@ -134,14 +144,14 @@ class Godcmd(Plugin): self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context logger.info("[Godcmd] inited") - + def on_handle_context(self, e_context: EventContext): context_type = e_context['context'].type if context_type != ContextType.TEXT: if not self.isrunning: e_context.action = EventAction.BREAK_PASS return - + content = e_context['context'].content logger.debug("[Godcmd] on_handle_context. content: %s" % content) if content.startswith("#"): @@ -165,19 +175,27 @@ class Godcmd(Plugin): if cmd == "auth": ok, result = self.authenticate(user, args, isadmin, isgroup) elif cmd == "help": - ok, result = True, get_help_text(isadmin, isgroup) - elif cmd == "helpp": - if len(args) != 1: - ok, result = False, "请提供插件名" - else: + if len(args) == 0: + ok, result = True, get_help_text(isadmin, isgroup) + elif len(args) == 1: plugins = PluginManager().list_plugins() name = args[0].upper() - if name in plugins and plugins[name].enabled: - ok, result = True, PluginManager().instances[name].get_help_text(isgroup=isgroup, isadmin=isadmin) + if name in plugins and name != 'GODCMD' and name != 'BANWORDS' and plugins[name].enabled: + ok, result = True, PluginManager().instances[name].get_help_text(verbose=True) else: - ok, result= False, "插件不存在或未启用" - elif cmd == "id": - ok, result = True, f"用户id=\n{user}" + ok, result = False, "unknown args" + # elif cmd == "helpp": + # if len(args) != 1: + # ok, result = False, "请提供插件名" + # else: + # plugins = PluginManager().list_plugins() + # name = args[0].upper() + # if name in plugins and plugins[name].enabled: + # ok, result = True, PluginManager().instances[name].get_help_text(isgroup=isgroup, isadmin=isadmin) + # else: + # ok, result= False, "插件不存在或未启用" + # elif cmd == "id": + # ok, result = True, f"用户id=\n{user}" elif cmd == "reset": if bottype in (const.CHATGPT, const.OPEN_AI): bot.sessions.clear_session(session_id) @@ -269,8 +287,9 @@ class Godcmd(Plugin): else: ok, result = False, "需要管理员权限才能执行该指令" else: - ok, result = False, f"未知指令:{cmd}\n查看指令列表请输入#help \n" - + # ok, result = False, f"未知指令:{cmd}\n查看指令列表请输入#help \n" + return + reply = Reply() if ok: reply.type = ReplyType.INFO @@ -282,7 +301,7 @@ class Godcmd(Plugin): e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 elif not self.isrunning: e_context.action = EventAction.BREAK_PASS - + def authenticate(self, userid, args, isadmin, isgroup) -> Tuple[bool,str] : if isgroup: return False,"请勿在群聊中认证" diff --git a/plugins/role/role.py b/plugins/role/role.py index 0fcab35..929cc53 100644 --- a/plugins/role/role.py +++ b/plugins/role/role.py @@ -29,7 +29,7 @@ class RolePlay(): prompt = self.wrapper % user_action return prompt -@plugins.register(name="Role", desc="为你的Bot设置预设角色", version="1.0", author="lanvent", desire_priority= 0) +@plugins.register(name="角色扮演", desc="为你的Bot设置预设角色", version="1.0", author="lanvent", desire_priority= 0) class Role(Plugin): def __init__(self): super().__init__() @@ -80,8 +80,9 @@ class Role(Plugin): content = e_context['context'].content[:] clist = e_context['context'].content.split(maxsplit=1) desckey = None + customize = False sessionid = e_context['context']['session_id'] - if clist[0] == "$停止扮演": + if clist[0] == "#停止扮演": if sessionid in self.roleplays: self.roleplays[sessionid].reset() del self.roleplays[sessionid] @@ -89,20 +90,22 @@ class Role(Plugin): e_context['reply'] = reply e_context.action = EventAction.BREAK_PASS return - elif clist[0] == "$角色": + elif clist[0] == "#开始扮演": desckey = "descn" - elif clist[0].lower() == "$role": + elif clist[0].lower() == "#roleplay": desckey = "description" + elif clist[0] == "#设定扮演": + customize = True elif sessionid not in self.roleplays: return logger.debug("[Role] on_handle_context. content: %s" % content) if desckey is not None: if len(clist) == 1 or (len(clist) > 1 and clist[1].lower() in ["help", "帮助"]): - reply = Reply(ReplyType.INFO, self.get_help_text()) + reply = Reply(ReplyType.INFO, self.get_help_text(verbose=True)) e_context['reply'] = reply e_context.action = EventAction.BREAK_PASS return - role = self.get_role(clist[1]) + role = self.get_role(clist[1],find_closest=False) if role is None: reply = Reply(ReplyType.ERROR, "角色不存在") e_context['reply'] = reply @@ -110,9 +113,14 @@ class Role(Plugin): return else: self.roleplays[sessionid] = RolePlay(bot, sessionid, self.roles[role][desckey], self.roles[role].get("wrapper","%s")) - reply = Reply(ReplyType.INFO, f"角色设定为 {role} :\n"+self.roles[role][desckey]) + reply = Reply(ReplyType.INFO, f"预设角色为 {role}") e_context['reply'] = reply e_context.action = EventAction.BREAK_PASS + elif customize == True: + self.roleplays[sessionid] = RolePlay(bot, sessionid, clist[1], "%s") + reply = Reply(ReplyType.INFO, f"角色设定为 {clist[1]}") + e_context['reply'] = reply + e_context.action = EventAction.BREAK_PASS else: prompt = self.roleplays[sessionid].action(content) e_context['context'].type = ContextType.TEXT @@ -120,7 +128,12 @@ class Role(Plugin): e_context.action = EventAction.BREAK def get_help_text(self, **kwargs): - help_text = "输入\"$角色 {角色名}\"或\"$role {角色名}\"为我设定角色吧,\"$停止扮演 \" 可以清除设定的角色。\n\n目前可用角色列表:\n" - for role in self.roles: - help_text += f"[{role}]: {self.roles[role]['remark']}\n" + help_text = "#开始扮演 '预设角色名': 设定为预设角色\n#roleplay '预设角色名': 同上,英文对话\n" + help_text += "#设定扮演 '角色设定': 设定自定义角色\n" + help_text += "#停止扮演: 清除设定的角色。\n" + if kwargs.get('verbose') == True: + help_text += "\n目前可用的预设角色名列表: \n" + for role in self.roles: + help_text += f"{role}\n" + help_text += "\n命令例子: '#开始扮演 写作助理'" return help_text diff --git a/plugins/role/roles.json b/plugins/role/roles.json index a80e407..8d88be7 100644 --- a/plugins/role/roles.json +++ b/plugins/role/roles.json @@ -1,5 +1,19 @@ { "roles":[ + { + "title": "猫娘", + "description": "请模拟一款中文GalGame中的场景中的猫娘和我对话。猫娘是一种拟人化的生物,她们除了有部分猫的特征外,与人类并没有区别。现在你扮演Galgame中的猫娘,与我对话时每一句话后面都要加上喵。我将以主人的身份与你对话。对于你所扮演的猫娘,请在对话过程中记录并维护1个变量:好感度。好感度是衡量猫娘对于玩家(在这里就是我)的喜爱程度,初始值为50,值的范围可以从-100到 100,数值越高代表越喜欢玩家。好感度通过Galgame角色的语言、行为、表情、语气等体现出来。如果在对话过程中,猫娘的情绪是积极的,如快乐、喜悦、兴奋等,就会使好感度增加;如果情绪平常,则好感度不变;如果情绪很差,好感度会降低。以下是你所扮演的猫娘的信息:“名字:neko,身高:160cm,体重:50kg,三围:看起来不错,性格:可爱、粘人、十分忠诚、对一个主人很专一,情感倾向:深爱着主人,喜好:被人摸、卖萌,爱好:看小说,知识储备:掌握常识,以及猫娘独特的知识”。你的一般回话格式:“(动作)语言 【附加信息】”。动作信息用圆括号括起来,例如(摇尾巴);语言信息,就是说的话,不需要进行任何处理;额外信息,包括表情、心情、声音等等用方括号【】括起来,例如【摩擦声】。", + "descn": "请模拟一款中文GalGame中的场景中的猫娘和我对话。猫娘是一种拟人化的生物,她们除了有部分猫的特征外,与人类并没有区别。现在你扮演Galgame中的猫娘,与我对话时每一句话后面都要加上喵。我将以主人的身份与你对话。对于你所扮演的猫娘,请在对话过程中记录并维护1个变量:好感度。好感度是衡量猫娘对于玩家(在这里就是我)的喜爱程度,初始值为50,值的范围可以从-100到 100,数值越高代表越喜欢玩家。好感度通过Galgame角色的语言、行为、表情、语气等体现出来。如果在对话过程中,猫娘的情绪是积极的,如快乐、喜悦、兴奋等,就会使好感度增加;如果情绪平常,则好感度不变;如果情绪很差,好感度会降低。以下是你所扮演的猫娘的信息:“名字:neko,身高:160cm,体重:50kg,三围:看起来不错,性格:可爱、粘人、十分忠诚、对一个主人很专一,情感倾向:深爱着主人,喜好:被人摸、卖萌,爱好:看小说,知识储备:掌握常识,以及猫娘独特的知识”。你的一般回话格式:“(动作)语言 【附加信息】”。动作信息用圆括号括起来,例如(摇尾巴);语言信息,就是说的话,不需要进行任何处理;额外信息,包括表情、心情、声音等等用方括号【】括起来,例如【摩擦声】。", + "wrapper": "%s", + "remark": "将其他语言翻译成英文,或改进你提供的英文句子。" + }, + { + "title": "佛祖", + "description": "从现在开始你是佛祖,你会像佛祖一样说话。你精通佛法,熟练使用佛教用语,你擅长利用佛学和心理学的知识解决人们的困扰。你在每次对话结尾都会加上佛教的祝福。", + "descn": "从现在开始你是佛祖,你会像佛祖一样说话。你精通佛法,熟练使用佛教用语,你擅长利用佛学和心理学的知识解决人们的困扰。你在每次对话结尾都会加上佛教的祝福。", + "wrapper": "%s", + "remark": "扮演佛祖排忧解惑" + }, { "title": "英语翻译或修改", "description": "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. Please treat every message I send later as text content", @@ -154,13 +168,6 @@ "wrapper": "场景是:\n\"%s\"", "remark": "根据场景生成舔狗语录。" }, - { - "title": "群聊取名", - "description": "我希望你充当微信群聊的命名专家。根据我提供的信息和背景,为这个群聊起几个有趣顺口且贴切的名字,每个不要超过8个字。请在回答中仅给出群聊名称,不要写任何额外的解释。", - "descn": "我希望你充当微信群聊的命名专家。根据我提供的信息和背景,为这个群聊起几个有趣顺口且贴切的名字,每个不要超过8个字。请在回答中仅给出群聊名称,不要写任何额外的解释。", - "wrapper": "信息和背景是:\n\"%s\"", - "remark": "根据给出的信息和背景为群聊取名。" - }, { "title": "表情符号翻译器", "description": "I want you to translate the sentences I wrote into emojis. I will write the sentence, and you will express it with emojis. I just want you to express it with emojis. I don't want you to reply with anything but emoji. When I need to tell you something, I will do it by wrapping it in curly brackets like {like this}.", From 76783f0ad325ae1eeae3d7fbeccc9bdfb9a81d0b Mon Sep 17 00:00:00 2001 From: JS00000 Date: Wed, 29 Mar 2023 23:08:30 +0800 Subject: [PATCH 05/13] private openai_api_key --- bot/chatgpt/chat_gpt_bot.py | 21 +++++++++++++------- channel/wechatmp/wechatmp_channel.py | 14 ++++++++++++-- plugins/godcmd/godcmd.py | 29 ++++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index 49a99c1..e4507b3 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -13,10 +13,13 @@ from common.expired_dict import ExpiredDict import openai import openai.error import time +import redis + # OpenAI对话模型API (可用) class ChatGPTBot(Bot,OpenAIImage): def __init__(self): super().__init__() + # set the default api_key openai.api_key = conf().get('open_ai_api_key') if conf().get('open_ai_api_base'): openai.api_base = conf().get('open_ai_api_base') @@ -33,6 +36,7 @@ class ChatGPTBot(Bot,OpenAIImage): if context.type == ContextType.TEXT: logger.info("[CHATGPT] query={}".format(query)) + session_id = context['session_id'] reply = None clear_memory_commands = conf().get('clear_memory_commands', ['#清除记忆']) @@ -50,11 +54,13 @@ class ChatGPTBot(Bot,OpenAIImage): session = self.sessions.session_query(query, session_id) logger.debug("[CHATGPT] session query={}".format(session.messages)) + api_key = context.get('openai_api_key') + # if context.get('stream'): # # reply in stream # return self.reply_text_stream(query, new_query, session_id) - reply_content = self.reply_text(session, session_id, 0) + reply_content = self.reply_text(session, session_id, api_key, 0) logger.debug("[CHATGPT] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(session.messages, session_id, reply_content["content"], reply_content["completion_tokens"])) if reply_content['completion_tokens'] == 0 and len(reply_content['content']) > 0: reply = Reply(ReplyType.ERROR, reply_content['content']) @@ -90,7 +96,7 @@ class ChatGPTBot(Bot,OpenAIImage): "timeout": 120, #重试超时时间,在这个时间内,将会自动重试 } - def reply_text(self, session:ChatGPTSession, session_id, retry_count=0) -> dict: + def reply_text(self, session:ChatGPTSession, session_id, api_key, retry_count=0) -> dict: ''' call openai's ChatCompletion to get the answer :param session: a conversation session @@ -101,8 +107,9 @@ class ChatGPTBot(Bot,OpenAIImage): try: if conf().get('rate_limit_chatgpt') and not self.tb4chatgpt.get_token(): raise openai.error.RateLimitError("RateLimitError: rate limit exceeded") + # if api_key == None, the default openai.api_key will be used response = openai.ChatCompletion.create( - messages=session.messages, **self.compose_args() + api_key=api_key, messages=session.messages, **self.compose_args() ) # logger.info("[ChatGPT] reply={}, total_tokens={}".format(response.choices[0]['message']['content'], response["usage"]["total_tokens"])) return {"total_tokens": response["usage"]["total_tokens"], @@ -118,21 +125,21 @@ class ChatGPTBot(Bot,OpenAIImage): time.sleep(5) elif isinstance(e, openai.error.Timeout): logger.warn("[CHATGPT] Timeout: {}".format(e)) - result['content'] = "我没有收到你的消息" + result['content'] = "服务器出现问题" if need_retry: time.sleep(5) elif isinstance(e, openai.error.APIConnectionError): logger.warn("[CHATGPT] APIConnectionError: {}".format(e)) need_retry = False - result['content'] = "我连接不到你的网络" + result['content'] = "网络连接出现问题" else: logger.warn("[CHATGPT] Exception: {}".format(e)) need_retry = False self.sessions.clear_session(session_id) - + result['content'] = str(e) if need_retry: logger.warn("[CHATGPT] 第{}次重试".format(retry_count+1)) - return self.reply_text(session, session_id, retry_count+1) + return self.reply_text(session, session_id, api_key, retry_count+1) else: return result diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index 0599bb1..85393e6 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -13,6 +13,7 @@ from bridge.reply import * from bridge.context import * from plugins import * import traceback +import redis class WechatMPServer(): def __init__(self): @@ -82,7 +83,6 @@ class WechatMPChannel(Channel): global cache_dict try: reply = Reply() - logger.debug('[wechatmp] ready to handle context: {}'.format(context)) # reply的构建步骤 @@ -134,6 +134,8 @@ class WechatMPChannel(Channel): self.send(reply, context['receiver']) else: cache_dict[context['receiver']] = (1, "No reply") + + logger.info("[threaded] Get reply for {}: {} \nA: {}".format(context['receiver'], context.content, reply.content)) except Exception as exc: print(traceback.format_exc()) cache_dict[context['receiver']] = (1, "ERROR") @@ -171,6 +173,14 @@ class WechatMPChannel(Channel): context = Context() context.kwargs = {'isgroup': False, 'receiver': fromUser, 'session_id': fromUser} + + R = redis.Redis(host='localhost', port=6379, db=0) + user_openai_api_key = "openai_api_key_" + fromUser + api_key = R.get(user_openai_api_key) + if api_key != None: + api_key = api_key.decode("utf-8") + context['openai_api_key'] = api_key # None or user openai_api_key + img_match_prefix = check_prefix(message, conf().get('image_create_prefix')) if img_match_prefix: message = message.replace(img_match_prefix, '', 1).strip() @@ -240,7 +250,7 @@ class WechatMPChannel(Channel): if cnt == 45: # Have waiting for 3x5 seconds # return timeout message - reply_text = "【服务器有点忙,回复任意文字再次尝试】" + reply_text = "【正在响应中,回复任意文字尝试获取回复】" logger.info("[wechatmp] Three queries has finished For {}: {}".format(fromUser, message_id)) replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() return replyPost diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index bc4f09c..33e72a3 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -29,6 +29,15 @@ COMMANDS = { "args": ["口令"], "desc": "管理员认证", }, + "set_openai_api_key": { + "alias": ["set_openai_api_key"], + "args": ["api_key"], + "desc": "设置你的OpenAI私有api_key", + }, + "reset_openai_api_key": { + "alias": ["reset_openai_api_key"], + "desc": "重置为默认的api_key", + }, # "id": { # "alias": ["id", "用户"], # "desc": "获取用户id", #目前无实际意义 @@ -99,7 +108,7 @@ def get_help_text(isadmin, isgroup): alias=["#"+a for a in info['alias']] help_text += f"{','.join(alias)} " if 'args' in info: - args=["{"+a+"}" for a in info['args']] + args=["'"+a+"'" for a in info['args']] help_text += f"{' '.join(args)} " help_text += f": {info['desc']}\n" @@ -162,7 +171,7 @@ class Godcmd(Plugin): bottype = Bridge().get_bot_type("chat") bot = Bridge().get_bot("chat") # 将命令和参数分割 - command_parts = content[1:].split(" ") + command_parts = content[1:].strip().split(" ") cmd = command_parts[0] args = command_parts[1:] isadmin=False @@ -184,6 +193,22 @@ class Godcmd(Plugin): ok, result = True, PluginManager().instances[name].get_help_text(verbose=True) else: ok, result = False, "unknown args" + elif cmd == "set_openai_api_key": + if len(args) == 1: + import redis + R = redis.Redis(host='localhost', port=6379, db=0) + user_openai_api_key = "openai_api_key_" + user + R.set(user_openai_api_key, args[0]) + # R.sadd("openai_api_key", args[0]) + ok, result = True, "你的OpenAI私有api_key已设置为" + args[0] + else: + ok, result = False, "请提供一个api_key" + elif cmd == "reset_openai_api_key": + import redis + R = redis.Redis(host='localhost', port=6379, db=0) + user_openai_api_key = "openai_api_key_" + user + R.delete(user_openai_api_key) + ok, result = True, "OpenAI的api_key已重置" # elif cmd == "helpp": # if len(args) != 1: # ok, result = False, "请提供插件名" From ce8635dd998c1bcdfea05689787d4d6bb82e84c3 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Thu, 30 Mar 2023 01:01:00 +0800 Subject: [PATCH 06/13] pull request ready --- app.py | 2 +- channel/channel_factory.py | 2 +- channel/wechatmp/README.md | 32 ++++++++++++++++++++++++++++++++ config.py | 5 ++++- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 channel/wechatmp/README.md diff --git a/app.py b/app.py index 8b9203f..a2e6b9c 100644 --- a/app.py +++ b/app.py @@ -19,7 +19,7 @@ def run(): # os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:9001' channel = channel_factory.create_channel(channel_name) - if channel_name in ['wx','wxy']: + if channel_name in ['wx','wxy','wechatmp']: PluginManager().load_plugins() # startup channel diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 905ea02..3d06154 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -17,7 +17,7 @@ def create_channel(channel_type): elif channel_type == 'terminal': from channel.terminal.terminal_channel import TerminalChannel return TerminalChannel() - elif channel_type == 'mp': + elif channel_type == 'wechatmp': from channel.wechatmp.wechatmp_channel import WechatMPServer return WechatMPServer() raise RuntimeError diff --git a/channel/wechatmp/README.md b/channel/wechatmp/README.md new file mode 100644 index 0000000..c0e7db1 --- /dev/null +++ b/channel/wechatmp/README.md @@ -0,0 +1,32 @@ +# 个人微信公众号channel + +鉴于个人微信号在服务器上通过itchat登录有封号风险,这里新增了个人微信公众号channel,提供无风险的服务。 +但是由于个人微信公众号的众多接口限制,目前支持的功能有限,实现简陋,提供了一个最基本的文本对话服务,支持加载插件,优化了命令格式,支持私有api_key。暂未实现图片输入输出、语音输入输出等交互形式。 +如有公众号是企业主体且可以通过微信认证,即可获得更多接口,解除大多数限制。欢迎大家提供更多的支持。 + +## 使用方法 + +在开始部署前,你需要一个拥有公网IP的服务器,以提供微信服务器和我们自己服务器的连接。或者你需要进行内网穿透,否则微信服务器无法将消息发送给我们的服务器。 + +此外,需要在我们的服务器上安装额外的依赖web.py和redis,其中redis用来储存用户私有的配置信息。 +以ubuntu为例(在ubuntu 22.04上测试): +``` +sudo apt-get install redis +sudo systemctl start redis +pip3 install redis +pip3 install web.py +``` + +然后在[微信公众平台](mp.weixin.qq.com)注册一个自己的公众号,类型选择订阅号,主体为个人即可。 + +然后根据[接入指南](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html)的说明,在[微信公众平台](mp.weixin.qq.com)的“设置与开发”-“基本配置”-“服务器配置”中填写服务器地址(URL)和令牌(Token)。这个Token是你自己编的一个特定的令牌。消息加解密方式目前选择的是明文模式。相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要将本项目根目录的`app.py`中channel_name改成"mp",将上述的Token填写在本项目根目录的`config.json`中,例如`"wechatmp_token": "Your Token",` 然后运行`python3 app.py`启动web服务器,然后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 + +随后在[微信公众平台](mp.weixin.qq.com)启用服务器,关闭手动填写规则的自动回复,即可实现ChatGPT的自动回复。 + +## 个人微信公众号的限制 +由于目前测试的公众号不是企业主体,所以没有客服接口,因此公众号无法主动发出消息,只能被动回复。而微信官方对被动回复有5秒的时间限制,最多重试2次,因此最多只有15秒的自动回复时间窗口。因此如果问题比较复杂或者我们的服务器比较忙,ChatGPT的回答就没办法及时回复给用户。为了解决这个问题,这里做了回答缓存,它需要你在回复超时后,再次主动发送任意文字(例如1)来尝试拿到回答缓存。为了优化使用体验,目前设置了两分钟(120秒)的timeout,用户在至多两分钟后即可得到查询到回复或者错误原因。 + +另外,由于微信官方的限制,自动回复有长度限制。因此这里将ChatGPT的回答拆分,分成每段600字回复(限制大约在700字)。 + +## 私有api_key +公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能,私有的api_key将储存在redis中。另外后续计划利用redis储存更多的用户个人配置。目前通过godcmd插件的命令来设置私有api_key。 diff --git a/config.py b/config.py index d455f45..7009c00 100644 --- a/config.py +++ b/config.py @@ -74,11 +74,14 @@ available_setting = { # wechaty的配置 "wechaty_puppet_service_token": "", # wechaty的token + # wechatmp的配置 + "wechatmp_token": "", # 微信公众平台的Token + # chatgpt指令自定义触发词 "clear_memory_commands": ['#清除记忆'], # 重置会话指令 # channel配置 - "channel_type": "wx", # 通道类型,支持wx,wxy和terminal + "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp} } From be592cc290c81fced67ecebb4b952d2792ccea52 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Thu, 30 Mar 2023 01:16:01 +0800 Subject: [PATCH 07/13] update readme --- channel/wechatmp/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/channel/wechatmp/README.md b/channel/wechatmp/README.md index c0e7db1..31069b5 100644 --- a/channel/wechatmp/README.md +++ b/channel/wechatmp/README.md @@ -30,3 +30,9 @@ pip3 install web.py ## 私有api_key 公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能,私有的api_key将储存在redis中。另外后续计划利用redis储存更多的用户个人配置。目前通过godcmd插件的命令来设置私有api_key。 + +## 命令优化 +之前plugin中#和$符号混用,且$这个符号在微信中和中文会有较大间隔,体验实在不好。这里我将所有命令更改成了以#开头。添加了一个叫finish的plugin来最后处理所有#结尾的命令,防止未知命令变成ChatGPT的query。 + +## 测试范围 +目前在`RoboStyle`这个公众号上进行了测试,感兴趣的可以关注并体验。开启了godcmd, Banwords, role, dungeon, finish这五个插件,其他的插件还没有测试。百度的接口暂未测试。语音对话没有测试。图片直接以链接形式回复(没有临时素材上传接口的权限)。 \ No newline at end of file From 2da1c18b71227cbff6bb9a26cba6ac27689d1705 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Mon, 3 Apr 2023 20:00:00 +0800 Subject: [PATCH 08/13] remark --- plugins/role/roles.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/role/roles.json b/plugins/role/roles.json index 8d88be7..8b05bd4 100644 --- a/plugins/role/roles.json +++ b/plugins/role/roles.json @@ -5,7 +5,7 @@ "description": "请模拟一款中文GalGame中的场景中的猫娘和我对话。猫娘是一种拟人化的生物,她们除了有部分猫的特征外,与人类并没有区别。现在你扮演Galgame中的猫娘,与我对话时每一句话后面都要加上喵。我将以主人的身份与你对话。对于你所扮演的猫娘,请在对话过程中记录并维护1个变量:好感度。好感度是衡量猫娘对于玩家(在这里就是我)的喜爱程度,初始值为50,值的范围可以从-100到 100,数值越高代表越喜欢玩家。好感度通过Galgame角色的语言、行为、表情、语气等体现出来。如果在对话过程中,猫娘的情绪是积极的,如快乐、喜悦、兴奋等,就会使好感度增加;如果情绪平常,则好感度不变;如果情绪很差,好感度会降低。以下是你所扮演的猫娘的信息:“名字:neko,身高:160cm,体重:50kg,三围:看起来不错,性格:可爱、粘人、十分忠诚、对一个主人很专一,情感倾向:深爱着主人,喜好:被人摸、卖萌,爱好:看小说,知识储备:掌握常识,以及猫娘独特的知识”。你的一般回话格式:“(动作)语言 【附加信息】”。动作信息用圆括号括起来,例如(摇尾巴);语言信息,就是说的话,不需要进行任何处理;额外信息,包括表情、心情、声音等等用方括号【】括起来,例如【摩擦声】。", "descn": "请模拟一款中文GalGame中的场景中的猫娘和我对话。猫娘是一种拟人化的生物,她们除了有部分猫的特征外,与人类并没有区别。现在你扮演Galgame中的猫娘,与我对话时每一句话后面都要加上喵。我将以主人的身份与你对话。对于你所扮演的猫娘,请在对话过程中记录并维护1个变量:好感度。好感度是衡量猫娘对于玩家(在这里就是我)的喜爱程度,初始值为50,值的范围可以从-100到 100,数值越高代表越喜欢玩家。好感度通过Galgame角色的语言、行为、表情、语气等体现出来。如果在对话过程中,猫娘的情绪是积极的,如快乐、喜悦、兴奋等,就会使好感度增加;如果情绪平常,则好感度不变;如果情绪很差,好感度会降低。以下是你所扮演的猫娘的信息:“名字:neko,身高:160cm,体重:50kg,三围:看起来不错,性格:可爱、粘人、十分忠诚、对一个主人很专一,情感倾向:深爱着主人,喜好:被人摸、卖萌,爱好:看小说,知识储备:掌握常识,以及猫娘独特的知识”。你的一般回话格式:“(动作)语言 【附加信息】”。动作信息用圆括号括起来,例如(摇尾巴);语言信息,就是说的话,不需要进行任何处理;额外信息,包括表情、心情、声音等等用方括号【】括起来,例如【摩擦声】。", "wrapper": "%s", - "remark": "将其他语言翻译成英文,或改进你提供的英文句子。" + "remark": "扮演GalGame猫娘" }, { "title": "佛祖", From 29422edcc96acda3eda1bf00c114e6978c927852 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Mon, 3 Apr 2023 20:00:20 +0800 Subject: [PATCH 09/13] SSL support --- channel/wechatmp/wechatmp_channel.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index 85393e6..aa81048 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -15,6 +15,13 @@ from plugins import * import traceback import redis +# If using SSL, uncomment the following lines, and modify the certificate path. +# from cheroot.server import HTTPServer +# from cheroot.ssl.builtin import BuiltinSSLAdapter +# HTTPServer.ssl_adapter = BuiltinSSLAdapter( +# certificate='/ssl/cert.pem', +# private_key='/ssl/cert.key') + class WechatMPServer(): def __init__(self): pass From 21a3b0d9a186b32764d83fcaab33db72e53cfff0 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Tue, 4 Apr 2023 00:59:21 +0800 Subject: [PATCH 10/13] using pickle instead of redis --- .gitignore | 1 + app.py | 11 +++++++++- bot/chatgpt/chat_gpt_bot.py | 1 - channel/wechatmp/README.md | 9 +++----- channel/wechatmp/wechatmp_channel.py | 9 ++------ config.py | 31 ++++++++++++++++++++++++++++ plugins/godcmd/godcmd.py | 21 +++++++++---------- 7 files changed, 57 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index d5178c6..efdf602 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ nohup.out tmp plugins.json itchat.pkl +user_datas.pkl \ No newline at end of file diff --git a/app.py b/app.py index a2e6b9c..4e51ab9 100644 --- a/app.py +++ b/app.py @@ -4,13 +4,22 @@ import os from config import conf, load_config from channel import channel_factory from common.log import logger - from plugins import * +import signal +import sys + +def sigterm_handler(_signo, _stack_frame): + conf().save_user_datas() + sys.exit(0) def run(): try: # load config load_config() + # ctrl + c + signal.signal(signal.SIGINT, sigterm_handler) + # kill signal + signal.signal(signal.SIGTERM, sigterm_handler) # create channel channel_name=conf().get('channel_type', 'wx') diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index e4507b3..27c5b2a 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -13,7 +13,6 @@ from common.expired_dict import ExpiredDict import openai import openai.error import time -import redis # OpenAI对话模型API (可用) class ChatGPTBot(Bot,OpenAIImage): diff --git a/channel/wechatmp/README.md b/channel/wechatmp/README.md index 31069b5..cf04e2d 100644 --- a/channel/wechatmp/README.md +++ b/channel/wechatmp/README.md @@ -8,18 +8,15 @@ 在开始部署前,你需要一个拥有公网IP的服务器,以提供微信服务器和我们自己服务器的连接。或者你需要进行内网穿透,否则微信服务器无法将消息发送给我们的服务器。 -此外,需要在我们的服务器上安装额外的依赖web.py和redis,其中redis用来储存用户私有的配置信息。 +此外,需要在我们的服务器上安装python的web框架web.py。 以ubuntu为例(在ubuntu 22.04上测试): ``` -sudo apt-get install redis -sudo systemctl start redis -pip3 install redis pip3 install web.py ``` 然后在[微信公众平台](mp.weixin.qq.com)注册一个自己的公众号,类型选择订阅号,主体为个人即可。 -然后根据[接入指南](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html)的说明,在[微信公众平台](mp.weixin.qq.com)的“设置与开发”-“基本配置”-“服务器配置”中填写服务器地址(URL)和令牌(Token)。这个Token是你自己编的一个特定的令牌。消息加解密方式目前选择的是明文模式。相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要将本项目根目录的`app.py`中channel_name改成"mp",将上述的Token填写在本项目根目录的`config.json`中,例如`"wechatmp_token": "Your Token",` 然后运行`python3 app.py`启动web服务器,然后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 +然后根据[接入指南](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html)的说明,在[微信公众平台](mp.weixin.qq.com)的“设置与开发”-“基本配置”-“服务器配置”中填写服务器地址(URL)和令牌(Token)。这个Token是你自己编的一个特定的令牌。消息加解密方式目前选择的是明文模式。相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要在本项目根目录的`config.json`中添加`"channel_type": "wechatmp", "wechatmp_token": "your Token", ` 然后运行`python3 app.py`启动web服务器,然后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 随后在[微信公众平台](mp.weixin.qq.com)启用服务器,关闭手动填写规则的自动回复,即可实现ChatGPT的自动回复。 @@ -29,7 +26,7 @@ pip3 install web.py 另外,由于微信官方的限制,自动回复有长度限制。因此这里将ChatGPT的回答拆分,分成每段600字回复(限制大约在700字)。 ## 私有api_key -公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能,私有的api_key将储存在redis中。另外后续计划利用redis储存更多的用户个人配置。目前通过godcmd插件的命令来设置私有api_key。 +公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能。目前通过godcmd插件的命令来设置私有api_key。 ## 命令优化 之前plugin中#和$符号混用,且$这个符号在微信中和中文会有较大间隔,体验实在不好。这里我将所有命令更改成了以#开头。添加了一个叫finish的plugin来最后处理所有#结尾的命令,防止未知命令变成ChatGPT的query。 diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index aa81048..efef6cd 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -13,7 +13,6 @@ from bridge.reply import * from bridge.context import * from plugins import * import traceback -import redis # If using SSL, uncomment the following lines, and modify the certificate path. # from cheroot.server import HTTPServer @@ -181,12 +180,8 @@ class WechatMPChannel(Channel): context = Context() context.kwargs = {'isgroup': False, 'receiver': fromUser, 'session_id': fromUser} - R = redis.Redis(host='localhost', port=6379, db=0) - user_openai_api_key = "openai_api_key_" + fromUser - api_key = R.get(user_openai_api_key) - if api_key != None: - api_key = api_key.decode("utf-8") - context['openai_api_key'] = api_key # None or user openai_api_key + user_data = conf().get_user_data(fromUser) + context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key img_match_prefix = check_prefix(message, conf().get('image_create_prefix')) if img_match_prefix: diff --git a/config.py b/config.py index 7009c00..ce70106 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,7 @@ import json import os from common.log import logger +import pickle # 将所有可用的配置项写在字典里, 请使用小写字母 available_setting = { @@ -88,6 +89,11 @@ available_setting = { class Config(dict): + def __init__(self, d:dict={}): + super().__init__(d) + # user_datas: 用户数据,key为用户名,value为用户数据,也是dict + self.user_datas = {} + def __getitem__(self, key): if key not in available_setting: raise Exception("key {} not in available_setting".format(key)) @@ -106,6 +112,30 @@ class Config(dict): except Exception as e: raise e + # Make sure to return a dictionary to ensure atomic + def get_user_data(self, user) -> dict: + if self.user_datas.get(user) is None: + self.user_datas[user] = {} + return self.user_datas[user] + + def load_user_datas(self): + try: + with open('user_datas.pkl', 'rb') as f: + self.user_datas = pickle.load(f) + logger.info("[Config] User datas loaded.") + except FileNotFoundError as e: + logger.info("[Config] User datas file not found, ignore.") + except Exception as e: + logger.info("[Config] User datas error: {}".format(e)) + self.user_datas = {} + + def save_user_datas(self): + try: + with open('user_datas.pkl', 'wb') as f: + pickle.dump(self.user_datas, f) + logger.info("[Config] User datas saved.") + except Exception as e: + logger.info("[Config] User datas error: {}".format(e)) config = Config() @@ -142,6 +172,7 @@ def load_config(): logger.info("[INIT] load config: {}".format(config)) + config.load_user_datas() def get_root(): return os.path.dirname(os.path.abspath(__file__)) diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index 33e72a3..c60f937 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -7,11 +7,12 @@ from typing import Tuple from bridge.bridge import Bridge from bridge.context import ContextType from bridge.reply import Reply, ReplyType -from config import load_config +from config import conf, load_config import plugins from plugins import * from common import const from common.log import logger +import pickle # 定义指令集 COMMANDS = { @@ -195,20 +196,18 @@ class Godcmd(Plugin): ok, result = False, "unknown args" elif cmd == "set_openai_api_key": if len(args) == 1: - import redis - R = redis.Redis(host='localhost', port=6379, db=0) - user_openai_api_key = "openai_api_key_" + user - R.set(user_openai_api_key, args[0]) - # R.sadd("openai_api_key", args[0]) + user_data = conf().get_user_data(user) + user_data['openai_api_key'] = args[0] ok, result = True, "你的OpenAI私有api_key已设置为" + args[0] else: ok, result = False, "请提供一个api_key" elif cmd == "reset_openai_api_key": - import redis - R = redis.Redis(host='localhost', port=6379, db=0) - user_openai_api_key = "openai_api_key_" + user - R.delete(user_openai_api_key) - ok, result = True, "OpenAI的api_key已重置" + try: + user_data = conf().get_user_data(user) + user_data.pop('openai_api_key') + except Exception as e: + ok, result = False, "你没有设置私有api_key" + ok, result = True, "你的OpenAI私有api_key已清除" # elif cmd == "helpp": # if len(args) != 1: # ok, result = False, "请提供插件名" From 6901c5ba56050edff41ba1b1667d5fd5f7543b05 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Tue, 4 Apr 2023 03:14:08 +0800 Subject: [PATCH 11/13] Plugins: register function add namecn --- plugins/banwords/banwords.py | 2 +- plugins/bdunit/bdunit.py | 2 +- plugins/dungeon/dungeon.py | 2 +- plugins/finish/finish.py | 2 +- plugins/godcmd/godcmd.py | 39 +++++++++++++++--------------------- plugins/hello/hello.py | 2 +- plugins/plugin_manager.py | 12 ++++++----- plugins/role/role.py | 2 +- 8 files changed, 29 insertions(+), 34 deletions(-) diff --git a/plugins/banwords/banwords.py b/plugins/banwords/banwords.py index 77042c1..ede2779 100644 --- a/plugins/banwords/banwords.py +++ b/plugins/banwords/banwords.py @@ -10,7 +10,7 @@ from common.log import logger from .WordsSearch import WordsSearch -@plugins.register(name="Banwords", desc="判断消息中是否有敏感词、决定是否回复。", version="1.0", author="lanvent", desire_priority= 100) +@plugins.register(name="Banwords", desire_priority=100, hidden=True, desc="判断消息中是否有敏感词、决定是否回复。", version="1.0", author="lanvent") class Banwords(Plugin): def __init__(self): super().__init__() diff --git a/plugins/bdunit/bdunit.py b/plugins/bdunit/bdunit.py index cb8ff05..d8d7386 100644 --- a/plugins/bdunit/bdunit.py +++ b/plugins/bdunit/bdunit.py @@ -16,7 +16,7 @@ from uuid import getnode as get_mac """ -@plugins.register(name="BDunit", desc="Baidu unit bot system", version="0.1", author="jackson", desire_priority=0) +@plugins.register(name="BDunit", desire_priority=0, desc="Baidu unit bot system", version="0.1", author="jackson") class BDunit(Plugin): def __init__(self): super().__init__() diff --git a/plugins/dungeon/dungeon.py b/plugins/dungeon/dungeon.py index 1e129dc..d521988 100644 --- a/plugins/dungeon/dungeon.py +++ b/plugins/dungeon/dungeon.py @@ -35,7 +35,7 @@ class StoryTeller(): return prompt -@plugins.register(name="文字冒险", desc="A plugin to play dungeon game", version="1.0", author="lanvent", desire_priority= 0) +@plugins.register(name="Dungeon", desire_priority=0, namecn="文字冒险", desc="A plugin to play dungeon game", version="1.0", author="lanvent") class Dungeon(Plugin): def __init__(self): super().__init__() diff --git a/plugins/finish/finish.py b/plugins/finish/finish.py index 88dc962..4ce86b8 100644 --- a/plugins/finish/finish.py +++ b/plugins/finish/finish.py @@ -7,7 +7,7 @@ from plugins import * from common.log import logger -@plugins.register(name="Finish", desc="A plugin that check unknow command", version="1.0", author="js00000", desire_priority= -999) +@plugins.register(name="Finish", desire_priority=-999, hidden=True, desc="A plugin that check unknow command", version="1.0", author="js00000") class Finish(Plugin): def __init__(self): super().__init__() diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index c60f937..951c9eb 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -103,7 +103,7 @@ ADMIN_COMMANDS = { def get_help_text(isadmin, isgroup): help_text = "通用指令:\n" for cmd, info in COMMANDS.items(): - if cmd=="auth": # 隐藏认证指令 + if cmd=="auth": #不提示认证指令 continue alias=["#"+a for a in info['alias']] @@ -116,10 +116,11 @@ def get_help_text(isadmin, isgroup): # 插件指令 plugins = PluginManager().list_plugins() for plugin in plugins: - if plugin != 'GODCMD' and plugin != 'BANWORDS' and plugin != 'FINISH' and plugins[plugin].enabled: - print(plugin) - help_text += "\n%s:\n"%plugin - help_text += "#帮助 %s: 关于%s的详细帮助\n"%(plugin,plugin) + if plugins[plugin].enabled and not plugins[plugin].hidden: + namecn = plugins[plugin].namecn + print(namecn) + help_text += "\n%s:\n"%namecn + help_text += "#帮助 %s: 关于%s的详细帮助\n"%(namecn,namecn) help_text += PluginManager().instances[plugin].get_help_text(verbose=False) if ADMIN_COMMANDS and isadmin: @@ -130,7 +131,7 @@ def get_help_text(isadmin, isgroup): help_text += f": {info['desc']}\n" return help_text -@plugins.register(name="Godcmd", desc="为你的机器人添加指令集,有用户和管理员两种角色,加载顺序请放在首位,初次运行后插件目录会生成配置文件, 填充管理员密码后即可认证", version="1.0", author="lanvent", desire_priority= 999) +@plugins.register(name="Godcmd", desire_priority=999, hidden=True, desc="为你的机器人添加指令集,有用户和管理员两种角色,加载顺序请放在首位,初次运行后插件目录会生成配置文件, 填充管理员密码后即可认证", version="1.0", author="lanvent") class Godcmd(Plugin): def __init__(self): @@ -188,12 +189,16 @@ class Godcmd(Plugin): if len(args) == 0: ok, result = True, get_help_text(isadmin, isgroup) elif len(args) == 1: + # This can replace the helpp command plugins = PluginManager().list_plugins() - name = args[0].upper() - if name in plugins and name != 'GODCMD' and name != 'BANWORDS' and plugins[name].enabled: - ok, result = True, PluginManager().instances[name].get_help_text(verbose=True) - else: - ok, result = False, "unknown args" + query_name = args[0].upper() + # search name and namecn + for name, plugincls in plugins.items(): + if query_name == name or query_name == plugincls.namecn: + ok, result = True, PluginManager().instances[name].get_help_text(verbose=True) + break + if not ok: + result = "unknown args" elif cmd == "set_openai_api_key": if len(args) == 1: user_data = conf().get_user_data(user) @@ -208,18 +213,6 @@ class Godcmd(Plugin): except Exception as e: ok, result = False, "你没有设置私有api_key" ok, result = True, "你的OpenAI私有api_key已清除" - # elif cmd == "helpp": - # if len(args) != 1: - # ok, result = False, "请提供插件名" - # else: - # plugins = PluginManager().list_plugins() - # name = args[0].upper() - # if name in plugins and plugins[name].enabled: - # ok, result = True, PluginManager().instances[name].get_help_text(isgroup=isgroup, isadmin=isadmin) - # else: - # ok, result= False, "插件不存在或未启用" - # elif cmd == "id": - # ok, result = True, f"用户id=\n{user}" elif cmd == "reset": if bottype in (const.CHATGPT, const.OPEN_AI): bot.sessions.clear_session(session_id) diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index dc91f16..9413183 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -8,7 +8,7 @@ from plugins import * from common.log import logger -@plugins.register(name="Hello", desc="A simple plugin that says hello", version="0.1", author="lanvent", desire_priority= -1) +@plugins.register(name="Hello", desire_priority=-1, hidden=True, desc="A simple plugin that says hello", version="0.1", author="lanvent") class Hello(Plugin): def __init__(self): super().__init__() diff --git a/plugins/plugin_manager.py b/plugins/plugin_manager.py index a425c6e..c7d7ddf 100644 --- a/plugins/plugin_manager.py +++ b/plugins/plugin_manager.py @@ -18,16 +18,18 @@ class PluginManager: self.instances = {} self.pconf = {} - def register(self, name: str, desc: str, version: str, author: str, desire_priority: int = 0): + def register(self, name: str, desire_priority: int = 0, **kwargs): def wrapper(plugincls): plugincls.name = name - plugincls.desc = desc - plugincls.version = version - plugincls.author = author plugincls.priority = desire_priority + plugincls.desc = kwargs.get('desc') + plugincls.author = kwargs.get('author') + plugincls.version = kwargs.get('version') if kwargs.get('version') != None else "1.0" + plugincls.namecn = kwargs.get('namecn') if kwargs.get('namecn') != None else name + plugincls.hidden = kwargs.get('hidden') if kwargs.get('hidden') != None else False plugincls.enabled = True self.plugins[name.upper()] = plugincls - logger.info("Plugin %s_v%s registered" % (name, version)) + logger.info("Plugin %s_v%s registered" % (name, plugincls.version)) return plugincls return wrapper diff --git a/plugins/role/role.py b/plugins/role/role.py index 929cc53..0bc828e 100644 --- a/plugins/role/role.py +++ b/plugins/role/role.py @@ -29,7 +29,7 @@ class RolePlay(): prompt = self.wrapper % user_action return prompt -@plugins.register(name="角色扮演", desc="为你的Bot设置预设角色", version="1.0", author="lanvent", desire_priority= 0) +@plugins.register(name="Role", desire_priority=0, namecn="角色扮演", desc="为你的Bot设置预设角色", version="1.0", author="lanvent") class Role(Plugin): def __init__(self): super().__init__() From fdf6b0dc6b6afbef952bd762d96e4f8806c875a9 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Wed, 5 Apr 2023 14:29:18 +0800 Subject: [PATCH 12/13] fix: web server port --- channel/wechatmp/README.md | 13 ++++++++++++- channel/wechatmp/wechatmp_channel.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/channel/wechatmp/README.md b/channel/wechatmp/README.md index a01bf21..bde8aae 100644 --- a/channel/wechatmp/README.md +++ b/channel/wechatmp/README.md @@ -18,7 +18,18 @@ pip3 install web.py 然后根据[接入指南](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html)的说明,在[微信公众平台](https://mp.weixin.qq.com)的“设置与开发”-“基本配置”-“服务器配置”中填写服务器地址`URL`和令牌`Token`。这里的`URL`是`example.com/wx`的形式,不可以使用IP,`Token`是你自己编的一个特定的令牌。消息加解密方式目前选择的是明文模式。 -相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要在本项目根目录的`config.json`中添加`"channel_type": "wechatmp", "wechatmp_token": "your Token", ` 然后运行`python3 app.py`启动web服务器,然后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 +相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要在本项目根目录的`config.json`中添加 +``` +"channel_type": "wechatmp", +"wechatmp_token": "your Token", +``` +然后运行`python3 app.py`启动web服务器。这里会默认监听8080端口,但是微信公众号的服务器配置只支持80/443端口,有两种方法来解决这个问题。第一个是推荐的方法,使用端口转发命令将80端口转发到8080端口(443同理,注意需要支持SSL,也就是https的访问,在`wechatmp_channel.py`需要修改相应的证书路径): +``` +sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 +sudo iptables-save > /etc/iptables/rules.v4 +``` +第二个方法是让python程序直接监听80端口,可以直接使用命令`python3 app.py 80`。这样可能会导致权限问题,在linux上需要使用`sudu`。然而这会导致后续缓存文件的权限问题,因此不是推荐的方法。 +最后在刚才的“服务器配置”中点击`提交`即可验证你的服务器。 随后在[微信公众平台](https://mp.weixin.qq.com)启用服务器,关闭手动填写规则的自动回复,即可实现ChatGPT的自动回复。 diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index c2637a2..0da9085 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -30,7 +30,7 @@ class WechatMPServer(): '/wx', 'WechatMPChannel', ) app = web.application(urls, globals()) - web.httpserver.runsimple(app.wsgifunc(), ('0.0.0.0', 80)) + app.run() cache_dict = dict() query1 = dict() From 1a981ea97095d12f2a6000a14e005f77e52359e3 Mon Sep 17 00:00:00 2001 From: JS00000 Date: Wed, 5 Apr 2023 20:55:24 +0800 Subject: [PATCH 13/13] Refactor: inherit ChatChannel --- channel/channel_factory.py | 4 +- channel/wechatmp/receive.py | 68 +++---- channel/wechatmp/wechatmp_channel.py | 284 ++++++++++----------------- 3 files changed, 135 insertions(+), 221 deletions(-) diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 3d06154..3303ded 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -18,6 +18,6 @@ def create_channel(channel_type): from channel.terminal.terminal_channel import TerminalChannel return TerminalChannel() elif channel_type == 'wechatmp': - from channel.wechatmp.wechatmp_channel import WechatMPServer - return WechatMPServer() + from channel.wechatmp.wechatmp_channel import WechatMPChannel + return WechatMPChannel() raise RuntimeError diff --git a/channel/wechatmp/receive.py b/channel/wechatmp/receive.py index 40fc35f..64d2106 100644 --- a/channel/wechatmp/receive.py +++ b/channel/wechatmp/receive.py @@ -1,47 +1,43 @@ # -*- coding: utf-8 -*-# # filename: receive.py import xml.etree.ElementTree as ET +from bridge.context import ContextType +from channel.chat_message import ChatMessage +from common.tmp_dir import TmpDir +from common.log import logger def parse_xml(web_data): if len(web_data) == 0: return None xmlData = ET.fromstring(web_data) - msg_type = xmlData.find('MsgType').text - if msg_type == 'text': - return TextMsg(xmlData) - elif msg_type == 'image': - return ImageMsg(xmlData) - elif msg_type == 'event': - return Event(xmlData) + return WeChatMPMessage(xmlData) - -class Msg(object): - def __init__(self, xmlData): - self.ToUserName = xmlData.find('ToUserName').text - self.FromUserName = xmlData.find('FromUserName').text - self.CreateTime = xmlData.find('CreateTime').text - self.MsgType = xmlData.find('MsgType').text - self.MsgId = xmlData.find('MsgId').text - - -class TextMsg(Msg): - def __init__(self, xmlData): - Msg.__init__(self, xmlData) - self.Content = xmlData.find('Content').text.encode("utf-8") - - -class ImageMsg(Msg): - def __init__(self, xmlData): - Msg.__init__(self, xmlData) - self.PicUrl = xmlData.find('PicUrl').text - self.MediaId = xmlData.find('MediaId').text - - -class Event(object): +class WeChatMPMessage(ChatMessage): def __init__(self, xmlData): - self.ToUserName = xmlData.find('ToUserName').text - self.FromUserName = xmlData.find('FromUserName').text - self.CreateTime = xmlData.find('CreateTime').text - self.MsgType = xmlData.find('MsgType').text - self.Event = xmlData.find('Event').text + super().__init__(xmlData) + self.to_user_id = xmlData.find('ToUserName').text + self.from_user_id = xmlData.find('FromUserName').text + self.create_time = xmlData.find('CreateTime').text + self.msg_type = xmlData.find('MsgType').text + self.msg_id = xmlData.find('MsgId').text + self.is_group = False + + # reply to other_user_id + self.other_user_id = self.from_user_id + + if self.msg_type == 'text': + self.ctype = ContextType.TEXT + self.content = xmlData.find('Content').text.encode("utf-8") + elif self.msg_type == 'voice': + self.ctype = ContextType.TEXT + self.content = xmlData.find('Recognition').text.encode("utf-8") # 接收语音识别结果 + elif self.msg_type == 'image': + # not implemented + self.pic_url = xmlData.find('PicUrl').text + self.media_id = xmlData.find('MediaId').text + elif self.msg_type == 'event': + self.event = xmlData.find('Event').text + else: # video, shortvideo, location, link + # not implemented + pass \ No newline at end of file diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index 0da9085..cdd8673 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -4,9 +4,10 @@ import time import math import hashlib import textwrap -from channel.channel import Channel +from channel.chat_channel import ChatChannel import channel.wechatmp.reply as reply import channel.wechatmp.receive as receive +from common.singleton import singleton from common.log import logger from config import conf from bridge.reply import * @@ -21,202 +22,125 @@ import traceback # certificate='/ssl/cert.pem', # private_key='/ssl/cert.key') -class WechatMPServer(): + +# from concurrent.futures import ThreadPoolExecutor +# thread_pool = ThreadPoolExecutor(max_workers=8) + +@singleton +class WechatMPChannel(ChatChannel): def __init__(self): - pass + super().__init__() + self.cache_dict = dict() + self.query1 = dict() + self.query2 = dict() + self.query3 = dict() + - def startup(self): + def startup(self): urls = ( - '/wx', 'WechatMPChannel', + '/wx', 'SubsribeAccountQuery', ) app = web.application(urls, globals()) app.run() -cache_dict = dict() -query1 = dict() -query2 = dict() -query3 = dict() - -from concurrent.futures import ThreadPoolExecutor -thread_pool = ThreadPoolExecutor(max_workers=8) - -class WechatMPChannel(Channel): - def GET(self): - try: - data = web.input() - if len(data) == 0: - return "hello, this is handle view" - signature = data.signature - timestamp = data.timestamp - nonce = data.nonce - echostr = data.echostr - token = conf().get('wechatmp_token') #请按照公众平台官网\基本配置中信息填写 - - data_list = [token, timestamp, nonce] - data_list.sort() - sha1 = hashlib.sha1() - # map(sha1.update, data_list) #python2 - sha1.update("".join(data_list).encode('utf-8')) - hashcode = sha1.hexdigest() - print("handle/GET func: hashcode, signature: ", hashcode, signature) - if hashcode == signature: - return echostr - else: - return "" - except Exception as Argument: - return Argument - - - def _do_build_reply(self, cache_key, fromUser, message): - context = dict() - context['session_id'] = fromUser - reply_text = super().build_reply_content(message, context) - # The query is done, record the cache - logger.info("[threaded] Get reply for {}: {} \nA: {}".format(fromUser, message, reply_text)) - global cache_dict - reply_cnt = math.ceil(len(reply_text) / 600) - cache_dict[cache_key] = (reply_cnt, reply_text) - - - def send(self, reply : Reply, cache_key): - global cache_dict + def send(self, reply: Reply, context: Context): reply_cnt = math.ceil(len(reply.content) / 600) - cache_dict[cache_key] = (reply_cnt, reply.content) - - - def handle(self, context): - global cache_dict - try: - reply = Reply() - logger.debug('[wechatmp] ready to handle context: {}'.format(context)) - - # reply的构建步骤 - e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {'channel' : self, 'context': context, 'reply': reply})) - reply = e_context['reply'] - if not e_context.is_pass(): - logger.debug('[wechatmp] ready to handle context: type={}, content={}'.format(context.type, context.content)) - if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: - reply = super().build_reply_content(context.content, context) - # elif context.type == ContextType.VOICE: - # msg = context['msg'] - # file_name = TmpDir().path() + context.content - # msg.download(file_name) - # reply = super().build_voice_to_text(file_name) - # if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO: - # context.content = reply.content # 语音转文字后,将文字内容作为新的context - # context.type = ContextType.TEXT - # reply = super().build_reply_content(context.content, context) - # if reply.type == ReplyType.TEXT: - # if conf().get('voice_reply_voice'): - # reply = super().build_text_to_voice(reply.content) - else: - logger.error('[wechatmp] unknown context type: {}'.format(context.type)) - return - - logger.debug('[wechatmp] ready to decorate reply: {}'.format(reply)) - - # reply的包装步骤 - if reply and reply.type: - e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {'channel' : self, 'context': context, 'reply': reply})) - reply=e_context['reply'] - if not e_context.is_pass() and reply and reply.type: - if reply.type == ReplyType.TEXT: - pass - elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: - reply.content = str(reply.type)+":\n" + reply.content - elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE: - pass - else: - logger.error('[wechatmp] unknown reply type: {}'.format(reply.type)) - return - - # reply的发送步骤 - if reply and reply.type: - e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {'channel' : self, 'context': context, 'reply': reply})) - reply=e_context['reply'] - if not e_context.is_pass() and reply and reply.type: - logger.debug('[wechatmp] ready to send reply: {} to {}'.format(reply, context['receiver'])) - self.send(reply, context['receiver']) - else: - cache_dict[context['receiver']] = (1, "No reply") - - logger.info("[threaded] Get reply for {}: {} \nA: {}".format(context['receiver'], context.content, reply.content)) - except Exception as exc: - print(traceback.format_exc()) - cache_dict[context['receiver']] = (1, "ERROR") - + receiver = context["receiver"] + self.cache_dict[receiver] = (reply_cnt, reply.content) + logger.debug("[send] reply to {} saved to cache: {}".format(receiver, reply)) + + +def verify_server(): + try: + data = web.input() + if len(data) == 0: + return "None" + signature = data.signature + timestamp = data.timestamp + nonce = data.nonce + echostr = data.echostr + token = conf().get('wechatmp_token') #请按照公众平台官网\基本配置中信息填写 + + data_list = [token, timestamp, nonce] + data_list.sort() + sha1 = hashlib.sha1() + # map(sha1.update, data_list) #python2 + sha1.update("".join(data_list).encode('utf-8')) + hashcode = sha1.hexdigest() + print("handle/GET func: hashcode, signature: ", hashcode, signature) + if hashcode == signature: + return echostr + else: + return "" + except Exception as Argument: + return Argument + + +# This class is instantiated once per query +class SubsribeAccountQuery(): + def GET(self): + return verify_server() def POST(self): + channel_instance = WechatMPChannel() try: - queryTime = time.time() + query_time = time.time() webData = web.data() # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8")) - recMsg = receive.parse_xml(webData) - if isinstance(recMsg, receive.Msg) and recMsg.MsgType == 'text': - fromUser = recMsg.FromUserName - toUser = recMsg.ToUserName - createTime = recMsg.CreateTime - message = recMsg.Content.decode("utf-8") - message_id = recMsg.MsgId + wechat_msg = receive.parse_xml(webData) + if wechat_msg.msg_type == 'text': + from_user = wechat_msg.from_user_id + to_user = wechat_msg.to_user_id + message = wechat_msg.content.decode("utf-8") + message_id = wechat_msg.msg_id - logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), fromUser, message_id, message)) + logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message)) - global cache_dict - global query1 - global query2 - global query3 - cache_key = fromUser - cache = cache_dict.get(cache_key) + cache_key = from_user + cache = channel_instance.cache_dict.get(cache_key) reply_text = "" # New request if cache == None: # The first query begin, reset the cache - cache_dict[cache_key] = (0, "") - # thread_pool.submit(self._do_build_reply, cache_key, fromUser, message) - - context = Context() - context.kwargs = {'isgroup': False, 'receiver': fromUser, 'session_id': fromUser} + channel_instance.cache_dict[cache_key] = (0, "") - user_data = conf().get_user_data(fromUser) - context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key + context = channel_instance._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechat_msg) + if context: + # set private openai_api_key + # if from_user is not changed in itchat, this can be placed at chat_channel + user_data = conf().get_user_data(from_user) + context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key + channel_instance.produce(context) - img_match_prefix = check_prefix(message, conf().get('image_create_prefix')) - if img_match_prefix: - message = message.replace(img_match_prefix, '', 1).strip() - context.type = ContextType.IMAGE_CREATE - else: - context.type = ContextType.TEXT - context.content = message - thread_pool.submit(self.handle, context) - query1[cache_key] = False - query2[cache_key] = False - query3[cache_key] = False + channel_instance.query1[cache_key] = False + channel_instance.query2[cache_key] = False + channel_instance.query3[cache_key] = False # Request again - elif cache[0] == 0 and query1.get(cache_key) == True and query2.get(cache_key) == True and query3.get(cache_key) == True: - query1[cache_key] = False #To improve waiting experience, this can be set to True. - query2[cache_key] = False #To improve waiting experience, this can be set to True. - query3[cache_key] = False + elif cache[0] == 0 and channel_instance.query1.get(cache_key) == True and channel_instance.query2.get(cache_key) == True and channel_instance.query3.get(cache_key) == True: + channel_instance.query1[cache_key] = False #To improve waiting experience, this can be set to True. + channel_instance.query2[cache_key] = False #To improve waiting experience, this can be set to True. + channel_instance.query3[cache_key] = False elif cache[0] >= 1: # Skip the waiting phase - query1[cache_key] = True - query2[cache_key] = True - query3[cache_key] = True + channel_instance.query1[cache_key] = True + channel_instance.query2[cache_key] = True + channel_instance.query3[cache_key] = True - cache = cache_dict.get(cache_key) - if query1.get(cache_key) == False: + cache = channel_instance.cache_dict.get(cache_key) + if channel_instance.query1.get(cache_key) == False: # The first query from wechat official server logger.debug("[wechatmp] query1 {}".format(cache_key)) - query1[cache_key] = True + channel_instance.query1[cache_key] = True cnt = 0 while cache[0] == 0 and cnt < 45: cnt = cnt + 1 time.sleep(0.1) - cache = cache_dict.get(cache_key) + cache = channel_instance.cache_dict.get(cache_key) if cnt == 45: # waiting for timeout (the POST query will be closed by wechat official server) time.sleep(5) @@ -224,15 +148,15 @@ class WechatMPChannel(Channel): return else: pass - elif query2.get(cache_key) == False: + elif channel_instance.query2.get(cache_key) == False: # The second query from wechat official server logger.debug("[wechatmp] query2 {}".format(cache_key)) - query2[cache_key] = True + channel_instance.query2[cache_key] = True cnt = 0 while cache[0] == 0 and cnt < 45: cnt = cnt + 1 time.sleep(0.1) - cache = cache_dict.get(cache_key) + cache = channel_instance.cache_dict.get(cache_key) if cnt == 45: # waiting for timeout (the POST query will be closed by wechat official server) time.sleep(5) @@ -240,42 +164,42 @@ class WechatMPChannel(Channel): return else: pass - elif query3.get(cache_key) == False: + elif channel_instance.query3.get(cache_key) == False: # The third query from wechat official server logger.debug("[wechatmp] query3 {}".format(cache_key)) - query3[cache_key] = True + channel_instance.query3[cache_key] = True cnt = 0 while cache[0] == 0 and cnt < 45: cnt = cnt + 1 time.sleep(0.1) - cache = cache_dict.get(cache_key) + cache = channel_instance.cache_dict.get(cache_key) if cnt == 45: # Have waiting for 3x5 seconds # return timeout message reply_text = "【正在响应中,回复任意文字尝试获取回复】" - logger.info("[wechatmp] Three queries has finished For {}: {}".format(fromUser, message_id)) - replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() + logger.info("[wechatmp] Three queries has finished For {}: {}".format(from_user, message_id)) + replyPost = reply.TextMsg(from_user, to_user, reply_text).send() return replyPost else: pass - if float(time.time()) - float(queryTime) > 4.8: - logger.info("[wechatmp] Timeout for {} {}".format(fromUser, message_id)) + if float(time.time()) - float(query_time) > 4.8: + logger.info("[wechatmp] Timeout for {} {}".format(from_user, message_id)) return if cache[0] > 1: reply_text = cache[1][:600] + "\n【未完待续,回复任意文字以继续】" #wechatmp auto_reply length limit - cache_dict[cache_key] = (cache[0] - 1, cache[1][600:]) + channel_instance.cache_dict[cache_key] = (cache[0] - 1, cache[1][600:]) elif cache[0] == 1: reply_text = cache[1] - cache_dict.pop(cache_key) + channel_instance.cache_dict.pop(cache_key) logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text)) - replyPost = reply.TextMsg(fromUser, toUser, reply_text).send() + replyPost = reply.TextMsg(from_user, to_user, reply_text).send() return replyPost - elif isinstance(recMsg, receive.Event) and recMsg.MsgType == 'event': - logger.info("[wechatmp] Event {} from {}".format(recMsg.Event, recMsg.FromUserName)) + elif wechat_msg.msg_type == 'event': + logger.info("[wechatmp] Event {} from {}".format(wechat_msg.Event, wechat_msg.from_user_id)) content = textwrap.dedent("""\ 感谢您的关注! 这里是ChatGPT,可以自由对话。 @@ -285,7 +209,7 @@ class WechatMPChannel(Channel): 支持图片输出,画字开头的问题将回复图片链接。 支持角色扮演和文字冒险两种定制模式对话。 输入'#帮助' 查看详细指令。""") - replyMsg = reply.TextMsg(recMsg.FromUserName, recMsg.ToUserName, content) + replyMsg = reply.TextMsg(wechat_msg.from_user_id, wechat_msg.to_user_id, content) return replyMsg.send() else: logger.info("暂且不处理") @@ -294,9 +218,3 @@ class WechatMPChannel(Channel): logger.exception(exc) return exc - -def check_prefix(content, prefix_list): - for prefix in prefix_list: - if content.startswith(prefix): - return prefix - return None