PicoBot/session_plan.md
xiaoxixi 7220e89a22 简化斜杠命令,移除不可用的命令
删除:sessions, switch, rename, archive
保留:new, delete, compact, info
2026-04-28 20:50:01 +08:00

434 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Session 管理详细设计
## 一、设计目标
1. Session 数据持久化到 SQLite系统重启后可恢复
2. 支持 Dialog 的完整生命周期管理(创建/列表/切换/重命名/删除)
3. 基于 TTL 的自动内存清理DB 保留所有数据)
4. LLM 自动生成会话标题title帮助用户和 AI 理解对话上下文
5. 每条消息实时写入 DB失败后重试 + 告警
---
## 二、概念定义
### 2.1 层级结构
```
Channel (渠道)
└── Chat (聊天)
└── Dialog (对话)
└── Session (会话实例)
```
- **Channel**:消息来源渠道(如 `cli_chat``feishu`
- **Chat**:同一渠道下的一个聊天会话(如某个 CLI session ID 或飞书 open_conversation_id
- **Dialog**:聊天内的多个独立对话线程(如 `/new` 创建的新对话)
- **Session**:一个 Dialog 的运行时实例包含消息历史、LLM 配置、工具等
### 2.2 UnifiedSessionId
```
格式:{channel}:{chat_id}:{dialog_id}
示例cli_chat:sid_abc123:dialog_xyz
feishu:oc_123456:default
```
| 字段 | 说明 |
|------|------|
| channel | 渠道标识 |
| chat_id | 聊天标识 |
| dialog_id | 对话标识(默认 `default` |
### 2.3 Session 与 Dialog 的关系
- 每个 Dialog 在运行时对应一个 `Session` 实例
- `Session` 存在于内存中,可通过 `UnifiedSessionId` 访问
- `Dialog` 是 Storage 中的持久化记录,`Session` 是其运行时投影
---
## 三、数据库 Schema
### 3.1 sessions 表
```sql
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
chat_id TEXT NOT NULL,
dialog_id TEXT NOT NULL,
title TEXT NOT NULL,
created_at INTEGER NOT NULL,
last_active_at INTEGER NOT NULL,
message_count INTEGER DEFAULT 0,
deleted_at INTEGER,
UNIQUE(channel, chat_id, dialog_id)
);
```
> 注意:已删除 `archived_at` 字段,不保留归档概念。
### 3.2 messages 表
```sql
CREATE TABLE messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
seq INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
media_refs TEXT,
tool_call_id TEXT,
tool_name TEXT,
tool_calls TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX idx_messages_session_seq ON messages(session_id, seq);
CREATE INDEX idx_sessions_chat ON sessions(channel, chat_id, deleted_at);
```
---
## 四、Storage API
### 4.1 Storage 职责
- 唯一的持久化 source of truth
- 由调用方GatewayState构造通过 `Arc<Storage>` 注入 `SessionManager`
- 所有写操作失败后重试 3 次100ms, 200ms, 300ms 退避),仍失败则触发系统通知告警
### 4.2 Session 操作
| 方法 | 说明 |
|------|------|
| `new(db_path) -> Storage` | 打开/创建数据库 |
| `upsert_session(meta) -> Result<(), StorageError>` | 插入或更新 session 元数据 |
| `get_session(id) -> Result<SessionMeta, StorageError>` | 获取单个 session |
| `list_sessions(channel, chat_id, limit) -> Result<Vec<SessionMeta>>` | 最近 N 条(供 `/sessions` |
| `delete_session(id) -> Result<(), StorageError>` | 物理删除 session 及关联消息 |
| `touch_session(id, message_count, last_active_at)` | 更新计数和最后活跃时间 |
### 4.3 Message 操作
| 方法 | 说明 |
|------|------|
| `append_message(session_id, msg) -> Result<i64, StorageError>` | 追加单条消息,返回 seq |
| `append_messages(session_id, msgs) -> Result<Vec<i64>, StorageError>` | 批量追加 |
| `load_messages(session_id, from_seq) -> Result<Vec<MessageMeta>>` | 从指定 seq 加载消息 |
| `clear_messages(session_id) -> Result<(), StorageError>` | 清除消息(保留 session |
### 4.4 写入失败处理
```rust
async fn append_message_with_retry(&self, session_id: &str, msg: &MessageMeta) -> Result<i64, StorageError> {
let delays = [100, 200, 300];
for (i, delay) in delays.iter().enumerate() {
match self.append_message(session_id, msg) {
Ok(seq) => return Ok(seq),
Err(e) if i < delays.len() - 1 => {
sleep(Duration::from_millis(*delay)).await;
tracing::warn!("Storage write failed, retrying: {}", e);
}
Err(e) => {
// 全部重试失败后,通过 Session 发送系统通知
return Err(e);
}
}
}
unreachable!()
}
```
---
## 五、Session 结构
```rust
pub struct Session {
pub id: UnifiedSessionId,
pub title: String, // 会话标题(用户指定或 LLM 自动生成)
pub created_at: i64, // 创建时间ms
pub last_active_at: i64, // 最后活跃时间ms
pub message_count: i64, // 用户消息计数(触发 title 自动生成)
pub total_message_count: i64, // 含系统消息的总数
messages: Vec<ChatMessage>, // 内存中的消息历史(压缩后)
seq_counter: i64, // 下一个消息的 seq
provider_config: LLMProviderConfig,
provider: Arc<dyn LLMProvider>,
tools: Arc<ToolRegistry>,
compressor: ContextCompressor,
user_tx: mpsc::Sender<WsOutbound>,
storage: Arc<Storage>, // 持久化 sink
}
```
### 5.1 初始化流程
```
new() 或 from_storage()
注入 storage 引用
创建 provider, tools, compressor
从 Storage 加载 messagesfrom_seq = 0
设置 seq_counter = messages.len() + 1
返回 Session 实例
```
### 5.2 消息管理
```rust
pub async fn add_message(&mut self, msg: ChatMessage) -> Result<(), StorageError> {
// 1. 分配序号: seq = seq_counter; seq_counter += 1
// 2. 转换为 MessageMeta
// 3. 写入 Storage重试 + 告警)
// 4. 更新内存: messages.push(msg)
// 5. 更新计数: message_count / total_message_count += 1
// 6. 更新 last_active_at
}
```
### 5.3 系统通知接口(不记历史)
```rust
impl Session {
/// 发送系统通知(不记录进 session 历史)
pub async fn send_system_notification(&self, content: &str) {
let msg = WsOutbound::SystemNotification {
content: content.to_string(),
};
let _ = self.user_tx.send(msg).await;
}
}
```
### 5.4 Title 自动生成
调用时机:
1. Session 首次创建时(初始 title = "Dialog {dialog_id}"
2. `message_count` 达到阈值10 条)且 title 仍为默认值时,自动更新为 LLM 生成
3. 用户执行 `/rename` 命令手动更新
生成 Prompt
```
给定以下对话历史生成一个简短的会话标题5-15 个中文字符),
概括这个对话的核心内容或用户的主要需求。只返回一个标题,不要解释。
历史:
{messages}
```
---
## 六、SessionManager 设计
### 6.1 数据结构
```rust
pub struct SessionManager {
inner: Arc<Mutex<SessionManagerInner>>,
provider_config: LLMProviderConfig,
tools: Arc<ToolRegistry>,
skills_loader: Arc<SkillsLoader>,
storage: Arc<Storage>, // 由调用方注入
cleanup_interval: Duration,
}
struct SessionManagerInner {
sessions: HashMap<String, Arc<Mutex<Session>>>,
session_timestamps: HashMap<String, Instant>,
session_ttl: Duration,
}
```
### 6.2 handle_message 完整流程
```
handle_message(channel, sender_id, chat_id, dialog_id, content, media)
├── 1. 确定 UnifiedSessionId
│ │
│ ├── dialog_id 有值 → 直接使用
│ │
│ └── dialog_id 无值 → 查找 channel:chat_id 下最近活跃的 session
│ ├── 找到且未过期 → 使用该 session
│ └── 未找到或已过期 → 创建新 session
├── 2. 获取或创建 Session
│ 有 → 更新 session_timestamps
│ 无 → 从 Storage 恢复 或 创建新 Session
├── 3. 添加用户消息并持久化
│ seq = seq_counter; seq_counter += 1
│ Storage.append_message()(失败重试 → 仍失败则 send_system_notification
│ messages.push(user_msg)
│ message_count += 1
├── 4. 检查 title 自动生成条件
│ message_count == 10 → 调用 LLM 生成 → 更新 title → 写回 Storage
├── 5. 注入 skills_promptindex 0 之后)
├── 6. 新 session 注入欢迎消息(作为系统消息,不计入 message_count
├── 7. 上下文压缩(如需要)
├── 8. 调用 AgentLoop
├── 9. 保存 Agent 响应消息并持久化(同样流程)
└── 10. 返回最终响应
```
**欢迎消息**(仅新 session 创建时注入历史):
```
新对话已创建!会话 ID: {dialog_id}
使用 /sessions 查看所有对话,/switch <id> 切换对话。
```
### 6.3 Dialog 生命周期
| 操作 | 方法 | 说明 |
|------|------|------|
| 创建 | `create_dialog()` | 生成 dialog_id创建 Session写入 Storage |
| 列表 | `list_dialogs()` | 从 Storage 读取limit=10 |
| 切换 | `switch_dialog()` | 从 Storage 加载 session激活到内存 |
| 重命名 | `rename_dialog()` | 更新 Storage 和内存 title |
| 删除 | `delete_dialog()` | 删除内存 session + 删除 Storage 记录 |
| 软重置 | **已删除** | 用户直接 `/new` 开新 session |
### 6.4 TTL 清理
```rust
fn start_cleanup_task(&self) {
tokio::spawn(async move {
loop {
sleep(cleanup_interval).await;
self.run_cleanup().await;
}
});
}
async fn run_cleanup(&self) {
// 扫描 session_timestamps
// 超时的 session → 从内存 HashMap 移除
// Storage 中的 session 记录保留(用户切回可重新加载)
}
```
清理策略:
- 内存 session 超时 → 仅释放内存Storage 记录保留
- 用户切换回该 session → 从 Storage 重新加载到内存
---
## 七、斜杠命令
| 命令 | 触发词 | 说明 |
|------|--------|------|
| new | `/new [标题]` | 创建新 dialog |
| sessions | `/sessions` | 列出当前 chat 最近 10 条 dialog |
| switch | `/switch <dialog_id>` | 切换到指定 dialog |
| rename | `/rename <新标题>` | 重命名当前 dialog |
| delete | `/delete` | 删除当前 dialog内存 + Storage |
| compact | `/compact` | 手动触发上下文压缩 |
| info | `/info` | 显示当前 dialog 信息 |
---
## 八、错误处理
```rust
pub enum StorageError {
NotFound(String),
AlreadyExists(String),
Database(String),
Serialization(String),
}
```
| 场景 | 处理 |
|------|------|
| Storage 写入失败 | 重试 3 次 → 发送系统通知告警 |
| Storage 读取失败 | 若 session 在内存中,继续使用内存数据 |
| Session 不存在 | 创建新 session |
| 并发冲突 | SQLite transaction 保护 |
---
## 九、配置项
```json
{
"session": {
"ttl_hours": 24,
"cleanup_interval_minutes": 60,
"auto_title_after_n_messages": 10,
"storage_retry_delays_ms": [100, 200, 300]
}
}
```
---
## 十、文件结构
```
src/
├── storage/
│ ├── mod.rs # Storage 主模块
│ ├── session.rs # Session CRUD
│ ├── message.rs # Message CRUD
│ └── error.rs # StorageError
└── session/
├── mod.rs # 导出 Session, SessionManager
├── session.rs # Session, SessionManager 实现
├── session_id.rs # UnifiedSessionId已有
├── commands.rs # SessionCommand已有
├── events.rs # SessionEvent, DialogInfo已有
└── error.rs # SessionError已有
```
---
## 十一、实现顺序
### Phase 1: Storage 基础
1. `Storage` 结构和数据库初始化
2. Session CRUDupsert_session, get_session, list_sessions, delete_session
3. Message CRUDappend_message, load_messages
4. 写入失败重试逻辑
5. 单元测试
### Phase 2: Session 扩展
1. 扩展 `Session` 结构体(添加 storage 引用、计数字段、seq_counter
2. `add_message` 持久化集成
3. `send_system_notification` 接口
4. `from_storage()` 恢复逻辑
5. Title 自动生成 LLM 调用
### Phase 3: SessionManager 完善
1.`Arc<Storage>` 集成到 `SessionManager`
2. 实现 `list_dialogs()`limit=10
3. 实现 `switch_dialog()`(从 Storage 加载)
4. 实现 `delete_dialog()`(内存 + Storage
5. 实现 `rename_dialog()`
6. 后台 TTL 清理任务
7. 集成测试
### Phase 4: 斜杠命令
1. 实现 `/sessions`(列出最近 10 条)
2. 实现 `/switch`
3. 实现 `/rename`
4. 实现 `/delete`
5. 端到端测试