## 锁迁移: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 权限问题,与本次改动无关)
286 lines
7.7 KiB
Rust
286 lines
7.7 KiB
Rust
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());
|
||
}
|
||
}
|