Skip to main content

dingtalk_stream/card/
replier.rs

1//! 卡片回复器,对齐 Python card_replier.py
2
3use crate::messages::chatbot::ChatbotMessage;
4use crate::transport::http::HttpClient;
5use crate::transport::token::TokenManager;
6use serde_repr::{Deserialize_repr, Serialize_repr};
7use sha2::{Digest, Sha256};
8use std::sync::Arc;
9
10/// AI 卡片状态枚举
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
12#[repr(u8)]
13#[non_exhaustive]
14pub enum AICardStatus {
15    /// 处理中
16    Processing = 1,
17    /// 输入中
18    Inputing = 2,
19    /// 完成
20    Finished = 3,
21    /// 执行中
22    Executing = 4,
23    /// 失败
24    Failed = 5,
25}
26
27/// 卡片回复器(使用 Arc 共享所有权,避免生命周期问题)
28#[derive(Clone)]
29pub struct CardReplier {
30    pub(crate) http_client: HttpClient,
31    pub(crate) token_manager: Arc<TokenManager>,
32    pub(crate) client_id: String,
33    pub(crate) incoming_message: ChatbotMessage,
34}
35
36impl CardReplier {
37    /// 创建新的卡片回复器
38    pub fn new(
39        http_client: HttpClient,
40        token_manager: Arc<TokenManager>,
41        client_id: String,
42        incoming_message: ChatbotMessage,
43    ) -> Self {
44        Self {
45            http_client,
46            token_manager,
47            client_id,
48            incoming_message,
49        }
50    }
51
52    /// 生成卡片 ID (SHA256)
53    pub fn gen_card_id(msg: &ChatbotMessage) -> String {
54        let factor = format!(
55            "{}_{}_{}_{}_{}",
56            msg.sender_id.as_deref().unwrap_or(""),
57            msg.sender_corp_id.as_deref().unwrap_or(""),
58            msg.conversation_id.as_deref().unwrap_or(""),
59            msg.message_id.as_deref().unwrap_or(""),
60            uuid::Uuid::new_v4()
61        );
62        let mut hasher = Sha256::new();
63        hasher.update(factor.as_bytes());
64        format!("{:x}", hasher.finalize())
65    }
66
67    /// 将 `cardParamMap` 中的所有值转为字符串(钉钉 API 要求)
68    fn stringify_card_param_map(card_data: &serde_json::Value) -> serde_json::Value {
69        match card_data {
70            serde_json::Value::Object(map) => {
71                let mut out = serde_json::Map::new();
72                for (k, v) in map {
73                    out.insert(
74                        k.clone(),
75                        match v {
76                            serde_json::Value::String(_) => v.clone(),
77                            _ => serde_json::Value::String(v.to_string()),
78                        },
79                    );
80                }
81                serde_json::Value::Object(out)
82            }
83            other => other.clone(),
84        }
85    }
86
87    /// 创建并发送卡片(两步:创建 + 投放)
88    #[allow(clippy::too_many_arguments)]
89    pub async fn create_and_send_card(
90        &self,
91        card_template_id: &str,
92        card_data: &serde_json::Value,
93        callback_type: &str,
94        callback_route_key: &str,
95        at_sender: bool,
96        at_all: bool,
97        recipients: Option<&[String]>,
98        support_forward: bool,
99    ) -> crate::Result<String> {
100        let access_token = self.token_manager.get_access_token().await?;
101        let card_instance_id = Self::gen_card_id(&self.incoming_message);
102        let param_map = Self::stringify_card_param_map(card_data);
103
104        let mut create_body = serde_json::json!({
105            "cardTemplateId": card_template_id,
106            "outTrackId": card_instance_id,
107            "cardData": {"cardParamMap": param_map},
108            "callbackType": callback_type,
109            "imGroupOpenSpaceModel": {"supportForward": support_forward},
110            "imRobotOpenSpaceModel": {"supportForward": support_forward},
111        });
112
113        if callback_type == "HTTP" {
114            create_body["callbackRouteKey"] = serde_json::json!(callback_route_key);
115        }
116
117        let url = format!(
118            "{}/v1.0/card/instances",
119            self.http_client.openapi_endpoint()
120        );
121        self.http_client
122            .post_json(&url, &create_body, Some(&access_token))
123            .await?;
124
125        let mut deliver_body = serde_json::json!({
126            "outTrackId": card_instance_id,
127            "userIdType": 1,
128        });
129
130        self.build_deliver_body(&mut deliver_body, at_sender, at_all, recipients);
131
132        let url = format!(
133            "{}/v1.0/card/instances/deliver",
134            self.http_client.openapi_endpoint()
135        );
136        self.http_client
137            .post_json(&url, &deliver_body, Some(&access_token))
138            .await?;
139
140        Ok(card_instance_id)
141    }
142
143    /// 创建并投放卡片(一步到位)
144    #[allow(clippy::too_many_arguments)]
145    pub async fn create_and_deliver_card(
146        &self,
147        card_template_id: &str,
148        card_data: &serde_json::Value,
149        callback_type: &str,
150        callback_route_key: &str,
151        at_sender: bool,
152        at_all: bool,
153        recipients: Option<&[String]>,
154        support_forward: bool,
155        extra: Option<&serde_json::Value>,
156    ) -> crate::Result<String> {
157        let access_token = self.token_manager.get_access_token().await?;
158        let card_instance_id = Self::gen_card_id(&self.incoming_message);
159        let param_map = Self::stringify_card_param_map(card_data);
160
161        let mut body = serde_json::json!({
162            "cardTemplateId": card_template_id,
163            "outTrackId": card_instance_id,
164            "cardData": {"cardParamMap": param_map},
165            "callbackType": callback_type,
166            "imGroupOpenSpaceModel": {"supportForward": support_forward},
167            "imRobotOpenSpaceModel": {"supportForward": support_forward},
168        });
169
170        if callback_type == "HTTP" {
171            body["callbackRouteKey"] = serde_json::json!(callback_route_key);
172        }
173
174        self.build_deliver_body(&mut body, at_sender, at_all, recipients);
175
176        if let Some(extra) = extra {
177            if let (Some(body_obj), Some(extra_obj)) = (body.as_object_mut(), extra.as_object()) {
178                for (k, v) in extra_obj {
179                    body_obj.insert(k.clone(), v.clone());
180                }
181            }
182        }
183
184        let url = format!(
185            "{}/v1.0/card/instances/createAndDeliver",
186            self.http_client.openapi_endpoint()
187        );
188        self.http_client
189            .post_json(&url, &body, Some(&access_token))
190            .await?;
191
192        Ok(card_instance_id)
193    }
194
195    /// 更新卡片数据
196    pub async fn put_card_data(
197        &self,
198        card_instance_id: &str,
199        card_data: &serde_json::Value,
200        extra: Option<&serde_json::Value>,
201    ) -> crate::Result<()> {
202        let access_token = self.token_manager.get_access_token().await?;
203        let param_map = Self::stringify_card_param_map(card_data);
204
205        let mut body = serde_json::json!({
206            "outTrackId": card_instance_id,
207            "cardData": {"cardParamMap": param_map},
208        });
209
210        if let Some(extra) = extra {
211            if let (Some(body_obj), Some(extra_obj)) = (body.as_object_mut(), extra.as_object()) {
212                for (k, v) in extra_obj {
213                    body_obj.insert(k.clone(), v.clone());
214                }
215            }
216        }
217
218        let url = format!(
219            "{}/v1.0/card/instances",
220            self.http_client.openapi_endpoint()
221        );
222        self.http_client
223            .put_json(&url, &body, Some(&access_token))
224            .await?;
225
226        Ok(())
227    }
228
229    /// 构建投放请求体中的会话相关字段
230    fn build_deliver_body(
231        &self,
232        body: &mut serde_json::Value,
233        at_sender: bool,
234        at_all: bool,
235        recipients: Option<&[String]>,
236    ) {
237        let msg = &self.incoming_message;
238        let Some(body_obj) = body.as_object_mut() else {
239            return;
240        };
241
242        if msg.conversation_type.as_deref() == Some("2") {
243            body_obj.insert(
244                "openSpaceId".to_owned(),
245                serde_json::json!(format!(
246                    "dtv1.card//IM_GROUP.{}",
247                    msg.conversation_id.as_deref().unwrap_or("")
248                )),
249            );
250
251            let mut group_model = serde_json::json!({
252                "robotCode": self.client_id,
253            });
254
255            if at_all {
256                group_model["atUserIds"] = serde_json::json!({"@ALL": "@ALL"});
257            } else if at_sender {
258                let staff_id = msg.sender_staff_id.as_deref().unwrap_or("");
259                let nick = msg.sender_nick.as_deref().unwrap_or("");
260                group_model["atUserIds"] = serde_json::json!({staff_id: nick});
261            }
262
263            if let Some(recipients) = recipients {
264                group_model["recipients"] = serde_json::json!(recipients);
265            }
266
267            if let Some(ref hosting) = msg.hosting_context {
268                group_model["extension"] = serde_json::json!({
269                    "hostingRepliedContext": serde_json::to_string(
270                        &serde_json::json!({"userId": hosting.user_id})
271                    ).unwrap_or_default()
272                });
273            }
274
275            body_obj.insert("imGroupOpenDeliverModel".to_owned(), group_model);
276        } else if msg.conversation_type.as_deref() == Some("1") {
277            body_obj.insert(
278                "openSpaceId".to_owned(),
279                serde_json::json!(format!(
280                    "dtv1.card//IM_ROBOT.{}",
281                    msg.sender_staff_id.as_deref().unwrap_or("")
282                )),
283            );
284
285            let mut robot_model = serde_json::json!({"spaceType": "IM_ROBOT"});
286
287            if let Some(ref hosting) = msg.hosting_context {
288                robot_model["extension"] = serde_json::json!({
289                    "hostingRepliedContext": serde_json::to_string(
290                        &serde_json::json!({"userId": hosting.user_id})
291                    ).unwrap_or_default()
292                });
293            }
294
295            body_obj.insert("imRobotOpenDeliverModel".to_owned(), robot_model);
296        }
297    }
298}
299
300/// AI 卡片回复器
301#[derive(Clone)]
302pub struct AICardReplier {
303    inner: CardReplier,
304}
305
306impl AICardReplier {
307    /// 创建新的 AI 卡片回复器
308    pub fn new(inner: CardReplier) -> Self {
309        Self { inner }
310    }
311
312    /// 获取内部 `CardReplier` 的引用
313    pub fn inner(&self) -> &CardReplier {
314        &self.inner
315    }
316
317    /// AI 卡片创建(flowStatus = PROCESSING)
318    pub async fn start(
319        &self,
320        card_template_id: &str,
321        card_data: &serde_json::Value,
322        recipients: Option<&[String]>,
323        support_forward: bool,
324    ) -> crate::Result<String> {
325        let mut data = card_data.clone();
326        if let Some(obj) = data.as_object_mut() {
327            obj.insert(
328                "flowStatus".to_owned(),
329                serde_json::json!(AICardStatus::Processing as u8),
330            );
331        }
332
333        self.inner
334            .create_and_send_card(
335                card_template_id,
336                &data,
337                "STREAM",
338                "",
339                false,
340                false,
341                recipients,
342                support_forward,
343            )
344            .await
345    }
346
347    /// AI 卡片完成(flowStatus = FINISHED)
348    pub async fn finish(
349        &self,
350        card_instance_id: &str,
351        card_data: &serde_json::Value,
352    ) -> crate::Result<()> {
353        let mut data = card_data.clone();
354        if let Some(obj) = data.as_object_mut() {
355            obj.insert(
356                "flowStatus".to_owned(),
357                serde_json::json!(AICardStatus::Finished as u8),
358            );
359        }
360
361        self.inner
362            .put_card_data(card_instance_id, &data, None)
363            .await
364    }
365
366    /// AI 卡片失败(flowStatus = FAILED)
367    pub async fn fail(
368        &self,
369        card_instance_id: &str,
370        card_data: &serde_json::Value,
371    ) -> crate::Result<()> {
372        let mut data = card_data.clone();
373        if let Some(obj) = data.as_object_mut() {
374            obj.insert(
375                "flowStatus".to_owned(),
376                serde_json::json!(AICardStatus::Failed as u8),
377            );
378        }
379
380        self.inner
381            .put_card_data(card_instance_id, &data, None)
382            .await
383    }
384
385    /// AI 卡片流式输出
386    pub async fn streaming(
387        &self,
388        card_instance_id: &str,
389        content_key: &str,
390        content_value: &str,
391        append: bool,
392        finished: bool,
393        failed: bool,
394    ) -> crate::Result<()> {
395        let access_token = self.inner.token_manager.get_access_token().await?;
396
397        let body = serde_json::json!({
398            "outTrackId": card_instance_id,
399            "guid": uuid::Uuid::new_v4().to_string(),
400            "key": content_key,
401            "content": content_value,
402            "isFull": !append,
403            "isFinalize": finished,
404            "isError": failed,
405        });
406
407        let url = format!(
408            "{}/v1.0/card/streaming",
409            self.inner.http_client.openapi_endpoint()
410        );
411        self.inner
412            .http_client
413            .put_json(&url, &body, Some(&access_token))
414            .await?;
415
416        Ok(())
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_gen_card_id() {
426        let msg = ChatbotMessage {
427            sender_id: Some("user_001".to_owned()),
428            sender_corp_id: Some("corp_001".to_owned()),
429            conversation_id: Some("conv_001".to_owned()),
430            message_id: Some("msg_001".to_owned()),
431            ..Default::default()
432        };
433        let id = CardReplier::gen_card_id(&msg);
434        assert_eq!(id.len(), 64);
435    }
436
437    #[test]
438    fn test_ai_card_status_values() {
439        assert_eq!(AICardStatus::Processing as u8, 1);
440        assert_eq!(AICardStatus::Inputing as u8, 2);
441        assert_eq!(AICardStatus::Finished as u8, 3);
442        assert_eq!(AICardStatus::Executing as u8, 4);
443        assert_eq!(AICardStatus::Failed as u8, 5);
444    }
445
446    #[test]
447    fn test_ai_card_status_serialize() {
448        let status = AICardStatus::Processing;
449        let json = serde_json::to_value(status).unwrap();
450        assert_eq!(json, 1);
451    }
452}