You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dingtalk_channel.py 8.6KB

10 maanden geleden
10 maanden geleden
10 maanden geleden
10 maanden geleden
10 maanden geleden
10 maanden geleden
10 maanden geleden
10 maanden geleden
10 maanden geleden
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """
  2. 钉钉通道接入
  3. @author huiwen
  4. @Date 2023/11/28
  5. """
  6. import copy
  7. import json
  8. # -*- coding=utf-8 -*-
  9. import logging
  10. import time
  11. import dingtalk_stream
  12. from dingtalk_stream import AckMessage
  13. from dingtalk_stream.card_replier import AICardReplier
  14. from dingtalk_stream.card_replier import AICardStatus
  15. from dingtalk_stream.card_replier import CardReplier
  16. from bridge.context import Context, ContextType
  17. from bridge.reply import Reply, ReplyType
  18. from channel.chat_channel import ChatChannel
  19. from channel.dingtalk.dingtalk_message import DingTalkMessage
  20. from common.expired_dict import ExpiredDict
  21. from common.log import logger
  22. from common.singleton import singleton
  23. from common.time_check import time_checker
  24. from config import conf
  25. class CustomAICardReplier(CardReplier):
  26. def __init__(self, dingtalk_client, incoming_message):
  27. super(AICardReplier, self).__init__(dingtalk_client, incoming_message)
  28. def start(
  29. self,
  30. card_template_id: str,
  31. card_data: dict,
  32. recipients: list = None,
  33. support_forward: bool = True,
  34. ) -> str:
  35. """
  36. AI卡片的创建接口
  37. :param support_forward:
  38. :param recipients:
  39. :param card_template_id:
  40. :param card_data:
  41. :return:
  42. """
  43. card_data_with_status = copy.deepcopy(card_data)
  44. card_data_with_status["flowStatus"] = AICardStatus.PROCESSING
  45. return self.create_and_send_card(
  46. card_template_id,
  47. card_data_with_status,
  48. at_sender=True,
  49. at_all=False,
  50. recipients=recipients,
  51. support_forward=support_forward,
  52. )
  53. # 对 AICardReplier 进行猴子补丁
  54. AICardReplier.start = CustomAICardReplier.start
  55. def _check(func):
  56. def wrapper(self, cmsg: DingTalkMessage):
  57. msgId = cmsg.msg_id
  58. if msgId in self.receivedMsgs:
  59. logger.info("DingTalk message {} already received, ignore".format(msgId))
  60. return
  61. self.receivedMsgs[msgId] = True
  62. create_time = cmsg.create_time # 消息时间戳
  63. if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息
  64. logger.debug("[DingTalk] History message {} skipped".format(msgId))
  65. return
  66. if cmsg.my_msg and not cmsg.is_group:
  67. logger.debug("[DingTalk] My message {} skipped".format(msgId))
  68. return
  69. return func(self, cmsg)
  70. return wrapper
  71. @singleton
  72. class DingTalkChanel(ChatChannel, dingtalk_stream.ChatbotHandler):
  73. dingtalk_client_id = conf().get('dingtalk_client_id')
  74. dingtalk_client_secret = conf().get('dingtalk_client_secret')
  75. def setup_logger(self):
  76. logger = logging.getLogger()
  77. handler = logging.StreamHandler()
  78. handler.setFormatter(
  79. logging.Formatter('%(asctime)s %(name)-8s %(levelname)-8s %(message)s [%(filename)s:%(lineno)d]'))
  80. logger.addHandler(handler)
  81. logger.setLevel(logging.INFO)
  82. return logger
  83. def __init__(self):
  84. super().__init__()
  85. super(dingtalk_stream.ChatbotHandler, self).__init__()
  86. self.logger = self.setup_logger()
  87. # 历史消息id暂存,用于幂等控制
  88. self.receivedMsgs = ExpiredDict(conf().get("expires_in_seconds"))
  89. logger.info("[DingTalk] client_id={}, client_secret={} ".format(
  90. self.dingtalk_client_id, self.dingtalk_client_secret))
  91. # 无需群校验和前缀
  92. conf()["group_name_white_list"] = ["ALL_GROUP"]
  93. # 单聊无需前缀
  94. conf()["single_chat_prefix"] = [""]
  95. def startup(self):
  96. credential = dingtalk_stream.Credential(self.dingtalk_client_id, self.dingtalk_client_secret)
  97. client = dingtalk_stream.DingTalkStreamClient(credential)
  98. client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self)
  99. client.start_forever()
  100. async def process(self, callback: dingtalk_stream.CallbackMessage):
  101. try:
  102. incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data)
  103. image_download_handler = self # 传入方法所在的类实例
  104. dingtalk_msg = DingTalkMessage(incoming_message, image_download_handler)
  105. if dingtalk_msg.is_group:
  106. self.handle_group(dingtalk_msg)
  107. else:
  108. self.handle_single(dingtalk_msg)
  109. return AckMessage.STATUS_OK, 'OK'
  110. except Exception as e:
  111. logger.error(f"dingtalk process error={e}")
  112. return AckMessage.STATUS_SYSTEM_EXCEPTION, 'ERROR'
  113. @time_checker
  114. @_check
  115. def handle_single(self, cmsg: DingTalkMessage):
  116. # 处理单聊消息
  117. if cmsg.ctype == ContextType.VOICE:
  118. logger.debug("[DingTalk]receive voice msg: {}".format(cmsg.content))
  119. elif cmsg.ctype == ContextType.IMAGE:
  120. logger.debug("[DingTalk]receive image msg: {}".format(cmsg.content))
  121. elif cmsg.ctype == ContextType.IMAGE_CREATE:
  122. logger.debug("[DingTalk]receive image create msg: {}".format(cmsg.content))
  123. elif cmsg.ctype == ContextType.PATPAT:
  124. logger.debug("[DingTalk]receive patpat msg: {}".format(cmsg.content))
  125. elif cmsg.ctype == ContextType.TEXT:
  126. logger.debug("[DingTalk]receive text msg: {}".format(cmsg.content))
  127. else:
  128. logger.debug("[DingTalk]receive other msg: {}".format(cmsg.content))
  129. context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg)
  130. if context:
  131. self.produce(context)
  132. @time_checker
  133. @_check
  134. def handle_group(self, cmsg: DingTalkMessage):
  135. # 处理群聊消息
  136. if cmsg.ctype == ContextType.VOICE:
  137. logger.debug("[DingTalk]receive voice msg: {}".format(cmsg.content))
  138. elif cmsg.ctype == ContextType.IMAGE:
  139. logger.debug("[DingTalk]receive image msg: {}".format(cmsg.content))
  140. elif cmsg.ctype == ContextType.IMAGE_CREATE:
  141. logger.debug("[DingTalk]receive image create msg: {}".format(cmsg.content))
  142. elif cmsg.ctype == ContextType.PATPAT:
  143. logger.debug("[DingTalk]receive patpat msg: {}".format(cmsg.content))
  144. elif cmsg.ctype == ContextType.TEXT:
  145. logger.debug("[DingTalk]receive text msg: {}".format(cmsg.content))
  146. else:
  147. logger.debug("[DingTalk]receive other msg: {}".format(cmsg.content))
  148. context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg)
  149. context['no_need_at'] = True
  150. if context:
  151. self.produce(context)
  152. def send(self, reply: Reply, context: Context):
  153. receiver = context["receiver"]
  154. isgroup = context.kwargs['msg'].is_group
  155. incoming_message = context.kwargs['msg'].incoming_message
  156. if conf().get("dingtalk_card_enabled"):
  157. logger.info("[Dingtalk] sendMsg={}, receiver={}".format(reply, receiver))
  158. def reply_with_text():
  159. self.reply_text(reply.content, incoming_message)
  160. def reply_with_at_text():
  161. self.reply_text("📢 您有一条新的消息,请查看。", incoming_message)
  162. def reply_with_ai_markdown():
  163. button_list, markdown_content = self.generate_button_markdown_content(context, reply)
  164. self.reply_ai_markdown_button(incoming_message, markdown_content, button_list, "", "📌 内容由AI生成", "",[incoming_message.sender_staff_id])
  165. if reply.type in [ReplyType.IMAGE_URL, ReplyType.IMAGE, ReplyType.TEXT]:
  166. if isgroup:
  167. reply_with_ai_markdown()
  168. reply_with_at_text()
  169. else:
  170. reply_with_ai_markdown()
  171. else:
  172. # 暂不支持其它类型消息回复
  173. reply_with_text()
  174. else:
  175. self.reply_text(reply.content, incoming_message)
  176. def generate_button_markdown_content(self, context, reply):
  177. image_url = context.kwargs.get("image_url")
  178. promptEn = context.kwargs.get("promptEn")
  179. reply_text = reply.content
  180. button_list = []
  181. markdown_content = f"""
  182. {reply.content}
  183. """
  184. if image_url is not None and promptEn is not None:
  185. button_list = [
  186. {"text": "查看原图", "url": image_url, "iosUrl": image_url, "color": "blue"}
  187. ]
  188. markdown_content = f"""
  189. {promptEn}
  190. !["图片"]({image_url})
  191. {reply_text}
  192. """
  193. logger.debug(f"[Dingtalk] generate_button_markdown_content, button_list={button_list} , markdown_content={markdown_content}")
  194. return button_list, markdown_content