feat: 添加会话管理命令,支持列出和加载会话功能

This commit is contained in:
ooodc 2026-05-14 23:35:34 +08:00
parent 5eb9a26843
commit e005d06a9b
10 changed files with 135 additions and 5 deletions

View File

@ -12,6 +12,8 @@ pub enum InputCommand {
Exit,
New(Option<String>),
Save(Option<String>),
Sessions,
Use(String),
}
pub struct InputHandler {
@ -70,6 +72,8 @@ impl InputHandler {
"/quit" | "/exit" | "/q" => Some(InputCommand::Exit),
"/new" => Some(InputCommand::New(arg.map(ToOwned::to_owned))),
"/save" => Some(InputCommand::Save(arg.map(ToOwned::to_owned))),
"/sessions" | "/list" => Some(InputCommand::Sessions),
"/use" => arg.map(|value| InputCommand::Use(value.to_string())),
_ => None,
}
}
@ -124,6 +128,19 @@ mod tests {
handler.handle_special_commands("/save ./debug/session.md"),
Some(InputCommand::Save(Some("./debug/session.md".to_string())))
);
assert_eq!(
handler.handle_special_commands("/list"),
Some(InputCommand::Sessions)
);
assert_eq!(
handler.handle_special_commands("/sessions"),
Some(InputCommand::Sessions)
);
assert_eq!(
handler.handle_special_commands("/use abc123"),
Some(InputCommand::Use("abc123".to_string()))
);
assert_eq!(handler.handle_special_commands("/unknown"), None);
assert_eq!(handler.handle_special_commands("/use"), None);
}
}

View File

@ -149,6 +149,63 @@ pub async fn run(gateway_url: &str) -> Result<(), Box<dyn std::error::Error>> {
}
continue;
}
InputEvent::Command(InputCommand::Sessions) => {
// 使用 CliInputAdapter 构建 Command
let adapter = CliInputAdapter::new();
let ctx = AdapterContext::new("cli")
.with_session_id(current_session_id.as_deref().unwrap_or(""));
// 解析为 Command
match adapter.try_parse("/list", ctx) {
Ok(Some(command)) => {
// 序列化为 JSON
let json = serde_json::to_string(&command).unwrap_or_default();
// 通过 Command 消息发送
let inbound = WsInbound::Command { payload: json };
if let Ok(text) = serialize_inbound(&inbound) {
let _ = sender.send(Message::Text(text.into())).await;
}
}
Ok(None) => {
tracing::warn!("Failed to parse /list command");
}
Err(e) => {
tracing::error!(error = %e, "Error parsing /list command");
}
}
continue;
}
InputEvent::Command(InputCommand::Use(session_id)) => {
// 使用 CliInputAdapter 构建 Command
let adapter = CliInputAdapter::new();
let ctx = AdapterContext::new("cli")
.with_session_id(current_session_id.as_deref().unwrap_or(""));
// 构建输入字符串
let input_str = format!("/use {}", session_id);
// 解析为 Command
match adapter.try_parse(&input_str, ctx) {
Ok(Some(command)) => {
// 序列化为 JSON
let json = serde_json::to_string(&command).unwrap_or_default();
// 通过 Command 消息发送
let inbound = WsInbound::Command { payload: json };
if let Ok(text) = serialize_inbound(&inbound) {
let _ = sender.send(Message::Text(text.into())).await;
}
// 更新当前会话 ID
current_session_id = Some(session_id.clone());
}
Ok(None) => {
tracing::warn!("Failed to parse /use command");
}
Err(e) => {
tracing::error!(error = %e, "Error parsing /use command");
}
}
continue;
}
InputEvent::Message(msg) => {
let inbound = WsInbound::Message {
content: msg.content,

View File

@ -65,6 +65,27 @@ impl InputAdapter for ChannelInputAdapter {
return Ok(Some(Command::SaveSession { filepath, include_all }));
}
// 解析 /list 命令
if trimmed == "/list" {
return Ok(Some(Command::ListSessions {
include_archived: false,
}));
}
if trimmed == "/list all" {
return Ok(Some(Command::ListSessions {
include_archived: true,
}));
}
// 解析 /use 命令
if let Some(session_id) = trimmed.strip_prefix("/use ") {
let session_id = session_id.trim();
return Ok(Some(Command::LoadSession {
session_id: session_id.to_string(),
}));
}
// 不是命令,返回 None
Ok(None)
}

View File

@ -66,6 +66,27 @@ impl InputAdapter for CliInputAdapter {
return Ok(Some(Command::SaveSession { filepath, include_all }));
}
// 解析 /list 命令
if trimmed == "/list" {
return Ok(Some(Command::ListSessions {
include_archived: false,
}));
}
if trimmed == "/list all" {
return Ok(Some(Command::ListSessions {
include_archived: true,
}));
}
// 解析 /use 命令
if let Some(session_id) = trimmed.strip_prefix("/use ") {
let session_id = session_id.trim();
return Ok(Some(Command::LoadSession {
session_id: session_id.to_string(),
}));
}
// 不是命令,返回 None
Ok(None)
}

View File

@ -1,2 +1,3 @@
pub mod save_session;
pub mod session;
pub mod session_query;

View File

@ -36,6 +36,7 @@ impl CommandHandler for SessionCommandHandler {
match cmd {
Command::CreateSession { title } => handle_create_session(self, title, ctx).await,
Command::SaveSession { .. } => unreachable!("SaveSession should be handled by SaveSessionCommandHandler"),
_ => unreachable!("Other commands should be handled by other handlers"),
}
}
}

View File

@ -117,7 +117,7 @@ mod tests {
async fn test_list_sessions_empty() {
let service = create_test_service();
let handler = SessionQueryCommandHandler::new(service);
let ctx = CommandContext::new("test");
let ctx = CommandContext::new("test", "test");
let cmd = Command::ListSessions {
include_archived: false,
};
@ -138,7 +138,7 @@ mod tests {
// 创建一些会话
service.create(Some("test session")).unwrap();
let ctx = CommandContext::new("test");
let ctx = CommandContext::new("test", "test");
let cmd = Command::ListSessions {
include_archived: false,
};
@ -155,7 +155,7 @@ mod tests {
async fn test_load_session_not_found() {
let service = create_test_service();
let handler = SessionQueryCommandHandler::new(service);
let ctx = CommandContext::new("test");
let ctx = CommandContext::new("test", "test");
let cmd = Command::LoadSession {
session_id: "nonexistent".to_string(),
};
@ -173,7 +173,7 @@ mod tests {
// 创建会话
let record = service.create(Some("test session")).unwrap();
let ctx = CommandContext::new("test");
let ctx = CommandContext::new("test", "test");
let cmd = Command::LoadSession {
session_id: record.id.clone(),
};

View File

@ -18,6 +18,10 @@ pub enum Command {
filepath: Option<String>,
include_all: bool,
},
/// 列出会话
ListSessions { include_archived: bool },
/// 加载指定会话
LoadSession { session_id: String },
}
impl Command {
@ -26,6 +30,8 @@ impl Command {
match self {
Command::CreateSession { .. } => "create_session",
Command::SaveSession { .. } => "save_session",
Command::ListSessions { .. } => "list_sessions",
Command::LoadSession { .. } => "load_session",
}
}
}

View File

@ -9,6 +9,7 @@ use crate::command::adapters::channel::ChannelInputAdapter;
use crate::command::handler::CommandRouter;
use crate::command::handlers::save_session::SaveSessionCommandHandler;
use crate::command::handlers::session::SessionCommandHandler;
use crate::command::handlers::session_query::SessionQueryCommandHandler;
use crate::config::LLMProviderConfig;
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
use crate::skills::SkillPromptProvider;
@ -36,7 +37,10 @@ impl InboundProcessor {
// 注册 Session 处理器
let cli_sessions = session_manager.cli_sessions();
command_router.register(Box::new(SessionCommandHandler::new(cli_sessions)));
command_router.register(Box::new(SessionCommandHandler::new(cli_sessions.clone())));
// 注册 session_query 处理器
command_router.register(Box::new(SessionQueryCommandHandler::new(cli_sessions)));
// 注册 save_session 处理器
let store = session_manager.store();

View File

@ -7,6 +7,7 @@ use crate::command::context::CommandContext;
use crate::command::handler::CommandRouter;
use crate::command::handlers::save_session::SaveSessionCommandHandler;
use crate::command::handlers::session::SessionCommandHandler;
use crate::command::handlers::session_query::SessionQueryCommandHandler;
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
use crate::protocol::{WsInbound, WsOutbound, parse_inbound, serialize_outbound};
use crate::skills::SkillPromptProvider;
@ -221,6 +222,7 @@ async fn handle_inbound(
let mut router = CommandRouter::new();
router.register(Box::new(SessionCommandHandler::new(cli_sessions.clone())));
router.register(Box::new(SessionQueryCommandHandler::new(cli_sessions)));
router.register(Box::new(SaveSessionCommandHandler::new(
store,
system_prompt_provider,