PicoBot/src/command/handler.rs
oudecheng bf8c227634 refactor: 迁移 parking_lot 锁并优化阻塞 IO 与内存管理
## 锁迁移:std::sync → parking_lot
消除锁中毒(poison)导致的级联崩溃风险。parking_lot 锁不会中毒,
且性能更优。迁移覆盖全部生产代码:
- experts/mod.rs: 4 RwLock + 13 expect
- skills/mod.rs: 1 RwLock + 11 expect
- tools/registry.rs: 1 RwLock + 2 expect
- gateway/model_selection.rs: 1 RwLock + 2 expect
- tools/task/repository.rs: 1 RwLock + 4 unwrap
- tools/task/runtime.rs: 2 RwLock + 17 expect
- gateway/session.rs + task/runtime.rs: stream_message_id Mutex
- gateway/processor.rs: description_generation_in_flight Mutex
- command/handler.rs + help.rs: metadata Mutex(公开 API)
- mcp/client.rs: stderr_lines Mutex

测试代码中的 std::sync::Mutex(串行化锁 + TestObserver)有意保留,
已通过 unwrap_or_else(|err| err.into_inner()) 做中毒恢复。

## P1: 阻塞 IO 迁移到 spawn_blocking
将 3 处阻塞 async worker 的操作迁移到 blocking 线程池:
- file_read.rs: read_to_string + 行处理 + base64 编码整体包入 spawn_blocking
- agent_loop.rs: 新增 preencode_images_for_request 两阶段预编码
  (顺序分配预算 → 并行 spawn_blocking 编码),build_llm_request 改为 async
- wechat.rs: media_to_send_content 改为 async,std::fs::read 用 spawn_blocking 包裹

## P2: session_history topic_histories 内存上限
新增 MAX_CACHED_TOPICS=32 上限和 evict_inactive_if_needed 方法。
超限时驱逐非活跃 topic(不在 chat_topic_ids、不在 compression_in_flight、
serial_lock 未被持有)。活跃 topic 永不误驱逐。
remove_history 同步清理 topic_serial_locks,防止无限增长。

## P3: 减少 panic 面
agent_loop.rs retry 循环的 response.expect(...) 改为 ok_or_else(...)?
返回 AgentError::Other,逻辑 bug 不再导致整个 agent 崩溃。

## 对抗性审查修复
- preencode_images_for_request: 用 seen HashSet 去重,防止同 path 重复
  编码导致 HashMap entry 覆盖(NoBudget 覆盖 Encoded 等)
- evict_inactive_if_needed: 检查 topic_serial_lock.try_lock(),防止驱逐
  正在 agent 处理中的 topic(original_topic_id 不在 chat_topic_ids 但
  agent 仍持锁)
- remove_history: 清理 topic_serial_locks

## 验证
- cargo check: 通过(仅既有 lifetime 警告)
- cargo test: 559 passed / 3 failed(均为环境/sandbox 权限问题,与本次改动无关)
2026-08-06 08:18:04 +08:00

286 lines
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::agent::AgentError;
use crate::bus::InboundMessage;
use crate::command::Command;
use crate::command::context::CommandContext;
use crate::command::response::{CommandError, CommandResponse};
use crate::gateway::session::SessionManager;
use async_trait::async_trait;
use std::sync::Arc;
/// 命令元数据(用于帮助系统)
#[derive(Debug, Clone)]
pub struct CommandMetadata {
pub name: &'static str,
pub description: &'static str,
pub usage: &'static str,
}
/// 命令处理器 trait
///
/// 实现此 trait 来处理特定类型的命令
/// 处理器是渠道无关的,只关心 Command 本身
#[async_trait]
pub trait CommandHandler: Send + Sync {
/// 是否可以处理此命令
fn can_handle(&self, cmd: &Command) -> bool;
/// 返回命令元数据(用于 /help 命令)
fn metadata(&self) -> Option<CommandMetadata> {
None
}
/// 执行命令
///
/// # Arguments
/// * `cmd` - 要执行的命令
/// * `ctx` - 命令执行上下文
///
/// # Returns
/// * `Ok(CommandResponse)` - 命令执行成功
/// * `Err(CommandError)` - 命令执行失败
async fn handle(
&self,
cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError>;
}
/// InChat 命令处理器 trait
///
/// 用于处理在聊天中直接输入的命令(如 Feishu/WeChat 等通道)
/// 接收 InboundMessage 和 SessionManager
#[async_trait]
pub trait InChatCommandHandler: Send + Sync {
/// 是否可以处理此命令
fn can_handle(&self, cmd: &Command) -> bool;
/// 执行命令
///
/// # Arguments
/// * `cmd` - 要执行的命令
/// * `inbound` - 入站消息(包含通道信息)
/// * `session_manager` - 会话管理器(用于获取 session
///
/// # Returns
/// * `Ok(Some(msg))` - 命令执行成功,返回要发送给用户的消息
/// * `Ok(None)` - 命令执行成功,无需发送消息
/// * `Err(AgentError)` - 命令执行失败
async fn handle(
&self,
cmd: Command,
inbound: &InboundMessage,
session_manager: &SessionManager,
) -> Result<Option<String>, AgentError>;
}
/// 命令路由器
///
/// 负责将命令分发到合适的处理器
pub struct CommandRouter {
handlers: Vec<Box<dyn CommandHandler>>,
metadata: Arc<parking_lot::Mutex<Vec<CommandMetadata>>>,
}
impl CommandRouter {
/// 创建新的命令路由器
pub fn new() -> Self {
Self {
handlers: Vec::new(),
metadata: Arc::new(parking_lot::Mutex::new(Vec::new())),
}
}
/// 注册命令处理器
///
/// # Arguments
/// * `handler` - 要注册的处理器
pub fn register(&mut self, handler: Box<dyn CommandHandler>) {
if let Some(meta) = handler.metadata() {
self.metadata.lock().push(meta);
}
self.handlers.push(handler);
}
/// 获取已注册命令的元数据列表(用于 Help 命令)
pub fn metadata_arc(&self) -> Arc<parking_lot::Mutex<Vec<CommandMetadata>>> {
self.metadata.clone()
}
/// 分发命令到合适的处理器
///
/// # Arguments
/// * `cmd` - 要执行的命令
/// * `ctx` - 命令执行上下文
///
/// # Returns
/// * `Ok(CommandResponse)` - 命令执行成功
/// * `Err(CommandError)` - 没有合适的处理器或执行失败
pub async fn dispatch(
&self,
cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
// 查找能处理此命令的处理器
for handler in &self.handlers {
if handler.can_handle(&cmd) {
return handler.handle(cmd, ctx).await;
}
}
// 没有找到合适的处理器
Err(CommandError::new(
"NO_HANDLER",
format!("No handler found for command: {}", cmd.name()),
))
}
/// 分发命令,返回响应(如果失败则返回错误响应)
///
/// 与 `dispatch` 不同,此方法不会返回 Err
/// 而是将错误包装在 CommandResponse 中
pub async fn dispatch_with_response(
&self,
cmd: Command,
ctx: CommandContext,
) -> CommandResponse {
let request_id = ctx.request_id;
match self.dispatch(cmd, ctx).await {
Ok(response) => response,
Err(err) => CommandResponse::error(request_id, err),
}
}
}
impl Default for CommandRouter {
fn default() -> Self {
Self::new()
}
}
/// InChat 命令路由器
///
/// 负责将在聊天中输入的命令分发到合适的处理器
pub struct InChatCommandRouter {
handlers: Vec<Box<dyn InChatCommandHandler>>,
}
impl InChatCommandRouter {
/// 创建新的 InChat 命令路由器
pub fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
/// 注册 InChat 命令处理器
///
/// # Arguments
/// * `handler` - 要注册的处理器
pub fn register(&mut self, handler: Box<dyn InChatCommandHandler>) {
self.handlers.push(handler);
}
/// 分发命令到合适的处理器
///
/// # Arguments
/// * `cmd` - 要执行的命令
/// * `inbound` - 入站消息
/// * `session_manager` - 会话管理器
///
/// # Returns
/// * `Ok(Some(msg))` - 命令被处理,返回成功消息
/// * `Ok(None)` - 没有合适的处理器
/// * `Err(AgentError)` - 执行失败
pub async fn dispatch(
&self,
cmd: Command,
inbound: &InboundMessage,
session_manager: &SessionManager,
) -> Result<Option<String>, AgentError> {
// 查找能处理此命令的处理器
for handler in &self.handlers {
if handler.can_handle(&cmd) {
let result = handler.handle(cmd, inbound, session_manager).await?;
return Ok(result);
}
}
// 没有找到合适的处理器
Ok(None)
}
}
impl Default for InChatCommandRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestHandler;
#[async_trait]
impl CommandHandler for TestHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::CreateSession { .. })
}
async fn handle(
&self,
_cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
Ok(CommandResponse::success(ctx.request_id)
.with_message(crate::command::response::MessageKind::Notification, "ok"))
}
}
struct NoOpHandler;
#[async_trait]
impl CommandHandler for NoOpHandler {
fn can_handle(&self, _cmd: &Command) -> bool {
false
}
async fn handle(
&self,
_cmd: Command,
_ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
unreachable!()
}
}
#[tokio::test]
async fn test_router_finds_handler() {
let mut router = CommandRouter::new();
router.register(Box::new(TestHandler));
router.register(Box::new(NoOpHandler));
let ctx = CommandContext::new("test", "test");
let cmd = Command::CreateSession { title: None };
let result = router.dispatch(cmd, ctx).await;
assert!(result.is_ok());
let resp = result.unwrap();
assert!(resp.success);
}
#[tokio::test]
async fn test_router_no_handler() {
let router = CommandRouter::new();
let ctx = CommandContext::new("test", "test");
let cmd = Command::CreateSession { title: None };
let result = router.dispatch(cmd, ctx).await;
assert!(result.is_err());
}
}