feat(skills): add built-in skill packaging mechanism and about-picobot documentation

- Add build.rs: scan resources/skills/, compress each with tar+zstd, embed via include_bytes!
- Add src/skills/builtin.rs: runtime auto-install built-in skills to ~/.picobot/skills/
- Add about-picobot built-in skill: SKILL.md index + references/ (config, db-schema, architecture, faq, commands) + assets/config.example.json
- Update skill loading: reverse priority (agents < picobot < workspace), deduplicate by name
- Update skills prompt: re-query get_skill when user asks about installed skills
- Change max_tool_iterations default from 20 to 99
This commit is contained in:
xiaoski 2026-05-15 12:00:18 +08:00
parent 7a30848f65
commit 2f11aed44a
14 changed files with 681 additions and 6 deletions

View File

@ -46,3 +46,9 @@ rmcp = { version = "1.7", default-features = false, features = [
] } ] }
http = "1" http = "1"
encoding_rs = "0.8" encoding_rs = "0.8"
zstd = "0.13"
tar = "0.4"
[build-dependencies]
zstd = "0.13"
tar = "0.4"

View File

@ -125,7 +125,7 @@ sequenceDiagram
- Jobs trigger agent processing via specified channel/chat - Jobs trigger agent processing via specified channel/chat
### Skills System ### Skills System
- Load Markdown skill files from `~/.picobot/skills` and `~/.agent/skills` - Load Markdown skill files from `~/.picobot/skills` and `~/.agents/skills`
- Skills inject specialized system prompts for specific tasks - Skills inject specialized system prompts for specific tasks
- Automatic hot-reload on file changes - Automatic hot-reload on file changes
@ -169,7 +169,7 @@ cargo build
"default": { "default": {
"provider": "openai", "provider": "openai",
"model": "gpt-4o", "model": "gpt-4o",
"max_tool_iterations": 20, "max_tool_iterations": 99,
"token_limit": 128000 "token_limit": 128000
} }
} }
@ -243,7 +243,7 @@ The `.env` file in the working directory is loaded manually (not via dotenv crat
|-----|------|---------|-------------| |-----|------|---------|-------------|
| `provider` | string | — | Provider name (key in `providers`) | | `provider` | string | — | Provider name (key in `providers`) |
| `model` | string | — | Model name (key in `models`) | | `model` | string | — | Model name (key in `models`) |
| `max_tool_iterations` | number | `20` | Max tool call iterations per turn | | `max_tool_iterations` | number | `99` | Max tool call iterations per turn |
| `token_limit` | number | `128000` | Context window token limit | | `token_limit` | number | `128000` | Context window token limit |
## Slash Commands ## Slash Commands

69
build.rs Normal file
View File

@ -0,0 +1,69 @@
use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let skills_dir = Path::new("resources/skills");
let skills_out_dir = Path::new(&out_dir).join("skills");
fs::create_dir_all(&skills_out_dir).unwrap();
let mut skills = Vec::new();
if let Ok(entries) = fs::read_dir(skills_dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let skill_name = path.file_name().unwrap().to_str().unwrap().to_string();
let compressed_path = skills_out_dir.join(format!("{}.tar.zst", skill_name));
let compressed = compress_skill_dir(&path);
fs::write(&compressed_path, &compressed).unwrap();
skills.push(skill_name);
}
}
let mut code = String::from(
r#"pub struct EmbeddedSkill {
pub name: &'static str,
pub data: &'static [u8],
}
pub static EMBEDDED_SKILLS: &[EmbeddedSkill] = &[
"#,
);
for name in &skills {
let file_path = skills_out_dir
.join(format!("{}.tar.zst", name))
.to_string_lossy()
.to_string();
code.push_str(&format!(
" EmbeddedSkill {{ name: \"{}\", data: include_bytes!(\"{}\") }},\n",
name, file_path
));
}
code.push_str("];\n");
let generated_path = Path::new(&out_dir).join("embedded_skills.rs");
let mut f = fs::File::create(&generated_path).unwrap();
f.write_all(code.as_bytes()).unwrap();
}
fn compress_skill_dir(dir: &Path) -> Vec<u8> {
let mut buf = Vec::new();
let mut builder = tar::Builder::new(&mut buf);
builder.follow_symlinks(false);
builder
.append_dir_all(dir.file_name().unwrap().to_str().unwrap(), dir)
.unwrap();
drop(builder);
zstd::encode_all(buf.as_slice(), 3).unwrap()
}

View File

@ -0,0 +1,23 @@
---
name: about-picobot
description: PicoBot 自身设计信息的索引入口。含配置、数据库、架构、常见问题等。具体内容在 references/ 目录下config 示例在 assets/ 目录下,请用 file_read 工具查阅对应文件。
always: true
---
# About PicoBot
PicoBot 是一个基于 Rust 的个人 AI 助手支持多渠道飞书、CLI、长记忆、定时任务、Skill 系统等。
## 目录索引
以下为 about-picobot 内置 skill 包含的参考文档,可使用 `file_read` 工具读取具体内容:
| 文件 | 内容 |
|------|------|
| `references/config.md` | 配置字段详解providers、models、agents、gateway、memory、channels、mcp |
| `references/db-schema.md` | 数据库表结构sessions、messages、memories、scheduled_jobs、llm_calls |
| `references/architecture.md` | 核心架构数据流、会话系统、上下文压缩、记忆系统、Skill 优先级机制 |
| `references/faq.md` | 常见问题模型切换、渠道添加、Skill 安装、历史查询、定时任务等 |
| `references/commands.md` | 常用命令:编译、启动网关、启动客户端、运行测试 |
| `assets/config.example.json` | config.json 完整示例 |
Skill 根目录路径见上方 **Skill Root Directory**

View File

@ -0,0 +1,74 @@
{
"providers": {
"aliyun": {
"type": "openai",
"base_url": "https://api.openai.com/v1",
"api_key": "<DASHSCOPE_API_KEY>",
"extra_headers": {}
},
"openai": {
"type": "openai",
"base_url": "https://api.openai.com/v1",
"api_key": "<OPENAI_API_KEY>",
"extra_headers": {}
},
"anthropic": {
"type": "anthropic",
"base_url": "https://api.anthropic.com/v1",
"api_key": "<ANTHROPIC_API_KEY>",
"extra_headers": {}
}
},
"models": {
"qwen-plus": {
"model_id": "qwen-plus",
"temperature": 0.0,
"max_tokens": 8192
},
"gpt-4o": {
"model_id": "gpt-4o",
"temperature": 0.7,
"max_tokens": 4096
},
"claude-sonnet-4-20250514": {
"model_id": "claude-sonnet-4-20250514",
"temperature": 0.7,
"max_tokens": 8192
}
},
"agents": {
"default": {
"provider": "aliyun",
"model": "qwen-plus",
"max_tool_iterations": 99,
"token_limit": 128000
}
},
"gateway": {
"host": "127.0.0.1",
"port": 19876
},
"client": {
"gateway_url": "ws://127.0.0.1:19876/ws"
},
"channels": {
"feishu": {
"enabled": true,
"app_id": "<FEISHU_APP_ID>",
"app_secret": "<FEISHU_APP_SECRET>",
"allow_from": ["*"],
"agent": "default",
"media_dir": "~/.picobot/media/feishu",
"reaction_emoji": "Typing"
}
},
"memory": {
"consolidation_provider": null,
"consolidation_model": null,
"recall_limit": 5,
"idle_consolidation_minutes": 10,
"timeline_retention_days": 90,
"max_failures_before_degrade": 3
},
"workspace_dir": "~/.picobot/workspace"
}

View File

@ -0,0 +1,194 @@
# 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 调用循环、工具执行、上下文压缩 |
| `providers` | LLM API 客户端OpenAI 兼容、Anthropic |
| `tools` | Agent 工具bash、文件操作、HTTP、web、get_skill 等) |
| `skills` | Skill 加载、管理和 prompt 构建 |
| `storage` | SQLite 持久化 |
| `scheduler` | Cron 作业调度 |
| `observability` | Observer 模式agent/工具遥测事件 |
| `protocol` | WebSocket 协议消息定义 |
| `config` | 配置加载、环境变量替换、路径解析 |
| `memory` | 长期记忆存储与检索 |
| `mcp` | MCPModel Context Protocol工具集成 |
## 功能边界
- Channels 仅收发消息,不感知 session 或 LLM
- MessageBus 是纯异步队列,不路由
- SessionManager 拥有 session 状态,不直接调 LLM负责注入 skills prompt
- AgentLoop 无状态,接收 dialog 事件调用 LLM、执行工具
- Providers 是纯 HTTP 客户端,无 bus/session/channel 感知
- Tools 接收原始参数,返回字符串结果
## 关键约束
- Gateway 启动时切换到 workspace 目录
- SQLite 数据在 `{workspace}/picobot.db`
- ChannelManager 持有 MessageBus 和所有 channel
- OutboundDispatcher 通过 ChannelManager 路由出站消息
- Config `.env` 加载使用 `unsafe { env::set_var(...) }`
## 上下文压缩
当上下文接近 token 限制时触发:
1. **快速裁剪**:合并连续同角色消息,截断工具输出
2. **硬截断**:移除过老消息
3. 压缩后保留用户消息确保结构完整
## Skill 系统
三个优先级(高覆盖低):
1. `{workspace}/skills/` — 最高优先级
2. `~/.picobot/skills/` — 中等优先级
3. `~/.agents/skills/` — 最低优先级
同名 skill 按优先级覆盖。每个 skill 是包含 `SKILL.md` 的目录。内置 skill 在 `~/.picobot/skills/` 下不存在时自动从二进制释放安装。
## 会话系统
### 会话 ID 格式
统一会话 ID 为三段式:**`<channel>:<chat_id>:<dialog_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<String, Arc<Mutex<Session>>>` — 所有已加载的 sessionkey 为完整 session ID
- **`current_sessions`**`HashMap<String, String>` — 每个 `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.01.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` |

View File

@ -0,0 +1,18 @@
# PicoBot 常用命令
```bash
# 编译
cargo build
# 启动网关 (默认 127.0.0.1:19876)
cargo run -- gateway
# 启动 CLI 客户端 (连接 ws://127.0.0.1:19876/ws)
cargo run -- chat
# 运行单元测试
cargo test --lib
# 运行集成测试 (需配置 tests/test.env)
cargo test --test test_integration -- --ignored
```

View File

@ -0,0 +1,100 @@
# PicoBot 配置说明
配置文件加载顺序:`~/.picobot/config.json` → 当前目录 `./config.json`
占位符 `<VAR_NAME>` 从环境变量替换,环境变量从 `.env` 文件或系统环境读取。
## config.json 结构
```jsonc
{
"providers": {}, // LLM 提供商配置
"models": {}, // 模型配置
"agents": {}, // agent 配置
"gateway": {}, // 网关配置
"client": {}, // 客户端配置
"channels": {}, // 渠道配置
"memory": {}, // 记忆系统配置
"workspace_dir": // 工作目录,默认 ~/.picobot/workspace
"mcp": {} // MCP 服务器配置
}
```
完整示例见 `assets/config.example.json`
## providers 字段
| 字段 | 说明 |
|------|------|
| `type` | 提供商类型: `openai`(兼容 OpenAI API`anthropic` |
| `base_url` | API 端点地址 |
| `api_key` | API 密钥,支持 `<ENV_VAR>` 占位符 |
| `extra_headers` | 额外 HTTP 头 |
## models 字段
| 字段 | 说明 |
|------|------|
| `model_id` | 模型标识名称 |
| `temperature` | 采样温度,可选 |
| `max_tokens` | 最大输出 token 数,可选 |
## agents 字段
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `provider` | string | - | 提供商名称(对应 providers key |
| `model` | string | - | 模型名称(对应 models key |
| `max_tool_iterations` | int | 99 | 最大工具调用轮数 |
| `token_limit` | int | 128000 | 上下文 token 限制 |
## gateway 字段
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `host` | string | 127.0.0.1 | 监听地址 |
| `port` | int | 19876 | 监听端口 |
| `session_ttl_hours` | int | - | 会话过期小时数 |
| `session_db_path` | string | - | SQLite 数据库路径,默认在 workspace 下 |
| `cleanup_interval_minutes` | int | - | 清理间隔 |
| `scheduler` | object | - | 调度器配置 |
## memory 字段
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `consolidation_provider` | string | - | 记忆归并 LLM 提供商 |
| `consolidation_model` | string | - | 记忆归并 LLM 模型 |
| `recall_limit` | int | 5 | 每轮注入的知识记忆条数 |
| `idle_consolidation_minutes` | int | 10 | 空闲后触发归并的分钟数 |
| `timeline_retention_days` | int | 90 | 时间线记忆保留天数 |
| `max_failures_before_degrade` | int | 3 | 归并失败次数阈值 |
## channels.feishu 字段
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `enabled` | bool | false | 是否启用 |
| `app_id` | string | - | 飞书应用 ID |
| `app_secret` | string | - | 飞书应用密钥 |
| `allow_from` | []string | ["*"] | 允许交互的用户列表 |
| `agent` | string | - | 使用的 agent 名称 |
| `media_dir` | string | ~/.picobot/media/feishu | 媒体存储目录 |
| `reaction_emoji` | string | "Typing" | 回复意向表达的表情 |
## mcp 字段
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `servers` | array | [] | MCP 服务器列表 |
| `tool_timeout_secs` | int | 180 | 工具调用超时秒数 |
MCP 服务器单条配置:
| 字段 | 说明 |
|------|------|
| `name` | 服务器名称 |
| `transport` | 传输方式: `Stdio``Sse``streamable-http` |
| `command` | 启动命令Stdio 模式) |
| `args` | 命令参数 |
| `url` | URLSse / streamable-http 模式) |
| `tool_timeout_secs` | 单独的超时设置 |

View File

@ -0,0 +1,101 @@
# PicoBot 数据库表结构
数据库为 SQLite默认位于 workspace 下的 `picobot.db`
## sessions 表
会话表,一个 session 对应一个 (channel, chat_id, dialog_id) 组合。
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | session ID格式 `<channel>:<chat_id>:<dialog_id>` |
| `channel` | TEXT | 渠道名称 |
| `chat_id` | TEXT | 聊天/群组标识 |
| `dialog_id` | TEXT | 对话标识 |
| `title` | TEXT | 会话标题(默认 "新对话" |
| `created_at` | INTEGER | 创建时间unix 秒) |
| `last_active_at` | INTEGER | 最后活跃时间 |
| `message_count` | INTEGER | 消息计数 |
| `routing_info` | TEXT | 路由信息 |
| `deleted_at` | INTEGER | 软删除时间戳 |
| `last_consolidated_at` | INTEGER | 上次记忆归并时间 |
| `last_compressed_message_at` | INTEGER | 上次上下文压缩消息序号 |
## messages 表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 消息 UUID |
| `session_id` | TEXT FK | 所属会话,外键关联 sessions(id) |
| `seq` | INTEGER | 消息序号 |
| `role` | TEXT | 角色: user / assistant / tool / system |
| `content` | TEXT | 消息内容 |
| `media_refs` | TEXT | 多媒体引用 JSON |
| `tool_call_id` | TEXT | 工具调用 ID |
| `tool_name` | TEXT | 工具名称 |
| `tool_calls` | TEXT | 工具调用参数 JSON |
| `source` | TEXT | 消息来源(跨会话消息时标记来源 session_id |
| `created_at` | INTEGER | 创建时间unix 秒) |
## memories 表
长期记忆存储。
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 记忆 UUID |
| `key` | TEXT UNIQUE | 记忆唯一键 |
| `content` | TEXT | 记忆内容 |
| `category` | TEXT | 类别: knowledge / timeline |
| `importance` | REAL | 重要性权重 (0-1) |
| `session_id` | TEXT | 关联会话 |
| `created_at` | TEXT | 创建时间 |
| `updated_at` | TEXT | 更新时间 |
配套 FTS5 全文索引虚拟表 `memory_fts(key, content)`,用于关键词搜索,通过触发器自动同步。
## scheduled_jobs 表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | 任务 UUID |
| `name` | TEXT | 任务名称 |
| `schedule` | TEXT | 调度规则 JSONonce/every/cron |
| `prompt` | TEXT | 任务提示词 |
| `channel` | TEXT | 执行渠道 |
| `chat_id` | TEXT | 目标对话 |
| `model` | TEXT | 使用的模型(可选) |
| `enabled` | INTEGER | 是否启用 (1/0) |
| `delete_after_run` | INTEGER | 执行后自动删除 (1/0) |
| `next_run_at` | INTEGER | 下次执行时间 |
| `last_run_at` | INTEGER | 上次执行时间 |
| `last_status` | TEXT | 上次执行状态 |
| `last_error` | TEXT | 上次错误信息 |
## job_runs 表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK | 自增 ID |
| `job_id` | TEXT FK | 关联任务,外键关联 scheduled_jobs(id) |
| `started_at` | INTEGER | 开始时间 |
| `finished_at` | INTEGER | 结束时间 |
| `status` | TEXT | 执行状态 |
| `output` | TEXT | 执行输出 |
| `error` | TEXT | 错误信息 |
| `duration_ms` | INTEGER | 耗时(毫秒) |
## llm_calls 表
记录所有 LLM API 调用的请求/响应详情,自动保留最近 1000 条。
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK | 自增 ID |
| `created_at` | INTEGER | 调用时间 |
| `provider` | TEXT | 提供商类型 |
| `model` | TEXT | 模型名称 |
| `request_body` | TEXT | 请求体 JSON |
| `response_body` | TEXT | 响应体 JSON |
| `error` | TEXT | 错误信息 |
| `duration_ms` | INTEGER | 耗时(毫秒) |

View File

@ -0,0 +1,45 @@
# PicoBot 常见问题
## Q: 如何切换模型?
修改 `config.json``agents` 配置,指定不同的 `provider``model`。如需新的 provider`providers` 中添加对应 API 配置。
## Q: 如何添加新的渠道?
`config.json``channels` 中添加新条目。目前支持飞书 (feishu)CLI 客户端无需配置。
## Q: 如何安装 skill
将包含 `SKILL.md` 的目录放入 `~/.picobot/skills/`,程序自动检测。也可通过 agent 在 workspace 的 `skills/` 下创建新 skill。
## Q: 为什么 get_skill 找不到某个 skill
确保 skill 目录中有 `SKILL.md` 文件,且文件头有正确的 frontmattername、description。用 get_skill action="list" 查看当前加载的所有 skill。注意同名 skill 按目录优先级覆盖。
## Q: 内置的 about-picobot 文档在哪里?
`~/.picobot/skills/about-picobot/`SKILL.md 为索引references/ 下为各详细文档assets/ 下为 config 示例。如被删除,重启程序自动重新安装。
## Q: 数据库文件在哪里?
默认 `{workspace}/picobot.db`workspace 默认 `~/.picobot/workspace/`
## Q: 如何查看历史会话?
使用 `chat_manager` 工具:`action="list_sessions"` 列出会话,`action="list_messages"` 查看指定会话消息,支持 offset 翻页、before_time/after_time 时间范围过滤。
## Q: 如何创建定时任务?
使用 `cron` 工具,支持一次性 (`once`)、周期性 (`every`) 和 cron 表达式调度。
## Q: 上下文压缩是什么意思?
对话历史过长超出模型 token 限制时,系统自动精简历史消息。压缩后旧消息可通过 `timeline_recall` 工具检索。
## Q: 如何修改 gateway 监听端口?
`config.json``gateway.port` 中设置,重启网关生效。默认 19876。
## Q: 如何查看 LLM 调用日志?
LLM 调用记录存储在 `llm_calls` 表中。可通过 SQLite 客户端直接查询,或在代码中通过 storage 模块访问。

View File

@ -40,7 +40,7 @@
"default": { "default": {
"provider": "aliyun", "provider": "aliyun",
"model": "qwen-plus", "model": "qwen-plus",
"max_tool_iterations": 20, "max_tool_iterations": 99,
"token_limit": 128000 "token_limit": 128000
} }
}, },

View File

@ -125,7 +125,7 @@ pub struct AgentConfig {
} }
fn default_max_tool_iterations() -> usize { fn default_max_tool_iterations() -> usize {
20 99
} }
fn default_token_limit() -> usize { fn default_token_limit() -> usize {

34
src/skills/builtin.rs Normal file
View File

@ -0,0 +1,34 @@
use std::path::Path;
use super::embedded::{EmbeddedSkill, EMBEDDED_SKILLS};
pub fn install_builtin_skills(target_dir: &Path) {
for skill in EMBEDDED_SKILLS {
let skill_dir = target_dir.join(skill.name);
if skill_dir.exists() {
continue;
}
match install_one(skill, &skill_dir) {
Ok(()) => {
tracing::info!(name = skill.name, dir = %skill_dir.display(), "Installed built-in skill");
}
Err(e) => {
tracing::warn!(name = skill.name, error = %e, "Failed to install built-in skill");
}
}
}
}
fn install_one(skill: &EmbeddedSkill, target_dir: &Path) -> Result<(), String> {
let decompressed = zstd::decode_all(skill.data)
.map_err(|e| format!("zstd decode: {}", e))?;
let mut archive = tar::Archive::new(decompressed.as_slice());
archive
.unpack(target_dir.parent().unwrap_or_else(|| Path::new(".")))
.map_err(|e| format!("tar unpack: {}", e))?;
Ok(())
}

View File

@ -1,3 +1,9 @@
pub mod builtin;
mod embedded {
include!(concat!(env!("OUT_DIR"), "/embedded_skills.rs"));
}
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::SystemTime; use std::time::SystemTime;
@ -53,8 +59,12 @@ impl SkillsLoader {
/// Create a new loader with default paths /// Create a new loader with default paths
pub fn new() -> Self { pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let picobot_skills_dir = home.join(".picobot/skills");
builtin::install_builtin_skills(&picobot_skills_dir);
Self { Self {
picobot_skills_dir: home.join(".picobot/skills"), picobot_skills_dir,
agent_skills_dir: home.join(".agents/skills"), agent_skills_dir: home.join(".agents/skills"),
workspace_skills_dir: None, workspace_skills_dir: None,
state: Arc::new(Mutex::new(SkillsState::default())), state: Arc::new(Mutex::new(SkillsState::default())),
@ -292,6 +302,7 @@ impl SkillsLoader {
prompt.push_str("### 使用方法\n\n"); prompt.push_str("### 使用方法\n\n");
prompt.push_str("- 使用 `get_skill` 工具 action=\"list\" 列出所有可用 skill 及其名称、简介、路径\n"); prompt.push_str("- 使用 `get_skill` 工具 action=\"list\" 列出所有可用 skill 及其名称、简介、路径\n");
prompt.push_str("- 使用 `get_skill` 工具 action=\"get\" 并提供 `skill_name` 获取指定 skill 完整内容\n"); prompt.push_str("- 使用 `get_skill` 工具 action=\"get\" 并提供 `skill_name` 获取指定 skill 完整内容\n");
prompt.push_str("- 当用户询问已安装的 skill 相关信息时,需重新调用 get_skill 工具查询最新内容,避免 skill 已变更导致信息过时\n");
// Always skills full content // Always skills full content
if !always_skills.is_empty() { if !always_skills.is_empty() {