diff --git a/src/command/adapters/channel.rs b/src/command/adapters/channel.rs index 54f00da..292d179 100644 --- a/src/command/adapters/channel.rs +++ b/src/command/adapters/channel.rs @@ -120,11 +120,11 @@ impl InputAdapter for ChannelInputAdapter { })); } - // 解析 /use 命令 - 切换会话(支持 session_id 或序号) - if let Some(session_id) = trimmed.strip_prefix("/use ") { - let session_id = session_id.trim(); - return Ok(Some(Command::SwitchSession { - session_id: session_id.to_string(), + // 解析 /use 命令 - 切换话题(支持 topic_id 或序号) + if let Some(topic_id) = trimmed.strip_prefix("/use ") { + let topic_id = topic_id.trim(); + return Ok(Some(Command::SwitchTopic { + topic_id: topic_id.to_string(), })); } diff --git a/src/command/adapters/cli.rs b/src/command/adapters/cli.rs index 9783a82..5e3dca0 100644 --- a/src/command/adapters/cli.rs +++ b/src/command/adapters/cli.rs @@ -121,11 +121,11 @@ impl InputAdapter for CliInputAdapter { })); } - // 解析 /use 命令 - 切换会话(支持 session_id 或序号) - if let Some(session_id) = trimmed.strip_prefix("/use ") { - let session_id = session_id.trim(); - return Ok(Some(Command::SwitchSession { - session_id: session_id.to_string(), + // 解析 /use 命令 - 切换话题(支持 topic_id 或序号) + if let Some(topic_id) = trimmed.strip_prefix("/use ") { + let topic_id = topic_id.trim(); + return Ok(Some(Command::SwitchTopic { + topic_id: topic_id.to_string(), })); } diff --git a/src/command/handlers/load_session.rs b/src/command/handlers/load_topic.rs similarity index 79% rename from src/command/handlers/load_session.rs rename to src/command/handlers/load_topic.rs index 19dca42..5a85966 100644 --- a/src/command/handlers/load_session.rs +++ b/src/command/handlers/load_topic.rs @@ -7,27 +7,27 @@ use async_trait::async_trait; use std::sync::Arc; /// 加载话题命令处理器 -pub struct LoadSessionCommandHandler { +pub struct LoadTopicCommandHandler { store: Arc, } -impl LoadSessionCommandHandler { +impl LoadTopicCommandHandler { pub fn new(store: Arc) -> Self { Self { store } } } #[async_trait] -impl CommandHandler for LoadSessionCommandHandler { +impl CommandHandler for LoadTopicCommandHandler { fn can_handle(&self, cmd: &Command) -> bool { - matches!(cmd, Command::LoadSession { .. }) + matches!(cmd, Command::LoadTopic { .. }) } fn metadata(&self) -> Option { Some(CommandMetadata { name: "load", description: "加载指定话题", - usage: "/load ", + usage: "/load ", }) } @@ -37,16 +37,16 @@ impl CommandHandler for LoadSessionCommandHandler { ctx: CommandContext, ) -> Result { match cmd { - Command::LoadSession { session_id } => { - handle_load_session(self, session_id, ctx).await + Command::LoadTopic { topic_id } => { + handle_load_topic(self, topic_id, ctx).await } _ => unreachable!(), } } } -async fn handle_load_session( - handler: &LoadSessionCommandHandler, +async fn handle_load_topic( + handler: &LoadTopicCommandHandler, topic_id: String, ctx: CommandContext, ) -> Result { @@ -61,4 +61,4 @@ async fn handle_load_session( .with_metadata("topic_id", &topic.id) .with_metadata("title", &topic.title) .with_metadata("message_count", &topic.message_count.to_string())) -} \ No newline at end of file +} diff --git a/src/command/handlers/mod.rs b/src/command/handlers/mod.rs index fae3d06..63c770b 100644 --- a/src/command/handlers/mod.rs +++ b/src/command/handlers/mod.rs @@ -4,11 +4,11 @@ pub mod list_channels; pub mod list_sessions; pub mod list_sessions_by_channel; pub mod list_topics; -pub mod load_session; +pub mod load_topic; pub mod save_session; pub mod save_topic; pub mod session; -pub mod switch_session; +pub mod switch_topic; // 导出公共函数供其他模块复用 pub use save_session::{ diff --git a/src/command/handlers/switch_session.rs b/src/command/handlers/switch_topic.rs similarity index 89% rename from src/command/handlers/switch_session.rs rename to src/command/handlers/switch_topic.rs index a7629f0..7c22146 100644 --- a/src/command/handlers/switch_session.rs +++ b/src/command/handlers/switch_topic.rs @@ -8,12 +8,12 @@ use async_trait::async_trait; use std::sync::Arc; /// 切换话题命令处理器 -pub struct SwitchSessionCommandHandler { +pub struct SwitchTopicCommandHandler { store: Arc, session_manager: Option, } -impl SwitchSessionCommandHandler { +impl SwitchTopicCommandHandler { pub fn new(store: Arc) -> Self { Self { store, session_manager: None } } @@ -25,16 +25,16 @@ impl SwitchSessionCommandHandler { } #[async_trait] -impl CommandHandler for SwitchSessionCommandHandler { +impl CommandHandler for SwitchTopicCommandHandler { fn can_handle(&self, cmd: &Command) -> bool { - matches!(cmd, Command::SwitchSession { .. }) + matches!(cmd, Command::SwitchTopic { .. }) } fn metadata(&self) -> Option { Some(CommandMetadata { name: "use", description: "切换到指定话题", - usage: "/use ", + usage: "/use ", }) } @@ -44,16 +44,16 @@ impl CommandHandler for SwitchSessionCommandHandler { ctx: CommandContext, ) -> Result { match cmd { - Command::SwitchSession { session_id } => { - handle_switch_session(self, session_id, ctx).await + Command::SwitchTopic { topic_id } => { + handle_switch_topic(self, topic_id, ctx).await } _ => unreachable!(), } } } -async fn handle_switch_session( - handler: &SwitchSessionCommandHandler, +async fn handle_switch_topic( + handler: &SwitchTopicCommandHandler, topic_id: String, ctx: CommandContext, ) -> Result { @@ -112,4 +112,4 @@ async fn handle_switch_session( .with_metadata("topic_id", &topic.id) .with_metadata("title", &topic.title) .with_metadata("message_count", &msg_count.to_string())) -} \ No newline at end of file +} diff --git a/src/command/mod.rs b/src/command/mod.rs index 1ee46b7..0d3a050 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -27,9 +27,9 @@ pub enum Command { /// 列出当前 Session 的所有话题 ListSessions { include_archived: bool }, /// 加载指定话题 - LoadSession { session_id: String }, + LoadTopic { topic_id: String }, /// 切换到指定话题(清理当前历史并加载新话题) - SwitchSession { session_id: String }, + SwitchTopic { topic_id: String }, /// 获取当前话题信息 GetCurrentSession, /// 显示所有支持的命令 @@ -53,8 +53,8 @@ impl Command { Command::SaveTopic { .. } => "save_topic", Command::SaveSession { .. } => "save_session", Command::ListSessions { .. } => "list_sessions", - Command::LoadSession { .. } => "load_session", - Command::SwitchSession { .. } => "switch_session", + Command::LoadTopic { .. } => "load_topic", + Command::SwitchTopic { .. } => "switch_topic", Command::GetCurrentSession => "get_current_session", Command::Help => "help", Command::ListChannels => "list_channels", diff --git a/src/gateway/processor.rs b/src/gateway/processor.rs index 8f25e7f..aaf7128 100644 --- a/src/gateway/processor.rs +++ b/src/gateway/processor.rs @@ -10,11 +10,11 @@ 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::load_topic::LoadTopicCommandHandler; use crate::command::handlers::save_session::SaveSessionCommandHandler; use crate::command::handlers::save_topic::SaveTopicCommandHandler; use crate::command::handlers::session::SessionCommandHandler; -use crate::command::handlers::switch_session::SwitchSessionCommandHandler; +use crate::command::handlers::switch_topic::SwitchTopicCommandHandler; use crate::config::LLMProviderConfig; use crate::gateway::agent_prompt_provider::AgentPromptProvider; use crate::providers::{create_provider, ProviderRuntimeConfig}; @@ -52,8 +52,8 @@ impl InboundProcessor { // 注册 list_sessions 处理器 command_router.register(Box::new(ListSessionsCommandHandler::new(store.clone()))); - // 注册 switch_session 处理器 - let switch_handler = SwitchSessionCommandHandler::new(store.clone()) + // 注册 switch_topic 处理器 + let switch_handler = SwitchTopicCommandHandler::new(store.clone()) .with_session_manager(session_manager.clone()); command_router.register(Box::new(switch_handler)); @@ -76,8 +76,8 @@ impl InboundProcessor { .with_system_prompt_provider(system_prompt_provider.clone()) )); - // 注册 load_session 处理器 - command_router.register(Box::new(LoadSessionCommandHandler::new(store.clone()))); + // 注册 load_topic 处理器 + command_router.register(Box::new(LoadTopicCommandHandler::new(store.clone()))); // 注册 save_session 处理器 command_router.register(Box::new(SaveSessionCommandHandler::new( diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 49a6a16..f235751 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -11,10 +11,10 @@ use crate::command::handlers::list_channels::ListChannelsCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler; use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler; use crate::command::handlers::list_topics::ListTopicsCommandHandler; -use crate::command::handlers::load_session::LoadSessionCommandHandler; +use crate::command::handlers::load_topic::LoadTopicCommandHandler; use crate::command::handlers::save_session::SaveSessionCommandHandler; use crate::command::handlers::session::SessionCommandHandler; -use crate::command::handlers::switch_session::SwitchSessionCommandHandler; +use crate::command::handlers::switch_topic::SwitchTopicCommandHandler; use crate::gateway::agent_prompt_provider::AgentPromptProvider; use crate::protocol::{WsInbound, WsOutbound, parse_inbound, serialize_outbound}; use crate::skills::SkillPromptProvider; @@ -291,14 +291,14 @@ async fn handle_inbound( router.register(Box::new(ListChannelsCommandHandler::new())); // 注册 list_topics 处理器 router.register(Box::new(ListTopicsCommandHandler::new(store.clone()))); - // 注册 switch_session 处理器 - let switch_handler = SwitchSessionCommandHandler::new(store.clone()) + // 注册 switch_topic 处理器 + let switch_handler = SwitchTopicCommandHandler::new(store.clone()) .with_session_manager(state.session_manager.clone()); 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()))); + // 注册 load_topic 处理器 + router.register(Box::new(LoadTopicCommandHandler::new(store.clone()))); router.register(Box::new(SaveSessionCommandHandler::new( store.clone(), state.task_repository.clone(), @@ -397,6 +397,82 @@ fn resolve_ws_sender_id(sender_id: Option<&str>, runtime_session_id: &str) -> St .unwrap_or_else(|| runtime_session_id.to_string()) } +/// 加载并发送话题历史消息 +async fn send_topic_history( + store: &Arc, + topic_id: &str, + sender: &mpsc::Sender, +) -> Result<(), Box> { + // 加载话题消息 + let messages = store.load_messages_for_topic(topic_id)?; + + tracing::info!(topic_id = %topic_id, message_count = messages.len(), "Sending topic history"); + + // 将消息转换为 WsOutbound 并发送 + for msg in messages { + let outbound = chat_message_to_ws_outbound(&msg); + if let Some(outbound) = outbound { + let _ = sender.send(outbound).await; + } + } + + Ok(()) +} + +/// 将 ChatMessage 转换为 WsOutbound +fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option { + use crate::bus::message::ToolMessageState; + + match msg.role.as_str() { + "assistant" => { + if let Some(tool_calls) = &msg.tool_calls { + // 如果有 tool_calls,发送 ToolCall 消息 + if let Some(tool_call) = tool_calls.first() { + return Some(WsOutbound::ToolCall { + id: msg.id.clone(), + tool_call_id: tool_call.id.clone(), + tool_name: tool_call.name.clone(), + arguments: tool_call.arguments.clone(), + content: format!("{}\nargs: {}", tool_call.name, tool_call.arguments), + role: msg.role.clone(), + }); + } + } + // 普通助手消息 + Some(WsOutbound::AssistantResponse { + id: msg.id.clone(), + content: msg.content.clone(), + role: msg.role.clone(), + }) + } + "tool" => { + let tool_state = msg.tool_state.as_ref().unwrap_or(&ToolMessageState::Completed); + match tool_state { + ToolMessageState::Completed => Some(WsOutbound::ToolResult { + id: msg.id.clone(), + tool_call_id: msg.tool_call_id.clone().unwrap_or_default(), + tool_name: msg.tool_name.clone().unwrap_or_default(), + content: msg.content.clone(), + role: msg.role.clone(), + }), + ToolMessageState::PendingUserAction => Some(WsOutbound::ToolPending { + id: msg.id.clone(), + tool_call_id: msg.tool_call_id.clone().unwrap_or_default(), + tool_name: msg.tool_name.clone().unwrap_or_default(), + content: msg.content.clone(), + role: msg.role.clone(), + resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(), + }), + } + } + "user" => { + // 用户消息不通过 WsOutbound 发送,前端自己维护 + None + } + _ => None, + } +} + #[cfg(test)] mod tests { use super::resolve_ws_sender_id; diff --git a/web/src/App.tsx b/web/src/App.tsx index 924389f..c78488d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -85,7 +85,7 @@ function App() { break case 'use': if (args[0]) { - cmd = { type: 'switch_session', session_id: args[0] } + cmd = { type: 'switch_topic', topic_id: args[0] } } else { alert('Usage: /use ') return diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index afe6a4c..c9c8caa 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -244,8 +244,8 @@ export function useChat(): UseChatReturn { const handleCommand = useCallback((command: Command) => { switch (command.type) { case 'create_session': - case 'switch_session': - case 'load_session': + case 'switch_topic': + case 'load_topic': case 'list_sessions': case 'list_sessions_by_channel': case 'list_topics': @@ -273,8 +273,8 @@ export function useChat(): UseChatReturn { const switchTopic = useCallback((topicId: string): Command => { return { - type: 'switch_session', - session_id: topicId, + type: 'switch_topic', + topic_id: topicId, } }, []) diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 61f7fbc..63950c3 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -170,9 +170,9 @@ export interface ListSessionsCommand { include_archived: boolean } -export interface SwitchSessionCommand { - type: 'switch_session' - session_id: string +export interface SwitchTopicCommand { + type: 'switch_topic' + topic_id: string } export interface SaveTopicCommand { @@ -188,9 +188,9 @@ export interface SaveSessionCommand { include_subagents: boolean } -export interface LoadSessionCommand { - type: 'load_session' - session_id: string +export interface LoadTopicCommand { + type: 'load_topic' + topic_id: string } export interface GetCurrentSessionCommand { @@ -219,10 +219,10 @@ export interface ListTopicsCommand { export type Command = | CreateSessionCommand | ListSessionsCommand - | SwitchSessionCommand + | SwitchTopicCommand | SaveTopicCommand | SaveSessionCommand - | LoadSessionCommand + | LoadTopicCommand | GetCurrentSessionCommand | HelpCommand | ListChannelsCommand