microclaw_engine/internal/channels/
channel_adapter.rs1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6
7use crate::internal::channels::channel::ConversationKind;
8
9#[async_trait]
10pub trait ChannelAdapter: Send + Sync {
11 fn name(&self) -> &str;
13
14 fn chat_type_routes(&self) -> Vec<(&str, ConversationKind)>;
16
17 fn is_local_only(&self) -> bool {
19 false
20 }
21
22 fn allows_cross_chat(&self) -> bool {
24 true
25 }
26
27 async fn send_text(&self, external_chat_id: &str, text: &str) -> Result<(), String>;
29
30 fn text_chunk_limit_bytes(&self) -> Option<usize> {
33 None
34 }
35
36 fn text_chunk_delay(&self) -> Option<std::time::Duration> {
38 None
39 }
40
41 async fn send_text_chunk(
44 &self,
45 external_chat_id: &str,
46 text: &str,
47 _idempotency_key: &str,
48 ) -> Result<(), String> {
49 self.send_text(external_chat_id, text).await
50 }
51
52 async fn send_attachment(
54 &self,
55 _external_chat_id: &str,
56 _file_path: &Path,
57 _caption: Option<&str>,
58 ) -> Result<String, String> {
59 Err(format!("attachments not supported for {}", self.name()))
60 }
61}
62
63#[derive(Default)]
64pub struct ChannelRegistry {
65 adapters: HashMap<String, Arc<dyn ChannelAdapter>>,
66 type_to_channel: HashMap<String, String>,
68 type_to_conversation: HashMap<String, ConversationKind>,
70 output_guardrail: microclaw_core::redact::OutputGuardrailMode,
73}
74
75impl ChannelRegistry {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn set_output_guardrail(&mut self, mode: microclaw_core::redact::OutputGuardrailMode) {
82 self.output_guardrail = mode;
83 }
84
85 pub fn output_guardrail(&self) -> microclaw_core::redact::OutputGuardrailMode {
87 self.output_guardrail
88 }
89
90 pub fn register(&mut self, adapter: Arc<dyn ChannelAdapter>) {
91 let name = adapter.name().to_string();
92 for (chat_type, kind) in adapter.chat_type_routes() {
93 self.type_to_channel
94 .insert(chat_type.to_string(), name.clone());
95 self.type_to_conversation
96 .insert(chat_type.to_string(), kind);
97 }
98 self.adapters.insert(name, adapter);
99 }
100
101 pub fn get(&self, name: &str) -> Option<&Arc<dyn ChannelAdapter>> {
102 self.adapters.get(name)
103 }
104
105 pub fn resolve(
107 &self,
108 db_chat_type: &str,
109 ) -> Option<(&Arc<dyn ChannelAdapter>, ConversationKind)> {
110 let channel_name = self.type_to_channel.get(db_chat_type)?;
111 let adapter = self.adapters.get(channel_name)?;
112 let kind = self.type_to_conversation.get(db_chat_type)?;
113 Some((adapter, *kind))
114 }
115
116 pub fn resolve_routing(&self, db_chat_type: &str) -> Option<(&str, ConversationKind)> {
118 let channel_name = self.type_to_channel.get(db_chat_type)?;
119 let kind = self.type_to_conversation.get(db_chat_type)?;
120 Some((channel_name.as_str(), *kind))
121 }
122
123 pub fn has_any(&self) -> bool {
124 !self.adapters.is_empty()
125 }
126}