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::SaveTopic { filepath: None, include_subagents: false, })); } if let Some(args) = trimmed.strip_prefix("/save ") { let args = args.trim(); let parts: Vec<&str> = args.split_whitespace().collect(); // 解析参数 let mut include_subagents = false; let mut filepath = None; for part in parts { if part == "+sub" { include_subagents = true; } else if !part.is_empty() { // 非特殊参数视为文件路径 filepath = Some(part.to_string()); } } return Ok(Some(Command::SaveTopic { filepath, include_subagents, })); } // 解析 /save-session 命令 - 保存整个会话 if trimmed == "/save-session" { return Ok(Some(Command::SaveSession { filepath: None, include_all: false, include_subagents: false, })); } if let Some(args) = trimmed.strip_prefix("/save-session ") { let args = args.trim(); let parts: Vec<&str> = args.split_whitespace().collect(); // 解析参数 let mut include_all = false; let mut include_subagents = false; let mut filepath = None; for part in parts { if part == "all" { include_all = true; } else if part == "+sub" { include_subagents = true; } else if !part.is_empty() { // 非特殊参数视为文件路径 filepath = Some(part.to_string()); } } return Ok(Some(Command::SaveSession { filepath, include_all, include_subagents, })); } // 解析 /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)); } // 解析 /help 命令 - 显示所有支持的命令 if trimmed == "/help" { return Ok(Some(Command::Help)); } // 不是命令,返回 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_topic_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::SaveTopic { filepath: None, .. })); } #[test] fn test_cli_input_adapter_save_topic_with_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save ./debug/topic.md", ctx).unwrap(); assert!(result.is_some()); let cmd = result.unwrap(); assert!(matches!( cmd, Command::SaveTopic { filepath: Some(ref p), .. } if p == "./debug/topic.md" )); } #[test] fn test_cli_input_adapter_save_session_without_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save-session", 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_session_with_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save-session ./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_session_all() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save-session 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_session_all_with_path() { let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("test"); let result = adapter.try_parse("/save-session 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, "Topic saved to: topic.md") .with_metadata("filepath", "topic.md"); let output = adapter.adapt(response); assert!(output.contains("Topic saved to: topic.md")); } }