feat: 实现命令适配器和处理器,支持 CLI 和 WebSocket 输入,优化会话管理
This commit is contained in:
parent
1c1efcabf4
commit
aace4eaa14
@ -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<dyn std::error::Error>> {
|
||||
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;
|
||||
}
|
||||
|
||||
89
src/command/adapter.rs
Normal file
89
src/command/adapter.rs
Normal file
@ -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<Option<Command>, 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<String>) -> Self {
|
||||
Self::MissingArgument {
|
||||
expected: expected.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建解析错误
|
||||
pub fn parse_error(message: impl Into<String>) -> Self {
|
||||
Self::ParseError {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建不支持命令错误
|
||||
pub fn unsupported_command(command: impl Into<String>) -> 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<AdapterError> for CommandError {
|
||||
fn from(err: AdapterError) -> Self {
|
||||
CommandError::new("ADAPTER_ERROR", err.to_string())
|
||||
}
|
||||
}
|
||||
173
src/command/adapters/cli.rs
Normal file
173
src/command/adapters/cli.rs
Normal file
@ -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<Option<Command>, 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"));
|
||||
}
|
||||
}
|
||||
2
src/command/adapters/mod.rs
Normal file
2
src/command/adapters/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod cli;
|
||||
pub mod websocket;
|
||||
160
src/command/adapters/websocket.rs
Normal file
160
src/command/adapters/websocket.rs
Normal file
@ -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<Option<Command>, 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<WsOutbound>;
|
||||
|
||||
fn adapt(&self, response: CommandResponse) -> Vec<WsOutbound> {
|
||||
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 { .. }));
|
||||
}
|
||||
}
|
||||
82
src/command/context.rs
Normal file
82
src/command/context.rs
Normal file
@ -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<String>,
|
||||
/// 当前聊天ID
|
||||
pub chat_id: Option<String>,
|
||||
/// 发送者ID
|
||||
pub sender_id: String,
|
||||
/// 额外元数据
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl CommandContext {
|
||||
/// 创建新的命令上下文
|
||||
pub fn new(sender_id: impl Into<String>) -> 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<String>) -> Self {
|
||||
self.session_id = Some(session_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置聊天ID
|
||||
pub fn with_chat_id(mut self, chat_id: impl Into<String>) -> Self {
|
||||
self.chat_id = Some(chat_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// 添加元数据
|
||||
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.metadata.insert(key.into(), value.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 适配器上下文 - 用于输入解析
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdapterContext {
|
||||
/// 当前会话ID
|
||||
pub session_id: Option<String>,
|
||||
/// 当前聊天ID
|
||||
pub chat_id: Option<String>,
|
||||
/// 发送者ID
|
||||
pub sender_id: String,
|
||||
}
|
||||
|
||||
impl AdapterContext {
|
||||
/// 创建新的适配器上下文
|
||||
pub fn new(sender_id: impl Into<String>) -> 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<String>) -> Self {
|
||||
self.session_id = Some(session_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置聊天ID
|
||||
pub fn with_chat_id(mut self, chat_id: impl Into<String>) -> Self {
|
||||
self.chat_id = Some(chat_id.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
172
src/command/handler.rs
Normal file
172
src/command/handler.rs
Normal file
@ -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<CommandResponse, CommandError>;
|
||||
}
|
||||
|
||||
/// 命令路由器
|
||||
///
|
||||
/// 负责将命令分发到合适的处理器
|
||||
pub struct CommandRouter {
|
||||
handlers: Vec<Box<dyn CommandHandler>>,
|
||||
}
|
||||
|
||||
impl CommandRouter {
|
||||
/// 创建新的命令路由器
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
handlers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册命令处理器
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `handler` - 要注册的处理器
|
||||
pub fn register(&mut self, handler: Box<dyn CommandHandler>) {
|
||||
self.handlers.push(handler);
|
||||
}
|
||||
|
||||
/// 分发命令到合适的处理器
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cmd` - 要执行的命令
|
||||
/// * `ctx` - 命令执行上下文
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(CommandResponse)` - 命令执行成功
|
||||
/// * `Err(CommandError)` - 没有合适的处理器或执行失败
|
||||
pub async fn dispatch(
|
||||
&self,
|
||||
cmd: Command,
|
||||
ctx: CommandContext,
|
||||
) -> Result<CommandResponse, CommandError> {
|
||||
// 查找能处理此命令的处理器
|
||||
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<CommandResponse, CommandError> {
|
||||
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<CommandResponse, CommandError> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
1
src/command/handlers/mod.rs
Normal file
1
src/command/handlers/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod session;
|
||||
115
src/command/handlers/session.rs
Normal file
115
src/command/handlers/session.rs
Normal file
@ -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<CommandResponse, CommandError> {
|
||||
match cmd {
|
||||
Command::CreateSession { title } => handle_create_session(self, title, ctx).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理创建会话命令
|
||||
async fn handle_create_session(
|
||||
handler: &SessionCommandHandler,
|
||||
title: Option<String>,
|
||||
ctx: CommandContext,
|
||||
) -> Result<CommandResponse, CommandError> {
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
25
src/command/mod.rs
Normal file
25
src/command/mod.rs
Normal file
@ -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<String> },
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// 获取命令名称(用于日志和调试)
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Command::CreateSession { .. } => "create_session",
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/command/response.rs
Normal file
122
src/command/response.rs
Normal file
@ -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<serde_json::Value>,
|
||||
/// 响应消息列表
|
||||
pub messages: Vec<ResponseMessage>,
|
||||
/// 错误信息(如果有)
|
||||
pub error: Option<CommandError>,
|
||||
/// 元数据
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
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<String>) -> Self {
|
||||
self.messages.push(ResponseMessage {
|
||||
kind,
|
||||
content: content.into(),
|
||||
metadata: HashMap::new(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// 添加元数据
|
||||
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> 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<String, String>,
|
||||
}
|
||||
|
||||
/// 消息类型
|
||||
#[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<String>, message: impl Into<String>) -> 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 {}
|
||||
@ -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 } => {
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user