更新文档

This commit is contained in:
xiaoxixi 2026-07-14 13:02:06 +08:00
parent 18f1e47f77
commit 901b622b40
10 changed files with 490 additions and 87 deletions

View File

@ -1,5 +1,7 @@
# PicoBot # PicoBot
This file is the operational contract for coding agents working in this repository. Read [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) before making architectural or lifecycle changes. Code and tests are the final source of truth; update the document when an architectural invariant changes.
## Build & Run ## Build & Run
- `cargo build` — build the binary - `cargo build` — build the binary
@ -8,16 +10,18 @@
## Config ## Config
- Config load order: `~/.picobot/config.json` then fallback to `./config.json` (`src/config/mod.rs:237-267`) - Config load order: `~/.picobot/config.json` then fallback to `./config.json` (`Config::load_default` in `src/config/mod.rs`)
- `.env` (cwd) is loaded with a custom parser, not via dotenv crate; env var placeholders `<VAR_NAME>` in config JSON are substituted - `.env` (cwd) is loaded with a custom parser, not via dotenv crate; env var placeholders `<VAR_NAME>` in config JSON are substituted
- Config example: `resources/templates/config.example.json` (released to `~/.picobot/` on first run) - Config example: `resources/templates/config.example.json` (released to `~/.picobot/` on first run)
## Tests ## Tests
- `cargo test --lib` — run unit tests (runs all `#[test]` in `src/`) - `cargo test --lib` — run unit tests (runs all `#[test]` in `src/`)
- `cargo test --test test_integration -- --ignored` — run integration tests (also `test_tool_calling`, `test_request_format`) - `cargo clippy --all-targets --all-features -- -D warnings` — required for Rust changes
- **All** integration tests require `tests/test.env` with real API keys; copy from `tests/test.env.example` and fill in keys - `cargo test --test test_scheduler` and `cargo test --test test_request_format` — offline integration/protocol tests
- Integration tests are `#[ignore]` by default; use `-- --ignored` to run them - `cargo test --test test_integration -- --ignored` and `cargo test --test test_tool_calling -- --ignored` — model API integration tests
- API-backed tests require `tests/test.env` with real API keys; copy from `tests/test.env.example` and fill in keys
- API-backed tests are `#[ignore]` by default; use `-- --ignored` to run them
## Reference ## Reference
@ -33,9 +37,12 @@
### Core Data Flow ### Core Data Flow
``` ```
Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionManager → MessageBus → OutboundDispatcher → Channel Channel → MessageBus.inbound → Gateway processor → SessionManager → per-session worker → AgentLoop
↑ │
ControlChannel ──→ SessionManager (dialog ops: create/switch/archive/delete) └── Channel ← OutboundDispatcher ← per-conversation lane ← MessageBus.outbound
WebSocket/Channel → MessageBus.control → Gateway processor → SessionManager (dialog operations)
Scheduler → SessionManager.handle_cron_message → AgentLoop → send_message tool
``` ```
### Modules ### Modules
@ -45,8 +52,8 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
| `gateway` | Server lifecycle, HTTP/WS endpoints, owns `GatewayState` | `GatewayState`, `run()` | | `gateway` | Server lifecycle, HTTP/WS endpoints, owns `GatewayState` | `GatewayState`, `run()` |
| `client` | TUI rendering, WebSocket client for CLI chat | `App`, `run()` | | `client` | TUI rendering, WebSocket client for CLI chat | `App`, `run()` |
| `channels` | External integrations (Feishu, CLI chat) | `ChannelManager`, `Channel` trait | | `channels` | External integrations (Feishu, CLI chat) | `ChannelManager`, `Channel` trait |
| `bus` | Async message queue (inbound/outbound/control channels) | `MessageBus`, `InboundMessage`, `OutboundMessage`, `ControlMessage` | | `bus` | Bounded async queues and ordered outbound delivery lanes | `MessageBus`, `OutboundDispatcher`, `InboundMessage`, `OutboundMessage`, `ControlMessage` |
| `session` | Conversation session lifecycle, dialog operations | `SessionManager`, `Session` | | `session` | Conversation lifecycle, dialog operations, per-session serialization, persistence coordination | `SessionManager`, `Session` |
| `agent` | LLM call loop, tool execution, context compression | `AgentLoop` | | `agent` | LLM call loop, tool execution, context compression | `AgentLoop` |
| `providers` | LLM API clients (OpenAI-compatible, Anthropic) | `LLMProvider` trait, factory `create_provider()` | | `providers` | LLM API clients (OpenAI-compatible, Anthropic) | `LLMProvider` trait, factory `create_provider()` |
| `tools` | Agent tools (bash, file ops, http, web, get_skill) | `ToolRegistry`, `Tool` trait | | `tools` | Agent tools (bash, file ops, http, web, get_skill) | `ToolRegistry`, `Tool` trait |
@ -57,22 +64,51 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
| `protocol` | WebSocket protocol message types | `WsInbound`, `WsOutbound`, `SessionSummary` | | `protocol` | WebSocket protocol message types | `WsInbound`, `WsOutbound`, `SessionSummary` |
| `config` | Config loading, env substitution, path resolution | `Config`, `LLMProviderConfig` | | `config` | Config loading, env substitution, path resolution | `Config`, `LLMProviderConfig` |
| `logging` | Tracing initialization with file rotation | `init_logging()`, `init_logging_console_only()` | | `logging` | Tracing initialization with file rotation | `init_logging()`, `init_logging_console_only()` |
| `task_supervisor` | Owns, cancels, and boundedly joins gateway background tasks | `TaskSupervisor` |
### Functional Boundaries ### Functional Boundaries
- **Channels** only send/receive messages via `MessageBus`; they know nothing about sessions or LLM - **Channels** only send/receive messages via `MessageBus`; they know nothing about sessions or LLM
- **MessageBus** is a pure async queue; it routes nothing, just passes messages - **MessageBus** owns bounded inbound/outbound/control queues; outbound routing and retries belong to `OutboundDispatcher`, not the queue
- **SessionManager** owns session state and dialog operations; it does NOT call LLM directly - **SessionManager** owns session state, dialog operations, context construction, per-session work queues, and persistence coordination
- SessionManager is responsible for injecting skills prompt into conversation history - **AgentLoop** is stateless across turns; it receives prepared history, calls LLM providers, executes tools, and returns one result
- **AgentLoop** receives dialog events from `SessionManager`, calls LLM via `providers`, executes tools, returns text responses
- AgentLoop is stateless; all state is managed by Session/SessionManager
- **Providers** are pure HTTP clients; no bus/session/channel awareness - **Providers** are pure HTTP clients; no bus/session/channel awareness
- **Tools** are executed by `AgentLoop`; they receive raw arguments and return string results - **Tools** are executed by `AgentLoop`; they receive raw arguments and return string results
### Concurrency and Lifecycle Invariants
- Messages in one session are processed serially through a bounded queue; different sessions may run concurrently
- Outbound messages are ordered per `(channel, chat_id)`; a slow destination must not block unrelated destinations
- Never hold a Session mutex across model, network, or database I/O unless a documented invariant requires it
- Slow work derived from session state must validate `worker_generation`/`state_version` before committing results
- Related durable mutations use Storage transaction APIs; persistence failure must not leave silent memory/database divergence
- Long-lived gateway tasks must be owned by `TaskSupervisor`; connection-local tasks must be explicitly joined or aborted by their owner
- Connection, retry sleep, queue wait, and shutdown join paths must observe cancellation and have hard time bounds
- Retry only errors explicitly classified as transient; permanent failures must surface immediately
- Never log secrets, authorization headers, or full connection URLs containing temporary credentials
- The workspace cwd is not a filesystem sandbox: default file tools accept absolute paths and Bash inherits process permissions; add explicit canonical-path/process isolation when a hard boundary is required
### Key Constraints ### Key Constraints
- Gateway **changes working directory** to workspace on startup (`src/gateway/mod.rs:31`) - Gateway **changes working directory** to workspace in `GatewayState::new` (`src/gateway/mod.rs`)
- Session/message persistence uses SQLite via `sqlx`; DB stored in workspace as `picobot.db` by default - Session/message persistence uses SQLite via `sqlx`; DB stored in workspace as `picobot.db` by default
- `ChannelManager` owns the `MessageBus` and all channel instances - `ChannelManager` owns the `MessageBus` and all channel instances
- `OutboundDispatcher` routes outbound messages to the correct channel via `ChannelManager` - `OutboundDispatcher` routes outbound messages to the correct channel via `ChannelManager`
- Config `.env` loading uses `unsafe { env::set_var(...) }` — don't refactor to safer patterns without understanding side effects - Config `.env` loading uses `unsafe { env::set_var(...) }` — don't refactor to safer patterns without understanding side effects
## Change Workflow
1. Inspect the relevant implementation, tests, and `docs/ARCHITECTURE.md`; do not rely on names or old reports alone.
2. Search `reference/` only for comparison. Never edit it or copy behavior without checking PicoBot's boundaries.
3. Preserve unrelated user changes in a dirty worktree. Use `rg` for search and `apply_patch` for edits.
4. Add regression tests for bugs, especially cancellation, timeout, queue saturation, stale state, persistence failure, and retry classification.
5. For Rust changes run targeted tests, `cargo test --lib`, Clippy with warnings denied, and `cargo build`. Integration tests require real credentials.
6. For documentation-only changes verify links, commands, paths, and `git diff --check`.
7. Update README, this file, and the architecture document together when public behavior or an architectural invariant changes.
## Documentation Roles
- `README.md` — user-facing overview, setup, capabilities, and navigation
- `docs/ARCHITECTURE.md` — maintainer-facing runtime design, invariants, lifecycle, and extension guidance
- `AGENTS.md` — concise operational rules for repository agents
- `resources/skills/about-picobot/references/` — runtime knowledge shipped to PicoBot; update it only when the assistant's built-in product knowledge must change

View File

@ -4,7 +4,7 @@ PicoBot 是一个用 Rust 编写的个人 AI 助手运行时。它在本地启
它更像一个可扩展的“个人助手操作系统”渠道负责收发消息SessionManager 负责会话和上下文AgentLoop 负责模型与工具循环Storage 负责可靠落盘。 它更像一个可扩展的“个人助手操作系统”渠道负责收发消息SessionManager 负责会话和上下文AgentLoop 负责模型与工具循环Storage 负责可靠落盘。
![PicoBot runtime architecture](docs/assets/runtime-architecture.svg) 完整的组件边界、并发不变量、启动/关停顺序和扩展指南见 [架构文档](docs/ARCHITECTURE.md)。
## 适合做什么 ## 适合做什么
@ -95,7 +95,9 @@ CLI 默认连接 `ws://127.0.0.1:19876/ws`。如需指定地址,可使用 `--g
用户消息进入 PicoBot 后,会被转换为统一的 inbound message经由 MessageBus 交给 SessionManager。SessionManager 选择当前 dialog、组装上下文、调用 AgentLoopAgentLoop 调用模型和工具,最终响应通过 outbound bus 回到原渠道。 用户消息进入 PicoBot 后,会被转换为统一的 inbound message经由 MessageBus 交给 SessionManager。SessionManager 选择当前 dialog、组装上下文、调用 AgentLoopAgentLoop 调用模型和工具,最终响应通过 outbound bus 回到原渠道。
![Message flow](docs/assets/message-flow.svg) 详细时序和失败语义见 [架构文档:消息与控制数据流](docs/ARCHITECTURE.md#4-消息与控制数据流)。
同一 session 的普通消息由专属有界队列串行处理,不同 session 可以并发;出站消息按 `(channel, chat_id)` 分 lane 保序慢渠道不会阻塞其他目标。Gateway 的长生命周期任务统一由 `TaskSupervisor` 取消和限时回收。
核心边界: 核心边界:
@ -111,6 +113,7 @@ CLI 默认连接 `ws://127.0.0.1:19876/ws`。如需指定地址,可使用 `--g
| `scheduler` | 轮询 Cron 任务并把任务 prompt 送入目标会话 | | `scheduler` | 轮询 Cron 任务并把任务 prompt 送入目标会话 |
| `skills` | 加载 Skill并把 Skill 指南注入系统提示 | | `skills` | 加载 Skill并把 Skill 指南注入系统提示 |
| `mcp` | 连接 MCP Server将远端工具包装成普通 Tool | | `mcp` | 连接 MCP Server将远端工具包装成普通 Tool |
| `task_supervisor` | 统一管理 Gateway 后台任务的取消和有界关停 |
## 核心能力 ## 核心能力
@ -156,7 +159,7 @@ PicoBot 有两类记忆:
| Knowledge | 偏好、事实、项目规则、长期可复用信息 | 长期保留,手动删除 | | Knowledge | 偏好、事实、项目规则、长期可复用信息 | 长期保留,手动删除 |
| Timeline | 长对话压缩后的历史摘要 | 默认保留 90 天 | | Timeline | 长对话压缩后的历史摘要 | 默认保留 90 天 |
每轮处理用户消息时MemoryManager 会按用户输入召回最多 `memory.recall_limit` 条 Knowledge并注入系统提示。上下文压缩产生的摘要会保存为 Timeline后续可通过 `timeline_recall` 工具检索。 每轮处理用户消息时MemoryManager 会按用户输入召回 Knowledge并作为运行时上下文附加到本轮用户消息。当前召回上限固定为 5`memory.recall_limit` 已支持解析但尚未接入 worker。上下文压缩产生的摘要会保存为 Timeline后续可通过 `timeline_recall` 工具检索。
### 工具 ### 工具
@ -214,7 +217,7 @@ Skill 是包含 `SKILL.md` 的目录。加载优先级从高到低:
| `gateway.max_concurrent_background_tasks` | `10` | | `gateway.max_concurrent_background_tasks` | `10` |
| `gateway.scheduler.enabled` | `true` | | `gateway.scheduler.enabled` | `true` |
| `client.gateway_url` | `ws://127.0.0.1:19876/ws` | | `client.gateway_url` | `ws://127.0.0.1:19876/ws` |
| `memory.recall_limit` | `5` | | `memory.recall_limit` | `5`(当前运行时固定为 5 |
| `memory.timeline_retention_days` | `90` | | `memory.timeline_retention_days` | `90` |
| `mcp.tool_timeout_secs` | `180` | | `mcp.tool_timeout_secs` | `180` |
| `browser.enabled` | `false` | | `browser.enabled` | `false` |
@ -253,14 +256,17 @@ Outbound 消息类型包括 `assistant_response`、`error`、`session_establishe
# 单元测试 # 单元测试
cargo test --lib cargo test --lib
# 集成测试需要 tests/test.env 中有真实 API key # 离线集成/协议测试
cargo test --test test_scheduler
cargo test --test test_request_format
# 模型 API 集成测试需要 tests/test.env 中有真实 API key
cp tests/test.env.example tests/test.env cp tests/test.env.example tests/test.env
cargo test --test test_integration -- --ignored cargo test --test test_integration -- --ignored
cargo test --test test_tool_calling -- --ignored cargo test --test test_tool_calling -- --ignored
cargo test --test test_request_format -- --ignored
``` ```
集成测试默认 `#[ignore]`,因为它们会真实调用模型 API 会真实调用模型 API 的测试标记为 `#[ignore]`;离线集成测试默认执行
## 项目结构 ## 项目结构
@ -281,11 +287,12 @@ src/
skills/ Skill 加载和内置 Skill 安装 skills/ Skill 加载和内置 Skill 安装
storage/ SQLite schema 和 CRUD storage/ SQLite schema 和 CRUD
tools/ Agent 工具实现 tools/ Agent 工具实现
task_supervisor.rs Gateway 后台任务的生命周期管理
resources/ resources/
skills/ 构建时嵌入的内置 Skills skills/ 构建时嵌入的内置 Skills
templates/ 首次运行释放的配置和用户模板 templates/ 首次运行释放的配置和用户模板
tests/ 单元测试和 ignored 集成测试 tests/ 单元测试和 ignored 集成测试
docs/ 分析报告、文档插图和补充资料 docs/ 面向维护者和 Agent 的架构与开发文档
``` ```
## 关键依赖 ## 关键依赖
@ -304,9 +311,9 @@ docs/ 分析报告、文档插图和补充资料
## 进一步阅读 ## 进一步阅读
- [架构机制](resources/skills/about-picobot/references/architecture.md) - [维护者架构文档](docs/ARCHITECTURE.md)
- [内置 Skill架构机制](resources/skills/about-picobot/references/architecture.md)
- [配置说明](resources/skills/about-picobot/references/config.md) - [配置说明](resources/skills/about-picobot/references/config.md)
- [命令说明](resources/skills/about-picobot/references/commands.md) - [命令说明](resources/skills/about-picobot/references/commands.md)
- [工具说明](resources/skills/about-picobot/references/tools.md) - [工具说明](resources/skills/about-picobot/references/tools.md)
- [数据库结构](resources/skills/about-picobot/references/db-schema.md) - [数据库结构](resources/skills/about-picobot/references/db-schema.md)
- [代码质量分析](docs/CODE_QUALITY_ANALYSIS.md)

281
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,281 @@
# PicoBot 架构
本文档描述 PicoBot 当前实现的运行时边界、数据流、并发模型和演进约束。它面向维护者和后续参与改进的 Agent是代码架构的主入口行为细节仍以代码和测试为最终依据。
## 1. 设计目标
PicoBot 是一个单进程、异步、可扩展的个人 AI 助手运行时。核心目标是:
- 用统一消息模型接入不同聊天渠道。
- 隔离渠道、会话、模型、工具和持久化职责。
- 同一会话内保持消息顺序,不同会话之间允许并发。
- 让外部 I/O、后台任务和程序关停都有明确边界。
- 通过 SQLite 保存会话、消息、记忆、定时任务及后台任务状态。
当前不是分布式系统。除调度任务使用数据库租约防止重复领取外,运行时会话状态由单个 Gateway 进程持有。
## 2. 运行模式与进程边界
PicoBot 只有一个二进制,提供两种模式:
| 模式 | 入口 | 职责 |
|------|------|------|
| Gateway | `cargo run -- gateway` | 组装服务、监听 HTTP/WebSocket、运行渠道、会话、调度器和后台任务 |
| CLI client | `cargo run -- chat` | 运行 Ratatui UI通过 WebSocket 使用 Gateway不持有业务状态 |
Gateway 启动时会切换进程工作目录到 `workspace_dir`。因此所有相对路径都应按 workspace 解释,不能假设仍位于源码仓库。
## 3. 组件关系
```mermaid
flowchart LR
External[CLI / Feishu] --> Channels[channels]
Channels -->|InboundMessage| Bus[MessageBus]
Bus --> Processor[Gateway message processor]
Processor --> Sessions[SessionManager]
Sessions --> Agent[AgentLoop]
Agent --> Providers[LLM providers]
Agent --> Tools[ToolRegistry / MCP]
Sessions <--> Storage[(SQLite)]
Sessions -->|OutboundMessage| Bus
Scheduler[Scheduler] --> Sessions
Bus --> Dispatcher[OutboundDispatcher]
Dispatcher --> Channels
Supervisor[TaskSupervisor] -. lifecycle .-> Processor
Supervisor -. lifecycle .-> Dispatcher
Supervisor -. lifecycle .-> Scheduler
Supervisor -. lifecycle .-> Sessions
```
### 模块职责
| 模块 | 拥有的职责 | 不应承担的职责 |
|------|------------|----------------|
| `gateway` | 依赖装配、HTTP/WS 入口、启动和关停顺序 | 业务规则、渠道协议细节 |
| `channels` | 外部协议适配、权限检查、媒体收发 | 会话选择、LLM 调用 |
| `bus` | 三条有界异步队列与出站投递协调 | 会话路由、业务状态 |
| `session` | dialog 路由、会话状态、串行工作队列、上下文和持久化协调 | 外部渠道协议 |
| `agent` | 单次无状态模型/工具循环、上下文压缩、子 Agent | 持有 dialog 生命周期 |
| `providers` | 把统一请求映射到模型 API | Session、Bus 或 Channel 感知 |
| `tools` / `mcp` | 工具定义、注册和执行适配 | 隐式修改会话路由 |
| `storage` | SQLite schema、迁移、原子 CRUD | 运行时调度策略 |
| `memory` | Knowledge/Timeline 的存取与召回 | 直接驱动消息发送 |
| `scheduler` | 领取到期任务、限并发执行、原子记录结果 | 复用聊天会话历史 |
| `task_supervisor` | 后台任务注册、取消、限时回收 | 业务级重试和结果语义 |
## 4. 消息与控制数据流
`MessageBus` 包含三条容量相同的 Tokio MPSC 队列:
- `inbound`Channel → Gateway message processor。
- `outbound`Session/Tool → `OutboundDispatcher`
- `control`WebSocket/Channel → Gateway message processor用于 dialog 操作。
### 普通消息
```mermaid
sequenceDiagram
participant C as Channel
participant B as MessageBus
participant G as Message processor
participant S as SessionManager
participant W as Per-session worker
participant A as AgentLoop
participant D as OutboundDispatcher
C->>B: publish InboundMessage
B->>G: consume inbound
G->>S: handle_message
S->>W: try_send AgentTask
S-->>G: AgentProcessing
W->>A: process(history)
A-->>W: final response
W->>B: publish OutboundMessage
B->>D: consume outbound
D->>C: Channel::send
```
关键语义:
- Gateway 的主消息处理循环不等待模型完成;普通消息进入对应 session worker 后立即返回 `AgentProcessing`
- 每个 session 有一条容量为 32 的队列,同一 session 串行处理,不同 session 的 worker 可并发执行。
- 队列满时明确拒绝新消息,不允许无界积压。
- Slash command 不进入 Agent 队列,由 `SessionManager` 直接执行,因此 `/stop` 等控制操作不会排在长模型调用之后。
### 出站投递
`OutboundDispatcher``(channel, chat_id)` 建立独立 lane
- 同一目标的消息保持顺序。
- 慢目标不会阻塞其他目标。
- 每条 lane 容量为 64空闲 300 秒后退出。
- 单次发送超时 30 秒;最多尝试 3 次,前两次失败后分别等待 1/2 秒。只有 `ConnectionError``SendError` 会重试。
- `deliver_outbound` 可等待渠道真实投递结果,等待上限 120 秒;普通 `publish_outbound` 只保证成功入队。
不要把“已进入 Bus”误认为“外部渠道已收到”。需要确认语义时必须使用 `deliver_outbound`
### Control 消息
WebSocket dialog 操作通过 `ControlMessage` 携带一次性回复通道。Gateway 在统一 message processor 中调用 `SessionManager`,再将 `SessionEvent` 回传给发起者。Bus 只承载消息,不解释操作。
## 5. 会话模型与并发不变量
Session ID 格式为:
```text
<channel>:<chat_id>:<dialog_id>
```
`SessionManagerInner` 保存:
- `sessions`:完整 Session ID 到内存 Session 的映射。
- `current_sessions``channel:chat_id` 到当前 dialog 的映射。
必须维护以下不变量:
1. 同一 Session 的 Agent 工作由一个 generation 对应的 worker 串行执行。
2. `/stop` 或 worker 替换会递增 `worker_generation`;旧 worker 不得再提交结果。
3. 慢操作模型、记忆召回、压缩、SQLite I/O不能长期持有 Session mutex。
4. 慢操作开始前记录 `state_version`,提交前重新验证,防止旧快照覆盖 `/clear``/delete` 等并发修改。
5. 持久化写入由 `persistence_lock` 串行化;多条相关记录应使用 Storage 的原子接口。
6. 内存先变更但持久化失败时,必须回滚精确匹配的消息后缀,不能删除无关的新状态。
SessionManager 负责组装会话上下文系统提示、Skills、召回的 Knowledge、压缩后的 Timeline 和当前消息历史。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态。
## 6. 持久化
`Storage` 使用 SQLx + SQLite默认数据库为 `{workspace_dir}/picobot.db`。连接启用:
- WAL journal mode。
- foreign keys。
- 5 秒 busy timeout。
- schema version 迁移。
持久化范围包括 sessions、messages、memories、scheduled jobs、job runs 和 background tasks。修改 schema 时应:
1. 更新集中式 schema/迁移逻辑。
2. 保留已有数据库的升级路径。
3. 为新库初始化和旧库迁移分别增加测试。
4. 对“状态更新 + 执行记录”等复合写入使用事务。
### 安全边界
- API Key 和渠道凭据只来自配置占位符、`.env` 或进程环境,不得写入仓库。
- 日志不得输出 token、secret、Authorization header或包含临时凭据的完整 URL应记录脱敏后的 host/path 和必要诊断字段。
- Gateway 把 cwd 切到 workspace因此相对文件路径和 Shell 默认从 workspace 开始这不是硬沙箱。当前内置文件工具接受绝对路径Bash 也可访问进程权限允许的位置。若某场景需要硬边界,必须显式配置/实现 allowed directory 和进程隔离。
- `http_request``web_fetch` 的私网/回环地址校验属于 SSRF 防线,重构网络层时不能绕过。
- 外部内容、Tool 输出和 MCP 响应均是不可信输入;解析错误应返回结构化失败,不能 panic。
## 7. 后台任务与生命周期
`TaskSupervisor` 是 Gateway 内部后台任务的统一所有者。message processor、outbound dispatcher、scheduler、session workers、outbound lanes、通知消费者和子 Agent 后台任务都应通过它注册。
两种注册方式:
- `spawn`:收到全局取消后直接丢弃任务 future适合无需异步清理的任务。
- `spawn_graceful`:任务自己观察 cancellation token 并清理Supervisor 在总宽限期结束后再强制 abort。
新增长生命周期任务时必须满足:
- 有明确 owner禁止无法回收的裸 `tokio::spawn`
- 能响应取消;外部连接、重试 sleep 和阻塞式等待也要纳入取消分支。
- 等待任务退出必须有硬超时,超时后 abort 并回收 JoinHandle。
- 任务 panic、超时和永久错误要可观测且不能阻止其他组件清理。
WebSocket 每个客户端的 writer task 是连接局部任务,由连接 handler 自己限时回收;它不跨越连接生命周期。
## 8. 启动与关停顺序
### 启动
1. 加载配置和 `.env`,解析 workspace。
2. 创建并切换到 workspace。
3. 初始化 SQLite、MemoryManager、MessageBus 和 SessionManager。
4. 注册内置工具、渠道、MCP 工具和 Cron 工具。
5. 启动所有 Channel。
6. 通过 TaskSupervisor 启动 message processor、dispatcher 和 scheduler。
7. 绑定 Axum listener开始接收请求。
### 关停
1. `Ctrl-C` 触发 Axum graceful shutdown并取消所有 WebSocket 连接。
2. `ChannelManager::stop_all` 先停止外部消息入口并注销渠道。
3. 取消 TaskSupervisor停止接受新后台任务。
4. 在共享的 10 秒总宽限期内等待任务退出,之后 abort 剩余任务。
渠道自己的 `stop()` 也必须有界。以飞书为例端点请求、WebSocket 建连、重试等待和已连接循环共享 CancellationToken另有 5 秒强制回收兜底。
## 9. 扩展指南
### 新增 Channel
1. 实现 `Channel` trait仅处理外部协议和统一消息转换。
2. 在 `ChannelManager::init` 注册,并通过同一个 MessageBus 收发。
3. 将可重试错误表示为 `ConnectionError`/`SendError`,永久错误使用其他类型。
4. 为 start/stop 幂等性、取消建连、投递失败和媒体边界增加测试。
5. 不要从 Channel 直接调用 SessionManager 或 Provider。
### 新增 Tool
1. 实现 `Tool`,在集中注册点加入 `ToolRegistry`
2. 参数 schema 必须明确;返回值保持可供模型消费的字符串协议。
3. 明确工具需要“workspace 默认目录”还是“不可逃逸的硬边界”;后者必须显式校验 canonical path不能只依赖 cwd。
4. 网络工具必须保留 SSRF/私网地址校验。
5. 长操作应有超时;后台执行应交给 SubAgentManager/TaskSupervisor。
### 新增 Provider
1. 实现 `LLMProvider`,保持其为纯 HTTP/API 适配器。
2. 统一映射文本、媒体、tool calls、usage 和错误。
3. 不在 Provider 中访问 Session、Bus 或 Channel。
4. 为请求序列化和响应兼容性编写离线单元测试。
### 修改 Session
先画出锁、慢 I/O、版本检查和持久化顺序。任何跨 `await` 的 Session 锁都需要特别审查;任何由旧快照产生的结果都必须在提交前验证 generation/state version。
## 10. 验证策略
按改动范围选择最小但充分的验证:
| 改动 | 至少执行 |
|------|----------|
| 文档 | 检查链接、命令和源码路径;`git diff --check` |
| Rust 实现 | 相关定向测试、`cargo test --lib``cargo clippy --all-targets --all-features -- -D warnings` |
| 构建/依赖 | 上述检查加 `cargo build` |
| Provider/真实渠道 | 离线测试;有凭据时再运行 ignored integration tests |
| SQLite schema | 新库测试、迁移测试、原子性测试 |
| 生命周期/并发 | 成功、失败、超时、取消、队列满和重复 start/stop 测试 |
模型 API 集成测试需要 `tests/test.env` 中的真实 API Key并默认 ignored`test_scheduler``test_request_format` 是可直接运行的离线测试。不应在无凭据时假装已经验证真实 Provider。
## 11. 演进决策清单
提交架构性修改前,逐项确认:
- 是否仍遵守 Channel → Bus → Session → Agent 的依赖方向?
- 是否引入第二个 owner、重复状态或绕过统一注册点
- 队列、并发、重试和等待是否全部有界?
- 取消信号能否覆盖建连、sleep、I/O 和清理阶段?
- 是否在持锁时执行了网络、模型或数据库慢操作?
- 内存与 SQLite 失败时能否保持一致?
- 错误是否区分瞬态和永久语义?
- 是否有测试覆盖正常路径与最危险的失败路径?
- README、AGENTS.md 和本文档是否需要同步更新?
## 12. 代码导航
| 主题 | 入口 |
|------|------|
| Gateway 装配和关停 | `src/gateway/mod.rs` |
| WebSocket 生命周期 | `src/gateway/ws.rs` |
| Bus 和消息类型 | `src/bus/mod.rs`, `src/bus/message.rs` |
| 出站并发与重试 | `src/bus/dispatcher.rs` |
| Channel 接口与注册 | `src/channels/base.rs`, `src/channels/manager.rs` |
| Session 核心 | `src/session/session.rs` |
| Session 命令/事件 | `src/session/commands.rs`, `src/session/events.rs` |
| Agent loop | `src/agent/agent_loop.rs` |
| 后台任务监督 | `src/task_supervisor.rs` |
| SQLite 初始化和迁移 | `src/storage/mod.rs` |
| Scheduler | `src/scheduler/mod.rs` |
| 配置加载 | `src/config/mod.rs` |

View File

@ -14,10 +14,11 @@ PicoBot 是一个基于 Rust 的个人 AI 助手运行时,包含本地 Gateway
| 文件 | 内容 | | 文件 | 内容 |
|------|------| |------|------|
| `references/config.md` | 配置字段详解providers、models、agents、gateway、client、channels、memory、mcp、browser | | `references/config.md` | 配置字段详解providers、models、agents、gateway、client、channels、memory、mcp、browser |
| `references/db-schema.md` | 数据库表结构sessions、messages、memories、scheduled_jobs、llm_calls、background_tasks | | `references/db-schema.md` | 数据库表结构与运行约束sessions、messages、memories、scheduled_jobs、job_runs、llm_calls、background_tasks |
| `references/architecture.md` | 核心架构:数据流、会话系统、上下文压缩、记忆系统、Skill 优先级、MCP、子 Agent | | `references/architecture.md` | 核心架构:消息并发、会话系统、持久化、生命周期、上下文压缩、记忆、MCP、子 Agent |
| `references/faq.md` | 常见问题模型切换、渠道添加、Skill 安装、历史查询、定时任务、MCP 等 | | `references/faq.md` | 常见问题模型切换、渠道添加、Skill 安装、历史查询、定时任务、MCP 等 |
| `references/commands.md` | 常用命令:编译、启动网关、启动客户端、运行测试 | | `references/commands.md` | 常用命令:编译、启动网关、启动客户端、运行测试 |
| `references/tools.md` | 内置工具名称、参数和重要使用约束 |
| `assets/config.example.json` | config.json 完整示例 | | `assets/config.example.json` | config.json 完整示例 |
Skill 根目录路径见上方 **Skill Root Directory** Skill 根目录路径见上方 **Skill Root Directory**

View File

@ -3,9 +3,12 @@
## 核心数据流 ## 核心数据流
``` ```
Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionManager → MessageBus → OutboundDispatcher → Channel Channel → MessageBus.inbound → Gateway processor → SessionManager → per-session worker → AgentLoop
↑ │
ControlChannel → SessionManager (dialog 操作: 创建/切换/归档/删除) └── Channel ← OutboundDispatcher ← per-conversation lane ← MessageBus.outbound
WebSocket/Channel → MessageBus.control → Gateway processor → SessionManager (dialog 操作)
Scheduler → SessionManager.handle_cron_message → AgentLoop → send_message
``` ```
## 模块职责 ## 模块职责
@ -15,8 +18,8 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
| `gateway` | HTTP/WebSocket 服务器,持有 GatewayState | | `gateway` | HTTP/WebSocket 服务器,持有 GatewayState |
| `client` | TUI 聊天客户端 | | `client` | TUI 聊天客户端 |
| `channels` | 外部集成飞书、CLI仅收发消息 | | `channels` | 外部集成飞书、CLI仅收发消息 |
| `bus` | 异步消息队列,纯队列不路由 | | `bus` | 有界 inbound/outbound/control 队列;出站 dispatcher 与分目标 lane |
| `session` | 会话生命周期管理、dialog 操作 | | `session` | 会话生命周期、dialog 操作、每 session 串行队列、上下文与持久化协调 |
| `agent` | LLM 调用循环、工具执行、上下文压缩、媒体处理、子 Agent | | `agent` | LLM 调用循环、工具执行、上下文压缩、媒体处理、子 Agent |
| `providers` | LLM API 客户端OpenAI 兼容、Anthropic | | `providers` | LLM API 客户端OpenAI 兼容、Anthropic |
| `tools` | Agent 工具bash、文件操作、搜索、HTTP、web、browser、memory、delegate 等) | | `tools` | Agent 工具bash、文件操作、搜索、HTTP、web、browser、memory、delegate 等) |
@ -28,13 +31,14 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
| `config` | 配置加载、环境变量替换、路径解析 | | `config` | 配置加载、环境变量替换、路径解析 |
| `memory` | 长期记忆存储与检索 | | `memory` | 长期记忆存储与检索 |
| `mcp` | MCPModel Context Protocol工具集成 | | `mcp` | MCPModel Context Protocol工具集成 |
| `task_supervisor` | Gateway 后台任务注册、取消、限时等待和强制回收 |
## 功能边界 ## 功能边界
- Channels 仅收发消息,不感知 session 或 LLM - Channels 仅收发消息,不感知 session 或 LLM
- MessageBus 是纯异步队列,不路由 - MessageBus 本体持有三条有界队列;出站路由、顺序和重试由 `OutboundDispatcher` 负责
- SessionManager 拥有 session 状态,不直接调 LLM负责注入 skills prompt - SessionManager 拥有 session 状态、dialog 路由、上下文构建和每 session worker并通过 worker 创建 AgentLoop
- AgentLoop 无状态,接收 dialog 事件调用 LLM、执行工具 - AgentLoop 跨轮无状态,接收已准备的 history 调用 LLM、执行工具并返回一次结果
- Providers 是纯 HTTP 客户端,无 bus/session/channel 感知 - Providers 是纯 HTTP 客户端,无 bus/session/channel 感知
- Tools 接收原始参数,返回字符串结果 - Tools 接收原始参数,返回字符串结果
- MCP 工具在 Gateway 初始化时连接服务器、发现工具,并包装成普通 Tool 注册到 ToolRegistry - MCP 工具在 Gateway 初始化时连接服务器、发现工具,并包装成普通 Tool 注册到 ToolRegistry
@ -48,6 +52,11 @@ Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionM
- OutboundDispatcher 通过 ChannelManager 路由出站消息 - OutboundDispatcher 通过 ChannelManager 路由出站消息
- Config `.env` 加载使用 `unsafe { env::set_var(...) }` - Config `.env` 加载使用 `unsafe { env::set_var(...) }`
- `browser` 工具只有在 `browser.enabled=true` 时注册,依赖 Chrome/Chromium 与 WebDriver - `browser` 工具只有在 `browser.enabled=true` 时注册,依赖 Chrome/Chromium 与 WebDriver
- 同一 session 的普通消息串行处理,不同 session 可并发session 队列容量为 32满时明确拒绝
- 出站消息按 `(channel, chat_id)` 分 lane 保序lane 容量为 64慢目标不阻塞其他目标
- 长生命周期后台任务由 TaskSupervisor 管理;连接局部任务由其 owner 限时 join 或 abort
- 外部建连、重试等待和关停 join 必须可取消且有硬超时
- 不得记录 API Key、Authorization header 或包含临时凭据的完整连接 URL
## 上下文压缩 ## 上下文压缩
@ -101,7 +110,7 @@ create → 存入 Storage → 载入 memory → 设为当前 dialog
| `list_dialogs` | 列出 `channel:chat_id` 下最近 10 个 session | | `list_dialogs` | 列出 `channel:chat_id` 下最近 10 个 session |
| `rename` | 更新标题,内存 + Storage 同步 | | `rename` | 更新标题,内存 + Storage 同步 |
| `delete` | 软删除(设 deleted_at从内存移除 | | `delete` | 软删除(设 deleted_at从内存移除 |
| `archive` | 当前为空操作 | | `archive` | 设置 archived_at从内存和当前 dialog 追踪中移除;可通过 include_archived 查询 |
### SessionManager 数据结构 ### SessionManager 数据结构
@ -112,13 +121,17 @@ create → 存入 Storage → 载入 memory → 设为当前 dialog
消息到达时 `resolve_dialog_id()` 按顺序确定接收 session当前 session → Storage 最近活跃 session → 新建。 消息到达时 `resolve_dialog_id()` 按顺序确定接收 session当前 session → Storage 最近活跃 session → 新建。
### 消息处理三阶段 ### 消息处理与并发
**阶段 1持锁**:斜杠命令检测 → 用户消息入库 → 提取记忆上下文 → 构建系统提示skills + memory_context→ 上下文压缩 → 创建 AgentLoop 普通消息先 `try_send` 到该 session 的有界 worker 队列Gateway 主 processor 随即返回 `AgentProcessing`。Slash command 直接执行,不进入此队列,因此 `/stop` 不会排在长模型调用后。
**阶段 2无锁**`agent.process(history)` → LLM 调用 + 工具执行。上下文溢出时自动重新压缩重试 Worker 的处理原则:
**阶段 3持锁**:持久化 agent 响应消息 → 自动生成标题(消息数 ≥ 5 且标题为"新对话"时) 1. 短暂持 Session 锁抓取快照并记录 `worker_generation`/`state_version`
2. 释放锁后执行消息持久化、记忆召回、上下文压缩、LLM 和工具等慢操作。
3. 提交由旧快照产生的结果前重新验证 generation/version防止 `/stop``/clear``/delete` 后写回陈旧状态。
4. Session 持久化由独立 `persistence_lock` 串行化;批量消息使用原子写入,失败时精确回滚内存后缀。
5. 上下文溢出时按 Provider 返回的真实限制重新压缩并重试。
### 会话恢复 ### 会话恢复
@ -136,7 +149,7 @@ create → 存入 Storage → 载入 memory → 设为当前 dialog
| 类别 | 用途 | 生命周期 | 检索方式 | | 类别 | 用途 | 生命周期 | 检索方式 |
|------|------|----------|----------| |------|------|----------|----------|
| **Knowledge** | 事实、偏好、模式、洞察 | 长期保留,手动删除 | 每轮注入系统提示,关键词匹配 | | **Knowledge** | 事实、偏好、模式、洞察 | 长期保留,手动删除 | 每轮注入系统提示,关键词匹配 |
| **Timeline** | 历史会话摘要 | 自动清理(默认 90 天) | `timeline_recall` 工具按需检索 | | **Timeline** | 历史会话摘要 | 配置预期保留 90 天;当前尚无自动清理循环 | `timeline_recall` 工具按需检索 |
### MemoryEntry 字段 ### MemoryEntry 字段
@ -163,7 +176,7 @@ create → 存入 Storage → 载入 memory → 设为当前 dialog
→ MemoryManager::recall(content, 5, Knowledge) → MemoryManager::recall(content, 5, Knowledge)
返回最多 5 条匹配的知识记忆(按 importance DESC 返回最多 5 条匹配的知识记忆(按 importance DESC
→ 格式化为 "- key: content" → 格式化为 "- key: content"
注入系统提示的 "记忆上下文" 部分 作为运行时上下文附加到本轮 user message
→ LLM 可见,辅助回答 → LLM 可见,辅助回答
``` ```
@ -191,10 +204,12 @@ LLM 对话上下文接近 token 限制 (默认 128K × 70%) 时自动触发压
| 时机 | 操作 | | 时机 | 操作 |
|------|------| |------|------|
| 每次消息处理 | `memory_manager.recall()` 提取 Knowledge 上下文 | | 每次消息处理 | `memory_manager.recall()` 提取 Knowledge 上下文 |
| 系统提示构建 | `MemorySection` 渲染记忆指南 + 匹配的记忆 | | 系统提示构建 | `MemorySection` 渲染记忆工具指南;匹配的 Knowledge 附加到本轮 user message |
| 有压缩历史时 | `HistorySection` 提示 LLM 使用 `timeline_recall` | | 有压缩历史时 | `HistorySection` 提示 LLM 使用 `timeline_recall` |
| 压缩完成后 | 摘要自动存储为 Timeline 记忆 | | 压缩完成后 | 摘要自动存储为 Timeline 记忆 |
| 空闲时 | 可配置自动 consolidation`idle_consolidation_minutes` | | 会话恢复 | 加载最近 Timeline 和压缩边界后的原始消息 |
`memory.recall_limit``idle_consolidation_minutes``timeline_retention_days``max_failures_before_degrade` 当前会被配置解析;其中每轮 Knowledge 召回在 worker 中仍固定为 5其余自动维护策略尚未接入运行循环。不要把“配置可解析”误认为“行为已生效”。
--- ---
@ -223,6 +238,23 @@ Gateway 初始化时读取 `config.mcp.servers`
默认工具集是只读工具:`file_read``file_search``content_search``web_fetch``http_request``calculator`。调用时可通过 `allowed_tools` 显式放开其他工具。后台任务会写入 `background_tasks` 表,默认 24 小时后清理。 默认工具集是只读工具:`file_read``file_search``content_search``web_fetch``http_request``calculator`。调用时可通过 `allowed_tools` 显式放开其他工具。后台任务会写入 `background_tasks` 表,默认 24 小时后清理。
后台子 Agent 通过 `TaskSupervisor::spawn_graceful` 注册,受 `gateway.max_concurrent_background_tasks` 限制Gateway 关停时先收到取消信号,再在总宽限期内清理。
---
## 出站投递与关停
`OutboundDispatcher` 对每个 `(channel, chat_id)` 创建独立 lane同一目标保持顺序单次发送超时 30 秒,最多尝试 3 次。只有 `ConnectionError``SendError` 被视为瞬态错误;永久错误不重试。`deliver_outbound` 等待真实渠道投递结果,`publish_outbound` 只表示成功入队。
Gateway 关停顺序:
1. Ctrl-C 停止 Axum 接入并取消 WebSocket 连接。
2. `ChannelManager::stop_all` 停止外部渠道并注销它们。
3. 取消 TaskSupervisor并在共享的 10 秒宽限期内等待后台任务。
4. 超时后 abort 剩余任务并回收 JoinHandle。
渠道 `stop()` 也必须有界。飞书端点请求、WebSocket 建连、重试 sleep 和连接循环共享 CancellationToken另有 5 秒强制终止兜底。
--- ---
## 当前斜杠命令 ## 当前斜杠命令

View File

@ -13,6 +13,16 @@ cargo run -- chat
# 运行单元测试 # 运行单元测试
cargo test --lib cargo test --lib
# 运行集成测试 (需配置 tests/test.env) # 运行离线集成/协议测试
cargo test --test test_scheduler
cargo test --test test_request_format
# 运行代码检查Rust 修改必跑)
cargo clippy --all-targets --all-features -- -D warnings
# 运行模型 API 集成测试(需配置 tests/test.env
cargo test --test test_integration -- --ignored cargo test --test test_integration -- --ignored
cargo test --test test_tool_calling -- --ignored
``` ```
`test_scheduler``test_request_format` 不需要 API Key也没有标记 `#[ignore]`。只有会真实调用 Provider 的测试需要从 `tests/test.env.example` 创建 `tests/test.env` 后使用 `-- --ignored`

View File

@ -55,9 +55,9 @@
|------|------|------|------| |------|------|------|------|
| `host` | string | 127.0.0.1 | 监听地址 | | `host` | string | 127.0.0.1 | 监听地址 |
| `port` | int | 19876 | 监听端口 | | `port` | int | 19876 | 监听端口 |
| `session_ttl_hours` | int | - | 会话过期小时数 | | `session_ttl_hours` | int | - | 兼容/预留字段;当前没有会话 TTL 清理循环 |
| `session_db_path` | string | - | SQLite 数据库路径,默认在 workspace 下 | | `session_db_path` | string | - | SQLite 数据库路径,默认在 workspace 下 |
| `cleanup_interval_minutes` | int | - | 清理间隔 | | `cleanup_interval_minutes` | int | - | 兼容/预留字段;当前没有按此间隔运行的 session 清理任务 |
| `max_concurrent_background_tasks` | int | 10 | delegate 后台子任务最大并发数 | | `max_concurrent_background_tasks` | int | 10 | delegate 后台子任务最大并发数 |
| `scheduler` | object | - | 调度器配置 | | `scheduler` | object | - | 调度器配置 |
@ -67,18 +67,21 @@
|------|------|------|------| |------|------|------|------|
| `enabled` | bool | true | 是否启动调度器并注册 cron 工具 | | `enabled` | bool | true | 是否启动调度器并注册 cron 工具 |
| `poll_interval_secs` | int | 60 | 检查到期任务的轮询间隔 | | `poll_interval_secs` | int | 60 | 检查到期任务的轮询间隔 |
| `max_concurrent` | int | 1 | 最大并发任务数,当前实现预留 | | `max_concurrent` | int | 1 | 每批到期任务的最大并发数,运行时限制在 1256 |
| `execution_timeout_secs` | int | 900 | 单个定时任务 Agent 执行的硬超时;租约会额外增加 30 秒 |
## memory 字段 ## memory 字段
| 字段 | 类型 | 默认 | 说明 | | 字段 | 类型 | 默认 | 说明 |
|------|------|------|------| |------|------|------|------|
| `consolidation_provider` | string | - | 记忆归并 LLM 提供商 | | `consolidation_provider` | string | 主 Agent provider | 当前记录在 MemoryManager 中供后续归并使用;压缩摘要仍使用 Session provider |
| `consolidation_model` | string | - | 记忆归并 LLM 模型 | | `consolidation_model` | string | 主 Agent model | 当前记录在 MemoryManager 中供后续归并使用;压缩摘要仍使用 Session model |
| `recall_limit` | int | 5 | 每轮注入的知识记忆条数 | | `recall_limit` | int | 5 | 预期的每轮知识召回上限;当前 worker 固定使用 5 |
| `idle_consolidation_minutes` | int | 10 | 空闲后触发归并的分钟数 | | `idle_consolidation_minutes` | int | 10 | 预留的空闲归并阈值;当前无对应循环 |
| `timeline_retention_days` | int | 90 | 时间线记忆保留天数 | | `timeline_retention_days` | int | 90 | 预留的 Timeline 保留期;当前无自动清理循环 |
| `max_failures_before_degrade` | int | 3 | 归并失败次数阈值 | | `max_failures_before_degrade` | int | 3 | 预留的归并失败阈值;当前无失败降级循环 |
注意:这些字段都会被解析,但当前 worker 的 Knowledge 召回数量仍固定为 5idle consolidation、Timeline 自动清理和失败降级循环尚未接入。配置存在不等于对应后台行为已经生效。
## channels.feishu 字段 ## channels.feishu 字段
@ -89,7 +92,7 @@
| `app_secret` | string | - | 飞书应用密钥 | | `app_secret` | string | - | 飞书应用密钥 |
| `allow_from` | []string | ["*"] | 允许交互的用户列表 | | `allow_from` | []string | ["*"] | 允许交互的用户列表 |
| `agent` | string | - | 使用的 agent 名称 | | `agent` | string | - | 使用的 agent 名称 |
| `media_dir` | string | ~/.picobot/media/feishu | 媒体存储目录 | | `media_dir` | string | ~/.picobot/media/feishu | 配置默认值Gateway 注册渠道时会覆盖为 `{workspace}/media/feishu` |
| `reaction_emoji` | string | "Typing" | 回复意向表达的表情 | | `reaction_emoji` | string | "Typing" | 回复意向表达的表情 |
## mcp 字段 ## mcp 字段

View File

@ -2,6 +2,8 @@
数据库为 SQLite默认位于 workspace 下的 `picobot.db` 数据库为 SQLite默认位于 workspace 下的 `picobot.db`
连接启用 WAL、`synchronous=NORMAL`、foreign keys、5 秒 busy timeout连接池最多 8 个连接。当前 `PRAGMA user_version=1`;启动时会在事务内补齐旧库字段和索引,遇到比程序更新的 schema version 会拒绝启动。
## sessions 表 ## sessions 表
会话表,一个 session 对应一个 (channel, chat_id, dialog_id) 组合。 会话表,一个 session 对应一个 (channel, chat_id, dialog_id) 组合。
@ -13,13 +15,16 @@
| `chat_id` | TEXT | 聊天/群组标识 | | `chat_id` | TEXT | 聊天/群组标识 |
| `dialog_id` | TEXT | 对话标识 | | `dialog_id` | TEXT | 对话标识 |
| `title` | TEXT | 会话标题(默认 "新对话" | | `title` | TEXT | 会话标题(默认 "新对话" |
| `created_at` | INTEGER | 创建时间(unix 秒) | | `created_at` | INTEGER | 创建时间(Unix 毫秒) |
| `last_active_at` | INTEGER | 最后活跃时间 | | `last_active_at` | INTEGER | 最后活跃时间Unix 毫秒) |
| `message_count` | INTEGER | 消息计数 | | `message_count` | INTEGER | 消息计数 |
| `routing_info` | TEXT | 路由信息 | | `routing_info` | TEXT | 路由信息 |
| `archived_at` | INTEGER | 归档时间Unix 毫秒NULL 表示未归档 |
| `deleted_at` | INTEGER | 软删除时间戳 | | `deleted_at` | INTEGER | 软删除时间戳 |
| `last_consolidated_at` | INTEGER | 上次记忆归并时间 | | `last_consolidated_at` | INTEGER | 上次记忆归并时间 |
| `last_compressed_message_at` | INTEGER | 上次上下文压缩消息序号 | | `last_compressed_message_at` | INTEGER | 上次上下文压缩边界时间戳 |
`(channel, chat_id, dialog_id)` 唯一。普通列表排除 `deleted_at`;是否包含归档记录由查询参数决定。
## messages 表 ## messages 表
@ -35,9 +40,11 @@
| `tool_name` | TEXT | 工具名称 | | `tool_name` | TEXT | 工具名称 |
| `tool_calls` | TEXT | 工具调用参数 JSON | | `tool_calls` | TEXT | 工具调用参数 JSON |
| `source` | TEXT | 消息来源(跨会话消息时标记来源 session_id | | `source` | TEXT | 消息来源(跨会话消息时标记来源 session_id |
| `created_at` | INTEGER | 创建时间(unix 秒) | | `created_at` | INTEGER | 创建时间(Unix 毫秒) |
| `reasoning_content` | TEXT | provider 返回的推理内容(如有) | | `reasoning_content` | TEXT | provider 返回的推理内容(如有) |
`(session_id, seq)` 有唯一索引,防止并发写入重复序号。删除 session 会通过外键级联删除 messages。
## background_tasks 表 ## background_tasks 表
delegate 后台子任务表。`session_id` 不使用数据库外键,因为 session 使用软删除,关联关系由应用层维护。 delegate 后台子任务表。`session_id` 不使用数据库外键,因为 session 使用软删除,关联关系由应用层维护。
@ -82,17 +89,24 @@ delegate 后台子任务表。`session_id` 不使用数据库外键,因为 ses
|------|------|------| |------|------|------|
| `id` | TEXT PK | 任务 UUID | | `id` | TEXT PK | 任务 UUID |
| `name` | TEXT | 任务名称 | | `name` | TEXT | 任务名称 |
| `schedule` | TEXT | 调度规则 JSONonce/every/cron | | `schedule` | TEXT | 调度规则 JSONat/every/cron |
| `prompt` | TEXT | 任务提示词 | | `prompt` | TEXT | 任务提示词 |
| `channel` | TEXT | 执行渠道 | | `channel` | TEXT | 执行渠道 |
| `chat_id` | TEXT | 目标对话 | | `chat_id` | TEXT | 目标对话 |
| `model` | TEXT | 使用的模型(可选) | | `model` | TEXT | 可选模型标记;当前会存储/展示,但 Scheduler 执行仍使用默认 Agent 模型 |
| `enabled` | INTEGER | 是否启用 (1/0) | | `enabled` | INTEGER | 是否启用 (1/0) |
| `delete_after_run` | INTEGER | 执行后自动删除 (1/0) | | `delete_after_run` | INTEGER | 执行后自动删除 (1/0) |
| `next_run_at` | INTEGER | 下次执行时间 | | `next_run_at` | INTEGER | 下次执行时间 |
| `last_run_at` | INTEGER | 上次执行时间 | | `last_run_at` | INTEGER | 上次执行时间 |
| `last_status` | TEXT | 上次执行状态 | | `last_status` | TEXT | 上次执行状态 |
| `last_error` | TEXT | 上次错误信息 | | `last_error` | TEXT | 上次错误信息 |
| `locked_at` | INTEGER | 本次领取时间 |
| `lock_owner` | TEXT | 领取任务的 Scheduler owner UUID |
| `lease_until` | INTEGER | 租约到期时间;进程崩溃后允许其他实例重新领取 |
| `created_at` | INTEGER | 创建时间Unix 毫秒) |
| `updated_at` | INTEGER | 更新时间Unix 毫秒) |
Scheduler 使用原子 `UPDATE ... RETURNING` 领取到期任务。任务结果、下次运行时间和租约释放在同一事务中提交,并校验 owner防止过期 worker 覆盖已恢复的任务。
## job_runs 表 ## job_runs 表
@ -121,3 +135,5 @@ delegate 后台子任务表。`session_id` 不使用数据库外键,因为 ses
| `response_body` | TEXT | 响应体 JSON | | `response_body` | TEXT | 响应体 JSON |
| `error` | TEXT | 错误信息 | | `error` | TEXT | 错误信息 |
| `duration_ms` | INTEGER | 耗时(毫秒) | | `duration_ms` | INTEGER | 耗时(毫秒) |
`request_body`/`response_body` 可能包含用户内容,排障和导出数据库时应按敏感数据处理。

View File

@ -20,6 +20,8 @@
`~/.picobot/skills/about-picobot/`SKILL.md 为索引references/ 下为各详细文档assets/ 下为 config 示例。如被删除,重启程序自动重新安装。 `~/.picobot/skills/about-picobot/`SKILL.md 为索引references/ 下为各详细文档assets/ 下为 config 示例。如被删除,重启程序自动重新安装。
内置 Skill 只在目标目录不存在时释放,不会覆盖已安装目录。升级 PicoBot 后如需获取新版内置文档,应先备份自己的修改,再删除旧的 `~/.picobot/skills/about-picobot/` 并重启。也可把定制版放在 `{workspace}/skills/about-picobot/`,它的优先级更高。
## Q: 数据库文件在哪里? ## Q: 数据库文件在哪里?
默认 `{workspace}/picobot.db`workspace 默认 `~/.picobot/workspace/` 默认 `{workspace}/picobot.db`workspace 默认 `~/.picobot/workspace/`
@ -30,7 +32,7 @@
## Q: 如何创建定时任务? ## Q: 如何创建定时任务?
使用 `cron` 工具,支持一次性 (`once`)、周期性 (`every`) 和 cron 表达式调度 使用 `cron_add` 创建,`cron_list` 查看,`cron_update``cron_enable``cron_disable``cron_remove` 管理。Schedule 类型为 `at`Unix 毫秒时间戳)、`every`(毫秒间隔)或 6 段 `cron` 表达式,可指定 IANA 时区
## Q: 上下文压缩是什么意思? ## Q: 上下文压缩是什么意思?
@ -43,3 +45,13 @@
## Q: 如何查看 LLM 调用日志? ## Q: 如何查看 LLM 调用日志?
LLM 调用记录存储在 `llm_calls` 表中。可通过 SQLite 客户端直接查询,或在代码中通过 storage 模块访问。 LLM 调用记录存储在 `llm_calls` 表中。可通过 SQLite 客户端直接查询,或在代码中通过 storage 模块访问。
该表可能包含完整用户消息、工具参数和模型响应,按敏感数据处理;不要直接上传或粘贴到公开 issue。
## Q: 为什么修改了某些 memory 配置却没有看到行为变化?
当前 `recall_limit``idle_consolidation_minutes``timeline_retention_days``max_failures_before_degrade` 都能被配置解析,但每轮 Knowledge 召回仍固定为 5自动 idle consolidation、Timeline 清理和失败降级循环尚未接入。以当前代码行为为准。
## Q: Gateway 为什么无法立即退出?
正常关停会先停止渠道,再取消受 TaskSupervisor 管理的后台任务,并最多等待 10 秒。飞书自身另有 5 秒连接任务兜底。如果持续超过这些上限,应检查是否新增了未受监督的 `tokio::spawn`、不可取消的外部 I/O或没有 timeout 的 JoinHandle 等待。

View File

@ -12,16 +12,8 @@
| `content` | 是 | 消息文本内容 | | `content` | 是 | 消息文本内容 |
| `files` | 否 | 文件路径列表 | | `files` | 否 | 文件路径列表 |
| `origin` | 否 | 消息来源标识,不填则自动使用当前 session_id | | `origin` | 否 | 消息来源标识,不填则自动使用当前 session_id |
| `file_types` | 否 | 指定文件发送类型,`{"路径": "audio"|"file"}`。未指定则自动判断 |
### file_types 说明 `files` 支持绝对路径和 workspace 相对路径,媒体类型由文件扩展名/MIME 自动判断。目前 schema 不支持手工指定 `file_types`
控制文件以何种消息类型发送,主要用于飞书渠道:
- `"audio"`:作为语音消息发送(仅 opus 格式支持)
- `"file"`:作为文件附件发送
飞书渠道限制上传类型和消息类型必须一致。opus 文件以 `"audio"` 发送其他音频mp3、wav 等)只能以 `"file"` 发送。
### 示例 ### 示例
@ -29,11 +21,12 @@
{ {
"target_chat_id": "feishu:oc_abc123", "target_chat_id": "feishu:oc_abc123",
"content": "这是生成的音乐文件", "content": "这是生成的音乐文件",
"files": ["/workspace/music.mp3"], "files": ["/workspace/music.mp3"]
"file_types": {"/workspace/music.mp3": "file"}
} }
``` ```
发送流程会先把消息写入目标 Session再调用 `MessageBus::deliver_outbound` 等待真实渠道投递结果(上限 120 秒)。投递失败会作为工具失败返回,不应把“已入队”报告成“已送达”。
--- ---
## chat_manager — 会话管理 ## chat_manager — 会话管理
@ -53,20 +46,28 @@
--- ---
## cron — 定时任务管理 ## Cron 定时任务工具
管理 cron 定时任务 Cron 不是一个带 `action` 的统一工具,而是六个独立工具;仅在 `gateway.scheduler.enabled=true` 时注册
### 参数 | 工具 | 主要参数 | 说明 |
|------|----------|------|
| `cron_add` | `schedule`, `prompt`, `channel`, `chat_id`; 可选 `name`, `model` | 创建任务 |
| `cron_list` | 可选 `status=all|enabled|disabled` | 列出任务 |
| `cron_update` | `job_id`; 可选 `prompt`, `schedule`, `channel`, `chat_id`, `model` | 更新指定字段 |
| `cron_remove` | `job_id` | 永久删除任务和关联 job runs |
| `cron_enable` | `job_id` | 启用并重新计算下次运行时间 |
| `cron_disable` | `job_id` | 禁用但保留任务 |
| 参数 | 必填 | 说明 | `schedule` 支持:
|------|------|------|
| `action` | 是 | 操作: `add`, `list`, `update`, `remove`, `enable`, `disable` | ```json
| `name` | add必须 | 任务名称 | {"type":"at","at":1750000000000}
| `schedule` | add需要 | 调度规则: `once`(时间戳), `every`(间隔秒), `cron`(表达式) | {"type":"every","every_ms":3600000}
| `prompt` | add必须 | 任务提示词 | {"type":"cron","expr":"0 0 9 * * *","tz":"Asia/Shanghai"}
| `channel` | add必须 | 执行渠道 | ```
| `chat_id` | add必须 | 目标对话 |
时间戳和间隔单位为毫秒Cron 表达式为 6 段(秒、分、时、日、月、周)。定时 Agent 不复用聊天历史,`prompt` 必须包含完整上下文,并由 Agent 使用 `send_message` 投递结果。`model` 当前会持久化和展示,但执行仍使用默认 Agent Provider/Model不能依赖它实现模型覆盖。
--- ---
@ -168,15 +169,19 @@
## file_read / file_write / file_edit / file_search / content_search — 文件操作和搜索 ## file_read / file_write / file_edit / file_search / content_search — 文件操作和搜索
工作目录内的文件读写编辑、文件名搜索和内容搜索。详细的参数定义见各工具的 parameters_schema 文件读写编辑、文件名搜索和内容搜索。相对路径从 workspace cwd 解析;默认注册的文件工具也接受绝对路径,因此 workspace 不是硬沙箱。详细参数以各工具的 `parameters_schema` 为准
## bash — 执行命令 ## bash — 执行命令
在本地环境执行 bash 命令,有超时限制和安全检查(阻止 rm -rf /、fork bomb 等)。 默认从 workspace cwd 执行 bash 命令。参数为 `command` 和可选 `timeout`;默认 60 秒、最大 600 秒,输出最多约 50,000 字符。命令继承 Gateway 进程权限,可以访问 workspace 外路径;危险模式拦截不是完整沙箱。
## pty — 持久终端会话
用于交互式程序和需要保持状态的长运行命令。`action` 支持 `spawn``write``read``kill``list``write/read/kill` 需要 `session_id`。Gateway 进程退出时 PTY manager 会清理子进程。
## http_request / web_fetch — HTTP 和 Web 工具 ## http_request / web_fetch — HTTP 和 Web 工具
发送 HTTP 请求和获取网页内容,有 URL 安全校验(阻止内网/本地访问)。 `http_request` 支持 GET/POST/PUT/DELETE/PATCH、headers 和字符串 body`web_fetch` 提取 HTML/JSON 的可读文本。两者校验 URL 与 DNS 解析结果阻止回环、私网、link-local 和本地域名,并禁用自动重定向,以降低 SSRF 风险
## calculator — 计算器 ## calculator — 计算器