Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

简介

🌐 English Documentation

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

安装与部署

前置条件

条件版本备注
Rust1.85.0+通过 rustup.rs 安装
macOS13+Apple Silicon 或 Intel
Linuxglibc 2.31+Ubuntu 20.04+、Debian 11+、Fedora 34+
Windows10/11原生编译或 WSL2

你还需要至少一个受支持的 LLM 供应商的 API 密钥。

可选依赖

依赖用途安装方式
Node.jsWhatsApp 桥接、PPTX 创建技能brew install node / apt install nodejs
ffmpeg媒体/视频技能brew install ffmpeg / apt install ffmpeg
Chrome/Chromium浏览器自动化工具brew install --cask chromium
LibreOfficeOffice 文档转换brew install --cask libreoffice
PopplerPDF 渲染(pdftoppmbrew 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)。

脚本执行流程:

  1. 检查前置条件(Rust、平台依赖)
  2. 使用所选特性编译 octos 二进制文件
  3. 编译应用技能二进制文件(除非指定了 --no-skills
  4. 在 macOS 上对二进制文件进行签名(ad-hoc codesign)
  5. 创建运行时数据目录,并写入 ~/.octos/config.json,其中 mode"local""tenant"
  6. 在启用 dashboard/API 功能时创建后台服务
  7. 在租户部署场景下可选配置 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.logjournalctl --user -u octos-serve
找不到 API 密钥确保环境变量在服务环境中已设置,而不仅仅在你的 Shell 中

配置

配置文件位置

配置文件按以下顺序加载(找到第一个即生效):

  1. .octos/config.json – 项目级配置
  2. ~/.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_KEYAnthropic (Claude) API 密钥
OPENAI_API_KEYOpenAI API 密钥
GEMINI_API_KEYGoogle Gemini API 密钥
OPENROUTER_API_KEYOpenRouter API 密钥
DEEPSEEK_API_KEYDeepSeek API 密钥
GROQ_API_KEYGroq API 密钥
MOONSHOT_API_KEYMoonshot/Kimi API 密钥
DASHSCOPE_API_KEY阿里云 DashScope (Qwen) API 密钥
MINIMAX_API_KEYMiniMax API 密钥
ZHIPU_API_KEY智谱 (GLM) API 密钥
ZAI_API_KEYZ.AI API 密钥
NVIDIA_API_KEYNvidia NIM API 密钥

搜索

变量说明
BRAVE_API_KEYBrave Search API 密钥
PERPLEXITY_API_KEYPerplexity Sonar API 密钥
YDC_API_KEYYou.com API 密钥

渠道

变量说明
TELEGRAM_BOT_TOKENTelegram 机器人令牌
DISCORD_BOT_TOKENDiscord 机器人令牌
SLACK_BOT_TOKENSlack 机器人令牌
SLACK_APP_TOKENSlack 应用级令牌
FEISHU_APP_ID飞书/Lark 应用 ID
FEISHU_APP_SECRET飞书/Lark 应用密钥
WECOM_CORP_ID企业微信企业 ID
WECOM_AGENT_SECRET企业微信应用密钥
EMAIL_USERNAME邮箱账户用户名
EMAIL_PASSWORD邮箱账户密码

邮件(send-email 技能)

变量说明
SMTP_HOSTSMTP 服务器主机名
SMTP_PORTSMTP 服务器端口
SMTP_USERNAMESMTP 用户名
SMTP_PASSWORDSMTP 密码
SMTP_FROMSMTP 发件人地址
LARK_APP_ID飞书邮箱应用 ID
LARK_APP_SECRET飞书邮箱应用密钥
LARK_FROM_ADDRESS飞书邮箱发件人地址

语音

变量说明
OMINIX_API_URLOminiX 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 格式别名
anthropicANTHROPIC_API_KEYclaude-sonnet-4-20250514Native Anthropic
openaiOPENAI_API_KEYgpt-4oNative OpenAI
geminiGEMINI_API_KEYgemini-2.0-flashNative Gemini
openrouterOPENROUTER_API_KEYanthropic/claude-sonnet-4-20250514Native OpenRouter
deepseekDEEPSEEK_API_KEYdeepseek-chatOpenAI 兼容
groqGROQ_API_KEYllama-3.3-70b-versatileOpenAI 兼容
moonshotMOONSHOT_API_KEYkimi-k2.5OpenAI 兼容kimi
dashscopeDASHSCOPE_API_KEYqwen-maxOpenAI 兼容qwen
minimaxMINIMAX_API_KEYMiniMax-Text-01OpenAI 兼容
zhipuZHIPU_API_KEYglm-4-plusOpenAI 兼容glm
zaiZAI_API_KEYglm-5Anthropic 兼容z.ai
nvidiaNVIDIA_API_KEYmeta/llama-3.3-70b-instructOpenAI 兼容nim
ollama(无需)llama3.2OpenAI 兼容
vllmVLLM_API_KEY(须指定)OpenAI 兼容

配置方式

配置文件

config.json 中设置 providermodel

{
  "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_retries3每个服务商的重试次数
initial_delay1s首次重试延迟
max_delay60s最大重试延迟
backoff_multiplier2.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_rankingfalse启用 QoS 质量排名(使用模型目录评分)
latency_threshold_ms10000内部使用的软惩罚阈值
error_rate_threshold0.3错误率超过此值的服务商将被降低优先级
probe_probability0.1发送至非主服务商作为健康探测的请求比例
probe_interval_secs60对同一服务商两次探测之间的最小间隔(秒)
failure_threshold3连续失败多少次后触发熔断

评分公式(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_rate0.3混合基线 + 实时错误率。EMA 混合权重在 10 次调用中从 0 渐变到 1。
质量weight_latency0.360% 归一化 ds_output 质量 + 40% 归一化吞吐量(输出 tokens/秒 EMA)
优先级weight_priority0.2配置顺序偏好(0=主服务商,越高越靠后)。归一化到 [0, 1]。
成本weight_cost0.2归一化的每百万 token 输出价格。未知成本 → 0(无惩罚)。

目录可以从 model_catalog.json 基准文件预填充,使路由器在启动时即具备参考评分而非冷启动启发。

自动升级

当检测到持续的延迟恶化时,会话 actor 会自动激活对冲模式 + 投机队列:

  • ResponsivenessObserver 从前 5 次请求学习中位数基线(对异常值鲁棒),然后通过 80/20 EMA 每隔 20 个样本自适应调整基线。
  • 如果连续 3 次 LLM 响应超过 3×基线 延迟,对冲竞速和投机队列同时启用。
  • 当服务商恢复(一次正常延迟响应)时,两者都恢复为 Followup 和静态路由。

服务商包装栈

路由系统由分层包装器组成:

包装器用途
AdaptiveRouter顶层:指标驱动评分、对冲/选道模式、熔断器、探测请求
ProviderChain有序故障转移,带每服务商熔断器(失败次数 >= 阈值 → 降级)
FallbackProvider主服务商 + 按QoS排名的备选,通过 ProviderRouter 追踪冷却
RetryProvider429/5xx 指数退避。超时 → 不重试(改为转移)
ProviderRouter子 Agent 多模型路由。前缀键解析、冷却、QoS评分备选
SwappableProvider通过 RwLock 实现运行时模型切换(如 switch_model 工具)。每次切换泄漏约 50 字节

网关与频道

Octos 以网关模式运行,将各消息平台桥接到你的 LLM 智能体。每个平台连接称为一个频道。你可以在同一个网关进程中同时运行多个频道——例如同时接入 Telegram 和 Slack。

频道概览

频道在 config.jsongateway.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"
  }
}

WhatsApp

需要一个运行在 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

开发者控制台配置

  1. 前往 open.larksuite.com/app,创建或选择一个应用
  2. 在 Features 下添加 Bot 能力
  3. 配置事件订阅:
    • Events & Callbacks > Event Configuration > Edit subscription method
    • 选择 “Send events to developer server”
    • 将请求 URL 设为 https://YOUR_NGROK_URL/webhook/event
  4. 添加事件:im.message.receive_v1(接收消息)
  5. 启用权限:im:messageim:message:send_as_botim:resource
  6. 发布应用: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_envApp ID 的环境变量名FEISHU_APP_ID
app_secret_envApp Secret 的环境变量名FEISHU_APP_SECRET
region"cn"(飞书)或 "global" / "lark"(Lark 国际版)"cn"
mode"ws"(WebSocket)或 "webhook"(HTTP)"ws"
webhook_portwebhook HTTP 服务端口9321
encrypt_keyLark 控制台的 Encrypt Key(用于 AES-256-CBC)
verification_tokenLark 控制台的 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 端点返回 404Lark 国际版不支持 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 频道

配置步骤

  1. 企业微信管理后台的“应用管理 > 群机器人“中创建一个企业微信群机器人,记下 Bot ID 和 Secret。

  2. 配置 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"
  }
}
  1. 构建并启动:
cargo build --release -p octos-cli --features "wecom-bot"
octos gateway
  1. 安装 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。


消息合并

长回复会自动拆分为符合频道限制的分段:

频道每条消息最大字符数
Telegram4000
Discord1900
Slack3900

拆分优先级:段落边界 > 换行符 > 句末 > 空格 > 硬截断。


配置热更新

网关会自动检测配置文件变更:

  • 热更新(无需重启):系统提示词、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 文件,包含智能体对特定主题的所有认知。

工作原理:

  1. 摘要注入提示词 — 每个实体的第一个非标题行成为一行摘要。所有摘要被注入系统提示词,为智能体提供一个精简的知识索引。
  2. 按需加载全文 — 智能体使用 recall_memory 工具在需要详情时加载特定实体的完整内容。
  3. 智能体自主管理 — 智能体通过 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 抓取头条和全文内容。智能体会将原始数据整理为格式化的新闻摘要。

参数:

参数类型默认值说明
categoriesarray全部要获取的新闻分类
language"zh" / "en""zh"输出语言

分类:politicsworldbusinesstechnologyscienceentertainmenthealthsports

配置:

/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>/

参数类型默认值说明
querystring(必填)研究主题或问题
depth1–32研究深度级别
max_results1–108每轮搜索的结果数
search_enginestringautoperplexityduckduckgobraveyou

深度级别:

  • 1(快速): 单轮搜索,约 1 分钟,最多 10 个页面
  • 2(标准): 3 轮搜索 + 引用链追踪,约 3 分钟,最多 30 个页面
  • 3(深入): 5 轮搜索 + 积极链接追踪,约 5 分钟,最多 50 个页面

深度爬取

工具: deep_crawl | 依赖: PATH 中需有 Chrome/Chromium

使用无头 Chrome 通过 CDP 递归爬取网站。渲染 JavaScript,通过 BFS 跟踪同源链接,提取干净文本。

参数类型默认值说明
urlstring(必填)起始 URL
max_depth1–103最大链接跟踪深度
max_pages1–20050最大爬取页面数
path_prefixstring仅跟踪此路径下的链接

输出保存到 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 发送邮件(根据可用的环境变量自动检测)。

参数类型默认值说明
tostring(必填)收件人邮箱地址
subjectstring(必填)邮件主题
bodystring(必填)邮件正文(纯文本或 HTML)
htmlbooleanfalse将正文视为 HTML
attachmentsarray文件附件(仅 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_weatherget_forecast | API: Open-Meteo(免费,无需密钥)

参数类型默认值说明
citystring(必填)英文城市名
days1–167预报天数(仅预报)

时钟

工具: get_time

返回任意 IANA 时区的当前日期、时间、星期和 UTC 偏移。

参数类型默认值说明
timezonestring服务器本地时区IANA 时区名称(如 Asia/ShanghaiUS/Eastern

账户管理

工具: manage_account

管理当前配置下的子账户。操作:listcreateupdatedeleteinfostartstoprestart


平台技能(ASR/TTS)

平台技能提供设备端语音转写和合成。需要在 Apple Silicon(M1/M2/M3/M4)上运行 OminiX 后端。

语音转写

工具: voice_transcribe

参数类型默认值说明
audio_pathstring(必填)音频文件路径(WAV、OGG、MP3、FLAC、M4A)
languagestring"Chinese""Chinese""English""Japanese""Korean""Cantonese"

语音合成

工具: voice_synthesize

参数类型默认值说明
textstring(必填)要合成的文本
output_pathstring自动输出文件路径
languagestring"chinese""chinese""english""japanese""korean"
speakerstring"vivian"语音预设

可用语音: vivianserenaryanaidenericdylan(英/中)、uncle_fu(仅中文)、ono_anna(日语)、sohee(韩语)

语音克隆

工具: voice_clone_synthesize

使用 3–10 秒参考音频样本的克隆语音进行语音合成。

参数类型默认值说明
textstring(必填)要合成的文本
reference_audiostring(必填)参考音频路径
languagestring"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"   # 搜索在线注册表

技能解析顺序

技能按以下目录加载(优先级从高到低):

  1. .octos/plugins/(旧版兼容)
  2. .octos/skills/(用户安装的自定义技能)
  3. .octos/bundled-app-skills/(预装应用技能)
  4. .octos/platform-skills/(平台技能:ASR/TTS)
  5. ~/.octos/plugins/(全局旧版兼容)
  6. ~/.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简短描述
alwaystrue 时,每次系统提示词都会包含该技能;为 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_filewrite_fileshellglobgreplist_dirrun_pipelinedeep_search 等。
  • 动态工具save_memoryweb_searchrecall_memory 等按需激活、空闲后淘汰的工具。
  • 延迟工具browsermanage_skillsspawnconfigure_toolswitch_model 等仅列出名称的工具。

淘汰规则

当活跃工具数量超过 15 个时:

  • 空闲 5 次以上迭代且不在基础工具集中的工具成为淘汰候选。
  • 最久未使用的工具优先移入延迟列表。

重新激活

当 LLM 需要使用某个延迟工具时,它会调用 activate_tools({"tools": [...]})。这会将工具名称解析到对应的工具组,并激活整个组。

工具配置

可以在运行时通过 /config 斜杠命令配置工具。设置持久化存储在 {data_dir}/tool_config.json 中。

工具设置项类型默认值说明
news_digestlanguage"zh" / "en""zh"新闻摘要的输出语言
news_digesthn_top_stories5-10030Hacker News 抓取的故事数
news_digestmax_rss_items5-10030每个 RSS 源的条目数
news_digestmax_deep_fetch_total1-5020深度抓取的文章总数
news_digestmax_source_chars1000-5000012000每个来源的 HTML 字符上限
news_digestmax_article_chars1000-500008000每篇文章的内容字符上限
deep_crawlpage_settle_ms500-100003000JS 渲染等待时间(毫秒)
deep_crawlmax_output_chars10000-20000050000输出截断上限
web_searchcount1-105默认搜索结果数量
web_fetchextract_mode"markdown" / "text""markdown"内容提取格式
web_fetchmax_chars1000-20000050000内容大小上限
browseraction_timeout_secs30-600300单次操作超时时间
browseridle_timeout_secs60-600300空闲会话超时时间

聊天中的配置命令:

/config                              # 显示所有工具设置
/config web_search                   # 显示 web_search 的设置
/config set web_search.count 10      # 将默认结果数设为 10
/config set news_digest.language en  # 将新闻摘要切换为英文
/config reset web_search.count       # 重置为默认值

优先级顺序(从高到低):

  1. 显式的单次调用参数(工具调用时指定的参数)
  2. /config 覆盖值(存储在 tool_config.json 中)
  3. 硬编码的默认值

工具策略

工具策略控制 Agent 可以使用哪些工具,可在全局、按提供商或按上下文级别进行设置。

全局策略

{
  "tool_policy": {
    "allow": ["group:fs", "group:search", "web_search"],
    "deny": ["shell", "spawn"]
  }
}
  • allow – 如果非空,则只允许使用这些工具。如果为空,则允许所有工具。
  • deny – 始终禁止使用这些工具。deny 优先于 allow。

命名分组

分组展开为
group:fsread_filewrite_fileedit_filediff_edit
group:runtimeshell
group:webweb_searchweb_fetchbrowser
group:searchglobgreplist_dir
group:sessionsspawn

不在命名分组中的工具:send_fileswitch_modelrun_pipelineconfigure_toolcronmessage

通配符匹配

后缀 * 匹配前缀:

{
  "tool_policy": {
    "deny": ["web_*"]
  }
}

这会禁止 web_searchweb_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 启动前的会话历史快照。

溢出机制的工作原理

  1. 为第一条消息生成主 Agent。
  2. 主 Agent 运行期间,新消息到达收件箱。
  3. 每条新消息触发 serve_overflow(),生成一个拥有独立流式输出气泡的完整 Agent 任务。
  4. 溢出 Agent 使用主 Agent 启动前的历史快照,避免重复回答主问题。
  5. 所有 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四种事件类型之一
commandargv 数组(不经过 shell 解释)
timeout_ms5000超时后终止钩子进程
tool_filter全部仅对这些工具名称触发(仅限工具事件)

同一事件可以注册多个钩子。它们按顺序执行;第一个拒绝即生效。

熔断器

钩子在连续 3 次失败(超时、崩溃或退出码 2+)后会被自动禁用。一次成功执行(exit 0 或拒绝 exit 1)即可重置计数器。

安全性

  • 命令使用 argv 数组 – 不经过 shell 解释。
  • 18 个危险环境变量会被移除(LD_PRELOADDYLD_*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 命令在沙箱中运行以实现隔离。支持三种后端:

后端平台隔离方式网络控制
bwrapLinux只读绑定 /usr,/lib,/bin,/sbin,/etc;读写绑定工作目录;tmpfs /tmp;unshare-pid禁止网络时使用 --unshare-net
macOSmacOS使用 SBPL 配置的 sandbox-exec:process-exec/forkfile-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(自动检测最佳可用方案)、bwrapmacosdockernone
  • 挂载模式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 哈希和防抖机制检测。

消息合并

长响应在发送前会自动拆分为适合频道的分块:

频道每条消息最大字符数
Telegram4000
Discord1900
Slack3900

拆分优先级:段落分隔 > 换行符 > 句号结尾 > 空格 > 硬截断。超过 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_total
  • octos_tool_call_duration_seconds
  • octos_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

钥匙串条目格式

  • Serviceoctos(所有条目使用相同常量)
  • 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 钥匙串

为什么钥匙串在无头服务器上不可靠:

  1. 需要 macOS 登录密码 – 通过 SSH 解锁钥匙串需要用户的登录密码存储在某处,降低了安全收益。
  2. 重启/休眠后重新锁定 – 启动 octos serve 的 LaunchAgent 在 GUI 登录之前运行,此时钥匙串处于锁定状态。
  3. 空闲超时后重新锁定 – 即使解锁后,macOS 也可能重新锁定。set-keychain-settings 的变通方案可能被 macOS 更新重置。
  4. ACL 弹窗阻断无头访问 – 如果二进制文件不是最初存储密钥的那个,macOS 可能弹出一个无法回答的 GUI 对话框。
  5. 会话隔离 – 从 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 端点返回 404Larksuite 国际版不支持 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_KEYAnthropic API 密钥
OPENAI_API_KEYOpenAI API 密钥
GEMINI_API_KEYGemini API 密钥
OPENROUTER_API_KEYOpenRouter API 密钥
DEEPSEEK_API_KEYDeepSeek API 密钥
GROQ_API_KEYGroq API 密钥
MOONSHOT_API_KEYMoonshot API 密钥
DASHSCOPE_API_KEYDashScope API 密钥
MINIMAX_API_KEYMiniMax API 密钥
ZHIPU_API_KEY智谱 API 密钥
ZAI_API_KEYZ.AI API 密钥
NVIDIA_API_KEYNvidia NIM API 密钥
OMINIX_API_URL本地 ASR/TTS API 地址
RUST_LOG日志级别(error / warn / info / debug / trace
TELEGRAM_BOT_TOKENTelegram 机器人令牌
DISCORD_BOT_TOKENDiscord 机器人令牌
SLACK_BOT_TOKENSlack 机器人令牌
SLACK_APP_TOKENSlack 应用级令牌
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/quitexitquit: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_totaloctos_tool_call_duration_secondsoctos_llm_tokens_total)。


octos clean

清理数据库和状态文件。

octos clean [--all] [--dry-run]
参数说明
--all移除所有状态文件
--dry-run仅显示将被删除的内容,不实际执行

octos completions

生成 Shell 自动补全脚本。

octos completions <shell>

支持的 Shell:bashzshfishpowershell


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 应用技能开发指南

English | 中文

本指南涵盖了构建、注册和部署 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_secs30每次工具调用的最大执行时间(1-600)
requires_networkfalse信息性标志
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作者名称
alwaysfalse如果为 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}));
}

协议规则:

  1. argv[1] = 工具名称(例如 get_weatherget_forecast
  2. stdin = 匹配工具 input_schema 的 JSON 对象
  3. stdout = JSON 对象,包含:
    • output(字符串):人类可读的结果文本
    • success(布尔值):成功为 true,失败为 false
  4. 退出码:成功为 0,失败为非零
  5. 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_nametarget/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
headersHTTP 传输:附加请求头(键值对象)

* commandurl 必须设置其中一个。本地(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_ms5000最大执行时间(毫秒)
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_namearguments 和会话上下文。LLM 事件的载荷包含 modelmessage_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.mdreview-checklist.md 都会被注入到系统提示中。


纯扩展技能

技能不需要提供任何可执行工具。如果 manifest.jsontools 数组为空(或完全省略),但声明了 mcp_servershooksprompts,网关会加载扩展而不寻找二进制文件。这适用于:

  • 纯提示注入技能 – 一组 .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_secs30每次工具调用的最大执行时间(1-600)
requires_networkfalse信息性标志
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_PRELOADDYLD_INSERT_LIBRARIESDYLD_LIBRARY_PATH
  • NODE_OPTIONSPYTHONPATHPERL5LIB
  • RUSTFLAGSRUST_LOG
  • 以及 10 余个其他变量(见 sandbox.rs 中的 BLOCKED_ENV_VARS

技能开发者的最佳实践:

  • 验证所有输入(永远不要信任 citypath 等)
  • 为 HTTP 请求设置超时
  • 避免 shell 注入(不要将用户输入传递给 shell 命令)
  • 在发布构建中设置 manifest.json 中的 sha256 以启用完整性校验

平台技能 vs 应用技能

应用技能平台技能
位置crates/app-skills/crates/platform-skills/
数组BUNDLED_APP_SKILLSPLATFORM_SKILLS
引导每次网关启动仅管理员机器人
作用域按网关所有网关共享
使用场景始终可用、自包含需要外部服务

无需完整重建即可更新技能

技能可以独立重建和部署:

# 只构建该技能
cargo build --release -p weather

# 复制到远程服务器
scp target/release/weather remote:~/.octos/skills/weather/main

# 无需重启网关 — 下次工具调用时使用新二进制文件

注意:如果修改了 SKILL.mdmanifest.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")

技能加载优先级

网关从多个目录加载技能。名称冲突时先匹配者优先:

  1. <profile-data>/skills/ — 按配置文件(最高优先级)
  2. <project-dir>/skills/ — 项目本地
  3. <project-dir>/bundled-skills/ — 内置应用技能
  4. ~/.octos/skills/ — 全局(最低优先级)

发布到注册表

外部技能可通过 octos-hub 注册表被发现。

  1. 将你的技能仓库推送到 GitHub
  2. 通过 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"]
}
  1. 用户即可查找并安装你的技能:
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_KEYOPENAI_API_KEY
  • 非标准端点的 Base URL
  • OCTOS_DATA_DIROCTOS_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:设置了 commandurlenv 仅列出变量名称而非值
  • mcp_servers:相对命令路径(./bin/server)在技能目录中存在
  • hookseventbefore_tool_callafter_tool_callbefore_llm_callafter_llm_call 之一
  • hookscommand 是 argv 数组(非 shell 字符串);command[0] 的相对路径能正确解析
  • hooks:当钩子只应用于特定工具时设置了 tool_filter
  • promptsinclude 中的 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 strDisplay,用于跨提供商的一致字符串转换(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: PathBufgit_state: Option<GitState>working_memory: Vec<Message>episodic_refs: Vec<EpisodeRef>files_in_scope: Vec<PathBuf>

TaskResult

  • success: booloutput: Stringfiles_modified: Vec<PathBuf>subtasks: Vec<TaskId>token_usage: TokenUsage

TokenUsageinput_tokens: u32output_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认证头图片格式默认模型
Anthropicapi.anthropic.comx-api-keyBase64 块claude-sonnet-4-20250514
OpenAIapi.openai.com/v1Authorization: BearerData URIgpt-4o
Geminigenerativelanguage.googleapis.com/v1betax-goog-api-keyBase64 内联gemini-2.5-flash
OpenRouteropenrouter.ai/api/v1Authorization: BearerData URIanthropic/claude-sonnet-4-20250514

OpenAI 兼容提供商(通过 OpenAIProvider::with_base_url()

提供商别名基础 URL默认模型API 密钥环境变量
DeepSeekapi.deepseek.com/v1deepseek-chatDEEPSEEK_API_KEY
Groqapi.groq.com/openai/v1llama-3.3-70b-versatileGROQ_API_KEY
Moonshotkimiapi.moonshot.ai/v1kimi-k2.5MOONSHOT_API_KEY
DashScopeqwendashscope.aliyuncs.com/compatible-mode/v1qwen-maxDASHSCOPE_API_KEY
MiniMaxapi.minimax.io/v1MiniMax-Text-01MINIMAX_API_KEY
Zhipuglmopen.bigmodel.cn/api/paas/v4glm-4-plusZHIPU_API_KEY
Nvidianimintegrate.api.nvidia.com/v1meta/llama-3.3-70b-instructNVIDIA_API_KEY
Ollamalocalhost:11434/v1llama3.2(无)
vLLM(用户提供)(用户提供)VLLM_API_KEY

Anthropic 兼容提供商

提供商别名基础 URL默认模型API 密钥环境变量
Z.AIzai, z.aiapi.z.ai/api/anthropicglm-5ZAI_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

  • Anthropicmessage_start → 输入 token,content_block_start/delta → 文本/工具块,message_delta → 停止原因。自定义 SSE 状态机。
  • OpenAI/OpenRouter:标准 OpenAI SSE,[DONE] 哨兵。delta.content 用于文本,delta.tool_calls[] 用于工具。共享解析器:parse_openai_sse_events()
  • Geminialt=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。

可重试错误(三层检测):

  1. HTTP 状态码:429、500、502、503、504、529
  2. reqwest:is_connect()is_timeout()
  3. 字符串兜底:“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_idcached_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/4200,000
GPT-4o/4-turbo128,000
o1/o3/o4200,000
Gemini 2.0/1.51,000,000
默认(未知)128,000

定价

model_pricing(model_id) -> Option<ModelPricing> — 不区分大小写的子串匹配。费用 = (input/1M) * input_rate + (output/1M) * output_rate

模型输入 $/1M输出 $/1M
claude-opus-415.0075.00
claude-sonnet-43.0015.00
claude-haiku0.804.00
gpt-4o2.5010.00
gpt-4o-mini0.150.60
o3/o410.0040.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,插入内存中的 HybridIndex
  • get(id) — 按 episode_id 直接查找
  • find_relevant(cwd, query, limit) — 限定在目录范围内的关键词匹配
  • recent_for_cwd(cwd, n) — 按 created_at 降序取最近 N 条
  • store_embedding(id, Vec<f32>) — bincode 序列化,存入 embeddings 表,更新 HybridIndex
  • find_relevant_hybrid(query, query_embedding, limit) — 跨所有片段的全局混合搜索

初始化open() 时通过遍历所有片段并从数据库加载嵌入来重建内存中的 HybridIndex。

MemoryStore

基于文件的持久化记忆,位于 {data_dir}/memory/

  • MEMORY.md — 长期记忆(全量覆写)
  • YYYY-MM-DD.md — 每日笔记(带日期头的追加)

get_memory_context() 构建系统提示注入:

  1. ## Long-term Memory — 完整的 MEMORY.md
  2. ## Recent Activity — 7 天滚动窗口的每日笔记
  3. ## 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=16HNSW_CAPACITY=10_000HNSW_EF_CONSTRUCTION=200HNSW_MAX_LAYER=16DistCosine
  • 插入/搜索前进行 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 → 返回结果

ConversationResponsecontent: Stringtoken_usage: TokenUsagefiles_modified: Vec<PathBuf>streamed: bool

片段保存:任务完成后,如果有 embedder 则异步触发嵌入生成。

墙钟超时:Agent 在 max_timeout(默认 600 秒)后终止,不论迭代次数。

工具输出清理

在将工具结果反馈给 LLM 之前,sanitize_tool_output()(在 sanitize.rs 中)剥离噪声:

  • Base64 数据 URIdata:...;base64,<payload>[base64-data-redacted]
  • 长十六进制字符串:64+ 个连续十六进制字符(SHA-256、原始密钥)→ [hex-redacted]

上下文压缩

当估算的 token 超过上下文窗口的 80% / 1.2 安全系数时触发。

算法

  1. 保留最近的 MIN_RECENT_MESSAGES(6)条非系统消息
  2. 不在工具调用/结果对内部拆分
  3. 摘要旧消息:首行(200 字符),剥离工具参数,丢弃媒体
  4. 预算:摘要占总量的 40%(BASE_CHUNK_RATIO = 0.4)
  5. 替换为:[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>,
}
}

ToolRegistryHashMap<String, Arc<dyn Tool>>,带有 provider_policy: Option<ToolPolicy> 用于软过滤。

内置工具(14 个)

工具参数关键行为
read_filepath, start_line?, end_line?行号(NNN|),100KB 截断,拒绝符号链接
write_filepath, content创建父目录,返回 file_modified
edit_filepath, old_string, new_string要求精确匹配,0 或 >1 次出现报错
diff_editpath, diff统一 diff 格式,模糊匹配(+-3 行),反向 hunk 应用
globpattern, limit=100拒绝绝对路径和 ..,相对结果
greppattern, file_pattern?, limit=50, context=0, ignore_case=false通过 ignore::WalkBuilder 感知 .gitignore,正则带 (?i) 标志
list_dirpath排序,[dir]/[file] 前缀
shellcommand, timeout_secs=120SafePolicy 检查,50KB 输出截断,沙箱包装,超时钳制到 [1, 600] 秒
web_searchquery, count=5Brave Search API (BRAVE_API_KEY)
web_fetchurl, extract_mode=“markdown”, max_chars=50000SSRF 防护,htmd HTML→markdown,30 秒超时
messagecontent, channel?, chat_id?通过 OutboundMessage 跨频道消息。仅网关模式
spawntask, label?, mode=“background”, allowed_tools, context?继承提供商策略的子 Agent。sync=内联,background=异步。仅网关模式
cronaction, message, schedule params调度 add/list/remove/enable/disable。仅网关模式
browseraction, 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 询问模式sudorm -rfgit push --forcegit 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.rsgateway.rsserve.rs。钩子配置变更通过配置监视器触发重启。

MCP 集成

Model Context Protocol 服务器的 JSON-RPC 传输。两种传输模式:

传输方式

  1. Stdio:将服务器作为子进程启动(command + args + env)。行限制:1MB。通过 BLOCKED_ENV_VARS 清理环境。
  2. HTTP/SSE:通过 url 字段连接远程服务器。POST JSON,SSE 响应处理。

生命周期(stdio):

  1. 启动服务器(command + args + env,过滤 BLOCKED_ENV_VARS)
  2. 初始化:protocolVersion: "2024-11-05"
  3. 发现工具:tools/list RPC
  4. 验证输入 schema(最大深度 10,最大大小 64KB);拒绝无效 schema 的工具
  5. 注册 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: trueavailable: 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}/ 目录

与网关集成

在网关命令中,技能在系统提示构建期间加载:

  1. get_always_skills() — 收集自动加载的技能名称
  2. load_skills_for_context(names) — 加载并连接技能正文
  3. build_skills_summary() — 将 XML 技能索引追加到系统提示
  4. 始终开启的技能内容前置到系统提示

插件系统

插件通过独立可执行文件扩展 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)

  1. 扫描每个目录的子目录
  2. 对每个子目录查找 manifest.json
  3. 解析清单,查找可执行文件(先尝试目录名,再尝试 main
  4. 验证可执行权限(Unix:mode & 0o111 != 0;非 Unix:存在性检查)
  5. 将每个工具定义包装为实现 Tool trait 的 PluginTool
  6. 注册到 ToolRegistry
  7. 记录警告:"loaded unverified plugin (no signature check)"
  8. 返回工具总数。失败的插件带警告跳过,不会导致致命错误。

PluginTool — 执行协议

#![allow(unused)]
fn main() {
pub struct PluginTool {
    plugin_name: String,
    tool_def: PluginToolDef,
    executable: PathBuf,
}
}

调用executable <tool_name>(工具名称作为第一个参数传递)。

stdin/stdout 协议

  1. 以工具名称为参数启动可执行文件,管道连接 stdin/stdout/stderr
  2. 将 JSON 序列化的参数写入 stdin,关闭(EOF 表示输入结束)
  3. 等待退出,30 秒超时(PLUGIN_TIMEOUT
  4. 解析 stdout 为 JSON:
    • 结构化{"output": "...", "success": true/false} → 使用解析后的值
    • 回退:原始 stdout + stderr 拼接,成功由退出码决定
  5. 返回 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(绿色)
TokenUsageTokens: 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(减少系统调用)
StreamDoneFlush + 换行
CostUpdateTokens: 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
}
}
ProgressEventJSON type 字段附加字段
ToolStarted"tool_start"tool
ToolCompleted"tool_end"toolsuccess
StreamChunk"token"text
StreamDone"stream_end"
CostUpdate"cost_update"input_tokensoutput_tokenssession_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

TurnTurnKind(UserInput、AgentReply、ToolCall、ToolResult、System)和迭代次数包装 Messageturns_to_messages() 转换回 Vec<Message> 用于 LLM 调用。支持对对话历史的语义分析。

事件总线(event_bus.rs

EventBus 包含类型化的 EventSubscriber,用于 Agent 内部的发布/订阅。解耦事件生产者(工具执行、LLM 调用)与消费者(日志、指标、UI 更新)。

循环检测(loop_detect.rs

检测重复的 Agent 行为(如使用相同参数调用同一工具)。可配置阈值和窗口。检测到循环时提前返回诊断消息。

会话状态(session.rs

SessionState 包含 SessionLimitsSessionUsage 跟踪。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认证去重
CLIstdin/stdout(始终启用)
Telegramteloxide 长轮询telegramBot token (env)teloxide 内置
Discordserenity gatewaydiscordBot token (env)serenity 内置
SlackSocket Mode (tokio-tungstenite)slackBot token + App tokenmessage_ts
WhatsAppWebSocket 桥接 (ws://localhost:3001)whatsappBaileys 桥接HashSet(10K 上限,溢出时清空)
飞书WebSocket (tokio-tungstenite)feishuApp ID + Secret → tenant token (TTL 6000s)HashSet(10K 上限,溢出时清空)
邮件IMAP 轮询 + SMTP 发送email用户名/密码,rustls TLSIMAP UNSEEN 标志
企业微信企业微信 APIwecomCorp ID + Agent Secretmessage_id
TwilioTwilio SMS/MMStwilioAccount SID + Auth Tokenmessage 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 转 HTMLmarkdown_html.rs 将 Markdown 转换为 Telegram 兼容的 HTML 用于富文本消息格式化。

媒体download_media() 辅助函数将照片/语音/音频/文档下载到 .octos/media/

语音转文字:语音/音频在 Agent 处理前自动通过 GroqTranscriber 转录。

消息合并

将超大消息拆分为适合频道的分块:

频道最大字符数
Telegram4000
Discord1900
Slack3900

断开优先级:段落(\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 } — 通过 cron crate 的 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/statusOAuth PKCE(OpenAI)、设备码、粘贴 token
cron list/add/remove/enableCLI 定时任务管理
channels status/login频道编译状态、WhatsApp 桥接设置
skills list/install/remove技能管理、GitHub 获取
officeOffice/工作区管理
account账户管理
clean删除 .redb 文件,支持 dry-run
completionsShell 补全生成(bash/zsh/fish)
docs生成工具 + 提供商文档
serveREST 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):

  1. 生成 64 字符验证器(两个 UUIDv4 十六进制)
  2. SHA-256 挑战,base64-URL 编码(无填充)
  3. TCP 监听端口 1455
  4. 浏览器 → auth.openai.com + PKCE + state
  5. 回调验证 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/chatPOST发送消息 → 获取响应
/api/chat/streamGETProgressEvent 的 SSE 流
/api/sessionsGET列出所有会话
/api/sessions/{id}/messagesGET分页历史(?limit=100&offset=0,最大 500)
/api/statusGET版本、模型、提供商、运行时间
/metricsGETPrometheus 文本格式(无需认证)
/*(回退)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、DynamicParallel
  • condition.rs — 条件边求值(分支逻辑)
  • tool.rs — RunPipelineTool 集成(将流水线执行暴露为 Agent 工具)
  • validate.rs — 图验证和 lint 诊断
  • human_gate.rs — 人在环路门,包含 HumanInputProvider trait、ChannelInputProvider(mpsc + oneshot,默认 5 分钟超时)、AutoApproveProvider。输入类型:Approval、FreeText、Choice
  • fidelity.rsFidelityMode 枚举(Full、Truncate、Compact、Summary),用于节点间上下文传递控制。从配置字符串解析。安全上限:10MB max_chars、100K max_lines
  • manager.rsPipelineManager 管理器,包含 SupervisionStrategy(AllOrNothing、BestEffort、RetryFailed)。重试上限 10 次,指数退避(100ms-5s)。ManagerOutcome 转换为 NodeOutcome
  • thread.rsThreadRegistry 用于跨流水线节点的 LLM 会话复用。Thread 存储 model_id + 消息历史。限制:1000 线程,每线程 10000 条消息
  • server.rsPipelineServer trait,包含 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] 设置的工作区级 lint
  • secrecy::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 /ddmkfs、fork 炸弹、chmod -R 777 /;询问 sudorm -rfgit push --forcegit reset --hard。匹配前空白归一化。超时钳制到 [1, 600] 秒。SIGTERM→宽限期→SIGKILL 子进程清理。
  • 工具策略:allow/deny,deny 优先语义,8 个命名分组(group:fsgroup:runtimegroup:webgroup:searchgroup: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 覆盖(ULA fc00::/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. Clippycargo clippy --workspace -- -D warnings--quick 跳过
3. 工作区测试cargo test --workspace--serial 单线程
4. 针对性分组按子系统测试(见下文)始终运行

针对性测试分组

在完整工作区测试之后,CI 脚本会单独重新运行关键子系统,以便清晰地暴露失败:

分组Crate测试过滤器数量覆盖内容
自适应路由octos-llmadaptive::tests19Off/Hedge/Lane 模式、熔断器、故障转移、评分、指标、竞速
响应性octos-llmresponsiveness::tests8基线学习、劣化检测、恢复、阈值边界
会话 actoroctos-clisession_actor::tests9队列模式、Speculative 溢出、自动升级/降级
会话持久化octos-bussession::tests28JSONL 存储、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_orderOff 模式忽略延迟差异
test_lane_changing_off_skips_circuit_brokenOff 模式仍然遵守熔断器
test_hedged_off_uses_single_providerOff 模式使用优先级,不竞速

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_runtimeOff → Hedge → Lane → Off 切换
test_qos_ranking_toggleQoS 排名切换与模式正交
test_adaptive_status_reports_correctly状态结构体反映当前模式/数量
test_empty_router_panics断言至少需要 1 个提供商

响应性观察器(crates/octos-llm/src/responsiveness.rs — 8 个测试)

测试驱动自动升级的延迟跟踪器。

基线学习

测试验证内容
test_baseline_learning从前 5 个样本建立基线
test_sample_count_trackingsample_count() 返回正确值

劣化检测

测试验证内容
test_degradation_detection3 次连续慢请求(> 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_resultBackgroundResult 消息在 Speculative 的 select! 循环中被处理,不产生额外的 LLM 调用

自动升级 / 降级

测试验证内容
test_auto_escalation_on_degradation5 次快速预热(基线 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_processing10 个并行任务不会损坏会话

分支与重写

测试验证内容
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_keyslist_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 时运行:

  1. cargo fmt --all -- --check
  2. cargo clippy --workspace -- -D warnings
  3. cargo 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.ymlGitHub 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 个测试