From 0c356e7ac43912553309a2f0b0808fca4b533176 Mon Sep 17 00:00:00 2001 From: xiaoxixi Date: Sun, 26 Apr 2026 21:51:24 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=96=9C=E6=9D=A0=E5=91=BD=E4=BB=A4=E8=A7=A3=E6=9E=90=E5=92=8C?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channels/cli_chat.rs | 38 +++++++++++ src/channels/mod.rs | 2 + src/channels/slash_command.rs | 44 ++++++++++++ src/gateway/mod.rs | 10 +++ src/protocol.rs | 2 + src/session/commands.rs | 12 ++++ src/session/events.rs | 10 +++ src/session/mod.rs | 2 +- src/session/session.rs | 124 +++++++++++++++++----------------- 9 files changed, 180 insertions(+), 64 deletions(-) create mode 100644 src/channels/slash_command.rs diff --git a/src/channels/cli_chat.rs b/src/channels/cli_chat.rs index 11ab491..100336f 100644 --- a/src/channels/cli_chat.rs +++ b/src/channels/cli_chat.rs @@ -8,6 +8,7 @@ use crate::session::{SessionCommand, SessionEvent, UnifiedSessionId}; use crate::protocol::{parse_inbound, WsInbound, WsOutbound}; use super::base::{Channel, ChannelError}; +use super::slash_command::parse_slash_command; /// Generate a short ID (8 characters) from a UUID fn short_id() -> String { @@ -121,6 +122,43 @@ impl CliChatChannel { let session_id = current_session_guard.clone().unwrap(); + // Check for slash command + if let Some((cmd_name, _)) = parse_slash_command(&content) { + // Send ExecuteSlashCommand via control plane + let (reply_tx, mut reply_rx) = mpsc::channel(1); + let unified_id = UnifiedSessionId::parse(&session_id); + bus.publish_control(ControlMessage { + op: SessionCommand::ExecuteSlashCommand { + command: cmd_name.to_string(), + channel: self.name().to_string(), + chat_id: chat_id.clone(), + current_session_id: unified_id, + }, + reply_tx, + }).await?; + + // Handle response + if let Some(result) = reply_rx.recv().await { + match result { + Ok(SessionEvent::SlashCommandExecuted { new_session_id, message }) => { + // Update current session if new one was created + if let Some(new_id) = new_session_id { + *current_session_guard = Some(new_id.to_string()); + } + let _ = client.sender.send(WsOutbound::CommandExecuted { message }).await; + } + Ok(SessionEvent::Error { code, message }) => { + let _ = client.sender.send(WsOutbound::Error { code, message }).await; + } + Err(e) => { + let _ = client.sender.send(WsOutbound::Error { code: "EXECUTION_ERROR".to_string(), message: e.to_string() }).await; + } + _ => {} + } + } + return Ok(()); + } + // Parse UnifiedSessionId to get chat_id and dialog_id let (channel_name, chat_id_part, dialog_id_part) = UnifiedSessionId::parse(&session_id) .map(|sid| (sid.channel, sid.chat_id, Some(sid.dialog_id.clone()))) diff --git a/src/channels/mod.rs b/src/channels/mod.rs index fe8bc75..3503db1 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -2,8 +2,10 @@ pub mod base; pub mod feishu; pub mod cli_chat; pub mod manager; +pub mod slash_command; pub use base::{Channel, ChannelError}; pub use manager::ChannelManager; pub use feishu::FeishuChannel; pub use cli_chat::CliChatChannel; +pub use slash_command::{parse_slash_command, command_matches}; diff --git a/src/channels/slash_command.rs b/src/channels/slash_command.rs new file mode 100644 index 0000000..33942a7 --- /dev/null +++ b/src/channels/slash_command.rs @@ -0,0 +1,44 @@ +/// 解析斜杠命令 +/// 返回 (command_name, args) 或 None +pub fn parse_slash_command(content: &str) -> Option<(&str, &str)> { + let trimmed = content.trim(); + if !trimmed.starts_with('/') { + return None; + } + let rest = &trimmed[1..]; + if let Some((name, args)) = rest.split_once(' ') { + Some((name, args.trim())) + } else { + Some((rest, "")) + } +} + +/// 检查内容是否匹配指定命令 +pub fn command_matches(content: &str, aliases: &[&str]) -> bool { + let trimmed = content.trim(); + aliases.iter().any(|&alias| trimmed == alias || trimmed.starts_with(&format!("{} ", alias))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_slash_command() { + assert_eq!(parse_slash_command("/reset"), Some(("reset", ""))); + assert_eq!(parse_slash_command("/reset arg"), Some(("reset", "arg"))); + assert_eq!(parse_slash_command("/new hello world"), Some(("new", "hello world"))); + assert_eq!(parse_slash_command("hello"), None); + assert_eq!(parse_slash_command("/"), Some(("", ""))); + } + + #[test] + fn test_command_matches() { + let aliases = &["/reset", "/new"]; + assert!(command_matches("/reset", aliases)); + assert!(command_matches("/new", aliases)); + assert!(command_matches("/reset arg", aliases)); + assert!(!command_matches("/help", aliases)); + assert!(!command_matches("reset", aliases)); + } +} diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 8c1719f..c755085 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -165,6 +165,16 @@ impl GatewayState { .map(|()| SessionEvent::HistoryCleared { session_id }) .map_err(|e| ChannelError::Other(e.to_string())) } + GetSlashCommands { channel: _, chat_id: _ } => { + let commands = session_manager.get_slash_commands().to_vec(); + Ok(SessionEvent::SlashCommandsList { commands }) + } + ExecuteSlashCommand { command, channel, chat_id, current_session_id } => { + session_manager.execute_slash_command(&command, &channel, &chat_id, current_session_id.as_ref()) + .await + .map(|(new_id, msg)| SessionEvent::SlashCommandExecuted { new_session_id: new_id, message: msg }) + .map_err(|e| ChannelError::Other(e.to_string())) + } }; let _ = reply_tx.send(result).await; diff --git a/src/protocol.rs b/src/protocol.rs index b301c0e..a0fc8b9 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -99,6 +99,8 @@ pub enum WsOutbound { HistoryCleared { session_id: String }, #[serde(rename = "pong")] Pong, + #[serde(rename = "command_executed")] + CommandExecuted { message: String }, } pub fn parse_inbound(raw: &str) -> Result { diff --git a/src/session/commands.rs b/src/session/commands.rs index d7d9e81..491bc9a 100644 --- a/src/session/commands.rs +++ b/src/session/commands.rs @@ -43,6 +43,18 @@ pub enum SessionCommand { ClearHistory { session_id: UnifiedSessionId, }, + /// Get list of available slash commands + GetSlashCommands { + channel: String, + chat_id: String, + }, + /// Execute a slash command + ExecuteSlashCommand { + command: String, + channel: String, + chat_id: String, + current_session_id: Option, + }, } impl SessionCommand { diff --git a/src/session/events.rs b/src/session/events.rs index a086f52..01d694e 100644 --- a/src/session/events.rs +++ b/src/session/events.rs @@ -1,4 +1,5 @@ use super::session_id::UnifiedSessionId; +use super::session::SlashCommand; /// Dialog information returned by SessionManager #[derive(Debug, Clone)] @@ -49,6 +50,15 @@ pub enum SessionEvent { HistoryCleared { session_id: UnifiedSessionId, }, + /// List of available slash commands + SlashCommandsList { + commands: Vec, + }, + /// Slash command executed successfully + SlashCommandExecuted { + new_session_id: Option, + message: String, + }, /// Error occurred Error { code: String, diff --git a/src/session/mod.rs b/src/session/mod.rs index 1871703..cdb268a 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -7,5 +7,5 @@ pub mod session_id; pub use error::SessionError; pub use commands::SessionCommand; pub use events::{SessionEvent, DialogInfo}; -pub use session::{Session, SessionManager}; +pub use session::{Session, SessionManager, SlashCommand, SLASH_COMMANDS}; pub use session_id::UnifiedSessionId; diff --git a/src/session/session.rs b/src/session/session.rs index d63133d..fa4ba01 100644 --- a/src/session/session.rs +++ b/src/session/session.rs @@ -204,35 +204,33 @@ fn default_tools() -> ToolRegistry { registry } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum InChatCommand { - FreshConversation, +/// 斜杠命令定义 +#[derive(Debug, Clone)] +pub struct SlashCommand { + /// 命令名称 + pub name: &'static str, + /// 命令描述 + pub description: &'static str, + /// 命令别名(触发词) + pub aliases: &'static [&'static str], } -fn parse_in_chat_command(content: &str) -> Option { - match content.trim() { - "/new" | "/reset" => Some(InChatCommand::FreshConversation), - _ => None, +impl SlashCommand { + /// 检查给定内容是否匹配此命令 + pub fn matches(&self, content: &str) -> bool { + let trimmed = content.trim(); + self.aliases.iter().any(|&alias| trimmed == alias || trimmed.starts_with(&format!("{} ", alias))) } } -/// Handle in-chat commands like /reset -/// Returns Some(new_dialog_id) if FreshConversation was triggered -pub(crate) fn handle_in_chat_command( - session: &mut Session, - content: &str, -) -> Result, AgentError> { - match parse_in_chat_command(content) { - Some(InChatCommand::FreshConversation) => { - // Archive the current session - session.archive()?; - - // Return new dialog_id to be created - Ok(Some(short_id())) - } - None => Ok(None), - } -} +/// Session 支持的斜杠命令列表 +pub static SLASH_COMMANDS: &[SlashCommand] = &[ + SlashCommand { + name: "reset", + description: "Start a fresh conversation (archives current dialog)", + aliases: &["/reset", "/new"], + }, +]; impl SessionManager { pub fn new(session_ttl_hours: u64, provider_config: LLMProviderConfig) -> Result { @@ -257,6 +255,44 @@ impl SessionManager { self.tools.clone() } + /// 获取所有可用的斜杠命令 + pub fn get_slash_commands(&self) -> &[SlashCommand] { + SLASH_COMMANDS + } + + /// 执行斜杠命令 + /// 返回 (新session_id, 响应消息) + pub async fn execute_slash_command( + &self, + command: &str, + channel: &str, + chat_id: &str, + current_session_id: Option<&UnifiedSessionId>, + ) -> Result<(Option, String), AgentError> { + // 查找匹配的 command + let cmd = SLASH_COMMANDS + .iter() + .find(|c| c.name == command) + .ok_or_else(|| AgentError::Other(format!("Unknown command: {}", command)))?; + + match cmd.name { + "reset" => { + // Archive current session if exists + if let Some(sid) = current_session_id { + let unified_str = sid.to_string(); + self.store + .archive_session(&unified_str) + .map_err(|e| AgentError::Other(format!("archive session error: {}", e)))?; + } + + // Create new dialog + let (new_id, _title) = self.create_session(channel, chat_id, None).await?; + Ok((Some(new_id), "Starting a fresh conversation...".to_string())) + } + _ => Err(AgentError::Other(format!("Command not implemented: {}", cmd.name))), + } + } + pub fn store(&self) -> Arc { self.store.clone() } @@ -554,31 +590,6 @@ impl SessionManager { let response: String = { let mut session_guard = session.lock().await; - // 检查是否是 FreshConversation 命令 - let fresh_conversation_result = handle_in_chat_command(&mut session_guard, content)?; - - let (session_to_use, fresh_started) = match fresh_conversation_result { - Some(_new_dialog_id) => { - // Archive the old session - session_guard.archive()?; - drop(session_guard); - - // Create new session for the new dialog - // This creates and registers the session - let (new_unified_id, _title) = self.create_session(channel, chat_id, None).await?; - // Get the newly created session - let new_session = self.get_or_create_session(&new_unified_id).await?; - (new_session, true) - } - None => { - drop(session_guard); // Drop before using same session - (Arc::clone(&session), false) - } - }; - - // 使用选定的 session 进行处理 - let mut session_guard = session_to_use.lock().await; - // 确保 session 持久化记录存在 session_guard.ensure_persistent_session()?; @@ -611,12 +622,7 @@ impl SessionManager { session_guard.append_message(msg)?; } - // 如果是 FreshConversation 命令,返回命令消息 - if fresh_started { - "Starting a fresh conversation...".to_string() - } else { - result.final_response.content - } + result.final_response.content }; #[cfg(debug_assertions)] @@ -660,12 +666,4 @@ mod tests { token_limit: 4096, } } - - #[test] - fn test_parse_in_chat_command_aliases() { - assert_eq!(parse_in_chat_command("/new"), Some(InChatCommand::FreshConversation)); - assert_eq!(parse_in_chat_command(" /reset \n"), Some(InChatCommand::FreshConversation)); - assert_eq!(parse_in_chat_command("/new planning"), None); - assert_eq!(parse_in_chat_command("please /reset"), None); - } }