简介
Octos 是什么?
Octos 是一个开源 AI 智能体平台,能将任意大语言模型变成多渠道、多用户的智能助手。你只需部署一个 Rust 编译的二进制文件,配置好 LLM API 密钥和消息渠道(Telegram、Discord、Slack、WhatsApp、Email、微信等),Octos 会处理其余一切——对话路由、工具执行、记忆管理、模型故障切换,以及多租户隔离。
可以把它理解为 AI 智能体的后端操作系统。你无需为每个场景从零搭建聊天机器人,只需配置 Octos 的 Profile——每个 Profile 拥有独立的系统提示词、模型、工具和渠道——然后通过 Web 仪表板或 REST API 统一管理。一个小团队就能在一台机器上运行数百个专用 AI 智能体。
Octos 面向那些需求超越个人助手的用户:需要在 WhatsApp 和 Telegram 上部署 AI 客服的团队、希望在 REST API 之上构建 AI 产品的开发者、使用不同 LLM 编排多步骤研究流程的科研人员,或是共享一套 AI 系统并为每位家庭成员提供个性化配置的家庭用户。
运行模式
Octos 有两种主要运行模式:
- 对话模式 (
octos chat):交互式多轮对话,支持工具调用;也可通过--message发送单条消息后退出。 - 网关模式 (
octos gateway):常驻守护进程,同时服务多个消息渠道。
核心概念
| 术语 | 说明 |
|---|---|
| Agent(智能体) | 使用工具执行任务的 AI |
| Tool(工具) | 一项能力(Shell、文件操作、搜索、消息发送等) |
| Provider(供应商) | LLM API 服务(Anthropic、OpenAI 等) |
| Channel(渠道) | 消息平台(CLI、Telegram、Slack 等) |
| Session(会话) | 按渠道和聊天 ID 划分的对话历史 |
| Sandbox(沙箱) | 隔离的执行环境(bwrap、macOS sandbox-exec、Docker) |
| Tool Policy(工具策略) | 控制可用工具的允许/拒绝规则 |
| Skill(技能) | 可复用的指令模板(SKILL.md) |
| Bootstrap(引导文件) | 加载到系统提示词中的上下文文件(AGENTS.md、SOUL.md 等) |
快速上手
本指南带你快速完成 Octos 的基本配置和运行。
1. 初始化工作区
进入你的项目目录,初始化 Octos:
cd your-project
octos init
该命令会创建 .octos/ 目录,包含默认配置、引导文件(AGENTS.md、SOUL.md、USER.md),以及记忆、会话和技能的子目录。
2. 设置 API 密钥
至少导出一个 LLM 供应商的密钥:
export ANTHROPIC_API_KEY="sk-ant-..."
将此行添加到 ~/.bashrc 或 ~/.zshrc 中以持久保存。你也可以使用 octos auth login --provider openai 进行 OAuth 登录。
3. 检查配置
验证所有配置是否正确:
octos status
该命令会显示配置文件位置、当前使用的供应商和模型、API 密钥状态,以及引导文件的可用情况。
4. 开始对话
启动交互式多轮对话:
octos chat
或发送单条消息后退出:
octos chat --message "Add a hello function to lib.rs"
5. 运行网关
以常驻守护进程的方式服务多个消息渠道:
octos gateway
此命令要求配置文件中包含 gateway 部分,且至少配置了一个渠道。详见配置章节。
6. 启动 Web 界面
如果编译时启用了 api 特性,可以启动 Web 仪表板:
octos serve
然后在浏览器中打开 http://localhost:8080。
安装与部署
前置条件
| 条件 | 版本 | 备注 |
|---|---|---|
| Rust | 1.85.0+ | 通过 rustup.rs 安装 |
| macOS | 13+ | Apple Silicon 或 Intel |
| Linux | glibc 2.31+ | Ubuntu 20.04+、Debian 11+、Fedora 34+ |
| Windows | 10/11 | 原生编译或 WSL2 |
你还需要至少一个受支持的 LLM 供应商的 API 密钥。
可选依赖
| 依赖 | 用途 | 安装方式 |
|---|---|---|
| Node.js | WhatsApp 桥接、PPTX 创建技能 | brew install node / apt install nodejs |
| ffmpeg | 媒体/视频技能 | brew install ffmpeg / apt install ffmpeg |
| Chrome/Chromium | 浏览器自动化工具 | brew install --cask chromium |
| LibreOffice | Office 文档转换 | brew install --cask libreoffice |
| Poppler | PDF 渲染(pdftoppm) | brew install poppler / apt install poppler-utils |
从源码编译
git clone https://github.com/octos-org/octos
cd octos
# 基本功能(CLI、chat、run、gateway + CLI 渠道)
cargo install --path crates/octos-cli
# 启用消息渠道
cargo install --path crates/octos-cli --features telegram,discord,slack,whatsapp,feishu,email,wecom
# 启用浏览器自动化(需要 Chrome/Chromium)
cargo install --path crates/octos-cli --features browser
# 启用 Web 界面和 REST API
cargo install --path crates/octos-cli --features api
# 验证安装
octos --version
部署脚本
使用部署脚本可以简化安装流程:
# 最小安装(仅 CLI + 对话)
./scripts/local-tenant-deploy.sh --minimal
# 完整安装(所有渠道 + 仪表板 + 应用技能)
./scripts/local-tenant-deploy.sh --full
# 自定义渠道
./scripts/local-tenant-deploy.sh --channels telegram,discord,api
各平台安装指南
macOS
# 1. 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# 2. 安装可选依赖
brew install node ffmpeg poppler
brew install --cask libreoffice
# 3. 克隆并部署
git clone https://github.com/octos-org/octos.git
cd octos
./scripts/local-tenant-deploy.sh --full
# 4. 设置 API 密钥并运行
export ANTHROPIC_API_KEY=sk-ant-...
octos chat
后台服务(launchd 系统守护进程):
部署脚本会创建 /Library/LaunchDaemons/io.octos.serve.plist。
# 启动服务(需要 sudo)
sudo launchctl load /Library/LaunchDaemons/io.octos.serve.plist
# 停止服务
sudo launchctl unload /Library/LaunchDaemons/io.octos.serve.plist
# 查看状态
sudo launchctl print system/io.octos.serve
# 查看日志
tail -f ~/.octos/serve.log
Linux (Ubuntu/Debian)
# 1. 安装系统依赖
sudo apt update
sudo apt install -y build-essential pkg-config libssl-dev
# 2. 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# 3. 安装可选依赖
sudo apt install -y nodejs npm ffmpeg poppler-utils
# 4. 克隆并部署
git clone https://github.com/octos-org/octos.git
cd octos
./scripts/local-tenant-deploy.sh --full
# 5. 设置 API 密钥并运行
export ANTHROPIC_API_KEY=sk-ant-...
octos chat
后台服务(systemd 系统单元):
部署脚本会创建 /etc/systemd/system/octos-serve.service。
# 启动服务
sudo systemctl start octos-serve
# 开机自启
sudo systemctl enable octos-serve
# 查看状态
sudo systemctl status octos-serve
# 查看日志
sudo journalctl -u octos-serve -f
# 停止服务
sudo systemctl stop octos-serve
Linux (Fedora/RHEL)
# 安装系统依赖
sudo dnf install -y gcc pkg-config openssl-devel
# 然后按照上方 Ubuntu 的步骤从第 2 步开始操作
Windows(原生)
Octos 支持在 Windows 上原生编译和运行。Shell 命令通过 cmd /C 执行。
# 1. 安装 Rust(从 https://rustup.rs 下载 rustup-init.exe)
rustup-init.exe
# 2. 克隆并编译
git clone https://github.com/octos-org/octos.git
cd octos
cargo install --path crates/octos-cli
# 3. 设置 API 密钥并运行
$env:ANTHROPIC_API_KEY = "sk-ant-..."
octos chat
Windows 注意事项:
- Windows 上沙箱功能不可用(没有 bubblewrap/sandbox-exec 的等效工具);Shell 命令在无隔离环境下运行。如果安装了 Docker Desktop,Docker 沙箱模式仍然可用。
- API 密钥通过 Windows 凭据管理器存储。
- 进程管理使用
taskkill进行清理。
Windows (WSL2)
也可以使用 WSL2 获得 Linux 环境:
# 1. 安装 WSL2(以管理员身份运行 PowerShell)
wsl --install -d Ubuntu
# 2. 打开 Ubuntu 终端,然后按照上方 Linux (Ubuntu) 的步骤操作
在 WSL2 中运行 octos serve 时,可以通过 Windows 浏览器访问 http://localhost:8080(WSL2 自动转发端口)。
Docker
docker compose --profile gateway up -d
部署脚本参考
./scripts/local-tenant-deploy.sh [OPTIONS]
Options:
--minimal 仅 CLI + 对话(不含渠道和仪表板)
--full 所有渠道 + 仪表板 + 应用技能
--channels LIST 逗号分隔的渠道列表:telegram,discord,slack,whatsapp,feishu,email,twilio,wecom
--no-skills 跳过编译应用技能
--no-service 跳过 launchd/systemd 服务配置
--uninstall 移除二进制文件和服务文件
--debug 以 debug 模式编译(编译更快,二进制更大)
--prefix DIR 安装路径前缀(默认:~/.cargo/bin)
--no-tunnel 即使在 --full 模式下也跳过 frpc 隧道配置
--tenant-name NAME 租户子域名(例如 "alice")
--frps-token TOKEN frps 认证令牌
--frps-server ADDR frps 服务器地址(默认:163.192.33.32)
--ssh-port PORT SSH 隧道远端端口(默认:6001)
--domain DOMAIN 隧道域名(默认:octos-cloud.org)
--auth-token TOKEN 仪表板认证令牌(默认:自动生成)
在 Windows 原生环境中,请使用 .\scripts\install.ps1(PowerShell)。
脚本执行流程:
- 检查前置条件(Rust、平台依赖)
- 使用所选特性编译
octos二进制文件 - 编译应用技能二进制文件(除非指定了
--no-skills) - 在 macOS 上对二进制文件进行签名(ad-hoc codesign)
- 创建运行时数据目录,并写入
~/.octos/config.json,其中mode为"local"或"tenant" - 在启用 dashboard/API 功能时创建后台服务
- 在租户部署场景下可选配置
frpc隧道
卸载:
./scripts/local-tenant-deploy.sh --uninstall
# 数据目录(~/.octos)不会被移除。如需删除请手动执行:
rm -rf ~/.octos
安装后验证
设置 API 密钥
至少设置一个 LLM 供应商的密钥:
# 添加到 ~/.bashrc、~/.zshrc 或 ~/.profile
export ANTHROPIC_API_KEY=sk-ant-...
# 或
export OPENAI_API_KEY=sk-...
# 或使用 OAuth 登录
octos auth login --provider openai
验证
octos --version # 检查二进制文件
octos status # 检查配置和 API 密钥
octos chat --message "Hello" # 快速测试
升级
cd octos
git pull origin main
./scripts/local-tenant-deploy.sh --full # 重新编译并安装
# 如果以服务方式运行,需要重启:
# macOS:
sudo launchctl unload /Library/LaunchDaemons/io.octos.serve.plist
sudo launchctl load /Library/LaunchDaemons/io.octos.serve.plist
# Linux:
sudo systemctl restart octos-serve
常见问题
| 问题 | 解决方案 |
|---|---|
octos: command not found | 将 ~/.cargo/bin 加入 PATH:export PATH="$HOME/.cargo/bin:$PATH" |
| Linux 上编译失败 | 安装 build-essential pkg-config libssl-dev |
| macOS 代码签名警告 | 执行:codesign -s - ~/.cargo/bin/octos |
| 无法访问仪表板 | 检查端口:octos serve --port 8080,打开 http://localhost:8080 |
| WSL2 端口未转发 | 重启 WSL:wsl --shutdown,然后重新打开终端 |
| 服务无法启动 | 检查日志:tail -f ~/.octos/serve.log 或 journalctl --user -u octos-serve |
| 找不到 API 密钥 | 确保环境变量在服务环境中已设置,而不仅仅在你的 Shell 中 |
配置
配置文件位置
配置文件按以下顺序加载(找到第一个即生效):
.octos/config.json– 项目级配置~/.config/octos/config.json– 全局配置
基本配置
最简配置只需指定 LLM 供应商和模型:
{
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"api_key_env": "ANTHROPIC_API_KEY"
}
网关配置
要将 Octos 作为多渠道守护进程运行,需添加 gateway 部分:
{
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"gateway": {
"channels": [
{"type": "cli"},
{"type": "telegram", "allowed_senders": ["123456789"]},
{"type": "discord", "settings": {"token_env": "DISCORD_BOT_TOKEN"}},
{"type": "slack", "settings": {"bot_token_env": "SLACK_BOT_TOKEN", "app_token_env": "SLACK_APP_TOKEN"}},
{"type": "whatsapp", "settings": {"bridge_url": "ws://localhost:3001"}},
{"type": "feishu", "settings": {"app_id_env": "FEISHU_APP_ID", "app_secret_env": "FEISHU_APP_SECRET"}}
],
"max_history": 50,
"system_prompt": "You are a helpful assistant."
}
}
环境变量展开
可在配置值中使用 ${VAR_NAME} 语法引用环境变量:
{
"base_url": "${ANTHROPIC_BASE_URL}",
"model": "${OCTOS_MODEL}"
}
完整配置参考
以下是包含所有可用字段的完整配置结构:
{
"version": 1,
// LLM 供应商
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"base_url": null,
"api_key_env": null,
"api_type": null,
// 回退链
"fallback_models": [
{
"provider": "deepseek",
"model": "deepseek-chat",
"base_url": null,
"api_key_env": "DEEPSEEK_API_KEY"
}
],
// 自适应路由
"adaptive_routing": {
"enabled": false,
"latency_threshold_ms": 30000,
"error_rate_threshold": 0.3,
"probe_probability": 0.1,
"probe_interval_secs": 60,
"failure_threshold": 3
},
// 网关
"gateway": {
"channels": [{"type": "cli"}],
"max_history": 50,
"system_prompt": null,
"queue_mode": "followup",
"max_sessions": 1000,
"max_concurrent_sessions": 10,
"llm_timeout_secs": null,
"llm_connect_timeout_secs": null,
"tool_timeout_secs": null,
"session_timeout_secs": null,
"browser_timeout_secs": null
},
// 工具策略
"tool_policy": {"allow": [], "deny": []},
"tool_policy_by_provider": {},
"context_filter": [],
// 子供应商(用于 spawn 工具)
"sub_providers": [
{
"key": "cheap",
"provider": "deepseek",
"model": "deepseek-chat",
"description": "Fast model for simple tasks"
}
],
// 智能体设置
"max_iterations": 50,
// 向量嵌入(用于记忆的向量搜索)
"embedding": {
"provider": "openai",
"api_key_env": "OPENAI_API_KEY",
"base_url": null
},
// 语音
"voice": {
"auto_asr": true,
"auto_tts": false,
"default_voice": "vivian",
"asr_language": null
},
// 钩子
"hooks": [],
// MCP 服务器
"mcp_servers": [],
// 沙箱
"sandbox": {
"enabled": true,
"mode": "auto",
"allow_network": false
},
// 邮件(用于邮件渠道)
"email": null,
// 仪表板认证(仅 serve 模式)
"dashboard_auth": null,
// 监控(仅 serve 模式)
"monitor": null
}
环境变量
LLM 供应商
| 变量 | 说明 |
|---|---|
ANTHROPIC_API_KEY | Anthropic (Claude) API 密钥 |
OPENAI_API_KEY | OpenAI API 密钥 |
GEMINI_API_KEY | Google Gemini API 密钥 |
OPENROUTER_API_KEY | OpenRouter API 密钥 |
DEEPSEEK_API_KEY | DeepSeek API 密钥 |
GROQ_API_KEY | Groq API 密钥 |
MOONSHOT_API_KEY | Moonshot/Kimi API 密钥 |
DASHSCOPE_API_KEY | 阿里云 DashScope (Qwen) API 密钥 |
MINIMAX_API_KEY | MiniMax API 密钥 |
ZHIPU_API_KEY | 智谱 (GLM) API 密钥 |
ZAI_API_KEY | Z.AI API 密钥 |
NVIDIA_API_KEY | Nvidia NIM API 密钥 |
搜索
| 变量 | 说明 |
|---|---|
BRAVE_API_KEY | Brave Search API 密钥 |
PERPLEXITY_API_KEY | Perplexity Sonar API 密钥 |
YDC_API_KEY | You.com API 密钥 |
渠道
| 变量 | 说明 |
|---|---|
TELEGRAM_BOT_TOKEN | Telegram 机器人令牌 |
DISCORD_BOT_TOKEN | Discord 机器人令牌 |
SLACK_BOT_TOKEN | Slack 机器人令牌 |
SLACK_APP_TOKEN | Slack 应用级令牌 |
FEISHU_APP_ID | 飞书/Lark 应用 ID |
FEISHU_APP_SECRET | 飞书/Lark 应用密钥 |
WECOM_CORP_ID | 企业微信企业 ID |
WECOM_AGENT_SECRET | 企业微信应用密钥 |
EMAIL_USERNAME | 邮箱账户用户名 |
EMAIL_PASSWORD | 邮箱账户密码 |
邮件(send-email 技能)
| 变量 | 说明 |
|---|---|
SMTP_HOST | SMTP 服务器主机名 |
SMTP_PORT | SMTP 服务器端口 |
SMTP_USERNAME | SMTP 用户名 |
SMTP_PASSWORD | SMTP 密码 |
SMTP_FROM | SMTP 发件人地址 |
LARK_APP_ID | 飞书邮箱应用 ID |
LARK_APP_SECRET | 飞书邮箱应用密钥 |
LARK_FROM_ADDRESS | 飞书邮箱发件人地址 |
语音
| 变量 | 说明 |
|---|---|
OMINIX_API_URL | OminiX ASR/TTS API 地址 |
系统
| 变量 | 说明 |
|---|---|
RUST_LOG | 日志级别(error/warn/info/debug/trace) |
OCTOS_LOG_JSON | 启用 JSON 格式日志(设置为任意值即可) |
文件目录结构
~/.octos/ # 全局配置目录
├── auth.json # 已存储的 API 凭据(权限 0600)
├── profiles/ # Profile 配置(serve 模式)
│ ├── my-bot.json
│ └── work-bot.json
├── skills/ # 全局自定义技能
└── serve.log # serve 模式日志文件
.octos/ # 项目/Profile 数据目录
├── config.json # 配置文件
├── cron.json # 定时任务
├── AGENTS.md # 智能体指令
├── SOUL.md # 个性定义
├── USER.md # 用户信息
├── HEARTBEAT.md # 后台任务
├── sessions/ # 对话历史(JSONL)
├── memory/ # 记忆文件
│ ├── MEMORY.md # 长期记忆
│ └── 2025-02-10.md # 每日记忆
├── skills/ # 自定义技能
├── episodes.redb # 情景记忆数据库
└── history/
└── chat_history # Readline 历史记录
LLM 服务商与路由
Octos 开箱即用地支持 14 家 LLM 服务商。每个服务商需要一个存储在环境变量中的 API 密钥(本地服务商如 Ollama 除外)。
支持的服务商
| 服务商 | 环境变量 | 默认模型 | API 格式 | 别名 |
|---|---|---|---|---|
anthropic | ANTHROPIC_API_KEY | claude-sonnet-4-20250514 | Native Anthropic | – |
openai | OPENAI_API_KEY | gpt-4o | Native OpenAI | – |
gemini | GEMINI_API_KEY | gemini-2.0-flash | Native Gemini | – |
openrouter | OPENROUTER_API_KEY | anthropic/claude-sonnet-4-20250514 | Native OpenRouter | – |
deepseek | DEEPSEEK_API_KEY | deepseek-chat | OpenAI 兼容 | – |
groq | GROQ_API_KEY | llama-3.3-70b-versatile | OpenAI 兼容 | – |
moonshot | MOONSHOT_API_KEY | kimi-k2.5 | OpenAI 兼容 | kimi |
dashscope | DASHSCOPE_API_KEY | qwen-max | OpenAI 兼容 | qwen |
minimax | MINIMAX_API_KEY | MiniMax-Text-01 | OpenAI 兼容 | – |
zhipu | ZHIPU_API_KEY | glm-4-plus | OpenAI 兼容 | glm |
zai | ZAI_API_KEY | glm-5 | Anthropic 兼容 | z.ai |
nvidia | NVIDIA_API_KEY | meta/llama-3.3-70b-instruct | OpenAI 兼容 | nim |
ollama | (无需) | llama3.2 | OpenAI 兼容 | – |
vllm | VLLM_API_KEY | (须指定) | OpenAI 兼容 | – |
配置方式
配置文件
在 config.json 中设置 provider 和 model:
{
"provider": "moonshot",
"model": "kimi-2.5",
"api_key_env": "KIMI_API_KEY"
}
api_key_env 字段可覆盖服务商默认的环境变量名。例如,Moonshot 默认使用 MOONSHOT_API_KEY,但你可以将其指向 KIMI_API_KEY。
命令行参数
octos chat --provider deepseek --model deepseek-chat
octos chat --model gpt-4o # 根据模型名自动检测服务商
凭证存储
除了环境变量,你也可以通过 auth 命令行存储 API 密钥:
# OAuth PKCE (OpenAI)
octos auth login --provider openai
# Device code 流程 (OpenAI)
octos auth login --provider openai --device-code
# 粘贴令牌(其他所有服务商)
octos auth login --provider anthropic
# -> 提示: "Paste your API key:"
# 查看已存储的凭证
octos auth status
# 删除凭证
octos auth logout --provider openai
凭证存储在 ~/.octos/auth.json(文件权限 0600)。解析 API 密钥时,凭证存储的优先级高于环境变量。
自动检测
省略 --provider 时,Octos 会根据模型名推断服务商:
| 模型名模式 | 检测到的服务商 |
|---|---|
claude-* | anthropic |
gpt-*, o1-*, o3-*, o4-* | openai |
gemini-* | gemini |
deepseek-* | deepseek |
kimi-*, moonshot-* | moonshot |
qwen-* | dashscope |
glm-* | zhipu |
llama-* | groq |
octos chat --model gpt-4o # -> openai
octos chat --model claude-sonnet-4-20250514 # -> anthropic
octos chat --model deepseek-chat # -> deepseek
octos chat --model glm-4-plus # -> zhipu
octos chat --model qwen-max # -> dashscope
自定义端点
使用 base_url 指向自部署或代理端点:
{
"provider": "openai",
"model": "gpt-4o",
"base_url": "https://your-azure-endpoint.openai.azure.com/v1"
}
{
"provider": "ollama",
"model": "llama3.2",
"base_url": "http://localhost:11434/v1"
}
{
"provider": "vllm",
"model": "meta-llama/Llama-3-70b",
"base_url": "http://localhost:8000/v1"
}
API 类型覆盖
api_type 字段可在服务商使用非标准协议时强制指定传输格式:
{
"provider": "zai",
"model": "glm-5",
"api_type": "anthropic"
}
"openai"– OpenAI Chat Completions 格式(大多数服务商的默认值)"anthropic"– Anthropic Messages 格式(用于 Anthropic 兼容代理)
降级链
配置一个按优先级排列的降级链。当主服务商请求失败时,自动尝试列表中的下一个服务商:
{
"provider": "moonshot",
"model": "kimi-2.5",
"fallback_models": [
{
"provider": "deepseek",
"model": "deepseek-chat",
"api_key_env": "DEEPSEEK_API_KEY"
},
{
"provider": "gemini",
"model": "gemini-2.0-flash",
"api_key_env": "GEMINI_API_KEY"
}
]
}
故障转移规则:
- 401/403(认证错误)– 立即转移,不在同一服务商上重试
- 429(限流)/ 5xx(服务端错误)– 指数退避重试,之后转移
- 400(内容格式错误)– 当错误包含
"must not be empty"、"reasoning_content"、"API key not valid"或"invalid_value"时转移(不同服务商的验证规则可能不同) - 超时 – 立即转移(不在无响应的服务商上浪费 120s × 重试次数)
- 熔断器 – 连续 3 次失败将标记该服务商为降级状态
重试配置(指数退避):
| 参数 | 默认值 | 说明 |
|---|---|---|
max_retries | 3 | 每个服务商的重试次数 |
initial_delay | 1s | 首次重试延迟 |
max_delay | 60s | 最大重试延迟 |
backoff_multiplier | 2.0 | 指数倍增系数 |
429 错误会从响应体中解析 "try again in Xs" 以实现更智能的退避(回退默认:30s)。
自适应路由
当配置了多个降级模型时,自适应路由会以指标驱动的动态选择取代静态优先级链。
路由模式
三种互斥模式,通过 adaptive_routing.mode 设置:
| 模式 | 说明 |
|---|---|
off(默认) | 静态优先级顺序。仅在熔断器打开(3 次连续失败)时才转移。 |
hedge | 对冲竞速:同时向 2 个服务商发送请求,取先到的结果,取消后到的。两者都累积 QoS 指标。 |
lane | 评分选道:基于 4 因子评分公式动态选择最优服务商。比对冲更省(无重复请求)。 |
{
"adaptive_routing": {
"mode": "hedge",
"qos_ranking": true,
"latency_threshold_ms": 10000,
"error_rate_threshold": 0.3,
"probe_probability": 0.1,
"probe_interval_secs": 60,
"failure_threshold": 3
}
}
| 配置项 | 默认值 | 说明 |
|---|---|---|
mode | "off" | "off"、"hedge" 或 "lane" |
qos_ranking | false | 启用 QoS 质量排名(使用模型目录评分) |
latency_threshold_ms | 10000 | 内部使用的软惩罚阈值 |
error_rate_threshold | 0.3 | 错误率超过此值的服务商将被降低优先级 |
probe_probability | 0.1 | 发送至非主服务商作为健康探测的请求比例 |
probe_interval_secs | 60 | 对同一服务商两次探测之间的最小间隔(秒) |
failure_threshold | 3 | 连续失败多少次后触发熔断 |
评分公式(Lane 模式)
每个服务商通过加权 4 因子公式评分,越低越好。所有权重可通过 adaptive_routing 配置:
score = w_stability × blended_error_rate
+ w_quality × (0.6 × norm_quality + 0.4 × norm_throughput)
+ w_priority × norm_config_order
+ w_cost × norm_output_price
| 因子 | 权重键 | 默认 | 说明 |
|---|---|---|---|
| 稳定性 | weight_error_rate | 0.3 | 混合基线 + 实时错误率。EMA 混合权重在 10 次调用中从 0 渐变到 1。 |
| 质量 | weight_latency | 0.3 | 60% 归一化 ds_output 质量 + 40% 归一化吞吐量(输出 tokens/秒 EMA) |
| 优先级 | weight_priority | 0.2 | 配置顺序偏好(0=主服务商,越高越靠后)。归一化到 [0, 1]。 |
| 成本 | weight_cost | 0.2 | 归一化的每百万 token 输出价格。未知成本 → 0(无惩罚)。 |
目录可以从 model_catalog.json 基准文件预填充,使路由器在启动时即具备参考评分而非冷启动启发。
自动升级
当检测到持续的延迟恶化时,会话 actor 会自动激活对冲模式 + 投机队列:
ResponsivenessObserver从前 5 次请求学习中位数基线(对异常值鲁棒),然后通过 80/20 EMA 每隔 20 个样本自适应调整基线。- 如果连续 3 次 LLM 响应超过 3×基线 延迟,对冲竞速和投机队列同时启用。
- 当服务商恢复(一次正常延迟响应)时,两者都恢复为 Followup 和静态路由。
服务商包装栈
路由系统由分层包装器组成:
| 包装器 | 用途 |
|---|---|
AdaptiveRouter | 顶层:指标驱动评分、对冲/选道模式、熔断器、探测请求 |
ProviderChain | 有序故障转移,带每服务商熔断器(失败次数 >= 阈值 → 降级) |
FallbackProvider | 主服务商 + 按QoS排名的备选,通过 ProviderRouter 追踪冷却 |
RetryProvider | 429/5xx 指数退避。超时 → 不重试(改为转移) |
ProviderRouter | 子 Agent 多模型路由。前缀键解析、冷却、QoS评分备选 |
SwappableProvider | 通过 RwLock 实现运行时模型切换(如 switch_model 工具)。每次切换泄漏约 50 字节 |
网关与频道
Octos 以网关模式运行,将各消息平台桥接到你的 LLM 智能体。每个平台连接称为一个频道。你可以在同一个网关进程中同时运行多个频道——例如同时接入 Telegram 和 Slack。
频道概览
频道在 config.json 的 gateway.channels 数组中配置。每个条目指定一个 type、可选的 allowed_senders 用于访问控制,以及平台特定的 settings。
查看已编译和已配置的频道:
octos channels status
该命令会显示一张表格,列出每个频道的编译状态(feature flags)和配置摘要(环境变量的设置情况)。
Telegram
需要从 @BotFather 获取 bot token。
export TELEGRAM_BOT_TOKEN="123456:ABC..."
{
"type": "telegram",
"allowed_senders": ["your_user_id"],
"settings": {
"token_env": "TELEGRAM_BOT_TOKEN"
}
}
Telegram 支持 bot 命令、内联键盘、语音消息、图片和文件。
Slack
需要一个 Socket Mode 应用,同时提供 bot token 和 app-level token。
export SLACK_BOT_TOKEN="xoxb-..."
export SLACK_APP_TOKEN="xapp-..."
{
"type": "slack",
"settings": {
"bot_token_env": "SLACK_BOT_TOKEN",
"app_token_env": "SLACK_APP_TOKEN"
}
}
Discord
需要从 Discord Developer Portal 获取 bot token。
export DISCORD_BOT_TOKEN="..."
{
"type": "discord",
"settings": {
"token_env": "DISCORD_BOT_TOKEN"
}
}
需要一个运行在 WebSocket URL 上的 Node.js 桥接(Baileys)。
{
"type": "whatsapp",
"settings": {
"bridge_url": "ws://localhost:3001"
}
}
飞书(中国版)
飞书默认使用 WebSocket 长连接模式(无需公网 URL)。
export FEISHU_APP_ID="cli_..."
export FEISHU_APP_SECRET="..."
{
"type": "feishu",
"settings": {
"app_id_env": "FEISHU_APP_ID",
"app_secret_env": "FEISHU_APP_SECRET"
}
}
构建时需启用 feishu feature flag:
cargo build --release -p octos-cli --features feishu
Lark(国际版)
Lark(国际版飞书)不支持 WebSocket 模式,需改用 webhook 模式——由 Lark 通过 HTTP POST 将事件推送到你的服务器。
Lark Cloud --> ngrok --> localhost:9321/webhook/event --> Gateway --> LLM
开发者控制台配置
- 前往 open.larksuite.com/app,创建或选择一个应用
- 在 Features 下添加 Bot 能力
- 配置事件订阅:
- Events & Callbacks > Event Configuration > Edit subscription method
- 选择 “Send events to developer server”
- 将请求 URL 设为
https://YOUR_NGROK_URL/webhook/event
- 添加事件:
im.message.receive_v1(接收消息) - 启用权限:
im:message、im:message:send_as_bot、im:resource - 发布应用:App Release > Version Management > Create Version > Apply for Online Release
配置
export LARK_APP_ID="cli_..."
export LARK_APP_SECRET="..."
{
"type": "lark",
"allowed_senders": [],
"settings": {
"app_id_env": "LARK_APP_ID",
"app_secret_env": "LARK_APP_SECRET",
"region": "global",
"mode": "webhook",
"webhook_port": 9321
}
}
配置项参考
| 配置项 | 说明 | 默认值 |
|---|---|---|
app_id_env | App ID 的环境变量名 | FEISHU_APP_ID |
app_secret_env | App Secret 的环境变量名 | FEISHU_APP_SECRET |
region | "cn"(飞书)或 "global" / "lark"(Lark 国际版) | "cn" |
mode | "ws"(WebSocket)或 "webhook"(HTTP) | "ws" |
webhook_port | webhook HTTP 服务端口 | 9321 |
encrypt_key | Lark 控制台的 Encrypt Key(用于 AES-256-CBC) | 无 |
verification_token | Lark 控制台的 Verification Token | 无 |
加密(可选)
如果你在 Lark 控制台(Events & Callbacks > Encryption Strategy)配置了 Encrypt Key,需将其添加到配置中:
{
"type": "lark",
"settings": {
"app_id_env": "LARK_APP_ID",
"app_secret_env": "LARK_APP_SECRET",
"region": "global",
"mode": "webhook",
"webhook_port": 9321,
"encrypt_key": "your-encrypt-key-here",
"verification_token": "your-verification-token"
}
}
启用加密后,Lark 会发送加密的 POST 请求体。网关使用 SHA-256 密钥派生的 AES-256-CBC 进行解密,并通过 X-Lark-Signature 头验证签名。
支持的消息类型
入站消息: 文本、图片、文件(PDF、文档)、音频、视频、表情包
出站消息: Markdown(通过互动卡片)、图片上传、文件上传
运行
# 启动 ngrok 隧道
ngrok http 9321
# 启动网关
LARK_APP_ID="cli_xxxxx" LARK_APP_SECRET="xxxxx" octos gateway --cwd /path/to/workdir
常见问题排查
| 问题 | 解决方案 |
|---|---|
| WS 端点返回 404 | Lark 国际版不支持 WebSocket,请使用 "mode": "webhook" |
| Challenge 验证失败 | 确认 ngrok 正在运行且 URL 与 Lark 控制台中的一致 |
| 收不到事件 | 添加事件后需发布应用版本;检查控制台中的 Event Log |
| 机器人不回复 | 确认已授予 im:message:send_as_bot 权限 |
| Ngrok URL 变了 | 免费 ngrok URL 每次重启都会变化,需在 Lark 控制台更新请求 URL |
邮件(IMAP/SMTP)
通过 IMAP 轮询收件箱获取入站消息,通过 SMTP 发送回复。需启用 email feature flag。
export EMAIL_USERNAME="bot@example.com"
export EMAIL_PASSWORD="app-specific-password"
{
"type": "email",
"allowed_senders": ["trusted@example.com"],
"settings": {
"imap_host": "imap.gmail.com",
"imap_port": 993,
"smtp_host": "smtp.gmail.com",
"smtp_port": 465,
"username_env": "EMAIL_USERNAME",
"password_env": "EMAIL_PASSWORD",
"from_address": "bot@example.com",
"poll_interval_secs": 30,
"max_body_chars": 10000
}
}
企业微信(WeCom)
需要一个配置了消息回调 URL 的自建应用。需启用 wecom feature flag。
export WECOM_CORP_ID="ww..."
export WECOM_AGENT_SECRET="..."
{
"type": "wecom",
"settings": {
"corp_id_env": "WECOM_CORP_ID",
"agent_secret_env": "WECOM_AGENT_SECRET",
"agent_id": "1000002",
"verification_token": "...",
"encoding_aes_key": "...",
"webhook_port": 9322
}
}
微信(通过 WorkBuddy 桥接)
普通微信用户可以通过 WorkBuddy 桌面端桥接连接到你的智能体。WorkBuddy 负责微信传输层;Octos 通过其 WeCom Bot 频道处理 AI 逻辑。
微信(手机) --> WorkBuddy(桌面端) --> 企业微信群机器人(WSS) --> octos wecom-bot 频道
配置步骤
-
在企业微信管理后台的“应用管理 > 群机器人“中创建一个企业微信群机器人,记下 Bot ID 和 Secret。
-
配置
wecom-bot频道:
export WECOM_BOT_SECRET="your_robot_secret_here"
{
"type": "wecom-bot",
"allowed_senders": [],
"settings": {
"bot_id": "YOUR_BOT_ID",
"secret_env": "WECOM_BOT_SECRET"
}
}
- 构建并启动:
cargo build --release -p octos-cli --features "wecom-bot"
octos gateway
- 安装 WorkBuddy 桌面客户端,通过扫码关联你的微信,并连接到同一个企业微信群机器人。
连接详情
| 属性 | 值 |
|---|---|
| 协议 | WebSocket (WSS) |
| 端点 | wss://openws.work.weixin.qq.com |
| 心跳 | 每 30 秒 Ping/pong |
| 自动重连 | 支持,指数退避(5s–60s) |
| 最大消息长度 | 4096 字符 |
| 消息格式 | Markdown |
wecom-bot 频道使用出站 WebSocket 连接——无需公网 URL 或端口转发。适合部署在 NAT 或防火墙后的服务器。
限制
- 仅支持文本 – 语音和图片消息以占位符形式传递
- 不支持消息编辑 – 回复以新消息形式发送
- 单向触发 – 微信到 Octos 自动触发;主动推送需使用定时任务
会话控制命令
在任何网关频道中,以下命令用于管理对话会话:
| 命令 | 说明 |
|---|---|
/new | 创建新会话(从当前对话的最近 10 条消息分叉) |
/new <name> | 创建命名会话 |
/s <name> | 切换到指定名称的会话 |
/s | 切换到默认会话 |
/sessions | 列出当前聊天的所有会话 |
/back | 切换到上一个活跃会话 |
/delete | 删除当前会话 |
每个聊天同一时间只有一个活跃会话。消息会路由到活跃会话。非活跃会话仍可执行后台任务(深度搜索、流水线等)。当非活跃会话完成工作时,你会收到通知——使用 /s <name> 查看结果。
语音转写
来自频道的语音和音频消息在发送给智能体前会自动转写。系统优先尝试本地 ASR(通过 OminiX 引擎),本地不可用时降级到云端 Whisper。转写结果会以 [transcription: ...] 的形式前置。
# 本地 ASR(优先) -- 由 octos serve 自动设置
export OMINIX_API_URL="http://localhost:8080"
# 云端降级
export GROQ_API_KEY="gsk_..."
config.json 中的语音配置:
{
"voice": {
"auto_asr": true,
"auto_tts": true,
"default_voice": "vivian",
"asr_language": null
}
}
auto_asr– 自动转写收到的语音/音频消息auto_tts– 用户发送语音时自动合成语音回复default_voice– 自动 TTS 的语音预设asr_language– 强制指定转写语言(null= 自动检测)
访问控制
使用 allowed_senders 限制谁可以与智能体交互。空列表表示允许所有人。
{
"type": "telegram",
"allowed_senders": ["123456", "789012"]
}
每种频道类型使用各自的发送者标识格式(Telegram 用户 ID、邮箱地址、企业微信用户 ID 等)。
定时任务
智能体可以调度周期性任务,通过任意频道发送消息:
octos cron list # 列出活跃任务
octos cron list --all # 包含已禁用的任务
octos cron add --name "report" --message "Generate daily report" --cron "0 0 9 * * * *"
octos cron add --name "check" --message "Check status" --every 3600
octos cron add --name "once" --message "Run migration" --at "2025-03-01T09:00:00Z"
octos cron remove <job-id>
octos cron enable <job-id> # 启用任务
octos cron enable <job-id> --disable # 禁用任务
任务支持可选的 timezone 字段,使用 IANA 时区名称(如 "America/New_York"、"Asia/Shanghai")。未指定时使用 UTC。
消息合并
长回复会自动拆分为符合频道限制的分段:
| 频道 | 每条消息最大字符数 |
|---|---|
| Telegram | 4000 |
| Discord | 1900 |
| Slack | 3900 |
拆分优先级:段落边界 > 换行符 > 句末 > 空格 > 硬截断。
配置热更新
网关会自动检测配置文件变更:
- 热更新(无需重启):系统提示词、AGENTS.md、SOUL.md、USER.md
- 需要重启:服务商、模型、API 密钥、频道设置
变更通过 SHA-256 哈希检测,并附带防抖机制。
记忆与技能
Octos 拥有分层记忆系统和可扩展的技能框架。记忆赋予智能体跨会话的持久上下文,技能则为智能体提供新的工具和能力。
引导文件
这些文件在启动时加载到系统提示词中。使用 octos init 创建它们。
| 文件 | 用途 |
|---|---|
.octos/AGENTS.md | 智能体指令与准则 |
.octos/SOUL.md | 人格与价值观 |
.octos/USER.md | 用户信息与偏好 |
.octos/TOOLS.md | 工具使用指南 |
.octos/IDENTITY.md | 自定义身份定义 |
引导文件支持热更新——编辑后智能体会自动获取更改,无需重启。
记忆系统
Octos 采用三层记忆架构,结合自动记录与智能体驱动的知识管理:
┌──────────────────────────────────────────────────────────────────┐
│ 系统提示词(每轮对话) │
│ │
│ 1. 情景记忆 ─── 最相关的 6 条历史任务经验 │
│ 2. 记忆上下文 ─── MEMORY.md + 最近 7 天每日笔记 │
│ 3. 实体知识库 ─── 所有已知实体的一行摘要 │
│ │
│ 工具:save_memory / recall_memory (实体知识库 CRUD) │
└──────────────────────────────────────────────────────────────────┘
第一层:情景记忆(自动)
每个完成的任务会自动记录为一条情景(episode),存储在 episodes.redb 嵌入式数据库中。每条情景包含:
- 摘要 — 由 LLM 生成,截断至 500 字符
- 结果 — 成功、失败、阻塞或取消
- 修改的文件 — 任务期间涉及的文件路径列表
- 关键决策 — 执行过程中的重要选择
- 工作目录 — 用于按目录范围检索
每次开始新任务时,智能体会从情景库中检索最多 6 条相关历史经验,检索方式为:
- 混合搜索(配置了向量嵌入时默认):结合 BM25 关键词匹配(30% 权重)和 HNSW 向量相似度(70% 权重)
- 关键词搜索(未配置嵌入时的回退):将查询词与情景摘要进行匹配,限定在同一工作目录范围内
向量嵌入配置(在 config.json 中):
{
"embedding": {
"provider": "openai",
"api_key_env": "OPENAI_API_KEY",
"base_url": null
}
}
配置后,智能体会以“发射后不管“(fire-and-forget)的方式在后台对每条情景摘要生成向量嵌入,并与情景一同存储。查询时,任务指令会被嵌入并用于向量搜索。未配置时,系统回退到纯 BM25 关键词匹配。
第二层:长期记忆与每日笔记(基于文件)
长期记忆(.octos/memory/MEMORY.md)保存跨会话的持久化事实和笔记。可通过手动编辑或 write_file 工具写入——其内容会在每轮对话中完整注入系统提示词。
每日笔记(.octos/memory/YYYY-MM-DD.md)提供近期活动的滚动窗口。最近 7 天的每日笔记会自动纳入智能体上下文。这些文件可以手动创建或通过 write_file 工具生成。
注意: 每日笔记由系统提示词构建器读取,但不会自动填充内容。你可以手动写入或指示智能体通过
write_file工具写入。
第三层:实体知识库(工具驱动)
实体知识库是位于 .octos/memory/bank/entities/ 的结构化知识存储。每个实体是一个 Markdown 文件,包含智能体对特定主题的所有认知。
工作原理:
- 摘要注入提示词 — 每个实体的第一个非标题行成为一行摘要。所有摘要被注入系统提示词,为智能体提供一个精简的知识索引。
- 按需加载全文 — 智能体使用
recall_memory工具在需要详情时加载特定实体的完整内容。 - 智能体自主管理 — 智能体通过
save_memory工具自行决定何时创建和更新实体。
记忆工具:
save_memory— 创建或更新实体页面。智能体被要求先通过recall_memory读取已有内容,然后合并新信息再保存(避免数据丢失)。recall_memory— 加载指定实体的完整内容。如果实体不存在,则返回所有可用实体列表。
自动延迟: 当工具总数超过 15 个时,记忆工具会被移入
group:memory延迟组。智能体需先使用activate_tools启用它们,然后才能保存或回忆知识。
文件结构
.octos/
├── config.json # 配置文件(版本化,自动迁移)
├── cron.json # 定时任务存储
├── AGENTS.md # 智能体指令
├── SOUL.md # 人格设定
├── USER.md # 用户信息
├── HEARTBEAT.md # 后台任务
├── sessions/ # 聊天历史(JSONL)
├── memory/ # 记忆文件
│ ├── MEMORY.md # 长期记忆(手动或 write_file)
│ ├── 2025-02-10.md # 每日笔记(手动或 write_file)
│ └── bank/
│ └── entities/ # 实体知识库(由 save/recall 工具管理)
│ ├── yuechen.md # 实体:「用户是谁」
│ └── octos.md # 实体:「这个项目是什么」
├── skills/ # 自定义技能
├── episodes.redb # 情景记忆数据库(自动填充)
└── history/
└── chat_history # Readline 历史
内置系统技能
Octos 在编译时内置了 3 个系统技能:
| 技能 | 说明 |
|---|---|
cron | 定时任务工具使用示例(常驻) |
skill-store | 技能安装与管理 |
skill-creator | 自定义技能创建指南 |
工作区中 .octos/skills/ 下的技能会覆盖同名的内置技能。
预装应用技能
八个应用技能以编译后的二进制文件形式随 Octos 分发。它们在网关启动时自动部署到 .octos/skills/——无需手动安装。
新闻获取
工具: news_fetch | 常驻: 是
从 Google News RSS、Hacker News API、Yahoo News、Substack 和 Medium 抓取头条和全文内容。智能体会将原始数据整理为格式化的新闻摘要。
参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
categories | array | 全部 | 要获取的新闻分类 |
language | "zh" / "en" | "zh" | 输出语言 |
分类:politics、world、business、technology、science、entertainment、health、sports
配置:
/config set news_digest.language en
/config set news_digest.hn_top_stories 50
/config set news_digest.max_deep_fetch_total 30
深度搜索
工具: deep_search | 超时时间: 600 秒
多轮网络研究工具。执行迭代搜索、并行页面爬取、引用链追踪,并生成结构化报告保存到 ./research/<query-slug>/。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
query | string | (必填) | 研究主题或问题 |
depth | 1–3 | 2 | 研究深度级别 |
max_results | 1–10 | 8 | 每轮搜索的结果数 |
search_engine | string | auto | perplexity、duckduckgo、brave、you |
深度级别:
- 1(快速): 单轮搜索,约 1 分钟,最多 10 个页面
- 2(标准): 3 轮搜索 + 引用链追踪,约 3 分钟,最多 30 个页面
- 3(深入): 5 轮搜索 + 积极链接追踪,约 5 分钟,最多 50 个页面
深度爬取
工具: deep_crawl | 依赖: PATH 中需有 Chrome/Chromium
使用无头 Chrome 通过 CDP 递归爬取网站。渲染 JavaScript,通过 BFS 跟踪同源链接,提取干净文本。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url | string | (必填) | 起始 URL |
max_depth | 1–10 | 3 | 最大链接跟踪深度 |
max_pages | 1–200 | 50 | 最大爬取页面数 |
path_prefix | string | 无 | 仅跟踪此路径下的链接 |
输出保存到 crawl-<hostname>/,以编号的 Markdown 文件形式存储。
配置:
/config set deep_crawl.page_settle_ms 5000
/config set deep_crawl.max_output_chars 100000
发送邮件
工具: send_email
通过 SMTP 或飞书/Lark Mail API 发送邮件(根据可用的环境变量自动检测)。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
to | string | (必填) | 收件人邮箱地址 |
subject | string | (必填) | 邮件主题 |
body | string | (必填) | 邮件正文(纯文本或 HTML) |
html | boolean | false | 将正文视为 HTML |
attachments | array | 无 | 文件附件(仅 SMTP) |
SMTP 环境变量:
export SMTP_HOST="smtp.gmail.com"
export SMTP_PORT="465"
export SMTP_USERNAME="your-email@gmail.com"
export SMTP_PASSWORD="your-app-password"
export SMTP_FROM="your-email@gmail.com"
天气
工具: get_weather、get_forecast | API: Open-Meteo(免费,无需密钥)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
city | string | (必填) | 英文城市名 |
days | 1–16 | 7 | 预报天数(仅预报) |
时钟
工具: get_time
返回任意 IANA 时区的当前日期、时间、星期和 UTC 偏移。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
timezone | string | 服务器本地时区 | IANA 时区名称(如 Asia/Shanghai、US/Eastern) |
账户管理
工具: manage_account
管理当前配置下的子账户。操作:list、create、update、delete、info、start、stop、restart。
平台技能(ASR/TTS)
平台技能提供设备端语音转写和合成。需要在 Apple Silicon(M1/M2/M3/M4)上运行 OminiX 后端。
语音转写
工具: voice_transcribe
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
audio_path | string | (必填) | 音频文件路径(WAV、OGG、MP3、FLAC、M4A) |
language | string | "Chinese" | "Chinese"、"English"、"Japanese"、"Korean"、"Cantonese" |
语音合成
工具: voice_synthesize
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
text | string | (必填) | 要合成的文本 |
output_path | string | 自动 | 输出文件路径 |
language | string | "chinese" | "chinese"、"english"、"japanese"、"korean" |
speaker | string | "vivian" | 语音预设 |
可用语音: vivian、serena、ryan、aiden、eric、dylan(英/中)、uncle_fu(仅中文)、ono_anna(日语)、sohee(韩语)
语音克隆
工具: voice_clone_synthesize
使用 3–10 秒参考音频样本的克隆语音进行语音合成。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
text | string | (必填) | 要合成的文本 |
reference_audio | string | (必填) | 参考音频路径 |
language | string | "chinese" | 目标语言 |
播客生成
工具: generate_podcast
根据 {speaker, voice, text} 对象组成的脚本创建多说话人播客音频。
自定义技能安装
从 GitHub 安装
# 安装仓库中的所有技能
octos skills install user/repo
# 安装特定技能
octos skills install user/repo/skill-name
# 从指定分支安装
octos skills install user/repo --branch develop
# 强制覆盖已有技能
octos skills install user/repo --force
# 安装到指定配置文件
octos skills install user/repo --profile my-bot
安装程序会优先从技能注册表下载预编译二进制文件(SHA-256 校验),如有 Cargo.toml 则回退到 cargo build --release,如有 package.json 则运行 npm install。
技能管理
octos skills list # 列出已安装的技能
octos skills info skill-name # 查看技能详情
octos skills update skill-name # 更新指定技能
octos skills update all # 更新所有技能
octos skills remove skill-name # 删除技能
octos skills search "web scraping" # 搜索在线注册表
技能解析顺序
技能按以下目录加载(优先级从高到低):
.octos/plugins/(旧版兼容).octos/skills/(用户安装的自定义技能).octos/bundled-app-skills/(预装应用技能).octos/platform-skills/(平台技能:ASR/TTS)~/.octos/plugins/(全局旧版兼容)~/.octos/skills/(全局自定义技能)
用户安装的技能会覆盖同名的预装技能。
技能开发
自定义技能位于 .octos/skills/<name>/ 目录下,包含:
.octos/skills/my-skill/
├── SKILL.md # 必需:指令 + frontmatter
├── manifest.json # 工具技能必需:工具定义
├── main # 编译后的二进制文件(或脚本)
└── .source # 自动生成:追踪安装来源
SKILL.md 格式
---
name: my-skill
version: 1.0.0
author: Your Name
description: A brief description of what this skill does
always: false
requires_bins: curl,jq
requires_env: MY_API_KEY
---
# My Skill Instructions
Instructions for the agent on how and when to use this skill.
## When to Use
- Use this skill when the user asks about...
## Tool Usage
The `my_tool` tool accepts:
- `query` (required): The search query
- `limit` (optional): Maximum results (default: 10)
Frontmatter 字段:
| 字段 | 说明 |
|---|---|
name | 技能标识符(须与目录名一致) |
version | 语义化版本号 |
author | 技能作者 |
description | 简短描述 |
always | 为 true 时,每次系统提示词都会包含该技能;为 false 时,按需加载 |
requires_bins | 逗号分隔的二进制文件列表,通过 which 检查。任一缺失则技能不可用 |
requires_env | 逗号分隔的环境变量列表。任一未设置则技能不可用 |
manifest.json 格式
用于提供可执行工具的技能:
{
"name": "my-skill",
"version": "1.0.0",
"description": "My custom skill",
"tools": [
{
"name": "my_tool",
"description": "Does something useful",
"timeout_secs": 60,
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"limit": {
"type": "integer",
"description": "Maximum results",
"default": 10
}
},
"required": ["query"]
}
}
],
"entrypoint": "main"
}
工具二进制文件通过 stdin 接收 JSON 输入,须通过 stdout 输出 JSON:
// 输入 (stdin)
{"query": "test", "limit": 5}
// 输出 (stdout)
{"output": "Results here...", "success": true}
高级功能
本章介绍面向高级用户的功能:工具管理、队列模式、生命周期钩子、沙箱隔离、会话管理和 Web 仪表板。
工具与 LRU 延迟加载
Octos 通过将工具分为活跃和延迟两组来管理庞大的工具目录。活跃工具会作为可调用的工具规格发送给 LLM;延迟工具仅以名称列出在系统提示中,在需要时才发送完整规格。
工作原理
- 基础工具(不会被淘汰):
read_file、write_file、shell、glob、grep、list_dir、run_pipeline、deep_search等。 - 动态工具:
save_memory、web_search、recall_memory等按需激活、空闲后淘汰的工具。 - 延迟工具:
browser、manage_skills、spawn、configure_tool、switch_model等仅列出名称的工具。
淘汰规则
当活跃工具数量超过 15 个时:
- 空闲 5 次以上迭代且不在基础工具集中的工具成为淘汰候选。
- 最久未使用的工具优先移入延迟列表。
重新激活
当 LLM 需要使用某个延迟工具时,它会调用 activate_tools({"tools": [...]})。这会将工具名称解析到对应的工具组,并激活整个组。
工具配置
可以在运行时通过 /config 斜杠命令配置工具。设置持久化存储在 {data_dir}/tool_config.json 中。
| 工具 | 设置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
news_digest | language | "zh" / "en" | "zh" | 新闻摘要的输出语言 |
news_digest | hn_top_stories | 5-100 | 30 | Hacker News 抓取的故事数 |
news_digest | max_rss_items | 5-100 | 30 | 每个 RSS 源的条目数 |
news_digest | max_deep_fetch_total | 1-50 | 20 | 深度抓取的文章总数 |
news_digest | max_source_chars | 1000-50000 | 12000 | 每个来源的 HTML 字符上限 |
news_digest | max_article_chars | 1000-50000 | 8000 | 每篇文章的内容字符上限 |
deep_crawl | page_settle_ms | 500-10000 | 3000 | JS 渲染等待时间(毫秒) |
deep_crawl | max_output_chars | 10000-200000 | 50000 | 输出截断上限 |
web_search | count | 1-10 | 5 | 默认搜索结果数量 |
web_fetch | extract_mode | "markdown" / "text" | "markdown" | 内容提取格式 |
web_fetch | max_chars | 1000-200000 | 50000 | 内容大小上限 |
browser | action_timeout_secs | 30-600 | 300 | 单次操作超时时间 |
browser | idle_timeout_secs | 60-600 | 300 | 空闲会话超时时间 |
聊天中的配置命令:
/config # 显示所有工具设置
/config web_search # 显示 web_search 的设置
/config set web_search.count 10 # 将默认结果数设为 10
/config set news_digest.language en # 将新闻摘要切换为英文
/config reset web_search.count # 重置为默认值
优先级顺序(从高到低):
- 显式的单次调用参数(工具调用时指定的参数)
/config覆盖值(存储在tool_config.json中)- 硬编码的默认值
工具策略
工具策略控制 Agent 可以使用哪些工具,可在全局、按提供商或按上下文级别进行设置。
全局策略
{
"tool_policy": {
"allow": ["group:fs", "group:search", "web_search"],
"deny": ["shell", "spawn"]
}
}
allow– 如果非空,则只允许使用这些工具。如果为空,则允许所有工具。deny– 始终禁止使用这些工具。deny 优先于 allow。
命名分组
| 分组 | 展开为 |
|---|---|
group:fs | read_file、write_file、edit_file、diff_edit |
group:runtime | shell |
group:web | web_search、web_fetch、browser |
group:search | glob、grep、list_dir |
group:sessions | spawn |
不在命名分组中的工具:send_file、switch_model、run_pipeline、configure_tool、cron、message。
通配符匹配
后缀 * 匹配前缀:
{
"tool_policy": {
"deny": ["web_*"]
}
}
这会禁止 web_search、web_fetch 等工具。
按提供商的策略
为不同的 LLM 模型设置不同的工具集:
{
"tool_policy_by_provider": {
"openai/gpt-4o-mini": {
"deny": ["shell", "write_file"]
},
"gemini": {
"deny": ["diff_edit"]
}
}
}
队列模式
队列模式控制 Agent 正在处理上一个请求时,新到达的用户消息如何处理。可通过聊天中的 /queue <mode> 或配置文件中的 queue_mode 设置。
Followup(默认)
顺序处理。每条消息依次等待处理。
- Agent 处理 A,完成后处理 B,完成后处理 C。
- 简单且可预测。
- 当前请求完成前,用户处于等待状态。
Collect
将排队的消息批量合并为一个组合提示。
- Agent 正在处理 A。用户发送 B,然后 C。
- A 完成后,B 和 C 合并为一个提示:
B\n---\nQueued #1: C - 一次 LLM 调用处理整个批次。
- 适合习惯连续发送多条短消息的用户(在聊天应用中很常见)。
Steer
只保留最新的排队消息,丢弃较旧的。
- Agent 正在处理 A。用户发送 B,然后 C。
- A 完成后,B 被丢弃;只处理 C。
- 适合用户在等待过程中修正或完善问题的场景。
- 示例:“搜索 X” 然后 “还是搜索 Y 吧” – 只处理 Y。
Interrupt
只保留最新的排队消息并取消正在运行的 Agent。
- Agent 正在处理 A。用户发送 B,然后 C。
- A 被取消,B 被丢弃,C 立即开始处理。
- 对方向修正的响应最快。
- 当响应速度比完成当前任务更重要时使用。
注意: 当前 Interrupt 和 Steer 共享相同的排空并丢弃行为。不存在飞行中的 Agent 取消——正在运行的 Agent 会在处理最新消息之前完成。真正的飞行中取消功能正在计划中。
Speculative
为每条新消息生成并发的溢出 Agent,同时主 Agent 继续运行。
- Agent 正在处理 A。用户发送 B,然后 C。
- B 和 C 各自获得独立的并发 Agent 任务(溢出)。
- 三者并行运行 – 无阻塞。
- 最适合 LLM 提供商响应较慢、用户不想等待的场景。
- 溢出 Agent 使用主 Agent 启动前的会话历史快照。
溢出机制的工作原理
- 为第一条消息生成主 Agent。
- 主 Agent 运行期间,新消息到达收件箱。
- 每条新消息触发
serve_overflow(),生成一个拥有独立流式输出气泡的完整 Agent 任务。 - 溢出 Agent 使用主 Agent 启动前的历史快照,避免重复回答主问题。
- 所有 Agent 并发运行,结果保存到会话历史中。
已知限制
- 交互式提示在溢出中无法正常工作:如果 LLM 提出后续问题并返回 EndTurn,溢出 Agent 会退出。用户的回复会生成一个新的溢出,但没有上下文来理解之前的问题。
- 短回复可能被误分类:“是“或“2“这样的继续确认可能被当作独立的新查询处理。
自动升级
当检测到持续的延迟恶化时,会话 actor 可以自动从 Followup 升级到 Speculative:
ResponsivenessObserver从前 5 次请求中学习中位数基线(对异常值更鲁棒),然后在 20 样本的滚动窗口中跟踪 LLM 响应时间。基线每 20 个样本通过 80/20 EMA 混合当前窗口中位数进行自适应调整,可跟踪渐进漂移。- 如果连续 3 次响应超过 3×基线 延迟,同时自动激活 Speculative 队列模式和对冲竞速(Hedge)。
- 发送用户通知:“检测到响应缓慢,已启用对冲竞速 + 投机队列。”
- 当提供商恢复(一次正常延迟的响应)时,两者都恢复为 Followup 和静态路由。
- API 通道(Web 客户端)也会触发自动升级,因为它始终使用投机处理路径。
队列命令
/queue -- 显示当前模式
/queue followup -- 顺序处理
/queue collect -- 批量合并排队消息
/queue steer -- 只保留最新消息
/queue interrupt -- 取消当前任务 + 保留最新消息
/queue speculative -- 并发溢出 Agent
钩子
钩子是用于执行 LLM 策略、记录指标和审计 Agent 行为的主要扩展点 – 按配置文件定义,无需修改核心代码。
钩子是在 Agent 生命周期事件触发时运行的 shell 命令。每个钩子通过 stdin 接收 JSON 载荷,通过退出码传达决策。
退出码
| 退出码 | 含义 | Before 事件 | After 事件 |
|---|---|---|---|
| 0 | 允许 | 操作继续执行 | 记录为成功 |
| 1 | 拒绝 | 操作被阻止(原因输出到 stdout) | 视为错误 |
| 2+ | 错误 | 记录日志,操作继续执行 | 记录日志 |
事件
四个生命周期事件,每个都有特定的载荷:
before_tool_call
在每次工具执行前触发。可以拒绝(exit 1)。
{
"event": "before_tool_call",
"tool_name": "shell",
"arguments": {"command": "ls -la"},
"tool_id": "call_abc123",
"session_id": "telegram:12345",
"profile_id": "my-bot"
}
after_tool_call
在每次工具执行后触发。仅用于观测。
{
"event": "after_tool_call",
"tool_name": "shell",
"tool_id": "call_abc123",
"result": "file1.txt\nfile2.txt\n...",
"success": true,
"duration_ms": 142,
"session_id": "telegram:12345",
"profile_id": "my-bot"
}
注意:result 被截断到 500 个字符。
before_llm_call
在每次 LLM API 调用前触发。可以拒绝(exit 1)。
{
"event": "before_llm_call",
"model": "deepseek-chat",
"message_count": 12,
"iteration": 3,
"session_id": "telegram:12345",
"profile_id": "my-bot"
}
after_llm_call
在每次成功的 LLM 响应后触发。仅用于观测。
{
"event": "after_llm_call",
"model": "deepseek-chat",
"iteration": 3,
"stop_reason": "EndTurn",
"has_tool_calls": false,
"input_tokens": 1200,
"output_tokens": 350,
"provider_name": "deepseek",
"latency_ms": 2340,
"cumulative_input_tokens": 5600,
"cumulative_output_tokens": 1800,
"session_cost": 0.0042,
"response_cost": 0.0012,
"session_id": "telegram:12345",
"profile_id": "my-bot"
}
钩子配置
在 config.json 或按配置文件的 JSON 中:
{
"hooks": [
{
"event": "before_tool_call",
"command": ["python3", "~/.octos/hooks/guard.py"],
"timeout_ms": 3000,
"tool_filter": ["shell", "write_file"]
},
{
"event": "after_llm_call",
"command": ["python3", "~/.octos/hooks/cost-tracker.py"],
"timeout_ms": 5000
}
]
}
| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
event | 是 | – | 四种事件类型之一 |
command | 是 | – | argv 数组(不经过 shell 解释) |
timeout_ms | 否 | 5000 | 超时后终止钩子进程 |
tool_filter | 否 | 全部 | 仅对这些工具名称触发(仅限工具事件) |
同一事件可以注册多个钩子。它们按顺序执行;第一个拒绝即生效。
熔断器
钩子在连续 3 次失败(超时、崩溃或退出码 2+)后会被自动禁用。一次成功执行(exit 0 或拒绝 exit 1)即可重置计数器。
安全性
- 命令使用 argv 数组 – 不经过 shell 解释。
- 18 个危险环境变量会被移除(
LD_PRELOAD、DYLD_*、NODE_OPTIONS等)。 - 支持波浪号展开(
~/和~username/)。
按配置文件的钩子
每个配置文件可以通过配置中的 hooks 字段定义自己的钩子。这允许不同的频道或机器人使用不同的策略。钩子变更需要重启网关。
向后兼容性
- 载荷中可能会添加新字段。
- 现有字段永远不会被移除或重命名。
- 钩子脚本应忽略未知字段(标准 JSON 实践)。
示例:费用预算控制器
#!/usr/bin/env python3
"""Deny LLM calls when session cost exceeds $1.00."""
import json, sys
payload = json.load(sys.stdin)
if payload.get("event") == "before_llm_call":
try:
with open("/tmp/octos-cost.json") as f:
state = json.load(f)
except FileNotFoundError:
state = {}
sid = payload.get("session_id", "default")
if state.get(sid, 0) > 1.0:
print(f"Session cost exceeded $1.00 (${state[sid]:.4f})")
sys.exit(1)
elif payload.get("event") == "after_llm_call":
cost = payload.get("session_cost")
if cost is not None:
sid = payload.get("session_id", "default")
try:
with open("/tmp/octos-cost.json") as f:
state = json.load(f)
except FileNotFoundError:
state = {}
state[sid] = cost
with open("/tmp/octos-cost.json", "w") as f:
json.dump(state, f)
sys.exit(0)
示例:审计日志记录器
#!/usr/bin/env python3
"""Log all tool and LLM calls to a JSONL file."""
import json, sys, datetime
payload = json.load(sys.stdin)
payload["timestamp"] = datetime.datetime.utcnow().isoformat()
with open("/var/log/octos-audit.jsonl", "a") as f:
f.write(json.dumps(payload) + "\n")
sys.exit(0)
沙箱
Shell 命令在沙箱中运行以实现隔离。支持三种后端:
| 后端 | 平台 | 隔离方式 | 网络控制 |
|---|---|---|---|
| bwrap | Linux | 只读绑定 /usr,/lib,/bin,/sbin,/etc;读写绑定工作目录;tmpfs /tmp;unshare-pid | 禁止网络时使用 --unshare-net |
| macOS | macOS | 使用 SBPL 配置的 sandbox-exec:process-exec/fork、file-read*、工作目录 + /private/tmp 写入 | (allow network*) 或 (deny network*) |
| Docker | 任意平台 | --rm --security-opt no-new-privileges --cap-drop ALL | 禁止网络时使用 --network none |
在 config.json 中配置:
{
"sandbox": {
"enabled": true,
"mode": "auto",
"allow_network": false,
"docker": {
"image": "alpine:3.21",
"mount_mode": "rw",
"cpu_limit": "1.0",
"memory_limit": "512m",
"pids_limit": 100
}
}
}
- 模式:
auto(自动检测最佳可用方案)、bwrap、macos、docker、none。 - 挂载模式:
rw(读写)、ro(只读)、none(不挂载工作区)。 - Docker 资源限制:
--cpus、--memory、--pids-limit。 - Docker 绑定挂载安全:
docker.sock、/proc、/sys、/dev和/etc被阻止作为绑定挂载源。 - 路径验证:Docker 拒绝
:、\0、\n、\r;macOS 拒绝控制字符、(、)、\、"。 - 环境变量清理:18 个危险环境变量在所有沙箱后端、MCP 服务器启动、钩子和浏览器工具中自动清除:
LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, DYLD_FALLBACK_LIBRARY_PATH, DYLD_VERSIONED_LIBRARY_PATH, NODE_OPTIONS, PYTHONSTARTUP, PYTHONPATH, PERL5OPT, RUBYOPT, RUBYLIB, JAVA_TOOL_OPTIONS, BASH_ENV, ENV, ZDOTDIR。 - 进程清理:Shell 工具在超时时发送 SIGTERM,等待宽限期后发送 SIGKILL 清理子进程。
会话管理
会话分支
发送 /new 创建一个分支对话:
/new
这会创建一个新会话,复制当前对话的最近 10 条消息。子会话通过 parent_key 引用原始会话。每个分支获得一个由发送者和时间戳组成的唯一键。
会话持久化
每个 channel:chat_id 对维护各自独立的会话(对话历史)。
- 存储:
.octos/sessions/中的 JSONL 文件 - 最大历史:通过
gateway.max_history配置(默认:50 条消息) - 会话分支:
/new创建带有 parent_key 追踪的分支对话
配置热重载
网关自动检测配置文件变更:
- 可热重载(无需重启):系统提示、AGENTS.md、SOUL.md、USER.md
- 需要重启:提供商、模型、API 密钥、网关频道
变更通过 SHA-256 哈希和防抖机制检测。
消息合并
长响应在发送前会自动拆分为适合频道的分块:
| 频道 | 每条消息最大字符数 |
|---|---|
| Telegram | 4000 |
| Discord | 1900 |
| Slack | 3900 |
拆分优先级:段落分隔 > 换行符 > 句号结尾 > 空格 > 硬截断。超过 50 块的消息会被截断并添加标记。
上下文压缩
当对话超出 LLM 的上下文窗口时,较旧的消息会被自动压缩:
- 工具参数被剥离(替换为
"[stripped]") - 消息被摘要为首行内容
- 最近的工具调用/结果对完整保留
- Agent 无缝继续,不会丢失关键上下文
聊天内命令
斜杠命令
| 命令 | 说明 |
|---|---|
/new | 分支对话(创建复制最近 10 条消息的新会话) |
/config | 查看和修改工具配置 |
/queue | 查看或更改队列模式 |
/exit、/quit、:q | 退出聊天(仅 CLI 模式) |
聊天中切换提供商
switch_model 工具允许用户通过自然对话列出可用的 LLM 提供商并在运行时切换模型。此工具仅在网关模式下可用。
列出可用提供商:
User: What models are available?
Bot: Current model: deepseek/deepseek-chat
Available providers:
- anthropic (default: claude-sonnet-4-20250514) [ready]
- openai (default: gpt-4o) [ready]
- deepseek (default: deepseek-chat) [ready]
- gemini (default: gemini-2.0-flash) [ready]
...
切换模型:
User: Switch to GPT-4o
Bot: Switched to openai/gpt-4o.
Previous model (deepseek/deepseek-chat) is kept as fallback.
切换模型时,之前的模型自动成为备选:
- 如果新模型失败(限流、服务器错误),请求自动回退到原始模型。
- 回退使用熔断器机制(连续 3 次失败触发故障转移)。
- 链式结构始终扁平:
[new_model, original_model]– 反复切换不会嵌套。
模型切换持久化到配置文件 JSON。网关重启时,机器人以最后选择的模型启动。
记忆系统
Agent 在会话间维护长期记忆:
MEMORY.md– 持久化笔记,始终加载到上下文中- 每日笔记 –
.octos/memory/YYYY-MM-DD.md,自动创建 - 近期记忆 – 最近 7 天的每日笔记包含在上下文中
- 片段记忆 – 任务完成摘要存储在
episodes.redb中
混合记忆搜索
记忆搜索结合了 BM25(关键词)和向量(语义)评分:
- 排名:
alpha * vector_score + (1 - alpha) * bm25_score(默认 alpha:0.7) - 索引:使用 L2 归一化嵌入的 HNSW
- 降级方案:未配置嵌入提供商时仅使用 BM25
配置嵌入提供商以启用向量搜索:
{
"embedding": {
"provider": "openai"
}
}
嵌入配置支持三个字段:provider(默认:"openai")、api_key_env(可选覆盖)和 base_url(可选自定义端点)。
定时任务(Cron Jobs)
Agent 可以使用 cron 工具调度周期性任务:
User: Schedule a daily news digest at 8am Beijing time
Bot: Created cron job "daily-news" running at 8:00 AM Asia/Shanghai every day.
Expression: 0 0 8 * * * *
定时任务也可以通过 CLI 管理:
octos cron list # 列出活跃任务
octos cron list --all # 包含已禁用的
octos cron add --name "report" --message "Generate daily report" --cron "0 0 9 * * * *"
octos cron add --name "check" --message "Check status" --every 3600
octos cron remove <job-id>
octos cron enable <job-id>
octos cron enable <job-id> --disable
Web 仪表板
REST API 服务器包含一个内嵌的 Web 界面:
octos serve # 绑定到 127.0.0.1:8080
octos serve --host 0.0.0.0 --port 3000 # 接受外部连接
# 打开 http://localhost:8080
功能:
- 会话侧边栏
- 聊天界面
- SSE 流式推送
- 暗色主题
/metrics 端点提供 Prometheus 格式的指标:
octos_tool_calls_totaloctos_tool_call_duration_secondsoctos_llm_tokens_total
运维操作
本章涵盖日常运维任务:升级、凭据管理和服务管理。
升级
拉取最新源码并重新构建:
cd octos
git pull origin main
./scripts/local-tenant-deploy.sh --full # Rebuilds and reinstalls
如果以服务方式运行,升级后需要重启:
# macOS (launchd):
launchctl unload ~/Library/LaunchAgents/io.octos.octos-serve.plist
launchctl load ~/Library/LaunchAgents/io.octos.octos-serve.plist
# Linux (systemd):
systemctl --user restart octos-serve
钥匙串集成
Octos 支持将 API 密钥存储在 macOS 钥匙串中,而不是以明文形式存放在配置文件的 JSON 中。这在 Apple Silicon 上提供硬件级加密和操作系统级别的访问控制。
架构
+------------------------------+
octos auth set-key | macOS Keychain |
-----------------> | (AES encrypted, per-user) |
| |
| service: "octos" |
| account: "OPENAI_API_KEY" |
| password: "sk-proj-abc..." |
+---------------+--------------+
| get_password()
Profile JSON |
+------------------+ v
| env_vars: { | resolve_env_vars()
| "OPENAI_API_ | if "keychain:" ->
| KEY": | lookup from Keychain
| "keychain:" | else -> use literal
| } |
+------------------+ |
v
Gateway process
解析链:配置文件中的 "keychain:" 标记触发钥匙串查找(3 秒超时)。如果钥匙串不可用,该密钥会被跳过并输出警告。
向后兼容:env_vars 中的字面值直接透传。无需迁移 – 可以按需逐个密钥切换到钥匙串。完全支持明文和钥匙串条目混合使用。
CLI 命令
# 为 SSH 会话解锁钥匙串(通过 SSH 使用 set-key 前必须执行)
octos auth unlock --password <login-password>
octos auth unlock # interactive prompt
# 将密钥存入钥匙串并更新配置文件使用 keychain 标记
octos auth set-key OPENAI_API_KEY sk-proj-abc123
octos auth set-key OPENAI_API_KEY # interactive prompt
# 指定配置文件
octos auth set-key GEMINI_API_KEY AIzaSy... -p my-profile
# 列出所有密钥及其存储状态
octos auth keys
octos auth keys -p my-profile
# 从钥匙串移除并清理配置文件
octos auth remove-key OPENAI_API_KEY
钥匙串条目格式
- Service:
octos(所有条目使用相同常量) - Account:环境变量名(例如
OPENAI_API_KEY) - Password:实际的密钥值
验证方法:
security find-generic-password -s octos -a OPENAI_API_KEY -w
SSH 和无头服务器设置
macOS 钥匙串绑定到 GUI 登录会话。SSH 会话无法访问已锁定的钥匙串 – macOS 会尝试弹出对话框,在无头服务器上这会导致卡死。
为什么 SSH 默认无法访问:macOS securityd 按会话解锁钥匙串。GUI 会话的解锁不会自动传播到 SSH 会话。
解决方案:解锁钥匙串并禁用自动锁定。每次启动执行一次(或加入部署脚本):
ssh user@<host>
# 解锁钥匙串(需要登录密码)
octos auth unlock --password <login-password>
# 完成 -- 自动锁定已自动禁用。
# 钥匙串保持解锁状态直到重启。
# 自动登录会在重启时重新解锁。
或使用原生 security 命令:
# 解锁
security unlock-keychain -p '<password>' ~/Library/Keychains/login.keychain-db
# 禁用自动锁定计时器(防止空闲后重新锁定)
security set-keychain-settings ~/Library/Keychains/login.keychain-db
常见问题:
| 现象 | 原因 | 解决方法 |
|---|---|---|
| “User interaction is not allowed” | 钥匙串已锁定(SSH 会话) | octos auth unlock --password <pw> |
| 钥匙串查找超时(3 秒) | 钥匙串已锁定(LaunchAgent) | 启用自动登录,重启 |
| “keychain marker found but no secret” | 密钥未存储或使用了错误的钥匙串 | 解锁后重新执行 octos auth set-key |
| 网关启动时卡住 | 钥匙串查找阻塞 | 更新到最新的 octos 二进制文件 |
安全性对比
| 威胁场景 | 明文 JSON | 钥匙串 |
|---|---|---|
| 文件被窃取(备份、git、scp) | 所有密钥暴露 | 只能看到 "keychain:" 标记 |
| 恶意软件读取磁盘 | 简单文件读取即可获取密钥 | 必须绕过操作系统钥匙串 ACL |
| 机器上的其他用户 | 文件权限有一定保护,root 可读 | 按用户加密 |
| 进程内存转储 | 密钥在环境变量中 | 密钥仅短暂存在于内存中 |
| 意外日志输出 | 配置文件 JSON 泄露密钥 | 仅记录引用字符串 |
服务器部署建议
macOS 钥匙串是为桌面交互使用设计的。在无头服务器上,它会引入可靠性问题。请根据部署类型选择凭据存储方式:
| 部署场景 | 推荐存储方式 | 原因 |
|---|---|---|
| 开发者笔记本 | 钥匙串("keychain:") | GUI 会话保持钥匙串解锁;ACL 弹窗可以接受 |
| 自动登录 + GUI 的 Mac | 钥匙串("keychain:") | 如果通过屏幕共享批准过 ACL 对话框则可用 |
| 无头 Mac(仅 SSH) | env_vars 或 launchd plist 中的明文 | 最可靠;无解锁/ACL 依赖 |
| Linux 服务器 | 环境变量中的明文 | 没有 macOS 钥匙串 |
为什么钥匙串在无头服务器上不可靠:
- 需要 macOS 登录密码 – 通过 SSH 解锁钥匙串需要用户的登录密码存储在某处,降低了安全收益。
- 重启/休眠后重新锁定 – 启动
octos serve的 LaunchAgent 在 GUI 登录之前运行,此时钥匙串处于锁定状态。 - 空闲超时后重新锁定 – 即使解锁后,macOS 也可能重新锁定。
set-keychain-settings的变通方案可能被 macOS 更新重置。 - ACL 弹窗阻断无头访问 – 如果二进制文件不是最初存储密钥的那个,macOS 可能弹出一个无法回答的 GUI 对话框。
- 会话隔离 – 从 SSH 解锁不会解锁 LaunchAgent 会话的钥匙串,反之亦然。
服务器的明文设置:
{
"env_vars": {
"OPENAI_API_KEY": "sk-proj-abc123",
"SMTP_PASSWORD": "xxxx xxxx xxxx xxxx",
"SMTP_HOST": "smtp.gmail.com",
"SMTP_PORT": "587",
"SMTP_USERNAME": "user@gmail.com",
"SMTP_FROM": "user@gmail.com"
}
}
使用文件系统权限保护文件:
chmod 600 ~/.octos/profiles/*.json
chmod 600 ~/Library/LaunchAgents/io.octos.octos-serve.plist
服务管理
macOS (launchd)
创建 LaunchAgent plist 将 octos 作为持久服务运行:
# 加载服务
launchctl load ~/Library/LaunchAgents/io.octos.octos-serve.plist
# 卸载服务
launchctl unload ~/Library/LaunchAgents/io.octos.octos-serve.plist
# 检查状态
launchctl list | grep octos
如果服务需要环境变量(例如 SMTP 凭据),将其添加到 plist 中:
<key>EnvironmentVariables</key>
<dict>
<key>SMTP_PASSWORD</key>
<string>xxxx xxxx xxxx xxxx</string>
</dict>
日志位于 ~/.octos/serve.log。
Linux (systemd)
使用 systemd 用户单元管理服务:
# 启动 / 停止 / 重启
systemctl --user start octos-serve
systemctl --user stop octos-serve
systemctl --user restart octos-serve
# 设置开机自启
systemctl --user enable octos-serve
# 查看状态和日志
systemctl --user status octos-serve
journalctl --user -u octos-serve
故障排查
本章按类别整理了常见问题及其解决方案,以及环境变量参考。
API 与提供商问题
API 密钥未设置
Error: ANTHROPIC_API_KEY environment variable not set
解决方法:在 shell 中导出密钥或通过 octos status 验证:
export ANTHROPIC_API_KEY="your-key"
如果以服务方式运行,确保环境变量设置在服务环境中(launchd plist 或 systemd 单元),而不仅仅是交互式 shell。
限流 (429)
重试机制会自动处理(3 次尝试,指数退避)。如果错误持续:
- 尝试通过
/queue或聊天中的模型切换功能切换到其他提供商。 - 等待限流窗口重置。
调试日志
启用详细日志以诊断问题:
RUST_LOG=debug octos chat
RUST_LOG=octos_agent=trace octos chat --message "task"
构建问题
| 问题 | 解决方案 |
|---|---|
| Linux 上构建失败 | 安装构建依赖:sudo apt install build-essential pkg-config libssl-dev |
| macOS 代码签名警告 | 签名二进制文件:codesign -s - ~/.cargo/bin/octos |
octos: command not found | 将 cargo bin 添加到 PATH:export PATH="$HOME/.cargo/bin:$PATH" |
频道特定问题
Lark / 飞书
| 问题 | 解决方案 |
|---|---|
| WebSocket 端点返回 404 | Larksuite 国际版不支持 WebSocket 模式。在配置中使用 "mode": "webhook" |
| Challenge 验证失败 | 确保隧道(如 ngrok)正在运行且 URL 与飞书控制台中配置的一致 |
| 未收到事件 | 添加事件后需要发布应用版本。在控制台中检查事件日志检索 |
| 机器人不回复 | 检查是否已授予 im:message:send_as_bot 权限 |
| Markdown 未渲染 | 消息以交互卡片发送;飞书支持 Markdown 的一个子集 |
| 隧道 URL 变更 | 免费隧道 URL 在重启后会变化。在飞书控制台中更新请求 URL |
企业微信
“Environment variable WECOM_BOT_SECRET not set”
启动网关前设置密钥:
export WECOM_BOT_SECRET="your_secret"
连接断开或无法订阅
- 验证
bot_id和密钥是否正确。 - 检查到
wss://openws.work.weixin.qq.com的网络连通性。 - 频道最多自动重连 100 次(指数退避)。查看日志获取错误详情。
消息未到达
- 确认上游中继服务正在运行且已关联到你的账号。
- 检查企业微信群机器人是否与 octos 中配置的一致。
- 如果使用了
allowed_senders,验证发送者的企业微信用户 ID 是否在列表中。 - 检查重复消息过滤 – 频道会对最近 1000 条消息 ID 去重。
长消息被截断
超过 4096 字符的消息会被 octos 自动拆分为多个分块。如果仍有截断,检查中继服务本身的消息长度设置。
平台特定问题
| 问题 | 解决方案 |
|---|---|
| 仪表板无法访问 | 检查端口:octos serve --port 8080,打开 http://localhost:8080/admin/ |
| WSL2 端口未转发 | 重启 WSL:wsl --shutdown 然后重新打开终端 |
| 服务无法启动 | 查看日志:tail -f ~/.octos/serve.log(macOS)或 journalctl --user -u octos-serve(Linux) |
Windows: 找不到 octos | 确保 %USERPROFILE%\.cargo\bin 在 PATH 中 |
| Windows: shell 命令失败 | 命令通过 cmd /C 执行;使用 Windows 兼容的语法 |
环境变量参考
| 变量 | 说明 |
|---|---|
ANTHROPIC_API_KEY | Anthropic API 密钥 |
OPENAI_API_KEY | OpenAI API 密钥 |
GEMINI_API_KEY | Gemini API 密钥 |
OPENROUTER_API_KEY | OpenRouter API 密钥 |
DEEPSEEK_API_KEY | DeepSeek API 密钥 |
GROQ_API_KEY | Groq API 密钥 |
MOONSHOT_API_KEY | Moonshot API 密钥 |
DASHSCOPE_API_KEY | DashScope API 密钥 |
MINIMAX_API_KEY | MiniMax API 密钥 |
ZHIPU_API_KEY | 智谱 API 密钥 |
ZAI_API_KEY | Z.AI API 密钥 |
NVIDIA_API_KEY | Nvidia NIM API 密钥 |
OMINIX_API_URL | 本地 ASR/TTS API 地址 |
RUST_LOG | 日志级别(error / warn / info / debug / trace) |
TELEGRAM_BOT_TOKEN | Telegram 机器人令牌 |
DISCORD_BOT_TOKEN | Discord 机器人令牌 |
SLACK_BOT_TOKEN | Slack 机器人令牌 |
SLACK_APP_TOKEN | Slack 应用级令牌 |
FEISHU_APP_ID | 飞书应用 ID |
FEISHU_APP_SECRET | 飞书应用密钥 |
EMAIL_USERNAME | 邮箱账户用户名 |
EMAIL_PASSWORD | 邮箱账户密码 |
WECOM_CORP_ID | 企业微信企业 ID |
WECOM_AGENT_SECRET | 企业微信应用密钥 |
CLI 命令参考
octos chat
交互式多轮对话,支持 readline 历史记录。
octos chat [OPTIONS]
Options:
-c, --cwd <PATH> 工作目录
--config <PATH> 配置文件路径
--provider <NAME> LLM 供应商
--model <NAME> 模型名称
--base-url <URL> 自定义 API 端点
-m, --message <MSG> 单条消息(非交互模式)
--max-iterations <N> 每条消息的最大工具迭代次数(默认:50)
-v, --verbose 显示工具输出
--no-retry 禁用重试
功能特性:
- 方向键和行编辑(rustyline)
- 持久化历史记录,保存在
.octos/history/chat_history - 退出方式:
/exit、/quit、exit、quit、:q、Ctrl+C、Ctrl+D - 完整工具访问(Shell、文件、搜索、Web)
示例:
octos chat # 交互模式(默认)
octos chat --provider deepseek # 使用 DeepSeek
octos chat --model glm-4-plus # 自动识别为智谱
octos chat --message "Fix auth bug" # 单条消息,执行后退出
octos gateway
以常驻多渠道守护进程方式运行。
octos gateway [OPTIONS]
Options:
-c, --cwd <PATH> 工作目录
--config <PATH> 配置文件路径
--provider <NAME> 覆盖供应商
--model <NAME> 覆盖模型
--base-url <URL> 覆盖 API 端点
-v, --verbose 详细日志
--no-retry 禁用重试
需要在配置文件中包含 gateway 部分及 channels 数组。持续运行直至按下 Ctrl+C。
octos init
初始化工作区,创建配置和引导文件。
octos init [OPTIONS]
Options:
-c, --cwd <PATH> 工作目录
--defaults 跳过交互提示,使用默认值
创建内容:
.octos/config.json– 供应商/模型配置.octos/.gitignore– 忽略状态文件.octos/AGENTS.md– 智能体指令模板.octos/SOUL.md– 个性模板.octos/USER.md– 用户信息模板.octos/memory/– 记忆存储目录.octos/sessions/– 会话历史目录.octos/skills/– 自定义技能目录
octos status
显示系统状态。
octos status [OPTIONS]
Options:
-c, --cwd <PATH> 工作目录
输出示例:
octos Status
══════════════════════════════════════════════════
Config: .octos/config.json (found)
Workspace: .octos/ (found)
Provider: anthropic
Model: claude-sonnet-4-20250514
API Keys
──────────────────────────────────────────────────
Anthropic ANTHROPIC_API_KEY set
OpenAI OPENAI_API_KEY not set
...
Bootstrap Files
──────────────────────────────────────────────────
AGENTS.md found
SOUL.md found
USER.md found
TOOLS.md missing
IDENTITY.md missing
octos serve
启动 Web 界面和 REST API 服务器。需要在编译时启用 api 特性。
cargo install --path crates/octos-cli --features api
octos serve # 绑定到 127.0.0.1:8080
octos serve --host 0.0.0.0 --port 3000 # 接受外部连接
功能包括:会话侧栏、聊天界面、SSE 流式传输、暗色主题。/metrics 端点提供 Prometheus 格式的指标(octos_tool_calls_total、octos_tool_call_duration_seconds、octos_llm_tokens_total)。
octos clean
清理数据库和状态文件。
octos clean [--all] [--dry-run]
| 参数 | 说明 |
|---|---|
--all | 移除所有状态文件 |
--dry-run | 仅显示将被删除的内容,不实际执行 |
octos completions
生成 Shell 自动补全脚本。
octos completions <shell>
支持的 Shell:bash、zsh、fish、powershell。
octos cron
管理定时任务。
octos cron list [--all] # 列出活跃任务(--all 包含已禁用的)
octos cron add [OPTIONS] # 添加定时任务
octos cron remove <job-id> # 移除定时任务
octos cron enable <job-id> # 启用定时任务
octos cron enable <job-id> --disable # 禁用定时任务
添加任务:
octos cron add --name "report" --message "Generate daily report" --cron "0 0 9 * * * *"
octos cron add --name "check" --message "Check status" --every 3600
octos cron add --name "once" --message "Run migration" --at "2025-03-01T09:00:00Z"
Cron 表达式使用标准语法。任务支持可选的 timezone 字段,使用 IANA 时区名称(如 "America/New_York"、"Asia/Shanghai")。未指定时默认使用 UTC。
octos channels
管理消息渠道。
octos channels status # 显示渠道的编译/配置状态
octos channels login # WhatsApp 二维码登录
status 命令会显示一张表格,包含渠道名称、编译状态(特性标志)和配置摘要(环境变量的设置/缺失情况)。
octos office
Office 文件操作(DOCX/PPTX/XLSX)。基本操作使用原生 Rust 实现,无需外部依赖。
octos office extract <file> # 提取文本为 Markdown
octos office unpack <file> <output-dir> # 解包为格式化的 XML
octos office pack <input-dir> <output> # 将目录打包为 Office 文件
octos office clean <dir> # 清理解包后 PPTX 中的孤立文件
octos account
管理 Profile 下的子账户。子账户继承 LLM 供应商配置,但拥有独立的数据目录(记忆、会话、技能)和渠道。
octos account list --profile <id> # 列出子账户
octos account create --profile <id> <name> [OPTIONS] # 创建子账户
octos account update <id> [OPTIONS] # 更新子账户
octos auth
OAuth 登录和 API 密钥管理。
octos auth login --provider openai # PKCE 浏览器 OAuth
octos auth login --provider openai --device-code # 设备码流程
octos auth login --provider anthropic # 粘贴令牌(标准输入)
octos auth logout --provider openai # 移除已存储的凭据
octos auth status # 显示已认证的供应商
凭据存储在 ~/.octos/auth.json(文件权限 0600)。解析 API 密钥时,优先检查凭据存储,其次才是环境变量。
octos skills
管理技能。
octos skills list # 列出已安装的技能
octos skills install user/repo/skill-name # 从 GitHub 安装
octos skills remove skill-name # 移除技能
从 GitHub 仓库的 main 分支获取 SKILL.md 并安装到 .octos/skills/。
Octos 应用技能开发指南
本指南涵盖了构建、注册和部署 octos 应用技能所需的全部内容。
架构概览
应用技能是一个独立的可执行二进制文件,通过简单的 stdin/stdout JSON 协议与 octos 网关通信。网关为每次工具调用将技能二进制文件作为子进程启动,通过 stdin 传递 JSON 参数,并从 stdout 读取 JSON 结果。
User message → LLM → tool_use("get_weather", {"city": "Paris"})
↓
Gateway spawns: ~/.octos/skills/weather/main get_weather
↓
Stdin: {"city": "Paris"}
Stdout: {"output": "Paris, France\nClear sky\n...", "success": true}
↓
LLM sees result → generates natural language response
技能目录结构
每个技能在 crates/app-skills/ 下有自己独立的 crate:
crates/app-skills/my-skill/
├── Cargo.toml # Crate 配置,二进制名称
├── manifest.json # 工具定义(JSON Schema)
├── SKILL.md # 文档 + frontmatter 元数据
└── src/
└── main.rs # 二进制入口
启动引导后,技能安装在:
~/.octos/skills/my-skill/
├── main # 可执行二进制文件(从 target/ 复制)
├── manifest.json # 工具定义
└── SKILL.md # 文档
分步指南:创建新技能
1. 创建 Crate
mkdir -p crates/app-skills/my-skill/src
2. Cargo.toml
[package]
name = "my-skill"
version = "1.0.0"
edition = "2021"
description = "Short description of what this skill does"
authors = ["your-name"]
[[bin]]
name = "my_skill" # Binary name (used in bundled_app_skills.rs)
path = "src/main.rs"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Add other deps as needed:
# reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"], default-features = false }
# chrono = "0.4"
重要: [[bin]] name 必须与 bundled_app_skills.rs 中的 binary_name 匹配。
3. manifest.json
定义 LLM 可调用的工具。使用 JSON Schema 进行输入验证。
{
"name": "my-skill",
"version": "1.0.0",
"author": "your-name",
"description": "What this skill does",
"timeout_secs": 15,
"requires_network": false,
"tools": [
{
"name": "my_tool",
"description": "Clear description for the LLM. What does this tool do? When should it be used?",
"input_schema": {
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "What this parameter means"
},
"param2": {
"type": "integer",
"description": "Optional numeric parameter (default: 10)"
}
},
"required": ["param1"]
}
}
]
}
清单字段:
| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
name | 是 | — | 技能标识符 |
version | 是 | — | 语义化版本 |
author | 否 | — | 作者名称 |
description | 否 | — | 可读的描述 |
timeout_secs | 否 | 30 | 每次工具调用的最大执行时间(1-600) |
requires_network | 否 | false | 信息性标志 |
sha256 | 否 | — | 二进制完整性校验(十六进制哈希) |
tools | 是 | — | 工具定义数组 |
工具定义字段:
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | 工具名称(snake_case,全局唯一) |
description | 是 | 展示给 LLM 的描述 – 明确说明何时使用 |
input_schema | 是 | 输入参数的 JSON Schema |
4. SKILL.md
带有 YAML frontmatter 的文档。LLM 通过阅读它来理解何时以及如何使用该技能。
---
name: my-skill
description: Short description. Triggers: keyword1, keyword2, 关键词, trigger phrase.
version: 1.0.0
author: your-name
always: false
---
# My Skill
Detailed description of what this skill does and when to use it.
## Tools
### my_tool
Explain what this tool does with examples.
\```json
{"param1": "example value", "param2": 5}
\```
**Parameters:**
- `param1` (required): What it means
- `param2` (optional): What it controls. Default: 10
Frontmatter 字段:
| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
name | 是 | — | 技能标识符 |
description | 是 | — | 一行描述。在 “Triggers:” 后面添加触发关键词 |
version | 是 | — | 语义化版本 |
author | 否 | — | 作者名称 |
always | 否 | false | 如果为 true,技能文档始终包含在系统提示中 |
requires_bins | 否 | — | 逗号分隔的二进制文件列表(通过 which 检查是否存在) |
requires_env | 否 | — | 逗号分隔的环境变量列表(必须已设置) |
触发关键词帮助 Agent 决定何时激活该技能。如果用户使用多种语言,请包含多语言的触发词。
5. src/main.rs
二进制文件实现 stdin/stdout 协议。
最小模板:
use std::io::Read;
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct MyToolInput {
param1: String,
#[serde(default = "default_param2")]
param2: i32,
}
fn default_param2() -> i32 { 10 }
fn main() {
let args: Vec<String> = std::env::args().collect();
let tool_name = args.get(1).map(|s| s.as_str()).unwrap_or("unknown");
let mut buf = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut buf) {
fail(&format!("Failed to read stdin: {e}"));
}
match tool_name {
"my_tool" => handle_my_tool(&buf),
_ => fail(&format!("Unknown tool '{tool_name}'. Expected: my_tool")),
}
}
fn fail(msg: &str) -> ! {
println!("{}", json!({"output": msg, "success": false}));
std::process::exit(1);
}
fn handle_my_tool(input_json: &str) {
let input: MyToolInput = match serde_json::from_str(input_json) {
Ok(v) => v,
Err(e) => fail(&format!("Invalid input: {e}")),
};
// ... your logic here ...
let result = format!("Processed {} with param2={}", input.param1, input.param2);
println!("{}", json!({"output": result, "success": true}));
}
协议规则:
- argv[1] = 工具名称(例如
get_weather、get_forecast) - stdin = 匹配工具
input_schema的 JSON 对象 - stdout = JSON 对象,包含:
output(字符串):人类可读的结果文本success(布尔值):成功为true,失败为false
- 退出码:成功为 0,失败为非零
- stderr:网关会忽略(可用于调试日志)
注册技能
6. 添加到工作区
在根目录的 Cargo.toml 中添加到 members:
[workspace]
members = [
# ... existing members ...
"crates/app-skills/my-skill",
]
7. 在 bundled_app_skills.rs 中注册
在 crates/octos-agent/src/bundled_app_skills.rs 中添加到 BUNDLED_APP_SKILLS:
#![allow(unused)]
fn main() {
pub const BUNDLED_APP_SKILLS: &[(&str, &str, &str, &str)] = &[
// ... existing skills ...
(
"my-skill", // dir_name (skill directory name)
"my_skill", // binary_name (must match [[bin]] name)
include_str!("../../app-skills/my-skill/SKILL.md"), // embedded docs
include_str!("../../app-skills/my-skill/manifest.json"), // embedded manifest
),
];
}
元组格式: (dir_name, binary_name, skill_md, manifest_json)
dir_name:~/.octos/skills/下的目录名binary_name:target/release/中的二进制文件名(必须与 Cargo.toml 中的[[bin]] name匹配)skill_md:嵌入的 SKILL.md 内容manifest_json:嵌入的 manifest.json 内容
构建与测试
8. 构建
# 只构建你的技能
cargo build -p my-skill
# 构建全部
cargo build --workspace
9. 独立测试
# 直接测试你的工具
echo '{"param1": "hello", "param2": 5}' | ./target/debug/my_skill my_tool
# 预期输出:
# {"output":"Processed hello with param2=5","success":true}
# 测试错误处理
echo '{}' | ./target/debug/my_skill my_tool
echo '{"param1": "test"}' | ./target/debug/my_skill unknown_tool
10. 网关集成测试
# 构建 release 版本并安装
cargo build --release --workspace
# 启动网关(技能自动引导加载)
octos gateway
# 检查技能是否已加载
ls ~/.octos/skills/my-skill/
# main manifest.json SKILL.md
# 让 Agent 使用你的技能
示例
示例 1:纯本地技能(时钟)
不需要网络,不需要环境变量。使用 chrono + chrono-tz。
crates/app-skills/time/
├── Cargo.toml # deps: chrono, chrono-tz, serde, serde_json
├── manifest.json # 1 tool: get_time, timeout_secs: 5
├── SKILL.md # Triggers: time, clock, 几点
└── src/main.rs # Reads system clock, formats with timezone
关键模式: 未指定时区时默认使用本地时间。
示例 2:网络技能(天气)
调用外部 API,需要网络。使用 reqwest(blocking)。
crates/app-skills/weather/
├── Cargo.toml # deps: reqwest (blocking, rustls-tls), serde, serde_json
├── manifest.json # 2 tools: get_weather, get_forecast, timeout_secs: 15
├── SKILL.md # Triggers: weather, forecast, 天气
└── src/main.rs # Geocode city → fetch weather from Open-Meteo
关键模式:
- 构建带超时的 HTTP 客户端
- 优雅处理 API 错误(返回
success: false) - 对用户输入进行 URL 编码
- 一个二进制文件中包含多个工具(根据
argv[1]匹配)
示例 3:需要环境变量的技能(发送邮件)
需要从环境变量获取凭据。
crates/app-skills/send-email/
├── Cargo.toml # deps: lettre, serde, serde_json, reqwest
├── manifest.json # 1 tool: send_email
├── SKILL.md # requires_env: SMTP_HOST,SMTP_USERNAME,SMTP_PASSWORD
└── src/main.rs # Reads SMTP_* env vars, sends via SMTP
关键模式: 尽早检查环境变量,用清晰的错误消息报错。
#![allow(unused)]
fn main() {
fn get_smtp_config() -> SmtpConfig {
let host = std::env::var("SMTP_HOST")
.unwrap_or_else(|_| fail("SMTP_HOST env var not set"));
// ...
}
}
清单扩展:MCP 服务器、钩子和提示片段
技能可以在 manifest.json 中声明的不仅仅是工具。三个额外的扩展点允许技能提供 MCP 服务器、生命周期钩子和系统提示内容。这些统称为 extras。
MCP 服务器
技能可以声明 MCP(Model Context Protocol)服务器,网关在技能加载时自动启动。这让技能可以通过 MCP 协议(而非或同时使用 stdin/stdout 二进制协议)暴露工具。
在 manifest.json 中添加 mcp_servers 数组:
{
"name": "my-skill",
"version": "1.0.0",
"tools": [],
"mcp_servers": [
{
"command": "node",
"args": ["mcp-server/index.js"],
"env": ["API_KEY", "API_SECRET"]
}
]
}
MCP 服务器字段:
| 字段 | 必填 | 说明 |
|---|---|---|
command | 否* | 启动 MCP 服务器进程的命令 |
args | 否 | 传递给命令的参数 |
env | 否 | 要转发的环境变量名称列表(非值) |
url | 否* | HTTP 传输:远程 MCP 服务器端点的 URL |
headers | 否 | HTTP 传输:附加请求头(键值对象) |
* command 和 url 必须设置其中一个。本地(stdio)MCP 服务器使用 command,远程(HTTP)MCP 服务器使用 url。
路径解析: 如果 command 以 ./ 或 ../ 开头,则相对于技能目录解析。裸命令(如 "node"、"python3")照常从 PATH 查找。
环境变量转发: env 数组包含环境变量的名称而非值。加载时,每个名称从进程环境中查找。只有实际设置的变量才会转发给 MCP 服务器进程。缺少的变量会被静默忽略。
示例:本地 stdio MCP 服务器
{
"mcp_servers": [
{
"command": "./bin/mcp-server",
"args": ["--port", "0"],
"env": ["DATABASE_URL"]
}
]
}
示例:远程 HTTP MCP 服务器
{
"mcp_servers": [
{
"url": "https://mcp.example.com/v1",
"headers": {
"Authorization": "Bearer ${API_KEY}"
}
}
]
}
生命周期钩子
技能可以声明生命周期钩子,在特定的 Agent 事件发生时运行 shell 命令。这适用于审计、策略执行或副作用。
在 manifest.json 中添加 hooks 数组:
{
"name": "my-audit-skill",
"version": "1.0.0",
"tools": [],
"hooks": [
{
"event": "after_tool_call",
"command": ["./hooks/audit.sh"],
"timeout_ms": 5000,
"tool_filter": ["shell"]
}
]
}
钩子字段:
| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
event | 是 | – | 生命周期事件名称(见下表) |
command | 是 | – | 作为 argv 数组的命令(不经过 shell 解释) |
timeout_ms | 否 | 5000 | 最大执行时间(毫秒) |
tool_filter | 否 | [](所有工具) | 仅对这些工具名称触发(仅限工具事件) |
支持的事件:
| 事件 | 可拒绝? | 触发时机 |
|---|---|---|
before_tool_call | 是 | 工具执行前。退出码 1 = 拒绝。 |
after_tool_call | 否 | 工具完成后(无论成功或失败)。 |
before_llm_call | 是 | 向 LLM 发送请求前。退出码 1 = 拒绝。 |
after_llm_call | 否 | 收到 LLM 响应后。 |
路径解析: command 数组的第一个元素(command[0])遵循与 MCP 服务器相同的规则 – 以 ./ 或 ../ 开头的路径相对于技能目录解析。其他元素原样传递。
钩子载荷: 网关通过 stdin 向钩子进程发送 JSON 载荷。工具事件的载荷包含 tool_name、arguments 和会话上下文。LLM 事件的载荷包含 model、message_count 等。
拒绝行为: before_* 钩子可以通过退出码 1 拒绝操作。钩子的 stdout 内容作为拒绝原因。
示例:审计所有 shell 工具调用
{
"hooks": [
{
"event": "before_tool_call",
"command": ["./hooks/policy-check.sh"],
"timeout_ms": 3000,
"tool_filter": ["shell", "bash"]
},
{
"event": "after_tool_call",
"command": ["./hooks/audit-log.sh"],
"timeout_ms": 5000,
"tool_filter": ["shell", "bash"]
}
]
}
提示片段
技能可以通过声明提示片段文件向系统提示中注入内容。这适用于向 Agent 传授特定领域的知识、规则或行为,无需编写任何代码。
在 manifest.json 中添加 prompts 对象:
{
"name": "my-style-guide",
"version": "1.0.0",
"tools": [],
"prompts": {
"include": ["prompts/*.md"]
}
}
提示字段:
| 字段 | 必填 | 说明 |
|---|---|---|
include | 是 | 要包含的文件的 glob 模式数组 |
路径解析: glob 模式相对于技能目录解析。例如,"prompts/*.md" 匹配技能目录下 prompts/ 子目录中的所有 .md 文件。
行为: 匹配的文件在加载时读取,其内容追加到系统提示中。文件按 glob 展开顺序处理。
示例:技能目录布局
~/.octos/skills/my-style-guide/
├── manifest.json
├── SKILL.md
└── prompts/
├── coding-rules.md
└── review-checklist.md
对应的清单:
{
"name": "my-style-guide",
"version": "1.0.0",
"tools": [],
"prompts": {
"include": ["prompts/*.md"]
}
}
当该技能激活时,coding-rules.md 和 review-checklist.md 都会被注入到系统提示中。
纯扩展技能
技能不需要提供任何可执行工具。如果 manifest.json 的 tools 数组为空(或完全省略),但声明了 mcp_servers、hooks 或 prompts,网关会加载扩展而不寻找二进制文件。这适用于:
- 纯提示注入技能 – 一组
.md文件,向 Agent 传授某个领域的知识 - 配置技能 – 对所有工具调用执行策略的钩子
- 远程 MCP 技能 – 运行在其他地方、通过
url声明的 MCP 服务器
示例:纯提示技能
{
"name": "company-policy",
"version": "1.0.0",
"prompts": {
"include": ["prompts/*.md"]
}
}
没有 tools、没有二进制文件、没有 mcp_servers、没有 hooks – 只有提示内容。
示例:纯钩子技能
{
"name": "audit-logger",
"version": "1.0.0",
"hooks": [
{
"event": "after_tool_call",
"command": ["./hooks/log-to-siem.sh"],
"timeout_ms": 5000
}
]
}
没有工具 – 技能只提供审计钩子。
示例:组合扩展
一个技能可以在常规工具之外同时声明全部三种扩展:
{
"name": "advanced-skill",
"version": "1.0.0",
"tools": [
{ "name": "analyze", "description": "Run analysis", "input_schema": { "type": "object" } }
],
"mcp_servers": [
{ "command": "node", "args": ["mcp/server.js"], "env": ["API_KEY"] }
],
"hooks": [
{ "event": "after_tool_call", "command": ["./hooks/audit.sh"], "tool_filter": ["analyze"] }
],
"prompts": {
"include": ["prompts/*.md"]
}
}
完整清单字段参考
manifest.json 顶层字段的完整集合,包含扩展:
| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
name | 是 | – | 技能标识符 |
version | 是 | – | 语义化版本 |
author | 否 | – | 作者名称 |
description | 否 | – | 可读的描述 |
timeout_secs | 否 | 30 | 每次工具调用的最大执行时间(1-600) |
requires_network | 否 | false | 信息性标志 |
sha256 | 否 | – | 二进制完整性校验(十六进制哈希) |
tools | 否 | [] | 工具定义数组 |
mcp_servers | 否 | [] | MCP 服务器声明数组 |
hooks | 否 | [] | 生命周期钩子定义数组 |
prompts | 否 | – | 提示片段配置对象 |
binaries | 否 | {} | 按 {os}-{arch} 索引的预编译二进制文件 |
进阶主题
单个技能中的多个工具
一个技能二进制文件可以实现多个工具。工具名称通过 argv[1] 传递:
#![allow(unused)]
fn main() {
match tool_name {
"get_weather" => handle_get_weather(&buf),
"get_forecast" => handle_get_forecast(&buf),
_ => fail(&format!("Unknown tool '{tool_name}'")),
}
}
每个工具必须在 manifest.json 中声明:
{
"tools": [
{ "name": "get_weather", "description": "...", "input_schema": { ... } },
{ "name": "get_forecast", "description": "...", "input_schema": { ... } }
]
}
环境变量
技能继承网关的环境(减去被屏蔽的变量)。使用 API 密钥的方式:
#![allow(unused)]
fn main() {
let api_key = std::env::var("MY_API_KEY")
.unwrap_or_else(|_| fail("MY_API_KEY not set"));
}
在 SKILL.md frontmatter 中声明依赖,这样在环境变量缺失时技能会被标记为不可用:
---
requires_env: MY_API_KEY
---
超时配置
在 manifest.json 中设置合理的超时:
| 技能类型 | 推荐超时 |
|---|---|
| 本地计算 | 5 秒 |
| 单次 API 调用 | 15 秒 |
| 多步 API 调用 | 30-60 秒 |
| 长时间研究 | 300-600 秒 |
安全
二进制完整性:
- 拒绝符号链接: 插件二进制文件必须是常规文件。加载时会拒绝符号链接,以防范链接替换攻击。加载器使用
symlink_metadata()(而非metadata())来检测。 - SHA-256 校验: 如果
manifest.json中存在sha256,加载器会计算二进制文件的哈希值,不匹配则拒绝。已校验的字节写入单独的文件供网关执行,消除 TOCTOU(检查时间/使用时间)漏洞。 - 大小限制: 插件可执行文件不得超过 100 MB。超大的二进制文件在读取前即被拒绝。
环境清理:
网关在启动技能进程前自动剥离以下环境变量:
LD_PRELOAD、DYLD_INSERT_LIBRARIES、DYLD_LIBRARY_PATHNODE_OPTIONS、PYTHONPATH、PERL5LIBRUSTFLAGS、RUST_LOG- 以及 10 余个其他变量(见
sandbox.rs中的BLOCKED_ENV_VARS)
技能开发者的最佳实践:
- 验证所有输入(永远不要信任
city、path等) - 为 HTTP 请求设置超时
- 避免 shell 注入(不要将用户输入传递给 shell 命令)
- 在发布构建中设置
manifest.json中的sha256以启用完整性校验
平台技能 vs 应用技能
| 应用技能 | 平台技能 | |
|---|---|---|
| 位置 | crates/app-skills/ | crates/platform-skills/ |
| 数组 | BUNDLED_APP_SKILLS | PLATFORM_SKILLS |
| 引导 | 每次网关启动 | 仅管理员机器人 |
| 作用域 | 按网关 | 所有网关共享 |
| 使用场景 | 始终可用、自包含 | 需要外部服务 |
无需完整重建即可更新技能
技能可以独立重建和部署:
# 只构建该技能
cargo build --release -p weather
# 复制到远程服务器
scp target/release/weather remote:~/.octos/skills/weather/main
# 无需重启网关 — 下次工具调用时使用新二进制文件
注意:如果修改了 SKILL.md 或 manifest.json,则需要重新构建 octos 二进制文件(因为它们通过 include_str! 嵌入)。
安装与分发
技能类型
| 类型 | 位置 | 安装方式 | 二进制文件 | 使用场景 |
|---|---|---|---|---|
| 内置 | crates/app-skills/ | 编译进 octos 二进制 | 嵌入式 | 随每个版本发布的核心技能 |
| 外部 | GitHub 仓库 | octos skills install user/repo | 下载或构建 | 社区/自定义技能 |
| 配置文件本地 | <profile-data>/skills/ | 按配置文件安装 | 自包含 | 租户隔离技能 |
按配置文件的技能管理
技能按配置文件安装以确保租户隔离。每个配置文件有独立的技能目录:
~/.octos/profiles/alice/data/
skills/
mofa-comic/
main ← 二进制文件(自包含,不在 ~/.cargo/bin)
SKILL.md
manifest.json
styles/*.toml ← 打包的资源文件
mofa-slides/
main
SKILL.md
manifest.json
styles/*.toml
重要: 技能二进制文件以 main 的形式保留在其技能目录中。它们不会被复制到 ~/.cargo/bin/ 或任何全局位置。插件加载器在 <skill-dir>/main 处找到它们。
安装/卸载/列表命令
所有操作界面都支持按配置文件操作:
# CLI(--profile 标志放在子命令之前)
octos skills --profile alice install mofa-org/mofa-skills/mofa-comic
octos skills --profile alice list
octos skills --profile alice remove mofa-comic
# 聊天中(自动使用当前配置文件)
/skills install mofa-org/mofa-skills/mofa-comic
/skills list
/skills remove mofa-comic
# Admin API
POST /api/admin/profiles/alice/skills {"repo": "mofa-org/mofa-skills/mofa-comic"}
GET /api/admin/profiles/alice/skills
DELETE /api/admin/profiles/alice/skills/mofa-comic
# Agent 工具(自动使用当前配置文件)
manage_skills(action="install", repo="mofa-org/mofa-skills/mofa-comic")
manage_skills(action="list")
manage_skills(action="remove", name="mofa-comic")
manage_skills(action="search", query="comic")
技能加载优先级
网关从多个目录加载技能。名称冲突时先匹配者优先:
<profile-data>/skills/— 按配置文件(最高优先级)<project-dir>/skills/— 项目本地<project-dir>/bundled-skills/— 内置应用技能~/.octos/skills/— 全局(最低优先级)
发布到注册表
外部技能可通过 octos-hub 注册表被发现。
- 将你的技能仓库推送到 GitHub
- 通过 PR 向
registry.json添加条目:
{
"name": "my-skills",
"description": "What your skills do",
"repo": "your-user/your-repo",
"skills": ["skill-a", "skill-b"],
"requires": ["git", "cargo"],
"tags": ["keyword1", "keyword2"]
}
- 用户即可查找并安装你的技能:
octos skills search keyword1
octos skills --profile alice install your-user/your-repo/skill-a
预编译二进制分发
为了加快安装速度(跳过编译),在 manifest.json 中添加 binaries 段:
{
"name": "my-skill",
"version": "1.0.0",
"binaries": {
"darwin-aarch64": {
"url": "https://github.com/you/repo/releases/download/v1.0.0/skill-darwin-aarch64.tar.gz",
"sha256": "abc123..."
},
"darwin-x86_64": {
"url": "https://github.com/you/repo/releases/download/v1.0.0/skill-darwin-x86_64.tar.gz",
"sha256": "def456..."
},
"linux-x86_64": {
"url": "https://github.com/you/repo/releases/download/v1.0.0/skill-linux-x86_64.tar.gz",
"sha256": "789ghi..."
}
},
"tools": [ ... ]
}
安装器下载匹配的二进制文件,验证 SHA-256,然后解压到 <skill-dir>/main。如果没有可用的预编译二进制文件,则回退到 cargo build --release。
技能的环境变量
网关自动向插件进程注入 API 密钥:
- 主提供商的 API 密钥(例如
DASHSCOPE_API_KEY) - 备选提供商的密钥(例如
GEMINI_API_KEY、OPENAI_API_KEY) - 非标准端点的 Base URL
OCTOS_DATA_DIR和OCTOS_WORK_DIR
密钥在网关启动时从 macOS 钥匙串解析。技能二进制文件以环境变量的形式接收它们 – 无需手动 export。
打包资源(样式、配置)
包含资源文件(样式、模板、配置)的技能应将它们打包在技能目录中:
my-skill/
main
SKILL.md
manifest.json
styles/
default.toml
manga.toml
templates/
report.html
二进制文件应相对于自身可执行文件的位置解析资源:
#![allow(unused)]
fn main() {
let exe = std::env::current_exe()?;
let skill_dir = exe.parent().unwrap();
let styles_dir = skill_dir.join("styles");
}
不要在工作目录(cwd)中查找资源 – cwd 指向配置文件的数据目录,而非技能目录。
检查清单
工具技能(二进制文件 + 工具)
- 创建
crates/app-skills/<name>/,包含 Cargo.toml、manifest.json、SKILL.md、src/main.rs - Cargo.toml 中的
[[bin]] name与 bundled_app_skills.rs 中的binary_name匹配 - manifest.json 对所有工具输入有有效的 JSON Schema
- SKILL.md 有包含触发关键词的 frontmatter
- 二进制文件读取
argv[1]获取工具名称,从 stdin 读取 JSON 输入 - 二进制文件向 stdout 输出
{"output": "...", "success": true/false} - 错误情况返回
success: false并附带清晰的消息 - 添加到工作区
Cargo.toml的 members - 添加到
bundled_app_skills.rs中的BUNDLED_APP_SKILLS -
cargo build --workspace成功 - 独立测试:
echo '{"param": "value"}' | ./target/debug/my_skill my_tool - 网关测试:技能出现在
~/.octos/skills/中且 Agent 可以使用
扩展(MCP 服务器、钩子、提示片段)
-
mcp_servers:设置了command或url;env仅列出变量名称而非值 -
mcp_servers:相对命令路径(./bin/server)在技能目录中存在 -
hooks:event是before_tool_call、after_tool_call、before_llm_call、after_llm_call之一 -
hooks:command是 argv 数组(非 shell 字符串);command[0]的相对路径能正确解析 -
hooks:当钩子只应用于特定工具时设置了tool_filter -
prompts:include中的 glob 模式匹配技能目录中预期的.md文件 - 纯扩展技能:
tools数组为空或省略;不需要二进制文件 - 网关测试:扩展出现在加载器日志中(
loaded skill extras)
架构文档:octos
概述
octos 是一个包含 15 个成员的 Rust 工作区(Edition 2024,rust-version 1.85.0),提供编码 Agent CLI 和多频道消息网关。通过 rustls 实现纯 Rust TLS(无 OpenSSL 依赖)。错误处理使用 eyre/color-eyre。
工作区成员:
- 6 个核心 crate:octos-core、octos-memory、octos-llm、octos-agent、octos-bus、octos-cli
- 1 个流水线 crate:octos-pipeline
- 7 个应用技能 crate:news、deep-search、deep-crawl、send-email、account-manager、time、weather
- 1 个平台技能 crate:asr
┌─────────────────────────────────────────────────────────────┐
│ octos-cli │
│ (CLI: chat, gateway, init, status) │
├──────────────────────────┬──────────────────────────────────┤
│ octos-agent │ octos-bus │
│ (Agent, Tools, Skills) │ (Channels, Sessions, Cron) │
├──────────┬───────────────┼──────────────────────────────────┤
│octos-memory│ octos-llm │ octos-pipeline │
│(Episodes) │ (Providers) │ (DOT-based orchestration) │
├──────────┴───────────────┴──────────────────────────────────┤
│ octos-core │
│ (Types, Messages, Gateway Protocol) │
└─────────────────────────────────────────────────────────────┘
octos-core — 基础类型
无内部依赖的共享类型。仅依赖 serde、chrono、uuid、eyre。
MessageRole 实现了 as_str() -> &'static str 和 Display,用于跨提供商的一致字符串转换(system/user/assistant/tool)。
任务模型
#![allow(unused)]
fn main() {
pub struct Task {
pub id: TaskId, // UUID v7 (temporal ordering)
pub parent_id: Option<TaskId>, // For subtasks
pub status: TaskStatus,
pub kind: TaskKind,
pub context: TaskContext,
pub result: Option<TaskResult>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
}
TaskId:基于 Uuid 的新类型。通过 Uuid::now_v7() 生成 UUID v7。实现 Display、FromStr、Default。
TaskStatus(标记枚举,"state" 判别器):
Pending— 等待分配InProgress { agent_id: AgentId }— 执行中Blocked { reason: String }— 等待依赖Completed— 成功Failed { error: String }— 失败(附带消息)
TaskKind(标记枚举,"type" 判别器):
Plan { goal: String }Code { instruction: String, files: Vec<PathBuf> }Review { diff: String }Test { command: String }Custom { name: String, params: serde_json::Value }
TaskContext:
working_dir: PathBuf、git_state: Option<GitState>、working_memory: Vec<Message>、episodic_refs: Vec<EpisodeRef>、files_in_scope: Vec<PathBuf>
TaskResult:
success: bool、output: String、files_modified: Vec<PathBuf>、subtasks: Vec<TaskId>、token_usage: TokenUsage
TokenUsage:input_tokens: u32、output_tokens: u32(默认 0/0)
消息类型
#![allow(unused)]
fn main() {
pub struct Message {
pub role: MessageRole, // System | User | Assistant | Tool
pub content: String,
pub media: Vec<String>, // File paths (images, audio)
pub tool_calls: Option<Vec<ToolCall>>,
pub tool_call_id: Option<String>,
pub timestamp: DateTime<Utc>,
}
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
}
网关协议
#![allow(unused)]
fn main() {
pub struct InboundMessage { // channel → agent
pub channel: String, // "telegram", "cli", "discord", etc.
pub sender_id: String,
pub chat_id: String,
pub content: String,
pub timestamp: DateTime<Utc>,
pub media: Vec<String>,
pub metadata: serde_json::Value,
}
pub struct OutboundMessage { // agent → channel
pub channel: String,
pub chat_id: String,
pub content: String,
pub reply_to: Option<String>,
pub media: Vec<String>,
pub metadata: serde_json::Value,
}
}
InboundMessage::session_key() 派生 SessionKey::new(channel, chat_id) — 格式为 "{channel}:{chat_id}"。
Agent 间协调
#![allow(unused)]
fn main() {
pub enum AgentMessage { // tagged: "type", snake_case
TaskAssign { task: Box<Task> },
TaskUpdate { task_id: TaskId, status: TaskStatus },
TaskComplete { task_id: TaskId, result: TaskResult },
ContextRequest { task_id: TaskId, query: String },
ContextResponse { task_id: TaskId, context: Vec<Message> },
}
}
错误系统
#![allow(unused)]
fn main() {
pub struct Error {
pub kind: ErrorKind,
pub context: Option<String>, // Chained context
pub suggestion: Option<String>, // Actionable fix hint
}
}
ErrorKind 变体:TaskNotFound、AgentNotFound、InvalidStateTransition、LlmError、ApiError(状态码感知:401→检查密钥,429→限流)、ToolError、ConfigError、ApiKeyNotSet、UnknownProvider、Timeout、ChannelError、SessionError、IoError、SerializationError、Other(eyre::Report)。
工具函数
truncate_utf8(s: &mut String, max_len: usize, suffix: &str) — 在 UTF-8 字符边界处原地截断。截断后追加后缀。用于所有工具输出。
octos-llm — LLM 提供商抽象
提供商 Trait
#![allow(unused)]
fn main() {
#[async_trait]
pub trait LlmProvider: Send + Sync {
async fn chat(&self, messages: &[Message], tools: &[ToolSpec], config: &ChatConfig) -> Result<ChatResponse>;
async fn chat_stream(&self, messages: &[Message], tools: &[ToolSpec], config: &ChatConfig) -> Result<ChatStream>; // default: falls back to chat()
fn context_window(&self) -> u32; // default: context_window_tokens(self.model_id())
fn model_id(&self) -> &str;
fn provider_name(&self) -> &str;
}
}
配置
#![allow(unused)]
fn main() {
pub struct ChatConfig {
pub max_tokens: Option<u32>, // default: Some(4096)
pub temperature: Option<f32>, // default: Some(0.0)
pub tool_choice: ToolChoice, // Auto | Required | None | Specific { name }
pub stop_sequences: Vec<String>,
}
}
响应类型
#![allow(unused)]
fn main() {
pub struct ChatResponse {
pub content: Option<String>,
pub tool_calls: Vec<ToolCall>,
pub stop_reason: StopReason, // EndTurn | ToolUse | MaxTokens | StopSequence
pub usage: TokenUsage,
}
pub enum StreamEvent {
TextDelta(String),
ToolCallDelta { index, id, name, arguments_delta },
Usage(TokenUsage),
Done(StopReason),
Error(String),
}
pub type ChatStream = Pin<Box<dyn Stream<Item = StreamEvent> + Send>>;
}
提供商注册表(registry/)
所有提供商定义在 octos-llm/src/registry/ 中 — 每个提供商一个文件。每个文件导出一个 ProviderEntry,包含元数据(名称、别名、默认模型、API 密钥环境变量、基础 URL)和 create() 工厂函数。添加新提供商 = 一个文件 + mod.rs 中一行代码。
#![allow(unused)]
fn main() {
pub struct ProviderEntry {
pub name: &'static str, // canonical name
pub aliases: &'static [&'static str], // e.g. ["google"] for gemini
pub default_model: Option<&'static str>,
pub api_key_env: Option<&'static str>,
pub default_base_url: Option<&'static str>,
pub requires_api_key: bool,
pub requires_base_url: bool, // true for vllm
pub requires_model: bool, // true for vllm
pub detect_patterns: &'static [&'static str], // model→provider auto-detect
pub create: fn(CreateParams) -> Result<Arc<dyn LlmProvider>>,
}
pub struct CreateParams {
pub api_key: Option<String>,
pub model: Option<String>,
pub base_url: Option<String>,
pub model_hints: Option<ModelHints>, // config-level override
}
}
查找:registry::lookup(name) — 不区分大小写,匹配规范名称或别名。
自动检测:registry::detect_provider(model) — 从模型名称模式推断提供商。
原生提供商(4 种协议实现)
| 提供商 | 基础 URL | 认证头 | 图片格式 | 默认模型 |
|---|---|---|---|---|
| Anthropic | api.anthropic.com | x-api-key | Base64 块 | claude-sonnet-4-20250514 |
| OpenAI | api.openai.com/v1 | Authorization: Bearer | Data URI | gpt-4o |
| Gemini | generativelanguage.googleapis.com/v1beta | x-goog-api-key | Base64 内联 | gemini-2.5-flash |
| OpenRouter | openrouter.ai/api/v1 | Authorization: Bearer | Data URI | anthropic/claude-sonnet-4-20250514 |
OpenAI 兼容提供商(通过 OpenAIProvider::with_base_url())
| 提供商 | 别名 | 基础 URL | 默认模型 | API 密钥环境变量 |
|---|---|---|---|---|
| DeepSeek | — | api.deepseek.com/v1 | deepseek-chat | DEEPSEEK_API_KEY |
| Groq | — | api.groq.com/openai/v1 | llama-3.3-70b-versatile | GROQ_API_KEY |
| Moonshot | kimi | api.moonshot.ai/v1 | kimi-k2.5 | MOONSHOT_API_KEY |
| DashScope | qwen | dashscope.aliyuncs.com/compatible-mode/v1 | qwen-max | DASHSCOPE_API_KEY |
| MiniMax | — | api.minimax.io/v1 | MiniMax-Text-01 | MINIMAX_API_KEY |
| Zhipu | glm | open.bigmodel.cn/api/paas/v4 | glm-4-plus | ZHIPU_API_KEY |
| Nvidia | nim | integrate.api.nvidia.com/v1 | meta/llama-3.3-70b-instruct | NVIDIA_API_KEY |
| Ollama | — | localhost:11434/v1 | llama3.2 | (无) |
| vLLM | — | (用户提供) | (用户提供) | VLLM_API_KEY |
Anthropic 兼容提供商
| 提供商 | 别名 | 基础 URL | 默认模型 | API 密钥环境变量 |
|---|---|---|---|---|
| Z.AI | zai, z.ai | api.z.ai/api/anthropic | glm-5 | ZAI_API_KEY |
ModelHints(OpenAI 提供商)
从模型名称在构造时自动检测,可通过配置中的 model_hints 覆盖:
#![allow(unused)]
fn main() {
pub struct ModelHints {
pub uses_completion_tokens: bool, // o-series, gpt-5, gpt-4.1
pub fixed_temperature: bool, // o-series, kimi-k2.5
pub lacks_vision: bool, // deepseek, minimax, mistral, yi-
pub merge_system_messages: bool, // default: true
}
}
SSE 流式处理
parse_sse_response(response) -> impl Stream<Item = SseEvent> — 基于 unfold 的有状态解析器。最大缓冲区:1 MB。处理 \n\n 和 \r\n\r\n 分隔符。每个提供商将 SSE 事件映射为 StreamEvent:
- Anthropic:
message_start→ 输入 token,content_block_start/delta→ 文本/工具块,message_delta→ 停止原因。自定义 SSE 状态机。 - OpenAI/OpenRouter:标准 OpenAI SSE,
[DONE]哨兵。delta.content用于文本,delta.tool_calls[]用于工具。共享解析器:parse_openai_sse_events()。 - Gemini:
alt=sse端点。candidates[0].content.parts[],包含函数调用数据。
RetryProvider
用指数退避包装任意 Arc<dyn LlmProvider>。被 ProviderChain 包装以实现多提供商故障转移。
#![allow(unused)]
fn main() {
pub struct RetryConfig {
pub max_retries: u32, // default: 3
pub initial_delay: Duration, // default: 1s
pub max_delay: Duration, // default: 60s
pub backoff_multiplier: f64, // default: 2.0
}
}
延迟公式:initial_delay * backoff_multiplier^attempt,上限为 max_delay。
可重试错误(三层检测):
- HTTP 状态码:429、500、502、503、504、529
- reqwest:
is_connect()或is_timeout() - 字符串兜底:“connection refused”、“timed out”、“overloaded”
提供商故障转移链
ProviderChain 包装多个 Arc<dyn LlmProvider>,在可重试错误时透明地故障转移。通过配置中的 fallback_models 配置。
#![allow(unused)]
fn main() {
pub struct ProviderChain {
slots: Vec<ProviderSlot>, // provider + AtomicU32 failure count
failure_threshold: u32, // default: 3
}
}
行为:按顺序尝试提供商,跳过已劣化的(失败次数 >= 阈值)。可转移错误时移至下一个。成功时重置失败计数。如果全部劣化,选择失败次数最少的。
可转移范围(比可重试更广):包括 401/403(不应重试同一提供商但应转移到其他提供商)和超时(不应浪费 120s × 重试次数在无响应的提供商上)。
AdaptiveRouter(adaptive.rs)
指标驱动的提供商选择,支持三种互斥模式(Off/Hedge/Lane)。跟踪每个提供商的 EMA 延迟(可配置 ema_alpha,默认 0.3)、P95 延迟(64 样本循环缓冲区)、错误率、吞吐量(输出 tokens/sec EMA)和成本。四因子评分:稳定性、质量、优先级、成本(所有权重可配置)。包含熔断器、探测请求、模型目录播种(model_catalog.json)和 QoS 排名。评分使用 EMA 混合:冷启动时使用目录基线数据,实时指标逐渐替代(权重在 10 次调用中从 0 渐变到 1)。
#![allow(unused)]
fn main() {
pub struct AdaptiveSlot {
provider: Arc<dyn LlmProvider>,
metrics: ProviderMetrics,
priority: usize,
cost_per_m: f64,
model_type: Mutex<ModelType>, // Strong | Fast
cost_in: AtomicU64,
ds_output: AtomicU64, // 深度搜索输出质量
baseline_stability: AtomicU64,
baseline_tool_avg_ms: AtomicU64,
baseline_p95_ms: AtomicU64,
context_window: AtomicU64,
max_output: AtomicU64,
}
}
Hedge 模式:通过 tokio::select! 竞速主服务商 + 最便宜的备选,取消输家。只有完成的请求记录指标(被取消的输家指标不记录)。如果主服务商失败,备选会顺序重试。
Lane 模式:对所有服务商评分,选择最佳的一个。向过期服务商发送探测请求(概率可配置,默认 0.1;间隔默认 60s)。
FallbackProvider(fallback.rs)
包装主服务商 + 按 QoS 排名的备选。失败时通过 ProviderRouter 记录冷却。按顺序尝试每个备选。
SwappableProvider(swappable.rs)
通过 RwLock 实现运行时模型切换。每次切换泄漏约 50 字节(对于罕见的用户操作可接受)。cached_model_id 和 cached_provider_name 是泄漏的 &'static str,以满足 &str 返回类型的要求。
ProviderRouter(router.rs)
子 Agent 多模型路由,支持前缀键解析。支持冷却(默认 60s)、按模型目录评分的 compatible_fallbacks()、从 pricing.rs 自动推导的费用信息和 LLM 可见的工具模式元数据。
#![allow(unused)]
fn main() {
pub struct ProviderRouter {
providers: RwLock<HashMap<String, Arc<dyn LlmProvider>>>,
active_key: RwLock<Option<String>>,
metadata: RwLock<HashMap<String, SubProviderMeta>>,
cooldowns: RwLock<HashMap<String, Instant>>,
qos_scores: RwLock<HashMap<String, f64>>,
}
}
OminixClient(ominix.rs)
通过 Ominix 运行时访问本地 ASR/TTS 的客户端。
Token 估算
#![allow(unused)]
fn main() {
pub fn estimate_tokens(text: &str) -> u32 // ~4 chars/token ASCII, ~1.5 chars/token CJK
pub fn estimate_message_tokens(msg: &Message) -> u32 // content + tool_calls + 4 overhead
}
上下文窗口
| 模型系列 | Token 数 |
|---|---|
| Claude 3/4 | 200,000 |
| GPT-4o/4-turbo | 128,000 |
| o1/o3/o4 | 200,000 |
| Gemini 2.0/1.5 | 1,000,000 |
| 默认(未知) | 128,000 |
定价
model_pricing(model_id) -> Option<ModelPricing> — 不区分大小写的子串匹配。费用 = (input/1M) * input_rate + (output/1M) * output_rate。
| 模型 | 输入 $/1M | 输出 $/1M |
|---|---|---|
| claude-opus-4 | 15.00 | 75.00 |
| claude-sonnet-4 | 3.00 | 15.00 |
| claude-haiku | 0.80 | 4.00 |
| gpt-4o | 2.50 | 10.00 |
| gpt-4o-mini | 0.15 | 0.60 |
| o3/o4 | 10.00 | 40.00 |
嵌入
#![allow(unused)]
fn main() {
pub trait EmbeddingProvider: Send + Sync {
async fn embed(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>>;
fn dimension(&self) -> usize;
}
}
OpenAIEmbedder:默认模型 text-embedding-3-small(1536 维)。text-embedding-3-large = 3072 维。
语音转文字
GroqTranscriber:通过 https://api.groq.com/openai/v1/audio/transcriptions 使用 Whisper whisper-large-v3。Multipart 表单。60 秒超时。MIME 类型检测:ogg/opus→audio/ogg、mp3→audio/mpeg、m4a→audio/mp4、wav→audio/wav。
视觉
encode_image(path) -> (mime_type, base64_data) — JPEG/PNG/GIF/WebP。is_image(path) -> bool。
类型化错误层次(error.rs)
LlmError 包含 LlmErrorKind 枚举:Authentication、RateLimited、ContextOverflow、ModelNotFound、ServerError、Network、Timeout、InvalidRequest、ContentFiltered、StreamError、Provider。is_retryable() 对 RateLimited、ServerError、Network、Timeout、StreamError 返回 true。from_status(code, body) 将 HTTP 状态码映射为错误类型。提供商响应体仅在 debug 级别记录(不暴露在错误消息中)。
高级客户端(high_level.rs)
LlmClient 用友好 API 包装 Arc<dyn LlmProvider>:generate(prompt)、generate_with(messages, tools, config)、generate_object(prompt, schema_name, schema)、generate_typed<T>(prompt, schema_name, schema)、stream(prompt)、stream_with(messages, tools, config)。可通过 with_config(ChatConfig) 配置。
中间件流水线(middleware.rs)
LlmMiddleware trait 包含 before()/after()/on_error() 钩子。MiddlewareStack 包装 LlmProvider 并按插入顺序运行各层。before() 可通过缓存响应短路。内置:LoggingMiddleware(tracing)、CostTracker(AtomicU64 计数器,用于输入/输出 token 和请求数)。流式推送绕过中间件(记录为 debug 警告)。
模型目录(catalog.rs)
ModelCatalog 包含 ModelInfo(id、name、provider、context_window、max_output_tokens、capabilities、cost、aliases)。通过 HashMap 索引按 ID 或别名查找。with_defaults() 预注册 4 个模型(Claude Sonnet 4、Claude Haiku 4.5、GPT-4o、Gemini 2.5 Flash)。by_provider() 和 with_capability() 用于过滤查询。
octos-memory — 持久化与搜索
EpisodeStore
redb 数据库位于 .octos/episodes.redb,包含三张表:
| 表 | 键 | 值 | 用途 |
|---|---|---|---|
| episodes | &str (episode_id) | &str (JSON) | 完整的片段记录 |
| cwd_index | &str (working_dir) | &str (JSON array of IDs) | 按目录范围的查找 |
| embeddings | &str (episode_id) | &[u8] (bincode Vec | 向量嵌入 |
#![allow(unused)]
fn main() {
pub struct Episode {
pub id: String, // UUID v7
pub task_id: TaskId,
pub agent_id: AgentId,
pub working_dir: PathBuf,
pub summary: String, // LLM-generated, truncated to 500 chars
pub outcome: EpisodeOutcome, // Success | Failure | Blocked | Cancelled
pub key_decisions: Vec<String>,
pub files_modified: Vec<PathBuf>,
pub created_at: DateTime<Utc>,
}
}
操作:
store(episode)— 序列化为 JSON,更新 cwd_index,插入内存中的 HybridIndexget(id)— 按 episode_id 直接查找find_relevant(cwd, query, limit)— 限定在目录范围内的关键词匹配recent_for_cwd(cwd, n)— 按 created_at 降序取最近 N 条store_embedding(id, Vec<f32>)— bincode 序列化,存入 embeddings 表,更新 HybridIndexfind_relevant_hybrid(query, query_embedding, limit)— 跨所有片段的全局混合搜索
初始化:open() 时通过遍历所有片段并从数据库加载嵌入来重建内存中的 HybridIndex。
MemoryStore
基于文件的持久化记忆,位于 {data_dir}/memory/:
MEMORY.md— 长期记忆(全量覆写)YYYY-MM-DD.md— 每日笔记(带日期头的追加)
get_memory_context() 构建系统提示注入:
## Long-term Memory— 完整的 MEMORY.md## Recent Activity— 7 天滚动窗口的每日笔记## Today's Notes— 当天内容
HybridIndex — BM25 + 向量搜索
#![allow(unused)]
fn main() {
pub struct HybridIndex {
inverted: HashMap<String, Vec<(usize, u32)>>, // term → [(doc_idx, raw_tf_count)]
doc_lengths: Vec<usize>,
total_len: usize, // 运行总量,用于 O(1) avg_dl
avg_dl: f64,
ids: Vec<String>,
hnsw: Option<Hnsw<'static, f32, DistCosine>>,
has_embedding: Vec<bool>,
dimension: usize, // default: 1536
}
}
BM25 评分(常量:K1=1.2, B=0.75):
- 分词:小写化,按非字母数字字符拆分,过滤长度 < 2 的 token
- IDF:
ln((N - df + 0.5) / (df + 0.5) + 1.0) - 评分:
IDF * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * dl/avg_dl))— 使用原始词频计数(非归一化) - 去重检测:
ids.contains(episode_id)跳过已索引的文档(第 76-78 行) - 归一化到 [0, 1] 范围(epsilon
1e-10防止接近零的最大分数导致 NaN)
HNSW 向量索引(通过 hnsw_rs):
- 命名常量:
HNSW_MAX_NB_CONNECTION=16、HNSW_CAPACITY=10_000、HNSW_EF_CONSTRUCTION=200、HNSW_MAX_LAYER=16、DistCosine - 插入/搜索前进行 L2 归一化;拒绝零向量(返回
None) - 余弦相似度 =
1 - distance(DistCosine 返回 1-cos_sim)
混合排名 — 从每种方法获取 limit * 4 个候选:
- 可配置权重,通过
with_weights(vector_weight, bm25_weight)(默认:0.7 / 0.3) - 无向量时:仅使用 BM25(优雅降级)
octos-agent — Agent 运行时
Agent 核心
#![allow(unused)]
fn main() {
pub struct Agent {
id: AgentId,
llm: Arc<dyn LlmProvider>,
tools: ToolRegistry,
memory: Arc<EpisodeStore>,
embedder: Option<Arc<dyn EmbeddingProvider>>,
system_prompt: RwLock<String>,
config: AgentConfig,
reporter: Arc<dyn ProgressReporter>,
shutdown: Arc<AtomicBool>, // Acquire/Release ordering
}
pub struct AgentConfig {
pub max_iterations: u32, // default: 50 (CLI overrides to 20)
pub max_tokens: Option<u32>, // None = unlimited
pub max_timeout: Option<Duration>,// default: 600s wall-clock timeout
pub save_episodes: bool, // default: true
}
}
执行循环(run_task / process_message)
1. 构建消息:系统提示 + 历史 + 记忆上下文 + 输入
2. 循环(最多 max_iterations 次):
a. 检查 shutdown 标志和 token 预算
b. trim_to_context_window() — 必要时压缩
c. 通过 chat_stream() 调用 LLM
d. 消费流 → 累积文本、tool_calls、token
e. 匹配 stop_reason:
- EndTurn/StopSequence → 保存片段,返回结果
- ToolUse → execute_tools() → 追加结果 → 继续
- MaxTokens → 返回结果
ConversationResponse:content: String、token_usage: TokenUsage、files_modified: Vec<PathBuf>、streamed: bool
片段保存:任务完成后,如果有 embedder 则异步触发嵌入生成。
墙钟超时:Agent 在 max_timeout(默认 600 秒)后终止,不论迭代次数。
工具输出清理
在将工具结果反馈给 LLM 之前,sanitize_tool_output()(在 sanitize.rs 中)剥离噪声:
- Base64 数据 URI:
data:...;base64,<payload>→[base64-data-redacted] - 长十六进制字符串:64+ 个连续十六进制字符(SHA-256、原始密钥)→
[hex-redacted]
上下文压缩
当估算的 token 超过上下文窗口的 80% / 1.2 安全系数时触发。
算法:
- 保留最近的 MIN_RECENT_MESSAGES(6)条非系统消息
- 不在工具调用/结果对内部拆分
- 摘要旧消息:首行(200 字符),剥离工具参数,丢弃媒体
- 预算:摘要占总量的 40%(BASE_CHUNK_RATIO = 0.4)
- 替换为:
[System, CompactionSummary, Recent1, Recent2, ...]
格式:
- User:
> User: first line [media omitted] - Assistant:
> Assistant: content或- Called tool_name - Tool:
-> tool_name: ok|error - first 100 chars
内置应用技能(bundled_app_skills.rs)
编译时嵌入的应用技能条目。每个应用技能 crate(news、deep-search、deep-crawl 等)注册为运行时可用的内置技能。
引导(bootstrap.rs)
在网关启动时引导内置技能。确保所有内置应用技能已注册并可用。
提示词防护(prompt_guard.rs)
提示注入检测。ThreatKind 枚举分类检测到的威胁。在传递给 Agent 之前扫描用户输入。
工具系统
#![allow(unused)]
fn main() {
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn tags(&self) -> &[&str];
fn input_schema(&self) -> serde_json::Value;
async fn execute(&self, args: &serde_json::Value) -> Result<ToolResult>;
}
pub struct ToolResult {
pub output: String,
pub success: bool,
pub file_modified: Option<PathBuf>,
pub tokens_used: Option<TokenUsage>,
}
}
ToolRegistry:HashMap<String, Arc<dyn Tool>>,带有 provider_policy: Option<ToolPolicy> 用于软过滤。
内置工具(14 个)
| 工具 | 参数 | 关键行为 |
|---|---|---|
| read_file | path, start_line?, end_line? | 行号(NNN|),100KB 截断,拒绝符号链接 |
| write_file | path, content | 创建父目录,返回 file_modified |
| edit_file | path, old_string, new_string | 要求精确匹配,0 或 >1 次出现报错 |
| diff_edit | path, diff | 统一 diff 格式,模糊匹配(+-3 行),反向 hunk 应用 |
| glob | pattern, limit=100 | 拒绝绝对路径和 ..,相对结果 |
| grep | pattern, file_pattern?, limit=50, context=0, ignore_case=false | 通过 ignore::WalkBuilder 感知 .gitignore,正则带 (?i) 标志 |
| list_dir | path | 排序,[dir]/[file] 前缀 |
| shell | command, timeout_secs=120 | SafePolicy 检查,50KB 输出截断,沙箱包装,超时钳制到 [1, 600] 秒 |
| web_search | query, count=5 | Brave Search API (BRAVE_API_KEY) |
| web_fetch | url, extract_mode=“markdown”, max_chars=50000 | SSRF 防护,htmd HTML→markdown,30 秒超时 |
| message | content, channel?, chat_id? | 通过 OutboundMessage 跨频道消息。仅网关模式 |
| spawn | task, label?, mode=“background”, allowed_tools, context? | 继承提供商策略的子 Agent。sync=内联,background=异步。仅网关模式 |
| cron | action, message, schedule params | 调度 add/list/remove/enable/disable。仅网关模式 |
| browser | action, url?, selector?, text?, expression? | 通过 CDP 的无头 Chrome(始终编译)。操作:navigate(SSRF + scheme 检查)、get_text、get_html、click、type、screenshot、evaluate、close。5 分钟空闲超时,环境清理,10 秒 JS 超时,提前操作验证 |
注册:核心工具在 ToolRegistry::with_builtins() 中注册(所有模式)。Browser 始终编译。Message、spawn 和 cron 仅在网关模式注册(gateway.rs)。
工具策略
#![allow(unused)]
fn main() {
pub struct ToolPolicy {
pub allow: Vec<String>, // empty = allow all
pub deny: Vec<String>, // deny-wins
}
}
分组:group:fs(read_file、write_file、edit_file、diff_edit)、group:runtime(shell)、group:web(web_search、web_fetch、browser)、group:search(glob、grep、list_dir)、group:sessions(spawn)。
通配符:exec* 匹配前缀。按提供商的策略通过配置 tools.byProvider。
命令策略(ShellTool)
#![allow(unused)]
fn main() {
pub enum Decision { Allow, Deny, Ask }
}
SafePolicy 拒绝模式:rm -rf /、rm -rf /*、dd if=、mkfs、:(){:|:&};:、chmod -R 777 /。匹配前对命令进行空白归一化,防止通过额外空格/制表符绕过。
SafePolicy 询问模式:sudo、rm -rf、git push --force、git reset --hard
沙箱
#![allow(unused)]
fn main() {
pub enum SandboxMode { Auto, Bwrap, Macos, Docker, None }
}
BLOCKED_ENV_VARS(18 个变量,所有后端 + MCP 共享):
LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, DYLD_FALLBACK_LIBRARY_PATH, DYLD_VERSIONED_LIBRARY_PATH, NODE_OPTIONS, PYTHONSTARTUP, PYTHONPATH, PERL5OPT, RUBYOPT, RUBYLIB, JAVA_TOOL_OPTIONS, BASH_ENV, ENV, ZDOTDIR
| 后端 | 隔离 | 网络 | 路径验证 |
|---|---|---|---|
| Bwrap(Linux) | 只读绑定 /usr,/lib,/bin,/sbin,/etc;读写绑定工作目录;tmpfs /tmp;unshare-pid | 如果 !allow_network 则 --unshare-net | 无 |
| Macos(sandbox-exec) | SBPL 配置:process-exec/fork、file-read*、工作目录+/private/tmp 写入 | (allow network*) 或 (deny network*) | 拒绝控制字符、(、)、\、" |
| Docker | --rm --security-opt no-new-privileges --cap-drop ALL | --network none | 拒绝 :、\0、\n、\r |
Docker 资源限制:--cpus、--memory、--pids-limit。挂载模式:None(/tmp 工作目录)、ReadOnly、ReadWrite。
钩子系统
生命周期钩子在 Agent 事件时运行 shell 命令。通过配置中的 hooks 数组配置。
#![allow(unused)]
fn main() {
pub enum HookEvent { BeforeToolCall, AfterToolCall, BeforeLlmCall, AfterLlmCall }
pub struct HookConfig {
pub event: HookEvent,
pub command: Vec<String>, // argv array (no shell interpretation)
pub timeout_ms: u64, // default: 5000
pub tool_filter: Vec<String>, // tool events only; empty = all
}
}
Shell 协议:通过 stdin 传递 JSON 载荷。退出码语义:0=允许,1=拒绝(仅 before 钩子),2+=错误。Before 钩子可以拒绝操作;after 钩子的退出码仅计为错误。
熔断器:HookExecutor 在连续 3 次失败后自动禁用钩子(可通过 with_threshold() 配置)。成功时重置。
环境:命令通过 BLOCKED_ENV_VARS 清理。波浪号展开支持 ~/ 和 ~username/。
集成:接入 chat.rs、gateway.rs、serve.rs。钩子配置变更通过配置监视器触发重启。
MCP 集成
Model Context Protocol 服务器的 JSON-RPC 传输。两种传输模式:
传输方式:
- Stdio:将服务器作为子进程启动(command + args + env)。行限制:1MB。通过
BLOCKED_ENV_VARS清理环境。 - HTTP/SSE:通过
url字段连接远程服务器。POST JSON,SSE 响应处理。
生命周期(stdio):
- 启动服务器(command + args + env,过滤 BLOCKED_ENV_VARS)
- 初始化:
protocolVersion: "2024-11-05" - 发现工具:
tools/listRPC - 验证输入 schema(最大深度 10,最大大小 64KB);拒绝无效 schema 的工具
- 注册 McpTool 包装器(30 秒超时,1MB 最大响应)
McpTool 执行:tools/call 传入 name + arguments。从响应中提取 content[].text。
技能系统
技能是扩展 Agent 能力的 markdown 指令文件。两个来源:内置(编译进二进制)和工作区(用户安装)。
技能文件格式(SKILL.md)
---
name: skill_name
description: What it does
requires_bins: binary1, binary2 # comma-separated, checked via `which`
requires_env: ENV_VAR1, ENV_VAR2 # comma-separated, checked via std::env::var()
always: true|false # auto-load into system prompt when available
---
Skill instructions here (markdown). This body is injected into the agent's
system prompt when the skill is activated.
Frontmatter 解析:简单的 key: value 行匹配(非完整 YAML)。split_frontmatter() 在 --- 分隔符之间查找内容。strip_frontmatter() 仅返回正文。
SkillInfo
#![allow(unused)]
fn main() {
pub struct SkillInfo {
pub name: String,
pub description: String,
pub path: PathBuf, // filesystem path or "(built-in)/name/SKILL.md"
pub available: bool, // bins_ok && env_ok
pub always: bool, // auto-load into system prompt
pub builtin: bool, // true if from BUILTIN_SKILLS, false if workspace
}
}
可用性检查:available = requires_bins 全部在 PATH 中找到 且 requires_env 全部已设置。缺少依赖的技能不可用但仍会列出。
SkillsLoader
#![allow(unused)]
fn main() {
pub struct SkillsLoader {
skills_dir: PathBuf, // {data_dir}/skills/
}
}
方法:
list_skills()— 扫描工作区目录 + 内置。工作区技能覆盖同名内置(通过 HashSet 检查)。结果按字母排序。load_skill(name)— 返回正文(已剥离 frontmatter)。先检查工作区,回退到内置。build_skills_summary()— 生成 XML 用于系统提示注入:<skills> <skill available="true"> <name>skill_name</name> <description>What it does</description> <location>/path/to/SKILL.md</location> </skill> </skills>get_always_skills()— 过滤always: true且available: true的技能。load_skills_for_context(names)— 加载多个技能,用\n---\n连接。
内置技能(编译时 include_str!())
#![allow(unused)]
fn main() {
pub struct BuiltinSkill {
pub name: &'static str,
pub content: &'static str, // full SKILL.md including frontmatter
}
pub const BUILTIN_SKILLS: &[BuiltinSkill] = &[...];
}
| 技能 | 用途 |
|---|---|
| cron | 任务调度指令 |
| skill-store | 技能商店浏览和安装 |
| skill-creator | 创建新技能 |
| tmux | 终端复用器控制 |
| weather | 天气信息查询 |
CLI 管理(octos skills)
list— 显示内置技能(附覆盖状态)+ 工作区技能install <user/repo/skill-name>— 从https://raw.githubusercontent.com/{repo}/main/SKILL.md获取(15 秒超时),保存到.octos/skills/{name}/SKILL.md。如果技能已存在则失败。remove <name>— 删除.octos/skills/{name}/目录
与网关集成
在网关命令中,技能在系统提示构建期间加载:
get_always_skills()— 收集自动加载的技能名称load_skills_for_context(names)— 加载并连接技能正文build_skills_summary()— 将 XML 技能索引追加到系统提示- 始终开启的技能内容前置到系统提示
插件系统
插件通过独立可执行文件扩展 Agent 的工具。每个插件是一个包含 manifest.json 和可执行文件的目录。
目录布局
.octos/plugins/ # 本地(项目级)
~/.octos/plugins/ # 全局(用户级)
└── my-plugin/
├── manifest.json # 插件元数据 + 工具定义
└── my-plugin # 可执行文件(或 "main" 作为回退)
发现顺序:先本地 .octos/plugins/,再全局 ~/.octos/plugins/。两者均由 Config::plugin_dirs() 扫描。
PluginManifest
#![allow(unused)]
fn main() {
pub struct PluginManifest {
pub name: String,
pub version: String,
pub tools: Vec<PluginToolDef>, // default: empty vec
}
pub struct PluginToolDef {
pub name: String, // must be unique across all plugins
pub description: String,
pub input_schema: serde_json::Value, // default: {"type": "object"}
}
}
manifest.json 示例:
{
"name": "my-plugin",
"version": "0.1.0",
"tools": [
{
"name": "greet",
"description": "Greet someone by name",
"input_schema": {
"type": "object",
"properties": { "name": { "type": "string" } }
}
}
]
}
PluginLoader
#![allow(unused)]
fn main() {
pub struct PluginLoader; // stateless, all methods are associated functions
}
load_into(registry, dirs):
- 扫描每个目录的子目录
- 对每个子目录查找
manifest.json - 解析清单,查找可执行文件(先尝试目录名,再尝试
main) - 验证可执行权限(Unix:
mode & 0o111 != 0;非 Unix:存在性检查) - 将每个工具定义包装为实现
Tooltrait 的PluginTool - 注册到
ToolRegistry - 记录警告:
"loaded unverified plugin (no signature check)" - 返回工具总数。失败的插件带警告跳过,不会导致致命错误。
PluginTool — 执行协议
#![allow(unused)]
fn main() {
pub struct PluginTool {
plugin_name: String,
tool_def: PluginToolDef,
executable: PathBuf,
}
}
调用:executable <tool_name>(工具名称作为第一个参数传递)。
stdin/stdout 协议:
- 以工具名称为参数启动可执行文件,管道连接 stdin/stdout/stderr
- 将 JSON 序列化的参数写入 stdin,关闭(EOF 表示输入结束)
- 等待退出,30 秒超时(
PLUGIN_TIMEOUT) - 解析 stdout 为 JSON:
- 结构化:
{"output": "...", "success": true/false}→ 使用解析后的值 - 回退:原始 stdout + stderr 拼接,成功由退出码决定
- 结构化:
- 返回
ToolResult(插件不跟踪file_modified)
错误处理:
- 启动失败 → 包含插件名称和可执行文件路径的 eyre 错误
- 超时 → 包含插件名称、工具名称和持续时间的 eyre 错误
- JSON 解析失败 → 优雅回退到原始输出
进度报告
Agent 在执行期间通过基于 trait 的观察者模式发出结构化事件。消费者(CLI、REST API)实现该 trait 以各自的格式渲染进度。
ProgressReporter Trait
#![allow(unused)]
fn main() {
pub trait ProgressReporter: Send + Sync {
fn report(&self, event: ProgressEvent);
}
}
Agent 持有 reporter: Arc<dyn ProgressReporter>。事件在执行循环期间同步触发(非阻塞 — 实现不得阻塞)。
ProgressEvent 枚举
#![allow(unused)]
fn main() {
pub enum ProgressEvent {
TaskStarted { task_id: String },
Thinking { iteration: u32 },
Response { content: String, iteration: u32 },
ToolStarted { name: String, tool_id: String },
ToolCompleted { name: String, tool_id: String, success: bool,
output_preview: String, duration: Duration },
FileModified { path: String },
TokenUsage { input_tokens: u32, output_tokens: u32 },
TaskCompleted { success: bool, iterations: u32, duration: Duration },
TaskInterrupted { iterations: u32 },
MaxIterationsReached { limit: u32 },
TokenBudgetExceeded { used: u32, limit: u32 },
StreamChunk { text: String, iteration: u32 },
StreamDone { iteration: u32 },
CostUpdate { session_input_tokens: u32, session_output_tokens: u32,
response_cost: Option<f64>, session_cost: Option<f64> },
}
}
实现(3 种)
SilentReporter — 空操作,未配置报告器时用作默认值。
ConsoleReporter — 带 ANSI 颜色和流式支持的 CLI 输出:
#![allow(unused)]
fn main() {
pub struct ConsoleReporter {
use_colors: bool,
verbose: bool,
stdout: Mutex<BufWriter<Stdout>>, // buffered for streaming chunks
}
}
| 事件 | 输出 |
|---|---|
| Thinking | \r⟳ Thinking... (iteration N)(覆写行,黄色) |
| Response | ◆ first 3 lines...(青色,清除 Thinking 行) |
| ToolStarted | \r⚙ Running tool_name...(覆写行,黄色) |
| ToolCompleted | ✓ tool_name (duration) 绿色 或 ✗ tool_name 红色;verbose:5 行输出 + ... |
| FileModified | 📝 Modified: path(绿色) |
| TokenUsage | Tokens: N in, N out(仅 verbose,暗色) |
| TaskCompleted | ✓ Completed N iterations, Xs 或 ✗ Failed after N iterations |
| TaskInterrupted | ⚠ Interrupted after N iterations.(黄色) |
| MaxIterationsReached | ⚠ Reached max iterations limit (N).(黄色) |
| TokenBudgetExceeded | ⚠ Token budget exceeded (used, limit).(黄色) |
| StreamChunk | 写入缓冲 stdout;仅在 \n 时 flush(减少系统调用) |
| StreamDone | Flush + 换行 |
| CostUpdate | Tokens: N in / N out | Cost: $X.XXXX |
| TaskStarted | ▶ Task: id(仅 verbose,暗色) |
持续时间格式化:>1s → {:.1}s,≤1s → {N}ms。
SseBroadcaster(REST API,feature:api)— 将事件转换为 JSON 并通过 tokio::sync::broadcast 频道广播:
#![allow(unused)]
fn main() {
pub struct SseBroadcaster {
tx: broadcast::Sender<String>, // JSON-serialized events
}
}
| ProgressEvent | JSON type 字段 | 附加字段 |
|---|---|---|
| ToolStarted | "tool_start" | tool |
| ToolCompleted | "tool_end" | tool、success |
| StreamChunk | "token" | text |
| StreamDone | "stream_end" | — |
| CostUpdate | "cost_update" | input_tokens、output_tokens、session_cost |
| Thinking | "thinking" | iteration |
| Response | "response" | iteration |
| (其他) | "other" | —(debug 级别记录) |
订阅者通过 SseBroadcaster::subscribe() -> broadcast::Receiver<String> 接收事件。发送错误(无订阅者)静默忽略。
执行环境(exec_env.rs)
ExecEnvironment trait 包含 exec(cmd, args, env)、read_file(path)、write_file(path, content)、file_exists(path)、list_dir(path)。两种实现:LocalEnvironment(tokio::process::Command)和 DockerEnvironment(docker exec)。环境变量通过共享的 BLOCKED_ENV_VARS 清理。Docker 路径验证防止注入字符(\0、\n、\r、:)。Docker 环境变量通过 --env 标志转发。
提供商工具集(provider_tools.rs)
ToolAdjustment(prefer、demote、aliases、extras)按 LLM 提供商配置。ProviderToolsets 注册表包含 with_defaults() 用于 openai/anthropic/google。用于按提供商优化工具展示(例如 OpenAI 偏好 shell/read_file,降低 diff_edit)。
类型化回合(turn.rs)
Turn 用 TurnKind(UserInput、AgentReply、ToolCall、ToolResult、System)和迭代次数包装 Message。turns_to_messages() 转换回 Vec<Message> 用于 LLM 调用。支持对对话历史的语义分析。
事件总线(event_bus.rs)
EventBus 包含类型化的 EventSubscriber,用于 Agent 内部的发布/订阅。解耦事件生产者(工具执行、LLM 调用)与消费者(日志、指标、UI 更新)。
循环检测(loop_detect.rs)
检测重复的 Agent 行为(如使用相同参数调用同一工具)。可配置阈值和窗口。检测到循环时提前返回诊断消息。
会话状态(session.rs)
SessionState 包含 SessionLimits 和 SessionUsage 跟踪。SessionStateHandle 用于线程安全访问。根据配置的限制跟踪 token 用量、迭代次数和墙钟时间。
引导(steering.rs)
SteeringMessage 包含 SteeringSender/SteeringReceiver(mpsc 通道)。允许在对话中途从外部控制 Agent 行为(如注入引导、改变策略)。
提示层(prompt_layer.rs)
PromptLayerBuilder 用于从多个来源组合系统提示(基础提示、人设、用户上下文、记忆、技能)。各层按顺序拼接,可配置分隔符。
octos-bus — 网关基础设施
消息总线
create_bus() -> (AgentHandle, BusPublisher) 通过 mpsc 通道连接(容量 256)。AgentHandle 接收 InboundMessage;BusPublisher 分发 OutboundMessage。
队列模式(通过 gateway.queue_mode 配置):
Followup(默认):FIFO — 逐条处理排队消息Collect:按会话合并排队消息,拼接内容后再处理
频道 Trait
#![allow(unused)]
fn main() {
#[async_trait]
pub trait Channel: Send + Sync {
fn name(&self) -> &str;
async fn start(&self, inbound_tx: mpsc::Sender<InboundMessage>) -> Result<()>;
async fn send(&self, msg: &OutboundMessage) -> Result<()>;
fn is_allowed(&self, sender_id: &str) -> bool;
async fn stop(&self) -> Result<()>;
}
}
频道实现
| 频道 | 传输方式 | Feature Flag | 认证 | 去重 |
|---|---|---|---|---|
| CLI | stdin/stdout | (始终启用) | 无 | 无 |
| Telegram | teloxide 长轮询 | telegram | Bot token (env) | teloxide 内置 |
| Discord | serenity gateway | discord | Bot token (env) | serenity 内置 |
| Slack | Socket Mode (tokio-tungstenite) | slack | Bot token + App token | message_ts |
| WebSocket 桥接 (ws://localhost:3001) | whatsapp | Baileys 桥接 | HashSet(10K 上限,溢出时清空) | |
| 飞书 | WebSocket (tokio-tungstenite) | feishu | App ID + Secret → tenant token (TTL 6000s) | HashSet(10K 上限,溢出时清空) |
| 邮件 | IMAP 轮询 + SMTP 发送 | email | 用户名/密码,rustls TLS | IMAP UNSEEN 标志 |
| 企业微信 | 企业微信 API | wecom | Corp ID + Agent Secret | message_id |
| Twilio | Twilio SMS/MMS | twilio | Account SID + Auth Token | message SID |
邮件细节:IMAP 通过 async-imap + rustls 接收(轮询未读,标记 \Seen)。SMTP 通过 lettre 发送(端口 465=隐式 TLS,其他=STARTTLS)。mailparse 用于 RFC822 正文提取。正文通过 truncate_utf8(max_body_chars) 截断。
飞书细节:带 TTL 缓存的 Tenant Access Token(6000 秒)。从 /callback/ws/endpoint 获取 WebSocket 网关 URL。通过 header.event_type == "im.message.receive_v1" 检测消息类型。支持 oc_*(chat_id)vs ou_*(open_id)路由。
Markdown 转 HTML:markdown_html.rs 将 Markdown 转换为 Telegram 兼容的 HTML 用于富文本消息格式化。
媒体:download_media() 辅助函数将照片/语音/音频/文档下载到 .octos/media/。
语音转文字:语音/音频在 Agent 处理前自动通过 GroqTranscriber 转录。
消息合并
将超大消息拆分为适合频道的分块:
| 频道 | 最大字符数 |
|---|---|
| Telegram | 4000 |
| Discord | 1900 |
| Slack | 3900 |
断开优先级:段落(\n\n)> 换行(\n)> 句号(. )> 空格( )> 硬截断。
MAX_CHUNKS = 50(DoS 限制)。通过 char_indices() 实现 UTF-8 安全的边界检测。
会话管理器
JSONL 持久化位于 .octos/sessions/{key}.jsonl。
- 内存缓存:LRU + 写入时同步到磁盘
- 文件名:百分号编码的 SessionKey,截断到 183 字符,截断时添加
_{hash:016X}后缀防止冲突 - 文件大小限制:最大 10MB(
MAX_SESSION_FILE_SIZE);加载时跳过超大文件 - 崩溃安全:原子写入-重命名
- 分支:
fork()创建带parent_key追踪的子会话,复制最后 N 条消息
定时服务
JSON 持久化位于 .octos/cron.json。
调度类型:
Every { seconds: u64 }— 周期性间隔Cron { expr: String }— 通过croncrate 的 cron 表达式At { timestamp_ms: i64 }— 一次性(运行后自动删除)
CronJob 字段:id(来自 UUIDv7 的 8 字符十六进制)、name、enabled、schedule、payload(message + deliver 标志 + channel + chat_id)、state(next_run_at_ms, run_count)、delete_after_run。
心跳服务
定期检查 HEARTBEAT.md(默认:30 分钟间隔)。如果非空则将内容发送给 Agent。
octos-cli — CLI 与配置
命令
| 命令 | 说明 |
|---|---|
chat | 交互式多轮对话。Readline + 历史。退出:exit/quit/:q |
gateway | 带会话管理的持久多频道守护进程 |
init | 初始化 .octos/ 目录,包含配置、模板和子目录 |
status | 显示配置、提供商、API 密钥、引导文件 |
auth login/logout/status | OAuth PKCE(OpenAI)、设备码、粘贴 token |
cron list/add/remove/enable | CLI 定时任务管理 |
channels status/login | 频道编译状态、WhatsApp 桥接设置 |
skills list/install/remove | 技能管理、GitHub 获取 |
office | Office/工作区管理 |
account | 账户管理 |
clean | 删除 .redb 文件,支持 dry-run |
completions | Shell 补全生成(bash/zsh/fish) |
docs | 生成工具 + 提供商文档 |
serve | REST API 服务器(feature:api)— axum 监听 127.0.0.1:8080(--host 覆盖) |
配置
从 .octos/config.json(本地)或 ~/.config/octos/config.json(全局)加载。本地优先。
${VAR}展开:字符串值中的环境变量替换- 版本化配置:版本字段 + 自动
migrate_config()框架 - 提供商自动检测(
registry::detect_provider(model)):claude→anthropic、gpt/o1/o3/o4→openai、gemini→gemini、deepseek→deepseek、kimi/moonshot→moonshot、qwen→dashscope、glm→zhipu、llama/mixtral→groq。模式在registry/中按提供商定义。
API 密钥解析顺序:认证存储(~/.octos/auth.json)→ 环境变量。
认证模块
OAuth PKCE(OpenAI):
- 生成 64 字符验证器(两个 UUIDv4 十六进制)
- SHA-256 挑战,base64-URL 编码(无填充)
- TCP 监听端口 1455
- 浏览器 →
auth.openai.com+ PKCE + state - 回调验证 state(CSRF),用 code+verifier 换取 token
设备码流程(OpenAI):POST deviceauth/usercode,每 5 秒以上轮询 deviceauth/token。
粘贴 Token:从 stdin 提示输入 API 密钥,以 auth_method: "paste_token" 存储。
AuthStore:~/.octos/auth.json(mode 0600)。{credentials: {provider: AuthCredential}}。
配置监视器
每 5 秒轮询。通过文件内容的 SHA-256 哈希比较。
可热重载:system_prompt、max_history(实时生效)。
需要重启:provider、model、base_url、api_key_env、sandbox、mcp_servers、hooks、gateway.queue_mode、channels。
REST API(feature:api)
| 路由 | 方法 | 说明 |
|---|---|---|
/api/chat | POST | 发送消息 → 获取响应 |
/api/chat/stream | GET | ProgressEvent 的 SSE 流 |
/api/sessions | GET | 列出所有会话 |
/api/sessions/{id}/messages | GET | 分页历史(?limit=100&offset=0,最大 500) |
/api/status | GET | 版本、模型、提供商、运行时间 |
/metrics | GET | Prometheus 文本格式(无需认证) |
/*(回退) | GET | 嵌入式 Web UI(通过 rust-embed 的静态文件) |
认证:可选的 bearer token,常量时间比较(仅 API 路由;/metrics 和静态文件为公开)。CORS:localhost:3000/8080。最大消息:1MB。
Web UI:通过 rust-embed 嵌入的 SPA,作为回退处理器提供服务。会话侧边栏、聊天界面、SSE 流式推送、暗色主题。原生 HTML/CSS/JS(无构建工具)。
Prometheus 指标:octos_tool_calls_total(计数器,标签:tool, success)、octos_tool_call_duration_seconds(直方图,标签:tool)、octos_llm_tokens_total(计数器,标签:direction)。由 metrics + metrics-exporter-prometheus crate 驱动。
会话压缩(网关)
当消息数 > 40(阈值)时触发。保留最近 10 条消息。通过 LLM 将较旧消息摘要为 <500 词。重写 JSONL 会话文件。
octos-pipeline — 基于 DOT 的流水线编排
基于 DOT 的流水线编排引擎,用于定义和执行多步骤工作流。
parser.rs— DOT 图解析器(将 Graphviz DOT 格式解析为流水线定义)graph.rs— PipelineGraph,包含节点/边类型executor.rs— 异步流水线执行引擎handler.rs— 处理器类型:CodergenHandler、GateHandler、ShellHandler、NoopHandler、DynamicParallelcondition.rs— 条件边求值(分支逻辑)tool.rs— RunPipelineTool 集成(将流水线执行暴露为 Agent 工具)validate.rs— 图验证和 lint 诊断human_gate.rs— 人在环路门,包含HumanInputProvidertrait、ChannelInputProvider(mpsc + oneshot,默认 5 分钟超时)、AutoApproveProvider。输入类型:Approval、FreeText、Choicefidelity.rs—FidelityMode枚举(Full、Truncate、Compact、Summary),用于节点间上下文传递控制。从配置字符串解析。安全上限:10MB max_chars、100K max_linesmanager.rs—PipelineManager管理器,包含SupervisionStrategy(AllOrNothing、BestEffort、RetryFailed)。重试上限 10 次,指数退避(100ms-5s)。ManagerOutcome转换为NodeOutcomethread.rs—ThreadRegistry用于跨流水线节点的 LLM 会话复用。Thread存储 model_id + 消息历史。限制:1000 线程,每线程 10000 条消息server.rs—PipelineServertrait,包含SubmitRequest(已验证:1MB DOT、256KB 输入、64 个变量、安全流水线 ID)、RunStatus生命周期(Queued → Running → Completed/Failed/Cancelled)artifact.rs— 流水线中间产物存储checkpoint.rs— 流水线检查点/恢复,用于崩溃恢复events.rs— 流水线事件系统,用于进度跟踪run_dir.rs— 按运行隔离的工作目录stylesheet.rs— 流水线图渲染的视觉样式
数据流
Chat 模式
用户输入 → readline → Agent.process_message(input, history)
│
├─ 构建消息(系统提示 + 历史 + 记忆 + 输入)
├─ trim_to_context_window()(必要时)
├─ 通过 chat_stream() 调用 LLM,附带工具规格
├─ 如果 ToolUse 则执行工具(循环)
└─ 返回 ConversationResponse
│
打印响应,追加到历史
网关模式
频道 → InboundMessage → MessageBus → [转录音频] → [加载会话]
│
Agent.process_message()
│
OutboundMessage
│
ChannelManager.dispatch()
│
coalesce() → Channel.send()
系统消息(cron、心跳、spawn 结果)通过相同的总线流转,channel: "system" 和 metadata 路由。
Feature Flags
# octos-bus
telegram = ["teloxide"]
discord = ["serenity"]
slack = ["tokio-tungstenite"]
whatsapp = ["tokio-tungstenite"]
feishu = ["tokio-tungstenite"]
email = ["async-imap", "tokio-rustls", "rustls", "webpki-roots", "lettre", "mailparse"]
# octos-agent (browser is always compiled in, no longer feature-gated)
git = ["gix"] # git operations via gitoxide
ast = ["tree-sitter"] # code_structure.rs AST analysis
admin-bot = [...] # admin/ directory tools
# octos-bus (additional)
wecom = [...] # WeCom/WeChat Work channel
twilio = [...] # Twilio SMS/MMS channel
# octos-cli
api = ["axum", "tower-http", "futures"]
telegram = ["octos-bus/telegram"]
discord = ["octos-bus/discord"]
slack = ["octos-bus/slack"]
whatsapp = ["octos-bus/whatsapp"]
feishu = ["octos-bus/feishu"]
email = ["octos-bus/email"]
wecom = ["octos-bus/wecom"]
twilio = ["octos-bus/twilio"]
文件布局
crates/
├── octos-core/src/
│ ├── lib.rs, task.rs, types.rs, error.rs, gateway.rs, message.rs, utils.rs
├── octos-llm/src/
│ ├── lib.rs, provider.rs, config.rs, types.rs, retry.rs, failover.rs, sse.rs
│ ├── embedding.rs, pricing.rs, context.rs, transcription.rs, vision.rs
│ ├── adaptive.rs, swappable.rs, router.rs, ominix.rs
│ ├── anthropic.rs, openai.rs, gemini.rs, openrouter.rs (protocol impls)
│ └── registry/ (mod.rs + 14 provider entries: anthropic, openai, gemini,
│ openrouter, deepseek, groq, moonshot, dashscope, minimax,
│ zhipu, zai, nvidia, ollama, vllm)
├── octos-memory/src/
│ ├── lib.rs, episode.rs, store.rs, memory_store.rs, hybrid_search.rs
├── octos-agent/src/
│ ├── lib.rs, agent.rs, progress.rs, policy.rs, compaction.rs, sanitize.rs, hooks.rs
│ ├── sandbox.rs, mcp.rs, skills.rs, builtin_skills.rs
│ ├── bundled_app_skills.rs, bootstrap.rs, prompt_guard.rs
│ ├── plugins/ (mod.rs, loader.rs, manifest.rs, tool.rs)
│ ├── skills/ (cron, skill-store, skill-creator SKILL.md)
│ └── tools/ (mod, policy, shell, read_file, write_file, edit_file, diff_edit,
│ list_dir, glob_tool, grep_tool, web_search, web_fetch,
│ message, spawn, browser, ssrf, tool_config,
│ deep_search, site_crawl, recall_memory, save_memory,
│ send_file, take_photo, code_structure, git,
│ deep_research_pipeline, synthesize_research, research_utils,
│ admin/ (profiles, skills, sub_accounts, system,
│ platform_skills, update))
├── octos-bus/src/
│ ├── lib.rs, bus.rs, channel.rs, session.rs, coalesce.rs, media.rs
│ ├── cli_channel.rs, telegram_channel.rs, discord_channel.rs
│ ├── slack_channel.rs, whatsapp_channel.rs, feishu_channel.rs, email_channel.rs
│ ├── wecom_channel.rs, twilio_channel.rs, markdown_html.rs
│ ├── cron_service.rs, cron_types.rs, heartbeat.rs
└── octos-cli/src/
├── main.rs, config.rs, config_watcher.rs, cron_tool.rs, compaction.rs
├── auth/ (mod.rs, store.rs, oauth.rs, token.rs)
├── api/ (mod.rs, router.rs, handlers.rs, sse.rs, metrics.rs, static_files.rs)
└── commands/ (mod, chat, init, status, gateway, clean,
completions, cron, channels, auth, skills, docs, serve,
office, account)
├── octos-pipeline/src/
│ ├── lib.rs, parser.rs, graph.rs, executor.rs, handler.rs
│ ├── condition.rs, tool.rs, validate.rs
安全
工作区级安全
#![deny(unsafe_code)]— 通过[workspace.lints.rust]设置的工作区级 lintsecrecy::SecretString— 所有提供商 API 密钥都被包装;防止意外日志/显示
认证与凭据
- API 密钥:认证存储(
~/.octos/auth.json,mode 0600)优先于环境变量 - 带 SHA-256 挑战的 OAuth PKCE,state 参数(CSRF 保护)
- API bearer token 使用常量时间字节比较(防时序攻击)
执行沙箱
- 三种后端:bwrap(Linux)、sandbox-exec(macOS)、Docker —
SandboxMode::Auto检测 - 18 个 BLOCKED_ENV_VARS 在所有沙箱后端、MCP 服务器启动、钩子和浏览器工具中共享:
LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH, DYLD_FALLBACK_LIBRARY_PATH, DYLD_VERSIONED_LIBRARY_PATH, NODE_OPTIONS, PYTHONSTARTUP, PYTHONPATH, PERL5OPT, RUBYOPT, RUBYLIB, JAVA_TOOL_OPTIONS, BASH_ENV, ENV, ZDOTDIR - 按后端的路径注入防护(Docker:
:、\0、\n、\r;macOS:控制字符、(、)、\、") - Docker:
--cap-drop ALL、--security-opt no-new-privileges、--network none,阻止绑定挂载源(docker.sock、/proc、/sys、/dev、/etc)
工具安全
- ShellTool SafePolicy:拒绝
rm -rf /、dd、mkfs、fork 炸弹、chmod -R 777 /;询问sudo、rm -rf、git push --force、git reset --hard。匹配前空白归一化。超时钳制到 [1, 600] 秒。SIGTERM→宽限期→SIGKILL 子进程清理。 - 工具策略:allow/deny,deny 优先语义,8 个命名分组(
group:fs、group:runtime、group:web、group:search、group:sessions等),通配符匹配,按提供商过滤(tools.byProvider) - 工具参数大小限制:每次调用 1MB(非分配的
estimate_json_size,含转义字符计算) - 符号链接安全文件 I/O:Unix 上通过
O_NOFOLLOW实现原子级内核检查,消除 TOCTOU 竞态;Windows 上使用基于元数据的符号链接检查回退 - SSRF 防护在共享的
ssrf.rs模块中:DNS 解析失败时采用故障关闭策略(DNS 失败时阻止请求),私有 IP 阻止(10/8、172.16/12、192.168/16、169.254/16),IPv6 覆盖(ULAfc00::/7、链路本地fe80::/10、站点本地fec0::/10、IPv4 映射::ffff:0:0/96、IPv4 兼容::/96),回环地址阻止。被 web_fetch 和 browser 使用。 - 浏览器:URL scheme 白名单(仅 http/https)、10 秒 JS 执行超时、僵尸进程清理、截图使用安全临时文件
- MCP:输入 schema 验证(最大深度 10,最大大小 64KB)防止恶意工具定义
- 提示注入防护(
prompt_guard.rs):5 种威胁类别(SystemOverride、RoleConfusion、ToolCallInjection、SecretExtraction、InstructionInjection),10 种检测模式。检测到的威胁被包裹在[injection-blocked:...]中进行清理。
数据安全
- 工具输出清理(
sanitize.rs):剥离 base64 数据 URI、长十六进制字符串(64+ 字符),以及凭据脱敏 — 7 个正则表达式覆盖 OpenAI(sk-...)、Anthropic(sk-ant-...)、AWS(AKIA...)、GitHub(ghp_/gho_/ghs_/ghr_/github_pat_...)、GitLab(glpat-...)、Bearer token 和通用password/api_key赋值 - 通过
truncate_utf8()在所有工具输出和邮件正文中实现 UTF-8 安全截断 - 通过百分号编码文件名 + 截断时的哈希后缀防止会话文件冲突
- 会话文件大小限制:最大 10MB 防止损坏文件导致 OOM
- 原子写入-重命名用于会话持久化(崩溃安全)
- API 服务器默认绑定到 127.0.0.1(非 0.0.0.0)
- 通过
allowed_senders列表进行频道访问控制 - MCP 响应限制:每条 JSON-RPC 行 1MB(DoS 防护)
- 消息合并:MAX_CHUNKS=50 DoS 限制
- API 消息限制:每个请求 1MB
并发模型
为什么选择 Rust
octos 使用 Rust + tokio 异步运行时,与 Python(OpenClaw 等)和 Node.js(NanoCloud 等)Agent 框架相比,在并发会话处理方面具有显著优势:
真正的并行 — Tokio 任务跨所有 CPU 核心同时运行。Python 有 GIL,即使使用 asyncio,CPU 密集型工作(JSON 解析、上下文压缩、token 计数)也是单核的。Node.js 完全是单线程的。在 octos 中,10 个并发会话进行上下文压缩实际上会跨核心并行执行。
内存效率 — 无垃圾回收器,无每对象运行时开销。Agent 会话是堆上的紧凑结构体。Python Agent 会话携带解释器开销、每个对象的 GC 元数据和基于 dict 的属性查找。在数百个会话和大量对话历史都在内存中时,这一点很重要。
无 GC 暂停 — Python 和 Node.js 的 GC 可能导致响应中途的延迟尖峰。Rust 有确定性的内存释放 — 当拥有者结构体 drop 时内存立即释放。
单二进制部署 — 无需安装 Python/Node 运行时,无依赖地狱,可预测的资源使用。网关是一个静态二进制文件。
Tokio 任务 vs 操作系统线程
所有并发会话处理使用 tokio 任务(绿色线程),而非操作系统线程。tokio 任务是堆上的状态机(约几 KB)。操作系统线程约 8MB 栈。数千个任务复用在少量操作系统线程上(默认为 CPU 核心数)。由于 Agent 会话大部分时间都在等待 I/O(LLM API 响应),它们会高效地让出线程给其他任务。
网关并发
入站消息 → 主循环
│
├─ tokio::spawn() 每条消息
│ │
│ ├─ Semaphore(max_concurrent_sessions,默认 10)
│ │ 限制总并发 Agent 运行数
│ │
│ └─ 按会话的 Mutex
│ 序列化同一会话内的消息
│
└─ 不同会话并发运行
同一会话顺序排队
- 跨会话:并发,由
max_concurrent_sessions信号量限制(默认 10) - 同一会话内:通过按会话 mutex 序列化 — 防止对话历史的竞态条件
- 按会话锁:完成后修剪(Arc strong_count == 1)以防止 HashMap 无限增长
工具执行
在单次 Agent 迭代内,一个 LLM 响应中的所有工具调用通过 join_all() 并发执行:
LLM 响应:[web_search, read_file, send_email]
│ │ │
└────────────┼───────────┘
join_all()
┌────────────┼───────────┐
│ │ │
完成 完成 完成
↓
所有结果追加到消息
↓
下一次 LLM 调用
子 Agent 模式(spawn 工具)
| 方面 | 同步 | 后台 |
|---|---|---|
| 父 Agent 是否阻塞? | 是 | 否(tokio::spawn()) |
| 结果传递 | 同一对话轮次 | 通过网关的新入站消息 |
| Token 计算 | 计入父预算 | 独立 |
| 使用场景 | 顺序流水线 | 触发后不管的长任务 |
子 Agent 不能再生成子 Agent(spawn 工具在子 Agent 策略中始终被拒绝)。
多租户仪表板
仪表板(octos serve)将每个用户配置文件作为独立的网关操作系统进程运行:
Dashboard (octos serve)
├─ Profile "alice" → octos gateway --config alice.json (deepseek, own semaphore)
├─ Profile "bob" → octos gateway --config bob.json (kimi, own semaphore)
└─ Profile "carol" → octos gateway --config carol.json (openai, own semaphore)
每个配置文件拥有自己的 LLM 提供商、API 密钥、频道、数据目录和 max_concurrent_sessions 信号量。配置文件完全隔离 — 网关进程间无共享状态。
测试
全部 crate 共 1300+ 测试。完整清单和 CI 指南见 TESTING.md。
- 单元测试:类型 serde 往返、工具参数解析、配置验证、提供商检测、工具策略、压缩、合并、BM25 评分、L2 归一化、SSE 解析
- 自适应路由:Off/Hedge/Lane 模式、熔断器、故障转移、评分、指标、提供商竞速(19 个测试)
- 响应性:基线学习、劣化检测、恢复、阈值边界(8 个测试)
- 队列模式:Followup、Collect、Steer、Speculative 溢出、自动升级/降级(9 个测试)
- 会话持久化:JSONL 存储、LRU 淘汰、分支、重写、时间戳排序、并发访问(28 个测试)
- 集成测试:CLI 命令、文件工具、定时任务、会话分支、插件加载
- 安全测试:沙箱路径注入、环境清理、SSRF 阻断、符号链接拒绝(O_NOFOLLOW)、私有 IP 检测、去重溢出、工具参数大小限制、会话文件大小限制、熔断器阈值边界、MCP schema 验证
- 频道测试:allowed_senders、消息解析、去重逻辑、邮件地址提取
本地 CI:./scripts/ci.sh(与 GitHub Actions 一致 + 针对性子系统测试)。见 TESTING.md。
测试指南
快速开始
# 完整的本地 CI(与 GitHub Actions 一致)
./scripts/ci.sh
# 快速迭代(跳过 clippy)
./scripts/ci.sh --quick
# 自动修复格式
./scripts/ci.sh --fix
# 内存受限的机器
./scripts/ci.sh --serial
CI 流水线
scripts/ci.sh 运行与 .github/workflows/ci.yml 相同的检查,外加针对性的子系统测试。
步骤
| 步骤 | 命令 | 标志 |
|---|---|---|
| 1. 格式化 | cargo fmt --all -- --check | --fix 自动修复 |
| 2. Clippy | cargo clippy --workspace -- -D warnings | --quick 跳过 |
| 3. 工作区测试 | cargo test --workspace | --serial 单线程 |
| 4. 针对性分组 | 按子系统测试(见下文) | 始终运行 |
针对性测试分组
在完整工作区测试之后,CI 脚本会单独重新运行关键子系统,以便清晰地暴露失败:
| 分组 | Crate | 测试过滤器 | 数量 | 覆盖内容 |
|---|---|---|---|---|
| 自适应路由 | octos-llm | adaptive::tests | 19 | Off/Hedge/Lane 模式、熔断器、故障转移、评分、指标、竞速 |
| 响应性 | octos-llm | responsiveness::tests | 8 | 基线学习、劣化检测、恢复、阈值边界 |
| 会话 actor | octos-cli | session_actor::tests | 9 | 队列模式、Speculative 溢出、自动升级/降级 |
| 会话持久化 | octos-bus | session::tests | 28 | JSONL 存储、LRU 淘汰、分支、重写、时间戳排序 |
会话 actor 测试始终以单线程运行(--test-threads=1),因为它们会启动完整的 actor 和 mock 提供商,并行执行可能导致 OOM。
功能覆盖
自适应路由(crates/octos-llm/src/adaptive.rs — 19 个测试)
测试管理多个 LLM 提供商的 AdaptiveRouter,基于指标驱动选择。
Off 模式(静态优先级)
| 测试 | 验证内容 |
|---|---|
test_selects_primary_on_cold_start | 首次调用时的优先级顺序(尚无指标) |
test_lane_changing_off_uses_priority_order | Off 模式忽略延迟差异 |
test_lane_changing_off_skips_circuit_broken | Off 模式仍然遵守熔断器 |
test_hedged_off_uses_single_provider | Off 模式使用优先级,不竞速 |
Hedge 模式(提供商竞速)
| 测试 | 验证内容 |
|---|---|
test_hedged_racing_picks_faster_provider | 通过 tokio::select! 竞速 2 个提供商,更快者胜出 |
test_hedged_racing_survives_one_failure | 主竞速者失败时回退到备选 |
test_hedge_single_provider_falls_through | 只有 1 个提供商时 Hedge 使用单提供商路径 |
Lane 模式(基于评分的选择)
| 测试 | 验证内容 |
|---|---|
test_lane_mode_picks_best_by_score | 指标预热后切换到更快的提供商 |
熔断器与故障转移
| 测试 | 验证内容 |
|---|---|
test_circuit_breaker_skips_degraded | 连续 N 次失败后跳过提供商 |
test_failover_on_error | 主提供商失败时转移到下一个 |
test_all_providers_fail | 所有提供商都失败时返回错误 |
评分与指标
| 测试 | 验证内容 |
|---|---|
test_scoring_cold_start_respects_priority | 冷启动评分遵循配置优先级 |
test_latency_samples_p95 | 从环形缓冲区计算 P95 |
test_metrics_snapshot | 延迟/成功/失败正确记录 |
test_metrics_export_after_calls | 导出包含按提供商的指标 |
运行时控制
| 测试 | 验证内容 |
|---|---|
test_mode_switch_at_runtime | Off → Hedge → Lane → Off 切换 |
test_qos_ranking_toggle | QoS 排名切换与模式正交 |
test_adaptive_status_reports_correctly | 状态结构体反映当前模式/数量 |
test_empty_router_panics | 断言至少需要 1 个提供商 |
响应性观察器(crates/octos-llm/src/responsiveness.rs — 8 个测试)
测试驱动自动升级的延迟跟踪器。
基线学习
| 测试 | 验证内容 |
|---|---|
test_baseline_learning | 从前 5 个样本建立基线 |
test_sample_count_tracking | sample_count() 返回正确值 |
劣化检测
| 测试 | 验证内容 |
|---|---|
test_degradation_detection | 3 次连续慢请求(> 3 倍基线)触发激活 |
test_at_threshold_boundary_not_triggered | 恰好在阈值处的延迟不视为“慢“ |
test_no_false_trigger_before_baseline | 基线建立前不会激活 |
恢复与生命周期
| 测试 | 验证内容 |
|---|---|
test_recovery_detection | 激活后 1 次快速请求触发停用 |
test_multiple_activation_cycles | 激活 → 停用 → 再激活正常工作 |
test_window_caps_at_max_size | 滚动窗口保持在 20 条 |
队列模式与会话 Actor(crates/octos-cli/src/session_actor.rs — 9 个测试)
测试拥有消息处理、队列策略和自动保护的按会话 actor。
Mock 基础设施: DelayedMockProvider — 可配置延迟 + 脚本化 FIFO 响应。setup_speculative_actor / setup_actor_with_mode — 构建带有指定队列模式和可选自适应路由器的最小 actor。
队列模式:Followup
| 测试 | 验证内容 |
|---|---|
test_queue_mode_followup_sequential | 每条消息独立处理 — 3 条消息产生 3 个响应,全部独立出现在会话历史中 |
队列模式:Collect
| 测试 | 验证内容 |
|---|---|
test_queue_mode_collect_batches | 慢 LLM 调用期间排队的消息被批量合并为一个组合提示("msg2\n---\nQueued #1: msg3") |
队列模式:Steer
| 测试 | 验证内容 |
|---|---|
test_queue_mode_steer_keeps_newest | 较旧的排队消息被丢弃,只处理最新的 — 被丢弃的消息不出现在会话历史中 |
队列模式:Speculative
| 测试 | 验证内容 |
|---|---|
test_speculative_overflow_concurrent | 慢主 Agent 期间生成溢出作为完整 Agent 任务(12 秒 > 10 秒耐心值);两个响应都到达;历史按时间戳排序 |
test_speculative_within_patience_drops | 主 Agent 在耐心值内时溢出被丢弃(5 秒 < 10 秒);只有 1 个响应到达 |
test_speculative_handles_background_result | BackgroundResult 消息在 Speculative 的 select! 循环中被处理,不产生额外的 LLM 调用 |
自动升级 / 降级
| 测试 | 验证内容 |
|---|---|
test_auto_escalation_on_degradation | 5 次快速预热(基线 100ms)→ 3 次慢调用(400ms > 3 倍)→ 模式切换为 Hedge + Speculative,用户收到通知 |
test_auto_deescalation_on_recovery | 升级后 1 次快速响应 → 模式恢复为 Off + Followup,路由器确认 Off |
工具函数
| 测试 | 验证内容 |
|---|---|
test_strip_think_tags | 从 LLM 输出中移除 <think>...</think> 块 |
会话持久化(crates/octos-bus/src/session.rs — 28 个测试)
测试基于 JSONL 的会话存储和 LRU 缓存。
CRUD 与持久化
| 测试 | 验证内容 |
|---|---|
test_session_manager_create_and_retrieve | 创建会话、添加消息、检索 |
test_session_manager_persistence | 消息在管理器重启后存活(磁盘重载) |
test_session_manager_clear | 清除从内存和磁盘中删除 |
历史与排序
| 测试 | 验证内容 |
|---|---|
test_session_get_history | 尾部切片返回最后 N 条消息 |
test_session_get_history_all | 不足最大值时返回全部 |
test_sort_by_timestamp_restores_order | 并发溢出写入后恢复时间顺序 |
LRU 缓存
| 测试 | 验证内容 |
|---|---|
test_eviction_keeps_max_sessions | 缓存遵守容量限制 |
test_evicted_session_reloads_from_disk | 被淘汰的会话访问时从磁盘重载 |
test_with_max_sessions_clamps_zero | 容量下限钳制为 1 |
并发
| 测试 | 验证内容 |
|---|---|
test_concurrent_sessions | 多个会话互不干扰 |
test_concurrent_session_processing | 10 个并行任务不会损坏会话 |
分支与重写
| 测试 | 验证内容 |
|---|---|
test_fork_creates_child | 分支复制最后 N 条消息并带有父链接 |
test_fork_persists_to_disk | 分支的会话在重启后存活 |
test_session_rewrite | 变更后的原子写入-重命名 |
多会话(主题)
| 测试 | 验证内容 |
|---|---|
test_list_sessions_for_chat | 列出某个聊天的所有主题会话 |
test_session_topic_persists | 主题在重启后存活 |
test_update_summary | 摘要更新持久化 |
test_active_session_store | 活跃主题切换和返回 |
test_active_session_store_persistence | 活跃主题在重启后存活 |
test_validate_topic_name | 拒绝无效字符和长度 |
文件名编码
| 测试 | 验证内容 |
|---|---|
test_truncated_session_keys_no_collision | 带哈希后缀的长键不会冲突 |
test_decode_filename | 百分号编码的文件名正确解码 |
test_list_sessions_returns_decoded_keys | list_sessions() 返回人类可读的键 |
test_short_key_no_hash_suffix | 短键不添加哈希后缀 |
安全限制
| 测试 | 验证内容 |
|---|---|
test_load_rejects_oversized_file | 超过 10 MB 的文件被拒绝 |
test_append_respects_file_size_limit | 文件达到 10 MB 限制时追加被跳过 |
test_load_rejects_future_schema_version | 拒绝未知的 schema 版本 |
test_purge_stale_sessions | 删除超过 N 天的会话 |
已知空白
| 领域 | 未测试原因 |
|---|---|
| Interrupt 队列模式 | 与 Steer 共用代码路径 — 由 test_queue_mode_steer_keeps_newest 覆盖 |
| 探测/金丝雀请求 | 在所有测试中通过 probe_probability: 0.0 禁用以确保确定性 |
流式推送(chat_stream) | 无 mock 流式基础设施;流式功能通过手动测试 |
| 会话压缩 | 在 actor 测试中调用但未验证输出(需要 LLM mock 进行摘要) |
| 实际提供商集成 | 需要 API 密钥;有 1 个测试但标记为 #[ignore] |
| 频道特定路由 | 由频道 crate 测试覆盖,不属于此子系统 |
| “Earlier task” 标记 | 当溢出已响应时主响应添加 “Earlier task completed:” 前缀;测试中未直接断言(需要在慢主 + 快溢出竞速后检查出站内容) |
| 溢出 Agent 工具执行 | serve_overflow 启动完整的 agent.process_message_tracked() 并带有工具访问权限;当前测试使用 DelayedMockProvider 返回预设响应而不进行工具调用 |
运行单个测试
# 单个测试
cargo test -p octos-llm --lib adaptive::tests::test_hedged_racing_picks_faster_provider
# 一个子系统
cargo test -p octos-llm --lib adaptive::tests
# 会话 actor(始终单线程)
cargo test -p octos-cli session_actor::tests -- --test-threads=1
# 带输出
cargo test -p octos-cli session_actor::tests -- --test-threads=1 --nocapture
GitHub Actions CI
.github/workflows/ci.yml 在 push/PR 到 main 时运行:
cargo fmt --all -- --checkcargo clippy --workspace -- -D warningscargo test --workspace
本地的 scripts/ci.sh 是其超集 – 除了运行相同的三个步骤外,还包含针对性的子系统测试组。如果本地 CI 通过,GitHub 上也会通过。
运行器: macos-14(ARM64)。私有仓库每月 2000 分钟免费额度(macOS runner 有 10 倍乘数 = 约 200 有效分钟)。
文件
| 文件 | 用途 |
|---|---|
scripts/ci.sh | 本地 CI 脚本(本文档描述) |
scripts/pre-release.sh | 完整的发布前冒烟测试(构建、端到端、技能二进制) |
.github/workflows/ci.yml | GitHub Actions CI |
crates/octos-llm/src/adaptive.rs | 自适应路由器 + 19 个测试 |
crates/octos-llm/src/responsiveness.rs | 响应性观察器 + 8 个测试 |
crates/octos-cli/src/session_actor.rs | 会话 actor + 9 个测试 |
crates/octos-bus/src/session.rs | 会话持久化 + 28 个测试 |