robot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import json
  2. import logging
  3. from queue import Empty
  4. from threading import Thread
  5. from typing import Callable
  6. import blackboxprotobuf
  7. import openai
  8. from openai import OpenAI, api_key, base_url, AuthenticationError, APIConnectionError, APIError
  9. from wcferry import Wcf, WxMsg
  10. from common.log import logger
  11. from config import Config
  12. class Robot():
  13. def __init__(self, config: Config, wcf: Wcf) -> None:
  14. self.wcf = wcf
  15. self.config = config
  16. self.LOG = logger
  17. self.wxid = self.wcf.get_self_wxid()
  18. # self.user = self.wcf.get_user_info()
  19. # self.allContacts = self.getAllContacts()
  20. # self.aiClient = OpenAI(api_key=self.config.get("api_key"), base_url=self.config.get("api_base"))
  21. self.contacts = self.get_contacts()
  22. self.user = self.get_user_info()
  23. self.LOG.info(f"{self.user} 登录成功")
  24. def enableRecvMsg(self) -> None:
  25. """
  26. 打开消息通知,可能会丢消息
  27. :return:
  28. """
  29. self.wcf.enable_recv_msg(self.onMsg)
  30. def enableReceivingMsgCallback(self,callback:Callable[[WxMsg], None] = None) -> bool:
  31. """
  32. 打开消息通知,使用消息队列的方式获取
  33. callback 必须包含参数 msg:WxMsg
  34. :return:
  35. """
  36. if callback is None:
  37. self.enableReceivingMsg()
  38. return True
  39. def innerProcessMsg(wcf: Wcf):
  40. while wcf.is_receiving_msg():
  41. try:
  42. msg = wcf.get_msg()
  43. self.LOG.info(msg)
  44. # self.processMsg(msg)
  45. # self.onMsg(msg)
  46. callback(msg)
  47. except Empty:
  48. continue # Empty message
  49. except Exception as e:
  50. self.LOG.error(f"Receiving message error: {e}")
  51. self.wcf.enable_receiving_msg()
  52. Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start()
  53. def enableReceivingMsg(self) -> None:
  54. """
  55. 打开消息通知,使用消息队列的方式获取
  56. :return:
  57. """
  58. def innerProcessMsg(wcf: Wcf):
  59. while wcf.is_receiving_msg():
  60. try:
  61. msg = wcf.get_msg()
  62. # self.LOG.info(msg)
  63. # self.processMsg(msg)
  64. self.onMsg(msg)
  65. except Empty:
  66. continue # Empty message
  67. except Exception as e:
  68. self.LOG.error(f"Receiving message error: {e}")
  69. self.wcf.enable_receiving_msg()
  70. Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start()
  71. def onMsg(self, msg: WxMsg) -> int:
  72. """
  73. 消息处理
  74. :param msg:
  75. :return:
  76. """
  77. # 判断 self.config.get("api_base") 是否包含 gkscrm.com 域
  78. if "gkscrm.com" not in self.config.get("api_base"):
  79. return 0
  80. try:
  81. self.LOG.info(f"Received message: {msg}") # 打印信息
  82. self.processMsg(msg)
  83. except Exception as e:
  84. self.LOG.error(e)
  85. return 0
  86. def processMsg(self, msg: WxMsg) -> None:
  87. """当接收到消息的时候,会调用本方法。如果不实现本方法,则打印原始消息。
  88. 此处可进行自定义发送的内容,如通过 msg.content 关键字自动获取当前天气信息,并发送到对应的群组@发送者
  89. 群号:msg.roomid 微信ID:msg.sender 消息内容:msg.content
  90. content = "xx天气信息为:"
  91. receivers = msg.roomid
  92. self.sendTextMsg(content, receivers, msg.sender)
  93. """
  94. rsp = ""
  95. if msg.from_self():
  96. return
  97. elif msg.is_text():
  98. if msg.from_group():
  99. if "ALL" not in self.config.get("contacts_white_list") and "ALL_GROUPS" not in self.config.get("contacts_white_list") and msg.roomid not in self.config.get("contacts_white_list") and self.allContacts.get(msg.roomid) not in self.config.get("contacts_white_list"):
  100. return
  101. if msg.is_at(self.wxid) is False and "@"+self.user.get("name") not in msg.content:
  102. return
  103. rsp = self.get_answer(msg) # 闲聊
  104. else:
  105. if "ALL" not in self.config.get("contacts_white_list") and "ALL_CONTACTS" not in self.config.get("contacts_white_list") and msg.sender not in self.config.get("contacts_white_list") and self.allContacts.get(msg.sender) not in self.config.get("contacts_white_list"):
  106. return
  107. rsp = self.get_answer(msg) # 闲聊
  108. if rsp != '':
  109. try:
  110. json_object = json.loads(rsp)
  111. if "type" in json_object[0] and "content" in json_object[0]:
  112. for item in json_object:
  113. if item["content"] == "":
  114. continue
  115. if item["type"] == "TEXT":
  116. if msg.from_group():
  117. self.sendTextMsg(item["content"], msg.roomid, msg.sender)
  118. else:
  119. self.sendTextMsg(item["content"], msg.sender)
  120. elif item["type"] == "IMAGE_URL" or item["type"] == "IMAGE":
  121. if msg.from_group():
  122. self.wcf.send_image(item["content"], msg.roomid)
  123. else:
  124. self.wcf.send_image(item["content"], msg.sender)
  125. elif item["type"] == "FILE" or item["type"] == "FILE_URL" or item["type"] == "VIDEO_URL":
  126. if msg.from_group():
  127. self.wcf.send_file(item["content"], msg.roomid)
  128. else:
  129. self.wcf.send_file(item["content"], msg.sender)
  130. except json.JSONDecodeError:
  131. if msg.from_group():
  132. self.sendTextMsg(rsp, msg.roomid, msg.sender)
  133. else:
  134. self.sendTextMsg(rsp, msg.sender)
  135. def get_answer(self, msg: WxMsg) -> str:
  136. rsp = ""
  137. try:
  138. self.aiClient.api_key = self.config.get("api_key")
  139. self.aiClient.base_url = self.config.get("api_base")
  140. # 在fastgpt的时候增加chatId字段
  141. if "fastgpt" in self.config.get("api_base"):
  142. extra_body = {
  143. "chatId": "chatId-"+msg.sender
  144. }
  145. else:
  146. extra_body = {}
  147. ret = self.aiClient.chat.completions.create(
  148. model=self.config.get("open_ai_model", "gpt-3.5-turbo"),
  149. max_tokens=self.config.get("open_ai_max_tokens",8192),
  150. temperature=self.config.get("open_ai_temperature",0.7),
  151. top_p=self.config.get("open_ai_top_p",1),
  152. extra_body=extra_body,
  153. messages=[
  154. {"role": "user", "content": msg.content}
  155. ]
  156. )
  157. rsp = ret.choices[0].message.content
  158. rsp = rsp[2:] if rsp.startswith("\n\n") else rsp
  159. rsp = rsp.replace("\n\n", "\n")
  160. self.LOG.info(rsp)
  161. except AuthenticationError:
  162. self.LOG.error("OpenAI API 认证失败,请检查 API 密钥是否正确")
  163. except APIConnectionError:
  164. self.LOG.error("无法连接到 OpenAI API,请检查网络连接")
  165. except APIError as e1:
  166. self.LOG.error(f"OpenAI API 返回了错误:{str(e1)}")
  167. except Exception as e0:
  168. self.LOG.error(f"发生未知错误:{str(e0)}")
  169. return rsp
  170. def sendTextMsg(self, msg: str, receiver: str, at_list: str = "") -> None:
  171. """ 发送消息
  172. :param msg: 消息字符串
  173. :param receiver: 接收人wxid或者群id
  174. :param at_list: 要@的wxid, @所有人的wxid为:notify@all
  175. """
  176. # msg 中需要有 @ 名单中一样数量的 @
  177. ats = ""
  178. if at_list:
  179. if at_list == "notify@all": # @所有人
  180. ats = " @所有人"
  181. else:
  182. wxids = at_list.split(",")
  183. for wxid in wxids:
  184. # 根据 wxid 查找群昵称
  185. ats += f" @{self.wcf.get_alias_in_chatroom(wxid, receiver)}"
  186. # {msg}{ats} 表示要发送的消息内容后面紧跟@,例如 北京天气情况为:xxx @张三
  187. if ats == "":
  188. self.LOG.info(f"To {receiver}: {msg}")
  189. self.wcf.send_text(f"{msg}", receiver, at_list)
  190. else:
  191. self.LOG.info(f"To {receiver}: {ats}\r{msg}")
  192. self.wcf.send_text(f"{ats}\n\n{msg}", receiver, at_list)
  193. def getAllContacts(self) -> dict:
  194. """
  195. 获取联系人(包括好友、公众号、服务号、群成员……)
  196. 格式: {"wxid": "NickName"}
  197. """
  198. contacts = self.wcf.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;")
  199. return {contact["UserName"]: contact["NickName"] for contact in contacts}
  200. def keepRunningAndBlockProcess(self) -> None:
  201. """
  202. 保持机器人运行,不让进程退出
  203. """
  204. self.wcf.keep_running()
  205. def get_contacts(self):
  206. """获取联系人(包括好友、公众号、服务号、群成员……)"""
  207. contacts = self.wcf.get_contacts()
  208. list = {}
  209. for contact in contacts:
  210. headimg = self.wcf.query_sql("MicroMsg.db",
  211. "select * from ContactHeadImgUrl WHERE usrName = '" + contact['wxid'] + "' limit 1")
  212. if len(headimg) > 0:
  213. contact['avatar'] = str(headimg[0].get('smallHeadImgUrl', ''))
  214. res = self.wcf.query_sql("MicroMsg.db", "select * from Contact where UserName='" + contact['wxid'] + "' limit 1")
  215. if len(res) > 0:
  216. contact.update(res[0])
  217. if "chatroom" in contact['wxid']:
  218. chatroom_res = self.wcf.query_sql("MicroMsg.db",
  219. "select * from ChatRoom where ChatRoomName='" + contact['wxid'] + "' limit 1")
  220. if len(chatroom_res) > 0:
  221. contact.update(chatroom_res[0])
  222. if "ExtraBuf" in contact and isinstance(contact['ExtraBuf'], bytes):
  223. contact['ExtraBuf'] = contact['ExtraBuf'].decode('utf-8',errors = 'replace')
  224. list[contact['wxid']] = contact
  225. return list
  226. def get_user_info(self):
  227. """获取登录账号个人信息"""
  228. user = self.wcf.get_user_info()
  229. headimg = self.wcf.query_sql("MicroMsg.db",
  230. "select * from ContactHeadImgUrl WHERE usrName = '" + self.wxid + "' limit 1")
  231. user['avatar'] = str(headimg[0].get('smallHeadImgUrl', ''))
  232. if self.contacts is None:
  233. self.contacts = self.get_contacts()
  234. if user['wxid'] in self.contacts:
  235. user.update(self.contacts.get(user['wxid']))
  236. return user
  237. def get_BytesExtra(self,BytesExtra:bytes):
  238. try:
  239. deserialize_data, message_type = blackboxprotobuf.decode_message(BytesExtra)
  240. return deserialize_data
  241. except Exception as e:
  242. logger.warning(f"\nget_BytesExtra: {e}\n{BytesExtra}", exc_info=True)
  243. return None