Jelajahi Sumber

Merge Pull Request #774 into master

lanvent 3 tahun lalu
induk
melakukan
8c7d1d4010

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 .DS_Store
 .idea
+.vscode
 .wechaty/
 __pycache__/
 venv*

+ 1 - 1
app.py

@@ -36,7 +36,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','wechatmp','terminal']:
+        if channel_name in ['wx','wxy','terminal','wechatmp','wechatmp_service']:
             PluginManager().load_plugins()
 
         # startup channel

+ 4 - 1
channel/channel_factory.py

@@ -19,5 +19,8 @@ def create_channel(channel_type):
         return TerminalChannel()
     elif channel_type == 'wechatmp':
         from channel.wechatmp.wechatmp_channel import WechatMPChannel
-        return WechatMPChannel()
+        return WechatMPChannel(passive_reply = True)
+    elif channel_type == 'wechatmp_service':
+        from channel.wechatmp.wechatmp_channel import WechatMPChannel
+        return WechatMPChannel(passive_reply = False)
     raise RuntimeError

+ 17 - 9
channel/wechatmp/README.md

@@ -1,10 +1,11 @@
-# 个人微信公众号channel
+# 微信公众号channel
 
-鉴于个人微信号在服务器上通过itchat登录有封号风险,这里新增了个人微信公众号channel,提供无风险的服务。
-但是由于个人微信公众号的众多接口限制,目前支持的功能有限,实现简陋,提供了一个最基本的文本对话服务,支持加载插件,优化了命令格式,支持私有api_key。暂未实现图片输入输出、语音输入输出等交互形式。
-如有公众号是企业主体且可以通过微信认证,即可获得更多接口,解除大多数限制。欢迎大家提供更多的支持。
+鉴于个人微信号在服务器上通过itchat登录有封号风险,这里新增了微信公众号channel,提供无风险的服务。
+目前支持订阅号(个人)和服务号(企业)两种类型的公众号,它们的主要区别就是被动回复和主动回复。
+个人微信订阅号有许多接口限制,目前仅支持最基本的文本对话和语音输入,支持加载插件,支持私有api_key。
+暂未实现图片输入输出、语音输出等交互形式。
 
-## 使用方法
+## 使用方法(订阅号,服务号类似)
 
 在开始部署前,你需要一个拥有公网IP的服务器,以提供微信服务器和我们自己服务器的连接。或者你需要进行内网穿透,否则微信服务器无法将消息发送给我们的服务器。
 
@@ -21,8 +22,10 @@ pip3 install web.py
 相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要在本项目根目录的`config.json`中添加
 ```
 "channel_type": "wechatmp", 
-"wechatmp_token": "your Token", 
-"wechatmp_port": 8080,
+"wechatmp_token": "Token",  # 微信公众平台的Token
+"wechatmp_port": 8080,      # 微信公众平台的端口,需要端口转发到80或443
+"wechatmp_app_id": "",      # 微信公众平台的appID,仅服务号需要
+"wechatmp_app_secret": "",  # 微信公众平台的appsecret,仅服务号需要
 ``` 
 然后运行`python3 app.py`启动web服务器。这里会默认监听8080端口,但是微信公众号的服务器配置只支持80/443端口,有两种方法来解决这个问题。第一个是推荐的方法,使用端口转发命令将80端口转发到8080端口(443同理,注意需要支持SSL,也就是https的访问,在`wechatmp_channel.py`需要修改相应的证书路径):
 ```
@@ -35,7 +38,7 @@ sudo iptables-save > /etc/iptables/rules.v4
 随后在[微信公众平台](https://mp.weixin.qq.com)启用服务器,关闭手动填写规则的自动回复,即可实现ChatGPT的自动回复。
 
 ## 个人微信公众号的限制
-由于目前测试的公众号不是企业主体,所以没有客服接口,因此公众号无法主动发出消息,只能被动回复。而微信官方对被动回复有5秒的时间限制,最多重试2次,因此最多只有15秒的自动回复时间窗口。因此如果问题比较复杂或者我们的服务器比较忙,ChatGPT的回答就没办法及时回复给用户。为了解决这个问题,这里做了回答缓存,它需要你在回复超时后,再次主动发送任意文字(例如1)来尝试拿到回答缓存。为了优化使用体验,目前设置了两分钟(120秒)的timeout,用户在至多两分钟后即可得到查询到回复或者错误原因。
+由于人微信公众号不能通过微信认证,所以没有客服接口,因此公众号无法主动发出消息,只能被动回复。而微信官方对被动回复有5秒的时间限制,最多重试2次,因此最多只有15秒的自动回复时间窗口。因此如果问题比较复杂或者我们的服务器比较忙,ChatGPT的回答就没办法及时回复给用户。为了解决这个问题,这里做了回答缓存,它需要你在回复超时后,再次主动发送任意文字(例如1)来尝试拿到回答缓存。为了优化使用体验,目前设置了两分钟(120秒)的timeout,用户在至多两分钟后即可得到查询到回复或者错误原因。
 
 另外,由于微信官方的限制,自动回复有长度限制。因此这里将ChatGPT的回答拆分,分成每段600字回复(限制大约在700字)。
 
@@ -43,4 +46,9 @@ sudo iptables-save > /etc/iptables/rules.v4
 公共api有访问频率限制(免费账号每分钟最多20次ChatGPT的API调用),这在服务多人的时候会遇到问题。因此这里多加了一个设置私有api_key的功能。目前通过godcmd插件的命令来设置私有api_key。
 
 ## 测试范围
-目前在`RoboStyle`这个公众号上进行了测试,感兴趣的可以关注并体验。开启了godcmd, Banwords, role, dungeon, finish这五个插件,其他的插件还没有测试。百度的接口暂未测试。语音对话没有测试。图片直接以链接形式回复(没有临时素材上传接口的权限)。
+目前在`RoboStyle`这个公众号上进行了测试(基于[wechatmp-stable分支](https://github.com/JS00000/chatgpt-on-wechat/tree/wechatmp-stable),而[master分支](https://github.com/zhayujie/chatgpt-on-wechat)含有最新功能,但是稳定性有待测试),感兴趣的可以关注并体验。开启了godcmd, Banwords, role, dungeon, finish这五个插件,其他的插件还没有测试。百度的接口暂未测试。语音对话没有测试。图片直接以链接形式回复(没有临时素材上传接口的权限)。
+
+## TODO
+* 服务号交互完善
+* 服务号使用临时素材接口,提供图片回复能力
+* 插件测试

+ 51 - 0
channel/wechatmp/ServiceAccount.py

@@ -0,0 +1,51 @@
+import web
+import time
+import channel.wechatmp.reply as reply
+import channel.wechatmp.receive as receive
+from config import conf
+from common.log import logger
+from bridge.context import *
+from channel.wechatmp.common import * 
+from channel.wechatmp.wechatmp_channel import WechatMPChannel
+
+# This class is instantiated once per query
+class Query():
+
+    def GET(self):
+        return verify_server(web.input())
+
+    def POST(self):
+        # Make sure to return the instance that first created, @singleton will do that. 
+        channel_instance = WechatMPChannel()
+        try:
+            webData = web.data()
+            # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
+            wechatmp_msg = receive.parse_xml(webData)
+            if wechatmp_msg.msg_type == 'text':
+                from_user = wechatmp_msg.from_user_id
+                message = wechatmp_msg.content.decode("utf-8")
+                message_id = wechatmp_msg.msg_id
+
+                logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message))
+                context = channel_instance._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_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)
+                # The reply will be sent by channel_instance.send() in another thread
+                return "success"
+
+            elif wechatmp_msg.msg_type == 'event':
+                logger.info("[wechatmp] Event {} from {}".format(wechatmp_msg.Event, wechatmp_msg.from_user_id))
+                content = subscribe_msg()
+                replyMsg = reply.TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content)
+                return replyMsg.send()
+            else:
+                logger.info("暂且不处理")
+                return "success"
+        except Exception as exc:
+            logger.exception(exc)
+            return exc
+

+ 165 - 0
channel/wechatmp/SubscribeAccount.py

@@ -0,0 +1,165 @@
+import web
+import time
+import channel.wechatmp.reply as reply
+import channel.wechatmp.receive as receive
+from config import conf
+from common.log import logger
+from bridge.context import *
+from channel.wechatmp.common import * 
+from channel.wechatmp.wechatmp_channel import WechatMPChannel
+
+# This class is instantiated once per query
+class Query():
+
+    def GET(self):
+        return verify_server(web.input())
+
+    def POST(self):
+        # Make sure to return the instance that first created, @singleton will do that. 
+        channel = WechatMPChannel()
+        try:
+            query_time = time.time()
+            webData = web.data()
+            logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
+            wechatmp_msg = receive.parse_xml(webData)
+            if wechatmp_msg.msg_type == 'text':
+                from_user = wechatmp_msg.from_user_id
+                to_user = wechatmp_msg.to_user_id
+                message = wechatmp_msg.content.decode("utf-8")
+                message_id = wechatmp_msg.msg_id
+
+                logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message))
+                supported = True
+                if "【收到不支持的消息类型,暂无法显示】" in message:
+                    supported = False # not supported, used to refresh
+                cache_key = from_user
+
+                reply_text = ""
+                # New request
+                if cache_key not in channel.cache_dict and cache_key not in channel.running:
+                    # The first query begin, reset the cache
+                    context = channel._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg)
+                    logger.debug("[wechatmp] context: {} {}".format(context, wechatmp_msg))
+                    if message_id in channel.received_msgs: # received and finished
+                        return 
+                    if supported and 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.received_msgs[message_id] = wechatmp_msg
+                        channel.running.add(cache_key)
+                        channel.produce(context)
+                    else:
+                        trigger_prefix = conf().get('single_chat_prefix',[''])[0]
+                        if trigger_prefix or not supported:
+                            if trigger_prefix:
+                                content = textwrap.dedent(f"""\
+                                    请输入'{trigger_prefix}'接你想说的话跟我说话。
+                                    例如:
+                                    {trigger_prefix}你好,很高兴见到你。""")
+                            else:
+                                content = textwrap.dedent("""\
+                                    你好,很高兴见到你。
+                                    请跟我说话吧。""")
+                        else:
+                            logger.error(f"[wechatmp] unknown error")
+                            content = textwrap.dedent("""\
+                                未知错误,请稍后再试""")
+                        replyMsg = reply.TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content)
+                        return replyMsg.send()
+                    channel.query1[cache_key] = False
+                    channel.query2[cache_key] = False
+                    channel.query3[cache_key] = False
+                # Request again
+                elif cache_key in channel.running and channel.query1.get(cache_key) == True and channel.query2.get(cache_key) == True and channel.query3.get(cache_key) == True:
+                    channel.query1[cache_key] = False  #To improve waiting experience, this can be set to True.
+                    channel.query2[cache_key] = False  #To improve waiting experience, this can be set to True.
+                    channel.query3[cache_key] = False
+                elif cache_key in channel.cache_dict:
+                    # Skip the waiting phase
+                    channel.query1[cache_key] = True
+                    channel.query2[cache_key] = True
+                    channel.query3[cache_key] = True
+
+                assert not (cache_key in channel.cache_dict and cache_key in channel.running)
+
+                if channel.query1.get(cache_key) == False:
+                    # The first query from wechat official server
+                    logger.debug("[wechatmp] query1 {}".format(cache_key))
+                    channel.query1[cache_key] = True
+                    cnt = 0
+                    while cache_key not in channel.cache_dict and cnt < 45:
+                        cnt = cnt + 1
+                        time.sleep(0.1)
+                    if cnt == 45:
+                        # waiting for timeout (the POST query will be closed by wechat official server)
+                        time.sleep(1)
+                        # and do nothing
+                        return
+                    else:
+                        pass
+                elif channel.query2.get(cache_key) == False:
+                    # The second query from wechat official server
+                    logger.debug("[wechatmp] query2 {}".format(cache_key))
+                    channel.query2[cache_key] = True
+                    cnt = 0
+                    while cache_key not in channel.cache_dict and cnt < 45:
+                        cnt = cnt + 1
+                        time.sleep(0.1)
+                    if cnt == 45:
+                        # waiting for timeout (the POST query will be closed by wechat official server)
+                        time.sleep(1)
+                        # and do nothing
+                        return
+                    else:
+                        pass
+                elif channel.query3.get(cache_key) == False:
+                    # The third query from wechat official server
+                    logger.debug("[wechatmp] query3 {}".format(cache_key))
+                    channel.query3[cache_key] = True
+                    cnt = 0
+                    while cache_key not in channel.cache_dict and cnt < 40:
+                        cnt = cnt + 1
+                        time.sleep(0.1)
+                    if cnt == 40:
+                        # Have waiting for 3x5 seconds
+                        # return timeout message
+                        reply_text = "【正在思考中,回复任意文字尝试获取回复】"
+                        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(query_time) > 4.8:
+                    reply_text = "【正在思考中,回复任意文字尝试获取回复】"
+                    logger.info("[wechatmp] Timeout for {} {}, return".format(from_user, message_id))
+                    replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
+                    return replyPost
+                
+                if cache_key in channel.cache_dict:
+                    content = channel.cache_dict[cache_key]
+                    if len(content.encode('utf8'))<=MAX_UTF8_LEN:
+                        reply_text = channel.cache_dict[cache_key]
+                        channel.cache_dict.pop(cache_key)
+                    else:
+                        continue_text = "\n【未完待续,回复任意文字以继续】"
+                        splits = split_string_by_utf8_length(content, MAX_UTF8_LEN - len(continue_text.encode('utf-8')), max_split= 1)
+                        reply_text = splits[0] + continue_text
+                        channel.cache_dict[cache_key] = splits[1]
+                logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text))
+                replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
+                return replyPost
+
+            elif wechatmp_msg.msg_type == 'event':
+                logger.info("[wechatmp] Event {} from {}".format(wechatmp_msg.content, wechatmp_msg.from_user_id))
+                content = subscribe_msg()
+                replyMsg = reply.TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content)
+                return replyMsg.send()
+            else:
+                logger.info("暂且不处理")
+                return "success"
+        except Exception as exc:
+            logger.exception(exc)
+            return exc

+ 63 - 0
channel/wechatmp/common.py

@@ -0,0 +1,63 @@
+from config import conf
+import hashlib
+import textwrap
+
+MAX_UTF8_LEN = 2048
+
+class WeChatAPIException(Exception):
+    pass
+
+
+def verify_server(data):
+    try:
+        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
+
+def subscribe_msg():
+    trigger_prefix = conf().get('single_chat_prefix',[''])[0]
+    msg = textwrap.dedent(f"""\
+                    感谢您的关注!
+                    这里是ChatGPT,可以自由对话。
+                    资源有限,回复较慢,请勿着急。
+                    支持通用表情输入。
+                    暂时不支持图片输入。
+                    支持图片输出,画字开头的问题将回复图片链接。
+                    支持角色扮演和文字冒险两种定制模式对话。
+                    输入'{trigger_prefix}#帮助' 查看详细指令。""")
+    return msg
+
+
+def split_string_by_utf8_length(string, max_length, max_split=0):
+    encoded = string.encode('utf-8')
+    start, end = 0, 0
+    result = []
+    while end < len(encoded):
+        if max_split > 0 and len(result) >= max_split:
+            result.append(encoded[start:].decode('utf-8'))
+            break
+        end = start + max_length
+        # 如果当前字节不是 UTF-8 编码的开始字节,则向前查找直到找到开始字节为止
+        while end < len(encoded) and (encoded[end] & 0b11000000) == 0b10000000:
+            end -= 1
+        result.append(encoded[start:end].decode('utf-8'))
+        start = end
+    return result

+ 89 - 232
channel/wechatmp/wechatmp_channel.py

@@ -1,20 +1,17 @@
 # -*- coding: utf-8 -*-
 import web
 import time
-import math
-import hashlib
-import textwrap
-from channel.chat_channel import ChatChannel
-import channel.wechatmp.reply as reply
-import channel.wechatmp.receive as receive
-from common.expired_dict import ExpiredDict
+import json
+import requests
+import threading
 from common.singleton import singleton
 from common.log import logger
+from common.expired_dict import ExpiredDict
 from config import conf
 from bridge.reply import *
 from bridge.context import *
-from plugins import *
-import traceback
+from channel.chat_channel import ChatChannel
+from channel.wechatmp.common import * 
 
 # If using SSL, uncomment the following lines, and modify the certificate path.
 # from cheroot.server import HTTPServer
@@ -23,37 +20,101 @@ import traceback
 #         certificate='/ssl/cert.pem',
 #         private_key='/ssl/cert.key')
 
-
-# from concurrent.futures import ThreadPoolExecutor
-# thread_pool = ThreadPoolExecutor(max_workers=8)
-
-MAX_UTF8_LEN = 2048
 @singleton
 class WechatMPChannel(ChatChannel):
-    NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE]
-    def __init__(self):
+    def __init__(self, passive_reply = True):
         super().__init__()
-        self.cache_dict = dict()
+        self.passive_reply = passive_reply
         self.running = set()
-        self.query1 = dict()
-        self.query2 = dict()
-        self.query3 = dict()
-        self.received_msgs = ExpiredDict(60*60*24) 
+        self.received_msgs = ExpiredDict(60*60*24)
+        if self.passive_reply:
+            self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE]
+            self.cache_dict = dict()
+            self.query1 = dict()
+            self.query2 = dict()
+            self.query3 = dict()
+        else:
+            # TODO support image
+            self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE]
+            self.app_id = conf().get('wechatmp_app_id')
+            self.app_secret = conf().get('wechatmp_app_secret')
+            self.access_token = None
+            self.access_token_expires_time = 0
+            self.access_token_lock = threading.Lock()
+            self.get_access_token()
 
     def startup(self):
-        urls = (
-            '/wx', 'SubsribeAccountQuery',
-        )
+        if self.passive_reply:
+            urls = ('/wx', 'channel.wechatmp.SubscribeAccount.Query')
+        else:
+            urls = ('/wx', 'channel.wechatmp.ServiceAccount.Query')
         app = web.application(urls, globals(), autoreload=False)
         port = conf().get('wechatmp_port', 8080)
         web.httpserver.runsimple(app.wsgifunc(), ('0.0.0.0', port))
 
 
+    def wechatmp_request(self, method, url, **kwargs):
+        r = requests.request(method=method, url=url, **kwargs)
+        r.raise_for_status()
+        r.encoding = "utf-8"
+        ret = r.json()
+        if "errcode" in ret and ret["errcode"] != 0:
+            raise WeChatAPIException("{}".format(ret))
+        return ret
+
+    def get_access_token(self):
+
+        # return the access_token
+        if self.access_token:
+            if self.access_token_expires_time - time.time() > 60:
+                return self.access_token
+
+        # Get new access_token
+        # Do not request access_token in parallel! Only the last obtained is valid.
+        if self.access_token_lock.acquire(blocking=False):
+            # Wait for other threads that have previously obtained access_token to complete the request
+            # This happens every 2 hours, so it doesn't affect the experience very much
+            time.sleep(1)
+            self.access_token = None
+            url="https://api.weixin.qq.com/cgi-bin/token"
+            params={
+                "grant_type": "client_credential",
+                "appid": self.app_id,
+                "secret": self.app_secret
+            }
+            data = self.wechatmp_request(method='get', url=url, params=params)
+            self.access_token = data['access_token']
+            self.access_token_expires_time = int(time.time()) + data['expires_in']
+            logger.info("[wechatmp] access_token: {}".format(self.access_token))
+            self.access_token_lock.release()
+        else:
+            # Wait for token update
+            while self.access_token_lock.locked():
+                time.sleep(0.1)
+        return self.access_token
+
     def send(self, reply: Reply, context: Context):
-        receiver = context["receiver"]
-        self.cache_dict[receiver] = reply.content
-        self.running.remove(receiver)
-        logger.debug("[send] reply to {} saved to cache: {}".format(receiver, reply))
+        if self.passive_reply:
+            receiver = context["receiver"]
+            self.cache_dict[receiver] = reply.content
+            self.running.remove(receiver)
+            logger.debug("[send] reply to {} saved to cache: {}".format(receiver, reply))
+        else:
+            receiver = context["receiver"]
+            reply_text = reply.content
+            url="https://api.weixin.qq.com/cgi-bin/message/custom/send"
+            params = {
+                "access_token": self.get_access_token()
+            }
+            json_data = {
+                "touser": receiver,
+                "msgtype": "text",
+                "text": {"content": reply_text}
+            }
+            self.wechatmp_request(method='post', url=url, params=params, data=json.dumps(json_data, ensure_ascii=False).encode('utf8'))
+            logger.info("[send] Do send to {}: {}".format(receiver, reply_text))
+        return
+
 
     def _fail_callback(self, session_id, exception, context, **kwargs):
         logger.exception("[wechatmp] Fail to generation message to user, msgId={}, exception={}".format(context['msg'].msg_id, exception))
@@ -61,208 +122,4 @@ class WechatMPChannel(ChatChannel):
         self.running.remove(session_id)
 
 
-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 = WechatMPChannel()
-        try:
-            query_time = time.time()
-            webData = web.data()
-            logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
-            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'), from_user, message_id, message))
-                supported = True
-                if "【收到不支持的消息类型,暂无法显示】" in message:
-                    supported = False # not supported, used to refresh
-                cache_key = from_user
-
-                reply_text = ""
-                # New request
-                if cache_key not in channel.cache_dict and cache_key not in channel.running:
-                    # The first query begin, reset the cache
-                    context = channel._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechat_msg)
-                    logger.debug("[wechatmp] context: {} {}".format(context, wechat_msg))
-                    if message_id in channel.received_msgs: # received and finished
-                        return 
-                    if supported and 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.received_msgs[message_id] = wechat_msg
-                        channel.running.add(cache_key)
-                        channel.produce(context)
-                    else:
-                        trigger_prefix = conf().get('single_chat_prefix',[''])[0]
-                        if trigger_prefix or not supported:
-                            if trigger_prefix:
-                                content = textwrap.dedent(f"""\
-                                    请输入'{trigger_prefix}'接你想说的话跟我说话。
-                                    例如:
-                                    {trigger_prefix}你好,很高兴见到你。""")
-                            else:
-                                content = textwrap.dedent("""\
-                                    你好,很高兴见到你。
-                                    请跟我说话吧。""")
-                        else:
-                            logger.error(f"[wechatmp] unknown error")
-                            content = textwrap.dedent("""\
-                                未知错误,请稍后再试""")
-                        replyMsg = reply.TextMsg(wechat_msg.from_user_id, wechat_msg.to_user_id, content)
-                        return replyMsg.send()
-                    channel.query1[cache_key] = False
-                    channel.query2[cache_key] = False
-                    channel.query3[cache_key] = False
-                # Request again
-                elif cache_key in channel.running and channel.query1.get(cache_key) == True and channel.query2.get(cache_key) == True and channel.query3.get(cache_key) == True:
-                    channel.query1[cache_key] = False  #To improve waiting experience, this can be set to True.
-                    channel.query2[cache_key] = False  #To improve waiting experience, this can be set to True.
-                    channel.query3[cache_key] = False
-                elif cache_key in channel.cache_dict:
-                    # Skip the waiting phase
-                    channel.query1[cache_key] = True
-                    channel.query2[cache_key] = True
-                    channel.query3[cache_key] = True
-
-                assert not (cache_key in channel.cache_dict and cache_key in channel.running)
-
-                if channel.query1.get(cache_key) == False:
-                    # The first query from wechat official server
-                    logger.debug("[wechatmp] query1 {}".format(cache_key))
-                    channel.query1[cache_key] = True
-                    cnt = 0
-                    while cache_key not in channel.cache_dict and cnt < 45:
-                        cnt = cnt + 1
-                        time.sleep(0.1)
-                    if cnt == 45:
-                        # waiting for timeout (the POST query will be closed by wechat official server)
-                        time.sleep(1)
-                        # and do nothing
-                        return
-                    else:
-                        pass
-                elif channel.query2.get(cache_key) == False:
-                    # The second query from wechat official server
-                    logger.debug("[wechatmp] query2 {}".format(cache_key))
-                    channel.query2[cache_key] = True
-                    cnt = 0
-                    while cache_key not in channel.cache_dict and cnt < 45:
-                        cnt = cnt + 1
-                        time.sleep(0.1)
-                    if cnt == 45:
-                        # waiting for timeout (the POST query will be closed by wechat official server)
-                        time.sleep(1)
-                        # and do nothing
-                        return
-                    else:
-                        pass
-                elif channel.query3.get(cache_key) == False:
-                    # The third query from wechat official server
-                    logger.debug("[wechatmp] query3 {}".format(cache_key))
-                    channel.query3[cache_key] = True
-                    cnt = 0
-                    while cache_key not in channel.cache_dict and cnt < 40:
-                        cnt = cnt + 1
-                        time.sleep(0.1)
-                    if cnt == 40:
-                        # Have waiting for 3x5 seconds
-                        # return timeout message
-                        reply_text = "【正在思考中,回复任意文字尝试获取回复】"
-                        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(query_time) > 4.8:
-                    reply_text = "【正在思考中,回复任意文字尝试获取回复】"
-                    logger.info("[wechatmp] Timeout for {} {}, return".format(from_user, message_id))
-                    replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
-                    return replyPost
-                
-                if cache_key in channel.cache_dict:
-                    content = channel.cache_dict[cache_key]
-                    if len(content.encode('utf8'))<=MAX_UTF8_LEN:
-                        reply_text = channel.cache_dict[cache_key]
-                        channel.cache_dict.pop(cache_key)
-                    else:
-                        continue_text = "\n【未完待续,回复任意文字以继续】"
-                        splits = split_string_by_utf8_length(content, MAX_UTF8_LEN - len(continue_text.encode('utf-8')), max_split= 1)
-                        reply_text = splits[0] + continue_text
-                        channel.cache_dict[cache_key] = splits[1]
-                logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text))
-                replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
-                return replyPost
-
-            elif wechat_msg.msg_type == 'event':
-                logger.info("[wechatmp] Event {} from {}".format(wechat_msg.content, wechat_msg.from_user_id))
-                trigger_prefix = conf().get('single_chat_prefix',[''])[0]
-                content = textwrap.dedent(f"""\
-                    感谢您的关注!
-                    这里是ChatGPT,可以自由对话。
-                    资源有限,回复较慢,请勿着急。
-                    支持通用表情输入。
-                    暂时不支持图片输入。
-                    支持图片输出,画字开头的问题将回复图片链接。
-                    支持角色扮演和文字冒险两种定制模式对话。
-                    输入'{trigger_prefix}#帮助' 查看详细指令。""")
-                replyMsg = reply.TextMsg(wechat_msg.from_user_id, wechat_msg.to_user_id, content)
-                return replyMsg.send()
-            else:
-                logger.info("暂且不处理")
-                return "success"
-        except Exception as exc:
-            logger.exception(exc)
-            return exc
 
-def split_string_by_utf8_length(string, max_length, max_split=0):
-    encoded = string.encode('utf-8')
-    start, end = 0, 0
-    result = []
-    while end < len(encoded):
-        if max_split > 0 and len(result) >= max_split:
-            result.append(encoded[start:].decode('utf-8'))
-            break
-        end = start + max_length
-        # 如果当前字节不是 UTF-8 编码的开始字节,则向前查找直到找到开始字节为止
-        while end < len(encoded) and (encoded[end] & 0b11000000) == 0b10000000:
-            end -= 1
-        result.append(encoded[start:end].decode('utf-8'))
-        start = end
-    return result

+ 5 - 3
config.py

@@ -79,14 +79,16 @@ available_setting = {
     "wechaty_puppet_service_token": "",  # wechaty的token
 
     # wechatmp的配置
-    "wechatmp_token": "",  # 微信公众平台的Token
-    "wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443
+    "wechatmp_token": "",       # 微信公众平台的Token
+    "wechatmp_port": 8080,      # 微信公众平台的端口,需要端口转发到80或443
+    "wechatmp_app_id": "",      # 微信公众平台的appID,仅服务号需要
+    "wechatmp_app_secret": "",  # 微信公众平台的appsecret,仅服务号需要
 
     # chatgpt指令自定义触发词
     "clear_memory_commands": ['#清除记忆'],  # 重置会话指令,必须以#开头
 
     # channel配置
-    "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp}
+    "channel_type": "wx", # 通道类型,支持:{wx,wxy,terminal,wechatmp,wechatmp_service}
 
     "debug": False,  # 是否开启debug模式,开启后会打印更多日志