# PicoBot 架构机制 ## 核心数据流 ``` Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionManager → MessageBus → OutboundDispatcher → Channel ↑ ControlChannel → SessionManager (dialog 操作: 创建/切换/归档/删除) ``` ## 模块职责 | 模块 | 职责 | |------|------| | `gateway` | HTTP/WebSocket 服务器,持有 GatewayState | | `client` | TUI 聊天客户端 | | `channels` | 外部集成(飞书、CLI),仅收发消息 | | `bus` | 异步消息队列,纯队列不路由 | | `session` | 会话生命周期管理、dialog 操作 | | `agent` | LLM 调用循环、工具执行、上下文压缩、媒体处理、子 Agent | | `providers` | LLM API 客户端(OpenAI 兼容、Anthropic) | | `tools` | Agent 工具(bash、文件操作、搜索、HTTP、web、browser、memory、delegate 等) | | `skills` | Skill 加载、管理和 prompt 构建 | | `storage` | SQLite 持久化 | | `scheduler` | Cron 作业调度 | | `observability` | Observer 模式,agent/工具遥测事件 | | `protocol` | WebSocket 协议消息定义 | | `config` | 配置加载、环境变量替换、路径解析 | | `memory` | 长期记忆存储与检索 | | `mcp` | MCP(Model Context Protocol)工具集成 | ## 功能边界 - Channels 仅收发消息,不感知 session 或 LLM - MessageBus 是纯异步队列,不路由 - SessionManager 拥有 session 状态,不直接调 LLM;负责注入 skills prompt - AgentLoop 无状态,接收 dialog 事件调用 LLM、执行工具 - Providers 是纯 HTTP 客户端,无 bus/session/channel 感知 - Tools 接收原始参数,返回字符串结果 - MCP 工具在 Gateway 初始化时连接服务器、发现工具,并包装成普通 Tool 注册到 ToolRegistry - 子 Agent 由 `delegate` 工具创建,复用 provider 配置和按需过滤后的工具集;后台任务结果通过 MessageBus 发回原会话 ## 关键约束 - Gateway 启动时切换到 workspace 目录 - SQLite 数据在 `{workspace}/picobot.db` - ChannelManager 持有 MessageBus 和所有 channel - OutboundDispatcher 通过 ChannelManager 路由出站消息 - Config `.env` 加载使用 `unsafe { env::set_var(...) }` - `browser` 工具只有在 `browser.enabled=true` 时注册,依赖 Chrome/Chromium 与 WebDriver ## 上下文压缩 当上下文接近 token 限制时触发: 1. **快速裁剪**:合并连续同角色消息,截断工具输出 2. **硬截断**:移除过老消息 3. 压缩后保留用户消息确保结构完整 ## Skill 系统 三个优先级(高覆盖低): 1. `{workspace}/skills/` — 最高优先级 2. `~/.picobot/skills/` — 中等优先级 3. `~/.agents/skills/` — 最低优先级 同名 skill 按优先级覆盖。每个 skill 是包含 `SKILL.md` 的目录。内置 skill 在 `~/.picobot/skills/` 下不存在时自动从二进制释放安装。 ## 会话系统 ### 会话 ID 格式 统一会话 ID 为三段式:**`::`** | 部分 | 含义 | 示例 | |------|------|------| | `channel` | 消息渠道 | `cli_chat`、`feishu` | | `chat_id` | 聊天/群组标识 | `sid_abc123` | | `dialog_id` | 对话标识 | `default`、`d_xxxx`(短 ID) | 同一 `channel:chat_id` 下可有多个 dialog。`chat_scope()` 返回 `"channel:chat_id"` 用于分组。 ### Session 生命周期 ``` create → 存入 Storage → 载入 memory → 设为当前 dialog ↓ get_or_create ↓ ← 接收消息、LLM 响应 → ↓ switch → rename → archive → delete(soft) ``` | 操作 | 效果 | |------|------| | `create` | 新建 dialog_id,立即持久化到 SQLite,设为当前 | | `get_or_create` | 先在内存 HashMap 中找 → 再查 Storage → 都不存在则新建 | | `switch_dialog` | 切换当前 dialog,目标 session 自动从 Storage 恢复入内存 | | `list_dialogs` | 列出 `channel:chat_id` 下最近 10 个 session | | `rename` | 更新标题,内存 + Storage 同步 | | `delete` | 软删除(设 deleted_at),从内存移除 | | `archive` | 当前为空操作 | ### SessionManager 数据结构 两层追踪: - **`sessions`**:`HashMap>>` — 所有已加载的 session,key 为完整 session ID - **`current_sessions`**:`HashMap` — 每个 `channel:chat_id` 当前的 session ID 消息到达时 `resolve_dialog_id()` 按顺序确定接收 session:当前 session → Storage 最近活跃 session → 新建。 ### 消息处理三阶段 **阶段 1(持锁)**:斜杠命令检测 → 用户消息入库 → 提取记忆上下文 → 构建系统提示(skills + memory_context)→ 上下文压缩 → 创建 AgentLoop **阶段 2(无锁)**:`agent.process(history)` → LLM 调用 + 工具执行。上下文溢出时自动重新压缩重试 **阶段 3(持锁)**:持久化 agent 响应消息 → 自动生成标题(消息数 ≥ 5 且标题为"新对话"时) ### 会话恢复 从 Storage 恢复 session 时: - 若 `last_compressed_message_at` 存在:先加载近 3 条 Timeline 记忆作为 `[Previous Context]`,再加载压缩标记后的原始消息 - 若无压缩记录:正常加载全部消息 - 自动修复断链的工具调用(gateway 崩溃中途重启导致) --- ## 记忆系统 ### 记忆类别 | 类别 | 用途 | 生命周期 | 检索方式 | |------|------|----------|----------| | **Knowledge** | 事实、偏好、模式、洞察 | 长期保留,手动删除 | 每轮注入系统提示,关键词匹配 | | **Timeline** | 历史会话摘要 | 自动清理(默认 90 天) | `timeline_recall` 工具按需检索 | ### MemoryEntry 字段 | 字段 | 说明 | |------|------| | `id` | UUID | | `key` | 唯一键,同 key 写入覆盖旧值 | | `content` | 记忆内容 | | `category` | `knowledge` 或 `timeline` | | `importance` | 权重 (0.0–1.0),Timeline 默认为 0.3 | | `session_id` | 关联会话(可选) | ### 存储与检索 - 主表 `memories` + FTS5 虚拟表 `memory_fts(key, content)` 全文索引 - 中文分词使用 jieba-rs 逐词精确匹配,用 OR 连接 - FTS5 无结果时回退到 LIKE 模糊匹配 - 支持 category、session_id、时间范围过滤 ### 工作流程 ``` 用户消息到达 → MemoryManager::recall(content, 5, Knowledge) 返回最多 5 条匹配的知识记忆(按 importance DESC) → 格式化为 "- key: content" → 注入系统提示的 "记忆上下文" 部分 → LLM 可见,辅助回答 ``` ### 记忆工具 | 工具 | 写操作 | 说明 | |------|--------|------| | `memory_store` | 是 | 存储 Knowledge。必填: key, content。可选: importance | | `memory_recall` | 否 | 搜索 Knowledge。必填: query(空格分隔关键词)。可选: since, until, limit | | `timeline_recall` | 否 | 搜索 Timeline(压缩后的会话摘要)。必填: query。可选: session_id, since, until | | `memory_forget` | 是 | 按 key 删除记忆 | ### 上下文压缩与 Timeline LLM 对话上下文接近 token 限制 (默认 128K × 70%) 时自动触发压缩: 1. **快速裁剪**:工具输出 ≥ 2000 字符时截断 2. **LLM 摘要**:最多 3 轮,每轮找连续用户消息对,将中间的 assistant/tool 消息压缩为摘要 → 摘要作为 **Timeline 记忆** 持久化(importance 0.3) 3. **硬截断**:若仍超 90%,只保留前 N + 后 N 条消息 压缩后 `last_compressed_message_at` 标记边界,后续恢复时从标记点加载原始消息,以 Timeline 提供更早的上下文。 ### 关键集成点 | 时机 | 操作 | |------|------| | 每次消息处理 | `memory_manager.recall()` 提取 Knowledge 上下文 | | 系统提示构建 | `MemorySection` 渲染记忆指南 + 匹配的记忆 | | 有压缩历史时 | `HistorySection` 提示 LLM 使用 `timeline_recall` | | 压缩完成后 | 摘要自动存储为 Timeline 记忆 | | 空闲时 | 可配置自动 consolidation(`idle_consolidation_minutes`) | --- ## MCP 工具集成 Gateway 初始化时读取 `config.mcp.servers`: 1. 按服务器配置连接 `stdio`、`sse` 或 `streamable-http` 传输 2. 调用 MCP `list_tools` 3. 将每个 MCP tool 包装为 `McpToolWrapper` 4. 注册到当前 session 的 `ToolRegistry` `/mcp` 斜杠命令会显示 MCP 服务器连接状态和工具列表。 --- ## 子 Agent / delegate `delegate` 工具用于把独立任务交给子 Agent: | 模式 | 行为 | |------|------| | `inline` | 当前轮阻塞等待子 Agent 返回 | | `background` | 后台运行,完成后通过原 channel/chat 通知 | | `parallel` | 多个子 Agent 并发执行并聚合结果 | 默认工具集是只读工具:`file_read`、`file_search`、`content_search`、`web_fetch`、`http_request`、`calculator`。调用时可通过 `allowed_tools` 显式放开其他工具。后台任务会写入 `background_tasks` 表,默认 24 小时后清理。 --- ## 当前斜杠命令 | 命令 | 说明 | |------|------| | `/new` | 创建新对话 | | `/sessions` | 列出最近对话 | | `/switch ` | 切换到指定对话 | | `/rename ` | 重命名当前对话 | | `/delete` | 删除当前对话 | | `/compact` | 手动触发上下文压缩 | | `/info` | 显示当前对话信息 | | `/dump` | 保存当前对话为 markdown | | `/?`, `/help` | 显示帮助 | | `/mcp` | 显示 MCP 状态 | | `/stop` | 停止当前任务并清空消息队列 |