重构: 添加斜杠命令解析和执行功能
This commit is contained in:
parent
38425e23f6
commit
0c356e7ac4
@ -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())))
|
||||
|
||||
@ -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};
|
||||
|
||||
44
src/channels/slash_command.rs
Normal file
44
src/channels/slash_command.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user