From 1ac6de118acdf9671097dc83718586ef10840678 Mon Sep 17 00:00:00 2001 From: xiaoxixi Date: Tue, 28 Apr 2026 21:27:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20/dump=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=EF=BC=9A=E4=BF=9D=E5=AD=98=E5=88=B0=E6=96=87=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E7=B3=BB=E7=BB=9F=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /dump 现在保存到 {workspace}/dumps/ 目录而非仅返回文本 - dump 文件包含注入模型的完整系统提示词(AgentLoop + Skills) - 修复 cli_chat 中 current_session_guard 变量声明问题 - gateway 正确处理 HandleResult::AgentResponse 和 CommandOutput - 移除 cli_chat 中未使用的 parse_slash_command 导入 --- src/channels/cli_chat.rs | 63 ++------------------ src/gateway/mod.rs | 17 +++++- src/session/session.rs | 122 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 136 insertions(+), 66 deletions(-) diff --git a/src/channels/cli_chat.rs b/src/channels/cli_chat.rs index 14b9e0f..e078de1 100644 --- a/src/channels/cli_chat.rs +++ b/src/channels/cli_chat.rs @@ -8,7 +8,6 @@ use crate::session::{SessionCommand, SessionEvent, UnifiedSessionId}; use crate::protocol::{parse_inbound, WsInbound, WsOutbound, SlashCommandInfo}; use super::base::{Channel, ChannelError}; -use super::slash_command::parse_slash_command; /// Generate a short ID (8 characters) from a UUID fn short_id() -> String { @@ -112,65 +111,13 @@ impl CliChatChannel { match inbound { WsInbound::UserInput { content, chat_id, .. } => { - let chat_id = chat_id.or(current_session_guard.clone()).unwrap_or_else(short_id); - - // If no session, create one first - if current_session_guard.is_none() { - let new_id = self.create_session_via_control(&chat_id, None).await?; - *current_session_guard = Some(new_id); - } - - let session_id = current_session_guard.clone().unwrap(); - - // Check for slash command - if let Some((cmd_name, cmd_args)) = parse_slash_command(&content) { - // Send ExecuteSlashCommand via control plane - let (reply_tx, mut reply_rx) = mpsc::channel(1); - let unified_id = UnifiedSessionId::parse(&session_id); - bus.publish_control(ControlMessage { - op: SessionCommand::ExecuteSlashCommand { - command: cmd_name.to_string(), - args: if cmd_args.is_empty() { None } else { Some(cmd_args.to_string()) }, - channel: self.name().to_string(), - chat_id: chat_id.clone(), - current_session_id: unified_id, - }, - reply_tx, - }).await?; - - // Handle response - if let Some(result) = reply_rx.recv().await { - match result { - Ok(SessionEvent::SlashCommandExecuted { new_session_id, message }) => { - // Update current session if new one was created - if let Some(new_id) = new_session_id { - *current_session_guard = Some(new_id.to_string()); - } - let _ = client.sender.send(WsOutbound::CommandExecuted { message }).await; - } - Ok(SessionEvent::Error { code, message }) => { - let _ = client.sender.send(WsOutbound::Error { code, message }).await; - } - Err(e) => { - let _ = client.sender.send(WsOutbound::Error { code: "EXECUTION_ERROR".to_string(), message: e.to_string() }).await; - } - _ => {} - } - } - return Ok(()); - } - - // Parse UnifiedSessionId to get chat_id and dialog_id - let (channel_name, chat_id_part, dialog_id_part) = UnifiedSessionId::parse(&session_id) - .map(|sid| (sid.channel, sid.chat_id, Some(sid.dialog_id.clone()))) - .unwrap_or_else(|| (self.name().to_string(), session_id.clone(), None)); - - // Publish to bus for AI processing + // All messages (including slash commands) go through the normal inbound flow + // SessionManager handles session creation/reuse internally let msg = InboundMessage { - channel: channel_name, + channel: self.name().to_string(), sender_id: "cli".to_string(), - chat_id: chat_id_part, - dialog_id: dialog_id_part, + chat_id: chat_id.unwrap_or_else(short_id), + dialog_id: None, content, timestamp: crate::bus::message::current_timestamp(), media: Vec::new(), diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index b380494..2683316 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -94,11 +94,24 @@ impl GatewayState { &inbound.content, inbound.media, ).await { - Ok(response_content) => { + Ok(crate::session::session::HandleResult::AgentResponse(content)) => { let outbound = crate::bus::OutboundMessage { channel: inbound.channel.clone(), chat_id: inbound.chat_id.clone(), - content: response_content, + content, + reply_to: None, + media: vec![], + metadata: inbound.forwarded_metadata, + }; + if let Err(e) = bus.publish_outbound(outbound).await { + tracing::error!(error = %e, "Failed to publish outbound"); + } + } + Ok(crate::session::session::HandleResult::CommandOutput(content)) => { + let outbound = crate::bus::OutboundMessage { + channel: inbound.channel.clone(), + chat_id: inbound.chat_id.clone(), + content, reply_to: None, media: vec![], metadata: inbound.forwarded_metadata, diff --git a/src/session/session.rs b/src/session/session.rs index ce1529e..d445b79 100644 --- a/src/session/session.rs +++ b/src/session/session.rs @@ -6,6 +6,14 @@ use tokio::sync::{Mutex, mpsc}; use uuid::Uuid; use crate::bus::ChatMessage; + +/// Result of handling a message - either an AI response or a command output +pub enum HandleResult { + /// AI response to be sent as AssistantResponse + AgentResponse(String), + /// Command output to be sent as CommandExecuted + CommandOutput(String), +} use crate::channels::slash_command::parse_slash_command; use crate::config::LLMProviderConfig; use crate::agent::{AgentLoop, AgentError, ContextCompressor}; @@ -144,7 +152,31 @@ impl Session { } } - /// 将当前 session 导出为 markdown 文档 + /// 将当前 session 导出为 markdown 文档并保存到文件 + pub fn dump_to_file(&self, system_prompt: &str) -> std::io::Result { + use chrono::{DateTime, Local}; + use std::fs; + use std::io::Write; + + let md = self.dump_as_markdown_with_system_prompt(system_prompt); + + // Create dumps directory under workspace + let dumps_dir = self.provider_config.workspace_dir.join("dumps"); + fs::create_dir_all(&dumps_dir)?; + + // Generate filename based on session info + let timestamp = Local::now().format("%Y%m%d_%H%M%S"); + let filename = format!("{}_{}_{}.md", self.id.channel, self.id.chat_id, timestamp); + let filepath = dumps_dir.join(&filename); + + // Write to file + let mut file = fs::File::create(&filepath)?; + file.write_all(md.as_bytes())?; + + Ok(filepath.to_string_lossy().to_string()) + } + + /// 将当前 session 导出为 markdown 文档(纯内存版本) pub fn dump_as_markdown(&self) -> String { use chrono::{DateTime, Local}; @@ -208,6 +240,78 @@ impl Session { md } + + /// 将当前 session 导出为 markdown 文档(包含系统提示词) + pub fn dump_as_markdown_with_system_prompt(&self, system_prompt: &str) -> String { + use chrono::{DateTime, Local}; + + let now = Local::now().format("%Y-%m-%d %H:%M:%S"); + + let mut md = String::new(); + md.push_str("# Session Dump\n\n"); + md.push_str(&format!("- **Session ID**: `{}`\n", self.id)); + md.push_str(&format!("- **Channel**: `{}`\n", self.id.channel)); + md.push_str(&format!("- **Chat ID**: `{}`\n", self.id.chat_id)); + md.push_str(&format!("- **Dialog ID**: `{}`\n", self.id.dialog_id)); + md.push_str(&format!("- **Message Count**: {}\n", self.messages.len())); + md.push_str(&format!("- **Model**: `{}`\n", self.provider_config.model_id)); + md.push_str(&format!("- **Exported At**: {}\n", now)); + md.push_str("\n---\n\n"); + + // System Prompt Section + md.push_str("## System Prompt (Injected to Model)\n\n"); + md.push_str("```\n"); + md.push_str(system_prompt); + md.push_str("\n```\n\n"); + md.push_str("---\n\n"); + + md.push_str("## Conversation History\n\n"); + + for (i, msg) in self.messages.iter().enumerate() { + let role = match msg.role.as_str() { + "system" => "System", + "user" => "User", + "assistant" => "Assistant", + "tool" => "Tool", + r => r, + }; + + let timestamp = if msg.timestamp > 0 { + DateTime::from_timestamp_millis(msg.timestamp) + .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) + .unwrap_or_default() + } else { + String::new() + }; + + md.push_str(&format!("### [{:03}] {} {}\n\n", i + 1, role, timestamp)); + md.push_str("```\n"); + + if let Some(ref tool_calls) = msg.tool_calls { + md.push_str("[Tool Calls]\n"); + for tc in tool_calls { + md.push_str(&format!("- {}: {:?}\n", tc.name, tc.arguments)); + } + } + + if let Some(ref tool_name) = msg.tool_name { + md.push_str(&format!("[Tool: {}]\n", tool_name)); + } + + if let Some(ref tool_call_id) = msg.tool_call_id { + md.push_str(&format!("[Tool Call ID: {}]\n", tool_call_id)); + } + + md.push_str(&msg.content); + md.push_str("\n```\n\n"); + + if !msg.media_refs.is_empty() { + md.push_str(&format!("**Media**: {:?}\n\n", msg.media_refs)); + } + } + + md + } } /// SessionManager 管理所有 Session,按 channel_name 路由 @@ -388,8 +492,14 @@ impl SessionManager { if let Some(sid) = current_session_id { let session = self.get_or_create_session(sid).await?; let session_guard = session.lock().await; - let md = session_guard.dump_as_markdown(); - Ok((None, md)) + + // Build the same system prompt that would be injected to the model + let skills_prompt = self.skills_loader.build_skills_prompt(); + let system_prompt = session_guard.build_system_prompt(&skills_prompt); + + let filepath = session_guard.dump_to_file(&system_prompt) + .map_err(|e| AgentError::Other(format!("Failed to save dump: {}", e)))?; + Ok((None, format!("Session dump saved to: {}", filepath))) } else { Ok((None, "No active session.".to_string())) } @@ -513,7 +623,7 @@ impl SessionManager { dialog_id: Option<&str>, content: &str, media: Vec, - ) -> Result { + ) -> Result { let dialog_id = dialog_id.unwrap_or(DEFAULT_DIALOG_ID); let unified_id = UnifiedSessionId::new(channel, chat_id, dialog_id); let session = self.get_or_create_session(&unified_id).await?; @@ -537,7 +647,7 @@ impl SessionManager { } } - return Ok(response); + return Ok(HandleResult::CommandOutput(response)); } // Normal message handling through LLM @@ -585,7 +695,7 @@ impl SessionManager { "Agent response received" ); - Ok(response) + Ok(HandleResult::AgentResponse(response)) } pub async fn clear_session_history(&self, unified_id: &UnifiedSessionId) -> Result<(), AgentError> {