From b5e2886068e9b12461b9960757ca5b9d117c7e2b Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sun, 7 Jun 2026 14:09:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=AF=9D=E9=A2=98=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=A4=84=E7=90=86=E5=99=A8=E5=92=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/command/handlers/delete_topic.rs | 181 +++++++++++++++++++++++ src/command/handlers/mod.rs | 1 + src/command/mod.rs | 3 + src/gateway/processor.rs | 7 + src/gateway/ws.rs | 6 + web/src/App.tsx | 17 +++ web/src/components/Sidebar/TopicList.tsx | 111 ++++++++++---- web/src/hooks/useChat.ts | 10 ++ web/src/types/protocol.ts | 6 + 9 files changed, 310 insertions(+), 32 deletions(-) create mode 100644 src/command/handlers/delete_topic.rs diff --git a/src/command/handlers/delete_topic.rs b/src/command/handlers/delete_topic.rs new file mode 100644 index 0000000..fabe5fa --- /dev/null +++ b/src/command/handlers/delete_topic.rs @@ -0,0 +1,181 @@ +use crate::command::context::CommandContext; +use crate::command::handler::{CommandHandler, CommandMetadata}; +use crate::command::handlers::list_topics::TopicSummary; +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 DeleteTopicCommandHandler { + store: Arc, + session_manager: Option, +} + +impl DeleteTopicCommandHandler { + pub fn new(store: Arc) -> 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 DeleteTopicCommandHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!(cmd, Command::DeleteTopic { .. }) + } + + fn metadata(&self) -> Option { + Some(CommandMetadata { + name: "delete", + description: "删除指定话题", + usage: "/delete ", + }) + } + + async fn handle( + &self, + cmd: Command, + ctx: CommandContext, + ) -> Result { + match cmd { + Command::DeleteTopic { topic_id } => handle_delete_topic(self, topic_id, ctx).await, + _ => unreachable!(), + } + } +} + +async fn handle_delete_topic( + handler: &DeleteTopicCommandHandler, + topic_id: String, + ctx: CommandContext, +) -> Result { + let session_id = ctx + .session_id + .as_deref() + .ok_or_else(|| CommandError::new("NO_SESSION", "No active session"))?; + + // 验证话题存在 + 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 topic_title = topic.title.clone(); + + // 删除话题(存储层方法已存在) + handler + .store + .delete_topic(&topic_id) + .map_err(|e| CommandError::new("DELETE_TOPIC_ERROR", e.to_string()))?; + + // 查询更新后的话题列表,返回给前端刷新侧边栏 + let topics = handler + .store + .list_topics(session_id) + .map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?; + + let topic_summaries: Vec = topics + .into_iter() + .map(|t| TopicSummary { + topic_id: t.id, + session_id: t.session_id, + title: t.title, + description: t.description.filter(|d| !d.is_empty()), + message_count: t.message_count, + created_at: t.created_at, + last_active_at: t.last_active_at, + }) + .collect(); + + let topics_json = + serde_json::to_string(&topic_summaries) + .map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?; + + let message = format!("✓ 已删除话题: {}", topic_title); + + Ok(CommandResponse::success(ctx.request_id) + .with_message(MessageKind::Notification, &message) + .with_metadata("topics", &topics_json) + .with_metadata("deleted_topic_id", &topic_id)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::SessionStore; + use std::sync::Arc; + + fn create_test_handler() -> DeleteTopicCommandHandler { + let store = Arc::new(SessionStore::in_memory().unwrap()); + DeleteTopicCommandHandler::new(store) + } + + #[tokio::test] + async fn test_delete_topic_success() { + let handler = create_test_handler(); + let store = handler.store.clone(); + + // 先创建 session 和 topic + let session = store.create_session("test_channel", Some("test")).unwrap(); + let topic = store + .create_topic(&session.id, "test topic", None) + .unwrap(); + + let ctx = CommandContext::new("test", "test_channel") + .with_session_id(&session.id) + .with_chat_id(&session.id); + let cmd = Command::DeleteTopic { + topic_id: topic.id.clone(), + }; + + let result = handler.handle(cmd, ctx).await; + assert!(result.is_ok()); + + let resp = result.unwrap(); + assert!(resp.success); + assert!(resp.metadata.contains_key("deleted_topic_id")); + + // 验证话题已被删除 + let deleted = store.get_topic(&topic.id).unwrap(); + assert!(deleted.is_none()); + } + + #[tokio::test] + async fn test_delete_nonexistent_topic() { + let handler = create_test_handler(); + let store = handler.store.clone(); + + let session = store.create_session("test_channel", Some("test")).unwrap(); + let ctx = CommandContext::new("test", "test_channel") + .with_session_id(&session.id) + .with_chat_id(&session.id); + let cmd = Command::DeleteTopic { + topic_id: "nonexistent".to_string(), + }; + + let result = handler.handle(cmd, ctx).await; + assert!(result.is_err()); + } + + #[test] + fn test_can_handle() { + let handler = create_test_handler(); + assert!(handler.can_handle(&Command::DeleteTopic { + topic_id: "test".to_string() + })); + assert!(!handler.can_handle(&Command::Help)); + } +} diff --git a/src/command/handlers/mod.rs b/src/command/handlers/mod.rs index 4986697..ba7497d 100644 --- a/src/command/handlers/mod.rs +++ b/src/command/handlers/mod.rs @@ -1,3 +1,4 @@ +pub mod delete_topic; pub mod get_current; pub mod help; pub mod list_channels; diff --git a/src/command/mod.rs b/src/command/mod.rs index 25ca552..6d5ba74 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -52,6 +52,8 @@ pub enum Command { channel: String, chat_id: String, }, + /// 删除指定话题 + DeleteTopic { topic_id: String }, /// 停止当前正在执行的 Agent StopExecution, } @@ -74,6 +76,7 @@ impl Command { Command::LoadTaskMessages { .. } => "load_task_messages", Command::ListSchedulerJobs => "list_scheduler_jobs", Command::LoadChatMessages { .. } => "load_chat_messages", + Command::DeleteTopic { .. } => "delete_topic", Command::StopExecution => "stop_execution", } } diff --git a/src/gateway/processor.rs b/src/gateway/processor.rs index 7fb7b09..8d1d841 100644 --- a/src/gateway/processor.rs +++ b/src/gateway/processor.rs @@ -7,6 +7,7 @@ use crate::bus::{InboundMessage, MessageBus, OutboundMessage}; use crate::command::adapter::InputAdapter; use crate::command::adapters::channel::ChannelInputAdapter; use crate::command::handler::CommandRouter; +use crate::command::handlers::delete_topic::DeleteTopicCommandHandler; use crate::command::handlers::get_current::GetCurrentSessionCommandHandler; use crate::command::handlers::help::HelpCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler; @@ -97,6 +98,12 @@ impl InboundProcessor { system_prompt_provider, ).with_session_manager(session_manager.clone()))); + // 注册 delete_topic 处理器 + command_router.register(Box::new( + DeleteTopicCommandHandler::new(store.clone()) + .with_session_manager(session_manager.clone()), + )); + // 注册 help 处理器(最后注册,获取所有已注册命令的元数据) let metadata = command_router.metadata_arc(); command_router.register(Box::new(HelpCommandHandler::new(metadata))); diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index f6222fc..c9a9b37 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -5,6 +5,7 @@ use crate::command::adapter::{InputAdapter, OutputAdapter}; use crate::command::adapters::websocket::{WebSocketInputAdapter, WebSocketOutputAdapter}; use crate::command::context::CommandContext; use crate::command::handler::CommandRouter; +use crate::command::handlers::delete_topic::DeleteTopicCommandHandler; use crate::command::handlers::get_current::GetCurrentSessionCommandHandler; use crate::command::handlers::help::HelpCommandHandler; use crate::command::handlers::list_channels::ListChannelsCommandHandler; @@ -403,6 +404,11 @@ async fn handle_inbound( state.task_repository.clone(), system_prompt_provider.clone(), ))); + // 注册 delete_topic 处理器 + router.register(Box::new( + DeleteTopicCommandHandler::new(store.clone()) + .with_session_manager(state.session_manager.clone()), + )); // 注册 help 处理器 let metadata = router.metadata_arc(); router.register(Box::new(HelpCommandHandler::new(metadata))); diff --git a/web/src/App.tsx b/web/src/App.tsx index f3ba345..b6b2bbc 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -52,10 +52,12 @@ function App() { // 方法 handleMessage, handleCommand, + clearMessages, handleServerMessage, selectTopic, createTopic, switchTopic, + deleteTopic, requestSessionList, requestTopicList, enterSubAgentView, @@ -229,6 +231,20 @@ function App() { [sendMessage, handleCommand, switchTopic, selectTopic] ) + const handleDeleteTopic = useCallback( + (topicId: string) => { + const cmd = deleteTopic(topicId) + handleCommand(cmd) + sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) + // 如果删除的是当前选中话题,清空选中状态和消息 + if (topicId === selectedTopic) { + selectTopic('') + clearMessages() + } + }, + [sendMessage, handleCommand, deleteTopic, selectedTopic, selectTopic, clearMessages] + ) + const handleNavigateToSubAgent = useCallback( (taskId: string, description: string) => { const cmd = enterSubAgentView(taskId, description) @@ -407,6 +423,7 @@ function App() { onCreateTopic={handleCreateTopic} onRefresh={handleRefreshTopics} onSwitchTopic={handleSwitchTopic} + onDeleteTopic={handleDeleteTopic} /> ) : ( void onRefresh: () => void onSwitchTopic: (topicId: string) => void + onDeleteTopic: (topicId: string) => void } function formatTime(timestamp: number): string { @@ -35,7 +37,10 @@ export function TopicList({ onCreateTopic, onRefresh, onSwitchTopic, + onDeleteTopic, }: TopicListProps) { + const [confirmDeleteId, setConfirmDeleteId] = useState(null) + return (
{/* Header */} @@ -93,41 +98,83 @@ export function TopicList({ ) : (
{topics.map((topic, index) => ( - + + {/* Delete button — visible on group hover */} +
+ {confirmDeleteId === topic.id ? ( + + 确认删除? + + + + ) : ( + )}
- +
))}
)} diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index e4a8d25..e288f25 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -63,6 +63,7 @@ interface UseChatReturn { selectTopic: (topicId: string) => void createTopic: (title?: string) => Command switchTopic: (topicId: string) => Command + deleteTopic: (topicId: string) => Command // 初始化方法 requestSessionList: () => Command @@ -528,6 +529,7 @@ export function useChat(): UseChatReturn { case 'load_topic': case 'list_sessions': case 'list_sessions_by_channel': + case 'delete_topic': case 'list_topics': setIsLoading(true) break @@ -558,6 +560,13 @@ export function useChat(): UseChatReturn { } }, []) + const deleteTopic = useCallback((topicId: string): Command => { + return { + type: 'delete_topic', + topic_id: topicId, + } + }, []) + // 初始化方法 const requestSessionList = useCallback((): Command => { return { @@ -702,6 +711,7 @@ export function useChat(): UseChatReturn { selectTopic, createTopic, switchTopic, + deleteTopic, requestSessionList, requestTopicList, requestChannelList, diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 76312e5..6e0ba2e 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -303,6 +303,11 @@ export interface LoadChatMessagesCommand { chat_id: string } +export interface DeleteTopicCommand { + type: 'delete_topic' + topic_id: string +} + export interface StopExecutionCommand { type: 'stop_execution' } @@ -322,6 +327,7 @@ export type Command = | LoadTaskMessagesCommand | ListSchedulerJobsCommand | LoadChatMessagesCommand + | DeleteTopicCommand | StopExecutionCommand // ============================================================================