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
58pub struct ChatbotReplier {
60 http_client: HttpClient,
61 token_manager: Arc<TokenManager>,
62 client_id: String,
63}
64
65impl ChatbotReplier {
66 pub fn new(
68 http_client: HttpClient,
69 token_manager: Arc<TokenManager>,
70 client_id: String,
71 ) -> Self {
72 Self {
73 http_client,
74 token_manager,
75 client_id,
76 }
77 }
78
79 pub async fn reply_text(
81 &self,
82 text: &str,
83 incoming_message: &ChatbotMessage,
84 ) -> crate::Result<serde_json::Value> {
85 let webhook = incoming_message
86 .session_webhook
87 .as_deref()
88 .ok_or_else(|| crate::Error::Handler("session_webhook is empty".to_owned()))?;
89 let body = serde_json::json!({
90 "msgtype": "text",
91 "text": {"content": text},
92 "at": {"atUserIds": [incoming_message.sender_staff_id.as_deref().unwrap_or("")]}
93 });
94 self.http_client.post_json(webhook, &body, None).await
95 }
96
97 pub async fn reply_markdown(
99 &self,
100 title: &str,
101 text: &str,
102 incoming_message: &ChatbotMessage,
103 ) -> crate::Result<serde_json::Value> {
104 let webhook = incoming_message
105 .session_webhook
106 .as_deref()
107 .ok_or_else(|| crate::Error::Handler("session_webhook is empty".to_owned()))?;
108 let body = serde_json::json!({
109 "msgtype": "markdown",
110 "markdown": {"title": title, "text": text},
111 "at": {"atUserIds": [incoming_message.sender_staff_id.as_deref().unwrap_or("")]}
112 });
113 self.http_client.post_json(webhook, &body, None).await
114 }
115
116 pub async fn reply_card(
118 &self,
119 card_data: &serde_json::Value,
120 incoming_message: &ChatbotMessage,
121 at_sender: bool,
122 at_all: bool,
123 ) -> crate::Result<String> {
124 let access_token = self.token_manager.get_access_token().await?;
125 let card_biz_id = CardReplier::gen_card_id(incoming_message);
126 let mut body = serde_json::json!({
127 "cardTemplateId": "StandardCard",
128 "robotCode": self.client_id,
129 "cardData": serde_json::to_string(card_data).unwrap_or_default(),
130 "sendOptions": {},
131 "cardBizId": card_biz_id,
132 });
133 let Some(body_obj) = body.as_object_mut() else {
134 return Ok(card_biz_id);
135 };
136 if incoming_message.conversation_type.as_deref() == Some("2") {
137 body_obj.insert(
138 "openConversationId".to_owned(),
139 serde_json::json!(incoming_message.conversation_id),
140 );
141 } else if incoming_message.conversation_type.as_deref() == Some("1") {
142 let receiver = serde_json::json!({"userId": incoming_message.sender_staff_id});
143 body_obj.insert(
144 "singleChatReceiver".to_owned(),
145 serde_json::Value::String(serde_json::to_string(&receiver).unwrap_or_default()),
146 );
147 }
148 if let Some(send_options) = body_obj
149 .get_mut("sendOptions")
150 .and_then(|v| v.as_object_mut())
151 {
152 send_options.insert("atAll".to_owned(), serde_json::json!(at_all));
153 if at_sender {
154 let user_list = serde_json::json!([{"nickName": incoming_message.sender_nick, "userId": incoming_message.sender_staff_id}]);
155 send_options.insert(
156 "atUserListJson".to_owned(),
157 serde_json::Value::String(
158 serde_json::to_string(&user_list).unwrap_or_default(),
159 ),
160 );
161 }
162 }
163 let url = format!(
164 "{}/v1.0/im/v1.0/robot/interactiveCards/send",
165 self.http_client.openapi_endpoint()
166 );
167 self.http_client
168 .post_json(&url, &body, Some(&access_token))
169 .await?;
170 Ok(card_biz_id)
171 }
172
173 pub async fn update_card(
175 &self,
176 card_biz_id: &str,
177 card_data: &serde_json::Value,
178 ) -> crate::Result<serde_json::Value> {
179 let access_token = self.token_manager.get_access_token().await?;
180 let body = serde_json::json!({"cardBizId": card_biz_id, "cardData": serde_json::to_string(card_data).unwrap_or_default()});
181 let url = format!(
182 "{}/v1.0/im/robots/interactiveCards",
183 self.http_client.openapi_endpoint()
184 );
185 self.http_client
186 .put_json(&url, &body, Some(&access_token))
187 .await
188 }
189
190 fn make_card_replier(&self, incoming_message: &ChatbotMessage) -> CardReplier {
191 CardReplier::new(
192 self.http_client.clone(),
193 Arc::clone(&self.token_manager),
194 self.client_id.clone(),
195 incoming_message.clone(),
196 )
197 }
198
199 fn make_ai_card_replier(&self, incoming_message: &ChatbotMessage) -> AICardReplier {
200 AICardReplier::new(self.make_card_replier(incoming_message))
201 }
202
203 pub fn create_ai_card_replier(&self, incoming_message: &ChatbotMessage) -> AICardReplier {
205 self.make_ai_card_replier(incoming_message)
206 }
207
208 pub async fn reply_markdown_card(
210 &self,
211 markdown: &str,
212 incoming_message: &ChatbotMessage,
213 title: &str,
214 logo: &str,
215 at_sender: bool,
216 at_all: bool,
217 ) -> crate::Result<MarkdownCardInstance> {
218 let mut instance = MarkdownCardInstance::new(self.make_card_replier(incoming_message));
219 instance.set_title_and_logo(title, logo);
220 instance
221 .reply(markdown, at_sender, at_all, None, true)
222 .await?;
223 Ok(instance)
224 }
225
226 #[allow(clippy::too_many_arguments)]
228 pub async fn reply_rpa_plugin_card(
229 &self,
230 incoming_message: &ChatbotMessage,
231 plugin_id: &str,
232 plugin_version: &str,
233 plugin_name: &str,
234 ability_name: &str,
235 plugin_args: &serde_json::Value,
236 goal: &str,
237 corp_id: &str,
238 recipients: Option<&[String]>,
239 ) -> crate::Result<RPAPluginCardInstance> {
240 let mut instance = RPAPluginCardInstance::new(self.make_ai_card_replier(incoming_message));
241 instance.set_goal(goal);
242 instance.set_corp_id(corp_id);
243 instance
244 .reply(
245 plugin_id,
246 plugin_version,
247 plugin_name,
248 ability_name,
249 plugin_args,
250 recipients,
251 true,
252 )
253 .await?;
254 Ok(instance)
255 }
256
257 pub async fn reply_markdown_button(
259 &self,
260 incoming_message: &ChatbotMessage,
261 markdown: &str,
262 button_list: Vec<serde_json::Value>,
263 tips: &str,
264 title: &str,
265 logo: &str,
266 ) -> crate::Result<MarkdownButtonCardInstance> {
267 let mut instance =
268 MarkdownButtonCardInstance::new(self.make_card_replier(incoming_message));
269 instance.set_title_and_logo(title, logo);
270 instance
271 .reply(markdown, button_list, tips, None, true)
272 .await?;
273 Ok(instance)
274 }
275
276 #[allow(clippy::too_many_arguments)]
278 pub async fn reply_ai_markdown_button(
279 &self,
280 incoming_message: &ChatbotMessage,
281 markdown: &str,
282 button_list: Vec<serde_json::Value>,
283 tips: &str,
284 title: &str,
285 logo: &str,
286 recipients: Option<&[String]>,
287 support_forward: bool,
288 ) -> crate::Result<AIMarkdownCardInstance> {
289 let mut instance = AIMarkdownCardInstance::new(self.make_ai_card_replier(incoming_message));
290 instance.set_title_and_logo(title, logo);
291 instance.ai_start(recipients, support_forward).await?;
292 instance.ai_streaming(markdown, true).await?;
293 instance
294 .ai_finish(Some(markdown), Some(button_list), tips)
295 .await?;
296 Ok(instance)
297 }
298
299 pub async fn reply_carousel_card(
301 &self,
302 incoming_message: &ChatbotMessage,
303 markdown: &str,
304 image_slider: &[(String, String)],
305 button_text: &str,
306 title: &str,
307 logo: &str,
308 ) -> crate::Result<CarouselCardInstance> {
309 let mut instance = CarouselCardInstance::new(self.make_ai_card_replier(incoming_message));
310 instance.set_title_and_logo(title, logo);
311 instance
312 .reply(markdown, image_slider, button_text, None, true)
313 .await?;
314 Ok(instance)
315 }
316
317 pub async fn ai_markdown_card_start(
319 &self,
320 incoming_message: &ChatbotMessage,
321 title: &str,
322 logo: &str,
323 recipients: Option<&[String]>,
324 ) -> crate::Result<AIMarkdownCardInstance> {
325 let mut instance = AIMarkdownCardInstance::new(self.make_ai_card_replier(incoming_message));
326 instance.set_title_and_logo(title, logo);
327 instance.ai_start(recipients, true).await?;
328 Ok(instance)
329 }
330
331 pub fn extract_text(incoming_message: &ChatbotMessage) -> Option<Vec<String>> {
333 incoming_message.get_text_list()
334 }
335
336 pub async fn extract_and_reupload_images(
338 &self,
339 incoming_message: &ChatbotMessage,
340 ) -> crate::Result<Vec<String>> {
341 let image_list = match incoming_message.get_image_list() {
342 Some(list) if !list.is_empty() => list,
343 _ => return Ok(Vec::new()),
344 };
345 let mut media_ids = Vec::new();
346 for download_code in &image_list {
347 let download_url = self.get_image_download_url(download_code).await?;
348 let image_bytes = self.http_client.get_bytes(&download_url).await?;
349 let media_id = self
350 .upload_to_dingtalk(&image_bytes, "image", "image.png", "image/png")
351 .await?;
352 media_ids.push(media_id);
353 }
354 Ok(media_ids)
355 }
356
357 pub async fn get_image_download_url(&self, download_code: &str) -> crate::Result<String> {
359 let access_token = self.token_manager.get_access_token().await?;
360 let body = serde_json::json!({"robotCode": self.client_id, "downloadCode": download_code});
361 let url = format!(
362 "{}/v1.0/robot/messageFiles/download",
363 self.http_client.openapi_endpoint()
364 );
365 let resp: serde_json::Value = self
366 .http_client
367 .post_json(&url, &body, Some(&access_token))
368 .await?;
369 resp.get("downloadUrl")
370 .and_then(|v| v.as_str())
371 .map(String::from)
372 .ok_or_else(|| crate::Error::Handler("downloadUrl not found".to_owned()))
373 }
374
375 pub async fn upload_to_dingtalk(
377 &self,
378 content: &[u8],
379 filetype: &str,
380 filename: &str,
381 mimetype: &str,
382 ) -> crate::Result<String> {
383 let access_token = self.token_manager.get_access_token().await?;
384 self.http_client
385 .upload_file(&access_token, content, filetype, filename, mimetype)
386 .await
387 }
388
389 pub async fn set_off_duty_prompt(
391 &self,
392 text: &str,
393 title: &str,
394 logo: &str,
395 ) -> crate::Result<serde_json::Value> {
396 let access_token = self.token_manager.get_access_token().await?;
397 let title = if title.is_empty() {
398 "钉钉Stream机器人"
399 } else {
400 title
401 };
402 let logo = if logo.is_empty() {
403 "@lALPDfJ6V_FPDmvNAfTNAfQ"
404 } else {
405 logo
406 };
407 let prompt_card_data = generate_multi_text_line_card_data(title, logo, &[text]);
408 let body = serde_json::json!({
409 "robotCode": self.client_id,
410 "cardData": serde_json::to_string(&prompt_card_data).unwrap_or_default(),
411 "cardTemplateId": "StandardCard",
412 });
413 let url = format!(
414 "{}/v1.0/innerApi/robot/stream/away/template/update",
415 self.http_client.openapi_endpoint()
416 );
417 self.http_client
418 .post_json(&url, &body, Some(&access_token))
419 .await
420 }
421}