重构: 添加斜杠命令解析和执行功能
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 crate::protocol::{parse_inbound, WsInbound, WsOutbound};
|
||||||
|
|
||||||
use super::base::{Channel, ChannelError};
|
use super::base::{Channel, ChannelError};
|
||||||
|
use super::slash_command::parse_slash_command;
|
||||||
|
|
||||||
/// Generate a short ID (8 characters) from a UUID
|
/// Generate a short ID (8 characters) from a UUID
|
||||||
fn short_id() -> String {
|
fn short_id() -> String {
|
||||||
@ -121,6 +122,43 @@ impl CliChatChannel {
|
|||||||
|
|
||||||
let session_id = current_session_guard.clone().unwrap();
|
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
|
// Parse UnifiedSessionId to get chat_id and dialog_id
|
||||||
let (channel_name, chat_id_part, dialog_id_part) = UnifiedSessionId::parse(&session_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())))
|
.map(|sid| (sid.channel, sid.chat_id, Some(sid.dialog_id.clone())))
|
||||||
|
|||||||
@ -2,8 +2,10 @@ pub mod base;
|
|||||||
pub mod feishu;
|
pub mod feishu;
|
||||||
pub mod cli_chat;
|
pub mod cli_chat;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
|
pub mod slash_command;
|
||||||
|
|
||||||
pub use base::{Channel, ChannelError};
|
pub use base::{Channel, ChannelError};
|
||||||
pub use manager::ChannelManager;
|
pub use manager::ChannelManager;
|
||||||
pub use feishu::FeishuChannel;
|
pub use feishu::FeishuChannel;
|
||||||
pub use cli_chat::CliChatChannel;
|
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(|()| SessionEvent::HistoryCleared { session_id })
|
||||||
.map_err(|e| ChannelError::Other(e.to_string()))
|
.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;
|
let _ = reply_tx.send(result).await;
|
||||||
|
|||||||
@ -99,6 +99,8 @@ pub enum WsOutbound {
|
|||||||
HistoryCleared { session_id: String },
|
HistoryCleared { session_id: String },
|
||||||
#[serde(rename = "pong")]
|
#[serde(rename = "pong")]
|
||||||
Pong,
|
Pong,
|
||||||
|
#[serde(rename = "command_executed")]
|
||||||
|
CommandExecuted { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_inbound(raw: &str) -> Result<WsInbound, serde_json::Error> {
|
pub fn parse_inbound(raw: &str) -> Result<WsInbound, serde_json::Error> {
|
||||||
|
|||||||
@ -43,6 +43,18 @@ pub enum SessionCommand {
|
|||||||
ClearHistory {
|
ClearHistory {
|
||||||
session_id: UnifiedSessionId,
|
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 {
|
impl SessionCommand {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use super::session_id::UnifiedSessionId;
|
use super::session_id::UnifiedSessionId;
|
||||||
|
use super::session::SlashCommand;
|
||||||
|
|
||||||
/// Dialog information returned by SessionManager
|
/// Dialog information returned by SessionManager
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -49,6 +50,15 @@ pub enum SessionEvent {
|
|||||||
HistoryCleared {
|
HistoryCleared {
|
||||||
session_id: UnifiedSessionId,
|
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 occurred
|
||||||
Error {
|
Error {
|
||||||
code: String,
|
code: String,
|
||||||
|
|||||||
@ -7,5 +7,5 @@ pub mod session_id;
|
|||||||
pub use error::SessionError;
|
pub use error::SessionError;
|
||||||
pub use commands::SessionCommand;
|
pub use commands::SessionCommand;
|
||||||
pub use events::{SessionEvent, DialogInfo};
|
pub use events::{SessionEvent, DialogInfo};
|
||||||
pub use session::{Session, SessionManager};
|
pub use session::{Session, SessionManager, SlashCommand, SLASH_COMMANDS};
|
||||||
pub use session_id::UnifiedSessionId;
|
pub use session_id::UnifiedSessionId;
|
||||||
|
|||||||
@ -204,35 +204,33 @@ fn default_tools() -> ToolRegistry {
|
|||||||
registry
|
registry
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
/// 斜杠命令定义
|
||||||
enum InChatCommand {
|
#[derive(Debug, Clone)]
|
||||||
FreshConversation,
|
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> {
|
impl SlashCommand {
|
||||||
match content.trim() {
|
/// 检查给定内容是否匹配此命令
|
||||||
"/new" | "/reset" => Some(InChatCommand::FreshConversation),
|
pub fn matches(&self, content: &str) -> bool {
|
||||||
_ => None,
|
let trimmed = content.trim();
|
||||||
|
self.aliases.iter().any(|&alias| trimmed == alias || trimmed.starts_with(&format!("{} ", alias)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle in-chat commands like /reset
|
/// Session 支持的斜杠命令列表
|
||||||
/// Returns Some(new_dialog_id) if FreshConversation was triggered
|
pub static SLASH_COMMANDS: &[SlashCommand] = &[
|
||||||
pub(crate) fn handle_in_chat_command(
|
SlashCommand {
|
||||||
session: &mut Session,
|
name: "reset",
|
||||||
content: &str,
|
description: "Start a fresh conversation (archives current dialog)",
|
||||||
) -> Result<Option<String>, AgentError> {
|
aliases: &["/reset", "/new"],
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SessionManager {
|
impl SessionManager {
|
||||||
pub fn new(session_ttl_hours: u64, provider_config: LLMProviderConfig) -> Result<Self, AgentError> {
|
pub fn new(session_ttl_hours: u64, provider_config: LLMProviderConfig) -> Result<Self, AgentError> {
|
||||||
@ -257,6 +255,44 @@ impl SessionManager {
|
|||||||
self.tools.clone()
|
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> {
|
pub fn store(&self) -> Arc<SessionStore> {
|
||||||
self.store.clone()
|
self.store.clone()
|
||||||
}
|
}
|
||||||
@ -554,31 +590,6 @@ impl SessionManager {
|
|||||||
let response: String = {
|
let response: String = {
|
||||||
let mut session_guard = session.lock().await;
|
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 持久化记录存在
|
||||||
session_guard.ensure_persistent_session()?;
|
session_guard.ensure_persistent_session()?;
|
||||||
|
|
||||||
@ -611,12 +622,7 @@ impl SessionManager {
|
|||||||
session_guard.append_message(msg)?;
|
session_guard.append_message(msg)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是 FreshConversation 命令,返回命令消息
|
result.final_response.content
|
||||||
if fresh_started {
|
|
||||||
"Starting a fresh conversation...".to_string()
|
|
||||||
} else {
|
|
||||||
result.final_response.content
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@ -660,12 +666,4 @@ mod tests {
|
|||||||
token_limit: 4096,
|
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