/// Unified session identifier composed of channel, chat_id, and dialog_id /// /// Format: `channel:chat_id:dialog_id` /// /// Examples: /// - CLI: `"cli_chat:sid_abc123:dialog_xyz"` /// - Feishu: `"feishu:oc_123456:dialog_xyz"` /// /// For simple cases where only one dialog exists per chat: /// - `dialog_id` defaults to `"default"` use serde::{Deserialize, Serialize}; pub const DEFAULT_DIALOG_ID: &str = "default"; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct UnifiedSessionId { pub channel: String, pub chat_id: String, pub dialog_id: String, } impl UnifiedSessionId { /// Create a new UnifiedSessionId pub fn new(channel: impl Into, chat_id: impl Into, dialog_id: impl Into) -> Self { Self { channel: channel.into(), chat_id: chat_id.into(), dialog_id: dialog_id.into(), } } /// Create with default dialog_id ("default") pub fn with_default_dialog(channel: impl Into, chat_id: impl Into) -> Self { Self { channel: channel.into(), chat_id: chat_id.into(), dialog_id: DEFAULT_DIALOG_ID.to_string(), } } /// Parse from string format "channel:chat_id:dialog_id" pub fn parse(s: &str) -> Option { let parts: Vec<&str> = s.split(':').collect(); if parts.len() != 3 { return None; } Some(Self { channel: parts[0].to_string(), chat_id: parts[1].to_string(), dialog_id: parts[2].to_string(), }) } /// Convert to string format "channel:chat_id:dialog_id" pub fn to_string(&self) -> String { format!("{}:{}:{}", self.channel, self.chat_id, self.dialog_id) } /// Get the session key without dialog_id (channel:chat_id) /// This is used to group all dialogs within a chat pub fn chat_scope(&self) -> String { format!("{}:{}", self.channel, self.chat_id) } } impl std::fmt::Display for UnifiedSessionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_string()) } } // Note: No Deref implementation to avoid confusion between String and UnifiedSessionId #[cfg(test)] mod tests { use super::*; #[test] fn test_new() { let id = UnifiedSessionId::new("cli_chat", "sid123", "dialog456"); assert_eq!(id.channel, "cli_chat"); assert_eq!(id.chat_id, "sid123"); assert_eq!(id.dialog_id, "dialog456"); } #[test] fn test_with_default_dialog() { let id = UnifiedSessionId::with_default_dialog("feishu", "oc123"); assert_eq!(id.channel, "feishu"); assert_eq!(id.chat_id, "oc123"); assert_eq!(id.dialog_id, "default"); } #[test] fn test_parse() { let id = UnifiedSessionId::parse("cli_chat:sid123:dialog456").unwrap(); assert_eq!(id.channel, "cli_chat"); assert_eq!(id.chat_id, "sid123"); assert_eq!(id.dialog_id, "dialog456"); } #[test] fn test_parse_invalid() { assert!(UnifiedSessionId::parse("invalid").is_none()); assert!(UnifiedSessionId::parse("only:two").is_none()); } #[test] fn test_to_string() { let id = UnifiedSessionId::new("feishu", "oc123", "dialog789"); assert_eq!(id.to_string(), "feishu:oc123:dialog789"); } #[test] fn test_chat_scope() { let id = UnifiedSessionId::new("feishu", "oc123", "dialog789"); assert_eq!(id.chat_scope(), "feishu:oc123"); } }