fix: topic_id 穿透到 ToolContext,统一 todo scope_key 计算

- AgentBuildRequest 加 topic_id 字段
- Session.create_agent 传入 current_topic
- ToolContext.topic_id 不再硬编码 None
- 删除已废弃的 intercept_todo_write_results
- 工具/emitter/handler 三处 scope_key 计算全部统一

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
oudecheng 2026-06-12 17:36:10 +08:00
parent ec5ddf644a
commit eef0d24dcd
2 changed files with 5 additions and 84 deletions

View File

@ -24,6 +24,8 @@ pub(crate) struct AgentBuildRequest<'a> {
pub(crate) sender_id: Option<&'a str>, pub(crate) sender_id: Option<&'a str>,
pub(crate) message_id: Option<&'a str>, pub(crate) message_id: Option<&'a str>,
pub(crate) provider_config: LLMProviderConfig, pub(crate) provider_config: LLMProviderConfig,
/// 当前话题 ID可选用于 todo 等按 topic 隔离的工具
pub(crate) topic_id: Option<String>,
/// 取消信号接收端可选Agent 在每次迭代时检查是否被取消 /// 取消信号接收端可选Agent 在每次迭代时检查是否被取消
pub(crate) cancel_token: Option<tokio::sync::watch::Receiver<()>>, pub(crate) cancel_token: Option<tokio::sync::watch::Receiver<()>>,
} }
@ -73,7 +75,7 @@ impl AgentFactory {
sender_id: request.sender_id.map(str::to_string), sender_id: request.sender_id.map(str::to_string),
chat_id: Some(tool_chat_id.to_string()), chat_id: Some(tool_chat_id.to_string()),
session_id: Some(session_id), session_id: Some(session_id),
topic_id: None, topic_id: request.topic_id.clone(),
message_id: request.message_id.map(str::to_string), message_id: request.message_id.map(str::to_string),
message_seq: None, message_seq: None,
subagent_description: None, subagent_description: None,

View File

@ -445,89 +445,6 @@ impl Session {
let _ = self.user_tx.send(msg).await; let _ = self.user_tx.send(msg).await;
} }
/// 扫描 agent 结果中的 todo_write 工具消息,
/// 提取 todos 并做持久化 + 前端推送(同步版本)。
pub(crate) fn intercept_todo_write_results(
&self,
emitted_messages: &[ChatMessage],
chat_id: &str,
) {
for msg in emitted_messages {
if msg.role != "tool" {
continue;
}
if msg.tool_name.as_deref() != Some("todo_write") {
continue;
}
// 解析工具返回的 JSON
let parsed: serde_json::Value = match serde_json::from_str(&msg.content) {
Ok(v) => v,
Err(_) => continue,
};
let Some(todos_array) = parsed
.get("current_todos")
.and_then(|v| v.as_array())
else {
continue;
};
// 计算持久化所需的 key
let session_id = crate::storage::persistent_session_id(&self.channel_name, chat_id);
let topic_id = self.current_topic(chat_id);
let scope_key = topic_id.map(|t| t.to_string()).unwrap_or_else(|| session_id.clone());
// 转换为 TodoRecord 并持久化
let records: Vec<crate::storage::TodoRecord> = todos_array
.iter()
.filter_map(|item| {
Some(crate::storage::TodoRecord {
id: item.get("id")?.as_str()?.to_string(),
scope_key: scope_key.clone(),
session_id: session_id.clone(),
topic_id: topic_id.map(|t| t.to_string()),
content: item.get("content")?.as_str()?.to_string(),
status: item.get("status")?.as_str()?.to_string(),
priority: item.get("priority")?.as_str()?.to_string(),
created_at: item.get("created_at")?.as_i64()?,
updated_at: item.get("updated_at")?.as_i64()?,
})
})
.collect();
// 持久化到 SQLite
tracing::info!(
scope_key = %scope_key,
todo_count = records.len(),
"intercept_todo_write_results: persisting todos"
);
if let Err(e) = self.store.replace_todos(&scope_key, &records) {
tracing::warn!(error = %e, scope_key = %scope_key, "Failed to persist todo list");
}
// 推送到前端(使用 try_send 避免异步)
let summaries: Vec<crate::protocol::TodoItemSummary> = records
.iter()
.map(|r| crate::protocol::TodoItemSummary {
id: r.id.clone(),
content: r.content.clone(),
status: r.status.clone(),
priority: r.priority.clone(),
created_at: r.created_at,
updated_at: r.updated_at,
})
.collect();
let _ = self.user_tx.try_send(crate::protocol::WsOutbound::TodoList {
todos: summaries,
scope_key: scope_key.clone(),
});
break; // 只处理第一个成功的 todo_write
}
}
/// 获取 provider_config 引用 /// 获取 provider_config 引用
pub fn provider_config(&self) -> &LLMProviderConfig { pub fn provider_config(&self) -> &LLMProviderConfig {
&self.provider_config &self.provider_config
@ -604,9 +521,11 @@ impl Session {
) -> Result<AgentLoop, AgentError> { ) -> Result<AgentLoop, AgentError> {
// 消费 pending 的取消信号接收端(如果存在) // 消费 pending 的取消信号接收端(如果存在)
let cancel_token = self.pending_cancel_tokens.remove(session_chat_id); let cancel_token = self.pending_cancel_tokens.remove(session_chat_id);
let topic_id = self.current_topic(session_chat_id).map(|s| s.to_string());
self.agent_factory.create(AgentBuildRequest { self.agent_factory.create(AgentBuildRequest {
channel_name: &self.channel_name, channel_name: &self.channel_name,
session_chat_id, session_chat_id,
topic_id,
notification_chat_id, notification_chat_id,
sender_id, sender_id,
message_id, message_id,