feat: 添加 ListTodos 命令处理器,支持列出当前待办事项列表

This commit is contained in:
oudecheng 2026-06-12 15:53:01 +08:00
parent 3f32079f92
commit 750eed7326
7 changed files with 110 additions and 6 deletions

View File

@ -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<SessionStore>,
}
impl ListTodosCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self {
Self { store }
}
}
#[async_trait]
impl CommandHandler for ListTodosCommandHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::ListTodos)
}
fn metadata(&self) -> Option<CommandMetadata> {
Some(CommandMetadata {
name: "list_todos",
description: "列出当前 Todo 列表",
usage: "/list_todos",
})
}
async fn handle(
&self,
_cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
// 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<TodoItemSummary> = 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))
}
}

View File

@ -5,6 +5,7 @@ pub mod list_channels;
pub mod list_memories; pub mod list_memories;
pub mod list_scheduler_jobs; pub mod list_scheduler_jobs;
pub mod list_skills; pub mod list_skills;
pub mod list_todos;
pub mod memory_crud; pub mod memory_crud;
pub mod list_sessions; pub mod list_sessions;
pub mod list_sessions_by_channel; pub mod list_sessions_by_channel;

View File

@ -73,6 +73,8 @@ pub enum Command {
DeleteMemory { id: String }, DeleteMemory { id: String },
/// 列出所有技能 /// 列出所有技能
ListSkills, ListSkills,
/// 列出当前 Todo 列表
ListTodos,
} }
impl Command { impl Command {
@ -100,6 +102,7 @@ impl Command {
Command::UpdateMemory { .. } => "update_memory", Command::UpdateMemory { .. } => "update_memory",
Command::DeleteMemory { .. } => "delete_memory", Command::DeleteMemory { .. } => "delete_memory",
Command::ListSkills => "list_skills", Command::ListSkills => "list_skills",
Command::ListTodos => "list_todos",
} }
} }
} }

View File

@ -112,3 +112,7 @@
- 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话 - 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话
- 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具 - 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具
## todo工具使用规范
- 严格按照既定的未完成的todo工作项执行任务如果工作项不在适用就更新不得随意遗漏工作项
- 禁止将未完成的工作项标记为已完成

View File

@ -42,21 +42,30 @@ const TODO_WRITE_INSTRUCTIONS: &str = r#"
3. `completed` `cancelled` 3. `completed` `cancelled`
4. completed 4. completed
5. `content` 5. `content`
6. ** `id`** id idid todo_write `current_todos`
### 使 ### 使
merge pending in_progress id
```json ```json
{"merge": true, "todos": [{"content": "修复登录 bug", "status": "in_progress"}]} {"merge": true, "todos": [{"content": "修复登录 bug", "status": "in_progress"}]}
``` ```
```json ```json
{"merge": true, "todos": [{"content": "补充测试", "status": "pending"}]} {"merge": true, "todos": [{"content": "补充测试", "status": "pending"}]}
``` ```
id + ** id** current_todos
```json ```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"}
]}
``` ```
"#; "#;

View File

@ -12,6 +12,7 @@ use crate::command::handlers::list_channels::ListChannelsCommandHandler;
use crate::command::handlers::list_memories::ListMemoriesCommandHandler; use crate::command::handlers::list_memories::ListMemoriesCommandHandler;
use crate::command::handlers::list_skills::ListSkillsCommandHandler; use crate::command::handlers::list_skills::ListSkillsCommandHandler;
use crate::command::handlers::list_scheduler_jobs::ListSchedulerJobsCommandHandler; 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::memory_crud::MemoryCrudCommandHandler;
use crate::command::handlers::list_sessions::ListSessionsCommandHandler; use crate::command::handlers::list_sessions::ListSessionsCommandHandler;
use crate::command::handlers::list_sessions_by_channel::ListSessionsByChannelCommandHandler; 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()))); router.register(Box::new(ListMemoriesCommandHandler::new(store.clone())));
// 注册 list_skills 处理器 // 注册 list_skills 处理器
router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler))); router.register(Box::new(ListSkillsCommandHandler::new(skills_for_handler)));
// 注册 list_todos 处理器
router.register(Box::new(ListTodosCommandHandler::new(store.clone())));
// 注册 memory_crud 处理器 // 注册 memory_crud 处理器
router.register(Box::new(MemoryCrudCommandHandler::new(store.clone()))); router.register(Box::new(MemoryCrudCommandHandler::new(store.clone())));
// 注册 load_chat_messages 处理器 // 注册 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::<Vec<crate::protocol::TodoItemSummary>>(todos_json) {
let _ = sender.send(WsOutbound::TodoList { todos, scope_key: String::new() }).await;
}
}
// 处理记忆列表 // 处理记忆列表
if let Some(memories_json) = response.metadata.get("memories") { if let Some(memories_json) = response.metadata.get("memories") {
if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) { if let Ok(memories) = serde_json::from_str::<Vec<crate::protocol::MemorySummary>>(memories_json) {

View File

@ -323,7 +323,7 @@ function App() {
} }
}, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList]) }, [sidebarTab, status, handleCommand, sendMessage, requestSchedulerJobList])
// 连接就绪时自动拉取记忆和技能列表 // 连接就绪时自动拉取记忆、技能和待办列表
useEffect(() => { useEffect(() => {
if (status === 'connected') { if (status === 'connected') {
const memCmd = requestMemoryList() const memCmd = requestMemoryList()
@ -332,8 +332,11 @@ function App() {
const skillCmd = requestSkillList() const skillCmd = requestSkillList()
handleCommand(skillCmd) handleCommand(skillCmd)
sendMessage({ type: 'command', payload: JSON.stringify(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 handleRefreshMemories = useCallback(() => {
const cmd = requestMemoryList() const cmd = requestMemoryList()