diff --git a/src/client/mod.rs b/src/client/mod.rs index a580c8a..e16aef0 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,8 @@ pub use crate::protocol::{WsInbound, WsOutbound, serialize_inbound, serialize_outbound}; +use crate::command::adapter::InputAdapter; +use crate::command::adapters::cli::CliInputAdapter; +use crate::command::context::AdapterContext; use futures_util::{SinkExt, StreamExt}; use tokio_tungstenite::{connect_async, tungstenite::Message}; @@ -143,9 +146,38 @@ pub async fn run(gateway_url: &str) -> Result<(), Box> { continue; } InputEvent::Command(InputCommand::New(title)) => { - let inbound = WsInbound::CreateSession { title }; - if let Ok(text) = serialize_inbound(&inbound) { - let _ = sender.send(Message::Text(text.into())).await; + // 使用新的命令层:通过 CliInputAdapter 构建 Command + let adapter = CliInputAdapter::new(); + let ctx = AdapterContext::new("cli") + .with_session_id(current_session_id.as_deref().unwrap_or("")); + + // 构建输入字符串 + let input = match title { + Some(t) => format!("/new {}", t), + None => "/new".to_string(), + }; + + // 解析为 Command + match adapter.try_parse(&input, ctx) { + Ok(Some(command)) => { + // 序列化为 JSON 通过 WebSocket 发送 + let json = serde_json::to_string(&command).unwrap_or_default(); + let inbound = WsInbound::UserInput { + content: json, + channel: None, + chat_id: current_session_id.clone(), + sender_id: None, + }; + if let Ok(text) = serialize_inbound(&inbound) { + let _ = sender.send(Message::Text(text.into())).await; + } + } + Ok(None) => { + tracing::warn!("Failed to parse /new command"); + } + Err(e) => { + tracing::error!(error = %e, "Error parsing /new command"); + } } continue; } diff --git a/src/command/adapter.rs b/src/command/adapter.rs new file mode 100644 index 0000000..e8a9686 --- /dev/null +++ b/src/command/adapter.rs @@ -0,0 +1,89 @@ +use crate::command::context::AdapterContext; +use crate::command::response::{CommandError, CommandResponse}; +use crate::command::Command; + +/// 输入适配器:将渠道特定输入转换为 Command +/// +/// 不同渠道(CLI、WebSocket、HTTP 等)实现此 trait +/// 将各自的输入格式统一转换为 Command +pub trait InputAdapter: Send + Sync { + /// 尝试将输入解析为 Command + /// + /// # Returns + /// - `Ok(Some(Command))`:成功解析为命令 + /// - `Ok(None)`:不是命令(如普通聊天消息) + /// - `Err(CommandError)`:解析错误(如缺少参数) + fn try_parse( + &self, + input: &str, + ctx: AdapterContext, + ) -> Result, AdapterError>; +} + +/// 输出适配器:将 CommandResponse 转换为渠道特定输出 +/// +/// 不同渠道(CLI、WebSocket、HTTP 等)实现此 trait +/// 将统一的 CommandResponse 转换为自己的输出格式 +pub trait OutputAdapter: Send + Sync { + /// 输出类型 + type Output; + + /// 将 CommandResponse 转换为渠道特定输出 + fn adapt(&self, response: CommandResponse) -> Self::Output; +} + +/// 适配器错误 +#[derive(Debug, Clone)] +pub enum AdapterError { + /// 缺少参数 + MissingArgument { expected: String }, + /// 解析错误 + ParseError { message: String }, + /// 不支持的命令 + UnsupportedCommand { command: String }, +} + +impl AdapterError { + /// 创建缺少参数错误 + pub fn missing_argument(expected: impl Into) -> Self { + Self::MissingArgument { + expected: expected.into(), + } + } + + /// 创建解析错误 + pub fn parse_error(message: impl Into) -> Self { + Self::ParseError { + message: message.into(), + } + } + + /// 创建不支持命令错误 + pub fn unsupported_command(command: impl Into) -> Self { + Self::UnsupportedCommand { + command: command.into(), + } + } +} + +impl std::fmt::Display for AdapterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AdapterError::MissingArgument { expected } => { + write!(f, "Missing argument: expected {}", expected) + } + AdapterError::ParseError { message } => write!(f, "Parse error: {}", message), + AdapterError::UnsupportedCommand { command } => { + write!(f, "Unsupported command: {}", command) + } + } + } +} + +impl std::error::Error for AdapterError {} + +impl From for CommandError { + fn from(err: AdapterError) -> Self { + CommandError::new("ADAPTER_ERROR", err.to_string()) + } +} diff --git a/src/command/adapters/cli.rs b/src/command/adapters/cli.rs new file mode 100644 index 0000000..4d7adc8 --- /dev/null +++ b/src/command/adapters/cli.rs @@ -0,0 +1,173 @@ +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()), + })); + } + + // 不是命令,返回 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")); + } +} diff --git a/src/command/adapters/mod.rs b/src/command/adapters/mod.rs new file mode 100644 index 0000000..3ea0590 --- /dev/null +++ b/src/command/adapters/mod.rs @@ -0,0 +1,2 @@ +pub mod cli; +pub mod websocket; diff --git a/src/command/adapters/websocket.rs b/src/command/adapters/websocket.rs new file mode 100644 index 0000000..d6c82d8 --- /dev/null +++ b/src/command/adapters/websocket.rs @@ -0,0 +1,160 @@ +use crate::command::adapter::{AdapterError, InputAdapter, OutputAdapter}; +use crate::command::context::AdapterContext; +use crate::command::response::{CommandResponse, MessageKind}; +use crate::command::Command; +use crate::protocol::WsOutbound; + +/// WebSocket 输入适配器 +/// +/// 将 WebSocket 的 JSON 输入直接反序列化为 Command +pub struct WebSocketInputAdapter; + +impl WebSocketInputAdapter { + /// 创建新的 WebSocket 输入适配器 + pub fn new() -> Self { + Self + } +} + +impl Default for WebSocketInputAdapter { + fn default() -> Self { + Self::new() + } +} + +impl InputAdapter for WebSocketInputAdapter { + fn try_parse( + &self, + input: &str, + _ctx: AdapterContext, + ) -> Result, AdapterError> { + // 尝试将 JSON 反序列化为 Command + // 如果失败,说明不是 Command 消息,返回 None + match serde_json::from_str(input) { + Ok(cmd) => Ok(Some(cmd)), + Err(_) => Ok(None), + } + } +} + +/// WebSocket 输出适配器 +/// +/// 将 CommandResponse 转换为 WsOutbound 消息列表 +pub struct WebSocketOutputAdapter; + +impl WebSocketOutputAdapter { + /// 创建新的 WebSocket 输出适配器 + pub fn new() -> Self { + Self + } +} + +impl Default for WebSocketOutputAdapter { + fn default() -> Self { + Self::new() + } +} + +impl OutputAdapter for WebSocketOutputAdapter { + type Output = Vec; + + fn adapt(&self, response: CommandResponse) -> Vec { + let mut outbounds = Vec::new(); + + // 如果出错,返回错误消息 + if let Some(error) = response.error { + outbounds.push(WsOutbound::Error { + code: error.code, + message: error.message, + }); + return outbounds; + } + + // 转换响应消息为 WsOutbound + for msg in &response.messages { + let outbound = match msg.kind { + MessageKind::Text => WsOutbound::AssistantResponse { + id: response.request_id.to_string(), + content: msg.content.clone(), + role: "assistant".to_string(), + }, + MessageKind::Notification => { + // 根据元数据判断具体类型 + if let Some(session_id) = response.metadata.get("session_id") { + WsOutbound::SessionCreated { + session_id: session_id.clone(), + title: msg.content.clone(), + } + } else { + // 默认通知 + WsOutbound::AssistantResponse { + id: response.request_id.to_string(), + content: msg.content.clone(), + role: "assistant".to_string(), + } + } + } + MessageKind::Error => WsOutbound::Error { + code: "RESPONSE_ERROR".to_string(), + message: msg.content.clone(), + }, + _ => WsOutbound::AssistantResponse { + id: response.request_id.to_string(), + content: msg.content.clone(), + role: "assistant".to_string(), + }, + }; + outbounds.push(outbound); + } + + outbounds + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_websocket_input_adapter_valid_command() { + let adapter = WebSocketInputAdapter::new(); + let ctx = AdapterContext::new("test"); + + let json = r#"{"type":"create_session","title":"my session"}"#; + let result = adapter.try_parse(json, 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_websocket_input_adapter_invalid_json() { + let adapter = WebSocketInputAdapter::new(); + let ctx = AdapterContext::new("test"); + + let json = "not a command"; + let result = adapter.try_parse(json, ctx).unwrap(); + + assert!(result.is_none()); + } + + #[test] + fn test_websocket_output_adapter_session_created() { + let adapter = WebSocketOutputAdapter::new(); + let request_id = uuid::Uuid::new_v4(); + let response = CommandResponse::success(request_id) + .with_message(MessageKind::Notification, "My Session") + .with_metadata("session_id", "abc123"); + + let outbounds = adapter.adapt(response); + + assert_eq!(outbounds.len(), 1); + assert!(matches!(outbounds[0], WsOutbound::SessionCreated { .. })); + } +} diff --git a/src/command/context.rs b/src/command/context.rs new file mode 100644 index 0000000..dbcc551 --- /dev/null +++ b/src/command/context.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; +use uuid::Uuid; + +/// 命令执行上下文 - 携带执行所需信息 +#[derive(Debug, Clone)] +pub struct CommandContext { + /// 唯一请求ID + pub request_id: Uuid, + /// 当前会话ID + pub session_id: Option, + /// 当前聊天ID + pub chat_id: Option, + /// 发送者ID + pub sender_id: String, + /// 额外元数据 + pub metadata: HashMap, +} + +impl CommandContext { + /// 创建新的命令上下文 + pub fn new(sender_id: impl Into) -> Self { + Self { + request_id: Uuid::new_v4(), + session_id: None, + chat_id: None, + sender_id: sender_id.into(), + metadata: HashMap::new(), + } + } + + /// 设置会话ID + pub fn with_session_id(mut self, session_id: impl Into) -> Self { + self.session_id = Some(session_id.into()); + self + } + + /// 设置聊天ID + pub fn with_chat_id(mut self, chat_id: impl Into) -> Self { + self.chat_id = Some(chat_id.into()); + self + } + + /// 添加元数据 + pub fn with_metadata(mut self, key: impl Into, value: impl Into) -> Self { + self.metadata.insert(key.into(), value.into()); + self + } +} + +/// 适配器上下文 - 用于输入解析 +#[derive(Debug, Clone)] +pub struct AdapterContext { + /// 当前会话ID + pub session_id: Option, + /// 当前聊天ID + pub chat_id: Option, + /// 发送者ID + pub sender_id: String, +} + +impl AdapterContext { + /// 创建新的适配器上下文 + pub fn new(sender_id: impl Into) -> Self { + Self { + session_id: None, + chat_id: None, + sender_id: sender_id.into(), + } + } + + /// 设置会话ID + pub fn with_session_id(mut self, session_id: impl Into) -> Self { + self.session_id = Some(session_id.into()); + self + } + + /// 设置聊天ID + pub fn with_chat_id(mut self, chat_id: impl Into) -> Self { + self.chat_id = Some(chat_id.into()); + self + } +} diff --git a/src/command/handler.rs b/src/command/handler.rs new file mode 100644 index 0000000..2c568c4 --- /dev/null +++ b/src/command/handler.rs @@ -0,0 +1,172 @@ +use crate::command::context::CommandContext; +use crate::command::response::{CommandError, CommandResponse}; +use crate::command::Command; +use async_trait::async_trait; + +/// 命令处理器 trait +/// +/// 实现此 trait 来处理特定类型的命令 +/// 处理器是渠道无关的,只关心 Command 本身 +#[async_trait] +pub trait CommandHandler: Send + Sync { + /// 是否可以处理此命令 + fn can_handle(&self, cmd: &Command) -> bool; + + /// 执行命令 + /// + /// # Arguments + /// * `cmd` - 要执行的命令 + /// * `ctx` - 命令执行上下文 + /// + /// # Returns + /// * `Ok(CommandResponse)` - 命令执行成功 + /// * `Err(CommandError)` - 命令执行失败 + async fn handle( + &self, + cmd: Command, + ctx: CommandContext, + ) -> Result; +} + +/// 命令路由器 +/// +/// 负责将命令分发到合适的处理器 +pub struct CommandRouter { + handlers: Vec>, +} + +impl CommandRouter { + /// 创建新的命令路由器 + pub fn new() -> Self { + Self { + handlers: Vec::new(), + } + } + + /// 注册命令处理器 + /// + /// # Arguments + /// * `handler` - 要注册的处理器 + pub fn register(&mut self, handler: Box) { + self.handlers.push(handler); + } + + /// 分发命令到合适的处理器 + /// + /// # Arguments + /// * `cmd` - 要执行的命令 + /// * `ctx` - 命令执行上下文 + /// + /// # Returns + /// * `Ok(CommandResponse)` - 命令执行成功 + /// * `Err(CommandError)` - 没有合适的处理器或执行失败 + pub async fn dispatch( + &self, + cmd: Command, + ctx: CommandContext, + ) -> Result { + // 查找能处理此命令的处理器 + for handler in &self.handlers { + if handler.can_handle(&cmd) { + return handler.handle(cmd, ctx).await; + } + } + + // 没有找到合适的处理器 + Err(CommandError::new( + "NO_HANDLER", + format!("No handler found for command: {}", cmd.name()), + )) + } + + /// 分发命令,返回响应(如果失败则返回错误响应) + /// + /// 与 `dispatch` 不同,此方法不会返回 Err, + /// 而是将错误包装在 CommandResponse 中 + pub async fn dispatch_with_response( + &self, + cmd: Command, + ctx: CommandContext, + ) -> CommandResponse { + let request_id = ctx.request_id; + match self.dispatch(cmd, ctx).await { + Ok(response) => response, + Err(err) => CommandResponse::error(request_id, err), + } + } +} + +impl Default for CommandRouter { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + struct TestHandler; + + #[async_trait] + impl CommandHandler for TestHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!(cmd, Command::CreateSession { .. }) + } + + async fn handle( + &self, + _cmd: Command, + ctx: CommandContext, + ) -> Result { + Ok(CommandResponse::success(ctx.request_id) + .with_message(crate::command::response::MessageKind::Notification, "ok")) + } + } + + struct NoOpHandler; + + #[async_trait] + impl CommandHandler for NoOpHandler { + fn can_handle(&self, _cmd: &Command) -> bool { + false + } + + async fn handle( + &self, + _cmd: Command, + _ctx: CommandContext, + ) -> Result { + unreachable!() + } + } + + #[tokio::test] + async fn test_router_finds_handler() { + let mut router = CommandRouter::new(); + router.register(Box::new(TestHandler)); + router.register(Box::new(NoOpHandler)); + + let ctx = CommandContext::new("test"); + let cmd = Command::CreateSession { title: None }; + + let result = router.dispatch(cmd, ctx).await; + + assert!(result.is_ok()); + let resp = result.unwrap(); + assert!(resp.success); + } + + #[tokio::test] + async fn test_router_no_handler() { + let router = CommandRouter::new(); + + let ctx = CommandContext::new("test"); + let cmd = Command::CreateSession { title: None }; + + let result = router.dispatch(cmd, ctx).await; + + assert!(result.is_err()); + } +} diff --git a/src/command/handlers/mod.rs b/src/command/handlers/mod.rs new file mode 100644 index 0000000..f52f1c4 --- /dev/null +++ b/src/command/handlers/mod.rs @@ -0,0 +1 @@ +pub mod session; diff --git a/src/command/handlers/session.rs b/src/command/handlers/session.rs new file mode 100644 index 0000000..f8c7cc7 --- /dev/null +++ b/src/command/handlers/session.rs @@ -0,0 +1,115 @@ +use crate::command::context::CommandContext; +use crate::command::handler::CommandHandler; +use crate::command::response::{CommandError, CommandResponse, MessageKind}; +use crate::command::Command; +use crate::gateway::cli_session::CliSessionService; +use async_trait::async_trait; + +/// 会话命令处理器 +/// +/// 处理与会话管理相关的命令 +pub struct SessionCommandHandler { + cli_sessions: CliSessionService, +} + +impl SessionCommandHandler { + /// 创建新的会话命令处理器 + /// + /// # Arguments + /// * `cli_sessions` - CLI 会话服务 + pub fn new(cli_sessions: CliSessionService) -> Self { + Self { cli_sessions } + } +} + +#[async_trait] +impl CommandHandler for SessionCommandHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!(cmd, Command::CreateSession { .. }) + } + + async fn handle( + &self, + cmd: Command, + ctx: CommandContext, + ) -> Result { + match cmd { + Command::CreateSession { title } => handle_create_session(self, title, ctx).await, + } + } +} + +/// 处理创建会话命令 +async fn handle_create_session( + handler: &SessionCommandHandler, + title: Option, + ctx: CommandContext, +) -> Result { + let record = handler + .cli_sessions + .create(title.as_deref()) + .map_err(|e| CommandError::new("CREATE_SESSION_ERROR", e.to_string()))?; + + Ok(CommandResponse::success(ctx.request_id) + .with_message(MessageKind::Notification, &record.title) + .with_metadata("session_id", &record.id) + .with_metadata("channel_name", &record.channel_name) + .with_metadata("message_count", &record.message_count.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::command::response::MessageKind; + use crate::storage::{SessionRecord, SessionStore}; + use std::sync::Arc; + + fn create_test_service() -> CliSessionService { + let store = Arc::new(SessionStore::in_memory().unwrap()); + CliSessionService::new(store) + } + + #[tokio::test] + async fn test_create_session_with_title() { + let service = create_test_service(); + let handler = SessionCommandHandler::new(service); + let ctx = CommandContext::new("test"); + let cmd = Command::CreateSession { + title: Some("my session".to_string()), + }; + + let result = handler.handle(cmd, ctx).await; + + assert!(result.is_ok()); + let resp = result.unwrap(); + assert!(resp.success); + assert_eq!(resp.messages.len(), 1); + assert_eq!(resp.messages[0].content, "my session"); + assert!(resp.metadata.contains_key("session_id")); + } + + #[tokio::test] + async fn test_create_session_without_title() { + let service = create_test_service(); + let handler = SessionCommandHandler::new(service); + let ctx = CommandContext::new("test"); + let cmd = Command::CreateSession { title: None }; + + let result = handler.handle(cmd, ctx).await; + + assert!(result.is_ok()); + let resp = result.unwrap(); + assert!(resp.success); + assert_eq!(resp.messages.len(), 1); + // 自动生成的标题 + assert!(!resp.messages[0].content.is_empty()); + } + + #[test] + fn test_can_handle() { + let service = create_test_service(); + let handler = SessionCommandHandler::new(service); + + assert!(handler.can_handle(&Command::CreateSession { title: None })); + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..2faa02b --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,25 @@ +pub mod adapter; +pub mod adapters; +pub mod context; +pub mod handler; +pub mod handlers; +pub mod response; + +use serde::{Deserialize, Serialize}; + +/// 统一命令枚举 - 与渠道无关 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Command { + /// 目前仅实现 /new 命令 + CreateSession { title: Option }, +} + +impl Command { + /// 获取命令名称(用于日志和调试) + pub fn name(&self) -> &'static str { + match self { + Command::CreateSession { .. } => "create_session", + } + } +} diff --git a/src/command/response.rs b/src/command/response.rs new file mode 100644 index 0000000..e7e4cfa --- /dev/null +++ b/src/command/response.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; +use uuid::Uuid; + +/// 命令响应 - 与渠道无关的统一响应格式 +#[derive(Debug, Clone)] +pub struct CommandResponse { + /// 对应的请求ID + pub request_id: Uuid, + /// 是否成功 + pub success: bool, + /// 响应数据(可选) + pub data: Option, + /// 响应消息列表 + pub messages: Vec, + /// 错误信息(如果有) + pub error: Option, + /// 元数据 + pub metadata: HashMap, +} + +impl CommandResponse { + /// 创建成功的响应 + pub fn success(request_id: Uuid) -> Self { + Self { + request_id, + success: true, + data: None, + messages: Vec::new(), + error: None, + metadata: HashMap::new(), + } + } + + /// 创建失败的响应 + pub fn error(request_id: Uuid, error: CommandError) -> Self { + Self { + request_id, + success: false, + data: None, + messages: Vec::new(), + error: Some(error), + metadata: HashMap::new(), + } + } + + /// 添加响应消息 + pub fn with_message(mut self, kind: MessageKind, content: impl Into) -> Self { + self.messages.push(ResponseMessage { + kind, + content: content.into(), + metadata: HashMap::new(), + }); + self + } + + /// 添加元数据 + pub fn with_metadata(mut self, key: impl Into, value: impl Into) -> Self { + self.metadata.insert(key.into(), value.into()); + self + } + + /// 设置响应数据 + pub fn with_data(mut self, data: serde_json::Value) -> Self { + self.data = Some(data); + self + } +} + +/// 响应消息 +#[derive(Debug, Clone)] +pub struct ResponseMessage { + /// 消息类型 + pub kind: MessageKind, + /// 消息内容 + pub content: String, + /// 消息元数据 + pub metadata: HashMap, +} + +/// 消息类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MessageKind { + /// 普通文本 + Text, + /// 通知 + Notification, + /// 错误 + Error, + /// 工具调用 + ToolCall, + /// 工具结果 + ToolResult, + /// 工具等待 + ToolPending, +} + +/// 命令错误 +#[derive(Debug, Clone)] +pub struct CommandError { + /// 错误代码 + pub code: String, + /// 错误消息 + pub message: String, +} + +impl CommandError { + /// 创建新的命令错误 + pub fn new(code: impl Into, message: impl Into) -> Self { + Self { + code: code.into(), + message: message.into(), + } + } +} + +impl std::fmt::Display for CommandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}", self.code, self.message) + } +} + +impl std::error::Error for CommandError {} diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index ca0be31..a886ceb 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -1,6 +1,11 @@ use super::GatewayState; use crate::agent::AgentError; use crate::bus::InboundMessage; +use crate::command::adapter::OutputAdapter; +use crate::command::adapters::websocket::{WebSocketInputAdapter, WebSocketOutputAdapter}; +use crate::command::context::CommandContext; +use crate::command::handler::CommandRouter; +use crate::command::handlers::session::SessionCommandHandler; use crate::protocol::{SessionSummary, WsInbound, WsOutbound, parse_inbound, serialize_outbound}; use axum::extract::State; use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; @@ -195,27 +200,44 @@ async fn handle_inbound( Ok(()) } WsInbound::CreateSession { title } => { - let record = state - .session_manager - .cli_sessions() - .create(title.as_deref())?; - *current_session_id = record.id.clone(); + // 使用新的命令层处理 + let _input_adapter = WebSocketInputAdapter::new(); + let output_adapter = WebSocketOutputAdapter::new(); + let cli_sessions = state.session_manager.cli_sessions(); + let handler = SessionCommandHandler::new(cli_sessions); + let router = { + let mut r = CommandRouter::new(); + r.register(Box::new(handler)); + r + }; - state - .channel_manager - .cli_channel() - .register_connection( - record.id.clone(), - runtime_session_id.to_string(), - sender.clone(), - ) - .await; - let _ = sender - .send(WsOutbound::SessionCreated { - session_id: record.id, - title: record.title, - }) - .await; + // 构建命令 + let cmd = crate::command::Command::CreateSession { title }; + let cmd_ctx = CommandContext::new("websocket") + .with_session_id(current_session_id.as_str()); + + // 执行命令 + let response = router.dispatch_with_response(cmd, cmd_ctx).await; + + // 适配输出 + let outbounds = output_adapter.adapt(response); + + // 处理响应 + for msg in outbounds { + if let WsOutbound::SessionCreated { session_id, title } = &msg { + *current_session_id = session_id.clone(); + state + .channel_manager + .cli_channel() + .register_connection( + session_id.clone(), + runtime_session_id.to_string(), + sender.clone(), + ) + .await; + } + let _ = sender.send(msg).await; + } Ok(()) } WsInbound::ListSessions { include_archived } => { diff --git a/src/lib.rs b/src/lib.rs index a7c4540..22bfc80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod bus; pub mod channels; pub mod cli; pub mod client; +pub mod command; pub mod config; pub mod domain; pub mod gateway;