Compare commits
5 Commits
6b5d45e3a5
...
428df8da59
| Author | SHA1 | Date | |
|---|---|---|---|
| 428df8da59 | |||
| 6a902b9ff9 | |||
| 831832664d | |||
| 3591822145 | |||
| 20f32a3f96 |
@ -40,26 +40,40 @@ impl InputAdapter for ChannelInputAdapter {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 /save 命令
|
// 解析 /save 命令 - 保存当前话题
|
||||||
if trimmed == "/save" {
|
if trimmed == "/save" {
|
||||||
|
return Ok(Some(Command::SaveTopic {
|
||||||
|
filepath: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(filepath) = trimmed.strip_prefix("/save ") {
|
||||||
|
let filepath = filepath.trim();
|
||||||
|
return Ok(Some(Command::SaveTopic {
|
||||||
|
filepath: Some(filepath.to_string()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 /save-session 命令 - 保存整个会话
|
||||||
|
if trimmed == "/save-session" {
|
||||||
return Ok(Some(Command::SaveSession {
|
return Ok(Some(Command::SaveSession {
|
||||||
filepath: None,
|
filepath: None,
|
||||||
include_all: false,
|
include_all: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(args) = trimmed.strip_prefix("/save ") {
|
if let Some(args) = trimmed.strip_prefix("/save-session ") {
|
||||||
let args = args.trim();
|
let args = args.trim();
|
||||||
// 解析参数:可能是 "all"、路径、或 "all 路径"
|
// 解析参数:可能是 "all"、路径、或 "all 路径"
|
||||||
let (include_all, filepath) = if args == "all" {
|
let (include_all, filepath) = if args == "all" {
|
||||||
// /save all - 保存全部消息
|
// /save-session all - 保存全部消息
|
||||||
(true, None)
|
(true, None)
|
||||||
} else if args.starts_with("all ") {
|
} else if args.starts_with("all ") {
|
||||||
// /save all <filepath> - 保存全部消息到指定路径
|
// /save-session all <filepath> - 保存全部消息到指定路径
|
||||||
let path = args[4..].trim();
|
let path = args[4..].trim();
|
||||||
(true, Some(path.to_string()))
|
(true, Some(path.to_string()))
|
||||||
} else {
|
} else {
|
||||||
// /save <filepath> - 保存活跃消息到指定路径
|
// /save-session <filepath> - 保存活跃消息到指定路径
|
||||||
(false, Some(args.to_string()))
|
(false, Some(args.to_string()))
|
||||||
};
|
};
|
||||||
return Ok(Some(Command::SaveSession { filepath, include_all }));
|
return Ok(Some(Command::SaveSession { filepath, include_all }));
|
||||||
@ -91,6 +105,11 @@ impl InputAdapter for ChannelInputAdapter {
|
|||||||
return Ok(Some(Command::GetCurrentSession));
|
return Ok(Some(Command::GetCurrentSession));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析 /help 命令 - 显示所有支持的命令
|
||||||
|
if trimmed == "/help" {
|
||||||
|
return Ok(Some(Command::Help));
|
||||||
|
}
|
||||||
|
|
||||||
// 不是命令,返回 None
|
// 不是命令,返回 None
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,26 +41,40 @@ impl InputAdapter for CliInputAdapter {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 /save 命令
|
// 解析 /save 命令 - 保存当前话题
|
||||||
if trimmed == "/save" {
|
if trimmed == "/save" {
|
||||||
|
return Ok(Some(Command::SaveTopic {
|
||||||
|
filepath: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(filepath) = trimmed.strip_prefix("/save ") {
|
||||||
|
let filepath = filepath.trim();
|
||||||
|
return Ok(Some(Command::SaveTopic {
|
||||||
|
filepath: Some(filepath.to_string()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 /save-session 命令 - 保存整个会话
|
||||||
|
if trimmed == "/save-session" {
|
||||||
return Ok(Some(Command::SaveSession {
|
return Ok(Some(Command::SaveSession {
|
||||||
filepath: None,
|
filepath: None,
|
||||||
include_all: false,
|
include_all: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(args) = trimmed.strip_prefix("/save ") {
|
if let Some(args) = trimmed.strip_prefix("/save-session ") {
|
||||||
let args = args.trim();
|
let args = args.trim();
|
||||||
// 解析参数:可能是 "all"、路径、或 "all 路径"
|
// 解析参数:可能是 "all"、路径、或 "all 路径"
|
||||||
let (include_all, filepath) = if args == "all" {
|
let (include_all, filepath) = if args == "all" {
|
||||||
// /save all - 保存全部消息
|
// /save-session all - 保存全部消息
|
||||||
(true, None)
|
(true, None)
|
||||||
} else if args.starts_with("all ") {
|
} else if args.starts_with("all ") {
|
||||||
// /save all <filepath> - 保存全部消息到指定路径
|
// /save-session all <filepath> - 保存全部消息到指定路径
|
||||||
let path = args[4..].trim();
|
let path = args[4..].trim();
|
||||||
(true, Some(path.to_string()))
|
(true, Some(path.to_string()))
|
||||||
} else {
|
} else {
|
||||||
// /save <filepath> - 保存活跃消息到指定路径
|
// /save-session <filepath> - 保存活跃消息到指定路径
|
||||||
(false, Some(args.to_string()))
|
(false, Some(args.to_string()))
|
||||||
};
|
};
|
||||||
return Ok(Some(Command::SaveSession { filepath, include_all }));
|
return Ok(Some(Command::SaveSession { filepath, include_all }));
|
||||||
@ -92,6 +106,11 @@ impl InputAdapter for CliInputAdapter {
|
|||||||
return Ok(Some(Command::GetCurrentSession));
|
return Ok(Some(Command::GetCurrentSession));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析 /help 命令 - 显示所有支持的命令
|
||||||
|
if trimmed == "/help" {
|
||||||
|
return Ok(Some(Command::Help));
|
||||||
|
}
|
||||||
|
|
||||||
// 不是命令,返回 None
|
// 不是命令,返回 None
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@ -223,23 +242,52 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_input_adapter_save_without_path() {
|
fn test_cli_input_adapter_save_topic_without_path() {
|
||||||
let adapter = CliInputAdapter::new();
|
let adapter = CliInputAdapter::new();
|
||||||
let ctx = AdapterContext::new("test");
|
let ctx = AdapterContext::new("test");
|
||||||
|
|
||||||
let result = adapter.try_parse("/save", ctx).unwrap();
|
let result = adapter.try_parse("/save", ctx).unwrap();
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
let cmd = result.unwrap();
|
||||||
|
assert!(matches!(cmd, Command::SaveTopic { filepath: None }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cli_input_adapter_save_topic_with_path() {
|
||||||
|
let adapter = CliInputAdapter::new();
|
||||||
|
let ctx = AdapterContext::new("test");
|
||||||
|
|
||||||
|
let result = adapter.try_parse("/save ./debug/topic.md", ctx).unwrap();
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
let cmd = result.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
cmd,
|
||||||
|
Command::SaveTopic {
|
||||||
|
filepath: Some(ref p),
|
||||||
|
} if p == "./debug/topic.md"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cli_input_adapter_save_session_without_path() {
|
||||||
|
let adapter = CliInputAdapter::new();
|
||||||
|
let ctx = AdapterContext::new("test");
|
||||||
|
|
||||||
|
let result = adapter.try_parse("/save-session", ctx).unwrap();
|
||||||
|
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let cmd = result.unwrap();
|
let cmd = result.unwrap();
|
||||||
assert!(matches!(cmd, Command::SaveSession { filepath: None, include_all: false }));
|
assert!(matches!(cmd, Command::SaveSession { filepath: None, include_all: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_input_adapter_save_with_path() {
|
fn test_cli_input_adapter_save_session_with_path() {
|
||||||
let adapter = CliInputAdapter::new();
|
let adapter = CliInputAdapter::new();
|
||||||
let ctx = AdapterContext::new("test");
|
let ctx = AdapterContext::new("test");
|
||||||
|
|
||||||
let result = adapter.try_parse("/save ./debug/session.md", ctx).unwrap();
|
let result = adapter.try_parse("/save-session ./debug/session.md", ctx).unwrap();
|
||||||
|
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let cmd = result.unwrap();
|
let cmd = result.unwrap();
|
||||||
@ -253,11 +301,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_input_adapter_save_all() {
|
fn test_cli_input_adapter_save_session_all() {
|
||||||
let adapter = CliInputAdapter::new();
|
let adapter = CliInputAdapter::new();
|
||||||
let ctx = AdapterContext::new("test");
|
let ctx = AdapterContext::new("test");
|
||||||
|
|
||||||
let result = adapter.try_parse("/save all", ctx).unwrap();
|
let result = adapter.try_parse("/save-session all", ctx).unwrap();
|
||||||
|
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let cmd = result.unwrap();
|
let cmd = result.unwrap();
|
||||||
@ -265,11 +313,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_input_adapter_save_all_with_path() {
|
fn test_cli_input_adapter_save_session_all_with_path() {
|
||||||
let adapter = CliInputAdapter::new();
|
let adapter = CliInputAdapter::new();
|
||||||
let ctx = AdapterContext::new("test");
|
let ctx = AdapterContext::new("test");
|
||||||
|
|
||||||
let result = adapter.try_parse("/save all ./debug/session.md", ctx).unwrap();
|
let result = adapter.try_parse("/save-session all ./debug/session.md", ctx).unwrap();
|
||||||
|
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let cmd = result.unwrap();
|
let cmd = result.unwrap();
|
||||||
@ -287,11 +335,11 @@ mod tests {
|
|||||||
let adapter = CliOutputAdapter::new();
|
let adapter = CliOutputAdapter::new();
|
||||||
let request_id = uuid::Uuid::new_v4();
|
let request_id = uuid::Uuid::new_v4();
|
||||||
let response = CommandResponse::success(request_id)
|
let response = CommandResponse::success(request_id)
|
||||||
.with_message(MessageKind::Notification, "Session saved to: session.md")
|
.with_message(MessageKind::Notification, "Topic saved to: topic.md")
|
||||||
.with_metadata("filepath", "session.md");
|
.with_metadata("filepath", "topic.md");
|
||||||
|
|
||||||
let output = adapter.adapt(response);
|
let output = adapter.adapt(response);
|
||||||
|
|
||||||
assert!(output.contains("Session saved to: session.md"));
|
assert!(output.contains("Topic saved to: topic.md"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,15 @@ use crate::command::Command;
|
|||||||
use crate::agent::AgentError;
|
use crate::agent::AgentError;
|
||||||
use crate::gateway::session::SessionManager;
|
use crate::gateway::session::SessionManager;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// 命令元数据(用于帮助系统)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CommandMetadata {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub usage: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
/// 命令处理器 trait
|
/// 命令处理器 trait
|
||||||
///
|
///
|
||||||
@ -15,6 +24,11 @@ pub trait CommandHandler: Send + Sync {
|
|||||||
/// 是否可以处理此命令
|
/// 是否可以处理此命令
|
||||||
fn can_handle(&self, cmd: &Command) -> bool;
|
fn can_handle(&self, cmd: &Command) -> bool;
|
||||||
|
|
||||||
|
/// 返回命令元数据(用于 /help 命令)
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// 执行命令
|
/// 执行命令
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@ -64,6 +78,7 @@ pub trait InChatCommandHandler: Send + Sync {
|
|||||||
/// 负责将命令分发到合适的处理器
|
/// 负责将命令分发到合适的处理器
|
||||||
pub struct CommandRouter {
|
pub struct CommandRouter {
|
||||||
handlers: Vec<Box<dyn CommandHandler>>,
|
handlers: Vec<Box<dyn CommandHandler>>,
|
||||||
|
metadata: Arc<std::sync::Mutex<Vec<CommandMetadata>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRouter {
|
impl CommandRouter {
|
||||||
@ -71,6 +86,7 @@ impl CommandRouter {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
handlers: Vec::new(),
|
handlers: Vec::new(),
|
||||||
|
metadata: Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,9 +95,17 @@ impl CommandRouter {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `handler` - 要注册的处理器
|
/// * `handler` - 要注册的处理器
|
||||||
pub fn register(&mut self, handler: Box<dyn CommandHandler>) {
|
pub fn register(&mut self, handler: Box<dyn CommandHandler>) {
|
||||||
|
if let Some(meta) = handler.metadata() {
|
||||||
|
self.metadata.lock().unwrap().push(meta);
|
||||||
|
}
|
||||||
self.handlers.push(handler);
|
self.handlers.push(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取已注册命令的元数据列表(用于 Help 命令)
|
||||||
|
pub fn metadata_arc(&self) -> Arc<std::sync::Mutex<Vec<CommandMetadata>>> {
|
||||||
|
self.metadata.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// 分发命令到合适的处理器
|
/// 分发命令到合适的处理器
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
96
src/command/handlers/get_current.rs
Normal file
96
src/command/handlers/get_current.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use crate::command::context::CommandContext;
|
||||||
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::storage::SessionStore;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// 获取当前话题命令处理器
|
||||||
|
pub struct GetCurrentSessionCommandHandler {
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetCurrentSessionCommandHandler {
|
||||||
|
pub fn new(store: Arc<SessionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for GetCurrentSessionCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::GetCurrentSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "current",
|
||||||
|
description: "获取当前话题信息",
|
||||||
|
usage: "/current",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
match cmd {
|
||||||
|
Command::GetCurrentSession => handle_get_current_session(self, ctx).await,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_get_current_session(
|
||||||
|
handler: &GetCurrentSessionCommandHandler,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
let topic_id = ctx.topic_id.as_deref()
|
||||||
|
.ok_or_else(|| CommandError::new("NO_CURRENT_TOPIC", "No current topic"))?;
|
||||||
|
|
||||||
|
let topic = handler
|
||||||
|
.store
|
||||||
|
.get_topic(topic_id)
|
||||||
|
.map_err(|e| CommandError::new("GET_TOPIC_ERROR", e.to_string()))?
|
||||||
|
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)))?;
|
||||||
|
|
||||||
|
let last_active = format_time_ago(topic.last_active_at);
|
||||||
|
let created_at = format_time_ago(topic.created_at);
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Created: {}\n Last Active: {}",
|
||||||
|
topic.id,
|
||||||
|
topic.title,
|
||||||
|
topic.message_count,
|
||||||
|
created_at,
|
||||||
|
last_active
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
|
.with_message(MessageKind::Notification, &message)
|
||||||
|
.with_metadata("topic_id", &topic.id)
|
||||||
|
.with_metadata("title", &topic.title)
|
||||||
|
.with_metadata("message_count", &topic.message_count.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time_ago(timestamp_ms: i64) -> String {
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
let diff_ms = now - timestamp_ms;
|
||||||
|
let diff_secs = diff_ms / 1000;
|
||||||
|
|
||||||
|
if diff_secs < 60 {
|
||||||
|
"just now".to_string()
|
||||||
|
} else if diff_secs < 3600 {
|
||||||
|
format!("{} mins ago", diff_secs / 60)
|
||||||
|
} else if diff_secs < 86400 {
|
||||||
|
format!("{} hours ago", diff_secs / 3600)
|
||||||
|
} else {
|
||||||
|
format!("{} days ago", diff_secs / 86400)
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/command/handlers/help.rs
Normal file
61
src/command/handlers/help.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
use crate::command::context::CommandContext;
|
||||||
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
|
use crate::command::Command;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// Help 命令处理器
|
||||||
|
///
|
||||||
|
/// 显示所有支持的命令列表
|
||||||
|
pub struct HelpCommandHandler {
|
||||||
|
metadata: Arc<Mutex<Vec<CommandMetadata>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HelpCommandHandler {
|
||||||
|
/// 创建新的 Help 命令处理器
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `metadata` - CommandRouter 的元数据列表引用
|
||||||
|
pub fn new(metadata: Arc<Mutex<Vec<CommandMetadata>>>) -> Self {
|
||||||
|
Self { metadata }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for HelpCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::Help)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "help",
|
||||||
|
description: "显示所有支持的命令",
|
||||||
|
usage: "/help",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
_cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
let metadata = self.metadata.lock().unwrap();
|
||||||
|
let help_text = format_help(&metadata);
|
||||||
|
|
||||||
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
|
.with_message(MessageKind::Text, &help_text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 格式化帮助文本
|
||||||
|
fn format_help(commands: &[CommandMetadata]) -> String {
|
||||||
|
let mut output = String::from("# 支持的命令\n\n");
|
||||||
|
|
||||||
|
for cmd in commands {
|
||||||
|
output.push_str(&format!("**{}** - {}\n", cmd.usage, cmd.description));
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
136
src/command/handlers/list_sessions.rs
Normal file
136
src/command/handlers/list_sessions.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
use crate::command::context::CommandContext;
|
||||||
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::storage::SessionStore;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// 列出话题命令处理器
|
||||||
|
pub struct ListSessionsCommandHandler {
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListSessionsCommandHandler {
|
||||||
|
pub fn new(store: Arc<SessionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for ListSessionsCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::ListSessions { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "list",
|
||||||
|
description: "列出所有话题",
|
||||||
|
usage: "/list [all]",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
match cmd {
|
||||||
|
Command::ListSessions { include_archived } => {
|
||||||
|
handle_list_sessions(self, include_archived, ctx).await
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list_sessions(
|
||||||
|
handler: &ListSessionsCommandHandler,
|
||||||
|
_include_archived: bool,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
let session_id = ctx.session_id.as_deref()
|
||||||
|
.ok_or_else(|| CommandError::new("NO_SESSION", "No active session"))?;
|
||||||
|
|
||||||
|
let topics = handler
|
||||||
|
.store
|
||||||
|
.list_topics(session_id)
|
||||||
|
.map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?;
|
||||||
|
|
||||||
|
let current_topic_id = ctx.topic_id.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
let message = if topics.is_empty() {
|
||||||
|
"No topics found. Use /new <title> to create a topic.".to_string()
|
||||||
|
} else {
|
||||||
|
let mut lines = vec![format!("Found {} topic(s):", topics.len())];
|
||||||
|
lines.push(String::new());
|
||||||
|
lines.push("┌────┬─────────────────┬──────────────────────┬──────────┬─────────────────┐".to_string());
|
||||||
|
lines.push("│ No │ Topic ID │ Title │ Messages │ Last Active │".to_string());
|
||||||
|
lines.push("├────┼─────────────────┼──────────────────────┼──────────┼─────────────────┤".to_string());
|
||||||
|
|
||||||
|
for (idx, topic) in topics.iter().enumerate() {
|
||||||
|
let row_num = idx + 1;
|
||||||
|
let is_current = topic.id == current_topic_id;
|
||||||
|
let num_marker = if is_current { " * ".to_string() } else { format!(" {:<2}", row_num) };
|
||||||
|
|
||||||
|
let topic_id_display = if topic.id.len() > 15 {
|
||||||
|
format!("{}...", &topic.id[..12])
|
||||||
|
} else {
|
||||||
|
topic.id.clone()
|
||||||
|
};
|
||||||
|
let title_display = if topic.title.len() > 20 {
|
||||||
|
format!("{}...", &topic.title[..17])
|
||||||
|
} else {
|
||||||
|
topic.title.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let last_active = format_time_ago(topic.last_active_at);
|
||||||
|
|
||||||
|
lines.push(format!(
|
||||||
|
"│{}│ {:<15} │ {:<20} │ {:<8} │ {:<15} │",
|
||||||
|
num_marker,
|
||||||
|
topic_id_display,
|
||||||
|
title_display,
|
||||||
|
topic.message_count,
|
||||||
|
last_active
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("└────┴─────────────────┴──────────────────────┴──────────┴─────────────────┘".to_string());
|
||||||
|
lines.push(String::new());
|
||||||
|
lines.push("* = current topic".to_string());
|
||||||
|
lines.push("Use /use <number> or /use <topic_id> to switch".to_string());
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
let topics_json = serde_json::to_string(&topics)
|
||||||
|
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
|
.with_message(MessageKind::Notification, &message)
|
||||||
|
.with_metadata("topics", &topics_json)
|
||||||
|
.with_metadata("count", &topics.len().to_string())
|
||||||
|
.with_metadata("current_topic_id", current_topic_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time_ago(timestamp_ms: i64) -> String {
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
let diff_ms = now - timestamp_ms;
|
||||||
|
let diff_secs = diff_ms / 1000;
|
||||||
|
|
||||||
|
if diff_secs < 60 {
|
||||||
|
"just now".to_string()
|
||||||
|
} else if diff_secs < 3600 {
|
||||||
|
format!("{} mins ago", diff_secs / 60)
|
||||||
|
} else if diff_secs < 86400 {
|
||||||
|
format!("{} hours ago", diff_secs / 3600)
|
||||||
|
} else {
|
||||||
|
format!("{} days ago", diff_secs / 86400)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/command/handlers/load_session.rs
Normal file
64
src/command/handlers/load_session.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use crate::command::context::CommandContext;
|
||||||
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::storage::SessionStore;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// 加载话题命令处理器
|
||||||
|
pub struct LoadSessionCommandHandler {
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadSessionCommandHandler {
|
||||||
|
pub fn new(store: Arc<SessionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for LoadSessionCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::LoadSession { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "load",
|
||||||
|
description: "加载指定话题",
|
||||||
|
usage: "/load <session_id>",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
match cmd {
|
||||||
|
Command::LoadSession { session_id } => {
|
||||||
|
handle_load_session(self, session_id, ctx).await
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_load_session(
|
||||||
|
handler: &LoadSessionCommandHandler,
|
||||||
|
topic_id: String,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
let topic = handler
|
||||||
|
.store
|
||||||
|
.get_topic(&topic_id)
|
||||||
|
.map_err(|e| CommandError::new("LOAD_TOPIC_ERROR", e.to_string()))?
|
||||||
|
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)))?;
|
||||||
|
|
||||||
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
|
.with_message(MessageKind::Notification, &topic.title)
|
||||||
|
.with_metadata("topic_id", &topic.id)
|
||||||
|
.with_metadata("title", &topic.title)
|
||||||
|
.with_metadata("message_count", &topic.message_count.to_string()))
|
||||||
|
}
|
||||||
@ -1,3 +1,14 @@
|
|||||||
|
pub mod get_current;
|
||||||
|
pub mod help;
|
||||||
|
pub mod list_sessions;
|
||||||
|
pub mod load_session;
|
||||||
pub mod save_session;
|
pub mod save_session;
|
||||||
|
pub mod save_topic;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod session_query;
|
pub mod switch_session;
|
||||||
|
|
||||||
|
// 导出公共函数供其他模块复用
|
||||||
|
pub use save_session::{
|
||||||
|
escape_yaml_string, format_message_content, format_timestamp,
|
||||||
|
generate_messages_markdown, generate_system_prompt_markdown,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
||||||
use crate::bus::InboundMessage;
|
use crate::bus::InboundMessage;
|
||||||
use crate::command::context::CommandContext;
|
use crate::command::context::CommandContext;
|
||||||
use crate::command::handler::{CommandHandler, InChatCommandHandler};
|
use crate::command::handler::{CommandHandler, CommandMetadata, InChatCommandHandler};
|
||||||
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
use crate::storage::{SessionRecord, SessionStore};
|
use crate::storage::{SessionRecord, SessionStore};
|
||||||
@ -107,6 +107,14 @@ impl CommandHandler for SaveSessionCommandHandler {
|
|||||||
matches!(cmd, Command::SaveSession { .. })
|
matches!(cmd, Command::SaveSession { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "save-session",
|
||||||
|
description: "保存当前会话到 Markdown 文件",
|
||||||
|
usage: "/save-session [all] [filepath]",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
cmd: Command,
|
cmd: Command,
|
||||||
@ -237,17 +245,52 @@ pub fn generate_markdown(
|
|||||||
output.push_str(&format!("message_count: {}\n", messages.len()));
|
output.push_str(&format!("message_count: {}\n", messages.len()));
|
||||||
output.push_str("---\n\n");
|
output.push_str("---\n\n");
|
||||||
|
|
||||||
// 系统提示词
|
// 系统提示词(复用公共函数)
|
||||||
output.push_str("# System Prompt\n\n");
|
output.push_str(&generate_system_prompt_markdown(system_prompt));
|
||||||
if let Some(prompt) = system_prompt {
|
|
||||||
output.push_str("```\n");
|
// 消息历史(复用公共函数)
|
||||||
output.push_str(&prompt.content);
|
output.push_str(&generate_messages_markdown(messages));
|
||||||
output.push_str("\n```\n\n");
|
|
||||||
} else {
|
output
|
||||||
output.push_str("*No system prompt available*\n\n");
|
}
|
||||||
}
|
|
||||||
|
/// 格式化消息内容
|
||||||
|
///
|
||||||
|
/// 如果内容包含特殊字符,使用代码块包装
|
||||||
|
pub fn format_message_content(content: &str) -> String {
|
||||||
|
// 如果内容包含代码块标记或表格标记,使用原始格式
|
||||||
|
if content.contains("```") || content.contains("| ") {
|
||||||
|
format!("```\n{}\n```", content)
|
||||||
|
} else {
|
||||||
|
content.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 转义 YAML 字符串
|
||||||
|
pub fn escape_yaml_string(s: &str) -> String {
|
||||||
|
if s.contains('\n') || s.contains('"') || s.contains(':') || s.starts_with(' ') {
|
||||||
|
// 使用双引号包裹并转义内部的双引号
|
||||||
|
format!("\"{}\"", s.replace('"', "\\\""))
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 格式化时间戳
|
||||||
|
pub fn format_timestamp(ts: i64) -> String {
|
||||||
|
Local
|
||||||
|
.timestamp_millis_opt(ts)
|
||||||
|
.single()
|
||||||
|
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
|
||||||
|
.unwrap_or_else(|| format!("{}", ts))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成消息历史的 Markdown 内容
|
||||||
|
///
|
||||||
|
/// 这是一个通用函数,可被 Session 和 Topic 的保存逻辑复用
|
||||||
|
pub fn generate_messages_markdown(messages: &[crate::bus::ChatMessage]) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
// 消息历史
|
|
||||||
output.push_str("# Message History\n\n");
|
output.push_str("# Message History\n\n");
|
||||||
|
|
||||||
for (idx, msg) in messages.iter().enumerate() {
|
for (idx, msg) in messages.iter().enumerate() {
|
||||||
@ -319,35 +362,20 @@ pub fn generate_markdown(
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 格式化消息内容
|
/// 生成系统提示词部分的 Markdown
|
||||||
///
|
pub fn generate_system_prompt_markdown(system_prompt: &Option<SystemPrompt>) -> String {
|
||||||
/// 如果内容包含特殊字符,使用代码块包装
|
let mut output = String::new();
|
||||||
fn format_message_content(content: &str) -> String {
|
|
||||||
// 如果内容包含代码块标记或表格标记,使用原始格式
|
|
||||||
if content.contains("```") || content.contains("| ") {
|
|
||||||
format!("```\n{}\n```", content)
|
|
||||||
} else {
|
|
||||||
content.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 转义 YAML 字符串
|
output.push_str("# System Prompt\n\n");
|
||||||
fn escape_yaml_string(s: &str) -> String {
|
if let Some(prompt) = system_prompt {
|
||||||
if s.contains('\n') || s.contains('"') || s.contains(':') || s.starts_with(' ') {
|
output.push_str("```\n");
|
||||||
// 使用双引号包裹并转义内部的双引号
|
output.push_str(&prompt.content);
|
||||||
format!("\"{}\"", s.replace('"', "\\\""))
|
output.push_str("\n```\n\n");
|
||||||
} else {
|
} else {
|
||||||
s.to_string()
|
output.push_str("*No system prompt available*\n\n");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// 格式化时间戳
|
output
|
||||||
pub fn format_timestamp(ts: i64) -> String {
|
|
||||||
Local
|
|
||||||
.timestamp_millis_opt(ts)
|
|
||||||
.single()
|
|
||||||
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
|
|
||||||
.unwrap_or_else(|| format!("{}", ts))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析文件路径
|
/// 解析文件路径
|
||||||
@ -557,6 +585,7 @@ mod tests {
|
|||||||
assert!(handler.can_handle(&Command::SaveSession { filepath: None, include_all: false }));
|
assert!(handler.can_handle(&Command::SaveSession { filepath: None, include_all: false }));
|
||||||
assert!(handler.can_handle(&Command::SaveSession { filepath: None, include_all: true }));
|
assert!(handler.can_handle(&Command::SaveSession { filepath: None, include_all: true }));
|
||||||
assert!(!handler.can_handle(&Command::CreateSession { title: None }));
|
assert!(!handler.can_handle(&Command::CreateSession { title: None }));
|
||||||
|
assert!(!handler.can_handle(&Command::SaveTopic { filepath: None }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试用的系统提示词提供者
|
/// 测试用的系统提示词提供者
|
||||||
|
|||||||
242
src/command/handlers/save_topic.rs
Normal file
242
src/command/handlers/save_topic.rs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
||||||
|
use crate::command::context::CommandContext;
|
||||||
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
|
use crate::command::handlers::{
|
||||||
|
escape_yaml_string, format_timestamp, generate_messages_markdown,
|
||||||
|
generate_system_prompt_markdown,
|
||||||
|
};
|
||||||
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::storage::{SessionStore, TopicRecord};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::Local;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// 保存话题到文件
|
||||||
|
pub async fn save_topic_to_file(
|
||||||
|
topic_id: &str,
|
||||||
|
filepath: Option<String>,
|
||||||
|
store: &SessionStore,
|
||||||
|
system_prompt_provider: &dyn SystemPromptProvider,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
// 获取话题记录
|
||||||
|
let topic = store
|
||||||
|
.get_topic(topic_id)
|
||||||
|
.map_err(|e| format!("Failed to get topic: {}", e))?
|
||||||
|
.ok_or_else(|| "Topic not found".to_string())?;
|
||||||
|
|
||||||
|
// 加载话题消息
|
||||||
|
let messages = store
|
||||||
|
.load_messages_for_topic(topic_id)
|
||||||
|
.map_err(|e| format!("Failed to load messages: {}", e))?;
|
||||||
|
|
||||||
|
// 获取 session 信息(用于系统提示词)
|
||||||
|
let session = store
|
||||||
|
.get_session(&topic.session_id)
|
||||||
|
.map_err(|e| format!("Failed to get session: {}", e))?;
|
||||||
|
|
||||||
|
// 构建系统提示词
|
||||||
|
let user_message_count = messages.iter().filter(|m| m.role == "user").count();
|
||||||
|
let system_prompt = build_system_prompt(system_prompt_provider, &session, user_message_count);
|
||||||
|
|
||||||
|
// 生成 Markdown 内容
|
||||||
|
let markdown = generate_topic_markdown(&topic, &system_prompt, &messages);
|
||||||
|
|
||||||
|
// 确定输出路径
|
||||||
|
let output_path = resolve_topic_filepath(filepath, &topic);
|
||||||
|
|
||||||
|
// 创建父目录
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
if !parent.as_os_str().is_empty() && !parent.exists() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
std::fs::write(&output_path, markdown)
|
||||||
|
.map_err(|e| format!("Failed to write file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(output_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建系统提示词
|
||||||
|
fn build_system_prompt(
|
||||||
|
provider: &dyn SystemPromptProvider,
|
||||||
|
session: &Option<crate::storage::SessionRecord>,
|
||||||
|
user_message_count: usize,
|
||||||
|
) -> Option<SystemPrompt> {
|
||||||
|
let session = session.as_ref()?;
|
||||||
|
let context = SystemPromptContext {
|
||||||
|
session_id: Some(session.id.clone()),
|
||||||
|
chat_id: session.chat_id.clone(),
|
||||||
|
user_message_count,
|
||||||
|
};
|
||||||
|
provider.build(&context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成话题 Markdown 内容(复用公共函数)
|
||||||
|
fn generate_topic_markdown(
|
||||||
|
topic: &TopicRecord,
|
||||||
|
system_prompt: &Option<SystemPrompt>,
|
||||||
|
messages: &[crate::bus::ChatMessage],
|
||||||
|
) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
// YAML frontmatter(Topic 特有)
|
||||||
|
output.push_str("---\n");
|
||||||
|
output.push_str(&format!("title: {}\n", escape_yaml_string(&topic.title)));
|
||||||
|
output.push_str(&format!("topic_id: {}\n", topic.id));
|
||||||
|
output.push_str(&format!("session_id: {}\n", topic.session_id));
|
||||||
|
if let Some(ref desc) = topic.description {
|
||||||
|
output.push_str(&format!("description: {}\n", escape_yaml_string(desc)));
|
||||||
|
}
|
||||||
|
output.push_str(&format!(
|
||||||
|
"created_at: {}\n",
|
||||||
|
format_timestamp(topic.created_at)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
"updated_at: {}\n",
|
||||||
|
format_timestamp(topic.updated_at)
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
"last_active_at: {}\n",
|
||||||
|
format_timestamp(topic.last_active_at)
|
||||||
|
));
|
||||||
|
output.push_str(&format!("message_count: {}\n", messages.len()));
|
||||||
|
output.push_str("---\n\n");
|
||||||
|
|
||||||
|
// 系统提示词(复用公共函数)
|
||||||
|
output.push_str(&generate_system_prompt_markdown(system_prompt));
|
||||||
|
|
||||||
|
// 消息历史(复用公共函数)
|
||||||
|
output.push_str(&generate_messages_markdown(messages));
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析话题文件路径(Topic 特有)
|
||||||
|
fn resolve_topic_filepath(filepath: Option<String>, topic: &TopicRecord) -> PathBuf {
|
||||||
|
match filepath {
|
||||||
|
Some(path) => PathBuf::from(path),
|
||||||
|
None => {
|
||||||
|
let safe_title = topic
|
||||||
|
.title
|
||||||
|
.replace(' ', "_")
|
||||||
|
.replace('/', "_")
|
||||||
|
.replace('\\', "_")
|
||||||
|
.replace(':', "_")
|
||||||
|
.replace('<', "_")
|
||||||
|
.replace('>', "_")
|
||||||
|
.replace('|', "_")
|
||||||
|
.replace('?', "_")
|
||||||
|
.replace('*', "_")
|
||||||
|
.replace('"', "_");
|
||||||
|
|
||||||
|
let base_name = if safe_title.is_empty() {
|
||||||
|
format!("topic_{}", &topic.id[..8.min(topic.id.len())])
|
||||||
|
} else {
|
||||||
|
safe_title
|
||||||
|
};
|
||||||
|
|
||||||
|
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
|
||||||
|
let filename = format!("{}_{}.md", base_name, timestamp);
|
||||||
|
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join(".picobot")
|
||||||
|
.join("topics")
|
||||||
|
.join(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存话题命令处理器
|
||||||
|
pub struct SaveTopicCommandHandler {
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
system_prompt_provider: Arc<dyn SystemPromptProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaveTopicCommandHandler {
|
||||||
|
pub fn new(
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
system_prompt_provider: Arc<dyn SystemPromptProvider>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
store,
|
||||||
|
system_prompt_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for SaveTopicCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::SaveTopic { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "save",
|
||||||
|
description: "保存当前话题到 Markdown 文件",
|
||||||
|
usage: "/save [filepath]",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
match cmd {
|
||||||
|
Command::SaveTopic { filepath } => handle_save_topic(self, filepath, ctx).await,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_save_topic(
|
||||||
|
handler: &SaveTopicCommandHandler,
|
||||||
|
filepath: Option<String>,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
tracing::debug!(
|
||||||
|
ctx_topic_id = ?ctx.topic_id,
|
||||||
|
ctx_session_id = ?ctx.session_id,
|
||||||
|
channel = %ctx.channel_name,
|
||||||
|
"SaveTopic command received"
|
||||||
|
);
|
||||||
|
|
||||||
|
let topic_id = ctx
|
||||||
|
.topic_id
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| CommandError::new("NO_TOPIC", "No active topic".to_string()))?;
|
||||||
|
|
||||||
|
tracing::debug!(topic_id = %topic_id, "Attempting to save topic");
|
||||||
|
|
||||||
|
// 调用保存函数
|
||||||
|
let output_path = save_topic_to_file(
|
||||||
|
topic_id,
|
||||||
|
filepath,
|
||||||
|
&*handler.store,
|
||||||
|
&*handler.system_prompt_provider,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CommandError::new("SAVE_ERROR", e))?;
|
||||||
|
|
||||||
|
// 获取消息数量
|
||||||
|
let message_count = handler
|
||||||
|
.store
|
||||||
|
.load_messages_for_topic(topic_id)
|
||||||
|
.map_err(|e| CommandError::new("LOAD_MESSAGES_ERROR", e.to_string()))?
|
||||||
|
.len();
|
||||||
|
|
||||||
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
|
.with_message(
|
||||||
|
MessageKind::Notification,
|
||||||
|
&format!("Topic saved to: {}", output_path.display()),
|
||||||
|
)
|
||||||
|
.with_metadata("filepath", output_path.to_string_lossy().as_ref())
|
||||||
|
.with_metadata("message_count", &message_count.to_string()))
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::command::context::CommandContext;
|
use crate::command::context::CommandContext;
|
||||||
use crate::command::handler::CommandHandler;
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
use crate::gateway::session::SessionManager;
|
use crate::gateway::session::SessionManager;
|
||||||
@ -40,6 +40,14 @@ impl CommandHandler for SessionCommandHandler {
|
|||||||
matches!(cmd, Command::CreateSession { .. })
|
matches!(cmd, Command::CreateSession { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "new",
|
||||||
|
description: "创建新话题",
|
||||||
|
usage: "/new [title]",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
cmd: Command,
|
cmd: Command,
|
||||||
|
|||||||
@ -1,368 +0,0 @@
|
|||||||
use crate::command::context::CommandContext;
|
|
||||||
use crate::command::handler::CommandHandler;
|
|
||||||
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
|
||||||
use crate::command::Command;
|
|
||||||
use crate::gateway::session::SessionManager;
|
|
||||||
use crate::storage::SessionStore;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// 会话查询命令处理器
|
|
||||||
///
|
|
||||||
/// 处理 ListSessions、LoadSession 和 SwitchSession 命令(现在操作 Topic)
|
|
||||||
pub struct SessionQueryCommandHandler {
|
|
||||||
store: Arc<SessionStore>,
|
|
||||||
session_manager: Option<SessionManager>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SessionQueryCommandHandler {
|
|
||||||
/// 创建新的会话查询命令处理器
|
|
||||||
pub fn new(store: Arc<SessionStore>) -> Self {
|
|
||||||
Self {
|
|
||||||
store,
|
|
||||||
session_manager: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 设置 SessionManager(用于 SwitchSession 命令)
|
|
||||||
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
|
|
||||||
self.session_manager = Some(session_manager);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl CommandHandler for SessionQueryCommandHandler {
|
|
||||||
fn can_handle(&self, cmd: &Command) -> bool {
|
|
||||||
matches!(cmd, Command::ListSessions { .. } | Command::LoadSession { .. } | Command::SwitchSession { .. } | Command::GetCurrentSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&self,
|
|
||||||
cmd: Command,
|
|
||||||
ctx: CommandContext,
|
|
||||||
) -> Result<CommandResponse, CommandError> {
|
|
||||||
match cmd {
|
|
||||||
Command::ListSessions { include_archived } => {
|
|
||||||
handle_list_sessions(self, include_archived, ctx).await
|
|
||||||
}
|
|
||||||
Command::LoadSession { session_id } => {
|
|
||||||
handle_load_session(self, session_id, ctx).await
|
|
||||||
}
|
|
||||||
Command::SwitchSession { session_id } => {
|
|
||||||
handle_switch_session(self, session_id, ctx).await
|
|
||||||
}
|
|
||||||
Command::GetCurrentSession => {
|
|
||||||
handle_get_current_session(self, ctx).await
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理列出话题命令
|
|
||||||
async fn handle_list_sessions(
|
|
||||||
handler: &SessionQueryCommandHandler,
|
|
||||||
_include_archived: bool,
|
|
||||||
ctx: CommandContext,
|
|
||||||
) -> Result<CommandResponse, CommandError> {
|
|
||||||
// 获取当前 session_id
|
|
||||||
let session_id = ctx.session_id.as_deref()
|
|
||||||
.ok_or_else(|| CommandError::new("NO_SESSION", "No active session"))?;
|
|
||||||
|
|
||||||
// 查询该 session 的所有 topic
|
|
||||||
let topics = handler
|
|
||||||
.store
|
|
||||||
.list_topics(session_id)
|
|
||||||
.map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?;
|
|
||||||
|
|
||||||
// 获取当前 topic ID
|
|
||||||
let current_topic_id = ctx.topic_id.as_deref().unwrap_or("");
|
|
||||||
|
|
||||||
// 构建表格格式的话题列表消息
|
|
||||||
let message = if topics.is_empty() {
|
|
||||||
"No topics found. Use /new <title> to create a topic.".to_string()
|
|
||||||
} else {
|
|
||||||
let mut lines = vec![format!("Found {} topic(s):", topics.len())];
|
|
||||||
lines.push(String::new());
|
|
||||||
|
|
||||||
// 表格头部
|
|
||||||
lines.push("┌────┬─────────────────┬──────────────────────┬──────────┬─────────────────┐".to_string());
|
|
||||||
lines.push("│ No │ Topic ID │ Title │ Messages │ Last Active │".to_string());
|
|
||||||
lines.push("├────┼─────────────────┼──────────────────────┼──────────┼─────────────────┤".to_string());
|
|
||||||
|
|
||||||
// 表格内容
|
|
||||||
for (idx, topic) in topics.iter().enumerate() {
|
|
||||||
let row_num = idx + 1;
|
|
||||||
let is_current = topic.id == current_topic_id;
|
|
||||||
let num_marker = if is_current { " * ".to_string() } else { format!(" {:<2}", row_num) };
|
|
||||||
|
|
||||||
// 截断过长的字段
|
|
||||||
let topic_id_display = if topic.id.len() > 15 {
|
|
||||||
format!("{}...", &topic.id[..12])
|
|
||||||
} else {
|
|
||||||
topic.id.clone()
|
|
||||||
};
|
|
||||||
let title_display = if topic.title.len() > 20 {
|
|
||||||
format!("{}...", &topic.title[..17])
|
|
||||||
} else {
|
|
||||||
topic.title.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let last_active = format_time_ago(topic.last_active_at);
|
|
||||||
|
|
||||||
lines.push(format!(
|
|
||||||
"│{}│ {:<15} │ {:<20} │ {:<8} │ {:<15} │",
|
|
||||||
num_marker,
|
|
||||||
topic_id_display,
|
|
||||||
title_display,
|
|
||||||
topic.message_count,
|
|
||||||
last_active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表格底部
|
|
||||||
lines.push("└────┴─────────────────┴──────────────────────┴──────────┴─────────────────┘".to_string());
|
|
||||||
lines.push(String::new());
|
|
||||||
lines.push("* = current topic".to_string());
|
|
||||||
lines.push("Use /use <number> or /use <topic_id> to switch".to_string());
|
|
||||||
|
|
||||||
lines.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
let topics_json = serde_json::to_string(&topics)
|
|
||||||
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(CommandResponse::success(ctx.request_id)
|
|
||||||
.with_message(MessageKind::Notification, &message)
|
|
||||||
.with_metadata("topics", &topics_json)
|
|
||||||
.with_metadata("count", &topics.len().to_string())
|
|
||||||
.with_metadata("current_topic_id", current_topic_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理加载话题命令
|
|
||||||
async fn handle_load_session(
|
|
||||||
handler: &SessionQueryCommandHandler,
|
|
||||||
topic_id: String,
|
|
||||||
ctx: CommandContext,
|
|
||||||
) -> Result<CommandResponse, CommandError> {
|
|
||||||
let topic = handler
|
|
||||||
.store
|
|
||||||
.get_topic(&topic_id)
|
|
||||||
.map_err(|e| CommandError::new("LOAD_TOPIC_ERROR", e.to_string()))?
|
|
||||||
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)))?;
|
|
||||||
|
|
||||||
Ok(CommandResponse::success(ctx.request_id)
|
|
||||||
.with_message(MessageKind::Notification, &topic.title)
|
|
||||||
.with_metadata("topic_id", &topic.id)
|
|
||||||
.with_metadata("title", &topic.title)
|
|
||||||
.with_metadata("message_count", &topic.message_count.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 格式化时间为相对时间(如 "2 mins ago")
|
|
||||||
fn format_time_ago(timestamp_ms: i64) -> String {
|
|
||||||
let now = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis() as i64;
|
|
||||||
|
|
||||||
let diff_ms = now - timestamp_ms;
|
|
||||||
let diff_secs = diff_ms / 1000;
|
|
||||||
|
|
||||||
if diff_secs < 60 {
|
|
||||||
"just now".to_string()
|
|
||||||
} else if diff_secs < 3600 {
|
|
||||||
format!("{} mins ago", diff_secs / 60)
|
|
||||||
} else if diff_secs < 86400 {
|
|
||||||
format!("{} hours ago", diff_secs / 3600)
|
|
||||||
} else {
|
|
||||||
format!("{} days ago", diff_secs / 86400)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理获取当前话题命令
|
|
||||||
async fn handle_get_current_session(
|
|
||||||
handler: &SessionQueryCommandHandler,
|
|
||||||
ctx: CommandContext,
|
|
||||||
) -> Result<CommandResponse, CommandError> {
|
|
||||||
let topic_id = ctx.topic_id.as_deref()
|
|
||||||
.ok_or_else(|| CommandError::new("NO_CURRENT_TOPIC", "No current topic"))?;
|
|
||||||
|
|
||||||
let topic = handler
|
|
||||||
.store
|
|
||||||
.get_topic(topic_id)
|
|
||||||
.map_err(|e| CommandError::new("GET_TOPIC_ERROR", e.to_string()))?
|
|
||||||
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)))?;
|
|
||||||
|
|
||||||
let last_active = format_time_ago(topic.last_active_at);
|
|
||||||
let created_at = format_time_ago(topic.created_at);
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Created: {}\n Last Active: {}",
|
|
||||||
topic.id,
|
|
||||||
topic.title,
|
|
||||||
topic.message_count,
|
|
||||||
created_at,
|
|
||||||
last_active
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(CommandResponse::success(ctx.request_id)
|
|
||||||
.with_message(MessageKind::Notification, &message)
|
|
||||||
.with_metadata("topic_id", &topic.id)
|
|
||||||
.with_metadata("title", &topic.title)
|
|
||||||
.with_metadata("message_count", &topic.message_count.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理切换话题命令
|
|
||||||
async fn handle_switch_session(
|
|
||||||
handler: &SessionQueryCommandHandler,
|
|
||||||
topic_id: String,
|
|
||||||
ctx: CommandContext,
|
|
||||||
) -> Result<CommandResponse, CommandError> {
|
|
||||||
// 获取当前 session_id 和 chat_id
|
|
||||||
let session_id = ctx.session_id.as_deref()
|
|
||||||
.ok_or_else(|| CommandError::new("NO_SESSION", "No active session"))?;
|
|
||||||
let chat_id = ctx.chat_id.as_deref()
|
|
||||||
.ok_or_else(|| CommandError::new("NO_CHAT_ID", "No chat_id in context"))?;
|
|
||||||
|
|
||||||
// 尝试解析为序号
|
|
||||||
let target_topic_id = if let Ok(index) = topic_id.parse::<usize>() {
|
|
||||||
let topics = handler
|
|
||||||
.store
|
|
||||||
.list_topics(session_id)
|
|
||||||
.map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?;
|
|
||||||
|
|
||||||
let index = index.saturating_sub(1);
|
|
||||||
if index >= topics.len() {
|
|
||||||
return Err(CommandError::new(
|
|
||||||
"INVALID_TOPIC_INDEX",
|
|
||||||
format!("Topic index {} is out of range (1-{})", index + 1, topics.len())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
topics[index].id.clone()
|
|
||||||
} else {
|
|
||||||
topic_id
|
|
||||||
};
|
|
||||||
|
|
||||||
// 验证目标话题存在
|
|
||||||
let topic = handler
|
|
||||||
.store
|
|
||||||
.get_topic(&target_topic_id)
|
|
||||||
.map_err(|e| CommandError::new("SWITCH_TOPIC_ERROR", e.to_string()))?
|
|
||||||
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", target_topic_id)))?;
|
|
||||||
|
|
||||||
// 如果有 SessionManager,实际切换话题历史
|
|
||||||
if let Some(ref session_manager) = handler.session_manager {
|
|
||||||
if let Some(session) = session_manager.get(&ctx.channel_name).await {
|
|
||||||
let mut session_guard = session.lock().await;
|
|
||||||
session_guard.switch_topic(chat_id, &target_topic_id)
|
|
||||||
.map_err(|e| CommandError::new("SWITCH_TOPIC_ERROR", e.to_string()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回切换成功响应
|
|
||||||
let message = format!(
|
|
||||||
"✓ Switched to topic: {} ({} messages)",
|
|
||||||
topic.title, topic.message_count
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(CommandResponse::success(ctx.request_id)
|
|
||||||
.with_message(MessageKind::Notification, &message)
|
|
||||||
.with_metadata("topic_id", &topic.id)
|
|
||||||
.with_metadata("title", &topic.title)
|
|
||||||
.with_metadata("message_count", &topic.message_count.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::storage::SessionStore;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
fn create_test_handler() -> SessionQueryCommandHandler {
|
|
||||||
let store = Arc::new(SessionStore::in_memory().unwrap());
|
|
||||||
SessionQueryCommandHandler::new(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_list_sessions_empty() {
|
|
||||||
let handler = create_test_handler();
|
|
||||||
// 需要先创建一个 session 和 topic
|
|
||||||
let store = handler.store.clone();
|
|
||||||
let session = store.create_session("cli", Some("test")).unwrap();
|
|
||||||
|
|
||||||
let ctx = CommandContext::new("test", "cli")
|
|
||||||
.with_session_id(&session.id);
|
|
||||||
let cmd = Command::ListSessions {
|
|
||||||
include_archived: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = handler.handle(cmd, ctx).await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let resp = result.unwrap();
|
|
||||||
assert!(resp.success);
|
|
||||||
assert!(resp.messages[0].content.contains("No topics"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_list_sessions_with_items() {
|
|
||||||
let handler = create_test_handler();
|
|
||||||
let store = handler.store.clone();
|
|
||||||
let session = store.create_session("cli", Some("test")).unwrap();
|
|
||||||
|
|
||||||
// 创建一个 topic
|
|
||||||
store.create_topic(&session.id, "Test Topic", None).unwrap();
|
|
||||||
|
|
||||||
let ctx = CommandContext::new("test", "cli")
|
|
||||||
.with_session_id(&session.id);
|
|
||||||
let cmd = Command::ListSessions {
|
|
||||||
include_archived: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = handler.handle(cmd, ctx).await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let resp = result.unwrap();
|
|
||||||
assert!(resp.success);
|
|
||||||
assert!(resp.metadata.contains_key("topics"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_load_session_not_found() {
|
|
||||||
let handler = create_test_handler();
|
|
||||||
let store = handler.store.clone();
|
|
||||||
let session = store.create_session("cli", Some("test")).unwrap();
|
|
||||||
|
|
||||||
let ctx = CommandContext::new("test", "test")
|
|
||||||
.with_session_id(&session.id);
|
|
||||||
let cmd = Command::LoadSession {
|
|
||||||
session_id: "nonexistent".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = handler.handle(cmd, ctx).await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_load_session_success() {
|
|
||||||
let handler = create_test_handler();
|
|
||||||
let store = handler.store.clone();
|
|
||||||
let session = store.create_session("cli", Some("test")).unwrap();
|
|
||||||
let topic = store.create_topic(&session.id, "Test Topic", None).unwrap();
|
|
||||||
|
|
||||||
let ctx = CommandContext::new("test", "test")
|
|
||||||
.with_session_id(&session.id);
|
|
||||||
let cmd = Command::LoadSession {
|
|
||||||
session_id: topic.id.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = handler.handle(cmd, ctx).await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let resp = result.unwrap();
|
|
||||||
assert!(resp.success);
|
|
||||||
assert_eq!(resp.metadata.get("topic_id").unwrap(), &topic.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
110
src/command/handlers/switch_session.rs
Normal file
110
src/command/handlers/switch_session.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use crate::command::context::CommandContext;
|
||||||
|
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||||
|
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||||
|
use crate::command::Command;
|
||||||
|
use crate::gateway::session::SessionManager;
|
||||||
|
use crate::storage::SessionStore;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// 切换话题命令处理器
|
||||||
|
pub struct SwitchSessionCommandHandler {
|
||||||
|
store: Arc<SessionStore>,
|
||||||
|
session_manager: Option<SessionManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchSessionCommandHandler {
|
||||||
|
pub fn new(store: Arc<SessionStore>) -> Self {
|
||||||
|
Self { store, session_manager: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
|
||||||
|
self.session_manager = Some(session_manager);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandHandler for SwitchSessionCommandHandler {
|
||||||
|
fn can_handle(&self, cmd: &Command) -> bool {
|
||||||
|
matches!(cmd, Command::SwitchSession { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Option<CommandMetadata> {
|
||||||
|
Some(CommandMetadata {
|
||||||
|
name: "use",
|
||||||
|
description: "切换到指定话题",
|
||||||
|
usage: "/use <session_id>",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
cmd: Command,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
match cmd {
|
||||||
|
Command::SwitchSession { session_id } => {
|
||||||
|
handle_switch_session(self, session_id, ctx).await
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_switch_session(
|
||||||
|
handler: &SwitchSessionCommandHandler,
|
||||||
|
topic_id: String,
|
||||||
|
ctx: CommandContext,
|
||||||
|
) -> Result<CommandResponse, CommandError> {
|
||||||
|
let session_id = ctx.session_id.as_deref()
|
||||||
|
.ok_or_else(|| CommandError::new("NO_SESSION", "No active session"))?;
|
||||||
|
let chat_id = ctx.chat_id.as_deref()
|
||||||
|
.ok_or_else(|| CommandError::new("NO_CHAT_ID", "No chat_id in context"))?;
|
||||||
|
|
||||||
|
// 尝试解析为序号
|
||||||
|
let target_topic_id = if let Ok(index) = topic_id.parse::<usize>() {
|
||||||
|
let topics = handler
|
||||||
|
.store
|
||||||
|
.list_topics(session_id)
|
||||||
|
.map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?;
|
||||||
|
|
||||||
|
let index = index.saturating_sub(1);
|
||||||
|
if index >= topics.len() {
|
||||||
|
return Err(CommandError::new(
|
||||||
|
"INVALID_TOPIC_INDEX",
|
||||||
|
format!("Topic index {} is out of range (1-{})", index + 1, topics.len())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
topics[index].id.clone()
|
||||||
|
} else {
|
||||||
|
topic_id
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证目标话题存在
|
||||||
|
let topic = handler
|
||||||
|
.store
|
||||||
|
.get_topic(&target_topic_id)
|
||||||
|
.map_err(|e| CommandError::new("SWITCH_TOPIC_ERROR", e.to_string()))?
|
||||||
|
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", target_topic_id)))?;
|
||||||
|
|
||||||
|
// 如果有 SessionManager,实际切换话题历史
|
||||||
|
if let Some(ref session_manager) = handler.session_manager {
|
||||||
|
if let Some(session) = session_manager.get(&ctx.channel_name).await {
|
||||||
|
let mut session_guard = session.lock().await;
|
||||||
|
session_guard.switch_topic(chat_id, &target_topic_id)
|
||||||
|
.map_err(|e| CommandError::new("SWITCH_TOPIC_ERROR", e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"✓ Switched to topic: {} ({} messages)",
|
||||||
|
topic.title, topic.message_count
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(CommandResponse::success(ctx.request_id)
|
||||||
|
.with_message(MessageKind::Notification, &message)
|
||||||
|
.with_metadata("topic_id", &topic.id)
|
||||||
|
.with_metadata("title", &topic.title)
|
||||||
|
.with_metadata("message_count", &topic.message_count.to_string()))
|
||||||
|
}
|
||||||
@ -13,7 +13,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// 创建新话题(在同一个 Session 内)
|
/// 创建新话题(在同一个 Session 内)
|
||||||
CreateSession { title: Option<String> },
|
CreateSession { title: Option<String> },
|
||||||
/// 保存话题内容到 Markdown 文件
|
/// 保存当前话题内容到 Markdown 文件
|
||||||
|
SaveTopic { filepath: Option<String> },
|
||||||
|
/// 保存会话内容到 Markdown 文件
|
||||||
SaveSession {
|
SaveSession {
|
||||||
filepath: Option<String>,
|
filepath: Option<String>,
|
||||||
include_all: bool,
|
include_all: bool,
|
||||||
@ -26,6 +28,8 @@ pub enum Command {
|
|||||||
SwitchSession { session_id: String },
|
SwitchSession { session_id: String },
|
||||||
/// 获取当前话题信息
|
/// 获取当前话题信息
|
||||||
GetCurrentSession,
|
GetCurrentSession,
|
||||||
|
/// 显示所有支持的命令
|
||||||
|
Help,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@ -33,11 +37,13 @@ impl Command {
|
|||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Command::CreateSession { .. } => "create_session",
|
Command::CreateSession { .. } => "create_session",
|
||||||
|
Command::SaveTopic { .. } => "save_topic",
|
||||||
Command::SaveSession { .. } => "save_session",
|
Command::SaveSession { .. } => "save_session",
|
||||||
Command::ListSessions { .. } => "list_sessions",
|
Command::ListSessions { .. } => "list_sessions",
|
||||||
Command::LoadSession { .. } => "load_session",
|
Command::LoadSession { .. } => "load_session",
|
||||||
Command::SwitchSession { .. } => "switch_session",
|
Command::SwitchSession { .. } => "switch_session",
|
||||||
Command::GetCurrentSession => "get_current_session",
|
Command::GetCurrentSession => "get_current_session",
|
||||||
|
Command::Help => "help",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ impl CliSessionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 创建指定通道的会话
|
/// 创建指定通道的会话
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn create_with_channel(
|
pub(crate) fn create_with_channel(
|
||||||
&self,
|
&self,
|
||||||
channel_name: &str,
|
channel_name: &str,
|
||||||
@ -30,12 +31,14 @@ impl CliSessionService {
|
|||||||
.map_err(|err| AgentError::Other(format!("create session error: {}", err)))
|
.map_err(|err| AgentError::Other(format!("create session error: {}", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn get(&self, session_id: &str) -> Result<Option<SessionRecord>, AgentError> {
|
pub(crate) fn get(&self, session_id: &str) -> Result<Option<SessionRecord>, AgentError> {
|
||||||
self.store
|
self.store
|
||||||
.get_session(session_id)
|
.get_session(session_id)
|
||||||
.map_err(|err| AgentError::Other(format!("get session error: {}", err)))
|
.map_err(|err| AgentError::Other(format!("get session error: {}", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn list(&self, include_archived: bool) -> Result<Vec<SessionRecord>, AgentError> {
|
pub(crate) fn list(&self, include_archived: bool) -> Result<Vec<SessionRecord>, AgentError> {
|
||||||
self.store
|
self.store
|
||||||
.list_sessions("cli", include_archived)
|
.list_sessions("cli", include_archived)
|
||||||
@ -43,6 +46,7 @@ impl CliSessionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 列出指定通道的会话
|
/// 列出指定通道的会话
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn list_by_channel(
|
pub(crate) fn list_by_channel(
|
||||||
&self,
|
&self,
|
||||||
channel_name: &str,
|
channel_name: &str,
|
||||||
@ -53,24 +57,28 @@ impl CliSessionService {
|
|||||||
.map_err(|err| AgentError::Other(format!("list sessions error: {}", err)))
|
.map_err(|err| AgentError::Other(format!("list sessions error: {}", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn rename(&self, session_id: &str, title: &str) -> Result<(), AgentError> {
|
pub(crate) fn rename(&self, session_id: &str, title: &str) -> Result<(), AgentError> {
|
||||||
self.store
|
self.store
|
||||||
.rename_session(session_id, title)
|
.rename_session(session_id, title)
|
||||||
.map_err(|err| AgentError::Other(format!("rename session error: {}", err)))
|
.map_err(|err| AgentError::Other(format!("rename session error: {}", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn archive(&self, session_id: &str) -> Result<(), AgentError> {
|
pub(crate) fn archive(&self, session_id: &str) -> Result<(), AgentError> {
|
||||||
self.store
|
self.store
|
||||||
.archive_session(session_id)
|
.archive_session(session_id)
|
||||||
.map_err(|err| AgentError::Other(format!("archive session error: {}", err)))
|
.map_err(|err| AgentError::Other(format!("archive session error: {}", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn delete(&self, session_id: &str) -> Result<(), AgentError> {
|
pub(crate) fn delete(&self, session_id: &str) -> Result<(), AgentError> {
|
||||||
self.store
|
self.store
|
||||||
.delete_session(session_id)
|
.delete_session(session_id)
|
||||||
.map_err(|err| AgentError::Other(format!("delete session error: {}", err)))
|
.map_err(|err| AgentError::Other(format!("delete session error: {}", err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn clear_messages(&self, session_id: &str) -> Result<(), AgentError> {
|
pub(crate) fn clear_messages(&self, session_id: &str) -> Result<(), AgentError> {
|
||||||
self.store
|
self.store
|
||||||
.clear_messages(session_id)
|
.clear_messages(session_id)
|
||||||
|
|||||||
@ -7,9 +7,14 @@ use crate::bus::{InboundMessage, MessageBus, OutboundMessage};
|
|||||||
use crate::command::adapter::InputAdapter;
|
use crate::command::adapter::InputAdapter;
|
||||||
use crate::command::adapters::channel::ChannelInputAdapter;
|
use crate::command::adapters::channel::ChannelInputAdapter;
|
||||||
use crate::command::handler::CommandRouter;
|
use crate::command::handler::CommandRouter;
|
||||||
|
use crate::command::handlers::get_current::GetCurrentSessionCommandHandler;
|
||||||
|
use crate::command::handlers::help::HelpCommandHandler;
|
||||||
|
use crate::command::handlers::list_sessions::ListSessionsCommandHandler;
|
||||||
|
use crate::command::handlers::load_session::LoadSessionCommandHandler;
|
||||||
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
||||||
|
use crate::command::handlers::save_topic::SaveTopicCommandHandler;
|
||||||
use crate::command::handlers::session::SessionCommandHandler;
|
use crate::command::handlers::session::SessionCommandHandler;
|
||||||
use crate::command::handlers::session_query::SessionQueryCommandHandler;
|
use crate::command::handlers::switch_session::SwitchSessionCommandHandler;
|
||||||
use crate::config::LLMProviderConfig;
|
use crate::config::LLMProviderConfig;
|
||||||
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
|
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
|
||||||
use crate::skills::SkillPromptProvider;
|
use crate::skills::SkillPromptProvider;
|
||||||
@ -42,13 +47,21 @@ impl InboundProcessor {
|
|||||||
.with_session_manager(session_manager.clone());
|
.with_session_manager(session_manager.clone());
|
||||||
command_router.register(Box::new(session_handler));
|
command_router.register(Box::new(session_handler));
|
||||||
|
|
||||||
// 注册 session_query 处理器
|
// 注册 list_sessions 处理器
|
||||||
let session_query_handler = SessionQueryCommandHandler::new(store)
|
command_router.register(Box::new(ListSessionsCommandHandler::new(store.clone())));
|
||||||
|
|
||||||
|
// 注册 switch_session 处理器
|
||||||
|
let switch_handler = SwitchSessionCommandHandler::new(store.clone())
|
||||||
.with_session_manager(session_manager.clone());
|
.with_session_manager(session_manager.clone());
|
||||||
command_router.register(Box::new(session_query_handler));
|
command_router.register(Box::new(switch_handler));
|
||||||
|
|
||||||
|
// 注册 get_current 处理器
|
||||||
|
command_router.register(Box::new(GetCurrentSessionCommandHandler::new(store.clone())));
|
||||||
|
|
||||||
|
// 注册 load_session 处理器
|
||||||
|
command_router.register(Box::new(LoadSessionCommandHandler::new(store.clone())));
|
||||||
|
|
||||||
// 注册 save_session 处理器
|
// 注册 save_session 处理器
|
||||||
let store = session_manager.store();
|
|
||||||
let skills = session_manager.skills();
|
let skills = session_manager.skills();
|
||||||
let prompt_repository = session_manager.store().clone();
|
let prompt_repository = session_manager.store().clone();
|
||||||
let system_prompt_provider: Arc<dyn crate::agent::SystemPromptProvider> = Arc::new(CompositeSystemPromptProvider::new(vec![
|
let system_prompt_provider: Arc<dyn crate::agent::SystemPromptProvider> = Arc::new(CompositeSystemPromptProvider::new(vec![
|
||||||
@ -60,10 +73,20 @@ impl InboundProcessor {
|
|||||||
Box::new(SkillPromptProvider::new(skills)),
|
Box::new(SkillPromptProvider::new(skills)),
|
||||||
]));
|
]));
|
||||||
command_router.register(Box::new(SaveSessionCommandHandler::new(
|
command_router.register(Box::new(SaveSessionCommandHandler::new(
|
||||||
store,
|
store.clone(),
|
||||||
|
system_prompt_provider.clone(),
|
||||||
|
)));
|
||||||
|
|
||||||
|
// 注册 save_topic 处理器
|
||||||
|
command_router.register(Box::new(SaveTopicCommandHandler::new(
|
||||||
|
store.clone(),
|
||||||
system_prompt_provider,
|
system_prompt_provider,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
// 注册 help 处理器(最后注册,获取所有已注册命令的元数据)
|
||||||
|
let metadata = command_router.metadata_arc();
|
||||||
|
command_router.register(Box::new(HelpCommandHandler::new(metadata)));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
bus,
|
bus,
|
||||||
session_manager,
|
session_manager,
|
||||||
@ -122,13 +145,10 @@ impl InboundProcessor {
|
|||||||
// 计算正确的 session_id(根据 channel_name 和 chat_id)
|
// 计算正确的 session_id(根据 channel_name 和 chat_id)
|
||||||
let session_id = persistent_session_id(&inbound.channel, &inbound.chat_id);
|
let session_id = persistent_session_id(&inbound.channel, &inbound.chat_id);
|
||||||
|
|
||||||
// 获取当前话题(如果有 Session)
|
// 获取当前话题(封装了 session 创建逻辑)
|
||||||
let current_topic = if let Some(session) = self.session_manager.get(&inbound.channel).await {
|
let current_topic = self.session_manager
|
||||||
let guard = session.lock().await;
|
.get_current_topic(&inbound.channel, &inbound.chat_id)
|
||||||
guard.current_topic(&inbound.chat_id).map(|s| s.to_string())
|
.await?;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用 ChannelInputAdapter 尝试解析命令
|
// 使用 ChannelInputAdapter 尝试解析命令
|
||||||
let adapter = ChannelInputAdapter::new();
|
let adapter = ChannelInputAdapter::new();
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use crate::storage::{
|
|||||||
};
|
};
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
DefaultSubAgentRuntime, InMemoryTaskRepository, NoopSessionMessageSender,
|
DefaultSubAgentRuntime, InMemoryTaskRepository, NoopSessionMessageSender,
|
||||||
SessionMessageSender, SubAgentRuntime, SubAgentRuntimeConfig, ToolRegistry,
|
SessionMessageSender, SubAgentRuntimeConfig, ToolRegistry,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::agent_factory::AgentFactory;
|
use super::agent_factory::AgentFactory;
|
||||||
@ -89,7 +89,6 @@ pub(crate) fn build_session_manager_with_sender(
|
|||||||
scheduler_jobs,
|
scheduler_jobs,
|
||||||
skill_events.clone(),
|
skill_events.clone(),
|
||||||
session_message_sender.clone(),
|
session_message_sender.clone(),
|
||||||
conversations.clone(),
|
|
||||||
known_agents,
|
known_agents,
|
||||||
default_timezone,
|
default_timezone,
|
||||||
disabled_tools,
|
disabled_tools,
|
||||||
|
|||||||
@ -547,6 +547,36 @@ impl SessionManager {
|
|||||||
self.lifecycle.get(channel_name).await
|
self.lifecycle.get(channel_name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取指定 chat 的当前话题(确保 session 存在,自动从数据库恢复)
|
||||||
|
pub async fn get_current_topic(&self, channel_name: &str, chat_id: &str) -> Result<Option<String>, AgentError> {
|
||||||
|
self.ensure_session(channel_name).await?;
|
||||||
|
if let Some(session) = self.get(channel_name).await {
|
||||||
|
let mut guard = session.lock().await;
|
||||||
|
|
||||||
|
// 如果内存中没有当前话题,从数据库恢复最近活跃的话题
|
||||||
|
if guard.current_topic(chat_id).is_none() {
|
||||||
|
let session_id = guard.persistent_session_id(chat_id);
|
||||||
|
let topics = self.store.list_topics(&session_id)
|
||||||
|
.map_err(|e| AgentError::Other(format!("Failed to list topics: {}", e)))?;
|
||||||
|
|
||||||
|
if let Some(latest_topic) = topics.first() {
|
||||||
|
// 设置最近活跃的话题为当前话题
|
||||||
|
guard.set_current_topic(chat_id, Some(latest_topic.id.clone()));
|
||||||
|
tracing::info!(
|
||||||
|
chat_id = %chat_id,
|
||||||
|
topic_id = %latest_topic.id,
|
||||||
|
topic_title = %latest_topic.title,
|
||||||
|
"Restored current topic from database"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(guard.current_topic(chat_id).map(|s| s.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 更新最后活跃时间
|
/// 更新最后活跃时间
|
||||||
pub async fn touch(&self, channel_name: &str) {
|
pub async fn touch(&self, channel_name: &str) {
|
||||||
self.lifecycle.touch(channel_name).await;
|
self.lifecycle.touch(channel_name).await;
|
||||||
|
|||||||
@ -187,8 +187,10 @@ impl SessionHistory {
|
|||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), AgentError> {
|
||||||
let session_id = self.persistent_session_id(chat_id);
|
let session_id = self.persistent_session_id(chat_id);
|
||||||
|
// 获取当前话题 ID,用于关联消息
|
||||||
|
let topic_id = self.chat_topic_ids.get(chat_id).map(|s| s.as_str());
|
||||||
self.conversations
|
self.conversations
|
||||||
.append_message(&session_id, &message)
|
.append_message_with_topic(&session_id, topic_id, &message)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
AgentError::Other(format!("append message persistence error: {}", err))
|
AgentError::Other(format!("append message persistence error: {}", err))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::config::TaskConfig;
|
use crate::config::TaskConfig;
|
||||||
use crate::skills::SkillRuntime;
|
use crate::skills::SkillRuntime;
|
||||||
use crate::storage::{ConversationRepository, MemoryRepository, SchedulerJobRepository, SkillEventRepository};
|
use crate::storage::{MemoryRepository, SchedulerJobRepository, SkillEventRepository};
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool,
|
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool,
|
||||||
HttpRequestTool, MemoryManageTool, MemorySearchTool,
|
HttpRequestTool, MemoryManageTool, MemorySearchTool,
|
||||||
@ -18,7 +18,6 @@ pub(crate) struct ToolRegistryFactory {
|
|||||||
scheduler_jobs: Arc<dyn SchedulerJobRepository>,
|
scheduler_jobs: Arc<dyn SchedulerJobRepository>,
|
||||||
skill_events: Arc<dyn SkillEventRepository>,
|
skill_events: Arc<dyn SkillEventRepository>,
|
||||||
session_message_sender: Arc<dyn SessionMessageSender>,
|
session_message_sender: Arc<dyn SessionMessageSender>,
|
||||||
conversations: Arc<dyn ConversationRepository>,
|
|
||||||
known_agents: HashSet<String>,
|
known_agents: HashSet<String>,
|
||||||
default_timezone: String,
|
default_timezone: String,
|
||||||
disabled_tools: HashSet<String>,
|
disabled_tools: HashSet<String>,
|
||||||
@ -33,7 +32,6 @@ impl ToolRegistryFactory {
|
|||||||
scheduler_jobs: Arc<dyn SchedulerJobRepository>,
|
scheduler_jobs: Arc<dyn SchedulerJobRepository>,
|
||||||
skill_events: Arc<dyn SkillEventRepository>,
|
skill_events: Arc<dyn SkillEventRepository>,
|
||||||
session_message_sender: Arc<dyn SessionMessageSender>,
|
session_message_sender: Arc<dyn SessionMessageSender>,
|
||||||
conversations: Arc<dyn ConversationRepository>,
|
|
||||||
known_agents: HashSet<String>,
|
known_agents: HashSet<String>,
|
||||||
default_timezone: String,
|
default_timezone: String,
|
||||||
disabled_tools: HashSet<String>,
|
disabled_tools: HashSet<String>,
|
||||||
@ -45,7 +43,6 @@ impl ToolRegistryFactory {
|
|||||||
scheduler_jobs,
|
scheduler_jobs,
|
||||||
skill_events,
|
skill_events,
|
||||||
session_message_sender,
|
session_message_sender,
|
||||||
conversations,
|
|
||||||
known_agents,
|
known_agents,
|
||||||
default_timezone,
|
default_timezone,
|
||||||
disabled_tools,
|
disabled_tools,
|
||||||
|
|||||||
@ -5,9 +5,13 @@ use crate::command::adapter::{InputAdapter, OutputAdapter};
|
|||||||
use crate::command::adapters::websocket::{WebSocketInputAdapter, WebSocketOutputAdapter};
|
use crate::command::adapters::websocket::{WebSocketInputAdapter, WebSocketOutputAdapter};
|
||||||
use crate::command::context::CommandContext;
|
use crate::command::context::CommandContext;
|
||||||
use crate::command::handler::CommandRouter;
|
use crate::command::handler::CommandRouter;
|
||||||
|
use crate::command::handlers::get_current::GetCurrentSessionCommandHandler;
|
||||||
|
use crate::command::handlers::help::HelpCommandHandler;
|
||||||
|
use crate::command::handlers::list_sessions::ListSessionsCommandHandler;
|
||||||
|
use crate::command::handlers::load_session::LoadSessionCommandHandler;
|
||||||
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
||||||
use crate::command::handlers::session::SessionCommandHandler;
|
use crate::command::handlers::session::SessionCommandHandler;
|
||||||
use crate::command::handlers::session_query::SessionQueryCommandHandler;
|
use crate::command::handlers::switch_session::SwitchSessionCommandHandler;
|
||||||
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
|
use crate::gateway::agent_prompt_provider::AgentPromptProvider;
|
||||||
use crate::protocol::{WsInbound, WsOutbound, parse_inbound, serialize_outbound};
|
use crate::protocol::{WsInbound, WsOutbound, parse_inbound, serialize_outbound};
|
||||||
use crate::skills::SkillPromptProvider;
|
use crate::skills::SkillPromptProvider;
|
||||||
@ -224,18 +228,27 @@ async fn handle_inbound(
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
let mut router = CommandRouter::new();
|
let mut router = CommandRouter::new();
|
||||||
// 注册 Session 处理器,添加 SessionManager
|
// 注册 Session 处理器
|
||||||
let session_handler = SessionCommandHandler::new(store.clone())
|
let session_handler = SessionCommandHandler::new(store.clone())
|
||||||
.with_session_manager(state.session_manager.clone());
|
.with_session_manager(state.session_manager.clone());
|
||||||
router.register(Box::new(session_handler));
|
router.register(Box::new(session_handler));
|
||||||
// 注册 SessionQuery 处理器
|
// 注册 list_sessions 处理器
|
||||||
let session_query_handler = SessionQueryCommandHandler::new(store.clone())
|
router.register(Box::new(ListSessionsCommandHandler::new(store.clone())));
|
||||||
|
// 注册 switch_session 处理器
|
||||||
|
let switch_handler = SwitchSessionCommandHandler::new(store.clone())
|
||||||
.with_session_manager(state.session_manager.clone());
|
.with_session_manager(state.session_manager.clone());
|
||||||
router.register(Box::new(session_query_handler));
|
router.register(Box::new(switch_handler));
|
||||||
|
// 注册 get_current 处理器
|
||||||
|
router.register(Box::new(GetCurrentSessionCommandHandler::new(store.clone())));
|
||||||
|
// 注册 load_session 处理器
|
||||||
|
router.register(Box::new(LoadSessionCommandHandler::new(store.clone())));
|
||||||
router.register(Box::new(SaveSessionCommandHandler::new(
|
router.register(Box::new(SaveSessionCommandHandler::new(
|
||||||
store,
|
store.clone(),
|
||||||
system_prompt_provider,
|
system_prompt_provider.clone(),
|
||||||
)));
|
)));
|
||||||
|
// 注册 help 处理器
|
||||||
|
let metadata = router.metadata_arc();
|
||||||
|
router.register(Box::new(HelpCommandHandler::new(metadata)));
|
||||||
|
|
||||||
// 构建命令上下文
|
// 构建命令上下文
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
|||||||
@ -15,6 +15,13 @@ pub trait ConversationRepository: Send + Sync + 'static {
|
|||||||
|
|
||||||
fn append_message(&self, session_id: &str, message: &ChatMessage) -> Result<(), StorageError>;
|
fn append_message(&self, session_id: &str, message: &ChatMessage) -> Result<(), StorageError>;
|
||||||
|
|
||||||
|
fn append_message_with_topic(
|
||||||
|
&self,
|
||||||
|
session_id: &str,
|
||||||
|
topic_id: Option<&str>,
|
||||||
|
message: &ChatMessage,
|
||||||
|
) -> Result<(), StorageError>;
|
||||||
|
|
||||||
fn clear_messages(&self, session_id: &str) -> Result<(), StorageError>;
|
fn clear_messages(&self, session_id: &str) -> Result<(), StorageError>;
|
||||||
|
|
||||||
fn reset_session(&self, session_id: &str) -> Result<(), StorageError>;
|
fn reset_session(&self, session_id: &str) -> Result<(), StorageError>;
|
||||||
@ -140,6 +147,15 @@ impl ConversationRepository for super::SessionStore {
|
|||||||
super::SessionStore::append_message(self, session_id, message)
|
super::SessionStore::append_message(self, session_id, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_message_with_topic(
|
||||||
|
&self,
|
||||||
|
session_id: &str,
|
||||||
|
topic_id: Option<&str>,
|
||||||
|
message: &ChatMessage,
|
||||||
|
) -> Result<(), StorageError> {
|
||||||
|
super::SessionStore::append_message_with_topic(self, session_id, topic_id, message)
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_messages(&self, session_id: &str) -> Result<(), StorageError> {
|
fn clear_messages(&self, session_id: &str) -> Result<(), StorageError> {
|
||||||
super::SessionStore::clear_messages(self, session_id)
|
super::SessionStore::clear_messages(self, session_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use crate::tools::{ToolContext, ToolRegistry};
|
|||||||
use super::error::TaskError;
|
use super::error::TaskError;
|
||||||
use super::prompt::{extract_summary, SubagentPromptBuilder};
|
use super::prompt::{extract_summary, SubagentPromptBuilder};
|
||||||
use super::repository::TaskRepository;
|
use super::repository::TaskRepository;
|
||||||
use super::types::{SubagentType, TaskDefinition, TaskHandle, TaskSession, TaskSessionState, TaskToolResult};
|
use super::types::{SubagentType, TaskDefinition, TaskSession, TaskToolResult};
|
||||||
|
|
||||||
/// 子代理运行时配置
|
/// 子代理运行时配置
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user