From 2f11aed44a18e0e8c13f35b5787dec42bcedda5f Mon Sep 17 00:00:00 2001 From: xiaoski Date: Fri, 15 May 2026 12:00:18 +0800 Subject: [PATCH] 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 --- Cargo.toml | 6 + README.md | 6 +- build.rs | 69 +++++++ resources/skills/about-picobot/SKILL.md | 23 +++ .../about-picobot/assets/config.example.json | 74 +++++++ .../about-picobot/references/architecture.md | 194 ++++++++++++++++++ .../about-picobot/references/commands.md | 18 ++ .../skills/about-picobot/references/config.md | 100 +++++++++ .../about-picobot/references/db-schema.md | 101 +++++++++ .../skills/about-picobot/references/faq.md | 45 ++++ resources/templates/config.example.json | 2 +- src/config/mod.rs | 2 +- src/skills/builtin.rs | 34 +++ src/skills/mod.rs | 13 +- 14 files changed, 681 insertions(+), 6 deletions(-) create mode 100644 build.rs create mode 100644 resources/skills/about-picobot/SKILL.md create mode 100644 resources/skills/about-picobot/assets/config.example.json create mode 100644 resources/skills/about-picobot/references/architecture.md create mode 100644 resources/skills/about-picobot/references/commands.md create mode 100644 resources/skills/about-picobot/references/config.md create mode 100644 resources/skills/about-picobot/references/db-schema.md create mode 100644 resources/skills/about-picobot/references/faq.md create mode 100644 src/skills/builtin.rs diff --git a/Cargo.toml b/Cargo.toml index 274dcb0..9214972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,3 +46,9 @@ rmcp = { version = "1.7", default-features = false, features = [ ] } http = "1" encoding_rs = "0.8" +zstd = "0.13" +tar = "0.4" + +[build-dependencies] +zstd = "0.13" +tar = "0.4" diff --git a/README.md b/README.md index 72a313d..a63b415 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ sequenceDiagram - Jobs trigger agent processing via specified channel/chat ### 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 - Automatic hot-reload on file changes @@ -169,7 +169,7 @@ cargo build "default": { "provider": "openai", "model": "gpt-4o", - "max_tool_iterations": 20, + "max_tool_iterations": 99, "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`) | | `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 | ## Slash Commands diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b032f2d --- /dev/null +++ b/build.rs @@ -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 { + 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() +} diff --git a/resources/skills/about-picobot/SKILL.md b/resources/skills/about-picobot/SKILL.md new file mode 100644 index 0000000..8b1127d --- /dev/null +++ b/resources/skills/about-picobot/SKILL.md @@ -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**。 diff --git a/resources/skills/about-picobot/assets/config.example.json b/resources/skills/about-picobot/assets/config.example.json new file mode 100644 index 0000000..1167c4b --- /dev/null +++ b/resources/skills/about-picobot/assets/config.example.json @@ -0,0 +1,74 @@ +{ + "providers": { + "aliyun": { + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": "", + "extra_headers": {} + }, + "openai": { + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": "", + "extra_headers": {} + }, + "anthropic": { + "type": "anthropic", + "base_url": "https://api.anthropic.com/v1", + "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": "", + "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" +} diff --git a/resources/skills/about-picobot/references/architecture.md b/resources/skills/about-picobot/references/architecture.md new file mode 100644 index 0000000..a38f346 --- /dev/null +++ b/resources/skills/about-picobot/references/architecture.md @@ -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` | MCP(Model 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` | 消息渠道 | `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`) | diff --git a/resources/skills/about-picobot/references/commands.md b/resources/skills/about-picobot/references/commands.md new file mode 100644 index 0000000..aebbb9f --- /dev/null +++ b/resources/skills/about-picobot/references/commands.md @@ -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 +``` diff --git a/resources/skills/about-picobot/references/config.md b/resources/skills/about-picobot/references/config.md new file mode 100644 index 0000000..31a1939 --- /dev/null +++ b/resources/skills/about-picobot/references/config.md @@ -0,0 +1,100 @@ +# PicoBot 配置说明 + +配置文件加载顺序:`~/.picobot/config.json` → 当前目录 `./config.json`。 +占位符 `` 从环境变量替换,环境变量从 `.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 密钥,支持 `` 占位符 | +| `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` | URL(Sse / streamable-http 模式) | +| `tool_timeout_secs` | 单独的超时设置 | diff --git a/resources/skills/about-picobot/references/db-schema.md b/resources/skills/about-picobot/references/db-schema.md new file mode 100644 index 0000000..39e1cc0 --- /dev/null +++ b/resources/skills/about-picobot/references/db-schema.md @@ -0,0 +1,101 @@ +# PicoBot 数据库表结构 + +数据库为 SQLite,默认位于 workspace 下的 `picobot.db`。 + +## sessions 表 + +会话表,一个 session 对应一个 (channel, chat_id, dialog_id) 组合。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | TEXT PK | session 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 | 调度规则 JSON(once/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 | 耗时(毫秒) | diff --git a/resources/skills/about-picobot/references/faq.md b/resources/skills/about-picobot/references/faq.md new file mode 100644 index 0000000..aeed40c --- /dev/null +++ b/resources/skills/about-picobot/references/faq.md @@ -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` 文件,且文件头有正确的 frontmatter(name、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 模块访问。 diff --git a/resources/templates/config.example.json b/resources/templates/config.example.json index e0bd112..1167c4b 100644 --- a/resources/templates/config.example.json +++ b/resources/templates/config.example.json @@ -40,7 +40,7 @@ "default": { "provider": "aliyun", "model": "qwen-plus", - "max_tool_iterations": 20, + "max_tool_iterations": 99, "token_limit": 128000 } }, diff --git a/src/config/mod.rs b/src/config/mod.rs index 35e34ca..2abe7e3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -125,7 +125,7 @@ pub struct AgentConfig { } fn default_max_tool_iterations() -> usize { - 20 + 99 } fn default_token_limit() -> usize { diff --git a/src/skills/builtin.rs b/src/skills/builtin.rs new file mode 100644 index 0000000..05feedd --- /dev/null +++ b/src/skills/builtin.rs @@ -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(()) +} diff --git a/src/skills/mod.rs b/src/skills/mod.rs index d3372ad..7affd8e 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -1,3 +1,9 @@ +pub mod builtin; + +mod embedded { + include!(concat!(env!("OUT_DIR"), "/embedded_skills.rs")); +} + use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; @@ -53,8 +59,12 @@ impl SkillsLoader { /// Create a new loader with default paths pub fn new() -> Self { 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 { - picobot_skills_dir: home.join(".picobot/skills"), + picobot_skills_dir, agent_skills_dir: home.join(".agents/skills"), workspace_skills_dir: None, state: Arc::new(Mutex::new(SkillsState::default())), @@ -292,6 +302,7 @@ impl SkillsLoader { prompt.push_str("### 使用方法\n\n"); prompt.push_str("- 使用 `get_skill` 工具 action=\"list\" 列出所有可用 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 if !always_skills.is_empty() {