Compare commits
No commits in common. "73faaa95e44aebfb11d340a3182e96c7c226fa9a" and "1c1efcabf47cf98a1b8f00ee702c5f97f2677aee" have entirely different histories.
73faaa95e4
...
1c1efcabf4
@ -1,8 +1,5 @@
|
|||||||
pub use crate::protocol::{WsInbound, WsOutbound, serialize_inbound, serialize_outbound};
|
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 futures_util::{SinkExt, StreamExt};
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||||
|
|
||||||
@ -146,39 +143,10 @@ pub async fn run(gateway_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
InputEvent::Command(InputCommand::New(title)) => {
|
InputEvent::Command(InputCommand::New(title)) => {
|
||||||
// 使用新的命令层:通过 CliInputAdapter 构建 Command
|
let inbound = WsInbound::CreateSession { title };
|
||||||
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) {
|
if let Ok(text) = serialize_inbound(&inbound) {
|
||||||
let _ = sender.send(Message::Text(text.into())).await;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
InputEvent::Command(InputCommand::Sessions) => {
|
InputEvent::Command(InputCommand::Sessions) => {
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
pub mod cli;
|
|
||||||
pub mod websocket;
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
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 { .. }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
pub mod session;
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
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(crate) 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 }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
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,11 +1,6 @@
|
|||||||
use super::GatewayState;
|
use super::GatewayState;
|
||||||
use crate::agent::AgentError;
|
use crate::agent::AgentError;
|
||||||
use crate::bus::InboundMessage;
|
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 crate::protocol::{SessionSummary, WsInbound, WsOutbound, parse_inbound, serialize_outbound};
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
||||||
@ -200,44 +195,27 @@ async fn handle_inbound(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
WsInbound::CreateSession { title } => {
|
WsInbound::CreateSession { title } => {
|
||||||
// 使用新的命令层处理
|
let record = state
|
||||||
let _input_adapter = WebSocketInputAdapter::new();
|
.session_manager
|
||||||
let output_adapter = WebSocketOutputAdapter::new();
|
.cli_sessions()
|
||||||
let cli_sessions = state.session_manager.cli_sessions();
|
.create(title.as_deref())?;
|
||||||
let handler = SessionCommandHandler::new(cli_sessions);
|
*current_session_id = record.id.clone();
|
||||||
let router = {
|
|
||||||
let mut r = CommandRouter::new();
|
|
||||||
r.register(Box::new(handler));
|
|
||||||
r
|
|
||||||
};
|
|
||||||
|
|
||||||
// 构建命令
|
|
||||||
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
|
state
|
||||||
.channel_manager
|
.channel_manager
|
||||||
.cli_channel()
|
.cli_channel()
|
||||||
.register_connection(
|
.register_connection(
|
||||||
session_id.clone(),
|
record.id.clone(),
|
||||||
runtime_session_id.to_string(),
|
runtime_session_id.to_string(),
|
||||||
sender.clone(),
|
sender.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
let _ = sender
|
||||||
let _ = sender.send(msg).await;
|
.send(WsOutbound::SessionCreated {
|
||||||
}
|
session_id: record.id,
|
||||||
|
title: record.title,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
WsInbound::ListSessions { include_archived } => {
|
WsInbound::ListSessions { include_archived } => {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ pub mod bus;
|
|||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod command;
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user