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

13 KiB
Raw Blame History

Session 管理详细设计

一、设计目标

  1. Session 数据持久化到 SQLite系统重启后可恢复
  2. 支持 Dialog 的完整生命周期管理(创建/列表/切换/重命名/删除)
  3. 基于 TTL 的自动内存清理DB 保留所有数据)
  4. LLM 自动生成会话标题title帮助用户和 AI 理解对话上下文
  5. 每条消息实时写入 DB失败后重试 + 告警

二、概念定义

2.1 层级结构

Channel (渠道)
└── Chat (聊天)
    └── Dialog (对话)
        └── Session (会话实例)
  • Channel:消息来源渠道(如 cli_chatfeishu
  • 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 表

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 表

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 写入失败处理

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

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 消息管理

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 系统通知接口(不记历史)

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

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 清理

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 信息

八、错误处理

pub enum StorageError {
    NotFound(String),
    AlreadyExists(String),
    Database(String),
    Serialization(String),
}
场景 处理
Storage 写入失败 重试 3 次 → 发送系统通知告警
Storage 读取失败 若 session 在内存中,继续使用内存数据
Session 不存在 创建新 session
并发冲突 SQLite transaction 保护

九、配置项

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