Compare commits

..

7 Commits

Author SHA1 Message Date
fe4088cd1f feat(session): 扩展 /info 命令显示更多 session 信息
新增:对话标题、模型名称、用户消息数/总消息数、创建时间、最后活跃时间
2026-04-29 22:59:49 +08:00
5c558027fa feat(session): 添加 /? 帮助命令,错误命令显示友好提示
- 新增 /? 和 /help 命令显示所有可用斜杠命令
- 未知命令不再报错,而是提示"未知命令:/xxx。输入 /? 获取帮助。"
- execute_slash_command 错误现在返回友好提示而非传播为服务器错误
2026-04-29 22:57:59 +08:00
e1681e0424 debug(session): 添加 handle_message 和 session 恢复的 tracing 日志
用于排查 restart 后 session 恢复和 TTL 刷新问题。
2026-04-29 22:51:50 +08:00
dcb2d552d9 chore(session): 将标题生成阈值从 10 条减至 5 条用户消息 2026-04-29 22:46:33 +08:00
228204517d fix(session): 每次 add_message 时同步更新 Storage 的 message_count
之前只持久化了消息内容,没有更新 SessionMeta.message_count,
导致重启后从 Storage 恢复时 message_count 低于实际值,
should_generate_title() 判断失败。
2026-04-29 22:45:04 +08:00
97e3be6d3a fix(session): 调用 generate_title 生成对话标题
之前 generate_title 函数已实现但从未被调用,导致对话标题
自动生成(10 条用户消息后)一直没有触发。
2026-04-29 22:34:02 +08:00
2f2631e36a fix(session): /new 后新对话被旧对话数据覆盖的问题
问题原因:execute_slash_command 执行 /new 后,错误地将旧 session 的数据
存储到新 session 的 key 下,导致 current_sessions 虽然指向新 session,
但 sessions 里面存的是旧数据。

解决方案:移除错误的 session 映射更新逻辑。create_session 已经正确地将
新 session 存入 sessions 并更新 current_sessions,不需要额外覆盖。
2026-04-29 22:32:58 +08:00
2 changed files with 56 additions and 15 deletions

View File

@ -28,6 +28,10 @@ mod tests {
assert_eq!(parse_slash_command("/reset"), Some(("reset", ""))); assert_eq!(parse_slash_command("/reset"), Some(("reset", "")));
assert_eq!(parse_slash_command("/reset arg"), Some(("reset", "arg"))); assert_eq!(parse_slash_command("/reset arg"), Some(("reset", "arg")));
assert_eq!(parse_slash_command("/new hello world"), Some(("new", "hello world"))); assert_eq!(parse_slash_command("/new hello world"), Some(("new", "hello world")));
assert_eq!(parse_slash_command("/??"), Some(("??", "")));
assert_eq!(parse_slash_command("/? arg"), Some(("?", "arg")));
assert_eq!(parse_slash_command("/?"), Some(("?", "")));
assert_eq!(parse_slash_command("/help"), Some(("help", "")));
assert_eq!(parse_slash_command("hello"), None); assert_eq!(parse_slash_command("hello"), None);
assert_eq!(parse_slash_command("/"), Some(("", ""))); assert_eq!(parse_slash_command("/"), Some(("", "")));
} }

View File

@ -204,6 +204,14 @@ impl Session {
} }
self.last_active_at = now; self.last_active_at = now;
// Sync message_count to Storage
if persist {
tracing::debug!(session_id = %self.id, last_active_at = %now, message_count = %self.message_count, "Persisting session meta after add_message");
if let Err(e) = self.persist_session_meta().await {
tracing::warn!("failed to persist session meta: {}", e);
}
}
Ok(()) Ok(())
} }
@ -278,9 +286,9 @@ impl Session {
Ok(()) Ok(())
} }
/// 检查是否需要自动生成 title10 条用户消息后) /// 检查是否需要自动生成 title5 条用户消息后)
pub fn should_generate_title(&self) -> bool { pub fn should_generate_title(&self) -> bool {
self.title == "新对话" && self.message_count >= 10 self.title == "新对话" && self.message_count >= 5
} }
/// 生成标题(调用 LLM /// 生成标题(调用 LLM
@ -629,6 +637,11 @@ pub static SLASH_COMMANDS: &[SlashCommand] = &[
description: "保存当前对话为 markdown 文档", description: "保存当前对话为 markdown 文档",
aliases: &["/dump"], aliases: &["/dump"],
}, },
SlashCommand {
name: "?",
description: "显示帮助",
aliases: &["/?", "/help"],
},
]; ];
impl SessionManager { impl SessionManager {
@ -758,9 +771,17 @@ impl SessionManager {
let session_guard = session.lock().await; let session_guard = session.lock().await;
let message_count = session_guard.get_history().len(); let message_count = session_guard.get_history().len();
let session_id_str = session_guard.session_id(); let session_id_str = session_guard.session_id();
let title = &session_guard.title;
let model_name = &session_guard.provider_config.name;
let created_at = chrono::DateTime::from_timestamp_millis(session_guard.created_at)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_default();
let last_active_at = chrono::DateTime::from_timestamp_millis(session_guard.last_active_at)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_default();
Ok((None, format!( Ok((None, format!(
"Session ID: {}\nMessage count: {}", "对话标题: {}\nSession ID: {}\n模型: {}\n用户消息: {} / 总消息: {}\n创建时间: {}\n最后活跃: {}",
session_id_str, message_count title, session_id_str, model_name, session_guard.message_count, message_count, created_at, last_active_at
))) )))
} else { } else {
Ok((None, "No active session.".to_string())) Ok((None, "No active session.".to_string()))
@ -812,7 +833,13 @@ impl SessionManager {
Ok((None, "No active session.".to_string())) Ok((None, "No active session.".to_string()))
} }
} }
_ => Err(AgentError::Other(format!("Command not implemented: {}", cmd.name))), "?" | "help" => {
let lines: Vec<String> = SLASH_COMMANDS.iter().map(|c| {
format!(" {} - {}", c.aliases.join(", "), c.description)
}).collect();
Ok((None, format!("可用命令:\n{}", lines.join("\n"))))
}
_ => Err(AgentError::Other(format!("未知命令:/{}。输入 /? 获取帮助。", cmd.name))),
} }
} }
@ -884,6 +911,7 @@ impl SessionManager {
// Try to restore from Storage // Try to restore from Storage
match self.storage.get_session(&session_id_str).await { match self.storage.get_session(&session_id_str).await {
Ok(meta) => { Ok(meta) => {
tracing::debug!(session_id = %session_id_str, last_active_at = %meta.last_active_at, message_count = %meta.message_count, "Restoring session from Storage");
let (user_tx, _rx) = mpsc::channel::<WsOutbound>(100); let (user_tx, _rx) = mpsc::channel::<WsOutbound>(100);
let session = Session::from_storage( let session = Session::from_storage(
unified_id.clone(), unified_id.clone(),
@ -1063,11 +1091,14 @@ impl SessionManager {
} else { } else {
// No current session tracked, find active or create new // No current session tracked, find active or create new
let ttl_millis = self.inner.lock().await.session_ttl.as_millis() as i64; let ttl_millis = self.inner.lock().await.session_ttl.as_millis() as i64;
tracing::debug!(channel, chat_id, ttl_millis, "No current_sessions entry, searching Storage for active session");
match self.storage.find_active_session(channel, chat_id, ttl_millis).await { match self.storage.find_active_session(channel, chat_id, ttl_millis).await {
Ok(Some(meta)) => { Ok(Some(meta)) => {
tracing::debug!(session_id = %meta.id, dialog_id = %meta.dialog_id, last_active_at = %meta.last_active_at, "Found active session in Storage");
UnifiedSessionId::new(channel, chat_id, &meta.dialog_id) UnifiedSessionId::new(channel, chat_id, &meta.dialog_id)
} }
Ok(None) | Err(_) => { Ok(None) | Err(_) => {
tracing::debug!("No active session found in Storage, creating new session");
// Create new session // Create new session
let (new_id, _) = self.create_session(channel, chat_id, None, String::new()).await?; let (new_id, _) = self.create_session(channel, chat_id, None, String::new()).await?;
new_id new_id
@ -1075,29 +1106,28 @@ impl SessionManager {
} }
} }
}; };
tracing::debug!(unified_id = %unified_id, "handle_message resolved unified_id");
let session = self.get_or_create_session(&unified_id).await?; let session = self.get_or_create_session(&unified_id).await?;
// Check for slash command // Check for slash command
if let Some((cmd_name, cmd_args)) = parse_slash_command(content) { if let Some((cmd_name, cmd_args)) = parse_slash_command(content) {
let (new_session_id, response) = self.execute_slash_command( let result = self.execute_slash_command(
cmd_name, cmd_name,
if cmd_args.is_empty() { None } else { Some(cmd_args) }, if cmd_args.is_empty() { None } else { Some(cmd_args) },
channel, channel,
chat_id, chat_id,
Some(&unified_id), Some(&unified_id),
).await?; ).await;
// If a new session was created (e.g., /new, /delete), update the session binding
if let Some(new_id) = new_session_id {
// Update the session in the map with the new ID
let mut inner = self.inner.lock().await;
if let Some(old_session) = inner.sessions.remove(&unified_id.to_string()) {
inner.sessions.insert(new_id.to_string(), old_session);
}
}
match result {
Ok((_new_session_id, response)) => {
return Ok(HandleResult::CommandOutput(response)); return Ok(HandleResult::CommandOutput(response));
} }
Err(e) => {
return Ok(HandleResult::CommandOutput(e.to_string()));
}
}
}
// Normal message handling through LLM // Normal message handling through LLM
let response: String = { let response: String = {
@ -1135,6 +1165,13 @@ impl SessionManager {
.map_err(|e| AgentError::Other(format!("persist error: {}", e)))?; .map_err(|e| AgentError::Other(format!("persist error: {}", e)))?;
} }
// Check if we need to generate a title (after 10 user messages)
if session_guard.should_generate_title() {
if let Err(e) = session_guard.generate_title().await {
tracing::warn!("failed to generate title: {}", e);
}
}
result.final_response.content result.final_response.content
}; };