use crate::command::adapter::{AdapterError, InputAdapter, OutputAdapter}; use crate::command::context::AdapterContext; use crate::command::response::{CommandResponse, MessageKind}; use crate::command::Command; /// CLI 输入适配器 /// /// 将 CLI 的 "/new title" 等输入格式转换为 Command pub struct CliInputAdapter; impl CliInputAdapter { /// 创建新的 CLI 输入适配器 pub fn new() -> Self { Self } } impl Default for CliInputAdapter { fn default() -> Self { Self::new() } } impl InputAdapter for CliInputAdapter { fn try_parse( &self, input: &str, _ctx: AdapterContext, ) -> Result, AdapterError> { let trimmed = input.trim(); // 解析 /new 命令 if trimmed == "/new" { return Ok(Some(Command::CreateSession { title: None })); } if let Some(title) = trimmed.strip_prefix("/new ") { let title = title.trim(); return Ok(Some(Command::CreateSession { title: Some(title.to_string()), })); } // 解析 /save 命令 if trimmed == "/save" { return Ok(Some(Command::SaveSession { filepath: None, include_all: false, })); } if let Some(args) = trimmed.strip_prefix("/save ") { let args = args.trim(); // 解析参数:可能是 "all"、路径、或 "all 路径" let (include_all, filepath) = if args == "all" { // /save all - 保存全部消息 (true, None) } else if args.starts_with("all ") { // /save all - 保存全部消息到指定路径 let path = args[4..].trim(); (true, Some(path.to_string())) } else { // /save - 保存活跃消息到指定路径 (false, Some(args.to_string())) }; return Ok(Some(Command::SaveSession { filepath, include_all })); } // 解析 /list 命令 if trimmed == "/list" { return Ok(Some(Command::ListSessions { include_archived: false, })); } if trimmed == "/list all" { return Ok(Some(Command::ListSessions { include_archived: true, })); } // 解析 /use 命令 - 切换会话(支持 session_id 或序号) if let Some(session_id) = trimmed.strip_prefix("/use ") { let session_id = session_id.trim(); return Ok(Some(Command::SwitchSession { session_id: session_id.to_string(), })); } // 解析 /current 命令 - 获取当前会话信息 if trimmed == "/current" { return Ok(Some(Command::GetCurrentSession)); } // 不是命令,返回 None Ok(None) } } /// CLI 输出适配器 /// /// 将 CommandResponse 转换为 CLI 可读的字符串 pub struct CliOutputAdapter; impl CliOutputAdapter { /// 创建新的 CLI 输出适配器 pub fn new() -> Self { Self } } impl Default for CliOutputAdapter { fn default() -> Self { Self::new() } } impl OutputAdapter for CliOutputAdapter { type Output = String; fn adapt(&self, response: CommandResponse) -> String { if let Some(error) = response.error { return format!("Error [{}]: {}", error.code, error.message); } if !response.success { return "Error: Unknown error".to_string(); } let mut lines = Vec::new(); for msg in &response.messages { match msg.kind { MessageKind::Text | MessageKind::Notification => { lines.push(msg.content.clone()); } MessageKind::Error => { lines.push(format!("Error: {}", msg.content)); } _ => { // 其他类型在 CLI 中简单显示 lines.push(msg.content.clone()); } } } if lines.is_empty() { "Success".to_string() } else { lines.join("\n") } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_cli_input_adapter_new_without_title() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/new", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!(cmd, Command::CreateSession { title: None })); } #[test] fn test_cli_input_adapter_new_with_title() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/new my session", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!( cmd, Command::CreateSession { title: Some(ref t) } if t == "my session" )); } #[test] fn test_cli_input_adapter_not_command() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("hello world", ctx).unwrap(); assert!(result.is_none()); } #[test] fn test_cli_output_adapter_success() { let adapter = CliOutputAdapter::new(); let request_id = uuid::Uuid::new_v4(); let response = CommandResponse::success(request_id) .with_message(MessageKind::Notification, "Session created: abc123"); let output = adapter.adapt(response); assert!(output.contains("Session created: abc123")); } #[test] fn test_cli_output_adapter_error() { let adapter = CliOutputAdapter::new(); let request_id = uuid::Uuid::new_v4(); let response = CommandResponse::error( request_id, crate::command::response::CommandError::new("TEST_ERROR", "something failed"), ); let output = adapter.adapt(response); assert!(output.contains("Error [TEST_ERROR]")); assert!(output.contains("something failed")); } #[test] fn test_cli_input_adapter_save_without_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!(cmd, Command::SaveSession { filepath: None, include_all: false })); } #[test] fn test_cli_input_adapter_save_with_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save ./debug/session.md", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!( cmd, Command::SaveSession { filepath: Some(ref p), include_all: false, } if p == "./debug/session.md" )); } #[test] fn test_cli_input_adapter_save_all() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save all", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!(cmd, Command::SaveSession { filepath: None, include_all: true })); } #[test] fn test_cli_input_adapter_save_all_with_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save all ./debug/session.md", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!( cmd, Command::SaveSession { filepath: Some(ref p), include_all: true, } if p == "./debug/session.md" )); } #[test] fn test_cli_output_adapter_save_success() { let adapter = CliOutputAdapter::new(); let request_id = uuid::Uuid::new_v4(); let response = CommandResponse::success(request_id) .with_message(MessageKind::Notification, "Session saved to: session.md") .with_metadata("filepath", "session.md"); let output = adapter.adapt(response); assert!(output.contains("Session saved to: session.md")); } }