重构: 添加斜杠命令解析和执行功能

This commit is contained in:
xiaoxixi 2026-04-26 21:51:24 +08:00
parent 38425e23f6
commit 0c356e7ac4
9 changed files with 180 additions and 64 deletions

View File

@ -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())))

View File

@ -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};

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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<WsInbound, serde_json::Error> {

View File

@ -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<UnifiedSessionId>,
},
}
impl SessionCommand {

View File

@ -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<SlashCommand>,
},
/// Slash command executed successfully
SlashCommandExecuted {
new_session_id: Option<UnifiedSessionId>,
message: String,
},
/// Error occurred
Error {
code: String,

View File

@ -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;

View File

@ -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<InChatCommand> {
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<Option<String>, 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<Self, AgentError> {
@ -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<UnifiedSessionId>, 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<SessionStore> {
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);
}
}