Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

10 місяці тому
10 місяці тому
10 місяці тому
10 місяці тому
10 місяці тому
10 місяці тому
10 місяці тому
10 місяці тому
10 місяці тому
5 місяці тому
5 місяці тому
5 місяці тому
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 patpat 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