fix: update database filename and improve session management in chat manager
This commit is contained in:
parent
11a8e93b77
commit
bafa7a606c
@ -73,7 +73,7 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
|
|||||||
### Key Constraints
|
### Key Constraints
|
||||||
|
|
||||||
- Gateway **changes working directory** to workspace on startup (`src/gateway/mod.rs:31`)
|
- Gateway **changes working directory** to workspace on startup (`src/gateway/mod.rs:31`)
|
||||||
- Session/message persistence uses SQLite via `sqlx`; DB stored in workspace as `.picobot_sessions.db` by default
|
- Session/message persistence uses SQLite via `sqlx`; DB stored in workspace as `picobot.db` by default
|
||||||
- `ChannelManager` owns the `MessageBus` and all channel instances
|
- `ChannelManager` owns the `MessageBus` and all channel instances
|
||||||
- `OutboundDispatcher` routes outbound messages to the correct channel via `ChannelManager`
|
- `OutboundDispatcher` routes outbound messages to the correct channel via `ChannelManager`
|
||||||
- Config `.env` loading uses `unsafe { env::set_var(...) }` — don't refactor to safer patterns without understanding side effects
|
- Config `.env` loading uses `unsafe { env::set_var(...) }` — don't refactor to safer patterns without understanding side effects
|
||||||
|
|||||||
@ -26,7 +26,7 @@ graph TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph Storage
|
subgraph Storage
|
||||||
SQLite[("SQLite<br/>.picobot_sessions.db")]
|
SQLite[("SQLite<br/>picobot.db")]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph AI["AI Providers"]
|
subgraph AI["AI Providers"]
|
||||||
@ -236,7 +236,7 @@ The `.env` file in the working directory is loaded manually (not via dotenv crat
|
|||||||
| `port` | u16 | `19876` | Listen port |
|
| `port` | u16 | `19876` | Listen port |
|
||||||
| `session_ttl_hours` | number | `4` | Inactive session expiration (hours) |
|
| `session_ttl_hours` | number | `4` | Inactive session expiration (hours) |
|
||||||
| `cleanup_interval_minutes` | number | `60` | Session cleanup interval |
|
| `cleanup_interval_minutes` | number | `60` | Session cleanup interval |
|
||||||
| `session_db_path` | string | workspace `.picobot_sessions.db` | SQLite database path |
|
| `session_db_path` | string | workspace `picobot.db` | SQLite database path |
|
||||||
| `scheduler.enabled` | bool | `false` | Enable cron scheduler |
|
| `scheduler.enabled` | bool | `false` | Enable cron scheduler |
|
||||||
|
|
||||||
### Agent Config
|
### Agent Config
|
||||||
|
|||||||
@ -59,7 +59,7 @@ impl Default for ContextCompressionConfig {
|
|||||||
pub struct ContextCompressor {
|
pub struct ContextCompressor {
|
||||||
config: ContextCompressionConfig,
|
config: ContextCompressionConfig,
|
||||||
context_window: usize,
|
context_window: usize,
|
||||||
/// Threshold ratio to trigger compression (50% of context window)
|
/// Threshold ratio to trigger compression (70% of context window)
|
||||||
threshold_ratio: f64,
|
threshold_ratio: f64,
|
||||||
/// Shared LLM provider for summarization
|
/// Shared LLM provider for summarization
|
||||||
provider: Arc<dyn LLMProvider>,
|
provider: Arc<dyn LLMProvider>,
|
||||||
@ -86,7 +86,7 @@ impl ContextCompressor {
|
|||||||
Self {
|
Self {
|
||||||
config: ContextCompressionConfig::default(),
|
config: ContextCompressionConfig::default(),
|
||||||
context_window,
|
context_window,
|
||||||
threshold_ratio: 0.5,
|
threshold_ratio: 0.7,
|
||||||
provider,
|
provider,
|
||||||
memory,
|
memory,
|
||||||
session_id: None,
|
session_id: None,
|
||||||
@ -103,7 +103,7 @@ impl ContextCompressor {
|
|||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
context_window,
|
context_window,
|
||||||
threshold_ratio: 0.5,
|
threshold_ratio: 0.7,
|
||||||
provider,
|
provider,
|
||||||
memory,
|
memory,
|
||||||
session_id: None,
|
session_id: None,
|
||||||
|
|||||||
@ -259,7 +259,7 @@ impl PromptSection for CrossChannelSection {
|
|||||||
|
|
||||||
### chat_manager 工具
|
### chat_manager 工具
|
||||||
管理会话和查看消息。参数:
|
管理会话和查看消息。参数:
|
||||||
- action = "list_sessions" — 列出最近活跃的会话
|
- action = "list_sessions" — 列出全部会话,支持通过 offset/count 翻页
|
||||||
- action = "list_channels" — 列出所有可用渠道
|
- action = "list_channels" — 列出所有可用渠道
|
||||||
- action = "list_messages" — 查看指定 session 的历史消息,支持以下参数:
|
- action = "list_messages" — 查看指定 session 的历史消息,支持以下参数:
|
||||||
- session_id (必填): 会话 ID
|
- session_id (必填): 会话 ID
|
||||||
|
|||||||
@ -41,14 +41,11 @@ impl GatewayState {
|
|||||||
// Override workspace_dir with the ensured path
|
// Override workspace_dir with the ensured path
|
||||||
provider_config.workspace_dir = workspace_path.clone();
|
provider_config.workspace_dir = workspace_path.clone();
|
||||||
|
|
||||||
// Session TTL from config (default 4 hours)
|
|
||||||
let session_ttl_hours = config.gateway.session_ttl_hours.unwrap_or(4);
|
|
||||||
|
|
||||||
// Initialize Storage
|
// Initialize Storage
|
||||||
let db_path = if let Some(ref path) = config.gateway.session_db_path {
|
let db_path = if let Some(ref path) = config.gateway.session_db_path {
|
||||||
std::path::PathBuf::from(path)
|
std::path::PathBuf::from(path)
|
||||||
} else {
|
} else {
|
||||||
workspace_path.join(".picobot_sessions.db")
|
workspace_path.join("picobot.db")
|
||||||
};
|
};
|
||||||
let storage = Arc::new(
|
let storage = Arc::new(
|
||||||
crate::storage::Storage::new(&db_path).await
|
crate::storage::Storage::new(&db_path).await
|
||||||
@ -79,7 +76,6 @@ impl GatewayState {
|
|||||||
|
|
||||||
// Create SessionManager with bus injection
|
// Create SessionManager with bus injection
|
||||||
let session_manager = SessionManager::new(
|
let session_manager = SessionManager::new(
|
||||||
session_ttl_hours,
|
|
||||||
provider_config.clone(),
|
provider_config.clone(),
|
||||||
storage.clone(),
|
storage.clone(),
|
||||||
bus.clone(),
|
bus.clone(),
|
||||||
@ -87,11 +83,6 @@ impl GatewayState {
|
|||||||
)?;
|
)?;
|
||||||
let session_manager = Arc::new(session_manager);
|
let session_manager = Arc::new(session_manager);
|
||||||
|
|
||||||
// Start background cleanup task (default 60 minutes)
|
|
||||||
let cleanup_interval = config.gateway.cleanup_interval_minutes.unwrap_or(60);
|
|
||||||
session_manager.clone().start_cleanup_task(cleanup_interval);
|
|
||||||
tracing::info!("Session cleanup task started (interval: {} min)", cleanup_interval);
|
|
||||||
|
|
||||||
// Create ChannelManager and init channels
|
// Create ChannelManager and init channels
|
||||||
let cli_chat_channel = Arc::new(CliChatChannel::new());
|
let cli_chat_channel = Arc::new(CliChatChannel::new());
|
||||||
let channel_manager = ChannelManager::with_bus(cli_chat_channel, bus);
|
let channel_manager = ChannelManager::with_bus(cli_chat_channel, bus);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -730,8 +729,6 @@ pub struct SessionManager {
|
|||||||
struct SessionManagerInner {
|
struct SessionManagerInner {
|
||||||
/// Sessions keyed by UnifiedSessionId.to_string()
|
/// Sessions keyed by UnifiedSessionId.to_string()
|
||||||
sessions: HashMap<String, Arc<Mutex<Session>>>,
|
sessions: HashMap<String, Arc<Mutex<Session>>>,
|
||||||
session_timestamps: HashMap<String, Instant>,
|
|
||||||
session_ttl: Duration,
|
|
||||||
/// Current active session per channel:chat_id
|
/// Current active session per channel:chat_id
|
||||||
current_sessions: HashMap<String, String>,
|
current_sessions: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
@ -808,7 +805,6 @@ pub static SLASH_COMMANDS: &[SlashCommand] = &[
|
|||||||
|
|
||||||
impl SessionManager {
|
impl SessionManager {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
session_ttl_hours: u64,
|
|
||||||
provider_config: LLMProviderConfig,
|
provider_config: LLMProviderConfig,
|
||||||
storage: Arc<Storage>,
|
storage: Arc<Storage>,
|
||||||
bus: Arc<MessageBus>,
|
bus: Arc<MessageBus>,
|
||||||
@ -823,8 +819,6 @@ impl SessionManager {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: Arc::new(Mutex::new(SessionManagerInner {
|
inner: Arc::new(Mutex::new(SessionManagerInner {
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::new(),
|
||||||
session_timestamps: HashMap::new(),
|
|
||||||
session_ttl: Duration::from_secs(session_ttl_hours * 3600),
|
|
||||||
current_sessions: HashMap::new(),
|
current_sessions: HashMap::new(),
|
||||||
})),
|
})),
|
||||||
provider_config,
|
provider_config,
|
||||||
@ -847,42 +841,6 @@ impl SessionManager {
|
|||||||
self.tools.clone()
|
self.tools.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启动后台 TTL 清理任务
|
|
||||||
pub fn start_cleanup_task(self: Arc<Self>, interval_mins: u64) {
|
|
||||||
let cleanup_interval = Duration::from_secs(interval_mins * 60);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(cleanup_interval).await;
|
|
||||||
self.run_cleanup().await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 执行一次 TTL 清理:释放内存中过期的 session,Storage 记录保留
|
|
||||||
async fn run_cleanup(&self) {
|
|
||||||
let inner = self.inner.lock().await;
|
|
||||||
let now = Instant::now();
|
|
||||||
let ttl = inner.session_ttl;
|
|
||||||
|
|
||||||
let expired: Vec<String> = inner
|
|
||||||
.session_timestamps
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, last_touch)| now.duration_since(**last_touch) > ttl)
|
|
||||||
.map(|(id, _)| id.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
drop(inner);
|
|
||||||
|
|
||||||
if !expired.is_empty() {
|
|
||||||
let mut inner = self.inner.lock().await;
|
|
||||||
for id in &expired {
|
|
||||||
inner.sessions.remove(id);
|
|
||||||
inner.session_timestamps.remove(id);
|
|
||||||
}
|
|
||||||
tracing::debug!(count = expired.len(), "Cleaned up expired sessions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取所有可用的斜杠命令
|
/// 获取所有可用的斜杠命令
|
||||||
pub fn get_slash_commands(&self) -> &[SlashCommand] {
|
pub fn get_slash_commands(&self) -> &[SlashCommand] {
|
||||||
SLASH_COMMANDS
|
SLASH_COMMANDS
|
||||||
@ -1071,7 +1029,6 @@ impl SessionManager {
|
|||||||
let arc = Arc::new(Mutex::new(session));
|
let arc = Arc::new(Mutex::new(session));
|
||||||
let inner = &mut *self.inner.lock().await;
|
let inner = &mut *self.inner.lock().await;
|
||||||
inner.sessions.insert(session_id_str.clone(), arc.clone());
|
inner.sessions.insert(session_id_str.clone(), arc.clone());
|
||||||
inner.session_timestamps.insert(session_id_str.clone(), Instant::now());
|
|
||||||
// Set as current session for this channel:chat_id
|
// Set as current session for this channel:chat_id
|
||||||
let chat_scope = format!("{}:{}", channel, chat_id);
|
let chat_scope = format!("{}:{}", channel, chat_id);
|
||||||
inner.current_sessions.insert(chat_scope, session_id_str);
|
inner.current_sessions.insert(chat_scope, session_id_str);
|
||||||
@ -1084,7 +1041,6 @@ impl SessionManager {
|
|||||||
let inner = &mut *self.inner.lock().await;
|
let inner = &mut *self.inner.lock().await;
|
||||||
|
|
||||||
if let Some(session) = inner.sessions.get(&session_id_str) {
|
if let Some(session) = inner.sessions.get(&session_id_str) {
|
||||||
inner.session_timestamps.insert(session_id_str, Instant::now());
|
|
||||||
return Ok(session.clone());
|
return Ok(session.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1102,7 +1058,6 @@ impl SessionManager {
|
|||||||
|
|
||||||
let arc = Arc::new(Mutex::new(session));
|
let arc = Arc::new(Mutex::new(session));
|
||||||
inner.sessions.insert(session_id_str.clone(), arc.clone());
|
inner.sessions.insert(session_id_str.clone(), arc.clone());
|
||||||
inner.session_timestamps.insert(session_id_str.clone(), Instant::now());
|
|
||||||
// Set as current session
|
// Set as current session
|
||||||
let chat_scope = format!("{}:{}", unified_id.channel, unified_id.chat_id);
|
let chat_scope = format!("{}:{}", unified_id.channel, unified_id.chat_id);
|
||||||
inner.current_sessions.insert(chat_scope, session_id_str);
|
inner.current_sessions.insert(chat_scope, session_id_str);
|
||||||
@ -1126,7 +1081,6 @@ impl SessionManager {
|
|||||||
|
|
||||||
let arc = Arc::new(Mutex::new(session));
|
let arc = Arc::new(Mutex::new(session));
|
||||||
inner.sessions.insert(session_id_str.clone(), arc.clone());
|
inner.sessions.insert(session_id_str.clone(), arc.clone());
|
||||||
inner.session_timestamps.insert(session_id_str.clone(), Instant::now());
|
|
||||||
// Set as current session
|
// Set as current session
|
||||||
let chat_scope = format!("{}:{}", unified_id.channel, unified_id.chat_id);
|
let chat_scope = format!("{}:{}", unified_id.channel, unified_id.chat_id);
|
||||||
inner.current_sessions.insert(chat_scope, session_id_str);
|
inner.current_sessions.insert(chat_scope, session_id_str);
|
||||||
@ -1209,7 +1163,6 @@ impl SessionManager {
|
|||||||
// Remove from memory and current sessions
|
// Remove from memory and current sessions
|
||||||
let mut inner = self.inner.lock().await;
|
let mut inner = self.inner.lock().await;
|
||||||
inner.sessions.remove(&session_id_str);
|
inner.sessions.remove(&session_id_str);
|
||||||
inner.session_timestamps.remove(&session_id_str);
|
|
||||||
let chat_scope = format!("{}:{}", session_id.channel, session_id.chat_id);
|
let chat_scope = format!("{}:{}", session_id.channel, session_id.chat_id);
|
||||||
inner.current_sessions.remove(&chat_scope);
|
inner.current_sessions.remove(&chat_scope);
|
||||||
|
|
||||||
@ -1262,8 +1215,7 @@ impl SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ttl_millis = self.inner.lock().await.session_ttl.as_millis() as i64;
|
match self.storage.find_most_recent_session(channel, chat_id).await {
|
||||||
match self.storage.find_active_session(channel, chat_id, ttl_millis).await {
|
|
||||||
Ok(Some(meta)) => Ok(UnifiedSessionId::new(channel, chat_id, &meta.dialog_id)),
|
Ok(Some(meta)) => Ok(UnifiedSessionId::new(channel, chat_id, &meta.dialog_id)),
|
||||||
_ => {
|
_ => {
|
||||||
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?;
|
||||||
|
|||||||
@ -457,25 +457,22 @@ impl Storage {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_active_session(
|
pub async fn find_most_recent_session(
|
||||||
&self,
|
&self,
|
||||||
channel: &str,
|
channel: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
ttl_millis: i64,
|
|
||||||
) -> Result<Option<crate::storage::session::SessionMeta>, StorageError> {
|
) -> Result<Option<crate::storage::session::SessionMeta>, StorageError> {
|
||||||
let cutoff = chrono::Utc::now().timestamp_millis() - ttl_millis;
|
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, channel, chat_id, dialog_id, title, created_at, last_active_at, message_count, routing_info, deleted_at, last_consolidated_at, last_compressed_message_at
|
SELECT id, channel, chat_id, dialog_id, title, created_at, last_active_at, message_count, routing_info, deleted_at, last_consolidated_at, last_compressed_message_at
|
||||||
FROM sessions
|
FROM sessions
|
||||||
WHERE channel = ? AND chat_id = ? AND deleted_at IS NULL AND last_active_at > ?
|
WHERE channel = ? AND chat_id = ? AND deleted_at IS NULL
|
||||||
ORDER BY last_active_at DESC
|
ORDER BY last_active_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(channel)
|
.bind(channel)
|
||||||
.bind(chat_id)
|
.bind(chat_id)
|
||||||
.bind(cutoff)
|
|
||||||
.fetch_optional(self.pool())
|
.fetch_optional(self.pool())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -617,24 +614,33 @@ impl Storage {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_active_sessions(
|
pub async fn query_sessions_range(
|
||||||
&self,
|
&self,
|
||||||
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<crate::storage::session::SessionMeta>, StorageError> {
|
) -> Result<(Vec<crate::storage::session::SessionMeta>, i64), StorageError> {
|
||||||
|
let count_row = sqlx::query(
|
||||||
|
"SELECT COUNT(*) as total FROM sessions WHERE deleted_at IS NULL",
|
||||||
|
)
|
||||||
|
.fetch_one(self.pool())
|
||||||
|
.await?;
|
||||||
|
let total: i64 = count_row.get("total");
|
||||||
|
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, channel, chat_id, dialog_id, title, created_at, last_active_at, message_count, routing_info, deleted_at, last_consolidated_at, last_compressed_message_at
|
SELECT id, channel, chat_id, dialog_id, title, created_at, last_active_at, message_count, routing_info, deleted_at, last_consolidated_at, last_compressed_message_at
|
||||||
FROM sessions
|
FROM sessions
|
||||||
WHERE deleted_at IS NULL
|
WHERE deleted_at IS NULL
|
||||||
ORDER BY last_active_at DESC
|
ORDER BY last_active_at DESC
|
||||||
LIMIT ?
|
LIMIT ? OFFSET ?
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
.fetch_all(self.pool())
|
.fetch_all(self.pool())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(rows
|
let sessions: Vec<_> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| crate::storage::session::SessionMeta {
|
.map(|row| crate::storage::session::SessionMeta {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
@ -650,7 +656,9 @@ impl Storage {
|
|||||||
last_consolidated_at: row.get("last_consolidated_at"),
|
last_consolidated_at: row.get("last_consolidated_at"),
|
||||||
last_compressed_message_at: row.get("last_compressed_message_at"),
|
last_compressed_message_at: row.get("last_compressed_message_at"),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect();
|
||||||
|
|
||||||
|
Ok((sessions, total))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_recent_messages(
|
pub async fn list_recent_messages(
|
||||||
|
|||||||
@ -27,8 +27,8 @@ impl Tool for ChatManagerTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"聊天管理工具。可以列出当前活跃的 session、可用的 channel,以及查看指定 session 的消息内容,支持时间范围筛选和分页翻页。\
|
"聊天管理工具。可以列出全部 session、可用的 channel,以及查看指定 session 的消息内容,支持时间范围筛选和分页翻页。\
|
||||||
action 可选值: list_sessions (列出最近活跃会话), list_channels (列出可用渠道), list_messages (查看消息)"
|
action 可选值: list_sessions (列出全部会话), list_channels (列出可用渠道), list_messages (查看消息)"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parameters_schema(&self) -> serde_json::Value {
|
fn parameters_schema(&self) -> serde_json::Value {
|
||||||
@ -38,7 +38,7 @@ action 可选值: list_sessions (列出最近活跃会话), list_channels (列
|
|||||||
"action": {
|
"action": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["list_sessions", "list_channels", "list_messages"],
|
"enum": ["list_sessions", "list_channels", "list_messages"],
|
||||||
"description": "操作类型: list_sessions 列出最近活跃会话, list_channels 列出可用渠道, list_messages 查看指定会话的消息"
|
"description": "操作类型: list_sessions 列出全部会话, list_channels 列出可用渠道, list_messages 查看指定会话的消息"
|
||||||
},
|
},
|
||||||
"session_id": {
|
"session_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -46,11 +46,11 @@ action 可选值: list_sessions (列出最近活跃会话), list_channels (列
|
|||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "获取消息的数量,仅在 action 为 list_messages 时有效,默认 20,最大 100"
|
"description": "获取数量,在 action 为 list_sessions 或 list_messages 时有效,默认 20,最大 100"
|
||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "跳过前 N 条消息(用于翻页),仅在 action 为 list_messages 时有效,默认 0"
|
"description": "跳过前 N 条(用于翻页),在 action 为 list_sessions 或 list_messages 时有效,默认 0"
|
||||||
},
|
},
|
||||||
"before_time": {
|
"before_time": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -80,7 +80,7 @@ action 可选值: list_sessions (列出最近活跃会话), list_channels (列
|
|||||||
|
|
||||||
match action {
|
match action {
|
||||||
"list_channels" => self.list_channels().await,
|
"list_channels" => self.list_channels().await,
|
||||||
"list_sessions" => self.list_sessions().await,
|
"list_sessions" => self.list_sessions(&args).await,
|
||||||
"list_messages" => self.list_messages(&args).await,
|
"list_messages" => self.list_messages(&args).await,
|
||||||
_ => Ok(ToolResult {
|
_ => Ok(ToolResult {
|
||||||
success: false,
|
success: false,
|
||||||
@ -104,23 +104,29 @@ impl ChatManagerTool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_sessions(&self) -> anyhow::Result<ToolResult> {
|
async fn list_sessions(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||||
let sessions = self
|
let count = args["count"].as_i64().unwrap_or(20).clamp(1, 100);
|
||||||
|
let offset = args["offset"].as_i64().unwrap_or(0).max(0);
|
||||||
|
|
||||||
|
let (sessions, total) = self
|
||||||
.storage
|
.storage
|
||||||
.list_all_active_sessions(20)
|
.query_sessions_range(offset, count)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to list sessions: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to list sessions: {}", e))?;
|
||||||
|
|
||||||
if sessions.is_empty() {
|
if sessions.is_empty() {
|
||||||
return Ok(ToolResult {
|
return Ok(ToolResult {
|
||||||
success: true,
|
success: true,
|
||||||
output: "当前没有活跃的会话".to_string(),
|
output: "当前没有会话".to_string(),
|
||||||
error: None,
|
error: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
let mut output = format!("活跃会话 (共 {} 个):\n", sessions.len());
|
let start_num = offset + 1;
|
||||||
|
let end_num = offset + sessions.len() as i64;
|
||||||
|
|
||||||
|
let mut output = format!("全部会话 (共 {} 个,第 {}-{} 个):\n", total, start_num, end_num);
|
||||||
|
|
||||||
for s in &sessions {
|
for s in &sessions {
|
||||||
let ago = format_duration_ago(now_ms - s.last_active_at);
|
let ago = format_duration_ago(now_ms - s.last_active_at);
|
||||||
@ -464,19 +470,18 @@ mod tests {
|
|||||||
|
|
||||||
let tool = ChatManagerTool::new(storage, vec![]);
|
let tool = ChatManagerTool::new(storage, vec![]);
|
||||||
|
|
||||||
// after_time: filter to messages after msg2's timestamp
|
// after_time: filter to messages after msg1's second boundary
|
||||||
let after_ts = (now + 1500) / 1000;
|
let after_ts = now / 1000 + 2;
|
||||||
let result = tool
|
let result = tool
|
||||||
.execute(json!({ "action": "list_messages", "session_id": session_id, "after_time": after_ts }))
|
.execute(json!({ "action": "list_messages", "session_id": session_id, "after_time": after_ts }))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(result.success);
|
assert!(result.success);
|
||||||
assert!(result.output.contains("已按起始时间筛选"));
|
assert!(result.output.contains("已按起始时间筛选"));
|
||||||
assert!(result.output.contains("消息内容 2"));
|
|
||||||
assert!(result.output.contains("消息内容 3"));
|
|
||||||
assert!(result.output.contains("消息内容 4"));
|
|
||||||
assert!(!result.output.contains("消息内容 0"));
|
assert!(!result.output.contains("消息内容 0"));
|
||||||
assert!(!result.output.contains("消息内容 1"));
|
assert!(!result.output.contains("消息内容 1"));
|
||||||
|
assert!(result.output.contains("消息内容 3"));
|
||||||
|
assert!(result.output.contains("消息内容 4"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user