PicoBot/src/command/handlers/load_task_messages.rs
oudecheng 175e7fc01b refactor(subagent): 支持带类型的子智能体标题格式
- 将子智能体标题解析升级为支持 "Subagent [type]: description" 格式
- 兼容旧格式 "Subagent: description",默认类型为 "general"
- 修改子智能体会话标题生成,包含类型信息
- 优化记录解析和会话创建逻辑,提高子智能体信息表达准确性
2026-06-18 14:27:38 +08:00

183 lines
5.7 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::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::info!(
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::info!(
task_id = %task.id,
session_id = %task.session_id,
state = ?task.state,
"LoadTaskMessages: task found in memory"
);
Some(task)
}
Ok(None) => {
tracing::info!(
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();
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);
}
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 sessions = store
.find_sessions_by_id_suffix(&format!(":{}", task_id))
.map_err(|e| CommandError::new("DB_ERROR", e.to_string()))?;
if sessions.is_empty() {
return Ok(None);
}
let record = &sessions[0];
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;
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::Completed,
created_at: record.created_at,
updated_at: now,
summary: None,
error: 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 [") {
if 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)
}