修复 /dump 命令:保存到文件并注入系统提示词

- /dump 现在保存到 {workspace}/dumps/ 目录而非仅返回文本
- dump 文件包含注入模型的完整系统提示词(AgentLoop + Skills)
- 修复 cli_chat 中 current_session_guard 变量声明问题
- gateway 正确处理 HandleResult::AgentResponse 和 CommandOutput
- 移除 cli_chat 中未使用的 parse_slash_command 导入
This commit is contained in:
xiaoxixi 2026-04-28 21:27:36 +08:00
parent e61b78eaff
commit 1ac6de118a
3 changed files with 136 additions and 66 deletions

View File

@ -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(),

View File

@ -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,

View File

@ -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<String> {
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<crate::bus::MediaItem>,
) -> Result<String, AgentError> {
) -> Result<HandleResult, AgentError> {
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> {