robot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import json
  2. import logging
  3. import signal
  4. from queue import Empty
  5. from threading import Thread
  6. from typing import Optional
  7. import requests
  8. import openai
  9. from google.protobuf import json_format
  10. from openai import OpenAI, api_key, base_url, AuthenticationError, APIConnectionError, APIError
  11. from wcferry import Wcf, WxMsg, wcf_pb2
  12. from common.log import logger
  13. from config import Config, conf
  14. from plugins.plugin import Plugin
  15. class Robot():
  16. def __init__(self, config: Config, wcf: Wcf) -> None:
  17. self.wcf = wcf
  18. self.config = config
  19. self.LOG = logger
  20. self.wxid = self.wcf.get_self_wxid()
  21. self.user = self.wcf.get_user_info()
  22. self.allContacts = self.getAllContacts()
  23. self.plugins: list[Plugin] = []
  24. # self.aiClient = OpenAI(api_key=self.config.get("openai_key"), base_url=self.config.get("openai_base"))
  25. self.LOG.info(f"{self.user} 登录成功")
  26. def register_plugin(self, plugin: Plugin):
  27. self.plugins.append(plugin)
  28. def enableRecvMsg(self) -> None:
  29. """
  30. 打开消息通知,可能会丢消息
  31. :return:
  32. """
  33. self.wcf.enable_recv_msg(self.onMsg)
  34. def enableReceivingMsg(self) -> None:
  35. """
  36. 打开消息通知,使用消息队列的方式获取
  37. :return:
  38. """
  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. except Empty:
  47. continue # Empty message
  48. except Exception as e:
  49. self.LOG.error(f"Receiving message error: {e}")
  50. self.wcf.enable_receiving_msg()
  51. Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start()
  52. def onMsg(self, msg: WxMsg) -> int:
  53. """
  54. 消息处理
  55. :param msg:
  56. :return:
  57. """
  58. # 判断 self.config.get("api_base") 是否包含 gkscrm.com 域
  59. if "gkscrm.com" not in self.config.get("api_base"):
  60. return 0
  61. try:
  62. self.LOG.info(f"Received message: {msg}") # 打印信息
  63. self.processMsg(msg)
  64. except Exception as e:
  65. self.LOG.error(e)
  66. return 0
  67. def processMsg(self, msg: WxMsg) -> None:
  68. """当接收到消息的时候,会调用本方法。如果不实现本方法,则打印原始消息。
  69. 此处可进行自定义发送的内容,如通过 msg.content 关键字自动获取当前天气信息,并发送到对应的群组@发送者
  70. 群号:msg.roomid 微信ID:msg.sender 消息内容:msg.content
  71. content = "xx天气信息为:"
  72. receivers = msg.roomid
  73. self.sendTextMsg(content, receivers, msg.sender)
  74. """
  75. rsp = ""
  76. if msg.from_self():
  77. return
  78. elif msg.is_text():
  79. if msg.from_group():
  80. if "ALL" not in self.config.get("contacts_white_list") and "ALL_GROUPS" not in self.config.get(
  81. "contacts_white_list") and msg.roomid not in self.config.get(
  82. "contacts_white_list") and self.allContacts.get(msg.roomid) not in self.config.get(
  83. "contacts_white_list"):
  84. return
  85. if msg.is_at(self.wxid) is False and "@" + self.user.get("name") not in msg.content:
  86. return
  87. rsp = self.get_answer(msg) # 闲聊
  88. else:
  89. if "ALL" not in self.config.get("contacts_white_list") and "ALL_CONTACTS" not in self.config.get(
  90. "contacts_white_list") and msg.sender not in self.config.get(
  91. "contacts_white_list") and self.allContacts.get(msg.sender) not in self.config.get(
  92. "contacts_white_list"):
  93. return
  94. rsp = self.get_answer(msg) # 闲聊
  95. if rsp != '':
  96. try:
  97. json_object = json.loads(rsp)
  98. if "type" in json_object[0] and "content" in json_object[0]:
  99. for item in json_object:
  100. if item["content"] == "":
  101. continue
  102. if item["type"] == "TEXT":
  103. if msg.from_group():
  104. self.sendTextMsg(item["content"], msg.roomid, msg.sender)
  105. else:
  106. self.sendTextMsg(item["content"], msg.sender)
  107. elif item["type"] == "IMAGE_URL" or item["type"] == "IMAGE":
  108. if msg.from_group():
  109. self.wcf.send_image(item["content"], msg.roomid)
  110. else:
  111. self.wcf.send_image(item["content"], msg.sender)
  112. elif item["type"] == "FILE" or item["type"] == "FILE_URL" or item["type"] == "VIDEO_URL":
  113. if msg.from_group():
  114. self.wcf.send_file(item["content"], msg.roomid)
  115. else:
  116. self.wcf.send_file(item["content"], msg.sender)
  117. except json.JSONDecodeError:
  118. if msg.from_group():
  119. self.sendTextMsg(rsp, msg.roomid, msg.sender)
  120. else:
  121. self.sendTextMsg(rsp, msg.sender)
  122. def get_answer(self, msg: WxMsg) -> str:
  123. rsp = ""
  124. for plugin in self.plugins:
  125. rsp = plugin.answer(msg, self.wxid)
  126. if rsp != "":
  127. break
  128. return rsp
  129. # try:
  130. # # self.aiClient.api_key = self.config.get("api_key")
  131. # # self.aiClient.base_url = self.config.get("api_base")
  132. #
  133. # # 在fastgpt的时候增加chatId字段
  134. # if "fastgpt" in self.config.get("api_base"):
  135. # extra_body = {
  136. # "chatId": "chatId-" + msg.sender + "-" + msg.roomid
  137. # }
  138. # else:
  139. # extra_body = {}
  140. #
  141. # ret = self.aiClient.chat.completions.create(
  142. # model=self.config.get("open_ai_model", "gpt-3.5-turbo"),
  143. # max_tokens=self.config.get("open_ai_max_tokens", 8192),
  144. # temperature=self.config.get("open_ai_temperature", 0.7),
  145. # top_p=self.config.get("open_ai_top_p", 1),
  146. # extra_body=extra_body,
  147. # messages=[
  148. # {"role": "user", "content": msg.content}
  149. # ]
  150. # )
  151. # rsp = ret.choices[0].message.content
  152. # rsp = rsp[2:] if rsp.startswith("\n\n") else rsp
  153. # rsp = rsp.replace("\n\n", "\n")
  154. # self.LOG.info(rsp)
  155. # except AuthenticationError:
  156. # self.LOG.error("OpenAI API 认证失败,请检查 API 密钥是否正确")
  157. # except APIConnectionError:
  158. # self.LOG.error("无法连接到 OpenAI API,请检查网络连接")
  159. # except APIError as e1:
  160. # self.LOG.error(f"OpenAI API 返回了错误:{str(e1)}")
  161. # except Exception as e0:
  162. # self.LOG.error(f"发生未知错误:{str(e0)}")
  163. # return rsp
  164. def sendTextMsg(self, msg: str, receiver: str, at_list: str = "") -> int:
  165. """ 发送消息
  166. :param msg: 消息字符串
  167. :param receiver: 接收人wxid或者群id
  168. :param at_list: 要@的wxid, @所有人的wxid为:notify@all
  169. """
  170. # msg 中需要有 @ 名单中一样数量的 @
  171. ats = ""
  172. if at_list:
  173. if at_list == "notify@all": # @所有人
  174. ats = " @所有人"
  175. else:
  176. wxids = at_list.split(",")
  177. for wxid in wxids:
  178. # 根据 wxid 查找群昵称
  179. ats += f" @{self.wcf.get_alias_in_chatroom(wxid, receiver)}"
  180. # {msg}{ats} 表示要发送的消息内容后面紧跟@,例如 北京天气情况为:xxx @张三
  181. if ats == "":
  182. self.LOG.info(f"To {receiver}: {msg}")
  183. result = self.wcf.send_text(f"{msg}", receiver, at_list)
  184. return result
  185. else:
  186. self.LOG.info(f"To {receiver}: {ats}\r{msg}")
  187. result = self.wcf.send_text(f"{ats}\n\n{msg}", receiver, at_list)
  188. return result
  189. def getAllContacts(self) -> dict:
  190. """
  191. 获取联系人(包括好友、公众号、服务号、群成员……)
  192. 格式: {"wxid": "NickName"}
  193. """
  194. contacts = self.wcf.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;")
  195. return {contact["UserName"]: contact["NickName"] for contact in contacts}
  196. def keepRunningAndBlockProcess(self) -> None:
  197. """
  198. 保持机器人运行,不让进程退出
  199. """
  200. self.wcf.keep_running()
  201. def getFriendOrChatRoomList(self, type: str, label_id: int) -> list:
  202. """
  203. 获取好友列表或者群列表
  204. :param type: 1: 联系人 2: 群组
  205. :param label_id:
  206. :return: 好友列表或者群列表
  207. """
  208. contacts_list = []
  209. contacts = self.wcf.query_sql("MicroMsg.db", "select UserName, NickName, LabelIDList, Type from Contact")
  210. not_contacts = {
  211. "fmessage": "朋友推荐消息",
  212. "medianote": "语音记事本",
  213. "floatbottle": "漂流瓶",
  214. "filehelper": "文件传输助手",
  215. "newsapp": "新闻",
  216. "mphelper": "公众平台安全助手",
  217. }
  218. for cnt in contacts:
  219. if ((type == "联系人" and (cnt["Type"] != 3 or (label_id != 0 and not is_int_in_string(label_id, cnt["LabelIDList"])))) or # 群组
  220. (type == "群组" and (cnt["Type"] != 2 or not cnt["UserName"].endswith("@chatroom"))) or # 联系人
  221. cnt["UserName"].startswith("gh_") or # 公众号
  222. cnt["UserName"] in not_contacts.keys() # 其他杂号
  223. ):
  224. continue
  225. contact = {
  226. "wxid": cnt.get("UserName", ""),
  227. "name": cnt.get("NickName", "")}
  228. contacts_list.append(contact)
  229. return contacts_list
  230. def getContactLabelList(self) -> list:
  231. """
  232. 查询数据库
  233. :param sql: 查询语句
  234. :param params: 参数
  235. :return: 查询结果
  236. """
  237. return self.wcf.query_sql("MicroMsg.db", "select * from ContactLabel")
  238. robot: Optional[Robot] = None
  239. def init_robot():
  240. global robot
  241. wcf = Wcf(debug=conf().get("debug", False))
  242. def handler(sig, frame):
  243. wcf.cleanup() # 退出前清理环境
  244. exit(0)
  245. signal.signal(signal.SIGINT, handler)
  246. robot = Robot(conf(), wcf)
  247. def get_robot():
  248. return robot
  249. def is_int_in_string(int_value, str_values):
  250. # 拆分字符串并去除多余空格
  251. split_values = map(str.strip, str_values.split(','))
  252. # 过滤掉空字符串,转换为整数列表
  253. values_list = list(map(int, filter(lambda x: x != '', split_values)))
  254. # 检查整数是否在列表中
  255. return int_value in values_list