docs: 添加 Session 持久化设计方案
设计目标: - SQLite 持久化,消息实时落盘 - Session = Dialog 概念等价,不再分层 - handle_message 自动恢复或创建 session - Dialog 完整生命周期管理(/new /sessions /switch /rename /delete) - Title 自动生成(10 条用户消息后) - TTL 自动内存清理,Storage 保留所有数据
This commit is contained in:
parent
1ac6de118a
commit
2cbd959ac4
278
docs/plans/2026-04-28-session-persistence-design.md
Normal file
278
docs/plans/2026-04-28-session-persistence-design.md
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
# Session 持久化设计方案
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
为 PicoBot 添加 SQLite 持久化层,实现 Session 数据的持久化、完整 Dialog 生命周期管理、消息实时落盘、以及基于 TTL 的自动内存清理。
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
```
|
||||||
|
UnifiedSessionId = {channel}:{chat_id}:{dialog_id}
|
||||||
|
Session = Dialog(两者等价,不再分层)
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 Session 独立管理自己的消息历史、LLM 配置和路由信息。
|
||||||
|
|
||||||
|
## 数据库 Schema
|
||||||
|
|
||||||
|
### 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 DEFAULT '新对话',
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
last_active_at INTEGER NOT NULL,
|
||||||
|
message_count INTEGER DEFAULT 0,
|
||||||
|
routing_info TEXT,
|
||||||
|
deleted_at INTEGER,
|
||||||
|
UNIQUE(channel, chat_id, dialog_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_sessions_chat ON sessions(channel, chat_id, deleted_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage API
|
||||||
|
|
||||||
|
### 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 条 |
|
||||||
|
| `touch_session(id, message_count, last_active_at)` | 更新计数和最后活跃时间 |
|
||||||
|
| `soft_delete_session(id) -> Result<(), StorageError>` | 软删除 |
|
||||||
|
|
||||||
|
### 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) |
|
||||||
|
|
||||||
|
### 写入失败处理
|
||||||
|
|
||||||
|
重试 3 次(100/200/300ms 退避),仍失败则发送系统通知告警。
|
||||||
|
|
||||||
|
## Session 结构
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Session {
|
||||||
|
pub id: UnifiedSessionId,
|
||||||
|
pub title: String,
|
||||||
|
pub created_at: i64,
|
||||||
|
pub last_active_at: i64,
|
||||||
|
pub message_count: i64, // 用户消息计数
|
||||||
|
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
|
||||||
|
routing_info: String, // JSON 路由信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 初始化流程
|
||||||
|
|
||||||
|
```
|
||||||
|
new() 或 from_storage()
|
||||||
|
↓
|
||||||
|
注入 storage 引用
|
||||||
|
↓
|
||||||
|
创建 provider, tools, compressor
|
||||||
|
↓
|
||||||
|
从 Storage 加载 messages(from_seq = 0)
|
||||||
|
↓
|
||||||
|
设置 seq_counter = messages.len() + 1
|
||||||
|
↓
|
||||||
|
返回 Session 实例
|
||||||
|
```
|
||||||
|
|
||||||
|
## handle_message 流程
|
||||||
|
|
||||||
|
```
|
||||||
|
handle_message(channel, chat_id, sender_id, content, media)
|
||||||
|
│
|
||||||
|
├── 1. 确定 dialog_id
|
||||||
|
│ │
|
||||||
|
│ ├── 显式传入 dialog_id → 使用
|
||||||
|
│ └── 无 dialog_id
|
||||||
|
│ ├── 查找 channel:chat_id 下最近活跃且未过期的 session
|
||||||
|
│ ├── 找到 → 使用该 session
|
||||||
|
│ └── 未找到 → 创建新 session(dialog_id = 新随机 ID)
|
||||||
|
│
|
||||||
|
├── 2. 获取或创建 Session
|
||||||
|
│ 有 → 更新 session_timestamps
|
||||||
|
│ 无 → 从 Storage 恢复 或 创建新 Session
|
||||||
|
│
|
||||||
|
├── 3. 追加用户消息并持久化
|
||||||
|
│ seq = seq_counter; seq_counter += 1
|
||||||
|
│ Storage.append_message()(失败重试 → 告警)
|
||||||
|
│ messages.push(user_msg)
|
||||||
|
│ message_count += 1
|
||||||
|
│
|
||||||
|
├── 4. 检查 title 自动生成
|
||||||
|
│ message_count == 10 且 title == 默认值 → LLM 生成 → 更新 title → 写回 Storage
|
||||||
|
│
|
||||||
|
├── 5. 注入 skills_prompt
|
||||||
|
│
|
||||||
|
├── 6. 新 session 注入欢迎消息(系统消息,不计入 message_count)
|
||||||
|
│
|
||||||
|
├── 7. 上下文压缩(如需要)
|
||||||
|
│
|
||||||
|
├── 8. 调用 AgentLoop
|
||||||
|
│
|
||||||
|
├── 9. 持久化 Agent 响应
|
||||||
|
│
|
||||||
|
└── 10. 返回响应
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dialog 生命周期命令
|
||||||
|
|
||||||
|
| 命令 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| `/new [标题]` | 创建新 dialog(新随机 dialog_id),新建 Session |
|
||||||
|
| `/sessions` | 列出 channel:chat_id 下最近 10 条 session(按 last_active_at 倒序) |
|
||||||
|
| `/switch <dialog_id>` | 切换到指定 session(从 Storage 恢复或内存命中) |
|
||||||
|
| `/rename <新标题>` | 重命名当前 session |
|
||||||
|
| `/delete` | 软删除当前 session(内存移除 + Storage 标记 deleted_at) |
|
||||||
|
| `/info` | 显示当前 session 信息 |
|
||||||
|
| `/compact` | 手动触发上下文压缩 |
|
||||||
|
|
||||||
|
## 路由信息
|
||||||
|
|
||||||
|
每种 Channel 在创建 Session 时注入路由信息:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// CLI
|
||||||
|
routing_info = json!({"type": "cli", "ws_sender_id": "xxx"})
|
||||||
|
|
||||||
|
// Feishu
|
||||||
|
routing_info = json!({"type": "feishu", "open_conversation_id": "oc_xxx", "tenant_key": "xxx"})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Title 自动生成
|
||||||
|
|
||||||
|
调用时机:
|
||||||
|
1. Session 首次创建时(初始 title = "新对话")
|
||||||
|
2. `message_count` 达到 10 且 title 仍为默认值时,自动更新
|
||||||
|
|
||||||
|
生成 Prompt:
|
||||||
|
```
|
||||||
|
给定以下对话历史,生成一个简短的会话标题(5-15 个中文字符),
|
||||||
|
概括这个对话的核心内容或用户的主要需求。只返回一个标题,不要解释。
|
||||||
|
|
||||||
|
历史:
|
||||||
|
{messages}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TTL 清理
|
||||||
|
|
||||||
|
- 内存 session 超时 → 释放内存,Storage 记录保留
|
||||||
|
- 用户切换回该 session → 从 Storage 重新加载到内存
|
||||||
|
- Storage 中的 session 记录通过 `deleted_at` 软删除,不会物理删除
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
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. 添加 `sqlx` + `sqlite` 依赖
|
||||||
|
2. 实现 `Storage` 结构(连接池、初始化)
|
||||||
|
3. Session CRUD + Message CRUD
|
||||||
|
4. 写入重试逻辑
|
||||||
|
5. 单元测试
|
||||||
|
|
||||||
|
### Phase 2: Session 扩展
|
||||||
|
1. 扩展 `Session` 结构(添加 storage、routing_info、计数字段、seq_counter)
|
||||||
|
2. `from_storage()` 恢复逻辑
|
||||||
|
3. `add_message` 持久化集成
|
||||||
|
4. `send_system_notification` 接口
|
||||||
|
5. Title 自动生成
|
||||||
|
|
||||||
|
### Phase 3: SessionManager 完善
|
||||||
|
1. 注入 `Arc<Storage>`
|
||||||
|
2. 实现 `list_dialogs()`
|
||||||
|
3. 实现 `switch_dialog()`
|
||||||
|
4. 实现 `delete_dialog()` / `rename_dialog()`
|
||||||
|
5. 后台 TTL 清理任务
|
||||||
|
6. 集成测试
|
||||||
|
|
||||||
|
### Phase 4: 斜杠命令
|
||||||
|
1. 实现 `/sessions`
|
||||||
|
2. 实现 `/switch`
|
||||||
|
3. 实现 `/rename`
|
||||||
|
4. 实现 `/delete`
|
||||||
|
5. 端到端测试
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session": {
|
||||||
|
"ttl_hours": 24,
|
||||||
|
"cleanup_interval_minutes": 60,
|
||||||
|
"auto_title_after_n_messages": 10,
|
||||||
|
"storage_retry_delays_ms": [100, 200, 300]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与现有代码的冲突点
|
||||||
|
|
||||||
|
| 冲突 | 处理方式 |
|
||||||
|
|------|----------|
|
||||||
|
| `DialogInfo` 有 `archived_at` | 删除该字段,改用 `deleted_at` |
|
||||||
|
| `SessionCommand::ArchiveDialog` | 删除 |
|
||||||
|
| `/new` 现有行为 | 改为创建新 session(新 dialog_id) |
|
||||||
|
| 现有 `Session` 无 storage/routing_info | 扩展结构,新增 `from_storage()` |
|
||||||
|
| `SessionManager` 需注入 `Arc<Storage>` | 扩展构造方法 |
|
||||||
|
| stub 方法 | 实现 |
|
||||||
Loading…
x
Reference in New Issue
Block a user