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

301 line
13KB

  1. # -*- coding: utf-8 -*-
  2. import web
  3. import time
  4. import math
  5. import hashlib
  6. import textwrap
  7. from channel.channel import Channel
  8. import channel.wechatmp.reply as reply
  9. import channel.wechatmp.receive as receive
  10. from common.log import logger
  11. from config import conf
  12. from bridge.reply import *
  13. from bridge.context import *
  14. from plugins import *
  15. import traceback
  16. import redis
  17. class WechatMPServer():
  18. def __init__(self):
  19. pass
  20. def startup(self):
  21. urls = (
  22. '/wx', 'WechatMPChannel',
  23. )
  24. app = web.application(urls, globals())
  25. app.run()
  26. cache_dict = dict()
  27. query1 = dict()
  28. query2 = dict()
  29. query3 = dict()
  30. from concurrent.futures import ThreadPoolExecutor
  31. thread_pool = ThreadPoolExecutor(max_workers=8)
  32. class WechatMPChannel(Channel):
  33. def GET(self):
  34. try:
  35. data = web.input()
  36. if len(data) == 0:
  37. return "hello, this is handle view"
  38. signature = data.signature
  39. timestamp = data.timestamp
  40. nonce = data.nonce
  41. echostr = data.echostr
  42. token = conf().get('wechatmp_token') #请按照公众平台官网\基本配置中信息填写
  43. data_list = [token, timestamp, nonce]
  44. data_list.sort()
  45. sha1 = hashlib.sha1()
  46. # map(sha1.update, data_list) #python2
  47. sha1.update("".join(data_list).encode('utf-8'))
  48. hashcode = sha1.hexdigest()
  49. print("handle/GET func: hashcode, signature: ", hashcode, signature)
  50. if hashcode == signature:
  51. return echostr
  52. else:
  53. return ""
  54. except Exception as Argument:
  55. return Argument
  56. def _do_build_reply(self, cache_key, fromUser, message):
  57. context = dict()
  58. context['session_id'] = fromUser
  59. reply_text = super().build_reply_content(message, context)
  60. # The query is done, record the cache
  61. logger.info("[threaded] Get reply for {}: {} \nA: {}".format(fromUser, message, reply_text))
  62. global cache_dict
  63. reply_cnt = math.ceil(len(reply_text) / 600)
  64. cache_dict[cache_key] = (reply_cnt, reply_text)
  65. def send(self, reply : Reply, cache_key):
  66. global cache_dict
  67. reply_cnt = math.ceil(len(reply.content) / 600)
  68. cache_dict[cache_key] = (reply_cnt, reply.content)
  69. def handle(self, context):
  70. global cache_dict
  71. try:
  72. reply = Reply()
  73. logger.debug('[wechatmp] ready to handle context: {}'.format(context))
  74. # reply的构建步骤
  75. e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {'channel' : self, 'context': context, 'reply': reply}))
  76. reply = e_context['reply']
  77. if not e_context.is_pass():
  78. logger.debug('[wechatmp] ready to handle context: type={}, content={}'.format(context.type, context.content))
  79. if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE:
  80. reply = super().build_reply_content(context.content, context)
  81. # elif context.type == ContextType.VOICE:
  82. # msg = context['msg']
  83. # file_name = TmpDir().path() + context.content
  84. # msg.download(file_name)
  85. # reply = super().build_voice_to_text(file_name)
  86. # if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO:
  87. # context.content = reply.content # 语音转文字后,将文字内容作为新的context
  88. # context.type = ContextType.TEXT
  89. # reply = super().build_reply_content(context.content, context)
  90. # if reply.type == ReplyType.TEXT:
  91. # if conf().get('voice_reply_voice'):
  92. # reply = super().build_text_to_voice(reply.content)
  93. else:
  94. logger.error('[wechatmp] unknown context type: {}'.format(context.type))
  95. return
  96. logger.debug('[wechatmp] ready to decorate reply: {}'.format(reply))
  97. # reply的包装步骤
  98. if reply and reply.type:
  99. e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {'channel' : self, 'context': context, 'reply': reply}))
  100. reply=e_context['reply']
  101. if not e_context.is_pass() and reply and reply.type:
  102. if reply.type == ReplyType.TEXT:
  103. pass
  104. elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
  105. reply.content = str(reply.type)+":\n" + reply.content
  106. elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE:
  107. pass
  108. else:
  109. logger.error('[wechatmp] unknown reply type: {}'.format(reply.type))
  110. return
  111. # reply的发送步骤
  112. if reply and reply.type:
  113. e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {'channel' : self, 'context': context, 'reply': reply}))
  114. reply=e_context['reply']
  115. if not e_context.is_pass() and reply and reply.type:
  116. logger.debug('[wechatmp] ready to send reply: {} to {}'.format(reply, context['receiver']))
  117. self.send(reply, context['receiver'])
  118. else:
  119. cache_dict[context['receiver']] = (1, "No reply")
  120. logger.info("[threaded] Get reply for {}: {} \nA: {}".format(context['receiver'], context.content, reply.content))
  121. except Exception as exc:
  122. print(traceback.format_exc())
  123. cache_dict[context['receiver']] = (1, "ERROR")
  124. def POST(self):
  125. try:
  126. queryTime = time.time()
  127. webData = web.data()
  128. # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
  129. recMsg = receive.parse_xml(webData)
  130. if isinstance(recMsg, receive.Msg) and recMsg.MsgType == 'text':
  131. fromUser = recMsg.FromUserName
  132. toUser = recMsg.ToUserName
  133. createTime = recMsg.CreateTime
  134. message = recMsg.Content.decode("utf-8")
  135. message_id = recMsg.MsgId
  136. logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), fromUser, message_id, message))
  137. global cache_dict
  138. global query1
  139. global query2
  140. global query3
  141. cache_key = fromUser
  142. cache = cache_dict.get(cache_key)
  143. reply_text = ""
  144. # New request
  145. if cache == None:
  146. # The first query begin, reset the cache
  147. cache_dict[cache_key] = (0, "")
  148. # thread_pool.submit(self._do_build_reply, cache_key, fromUser, message)
  149. context = Context()
  150. context.kwargs = {'isgroup': False, 'receiver': fromUser, 'session_id': fromUser}
  151. R = redis.Redis(host='localhost', port=6379, db=0)
  152. user_openai_api_key = "openai_api_key_" + fromUser
  153. api_key = R.get(user_openai_api_key)
  154. if api_key != None:
  155. api_key = api_key.decode("utf-8")
  156. context['openai_api_key'] = api_key # None or user openai_api_key
  157. img_match_prefix = check_prefix(message, conf().get('image_create_prefix'))
  158. if img_match_prefix:
  159. message = message.replace(img_match_prefix, '', 1).strip()
  160. context.type = ContextType.IMAGE_CREATE
  161. else:
  162. context.type = ContextType.TEXT
  163. context.content = message
  164. thread_pool.submit(self.handle, context)
  165. query1[cache_key] = False
  166. query2[cache_key] = False
  167. query3[cache_key] = False
  168. # Request again
  169. elif cache[0] == 0 and query1.get(cache_key) == True and query2.get(cache_key) == True and query3.get(cache_key) == True:
  170. query1[cache_key] = False #To improve waiting experience, this can be set to True.
  171. query2[cache_key] = False #To improve waiting experience, this can be set to True.
  172. query3[cache_key] = False
  173. elif cache[0] >= 1:
  174. # Skip the waiting phase
  175. query1[cache_key] = True
  176. query2[cache_key] = True
  177. query3[cache_key] = True
  178. cache = cache_dict.get(cache_key)
  179. if query1.get(cache_key) == False:
  180. # The first query from wechat official server
  181. logger.debug("[wechatmp] query1 {}".format(cache_key))
  182. query1[cache_key] = True
  183. cnt = 0
  184. while cache[0] == 0 and cnt < 45:
  185. cnt = cnt + 1
  186. time.sleep(0.1)
  187. cache = cache_dict.get(cache_key)
  188. if cnt == 45:
  189. # waiting for timeout (the POST query will be closed by wechat official server)
  190. time.sleep(5)
  191. # and do nothing
  192. return
  193. else:
  194. pass
  195. elif query2.get(cache_key) == False:
  196. # The second query from wechat official server
  197. logger.debug("[wechatmp] query2 {}".format(cache_key))
  198. query2[cache_key] = True
  199. cnt = 0
  200. while cache[0] == 0 and cnt < 45:
  201. cnt = cnt + 1
  202. time.sleep(0.1)
  203. cache = cache_dict.get(cache_key)
  204. if cnt == 45:
  205. # waiting for timeout (the POST query will be closed by wechat official server)
  206. time.sleep(5)
  207. # and do nothing
  208. return
  209. else:
  210. pass
  211. elif query3.get(cache_key) == False:
  212. # The third query from wechat official server
  213. logger.debug("[wechatmp] query3 {}".format(cache_key))
  214. query3[cache_key] = True
  215. cnt = 0
  216. while cache[0] == 0 and cnt < 45:
  217. cnt = cnt + 1
  218. time.sleep(0.1)
  219. cache = cache_dict.get(cache_key)
  220. if cnt == 45:
  221. # Have waiting for 3x5 seconds
  222. # return timeout message
  223. reply_text = "【正在响应中,回复任意文字尝试获取回复】"
  224. logger.info("[wechatmp] Three queries has finished For {}: {}".format(fromUser, message_id))
  225. replyPost = reply.TextMsg(fromUser, toUser, reply_text).send()
  226. return replyPost
  227. else:
  228. pass
  229. if float(time.time()) - float(queryTime) > 4.8:
  230. logger.info("[wechatmp] Timeout for {} {}".format(fromUser, message_id))
  231. return
  232. if cache[0] > 1:
  233. reply_text = cache[1][:600] + " 【未完待续,回复任意文字以继续】" #wechatmp auto_reply length limit
  234. cache_dict[cache_key] = (cache[0] - 1, cache[1][600:])
  235. elif cache[0] == 1:
  236. reply_text = cache[1]
  237. cache_dict.pop(cache_key)
  238. logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text))
  239. replyPost = reply.TextMsg(fromUser, toUser, reply_text).send()
  240. return replyPost
  241. elif isinstance(recMsg, receive.Event) and recMsg.MsgType == 'event':
  242. logger.info("[wechatmp] Event {} from {}".format(recMsg.Event, recMsg.FromUserName))
  243. content = textwrap.dedent("""\
  244. 感谢您的关注!
  245. 这里是ChatGPT,可以自由对话。
  246. 资源有限,回复较慢,请勿着急。
  247. 支持通用表情输入。
  248. 暂时不支持图片输入。
  249. 支持图片输出,画字开头的问题将回复图片链接。
  250. 支持角色扮演和文字冒险两种定制模式对话。
  251. 输入'#帮助' 查看详细指令。""")
  252. replyMsg = reply.TextMsg(recMsg.FromUserName, recMsg.ToUserName, content)
  253. return replyMsg.send()
  254. else:
  255. print("暂且不处理")
  256. return "success"
  257. except Exception as exc:
  258. print(exc)
  259. return exc
  260. def check_prefix(content, prefix_list):
  261. for prefix in prefix_list:
  262. if content.startswith(prefix):
  263. return prefix
  264. return None