From 750eed73265285d708a364d385a50e308859943d Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Fri, 12 Jun 2026 15:53:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20ListTodos=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=A4=84=E7=90=86=E5=99=A8=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=88=97=E5=87=BA=E5=BD=93=E5=89=8D=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=E4=BA=8B=E9=A1=B9=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/command/handlers/list_todos.rs | 74 +++++++++++++++++++++++++++++ src/command/handlers/mod.rs | 1 + src/command/mod.rs | 3 ++ src/gateway/default_agent_prompt.md | 4 ++ src/gateway/todo_prompt_provider.rs | 17 +++++-- src/gateway/ws.rs | 10 ++++ web/src/App.tsx | 7 ++- 7 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/command/handlers/list_todos.rs diff --git a/src/command/handlers/list_todos.rs b/src/command/handlers/list_todos.rs new file mode 100644 index 0000000..94c797f --- /dev/null +++ b/src/command/handlers/list_todos.rs @@ -0,0 +1,74 @@ +use crate::command::context::CommandContext; +use crate::command::handler::{CommandHandler, CommandMetadata}; +use crate::command::response::{CommandError, CommandResponse}; +use crate::command::Command; +use crate::protocol::TodoItemSummary; +use crate::storage::SessionStore; +use async_trait::async_trait; +use std::sync::Arc; + +pub struct ListTodosCommandHandler { + store: Arc, +} + +impl ListTodosCommandHandler { + pub fn new(store: Arc) -> Self { + Self { store } + } +} + +#[async_trait] +impl CommandHandler for ListTodosCommandHandler { + fn can_handle(&self, cmd: &Command) -> bool { + matches!(cmd, Command::ListTodos) + } + + fn metadata(&self) -> Option { + Some(CommandMetadata { + name: "list_todos", + description: "列出当前 Todo 列表", + usage: "/list_todos", + }) + } + + async fn handle( + &self, + _cmd: Command, + ctx: CommandContext, + ) -> Result { + // scope_key = topic_id.unwrap_or(session_id) + let scope_key = ctx + .topic_id + .as_deref() + .filter(|t| !t.is_empty()) + .or(ctx.session_id.as_deref()) + .ok_or_else(|| { + CommandError::new( + "MISSING_CONTEXT", + "Cannot list todos: no session_id or topic_id in command context", + ) + })?; + + let records = self + .store + .list_todos(scope_key) + .map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?; + + let summaries: Vec = records + .into_iter() + .map(|r| TodoItemSummary { + id: r.id, + content: r.content, + status: r.status, + priority: r.priority, + created_at: r.created_at, + updated_at: r.updated_at, + }) + .collect(); + + let todos_json = serde_json::to_string(&summaries) + .map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?; + + Ok(CommandResponse::success(ctx.request_id).with_metadata("todos", &todos_json)) + } +} diff --git a/src/command/handlers/mod.rs b/src/command/handlers/mod.rs index 37d90e2..596af6b 100644 --- a/src/command/handlers/mod.rs +++ b/src/command/handlers/mod.rs @@ -5,6 +5,7 @@ pub mod list_channels; pub mod list_memories; pub mod list_scheduler_jobs; pub mod list_skills; +pub mod list_todos; pub mod memory_crud; pub mod list_sessions; pub mod list_sessions_by_channel; diff --git a/src/command/mod.rs b/src/command/mod.rs index 7ed4d00..1831a29 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -73,6 +73,8 @@ pub enum Command { DeleteMemory { id: String }, /// 列出所有技能 ListSkills, + /// 列出当前 Todo 列表 + ListTodos, } impl Command { @@ -100,6 +102,7 @@ impl Command { Command::UpdateMemory { .. } => "update_memory", Command::DeleteMemory { .. } => "delete_memory", Command::ListSkills => "list_skills", + Command::ListTodos => "list_todos", } } } diff --git a/src/gateway/default_agent_prompt.md b/src/gateway/default_agent_prompt.md index ac9ebb1..141c383 100644 --- a/src/gateway/default_agent_prompt.md +++ b/src/gateway/default_agent_prompt.md @@ -112,3 +112,7 @@ - 默认创建静默任务(silent_agent_task),在独立后台会话中执行,不干扰主对话 - 静默模式下如需发送消息给用户,prompt中需显式使用 send_session_message 工具 +## todo工具使用规范 + +- 严格按照既定的未完成的todo工作项执行任务,如果工作项不在适用就更新,不得随意遗漏工作项 +- 禁止将未完成的工作项标记为已完成 \ No newline at end of file diff --git a/src/gateway/todo_prompt_provider.rs b/src/gateway/todo_prompt_provider.rs index b5de9c3..13d88a4 100644 --- a/src/gateway/todo_prompt_provider.rs +++ b/src/gateway/todo_prompt_provider.rs @@ -42,21 +42,30 @@ const TODO_WRITE_INSTRUCTIONS: &str = r#" 3. `completed` 和 `cancelled` 是终端状态,已完成的项不能被重新激活 4. 不要先标记 completed 再去实际执行 — 先完成工作,再标记 5. `content` 字段保持简洁、可执行 +6. **更新已有任务必须传 `id`**。新任务不传 id(工具会自动生成),更新状态时必须传 id。id 可以从之前 todo_write 返回的 `current_todos` 中获取 ### 使用范例 -开始任务时(merge 模式): +创建新任务(新任务必须 pending 或 in_progress,不传 id): ```json {"merge": true, "todos": [{"content": "修复登录 bug", "status": "in_progress"}]} ``` -发现新任务时: +追加新任务: ```json {"merge": true, "todos": [{"content": "补充测试", "status": "pending"}]} ``` -完成任务时(传入 id + 新状态): +更新已有任务(**必须传 id**,从上次返回的 current_todos 中取得): ```json -{"merge": true, "todos": [{"id": "xxx", "content": "修复登录 bug", "status": "completed"}]} +{"merge": true, "todos": [{"id": "abc-123", "content": "修复登录 bug", "status": "completed"}]} +``` + +同时更新多项: +```json +{"merge": true, "todos": [ + {"id": "abc-123", "content": "修复登录 bug", "status": "completed"}, + {"content": "代码审查", "status": "in_progress"} +]} ``` "#; diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 1b64e9d..3c1cf83 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -12,6 +12,7 @@ use crate::command::handlers::list_channels::ListChannelsCommandHandler; use crate::command::handlers::list_memories::ListMemoriesCommandHandler; use crate::command::handlers::list_skills::ListSkillsCommandHandler; use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler; +use crate::command::handlers::list_todos::ListTodosCommandHandler; use crate::command::handlers::memory_crud::MemoryCrudCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler; use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler; @@ -422,6 +423,8 @@ async fn handle_inbound( router.register(Box::new(ListMemoriesCommandHandler::new(store.clone()))); // 注册 list_skills 处理器 router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler))); + // 注册 list_todos 处理器 + router.register(Box::new(ListTodosCommandHandler::new(store.clone()))); // 注册 memory_crud 处理器 router.register(Box::new(MemoryCrudCommandHandler::new(store.clone()))); // 注册 load_chat_messages 处理器 @@ -519,6 +522,13 @@ async fn handle_inbound( } } + // 处理 Todo 列表 + if let Some(todos_json) = response.metadata.get("todos") { + if let Ok(todos) = serde_json::from_str::>(todos_json) { + let _ = sender.send(WsOutbound::TodoList { todos, scope_key: String::new() }).await; + } + } + // 处理记忆列表 if let Some(memories_json) = response.metadata.get("memories") { if let Ok(memories) = serde_json::from_str::>(memories_json) { diff --git a/web/src/App.tsx b/web/src/App.tsx index 97373b1..2536bb1 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -323,7 +323,7 @@ function App() { } }, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList]) - // 连接就绪时自动拉取记忆和技能列表 + // 连接就绪时自动拉取记忆、技能和待办列表 useEffect(() => { if (status === 'connected') { const memCmd = requestMemoryList() @@ -332,8 +332,11 @@ function App() { const skillCmd = requestSkillList() handleCommand(skillCmd) sendMessage({ type: 'command', payload: JSON.stringify(skillCmd) }) + const todoCmd = requestTodoList() + handleCommand(todoCmd) + sendMessage({ type: 'command', payload: JSON.stringify(todoCmd) }) } - }, [status, handleCommand, sendMessage, requestMemoryList, requestSkillList]) + }, [status, handleCommand, sendMessage, requestMemoryList, requestSkillList, requestTodoList]) const handleRefreshMemories = useCallback(() => { const cmd = requestMemoryList()