Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3923e71

Browse files
authoredJun 9, 2023
Merge pull request #6 from open-dingtalk/features/20230609_linya_addthreadexecutor
feat: 1、机器人模式下增加异步任务处理模式;2、增加一些实用的卡片模板
2 parents b83adba + 5abe1a5 commit 3923e71

File tree

4 files changed

+219
-29
lines changed

4 files changed

+219
-29
lines changed
 

‎dingtalk_stream/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
from .chatbot import ChatbotMessage
1111
from .chatbot import TextContent
1212
from .chatbot import AtUser
13-
from .chatbot import ChatbotHandler
13+
from .chatbot import ChatbotHandler, AsyncChatbotHandler

‎dingtalk_stream/chatbot.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# -*- coding:utf-8 -*-
22

33
import json
4-
import uuid
54
import requests
65
import platform
76
import hashlib
8-
from .stream import CallbackHandler
7+
from .stream import CallbackHandler, CallbackMessage
8+
from .frames import AckMessage, Headers
9+
from concurrent.futures import ThreadPoolExecutor
10+
import uuid
911

1012

1113
class AtUser(object):
@@ -272,6 +274,7 @@ def reply_card(self,
272274
) % platform.python_version(),
273275
}
274276

277+
card_biz_id = self._gen_card_id(incoming_message)
275278
body = {"cardTemplateId": "StandardCard",
276279
"robotCode": self.dingtalk_client.credential.client_id,
277280
"cardData": json.dumps(card_data),
@@ -281,7 +284,7 @@ def reply_card(self,
281284
# "receiverListJson": "String",
282285
# "cardPropertyJson": "String"
283286
},
284-
"cardBizId": self._gen_card_id(incoming_message)
287+
"cardBizId": card_biz_id
285288
}
286289

287290
if incoming_message.conversation_type == '2':
@@ -304,11 +307,12 @@ def reply_card(self,
304307
json=body)
305308
response.raise_for_status()
306309

307-
return response.json
310+
return card_biz_id
308311
except Exception as e:
309-
return {}
312+
self.logger.error('reply card failed, error=%s', e)
313+
return ""
310314

311-
def update_card(self, card_data: dict, incoming_message: ChatbotMessage):
315+
def update_card(self, card_biz_id: str, card_data: dict):
312316
"""
313317
回复互动卡片。由于sessionWebhook不支持发送互动卡片,所以需要使用OpenAPI,当前仅支持自建应用。
314318
https://open.dingtalk.com/document/orgapp/robots-send-interactive-cards
@@ -330,9 +334,8 @@ def update_card(self, card_data: dict, incoming_message: ChatbotMessage):
330334
) % platform.python_version(),
331335
}
332336

333-
card_id = self._gen_card_id(incoming_message)
334337
values = {
335-
'cardBizId': card_id,
338+
'cardBizId': card_biz_id,
336339
'cardData': json.dumps(card_data),
337340
}
338341
url = 'https://api.dingtalk.com/v1.0/im/robots/interactiveCards'
@@ -348,7 +351,45 @@ def update_card(self, card_data: dict, incoming_message: ChatbotMessage):
348351

349352
@staticmethod
350353
def _gen_card_id(msg: ChatbotMessage):
351-
factor = '%s_%s_%s_%s' % (msg.sender_id, msg.sender_corp_id, msg.conversation_id, msg.message_id)
354+
factor = '%s_%s_%s_%s_%s' % (
355+
msg.sender_id, msg.sender_corp_id, msg.conversation_id, msg.message_id, str(uuid.uuid1()))
352356
m = hashlib.sha256()
353357
m.update(factor.encode('utf-8'))
354358
return m.hexdigest()
359+
360+
361+
class AsyncChatbotHandler(ChatbotHandler):
362+
"""
363+
多任务执行handler,注意:process函数重载的时候不要用 async
364+
"""
365+
366+
async_executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=8)
367+
368+
def __init__(self, max_workers: int = 8):
369+
super(AsyncChatbotHandler, self).__init__()
370+
self.async_executor = ThreadPoolExecutor(max_workers=max_workers)
371+
372+
def process(self, message):
373+
'''
374+
不要用 async 修饰
375+
:param message:
376+
:return:
377+
'''
378+
return AckMessage.STATUS_NOT_IMPLEMENT, 'not implement'
379+
380+
async def raw_process(self, callback_message: CallbackMessage):
381+
def func():
382+
try:
383+
self.process(callback_message)
384+
except Exception as e:
385+
self.logger.error('async process exception, error=%s', e)
386+
387+
self.async_executor.submit(func)
388+
389+
ack_message = AckMessage()
390+
ack_message.code = AckMessage.STATUS_OK
391+
ack_message.headers.message_id = callback_message.headers.message_id
392+
ack_message.headers.content_type = Headers.CONTENT_TYPE_APPLICATION_JSON
393+
ack_message.message = "ok"
394+
ack_message.data = callback_message.data
395+
return ack_message

‎dingtalk_stream/interactive_card.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- coding:utf-8 -*-
22

3+
import copy, uuid
4+
35
"""
46
这里是卡片模板库,提供一些必要的卡片组件组合。
57
INTERACTIVE_CARD_JSON_SAMPLE_1 极简卡片组合:title-text-image-button
@@ -8,6 +10,132 @@
810
高阶需求请至卡片搭建平台:https://card.dingtalk.com/card-builder
911
"""
1012

13+
'''
14+
实用卡片模板:多行文本
15+
'''
16+
INTERACTIVE_CARD_JSON_SAMPLE_MULTI_TEXT_LINE = {
17+
"config": {
18+
"autoLayout": True,
19+
"enableForward": True
20+
},
21+
"header": {
22+
"title": {
23+
"type": "text",
24+
"text": "钉钉卡片"
25+
},
26+
"logo": "@lALPDfJ6V_FPDmvNAfTNAfQ"
27+
},
28+
"contents": [
29+
{
30+
"type": "text",
31+
"text": "钉钉正在为各行各业提供专业解决方案,沉淀钉钉1900万企业组织核心业务场景,提供专属钉钉、教育、医疗、新零售等多行业多维度的解决方案。",
32+
"id": "text_1686281949314"
33+
},
34+
{
35+
"type": "divider",
36+
"id": "divider_1686281949314"
37+
}
38+
]
39+
}
40+
41+
42+
def generate_multi_text_line_card_data(title: str, logo: str, texts: [str]) -> dict:
43+
card_data = copy.deepcopy(INTERACTIVE_CARD_JSON_SAMPLE_MULTI_TEXT_LINE)
44+
45+
if title != "":
46+
card_data["header"]["title"]["text"] = title
47+
48+
if logo != "":
49+
card_data["header"]["logo"] = logo
50+
51+
card_data["contents"] = []
52+
for text in texts:
53+
text_line = {
54+
"type": "text",
55+
"text": text,
56+
"id": "text_" + str(uuid.uuid1())
57+
}
58+
divider_line = {
59+
"type": "divider",
60+
"id": "divider_" + str(uuid.uuid1())
61+
}
62+
card_data["contents"].append(text_line)
63+
card_data["contents"].append(divider_line)
64+
65+
return card_data
66+
67+
68+
'''
69+
实用卡片模板,多行文本+多图组合
70+
'''
71+
INTERACTIVE_CARD_JSON_SAMPLE_MULTI_TEXT_IMAGE = {
72+
"config": {
73+
"autoLayout": True,
74+
"enableForward": True
75+
},
76+
"header": {
77+
"title": {
78+
"type": "text",
79+
"text": "钉钉卡片"
80+
},
81+
"logo": "@lALPDfJ6V_FPDmvNAfTNAfQ"
82+
},
83+
"contents": [
84+
{
85+
"type": "text",
86+
"text": "钉钉正在为各行各业提供专业解决方案,沉淀钉钉1900万企业组织核心业务场景,提供专属钉钉、教育、医疗、新零售等多行业多维度的解决方案。",
87+
"id": "text_1686281949314"
88+
},
89+
{
90+
"type": "divider",
91+
"id": "divider_1686281949314"
92+
},
93+
{
94+
"type": "imageList",
95+
"images": [
96+
"@lADPDe7s2ySi18PNA6XNBXg",
97+
"@lADPDf0i1beuNF3NAxTNBXg",
98+
"@lADPDe7s2ySRnIvNA6fNBXg"
99+
],
100+
"id": "imageList_1686283179480"
101+
}
102+
]
103+
}
104+
105+
106+
def generate_multi_text_image_card_data(title: str, logo: str, texts: [str], images: [str]) -> dict:
107+
card_data = copy.deepcopy(INTERACTIVE_CARD_JSON_SAMPLE_MULTI_TEXT_IMAGE)
108+
109+
if title != "":
110+
card_data["header"]["title"]["text"] = title
111+
112+
if logo != "":
113+
card_data["header"]["logo"] = logo
114+
115+
card_data["contents"] = []
116+
for text in texts:
117+
text_line = {
118+
"type": "text",
119+
"text": text,
120+
"id": "text_" + str(uuid.uuid1())
121+
}
122+
divider_line = {
123+
"type": "divider",
124+
"id": "divider_" + str(uuid.uuid1())
125+
}
126+
card_data["contents"].append(text_line)
127+
card_data["contents"].append(divider_line)
128+
129+
image_list = {
130+
"type": "imageList",
131+
"images": images,
132+
"id": "imageList_" + str(uuid.uuid1())
133+
}
134+
card_data["contents"].append(image_list)
135+
136+
return card_data
137+
138+
11139
'''
12140
极简卡片组合:title-text-image-button
13141
'''

‎examples/cardbot/cardbot.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from dingtalk_stream import AckMessage, interactive_card
1212
import dingtalk_stream
1313
import time
14-
import copy
14+
import copy, asyncio
1515

1616

1717
def setup_logger():
@@ -38,45 +38,66 @@ def define_options():
3838
return options
3939

4040

41-
class CardBotHandler(dingtalk_stream.ChatbotHandler):
41+
class CardBotHandler(dingtalk_stream.AsyncChatbotHandler):
4242
"""
4343
接收回调消息。
4444
回复一个卡片,然后更新卡片的文本和图片。
4545
"""
4646

47-
def __init__(self, logger: logging.Logger = None):
48-
super(dingtalk_stream.ChatbotHandler, self).__init__()
47+
def __init__(self, logger: logging.Logger = None, max_workers: int = 8):
48+
super(CardBotHandler, self).__init__(max_workers=max_workers)
4949
if logger:
5050
self.logger = logger
5151

52-
async def process(self, callback: dingtalk_stream.CallbackMessage):
52+
def process(self, callback: dingtalk_stream.CallbackMessage):
53+
'''
54+
多线程场景,process函数不要用 async 修饰
55+
:param message:
56+
:return:
57+
'''
58+
5359
incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data)
5460

55-
card_data = copy.deepcopy(interactive_card.INTERACTIVE_CARD_JSON_SAMPLE_1)
61+
texts = [
62+
"第一行文本"
63+
]
5664

57-
# 先回复一个卡片
58-
self.reply_card(card_data,
59-
incoming_message, False)
65+
# 先回复一个文本卡片
66+
self.reply_card(
67+
interactive_card.generate_multi_text_line_card_data(title="机器人名字", logo="@lALPDfJ6V_FPDmvNAfTNAfQ",
68+
texts=texts),
69+
incoming_message, False)
6070

61-
time.sleep(1)
71+
images = [
72+
"@lADPDe7s2ySi18PNA6XNBXg",
73+
"@lADPDf0i1beuNF3NAxTNBXg",
74+
"@lADPDe7s2ySRnIvNA6fNBXg"
75+
]
6276

63-
card_data = copy.deepcopy(interactive_card.INTERACTIVE_CARD_JSON_SAMPLE_1)
77+
# 再回复一个文本+图片卡片
78+
card_biz_id = self.reply_card(
79+
interactive_card.generate_multi_text_image_card_data(title="机器人名字", logo="@lALPDfJ6V_FPDmvNAfTNAfQ",
80+
texts=texts, images=images),
81+
incoming_message, False)
6482

65-
# 更新文本
66-
card_data["contents"][0]["text"] = "钉钉,让进步发生!\n 更新时间:{tt}".format(
67-
tt=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
83+
# 再试试更新卡片
84+
time.sleep(3)
6885

6986
# 上传图片
7087
media_id = self.dingtalk_client.upload_to_dingtalk(open('./img.png', 'rb'),
7188
filetype='image',
7289
filename='image.png',
7390
mimetype='image/png')
74-
# 更新图片
75-
card_data["contents"][1]["image"] = media_id
7691

77-
# 更新卡片
78-
self.update_card(card_data,
79-
incoming_message)
92+
texts = [
93+
"更新后的第一行文本",
94+
"更新后的第二行文本"
95+
]
96+
97+
self.update_card(card_biz_id, interactive_card.generate_multi_text_image_card_data(title="机器人名字",
98+
logo="@lALPDfJ6V_FPDmvNAfTNAfQ",
99+
texts=texts,
100+
images=[media_id]))
80101

81102
return AckMessage.STATUS_OK, 'OK'
82103

0 commit comments

Comments
 (0)
Please sign in to comment.