P0: src/tools/http_request.rs SSRF 重定向绕过 - reqwest::Client 默认跟随最多 10 次重定向,is_private_host 仅检查初始 URL - 攻击者可用公网 URL 返回 302 → http://127.0.0.1/ 或 http://169.254.169.254/(云元数据端点)绕过防护访问内网 - 修复:.redirect(reqwest::redirect::Policy::none()) 完全禁用重定向 P1: src/tools/file_read/write/edit.rs 符号链接路径遍历 - resolve_path 用 starts_with 检查但未 canonicalize - 攻击者可在 allowed_dir 内创建指向 /etc/passwd 的符号链接绕过限制 - 修复:对 resolved 和 allowed 均执行 canonicalize 后比较 - file_read: 文件必须存在,canonicalize 失败直接报错 - file_write/edit: 文件可能不存在,降级到父目录 canonicalize P1: src/protocol/mod.rs + list_todos.rs TodoItemSummary 字段缺失 - 后端 TodoItemSummary 仅返回 4 字段,前端期望 7 字段 - 缺失 priority, created_at, updated_at,前端 TodoPanel 无法显示 优先级和时间戳 - 修复:struct 补齐 3 字段,list_todos 构造时传递完整字段
94 lines
2.9 KiB
Rust
94 lines
2.9 KiB
Rust
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> {
|
||
let task_id = match cmd {
|
||
Command::ListTodos { task_id } => task_id,
|
||
_ => None,
|
||
};
|
||
|
||
// 子代理:scope_key = task_id(全局唯一,与 todo_write 保持一致)
|
||
// 主代理:scope_key = topic_id.unwrap_or(session_id)
|
||
let scope_key = if let Some(tid) = task_id.as_deref() {
|
||
tid.to_string()
|
||
} else {
|
||
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",
|
||
)
|
||
})?
|
||
.to_string()
|
||
};
|
||
|
||
let records = self
|
||
.store
|
||
.list_todos(&scope_key)
|
||
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
|
||
|
||
tracing::info!(
|
||
scope_key = %scope_key,
|
||
record_count = records.len(),
|
||
"list_todos handler: reading from store"
|
||
);
|
||
|
||
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,
|
||
created_by_message_id: r.created_by_message_id,
|
||
})
|
||
.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)
|
||
.with_metadata("todos_scope_key", &scope_key))
|
||
}
|
||
}
|