From 2cbd959ac44a99c9ad7b3dfa558c42598d465ba4 Mon Sep 17 00:00:00 2001 From: xiaoxixi Date: Tue, 28 Apr 2026 21:45:05 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Session=20?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E8=AE=BE=E8=AE=A1=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设计目标: - SQLite 持久化,消息实时落盘 - Session = Dialog 概念等价,不再分层 - handle_message 自动恢复或创建 session - Dialog 完整生命周期管理(/new /sessions /switch /rename /delete) - Title 自动生成(10 条用户消息后) - TTL 自动内存清理,Storage 保留所有数据 --- .../2026-04-28-session-persistence-design.md | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 docs/plans/2026-04-28-session-persistence-design.md diff --git a/docs/plans/2026-04-28-session-persistence-design.md b/docs/plans/2026-04-28-session-persistence-design.md new file mode 100644 index 0000000..5b893d1 --- /dev/null +++ b/docs/plans/2026-04-28-session-persistence-design.md @@ -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` | 获取单个 session | +| `list_sessions(channel, chat_id, limit) -> Result>` | 最近 N 条 | +| `touch_session(id, message_count, last_active_at)` | 更新计数和最后活跃时间 | +| `soft_delete_session(id) -> Result<(), StorageError>` | 软删除 | + +### Message 操作 + +| 方法 | 说明 | +|------|------| +| `append_message(session_id, msg) -> Result` | 追加单条消息,返回 seq | +| `append_messages(session_id, msgs) -> Result, StorageError>` | 批量追加 | +| `load_messages(session_id, from_seq) -> Result>` | 从指定 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, // 内存消息历史 + seq_counter: i64, // 下一个消息的 seq + + provider_config: LLMProviderConfig, + provider: Arc, + tools: Arc, + compressor: ContextCompressor, + user_tx: mpsc::Sender, + storage: Arc, // 持久化 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 ` | 切换到指定 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` +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` | 扩展构造方法 | +| stub 方法 | 实现 |