1use crate::card::instances::{
4 AIMarkdownCardInstance, CarouselCardInstance, MarkdownButtonCardInstance, MarkdownCardInstance,
5 RPAPluginCardInstance,
6};
7use crate::card::replier::{AICardReplier, CardReplier};
8use crate::card::templates::generate_multi_text_line_card_data;
9use crate::handlers::callback::CallbackHandler;
10use crate::messages::chatbot::ChatbotMessage;
11use crate::messages::frames::{AckMessage, Headers, MessageBody};
12use crate::transport::http::HttpClient;
13use crate::transport::token::TokenManager;
14use async_trait::async_trait;
15use std::sync::Arc;
16
17#[async_trait]
19pub trait ChatbotHandler: CallbackHandler {}
20
21pub trait AsyncChatbotHandler: Send + Sync + 'static {
23 fn process(&self, callback_message: &MessageBody);
25 fn pre_start(&self) {}
27}
28
29pub(crate) async fn async_raw_process(
31 handler: Arc<dyn AsyncChatbotHandler>,
32 callback_message: MessageBody,
33) -> AckMessage {
34 let message_id = callback_message.headers.message_id.clone();
35 let data = callback_message.data.clone();
36
37 tokio::spawn(async move {
38 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
39 handler.process(&callback_message);
40 }));
41 if let Err(e) = result {
42 tracing::error!("AsyncChatbotHandler.process panicked: {:?}", e);
43 }
44 });
45
46 AckMessage {
47 code: AckMessage::STATUS_OK,
48 headers: Headers {
49 message_id,
50 content_type: Some(Headers::CONTENT_TYPE_APPLICATION_JSON.to_owned()),
51 ..Default::default()
52 },
53 message: "ok".to_owned(),
54 data,
55 }
56}
57
58#[derive(Clone)]
60pub struct ChatbotReplier {
61 http_client: HttpClient,
62 token_manager: Arc<TokenManager>,
63 client_id: String,
64}
65
66impl ChatbotReplier {
67 pub fn new(
69 http_client: HttpClient,
70 token_manager: Arc<TokenManager>,
71 client_id: String,
72 ) -> Self {
73 Self {
74 http_client,
75 token_manager,
76 client_id,
77 }
78 }
79
80 pub async fn reply_text(
82 &self,
83 text: &str,
84 incoming_message: &ChatbotMessage,
85 ) -> crate::Result<serde_json::Value> {
86 let webhook = incoming_message
87 .session_webhook
88 .as_deref()
89 .ok_or_else(|| crate::Error::Handler("session_webhook is empty".to_owned()))?;
90 let body = serde_json::json!({
91 "msgtype": "text",
92 "text": {"content": text},
93 "at": {"atUserIds": [incoming_message.sender_staff_id.as_deref().unwrap_or("")]}
94 });
95 self.http_client.post_json(webhook, &body, None).await
96 }
97
98 pub async fn reply_markdown(
100 &self,
101 title: &str,
102 text: &str,
103 incoming_message: &ChatbotMessage,
104 ) -> crate::Result<serde_json::Value> {
105 let webhook = incoming_message
106 .session_webhook
107 .as_deref()
108 .ok_or_else(|| crate::Error::Handler("session_webhook is empty".to_owned()))?;
109 let body = serde_json::json!({
110 "msgtype": "markdown",
111 "markdown": {"title": title, "text": text},
112 "at": {"atUserIds": [incoming_message.sender_staff_id.as_deref().unwrap_or("")]}
113 });
114 self.http_client.post_json(webhook, &body, None).await
115 }
116
117 pub async fn reply_card(
119 &self,
120 card_data: &serde_json::Value,
121 incoming_message: &ChatbotMessage,
122 at_sender: bool,
123 at_all: bool,
124 ) -> crate::Result<String> {
125 let access_token = self.token_manager.get_access_token().await?;
126 let card_biz_id = CardReplier::gen_card_id(incoming_message);
127 let mut body = serde_json::json!({
128 "cardTemplateId": "StandardCard",
129 "robotCode": self.client_id,
130 "cardData": serde_json::to_string(card_data).unwrap_or_default(),
131 "sendOptions": {},
132 "cardBizId": card_biz_id,
133 });
134 let Some(body_obj) = body.as_object_mut() else {
135 return Ok(card_biz_id);
136 };
137 if incoming_message.conversation_type.as_deref() == Some("2") {
138 body_obj.insert(
139 "openConversationId".to_owned(),
140 serde_json::json!(incoming_message.conversation_id),
141 );
142 } else if incoming_message.conversation_type.as_deref() == Some("1") {
143 let receiver = serde_json::json!({"userId": incoming_message.sender_staff_id});
144 body_obj.insert(
145 "singleChatReceiver".to_owned(),
146 serde_json::Value::String(serde_json::to_string(&receiver).unwrap_or_default()),
147 );
148 }
149 if let Some(send_options) = body_obj
150 .get_mut("sendOptions")
151 .and_then(|v| v.as_object_mut())
152 {
153 send_options.insert("atAll".to_owned(), serde_json::json!(at_all));
154 if at_sender {
155 let user_list = serde_json::json!([{"nickName": incoming_message.sender_nick, "userId": incoming_message.sender_staff_id}]);
156 send_options.insert(
157 "atUserListJson".to_owned(),
158 serde_json::Value::String(
159 serde_json::to_string(&user_list).unwrap_or_default(),
160 ),
161 );
162 }
163 }
164 let url = format!(
165 "{}/v1.0/im/v1.0/robot/interactiveCards/send",
166 self.http_client.openapi_endpoint()
167 );
168 self.http_client
169 .post_json(&url, &body, Some(&access_token))
170 .await?;
171 Ok(card_biz_id)
172 }
173
174 pub async fn update_card(
176 &self,
177 card_biz_id: &str,
178 card_data: &serde_json::Value,
179 ) -> crate::Result<serde_json::Value> {
180 let access_token = self.token_manager.get_access_token().await?;
181 let body = serde_json::json!({"cardBizId": card_biz_id, "cardData": serde_json::to_string(card_data).unwrap_or_default()});
182 let url = format!(
183 "{}/v1.0/im/robots/interactiveCards",
184 self.http_client.openapi_endpoint()
185 );
186 self.http_client
187 .put_json(&url, &body, Some(&access_token))
188 .await
189 }
190
191 fn make_card_replier(&self, incoming_message: &ChatbotMessage) -> CardReplier {
192 CardReplier::new(
193 self.http_client.clone(),
194 Arc::clone(&self.token_manager),
195 self.client_id.clone(),
196 incoming_message.clone(),
197 )
198 }
199
200 fn make_ai_card_replier(&self, incoming_message: &ChatbotMessage) -> AICardReplier {
201 AICardReplier::new(self.make_card_replier(incoming_message))
202 }
203
204 pub fn create_ai_card_replier(&self, incoming_message: &ChatbotMessage) -> AICardReplier {
206 self.make_ai_card_replier(incoming_message)
207 }
208
209 pub async fn reply_markdown_card(
211 &self,
212 markdown: &str,
213 incoming_message: &ChatbotMessage,
214 title: &str,
215 logo: &str,
216 at_sender: bool,
217 at_all: bool,
218 ) -> crate::Result<MarkdownCardInstance> {
219 let mut instance = MarkdownCardInstance::new(self.make_card_replier(incoming_message));
220 instance.set_title_and_logo(title, logo);
221 instance
222 .reply(markdown, at_sender, at_all, None, true)
223 .await?;
224 Ok(instance)
225 }
226
227 #[allow(clippy::too_many_arguments)]
229 pub async fn reply_rpa_plugin_card(
230 &self,
231 incoming_message: &ChatbotMessage,
232 plugin_id: &str,
233 plugin_version: &str,
234 plugin_name: &str,
235 ability_name: &str,
236 plugin_args: &serde_json::Value,
237 goal: &str,
238 corp_id: &str,
239 recipients: Option<&[String]>,
240 ) -> crate::Result<RPAPluginCardInstance> {
241 let mut instance = RPAPluginCardInstance::new(self.make_ai_card_replier(incoming_message));
242 instance.set_goal(goal);
243 instance.set_corp_id(corp_id);
244 instance
245 .reply(
246 plugin_id,
247 plugin_version,
248 plugin_name,
249 ability_name,
250 plugin_args,
251 recipients,
252 true,
253 )
254 .await?;
255 Ok(instance)
256 }
257
258 pub async fn reply_markdown_button(
260 &self,
261 incoming_message: &ChatbotMessage,
262 markdown: &str,
263 button_list: Vec<serde_json::Value>,
264 tips: &str,
265 title: &str,
266 logo: &str,
267 ) -> crate::Result<MarkdownButtonCardInstance> {
268 let mut instance =
269 MarkdownButtonCardInstance::new(self.make_card_replier(incoming_message));
270 instance.set_title_and_logo(title, logo);
271 instance
272 .reply(markdown, button_list, tips, None, true)
273 .await?;
274 Ok(instance)
275 }
276
277 #[allow(clippy::too_many_arguments)]
279 pub async fn reply_ai_markdown_button(
280 &self,
281 incoming_message: &ChatbotMessage,
282 markdown: &str,
283 button_list: Vec<serde_json::Value>,
284 tips: &str,
285 title: &str,
286 logo: &str,
287 recipients: Option<&[String]>,
288 support_forward: bool,
289 ) -> crate::Result<AIMarkdownCardInstance> {
290 let mut instance = AIMarkdownCardInstance::new(self.make_ai_card_replier(incoming_message));
291 instance.set_title_and_logo(title, logo);
292 instance.ai_start(recipients, support_forward).await?;
293 instance.ai_streaming(markdown, true).await?;
294 instance
295 .ai_finish(Some(markdown), Some(button_list), tips)
296 .await?;
297 Ok(instance)
298 }
299
300 pub async fn reply_carousel_card(
302 &self,
303 incoming_message: &ChatbotMessage,
304 markdown: &str,
305 image_slider: &[(String, String)],
306 button_text: &str,
307 title: &str,
308 logo: &str,
309 ) -> crate::Result<CarouselCardInstance> {
310 let mut instance = CarouselCardInstance::new(self.make_ai_card_replier(incoming_message));
311 instance.set_title_and_logo(title, logo);
312 instance
313 .reply(markdown, image_slider, button_text, None, true)
314 .await?;
315 Ok(instance)
316 }
317
318 pub async fn ai_markdown_card_start(
320 &self,
321 incoming_message: &ChatbotMessage,
322 title: &str,
323 logo: &str,
324 recipients: Option<&[String]>,
325 ) -> crate::Result<AIMarkdownCardInstance> {
326 let mut instance = AIMarkdownCardInstance::new(self.make_ai_card_replier(incoming_message));
327 instance.set_title_and_logo(title, logo);
328 instance.ai_start(recipients, true).await?;
329 Ok(instance)
330 }
331
332 pub fn extract_text(incoming_message: &ChatbotMessage) -> Option<Vec<String>> {
334 incoming_message.get_text_list()
335 }
336
337 pub async fn extract_and_reupload_images(
339 &self,
340 incoming_message: &ChatbotMessage,
341 ) -> crate::Result<Vec<String>> {
342 let image_list = match incoming_message.get_image_list() {
343 Some(list) if !list.is_empty() => list,
344 _ => return Ok(Vec::new()),
345 };
346 let mut media_ids = Vec::new();
347 for download_code in &image_list {
348 let download_url = self.get_image_download_url(download_code).await?;
349 let image_bytes = self.http_client.get_bytes(&download_url).await?;
350 let media_id = self
351 .upload_to_dingtalk(&image_bytes, "image", "image.png", "image/png")
352 .await?;
353 media_ids.push(media_id);
354 }
355 Ok(media_ids)
356 }
357
358 pub async fn get_image_download_url(&self, download_code: &str) -> crate::Result<String> {
360 let access_token = self.token_manager.get_access_token().await?;
361 let body = serde_json::json!({"robotCode": self.client_id, "downloadCode": download_code});
362 let url = format!(
363 "{}/v1.0/robot/messageFiles/download",
364 self.http_client.openapi_endpoint()
365 );
366 let resp: serde_json::Value = self
367 .http_client
368 .post_json(&url, &body, Some(&access_token))
369 .await?;
370 resp.get("downloadUrl")
371 .and_then(|v| v.as_str())
372 .map(String::from)
373 .ok_or_else(|| crate::Error::Handler("downloadUrl not found".to_owned()))
374 }
375
376 pub async fn upload_to_dingtalk(
378 &self,
379 content: &[u8],
380 filetype: &str,
381 filename: &str,
382 mimetype: &str,
383 ) -> crate::Result<String> {
384 let access_token = self.token_manager.get_access_token().await?;
385 self.http_client
386 .upload_file(&access_token, content, filetype, filename, mimetype)
387 .await
388 }
389
390 pub async fn send_oto_message(
404 &self,
405 user_id: &str,
406 msg_key: &str,
407 msg_param: &str,
408 ) -> crate::Result<serde_json::Value> {
409 let access_token = self.token_manager.get_access_token().await?;
410 let body = serde_json::json!({
411 "robotCode": self.client_id,
412 "userIds": [user_id],
413 "msgKey": msg_key,
414 "msgParam": msg_param,
415 });
416 let url = format!(
417 "{}/v1.0/robot/oToMessages/batchSend",
418 self.http_client.openapi_endpoint()
419 );
420 self.http_client
421 .post_json(&url, &body, Some(&access_token))
422 .await
423 }
424
425 pub async fn download_bytes(&self, url: &str) -> crate::Result<Vec<u8>> {
432 self.http_client.get_bytes(url).await
433 }
434
435 pub async fn download_bytes_with_limit(
440 &self,
441 url: &str,
442 max_size: u64,
443 ) -> crate::Result<Vec<u8>> {
444 self.http_client.get_bytes_with_limit(url, max_size).await
445 }
446
447 pub async fn set_off_duty_prompt(
449 &self,
450 text: &str,
451 title: &str,
452 logo: &str,
453 ) -> crate::Result<serde_json::Value> {
454 let access_token = self.token_manager.get_access_token().await?;
455 let title = if title.is_empty() {
456 "钉钉Stream机器人"
457 } else {
458 title
459 };
460 let logo = if logo.is_empty() {
461 "@lALPDfJ6V_FPDmvNAfTNAfQ"
462 } else {
463 logo
464 };
465 let prompt_card_data = generate_multi_text_line_card_data(title, logo, &[text]);
466 let body = serde_json::json!({
467 "robotCode": self.client_id,
468 "cardData": serde_json::to_string(&prompt_card_data).unwrap_or_default(),
469 "cardTemplateId": "StandardCard",
470 });
471 let url = format!(
472 "{}/v1.0/innerApi/robot/stream/away/template/update",
473 self.http_client.openapi_endpoint()
474 );
475 self.http_client
476 .post_json(&url, &body, Some(&access_token))
477 .await
478 }
479}