PicoBot/src/command/handlers/load_task_messages.rs
oudecheng 0159227828 perf: 第二批性能修复——blocking 线程池隔离、HTTP 客户端复用、前端 memo 与流式节流
- 同步阻塞操作(附件处理、历史加载、scheduler/memory_search 的 SQLite 调用)
  移入 spawn_blocking,避免占用 async worker
- LLM Provider reqwest::Client 按超时配置缓存复用,减少 TLS/连接开销
- agent loop:图片过滤加廉价预判避免全量深拷贝;请求克隆改借用;工具定义 Arc 化
- 定向 COUNT/LIMIT 1 查询替代全量加载计数(wait_coordinator、task session 重建)
- 前端:面板/侧栏/聊天组件 memo 化;merged_tool 对象按值复用缓存;
  流式 delta rAF 节流批量 flush;useMemo 缓存分组排序结果
2026-08-18 06:55:21 +08:00

213 lines
7.1 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::command::Command;
use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::handlers::list_topics::TopicTokenStats;
use crate::command::response::{CommandError, CommandResponse};
use crate::storage::SessionStore;
use crate::tools::task::repository::TaskRepository;
use crate::tools::task::types::{TaskSession, TaskSessionState};
use async_trait::async_trait;
use std::sync::Arc;
pub struct LoadTaskMessagesCommandHandler {
task_repository: Arc<dyn TaskRepository>,
store: Arc<SessionStore>,
}
impl LoadTaskMessagesCommandHandler {
pub fn new(task_repository: Arc<dyn TaskRepository>, store: Arc<SessionStore>) -> Self {
Self {
task_repository,
store,
}
}
}
#[async_trait]
impl CommandHandler for LoadTaskMessagesCommandHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::LoadTaskMessages { .. })
}
fn metadata(&self) -> Option<CommandMetadata> {
Some(CommandMetadata {
name: "load_task_messages",
description: "加载子智能体任务的消息历史",
usage: "/load_task_messages <task_id>",
})
}
async fn handle(
&self,
cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
match cmd {
Command::LoadTaskMessages { task_id } => {
handle_load_task_messages(self, task_id, ctx).await
}
_ => unreachable!(),
}
}
}
async fn handle_load_task_messages(
handler: &LoadTaskMessagesCommandHandler,
task_id: String,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
tracing::debug!(
task_id = %task_id,
request_id = %ctx.request_id,
"LoadTaskMessages: looking up task"
);
// 1. Try in-memory repository first
let task = match handler.task_repository.load_task_session(&task_id).await {
Ok(Some(task)) => {
tracing::debug!(
task_id = %task.id,
session_id = %task.session_id,
state = ?task.state,
"LoadTaskMessages: task found in memory"
);
Some(task)
}
Ok(None) => {
tracing::debug!(
task_id = %task_id,
"LoadTaskMessages: task not in memory, searching database"
);
// 2. Fall back to database (survives restarts)
reconstruct_task_from_db(&handler.store, &task_id)?
}
Err(e) => {
tracing::error!(
task_id = %task_id,
error = %e,
"LoadTaskMessages: repository error during lookup"
);
return Err(CommandError::new("LOAD_TASK_ERROR", e.to_string()));
}
};
let task = task.ok_or_else(|| {
tracing::warn!(
task_id = %task_id,
"LoadTaskMessages: task not found in repository or database"
);
CommandError::new("TASK_NOT_FOUND", format!("Task not found: {}", task_id))
})?;
let status = format!("{:?}", task.state).to_lowercase();
// 查询子代理 session 的 token 统计(按 session_id 精确匹配,不过滤 sub:%
let token_stats = handler
.store
.get_session_token_stats(&task.session_id)
.map_err(|e| CommandError::new("TOKEN_STATS_ERROR", e.to_string()))?
.map(|s| TopicTokenStats {
prompt_tokens: s.prompt_tokens,
completion_tokens: s.completion_tokens,
total_tokens: s.total_tokens,
cached_tokens: s.cached_tokens,
last_prompt_tokens: s.last_prompt_tokens,
context_window_tokens: s.context_window_tokens.unwrap_or(0),
});
let mut response = CommandResponse::success(ctx.request_id)
.with_metadata("task_session_id", &task.session_id)
.with_metadata("task_id", &task.id)
.with_metadata("task_description", &task.description)
.with_metadata("task_subagent_type", &task.subagent_type)
.with_metadata("task_status", &status);
if let Some(ref summary) = task.summary {
response = response.with_metadata("task_summary", summary);
}
if let Some(ref stats) = token_stats {
let stats_json = serde_json::to_string(stats)
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
response = response.with_metadata("task_token_stats", &stats_json);
}
Ok(response)
}
/// Reconstruct a TaskSession from the database when it's not in the in-memory repository.
/// Task sessions have id format: sub:{parent_session_id}:task:{uuid}
fn reconstruct_task_from_db(
store: &SessionStore,
task_id: &str,
) -> Result<Option<TaskSession>, CommandError> {
let record = store
.find_first_session_by_id_suffix(&format!(":{}", task_id))
.map_err(|e| CommandError::new("DB_ERROR", e.to_string()))?;
let record = match record {
Some(record) => record,
None => return Ok(None),
};
let session_id = record.id.clone();
// Extract parent_session_id from session_id: "sub:{parent}:task:{uuid}"
let parent_session_id = session_id
.strip_prefix("sub:")
.and_then(|rest| {
let pos = rest.find(":task:");
pos.map(|p| rest[..p].to_string())
})
.unwrap_or_default();
// Extract subagent_type and description from title
let (subagent_type, description) = parse_subagent_title(&record.title);
let now = record.updated_at;
// DB 未持久化 task 状态字段,无法可靠区分 Running/Completed/Failed/Timeout。
// 用 Unknown 表示"重启后从 DB 重建,真实状态不可知",避免把 failed/timeout
// 误报为 Completed 误导用户。前端会把 Unknown 显示为"未知"。
tracing::warn!(
task_id = %task_id,
session_id = %session_id,
"Reconstructing task from DB after restart; true state unknown, marking as Unknown"
);
Ok(Some(TaskSession {
id: task_id.to_string(),
session_id,
parent_session_id,
parent_topic_id: None,
parent_chat_id: record.chat_id.clone(),
parent_channel_name: record.channel_name.clone(),
description,
subagent_type,
state: TaskSessionState::Unknown,
created_at: record.created_at,
updated_at: now,
summary: None,
error: None,
tool_call_id: None,
}))
}
/// Parse subagent title to extract type and description.
/// New format: "Subagent [type]: description"
/// Legacy format: "Subagent: description" (defaults to "general")
fn parse_subagent_title(title: &str) -> (String, String) {
if let Some(rest) = title.strip_prefix("Subagent [")
&& let Some(bracket_pos) = rest.find("]: ")
{
let agent_type = rest[..bracket_pos].to_string();
let desc = rest[bracket_pos + 3..].to_string();
return (agent_type, desc);
}
let desc = title
.strip_prefix("Subagent: ")
.unwrap_or(title)
.to_string();
("general".to_string(), desc)
}