PicoBot/docs/plans/2026-04-28-session-persistence-design.md
xiaoxixi 2cbd959ac4 docs: 添加 Session 持久化设计方案
设计目标:
- SQLite 持久化,消息实时落盘
- Session = Dialog 概念等价,不再分层
- handle_message 自动恢复或创建 session
- Dialog 完整生命周期管理(/new /sessions /switch /rename /delete)
- Title 自动生成(10 条用户消息后)
- TTL 自动内存清理,Storage 保留所有数据
2026-04-28 21:45:05 +08:00

8.2 KiB
Raw Blame History

Session 持久化设计方案

概述

为 PicoBot 添加 SQLite 持久化层,实现 Session 数据的持久化、完整 Dialog 生命周期管理、消息实时落盘、以及基于 TTL 的自动内存清理。

核心概念

UnifiedSessionId = {channel}:{chat_id}:{dialog_id}
Session = Dialog两者等价不再分层

每个 Session 独立管理自己的消息历史、LLM 配置和路由信息。

数据库 Schema

sessions 表

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 表

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 结构

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 加载 messagesfrom_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
    │           └── 未找到 → 创建新 sessiondialog_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 时注入路由信息:

// 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. 端到端测试

配置项

{
  "session": {
    "ttl_hours": 24,
    "cleanup_interval_minutes": 60,
    "auto_title_after_n_messages": 10,
    "storage_retry_delays_ms": [100, 200, 300]
  }
}

与现有代码的冲突点

冲突 处理方式
DialogInfoarchived_at 删除该字段,改用 deleted_at
SessionCommand::ArchiveDialog 删除
/new 现有行为 改为创建新 session新 dialog_id
现有 Session 无 storage/routing_info 扩展结构,新增 from_storage()
SessionManager 需注入 Arc<Storage> 扩展构造方法
stub 方法 实现