Skip to main content

openlark_communication/common/
chain.rs

1//! openlark-communication 链式调用入口(meta 风格,偏"模块入口")
2//!
3//! 说明:
4//! - 本文件放在 `common/` 下,避免被 strict API 校验脚本计入"额外实现文件"。
5//! - communication 模块 API 规模较大(IM/Contact/Moments 等)。为避免为大量 API 手写方法,
6//!   这里先提供"bizTag 级入口 + Config 透传"。
7//! - 具体 API 调用仍使用各 `*RequestBuilder/*Request` 的 `new(config)` / `execute(...)`。
8
9use std::sync::Arc;
10
11use openlark_core::config::Config;
12#[cfg(any(feature = "im", feature = "contact"))]
13use openlark_core::error::business_error;
14#[cfg(feature = "im")]
15use openlark_core::validate_required;
16#[cfg(any(feature = "im", feature = "contact"))]
17use openlark_core::{SDKResult, error::validation_error};
18
19#[cfg(feature = "contact")]
20use crate::contact::contact::v3::user::{
21    create::UserResponse,
22    get::GetUserRequest,
23    models::{DepartmentIdType, UserIdType as ContactUserIdType},
24};
25#[cfg(feature = "contact")]
26use crate::contact::contact_search::old::default::v1::user::SearchUserRequest;
27#[cfg(feature = "im")]
28use crate::im::v1::message::{
29    create::{CreateMessageBody, CreateMessageRequest},
30    models::ReceiveIdType,
31    reply::{ReplyMessageBody, ReplyMessageRequest},
32};
33#[cfg(feature = "im")]
34use crate::im::v1::thread::forward::{ForwardThreadBody, ForwardThreadRequest};
35#[cfg(feature = "im")]
36use crate::im::v1::{
37    chat::{get::GetChatRequest, search::SearchChatsRequest},
38    message::models::UserIdType as ImUserIdType,
39};
40#[cfg(feature = "im")]
41use crate::im::v1::{
42    file::{
43        create::{CreateFileBody, CreateFileRequest},
44        models::CreateFileResponse,
45    },
46    image::{
47        create::CreateImageRequest,
48        models::{CreateImageResponse, ImageType},
49    },
50};
51
52/// 消息接收者 helper。
53///
54/// 统一接收者 ID 与 `receive_id_type`,避免文本/卡片等消息 helper
55/// 每次都分别传两个参数。
56#[cfg(feature = "im")]
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct MessageRecipient {
59    /// 接收者 ID。
60    pub receive_id: String,
61    /// 接收者 ID 类型。
62    pub receive_id_type: ReceiveIdType,
63}
64
65#[cfg(feature = "im")]
66impl MessageRecipient {
67    /// 使用指定接收者 ID 和类型创建 helper。
68    pub fn new(receive_id: impl Into<String>, receive_id_type: ReceiveIdType) -> Self {
69        Self {
70            receive_id: receive_id.into(),
71            receive_id_type,
72        }
73    }
74
75    /// 创建使用 `open_id` 的接收者。
76    pub fn open_id(receive_id: impl Into<String>) -> Self {
77        Self::new(receive_id, ReceiveIdType::OpenId)
78    }
79
80    /// 创建使用 `user_id` 的接收者。
81    pub fn user_id(receive_id: impl Into<String>) -> Self {
82        Self::new(receive_id, ReceiveIdType::UserId)
83    }
84
85    /// 创建使用邮箱的接收者。
86    pub fn email(receive_id: impl Into<String>) -> Self {
87        Self::new(receive_id, ReceiveIdType::Email)
88    }
89
90    /// 创建使用群 ID 的接收者。
91    pub fn chat_id(receive_id: impl Into<String>) -> Self {
92        Self::new(receive_id, ReceiveIdType::ChatId)
93    }
94}
95
96/// 富文本(post)消息 helper。
97///
98/// 当前只覆盖最常见的“标题 + 单段文本”结构,不试图抽象完整消息 DSL。
99#[cfg(feature = "im")]
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct PostMessage {
102    /// 语言区域。
103    pub locale: String,
104    /// 标题。
105    pub title: String,
106    /// 文本内容。
107    pub text: String,
108}
109
110#[cfg(feature = "im")]
111impl PostMessage {
112    /// 创建中文富文本消息 helper。
113    pub fn zh_cn(title: impl Into<String>, text: impl Into<String>) -> Self {
114        Self {
115            locale: "zh_cn".to_string(),
116            title: title.into(),
117            text: text.into(),
118        }
119    }
120
121    fn into_content(self) -> SDKResult<String> {
122        let title = self.title.trim().to_string();
123        let text = self.text.trim().to_string();
124        if title.is_empty() {
125            return Err(validation_error("title", "title 不能为空"));
126        }
127        if text.is_empty() {
128            return Err(validation_error("text", "text 不能为空"));
129        }
130
131        Ok(serde_json::json!({
132            "post": {
133                self.locale: {
134                    "title": title,
135                    "content": [[{"tag": "text", "text": text}]]
136                }
137            }
138        })
139        .to_string())
140    }
141}
142
143/// 回复上下文 helper。
144///
145/// 统一回复目标消息与是否在话题内回复的上下文参数。
146#[cfg(feature = "im")]
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct ReplyTarget {
149    /// 目标消息 ID。
150    pub message_id: String,
151    /// 是否以话题形式回复。
152    pub reply_in_thread: bool,
153}
154
155#[cfg(feature = "im")]
156impl ReplyTarget {
157    /// 创建普通回复目标。
158    pub fn direct(message_id: impl Into<String>) -> Self {
159        Self {
160            message_id: message_id.into(),
161            reply_in_thread: false,
162        }
163    }
164
165    /// 创建话题内回复目标。
166    pub fn in_thread(message_id: impl Into<String>) -> Self {
167        Self {
168            message_id: message_id.into(),
169            reply_in_thread: true,
170        }
171    }
172}
173
174/// 图片上传 helper。
175///
176/// 默认按消息图片上传,调用方只需传入二进制内容;
177/// 如有需要,可额外补充文件名或切换为头像上传。
178#[cfg(feature = "im")]
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct MediaImageUpload {
181    /// 图片用途类型。
182    pub image_type: ImageType,
183    /// 文件名。
184    pub file_name: Option<String>,
185    /// 图片二进制内容。
186    pub bytes: Vec<u8>,
187}
188
189#[cfg(feature = "im")]
190impl MediaImageUpload {
191    /// 使用图片字节创建上传 helper。
192    pub fn new(bytes: Vec<u8>) -> Self {
193        Self {
194            image_type: ImageType::Message,
195            file_name: None,
196            bytes,
197        }
198    }
199
200    /// 将图片类型切换为头像。
201    pub fn avatar(mut self) -> Self {
202        self.image_type = ImageType::Avatar;
203        self
204    }
205
206    /// 设置上传文件名。
207    pub fn file_name(mut self, file_name: impl Into<String>) -> Self {
208        self.file_name = Some(file_name.into());
209        self
210    }
211}
212
213/// 文件上传 helper。
214///
215/// 默认根据文件名后缀推断 `file_type`,必要时允许调用方显式覆盖。
216#[cfg(feature = "im")]
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct MediaFileUpload {
219    /// 文件名。
220    pub file_name: String,
221    /// 文件类型。
222    pub file_type: String,
223    /// 时长,通常用于音视频。
224    pub duration: Option<i32>,
225    /// 文件二进制内容。
226    pub bytes: Vec<u8>,
227}
228
229#[cfg(feature = "im")]
230impl MediaFileUpload {
231    /// 使用文件名和内容创建上传 helper。
232    pub fn new(file_name: impl Into<String>, bytes: Vec<u8>) -> Self {
233        let file_name = file_name.into();
234        let file_type = infer_file_type(&file_name);
235        Self {
236            file_name,
237            file_type,
238            duration: None,
239            bytes,
240        }
241    }
242
243    /// 显式覆盖文件类型。
244    pub fn file_type(mut self, file_type: impl Into<String>) -> Self {
245        self.file_type = file_type.into();
246        self
247    }
248
249    /// 设置媒体时长。
250    pub fn duration(mut self, duration: i32) -> Self {
251        self.duration = Some(duration);
252        self
253    }
254}
255
256/// 用户查找 helper。
257///
258/// 统一承载搜索用户接口里最常用的字段,避免调用方直接处理原始 JSON。
259#[cfg(feature = "contact")]
260#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
261pub struct UserLookupItem {
262    /// 用户名称。
263    pub name: String,
264    /// 用户 open_id。
265    pub open_id: String,
266    /// 用户 user_id。
267    #[serde(default)]
268    pub user_id: Option<String>,
269    /// 用户所在部门 ID 列表。
270    #[serde(default)]
271    pub department_ids: Vec<String>,
272}
273
274/// 群查找 helper。
275///
276/// 统一承载群搜索结果里最常用的字段。
277#[cfg(feature = "im")]
278#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
279pub struct ChatLookupItem {
280    /// 群 ID。
281    pub chat_id: String,
282    /// 群名称。
283    pub name: String,
284    /// 群描述。
285    #[serde(default)]
286    pub description: Option<String>,
287    /// 群主 ID。
288    #[serde(default)]
289    pub owner_id: Option<String>,
290    /// 群主 ID 类型。
291    #[serde(default)]
292    pub owner_id_type: Option<String>,
293    /// 是否外部群。
294    #[serde(default)]
295    pub external: bool,
296    /// 租户 key。
297    #[serde(default)]
298    pub tenant_key: Option<String>,
299    /// 群状态。
300    #[serde(default)]
301    pub chat_status: Option<String>,
302}
303
304#[cfg(feature = "contact")]
305#[derive(Debug, Clone, serde::Deserialize)]
306struct UserLookupResponse {
307    #[serde(default)]
308    has_more: bool,
309    #[serde(default)]
310    page_token: Option<String>,
311    #[serde(default)]
312    users: Vec<UserLookupItem>,
313}
314
315#[cfg(feature = "im")]
316#[derive(Debug, Clone, serde::Deserialize)]
317struct ChatLookupResponse {
318    #[serde(default)]
319    has_more: bool,
320    #[serde(default)]
321    page_token: Option<String>,
322    #[serde(default)]
323    items: Vec<ChatLookupItem>,
324}
325
326/// Communication 链式入口:`communication.im` / `communication.contact` / `communication.moments`
327#[derive(Debug, Clone)]
328pub struct CommunicationClient {
329    config: Arc<Config>,
330    #[cfg(feature = "aily")]
331    /// AILY helper 入口。
332    pub aily: AilyClient,
333
334    #[cfg(feature = "im")]
335    /// IM helper 入口。
336    pub im: ImClient,
337
338    #[cfg(feature = "contact")]
339    /// 通讯录 helper 入口。
340    pub contact: ContactClient,
341
342    #[cfg(feature = "moments")]
343    /// Moments helper 入口。
344    pub moments: MomentsClient,
345}
346
347#[cfg(feature = "aily")]
348/// AILY 链式 helper 入口。
349#[derive(Debug, Clone)]
350pub struct AilyClient {
351    config: Arc<Config>,
352}
353
354#[cfg(feature = "aily")]
355impl AilyClient {
356    fn new(config: Arc<Config>) -> Self {
357        Self { config }
358    }
359
360    /// 返回底层共享配置。
361    pub fn config(&self) -> &Config {
362        &self.config
363    }
364
365    /// 访问 aily_session 资源。
366    pub fn aily_session(&self) -> AilySessionResource {
367        AilySessionResource::new(self.config.clone())
368    }
369
370    /// 访问 app 资源。
371    pub fn app(&self) -> AppResource {
372        AppResource::new(self.config.clone())
373    }
374
375    /// 访问 agent 资源。
376    pub fn agent(&self) -> AgentResource {
377        AgentResource::new(self.config.clone())
378    }
379
380    /// 访问 tenant 资源。
381    pub fn tenant(&self) -> TenantResource {
382        TenantResource::new(self.config.clone())
383    }
384}
385
386/// AILY 会话资源。
387#[cfg(feature = "aily")]
388#[derive(Debug, Clone)]
389pub struct AilySessionResource {
390    config: Arc<Config>,
391}
392
393#[cfg(feature = "aily")]
394impl AilySessionResource {
395    fn new(config: Arc<Config>) -> Self {
396        Self { config }
397    }
398
399    /// 创建会话请求。
400    pub fn create(&self) -> crate::aily::aily::v1::aily_session::create::CreateSessionRequest {
401        crate::aily::aily::v1::aily_session::create::CreateSessionRequest::new(
402            (*self.config).clone(),
403        )
404    }
405
406    /// 创建删除会话请求。
407    pub fn delete(
408        &self,
409        session_id: impl Into<String>,
410    ) -> crate::aily::aily::v1::aily_session::delete::DeleteSessionRequest {
411        crate::aily::aily::v1::aily_session::delete::DeleteSessionRequest::new(
412            (*self.config).clone(),
413        )
414        .aily_session_id(session_id)
415    }
416
417    /// 创建获取会话请求。
418    pub fn get(
419        &self,
420        session_id: impl Into<String>,
421    ) -> crate::aily::aily::v1::aily_session::get::GetSessionRequest {
422        crate::aily::aily::v1::aily_session::get::GetSessionRequest::new((*self.config).clone())
423            .aily_session_id(session_id)
424    }
425
426    /// 创建更新会话请求。
427    pub fn update(
428        &self,
429        session_id: impl Into<String>,
430    ) -> crate::aily::aily::v1::aily_session::update::UpdateSessionRequest {
431        crate::aily::aily::v1::aily_session::update::UpdateSessionRequest::new(
432            (*self.config).clone(),
433        )
434        .aily_session_id(session_id)
435    }
436
437    /// 访问 aily_message 资源。
438    pub fn aily_message(&self) -> AilyMessageResource {
439        AilyMessageResource::new(self.config.clone())
440    }
441
442    /// 访问 run 资源。
443    pub fn run(&self) -> RunResource {
444        RunResource::new(self.config.clone())
445    }
446}
447
448/// AILY 消息资源。
449#[cfg(feature = "aily")]
450#[derive(Debug, Clone)]
451pub struct AilyMessageResource {
452    config: Arc<Config>,
453}
454
455#[cfg(feature = "aily")]
456impl AilyMessageResource {
457    fn new(config: Arc<Config>) -> Self {
458        Self { config }
459    }
460
461    /// 创建 Aily 消息请求。
462    pub fn create(
463        &self,
464        session_id: impl Into<String>,
465    ) -> crate::aily::aily::v1::aily_session::aily_message::create::CreateAilyMessageRequest {
466        crate::aily::aily::v1::aily_session::aily_message::create::CreateAilyMessageRequest::new(
467            (*self.config).clone(),
468        )
469        .aily_session_id(session_id)
470    }
471
472    /// 创建获取 Aily 消息请求。
473    pub fn get(
474        &self,
475        session_id: impl Into<String>,
476        message_id: impl Into<String>,
477    ) -> crate::aily::aily::v1::aily_session::aily_message::get::GetMessageRequest {
478        crate::aily::aily::v1::aily_session::aily_message::get::GetMessageRequest::new(
479            (*self.config).clone(),
480        )
481        .aily_session_id(session_id)
482        .aily_message_id(message_id)
483    }
484
485    /// 创建列出 Aily 消息请求。
486    pub fn list(
487        &self,
488        session_id: impl Into<String>,
489    ) -> crate::aily::aily::v1::aily_session::aily_message::list::ListAilyMessagesRequest {
490        crate::aily::aily::v1::aily_session::aily_message::list::ListAilyMessagesRequest::new(
491            (*self.config).clone(),
492        )
493        .aily_session_id(session_id)
494    }
495}
496
497/// AILY 运行资源。
498#[cfg(feature = "aily")]
499#[derive(Debug, Clone)]
500pub struct RunResource {
501    config: Arc<Config>,
502}
503
504#[cfg(feature = "aily")]
505impl RunResource {
506    fn new(config: Arc<Config>) -> Self {
507        Self { config }
508    }
509
510    /// 创建取消运行请求。
511    pub fn cancel(
512        &self,
513        session_id: impl Into<String>,
514        run_id: impl Into<String>,
515    ) -> crate::aily::aily::v1::aily_session::run::cancel::CancelRunRequest {
516        crate::aily::aily::v1::aily_session::run::cancel::CancelRunRequest::new(
517            (*self.config).clone(),
518        )
519        .aily_session_id(session_id)
520        .run_id(run_id)
521    }
522
523    /// 创建运行请求。
524    pub fn create(
525        &self,
526        session_id: impl Into<String>,
527    ) -> crate::aily::aily::v1::aily_session::run::create::CreateRunRequest {
528        crate::aily::aily::v1::aily_session::run::create::CreateRunRequest::new(
529            (*self.config).clone(),
530        )
531        .aily_session_id(session_id)
532    }
533
534    /// 创建获取运行请求。
535    pub fn get(
536        &self,
537        session_id: impl Into<String>,
538        run_id: impl Into<String>,
539    ) -> crate::aily::aily::v1::aily_session::run::get::GetRunRequest {
540        crate::aily::aily::v1::aily_session::run::get::GetRunRequest::new((*self.config).clone())
541            .aily_session_id(session_id)
542            .run_id(run_id)
543    }
544
545    /// 创建列出运行请求。
546    pub fn list(
547        &self,
548        session_id: impl Into<String>,
549    ) -> crate::aily::aily::v1::aily_session::run::list::ListRunsRequest {
550        crate::aily::aily::v1::aily_session::run::list::ListRunsRequest::new((*self.config).clone())
551            .aily_session_id(session_id)
552    }
553}
554
555/// AILY 应用资源。
556#[cfg(feature = "aily")]
557#[derive(Debug, Clone)]
558pub struct AppResource {
559    config: Arc<Config>,
560}
561
562#[cfg(feature = "aily")]
563impl AppResource {
564    fn new(config: Arc<Config>) -> Self {
565        Self { config }
566    }
567
568    /// 访问 data_asset 资源。
569    pub fn data_asset(&self) -> DataAssetResource {
570        DataAssetResource::new(self.config.clone())
571    }
572
573    /// 访问 data_asset_tag 资源。
574    pub fn data_asset_tag(&self) -> DataAssetTagResource {
575        DataAssetTagResource::new(self.config.clone())
576    }
577
578    /// 访问 knowledge 资源。
579    pub fn knowledge(&self) -> KnowledgeResource {
580        KnowledgeResource::new(self.config.clone())
581    }
582
583    /// 访问 skill 资源。
584    pub fn skill(&self) -> SkillResource {
585        SkillResource::new(self.config.clone())
586    }
587}
588
589/// AILY 数据知识资源。
590#[cfg(feature = "aily")]
591#[derive(Debug, Clone)]
592pub struct DataAssetResource {
593    config: Arc<Config>,
594}
595
596#[cfg(feature = "aily")]
597impl DataAssetResource {
598    fn new(config: Arc<Config>) -> Self {
599        Self { config }
600    }
601
602    /// 创建数据知识请求。
603    pub fn create(
604        &self,
605        app_id: impl Into<String>,
606    ) -> crate::aily::aily::v1::app::data_asset::create::CreateDataAssetRequest {
607        crate::aily::aily::v1::app::data_asset::create::CreateDataAssetRequest::new(
608            (*self.config).clone(),
609        )
610        .app_id(app_id)
611    }
612
613    /// 创建删除数据知识请求。
614    pub fn delete(
615        &self,
616        app_id: impl Into<String>,
617        data_asset_id: impl Into<String>,
618    ) -> crate::aily::aily::v1::app::data_asset::delete::DeleteDataAssetRequest {
619        crate::aily::aily::v1::app::data_asset::delete::DeleteDataAssetRequest::new(
620            (*self.config).clone(),
621        )
622        .app_id(app_id)
623        .data_asset_id(data_asset_id)
624    }
625
626    /// 创建获取数据知识请求。
627    pub fn get(
628        &self,
629        app_id: impl Into<String>,
630        data_asset_id: impl Into<String>,
631    ) -> crate::aily::aily::v1::app::data_asset::get::GetDataAssetRequest {
632        crate::aily::aily::v1::app::data_asset::get::GetDataAssetRequest::new(
633            (*self.config).clone(),
634        )
635        .app_id(app_id)
636        .data_asset_id(data_asset_id)
637    }
638
639    /// 创建查询数据知识列表请求。
640    pub fn list(
641        &self,
642        app_id: impl Into<String>,
643    ) -> crate::aily::aily::v1::app::data_asset::list::ListDataAssetsRequest {
644        crate::aily::aily::v1::app::data_asset::list::ListDataAssetsRequest::new(
645            (*self.config).clone(),
646        )
647        .app_id(app_id)
648    }
649
650    /// 创建上传文件请求。
651    pub fn upload_file(
652        &self,
653        app_id: impl Into<String>,
654    ) -> crate::aily::aily::v1::app::data_asset::upload_file::UploadFileRequest {
655        crate::aily::aily::v1::app::data_asset::upload_file::UploadFileRequest::new(
656            (*self.config).clone(),
657        )
658        .app_id(app_id)
659    }
660}
661
662/// AILY 数据知识分类资源。
663#[cfg(feature = "aily")]
664#[derive(Debug, Clone)]
665pub struct DataAssetTagResource {
666    config: Arc<Config>,
667}
668
669#[cfg(feature = "aily")]
670impl DataAssetTagResource {
671    fn new(config: Arc<Config>) -> Self {
672        Self { config }
673    }
674
675    /// 创建获取数据知识分类列表请求。
676    pub fn list(
677        &self,
678        app_id: impl Into<String>,
679    ) -> crate::aily::aily::v1::app::data_asset_tag::list::ListDataAssetTagsRequest {
680        crate::aily::aily::v1::app::data_asset_tag::list::ListDataAssetTagsRequest::new(
681            (*self.config).clone(),
682        )
683        .app_id(app_id)
684    }
685}
686
687/// AILY 数据知识问答资源。
688#[cfg(feature = "aily")]
689#[derive(Debug, Clone)]
690pub struct KnowledgeResource {
691    config: Arc<Config>,
692}
693
694#[cfg(feature = "aily")]
695impl KnowledgeResource {
696    fn new(config: Arc<Config>) -> Self {
697        Self { config }
698    }
699
700    /// 创建数据知识问答请求。
701    pub fn ask(
702        &self,
703        app_id: impl Into<String>,
704    ) -> crate::aily::aily::v1::app::knowledge::ask::AskKnowledgeRequest {
705        crate::aily::aily::v1::app::knowledge::ask::AskKnowledgeRequest::new((*self.config).clone())
706            .app_id(app_id)
707    }
708}
709
710/// AILY 技能资源。
711#[cfg(feature = "aily")]
712#[derive(Debug, Clone)]
713pub struct SkillResource {
714    config: Arc<Config>,
715}
716
717#[cfg(feature = "aily")]
718impl SkillResource {
719    fn new(config: Arc<Config>) -> Self {
720        Self { config }
721    }
722
723    /// 创建获取技能请求。
724    pub fn get(
725        &self,
726        app_id: impl Into<String>,
727        skill_id: impl Into<String>,
728    ) -> crate::aily::aily::v1::app::skill::get::GetSkillRequest {
729        crate::aily::aily::v1::app::skill::get::GetSkillRequest::new((*self.config).clone())
730            .app_id(app_id)
731            .skill_id(skill_id)
732    }
733
734    /// 创建查询技能列表请求。
735    pub fn list(
736        &self,
737        app_id: impl Into<String>,
738    ) -> crate::aily::aily::v1::app::skill::list::ListSkillsRequest {
739        crate::aily::aily::v1::app::skill::list::ListSkillsRequest::new((*self.config).clone())
740            .app_id(app_id)
741    }
742
743    /// 创建调用技能请求。
744    pub fn start(
745        &self,
746        app_id: impl Into<String>,
747        skill_id: impl Into<String>,
748    ) -> crate::aily::aily::v1::app::skill::start::StartSkillRequest {
749        crate::aily::aily::v1::app::skill::start::StartSkillRequest::new((*self.config).clone())
750            .app_id(app_id)
751            .skill_id(skill_id)
752    }
753}
754
755/// AILY 智能体资源。
756#[cfg(feature = "aily")]
757#[derive(Debug, Clone)]
758pub struct AgentResource {
759    config: Arc<Config>,
760}
761
762#[cfg(feature = "aily")]
763impl AgentResource {
764    fn new(config: Arc<Config>) -> Self {
765        Self { config }
766    }
767
768    /// 访问 agent_artifact 资源。
769    pub fn agent_artifact(&self) -> AgentArtifactResource {
770        AgentArtifactResource::new(self.config.clone())
771    }
772
773    /// 访问 agent_attachment 资源。
774    pub fn agent_attachment(&self) -> AgentAttachmentResource {
775        AgentAttachmentResource::new(self.config.clone())
776    }
777
778    /// 访问 agent_chat 资源。
779    pub fn agent_chat(&self) -> AgentChatResource {
780        AgentChatResource::new(self.config.clone())
781    }
782
783    /// 访问 agent_chat_session 资源。
784    pub fn agent_chat_session(&self) -> AgentChatSessionResource {
785        AgentChatSessionResource::new(self.config.clone())
786    }
787
788    /// 访问 agent_visibility 资源。
789    pub fn agent_visibility(&self) -> AgentVisibilityResource {
790        AgentVisibilityResource::new(self.config.clone())
791    }
792}
793
794/// AILY 智能体产物资源。
795#[cfg(feature = "aily")]
796#[derive(Debug, Clone)]
797pub struct AgentArtifactResource {
798    config: Arc<Config>,
799}
800
801#[cfg(feature = "aily")]
802impl AgentArtifactResource {
803    fn new(config: Arc<Config>) -> Self {
804        Self { config }
805    }
806
807    /// 创建下载智能体产物请求。
808    pub fn get(
809        &self,
810        agent_id: impl Into<String>,
811        agent_artifact_id: impl Into<String>,
812    ) -> crate::aily::aily::v1::agent::agent_artifact::get::GetAgentArtifactRequest {
813        crate::aily::aily::v1::agent::agent_artifact::get::GetAgentArtifactRequest::new(
814            self.config.clone(),
815        )
816        .agent_id(agent_id)
817        .agent_artifact_id(agent_artifact_id)
818    }
819}
820
821/// AILY 智能体附件资源。
822#[cfg(feature = "aily")]
823#[derive(Debug, Clone)]
824pub struct AgentAttachmentResource {
825    config: Arc<Config>,
826}
827
828#[cfg(feature = "aily")]
829impl AgentAttachmentResource {
830    fn new(config: Arc<Config>) -> Self {
831        Self { config }
832    }
833
834    /// 创建上传附件请求。
835    pub fn create(
836        &self,
837        agent_id: impl Into<String>,
838    ) -> crate::aily::aily::v1::agent::agent_attachment::create::CreateAgentAttachmentRequest {
839        crate::aily::aily::v1::agent::agent_attachment::create::CreateAgentAttachmentRequest::new(
840            self.config.clone(),
841        )
842        .agent_id(agent_id)
843    }
844}
845
846/// AILY 智能体会话资源。
847#[cfg(feature = "aily")]
848#[derive(Debug, Clone)]
849pub struct AgentChatResource {
850    config: Arc<Config>,
851}
852
853#[cfg(feature = "aily")]
854impl AgentChatResource {
855    fn new(config: Arc<Config>) -> Self {
856        Self { config }
857    }
858
859    /// 创建发起智能体会话请求。
860    pub fn create(
861        &self,
862        agent_id: impl Into<String>,
863    ) -> crate::aily::aily::v1::agent::agent_chat::create::CreateAgentChatRequest {
864        crate::aily::aily::v1::agent::agent_chat::create::CreateAgentChatRequest::new(
865            self.config.clone(),
866        )
867        .agent_id(agent_id)
868    }
869
870    /// 创建获取会话结果请求。
871    pub fn get(
872        &self,
873        agent_id: impl Into<String>,
874        agent_chat_id: impl Into<String>,
875    ) -> crate::aily::aily::v1::agent::agent_chat::get::GetAgentChatRequest {
876        crate::aily::aily::v1::agent::agent_chat::get::GetAgentChatRequest::new(self.config.clone())
877            .agent_id(agent_id)
878            .agent_chat_id(agent_chat_id)
879    }
880}
881
882/// AILY 智能体会话(Session)资源。
883#[cfg(feature = "aily")]
884#[derive(Debug, Clone)]
885pub struct AgentChatSessionResource {
886    config: Arc<Config>,
887}
888
889#[cfg(feature = "aily")]
890impl AgentChatSessionResource {
891    fn new(config: Arc<Config>) -> Self {
892        Self { config }
893    }
894
895    /// 创建会话请求。
896    pub fn create(
897        &self,
898        agent_id: impl Into<String>,
899    ) -> crate::aily::aily::v1::agent::agent_chat_session::create::CreateAgentChatSessionRequest
900    {
901        crate::aily::aily::v1::agent::agent_chat_session::create::CreateAgentChatSessionRequest::new(
902            self.config.clone(),
903        )
904        .agent_id(agent_id)
905    }
906
907    /// 删除会话请求。
908    pub fn delete(
909        &self,
910        agent_id: impl Into<String>,
911        agent_chat_session_id: impl Into<String>,
912    ) -> crate::aily::aily::v1::agent::agent_chat_session::delete::DeleteAgentChatSessionRequest
913    {
914        crate::aily::aily::v1::agent::agent_chat_session::delete::DeleteAgentChatSessionRequest::new(
915            self.config.clone(),
916        )
917        .agent_id(agent_id)
918        .agent_chat_session_id(agent_chat_session_id)
919    }
920
921    /// 获取指定会话信息请求。
922    pub fn get(
923        &self,
924        agent_id: impl Into<String>,
925        agent_chat_session_id: impl Into<String>,
926    ) -> crate::aily::aily::v1::agent::agent_chat_session::get::GetAgentChatSessionRequest {
927        crate::aily::aily::v1::agent::agent_chat_session::get::GetAgentChatSessionRequest::new(
928            self.config.clone(),
929        )
930        .agent_id(agent_id)
931        .agent_chat_session_id(agent_chat_session_id)
932    }
933
934    /// 查询会话列表请求。
935    pub fn list(
936        &self,
937        agent_id: impl Into<String>,
938    ) -> crate::aily::aily::v1::agent::agent_chat_session::list::ListAgentChatSessionRequest {
939        crate::aily::aily::v1::agent::agent_chat_session::list::ListAgentChatSessionRequest::new(
940            self.config.clone(),
941        )
942        .agent_id(agent_id)
943    }
944}
945
946/// AILY 智能体可见性资源。
947#[cfg(feature = "aily")]
948#[derive(Debug, Clone)]
949pub struct AgentVisibilityResource {
950    config: Arc<Config>,
951}
952
953#[cfg(feature = "aily")]
954impl AgentVisibilityResource {
955    fn new(config: Arc<Config>) -> Self {
956        Self { config }
957    }
958
959    /// 创建获取当前用户可见性请求。
960    pub fn check(
961        &self,
962        agent_id: impl Into<String>,
963    ) -> crate::aily::aily::v1::agent::agent_visibility::check::CheckAgentVisibilityRequest {
964        crate::aily::aily::v1::agent::agent_visibility::check::CheckAgentVisibilityRequest::new(
965            self.config.clone(),
966        )
967        .agent_id(agent_id)
968    }
969}
970
971/// AILY 租户资源。
972#[cfg(feature = "aily")]
973#[derive(Debug, Clone)]
974pub struct TenantResource {
975    config: Arc<Config>,
976}
977
978#[cfg(feature = "aily")]
979impl TenantResource {
980    fn new(config: Arc<Config>) -> Self {
981        Self { config }
982    }
983
984    /// 访问 app_stat 资源。
985    pub fn app_stat(&self) -> AppStatResource {
986        AppStatResource::new(self.config.clone())
987    }
988}
989
990/// AILY 应用统计资源。
991#[cfg(feature = "aily")]
992#[derive(Debug, Clone)]
993pub struct AppStatResource {
994    config: Arc<Config>,
995}
996
997#[cfg(feature = "aily")]
998impl AppStatResource {
999    fn new(config: Arc<Config>) -> Self {
1000        Self { config }
1001    }
1002
1003    /// 创建查询应用统计数据请求。
1004    pub fn list(&self) -> crate::aily::aily::v1::tenant::app_stat::list::ListAppStatsRequest {
1005        crate::aily::aily::v1::tenant::app_stat::list::ListAppStatsRequest::new(
1006            (*self.config).clone(),
1007        )
1008    }
1009}
1010
1011impl CommunicationClient {
1012    /// 使用配置创建 communication 链式入口。
1013    pub fn new(config: Config) -> Self {
1014        let config = Arc::new(config);
1015        Self {
1016            config: config.clone(),
1017            #[cfg(feature = "aily")]
1018            aily: AilyClient::new(config.clone()),
1019            #[cfg(feature = "im")]
1020            im: ImClient::new(config.clone()),
1021            #[cfg(feature = "contact")]
1022            contact: ContactClient::new(config.clone()),
1023            #[cfg(feature = "moments")]
1024            moments: MomentsClient::new(config),
1025        }
1026    }
1027
1028    /// 返回底层共享配置。
1029    pub fn config(&self) -> &Config {
1030        &self.config
1031    }
1032}
1033
1034#[cfg(feature = "im")]
1035/// IM 链式 helper 入口。
1036#[derive(Debug, Clone)]
1037pub struct ImClient {
1038    config: Arc<Config>,
1039}
1040
1041#[cfg(feature = "im")]
1042impl ImClient {
1043    fn new(config: Arc<Config>) -> Self {
1044        Self { config }
1045    }
1046
1047    /// 返回底层共享配置。
1048    pub fn config(&self) -> &Config {
1049        &self.config
1050    }
1051
1052    /// 发送文本消息 helper。
1053    pub async fn send_text(
1054        &self,
1055        recipient: MessageRecipient,
1056        text: impl Into<String>,
1057    ) -> SDKResult<serde_json::Value> {
1058        let body = Self::build_text_body(recipient, text.into())?;
1059        Self::create_message_request(self.config.clone(), body.receive_id_type())
1060            .execute(body.into())
1061            .await
1062    }
1063
1064    /// 发送富文本(post)消息 helper。
1065    pub async fn send_post(
1066        &self,
1067        recipient: MessageRecipient,
1068        post: PostMessage,
1069    ) -> SDKResult<serde_json::Value> {
1070        let body = Self::build_post_body(recipient, post)?;
1071        Self::create_message_request(self.config.clone(), body.receive_id_type())
1072            .execute(body.into())
1073            .await
1074    }
1075
1076    /// 回复文本消息 helper。
1077    pub async fn reply_text(
1078        &self,
1079        target: ReplyTarget,
1080        text: impl Into<String>,
1081    ) -> SDKResult<serde_json::Value> {
1082        let body = Self::build_reply_text_body(target, text.into())?;
1083        Self::create_reply_request(self.config.clone(), body.message_id())
1084            .execute(body.into())
1085            .await
1086    }
1087
1088    /// 回复富文本(post)消息 helper。
1089    pub async fn reply_post(
1090        &self,
1091        target: ReplyTarget,
1092        post: PostMessage,
1093    ) -> SDKResult<serde_json::Value> {
1094        let body = Self::build_reply_post_body(target, post)?;
1095        Self::create_reply_request(self.config.clone(), body.message_id())
1096            .execute(body.into())
1097            .await
1098    }
1099
1100    /// 转发话题 helper。
1101    pub async fn forward_thread(
1102        &self,
1103        thread_id: impl Into<String>,
1104        recipient: MessageRecipient,
1105    ) -> SDKResult<serde_json::Value> {
1106        let request = ForwardThreadRequest::new(self.config.as_ref().clone())
1107            .thread_id(thread_id)
1108            .receive_id_type(recipient.receive_id_type);
1109        request
1110            .execute(ForwardThreadBody::new(recipient.receive_id))
1111            .await
1112    }
1113
1114    /// 上传消息图片 helper。
1115    pub async fn upload_image(&self, upload: MediaImageUpload) -> SDKResult<CreateImageResponse> {
1116        if upload.bytes.is_empty() {
1117            return Err(validation_error("image", "image 不能为空"));
1118        }
1119        let mut request =
1120            CreateImageRequest::new(self.config.as_ref().clone()).image_type(upload.image_type);
1121        if let Some(file_name) = upload.file_name {
1122            request = request.file_name(file_name);
1123        }
1124        request.execute(upload.bytes).await
1125    }
1126
1127    /// 上传消息文件 helper。
1128    pub async fn upload_file(&self, upload: MediaFileUpload) -> SDKResult<CreateFileResponse> {
1129        let mut body = CreateFileBody::new(upload.file_type, upload.file_name);
1130        if let Some(duration) = upload.duration {
1131            body = body.duration(duration);
1132        }
1133        CreateFileRequest::new(self.config.as_ref().clone())
1134            .execute(body, upload.bytes)
1135            .await
1136    }
1137
1138    /// 发送图片消息 helper。
1139    pub async fn send_image(
1140        &self,
1141        recipient: MessageRecipient,
1142        image_key: impl Into<String>,
1143    ) -> SDKResult<serde_json::Value> {
1144        let image_key = image_key.into();
1145        if image_key.trim().is_empty() {
1146            return Err(validation_error("image_key", "image_key 不能为空"));
1147        }
1148        let body = Self::build_media_body(
1149            recipient,
1150            "image",
1151            serde_json::json!({ "image_key": image_key }).to_string(),
1152        )?;
1153        Self::create_message_request(self.config.clone(), body.receive_id_type())
1154            .execute(body.into())
1155            .await
1156    }
1157
1158    /// 发送文件消息 helper。
1159    pub async fn send_file(
1160        &self,
1161        recipient: MessageRecipient,
1162        file_key: impl Into<String>,
1163    ) -> SDKResult<serde_json::Value> {
1164        let file_key = file_key.into();
1165        if file_key.trim().is_empty() {
1166            return Err(validation_error("file_key", "file_key 不能为空"));
1167        }
1168        let body = Self::build_media_body(
1169            recipient,
1170            "file",
1171            serde_json::json!({ "file_key": file_key }).to_string(),
1172        )?;
1173        Self::create_message_request(self.config.clone(), body.receive_id_type())
1174            .execute(body.into())
1175            .await
1176    }
1177
1178    /// 搜索可见群聊并自动处理分页。
1179    pub async fn search_chats_all(&self, query: impl AsRef<str>) -> SDKResult<Vec<ChatLookupItem>> {
1180        let query = query.as_ref().trim().to_string();
1181        if query.is_empty() {
1182            return Err(validation_error("query", "query 不能为空"));
1183        }
1184
1185        let mut items = Vec::new();
1186        let mut page_token: Option<String> = None;
1187
1188        loop {
1189            let mut request = SearchChatsRequest::new(self.config.as_ref().clone())
1190                .query(query.clone())
1191                .user_id_type(ImUserIdType::OpenId)
1192                .page_size(100);
1193            if let Some(token) = &page_token {
1194                request = request.page_token(token.clone());
1195            }
1196
1197            let response: ChatLookupResponse = serde_json::from_value(request.execute().await?)
1198                .map_err(|e| validation_error("chat_lookup_response", e.to_string().as_str()))?;
1199            items.extend(response.items);
1200
1201            if !response.has_more {
1202                break;
1203            }
1204            page_token = response.page_token;
1205        }
1206
1207        Ok(items)
1208    }
1209
1210    /// 按群名称唯一查找单个群聊。
1211    pub async fn find_chat_by_name(&self, name: &str) -> SDKResult<ChatLookupItem> {
1212        let items = self.search_chats_all(name).await?;
1213        find_unique_chat_by_name(&items, name)
1214    }
1215
1216    /// 通过 chat_id 获取群详情。
1217    pub async fn get_chat_info(&self, chat_id: impl Into<String>) -> SDKResult<serde_json::Value> {
1218        GetChatRequest::new(self.config.as_ref().clone())
1219            .chat_id(chat_id)
1220            .user_id_type(ImUserIdType::OpenId)
1221            .execute()
1222            .await
1223    }
1224
1225    fn create_message_request(
1226        config: Arc<Config>,
1227        receive_id_type: ReceiveIdType,
1228    ) -> CreateMessageRequest {
1229        CreateMessageRequest::new(config.as_ref().clone()).receive_id_type(receive_id_type)
1230    }
1231
1232    fn create_reply_request(config: Arc<Config>, message_id: String) -> ReplyMessageRequest {
1233        ReplyMessageRequest::new(config.as_ref().clone()).message_id(message_id)
1234    }
1235
1236    fn build_text_body(recipient: MessageRecipient, text: String) -> SDKResult<HelperMessageBody> {
1237        let text = text.trim().to_string();
1238        if text.is_empty() {
1239            return Err(validation_error("text", "text 不能为空"));
1240        }
1241
1242        Ok(HelperMessageBody::new(
1243            recipient,
1244            "text",
1245            serde_json::json!({ "text": text }).to_string(),
1246        ))
1247    }
1248
1249    fn build_post_body(
1250        recipient: MessageRecipient,
1251        post: PostMessage,
1252    ) -> SDKResult<HelperMessageBody> {
1253        Ok(HelperMessageBody::new(
1254            recipient,
1255            "post",
1256            post.into_content()?,
1257        ))
1258    }
1259
1260    fn build_media_body(
1261        recipient: MessageRecipient,
1262        msg_type: &str,
1263        content: String,
1264    ) -> SDKResult<HelperMessageBody> {
1265        validate_required!(content, "content 不能为空");
1266        Ok(HelperMessageBody::new(recipient, msg_type, content))
1267    }
1268
1269    fn build_reply_text_body(target: ReplyTarget, text: String) -> SDKResult<HelperReplyBody> {
1270        let text = text.trim().to_string();
1271        if text.is_empty() {
1272            return Err(validation_error("text", "text 不能为空"));
1273        }
1274
1275        Ok(HelperReplyBody::new(
1276            target,
1277            "text",
1278            serde_json::json!({ "text": text }).to_string(),
1279        ))
1280    }
1281
1282    fn build_reply_post_body(target: ReplyTarget, post: PostMessage) -> SDKResult<HelperReplyBody> {
1283        Ok(HelperReplyBody::new(target, "post", post.into_content()?))
1284    }
1285}
1286
1287#[cfg(feature = "im")]
1288#[derive(Debug, Clone)]
1289struct HelperMessageBody {
1290    body: CreateMessageBody,
1291    receive_id_type: ReceiveIdType,
1292}
1293
1294#[cfg(feature = "im")]
1295impl HelperMessageBody {
1296    fn new(recipient: MessageRecipient, msg_type: &str, content: String) -> Self {
1297        Self {
1298            receive_id_type: recipient.receive_id_type,
1299            body: CreateMessageBody {
1300                receive_id: recipient.receive_id,
1301                msg_type: msg_type.to_string(),
1302                content,
1303                uuid: None,
1304            },
1305        }
1306    }
1307
1308    fn receive_id_type(&self) -> ReceiveIdType {
1309        self.receive_id_type
1310    }
1311}
1312
1313#[cfg(feature = "im")]
1314impl From<HelperMessageBody> for CreateMessageBody {
1315    fn from(value: HelperMessageBody) -> Self {
1316        value.body
1317    }
1318}
1319
1320#[cfg(feature = "im")]
1321#[derive(Debug, Clone)]
1322struct HelperReplyBody {
1323    body: ReplyMessageBody,
1324    message_id: String,
1325}
1326
1327#[cfg(feature = "im")]
1328impl HelperReplyBody {
1329    fn new(target: ReplyTarget, msg_type: &str, content: String) -> Self {
1330        Self {
1331            message_id: target.message_id,
1332            body: ReplyMessageBody {
1333                content,
1334                msg_type: msg_type.to_string(),
1335                reply_in_thread: Some(target.reply_in_thread),
1336                uuid: None,
1337            },
1338        }
1339    }
1340
1341    fn message_id(&self) -> String {
1342        self.message_id.clone()
1343    }
1344}
1345
1346#[cfg(feature = "im")]
1347impl From<HelperReplyBody> for ReplyMessageBody {
1348    fn from(value: HelperReplyBody) -> Self {
1349        value.body
1350    }
1351}
1352
1353#[cfg(feature = "im")]
1354fn infer_file_type(file_name: &str) -> String {
1355    std::path::Path::new(file_name)
1356        .extension()
1357        .and_then(|ext| ext.to_str())
1358        .map(|ext| ext.to_ascii_lowercase())
1359        .filter(|ext| !ext.is_empty())
1360        .unwrap_or_else(|| "stream".to_string())
1361}
1362
1363#[cfg(feature = "contact")]
1364fn find_unique_user_by_name(users: &[UserLookupItem], name: &str) -> SDKResult<UserLookupItem> {
1365    let name = name.trim();
1366    if name.is_empty() {
1367        return Err(validation_error("name", "name 不能为空"));
1368    }
1369
1370    let mut matches = users.iter().filter(|user| user.name == name).cloned();
1371
1372    let first = matches
1373        .next()
1374        .ok_or_else(|| business_error(format!("未找到用户: {name}")))?;
1375    if matches.next().is_some() {
1376        return Err(business_error(format!(
1377            "找到多个同名用户,请缩小范围: {name}"
1378        )));
1379    }
1380    Ok(first)
1381}
1382
1383#[cfg(feature = "im")]
1384fn find_unique_chat_by_name(chats: &[ChatLookupItem], name: &str) -> SDKResult<ChatLookupItem> {
1385    let name = name.trim();
1386    if name.is_empty() {
1387        return Err(validation_error("name", "name 不能为空"));
1388    }
1389
1390    let mut matches = chats.iter().filter(|chat| chat.name == name).cloned();
1391
1392    let first = matches
1393        .next()
1394        .ok_or_else(|| business_error(format!("未找到群聊: {name}")))?;
1395    if matches.next().is_some() {
1396        return Err(business_error(format!(
1397            "找到多个同名群聊,请缩小范围: {name}"
1398        )));
1399    }
1400    Ok(first)
1401}
1402
1403#[cfg(feature = "contact")]
1404/// 通讯录链式 helper 入口。
1405#[derive(Debug, Clone)]
1406pub struct ContactClient {
1407    config: Arc<Config>,
1408}
1409
1410#[cfg(feature = "contact")]
1411impl ContactClient {
1412    fn new(config: Arc<Config>) -> Self {
1413        Self { config }
1414    }
1415
1416    /// 返回底层共享配置。
1417    pub fn config(&self) -> &Config {
1418        &self.config
1419    }
1420
1421    /// 搜索用户并自动处理分页。
1422    pub async fn search_users_all(&self, query: impl AsRef<str>) -> SDKResult<Vec<UserLookupItem>> {
1423        let query = query.as_ref().trim().to_string();
1424        if query.is_empty() {
1425            return Err(validation_error("query", "query 不能为空"));
1426        }
1427
1428        let mut users = Vec::new();
1429        let mut page_token: Option<String> = None;
1430
1431        loop {
1432            let mut request = SearchUserRequest::new(self.config.as_ref().clone())
1433                .query(query.clone())
1434                .page_size(100);
1435            if let Some(token) = &page_token {
1436                request = request.page_token(token.clone());
1437            }
1438
1439            let response: UserLookupResponse = serde_json::from_value(request.execute().await?)
1440                .map_err(|e| validation_error("user_lookup_response", e.to_string().as_str()))?;
1441            users.extend(response.users);
1442
1443            if !response.has_more {
1444                break;
1445            }
1446            page_token = response.page_token;
1447        }
1448
1449        Ok(users)
1450    }
1451
1452    /// 按用户名唯一查找单个用户。
1453    pub async fn find_user_by_name(&self, name: &str) -> SDKResult<UserLookupItem> {
1454        let users = self.search_users_all(name).await?;
1455        find_unique_user_by_name(&users, name)
1456    }
1457
1458    /// 通过 open_id 获取用户详情。
1459    pub async fn get_user_by_open_id(&self, open_id: impl Into<String>) -> SDKResult<UserResponse> {
1460        GetUserRequest::new(self.config.as_ref().clone())
1461            .user_id(open_id)
1462            .user_id_type(ContactUserIdType::OpenId)
1463            .department_id_type(DepartmentIdType::OpenDepartmentId)
1464            .execute()
1465            .await
1466    }
1467}
1468
1469#[cfg(feature = "moments")]
1470/// Moments 链式 helper 入口。
1471#[derive(Debug, Clone)]
1472pub struct MomentsClient {
1473    config: Arc<Config>,
1474}
1475
1476#[cfg(feature = "moments")]
1477impl MomentsClient {
1478    fn new(config: Arc<Config>) -> Self {
1479        Self { config }
1480    }
1481
1482    /// 返回底层共享配置。
1483    pub fn config(&self) -> &Config {
1484        &self.config
1485    }
1486}
1487
1488#[cfg(test)]
1489#[allow(unused_imports)]
1490mod tests {
1491    use super::*;
1492
1493    fn create_test_config() -> Config {
1494        Config::builder()
1495            .app_id("test_app")
1496            .app_secret("test_secret")
1497            .build()
1498    }
1499
1500    #[test]
1501    fn test_communication_client_creation() {
1502        let config = create_test_config();
1503        let client = CommunicationClient::new(config);
1504        assert_eq!(client.config().app_id(), "test_app");
1505    }
1506
1507    #[test]
1508    fn test_communication_client_debug() {
1509        let config = create_test_config();
1510        let client = CommunicationClient::new(config);
1511        let debug_str = format!("{client:?}");
1512        assert!(debug_str.contains("CommunicationClient"));
1513    }
1514
1515    #[test]
1516    fn test_communication_client_clone() {
1517        let config = create_test_config();
1518        let client = CommunicationClient::new(config);
1519        let cloned = client.clone();
1520        assert_eq!(cloned.config().app_id(), "test_app");
1521    }
1522
1523    #[cfg(feature = "im")]
1524    #[test]
1525    fn test_im_client_config() {
1526        let config = create_test_config();
1527        let client = CommunicationClient::new(config);
1528        assert_eq!(client.im.config().app_id(), "test_app");
1529    }
1530
1531    #[cfg(feature = "im")]
1532    #[test]
1533    fn test_message_recipient_constructors() {
1534        assert_eq!(
1535            MessageRecipient::open_id("ou_xxx"),
1536            MessageRecipient::new("ou_xxx", ReceiveIdType::OpenId)
1537        );
1538        assert_eq!(
1539            MessageRecipient::chat_id("oc_xxx"),
1540            MessageRecipient::new("oc_xxx", ReceiveIdType::ChatId)
1541        );
1542    }
1543
1544    #[cfg(feature = "im")]
1545    #[test]
1546    fn test_post_message_serialization() {
1547        let content = PostMessage::zh_cn("周报", "本周已完成 3 项任务")
1548            .into_content()
1549            .expect("post content should serialize");
1550
1551        let value: serde_json::Value =
1552            serde_json::from_str(&content).expect("content should be valid json");
1553        assert_eq!(value["post"]["zh_cn"]["title"], "周报");
1554        assert_eq!(
1555            value["post"]["zh_cn"]["content"][0][0]["text"],
1556            "本周已完成 3 项任务"
1557        );
1558    }
1559
1560    #[cfg(feature = "im")]
1561    #[test]
1562    fn test_reply_target_constructors() {
1563        assert_eq!(
1564            ReplyTarget::direct("om_xxx"),
1565            ReplyTarget {
1566                message_id: "om_xxx".to_string(),
1567                reply_in_thread: false,
1568            }
1569        );
1570        assert_eq!(
1571            ReplyTarget::in_thread("om_xxx"),
1572            ReplyTarget {
1573                message_id: "om_xxx".to_string(),
1574                reply_in_thread: true,
1575            }
1576        );
1577    }
1578
1579    #[cfg(feature = "im")]
1580    #[test]
1581    fn test_media_image_upload_defaults() {
1582        let upload = MediaImageUpload::new(vec![1, 2, 3]).file_name("image.png");
1583        assert_eq!(upload.image_type, ImageType::Message);
1584        assert_eq!(upload.file_name.as_deref(), Some("image.png"));
1585        assert_eq!(upload.bytes, vec![1, 2, 3]);
1586    }
1587
1588    #[cfg(feature = "im")]
1589    #[test]
1590    fn test_media_file_upload_infers_type() {
1591        let upload = MediaFileUpload::new("report.pdf", vec![1, 2, 3]).duration(15);
1592        assert_eq!(upload.file_type, "pdf");
1593        assert_eq!(upload.file_name, "report.pdf");
1594        assert_eq!(upload.duration, Some(15));
1595    }
1596
1597    #[cfg(feature = "im")]
1598    #[test]
1599    fn test_build_text_message_body() {
1600        let body = ImClient::build_text_body(MessageRecipient::open_id("ou_xxx"), "hello".into())
1601            .expect("text body should build");
1602        let request_body: CreateMessageBody = body.into();
1603        assert_eq!(request_body.msg_type, "text");
1604        assert_eq!(request_body.receive_id, "ou_xxx");
1605        assert_eq!(request_body.content, r#"{"text":"hello"}"#);
1606    }
1607
1608    #[cfg(feature = "im")]
1609    #[test]
1610    fn test_build_post_message_body() {
1611        let body = ImClient::build_post_body(
1612            MessageRecipient::chat_id("oc_xxx"),
1613            PostMessage::zh_cn("项目播报", "今天完成发布"),
1614        )
1615        .expect("post body should build");
1616        let request_body: CreateMessageBody = body.into();
1617        let value: serde_json::Value =
1618            serde_json::from_str(&request_body.content).expect("content should be valid json");
1619
1620        assert_eq!(request_body.msg_type, "post");
1621        assert_eq!(request_body.receive_id, "oc_xxx");
1622        assert_eq!(value["post"]["zh_cn"]["title"], "项目播报");
1623    }
1624
1625    #[cfg(feature = "im")]
1626    #[test]
1627    fn test_build_media_message_body_for_image() {
1628        let body = ImClient::build_media_body(
1629            MessageRecipient::open_id("ou_xxx"),
1630            "image",
1631            serde_json::json!({ "image_key": "img_xxx" }).to_string(),
1632        )
1633        .expect("image body should build");
1634        let request_body: CreateMessageBody = body.into();
1635        assert_eq!(request_body.msg_type, "image");
1636        assert_eq!(request_body.content, r#"{"image_key":"img_xxx"}"#);
1637    }
1638
1639    #[cfg(feature = "im")]
1640    #[test]
1641    fn test_build_media_message_body_for_file() {
1642        let body = ImClient::build_media_body(
1643            MessageRecipient::chat_id("oc_xxx"),
1644            "file",
1645            serde_json::json!({ "file_key": "file_xxx" }).to_string(),
1646        )
1647        .expect("file body should build");
1648        let request_body: CreateMessageBody = body.into();
1649        assert_eq!(request_body.msg_type, "file");
1650        assert_eq!(request_body.receive_id, "oc_xxx");
1651        assert_eq!(request_body.content, r#"{"file_key":"file_xxx"}"#);
1652    }
1653
1654    #[cfg(feature = "im")]
1655    #[test]
1656    fn test_build_reply_text_message_body() {
1657        let body = ImClient::build_reply_text_body(ReplyTarget::direct("om_xxx"), "收到".into())
1658            .expect("reply text body should build");
1659        let request_body: ReplyMessageBody = body.into();
1660        assert_eq!(request_body.msg_type, "text");
1661        assert_eq!(request_body.reply_in_thread, Some(false));
1662        assert_eq!(request_body.content, r#"{"text":"收到"}"#);
1663    }
1664
1665    #[cfg(feature = "im")]
1666    #[test]
1667    fn test_build_reply_post_message_body() {
1668        let body = ImClient::build_reply_post_body(
1669            ReplyTarget::in_thread("om_xxx"),
1670            PostMessage::zh_cn("进展", "线程内同步"),
1671        )
1672        .expect("reply post body should build");
1673        let request_body: ReplyMessageBody = body.into();
1674        let value: serde_json::Value =
1675            serde_json::from_str(&request_body.content).expect("content should be valid json");
1676
1677        assert_eq!(request_body.msg_type, "post");
1678        assert_eq!(request_body.reply_in_thread, Some(true));
1679        assert_eq!(value["post"]["zh_cn"]["title"], "进展");
1680    }
1681
1682    #[cfg(feature = "im")]
1683    #[tokio::test]
1684    async fn test_send_image_rejects_empty_key() {
1685        let client = CommunicationClient::new(create_test_config());
1686        let error = client
1687            .im
1688            .send_image(MessageRecipient::open_id("ou_xxx"), "")
1689            .await
1690            .expect_err("empty image_key should fail");
1691        assert!(error.to_string().contains("image_key"));
1692    }
1693
1694    #[cfg(feature = "im")]
1695    #[tokio::test]
1696    async fn test_send_file_rejects_empty_key() {
1697        let client = CommunicationClient::new(create_test_config());
1698        let error = client
1699            .im
1700            .send_file(MessageRecipient::chat_id("oc_xxx"), "")
1701            .await
1702            .expect_err("empty file_key should fail");
1703        assert!(error.to_string().contains("file_key"));
1704    }
1705
1706    #[cfg(feature = "im")]
1707    #[tokio::test]
1708    async fn test_upload_image_rejects_empty_bytes() {
1709        let client = CommunicationClient::new(create_test_config());
1710        let error = client
1711            .im
1712            .upload_image(MediaImageUpload::new(Vec::new()))
1713            .await
1714            .expect_err("empty image bytes should fail");
1715        assert!(error.to_string().contains("image"));
1716    }
1717
1718    #[cfg(feature = "contact")]
1719    #[test]
1720    fn test_user_lookup_response_deserializes() {
1721        let response: UserLookupResponse = serde_json::from_value(serde_json::json!({
1722            "has_more": true,
1723            "page_token": "token_1",
1724            "users": [
1725                {
1726                    "name": "zhangsan",
1727                    "open_id": "ou_xxx",
1728                    "user_id": "u_xxx",
1729                    "department_ids": ["od_1"]
1730                }
1731            ]
1732        }))
1733        .expect("user lookup response should deserialize");
1734
1735        assert!(response.has_more);
1736        assert_eq!(response.page_token.as_deref(), Some("token_1"));
1737        assert_eq!(response.users[0].name, "zhangsan");
1738        assert_eq!(response.users[0].open_id, "ou_xxx");
1739    }
1740
1741    #[cfg(feature = "contact")]
1742    #[test]
1743    fn test_find_unique_user_by_name_rejects_duplicates() {
1744        let users = vec![
1745            UserLookupItem {
1746                name: "zhangsan".to_string(),
1747                open_id: "ou_1".to_string(),
1748                user_id: None,
1749                department_ids: vec![],
1750            },
1751            UserLookupItem {
1752                name: "zhangsan".to_string(),
1753                open_id: "ou_2".to_string(),
1754                user_id: None,
1755                department_ids: vec![],
1756            },
1757        ];
1758
1759        let error =
1760            find_unique_user_by_name(&users, "zhangsan").expect_err("duplicate user should fail");
1761        assert!(error.to_string().contains("多个同名用户"));
1762    }
1763
1764    #[cfg(feature = "im")]
1765    #[test]
1766    fn test_chat_lookup_response_deserializes() {
1767        let response: ChatLookupResponse = serde_json::from_value(serde_json::json!({
1768            "has_more": false,
1769            "items": [
1770                {
1771                    "chat_id": "oc_xxx",
1772                    "name": "项目群",
1773                    "description": "研发群",
1774                    "owner_id": "ou_owner",
1775                    "owner_id_type": "open_id",
1776                    "external": false,
1777                    "tenant_key": "tenant_key",
1778                    "chat_status": "normal"
1779                }
1780            ]
1781        }))
1782        .expect("chat lookup response should deserialize");
1783
1784        assert!(!response.has_more);
1785        assert_eq!(response.items[0].chat_id, "oc_xxx");
1786        assert_eq!(response.items[0].name, "项目群");
1787    }
1788
1789    #[cfg(feature = "im")]
1790    #[test]
1791    fn test_find_unique_chat_by_name_rejects_duplicates() {
1792        let chats = vec![
1793            ChatLookupItem {
1794                chat_id: "oc_1".to_string(),
1795                name: "项目群".to_string(),
1796                description: None,
1797                owner_id: None,
1798                owner_id_type: None,
1799                external: false,
1800                tenant_key: None,
1801                chat_status: None,
1802            },
1803            ChatLookupItem {
1804                chat_id: "oc_2".to_string(),
1805                name: "项目群".to_string(),
1806                description: None,
1807                owner_id: None,
1808                owner_id_type: None,
1809                external: false,
1810                tenant_key: None,
1811                chat_status: None,
1812            },
1813        ];
1814
1815        let error =
1816            find_unique_chat_by_name(&chats, "项目群").expect_err("duplicate chat should fail");
1817        assert!(error.to_string().contains("多个同名群聊"));
1818    }
1819
1820    #[cfg(feature = "contact")]
1821    #[test]
1822    fn test_contact_client_config() {
1823        let config = create_test_config();
1824        let client = CommunicationClient::new(config);
1825        assert_eq!(client.contact.config().app_id(), "test_app");
1826    }
1827}