OpenClaw(原 Clawdbot/Moltbot)笔记

目录

什么是 OpenClaw?

OpenClaw 是一个自托管网关,可将你常用的聊天应用 ------ WhatsApp、Telegram、Discord、iMessage 等 ------ 连接到 AI 编码 Agent(例如 Pi)。

你只需在自己的电脑(或服务器)上运行一个 Gateway 进程,它就会成为消息应用与始终在线的 AI 助手之间的桥梁。

适合谁?

希望拥有一个可随时通过任意设备发送消息的个人 AI 助手,同时不放弃数据控制权、也不依赖托管服务的开发者和高级用户。

它有什么不同?

  • 自托管:运行在你的硬件上,遵循你的规则
  • 多渠道:一个 Gateway 同时服务 WhatsApp、Telegram、Discord 等多个平台
  • 原生 Agent 架构:专为编码 Agent 设计,支持工具调用、会话、记忆、多 Agent 路由
  • 开源:MIT 许可证,社区驱动

你需要什么?

  • Node 22+、一个 API Key(推荐 Anthropic),以及 5 分钟时间。

工作原理

Gateway 是会话管理、路由控制和渠道连接的唯一权威中心。

核心能力:

  1. 多渠道网关:通过单个 Gateway 进程连接 WhatsApp、Telegram、Discord 和 iMessage。

  2. 插件渠道:通过扩展包添加 Mattermost 等更多平台支持。

  3. 多 Agent 路由:按 Agent、工作空间或发送者进行会话隔离。

  4. 媒体支持:支持发送和接收图片、音频和文档。

  5. Web 控制界面:浏览器仪表盘,管理聊天、配置、会话和节点。

  6. 移动节点:支持 iOS 和 Android 节点配对,并支持 Canvas 功能。

快速开始

1. 安装 OpenClaw

bash 复制代码
npm install -g openclaw@latest

2. 初始化并安装服务

向导会引导你完成网关、工作区、通道与技能的配置,并安装系统服务(launchd / systemd 用户服务),使网关在后台常驻(别在你本机上执行):

bash 复制代码
openclaw onboard --install-daemon

3. 配对 WhatsApp 并启动 Gateway

按提示完成扫码或登录,凭证会保存在 ~/.openclaw/credentials。

bash 复制代码
openclaw channels login
openclaw gateway --port 18789

在 Gateway 启动后,打开浏览器控制界面。


配置(可选)

配置文件位于:

复制代码
~/.openclaw/openclaw.json

如果你不做任何配置,OpenClaw 会使用内置的 Pi 二进制文件(RPC 模式),并为每个发送者创建独立会话。

如果你希望加强访问控制,可以从 channels.whatsapp.allowFrom 和群组提及规则开始。

示例:

json 复制代码
{
  "channels": {
    "whatsapp": {
      "allowFrom": ["+15555550123"],
      "groups": { "*": { "requireMention": true } }
    }
  },
  "messages": {
    "groupChat": {
      "mentionPatterns": ["@openclaw"]
    }
  }
}

其他安装方式请参考官网:https://docs.openclaw.ai/install

核心概念

Channels(渠道)

通过单个 Gateway 同时连接 WhatsApp、Telegram、Discord 和 iMessage。

Plugins(插件)

通过扩展添加 Mattermost 等更多平台。

Routing(路由)

支持多 Agent 路由,并实现会话隔离。

Media(媒体)

支持图片、音频和文档的发送与接收。

Apps and UI(应用与界面)

提供 Web 控制界面和 macOS 伴随应用。

Mobile nodes(移动节点)

支持 iOS 和 Android 节点,并提供 Canvas 功能。

完整功能列表:

  • 通过 WhatsApp Web(Baileys)实现 WhatsApp 集成
  • Telegram 机器人支持(grammY)
  • Discord 机器人支持(channels.discord.js)
  • Mattermost 机器人支持(插件方式)
  • 通过本地 imsg CLI(macOS)实现 iMessage 集成
  • 基于 RPC 模式的 Pi Agent 桥接,支持工具流式调用
  • 长响应支持流式输出与分块处理
  • 多 Agent 路由:按工作空间或发送者隔离会话
  • 通过 OAuth 实现 Anthropic 与 OpenAI 的订阅认证
  • 会话机制:私聊会合并为共享主会话;群聊独立隔离
  • 群聊支持基于"@提及"的激活机制
  • 支持图片、音频和文档媒体传输
  • 可选语音留言转写钩子
  • WebChat 和 macOS 菜单栏应用
  • iOS 节点:支持配对与 Canvas 画布
  • Android 节点:支持配对、Canvas、聊天与相机功能

⚠️ 旧版 Claude、Codex、Gemini 和 Opencode 路径已被移除。目前仅保留 Pi 作为唯一的编码 Agent 路径。

OpenClaw 能做什么?

从功能上讲,OpenClaw 远不止是个"聊天机器人",而是一个 AI 助手操作层(Orchestration Layer):

多通道统一入口:

国外 IM 渠道:

Telegram、WhatsApp、Discord、Slack、LINE、Google Chat 等。

本地入口:

  • Web 控制台(浏览器访问)
  • Web 聊天界面
  • 终端 TUI(命令行聊天界面)

国内渠道(通过第三方插件 / 扩展):

  • 钉钉
  • 飞书
  • 企业微信
  • 社区正在开发 QQ 机器人等更多入口

你可以在配置中指定:

  • 启用 / 禁用哪些通道
  • 哪些用户、群组可以访问(白名单 / 配对模式)
  • 群聊中是否必须 @ 机器人才会响应

主动式 AI:不再只是"你问我答"

OpenClaw 内置定时任务与事件触发能力,可以变成一个"主动帮你干活"的秘书。例如:

基于 Cron 的定时任务:

  • 每天固定时间推送「今日待办 + 日程 + 邮件摘要」
  • 将 GitLab / GitHub 通知汇总成日报

基于事件的触发:

  • 被邮件、Webhook、系统事件唤起,自动执行某些 Workflow

支持多 Agent 路由:

  • "生活助理"负责日程与家庭事项
  • "开发助理"专注代码与部署
  • "团队助理"关注项目进度与会议纪要

能动型 Agent:真正"帮你干活"

在获得你的授权后,Agent 可以:

  • 执行命令:通过 node-pty 等方式实现伪终端,执行 shell / 命令行
  • 读写文件:查看、修改本地项目文件、配置与日志
  • 操控浏览器:使用 Playwright / CDP 控制浏览器,访问网页、登录后台、自动填写表单
  • 访问 API / 服务:调用邮件、日历、GitHub、云厂商 API 等外部服务

简单来说,Agent 可以做任何你能在电脑上完成的事情:

只要电脑能做的,它都可以代你完成。

当然,这也是安全风险最大的部分。因此 OpenClaw 提供了严格的权限控制与沙箱机制(详见后文"安全章节")。

Live Canvas / A2UI:从聊天到"可视化工作台"

OpenClaw 具备 Live Canvas(又称 A2UI,Agent-to-UI)能力:

  • Agent 不再只返回纯文本,而是返回结构化的"UI 描述"
  • 前端 Canvas Host 将其渲染为可交互画布

例如:

  • 任务看板、图表、统计面板
  • 可点击的按钮、切换器、表格等交互组件

这使其非常适合构建"个人或团队的 AI 控制面板",例如:

  • 项目进度仪表板
  • 日程 + 待办 + 邮件一体化看板

第一次运行:Onboard 向导

在终端执行:

bash 复制代码
openclaw onboard

1️⃣ 安全声明

系统会提示:Agent 可能具备以下能力:

  • 执行命令
  • 访问文件
  • 操控本地环境

请确认理解风险后继续。

2️⃣ 配置模型

  • Claude Pro / Max 用户可使用 setup-token

  • 也可以填写:

    • Claude / OpenAI / 国内代理 API Key
    • 自定义 baseUrl

3️⃣ 暂不配置消息通道

建议先不要接入 Telegram / Slack 等通道,

先使用 Web 控制台完成测试。

向导完成后,会输出 Web UI 地址:

复制代码
Web UI: http://127.0.0.1:18789/
Web UI (with token): http://127.0.0.1:18789/?token=xxx

在浏览器中打开带 token 的地址,发送一条测试消息即可。

考虑到国内对境外 IM 的使用限制,更现实稳定的方式是:

  • 在本地或 NAS 上运行 OpenClaw
  • 通过浏览器 + 局域网访问 Web 控制台

找到配置文件(视安装版本而定):

复制代码
~/.openclaw/openclaw.json
~/.clawdbot/clawdbot.json

修改 gateway 段:

json 复制代码
{
  "gateway": {
    "mode": "local",
    "bind": "lan",
    "port": 18789,
    "auth": {
      "mode": "token",
      "token": "使用 openssl rand -hex 32 生成"
    }
  }
}

重启网关:

bash 复制代码
openclaw gateway restart

在手机 / 平板访问:

假设局域网 IP 为:

复制代码
192.168.1.100

访问:

复制代码
http://192.168.1.100:18789/?token=你的token

只要在同一 Wi-Fi 下,即可像访问"家庭版 ChatGPT + 控制台"一样使用。

⚠ 安全提醒

  • 仅本机使用时,建议保持 bind=loopback 更安全
  • 18789 端口可改为其他端口
  • 必须确保防火墙配置正确

接入国内应用:钉钉、飞书、企业微信

在"浏览器 + LAN"稳定运行后,可按需接入国内 IM。

路线一:钉钉 + 阿里云 AppFlow(低代码)

1️⃣ 在 钉钉 开放平台创建企业内部应用:

  • 开启机器人能力

  • 开启 AI 卡片能力

  • 获取:

    • ClientID
    • ClientSecret
    • AI 卡片模板 ID

2️⃣ 在 OpenClaw 中启用 HTTP 响应路径(如 /dingtalk

3️⃣ 在 阿里云 AppFlow 控制台使用模板:

  • 一端配置钉钉凭证
  • 一端配置 OpenClaw 网关地址 + token

启用后,在钉钉群中 @ 机器人即可触发闭环。

路线二:插件方式(更灵活)

安装插件:

bash 复制代码
openclaw plugins install @openclaw-china/dingtalk

配置:

bash 复制代码
openclaw config set channels.dingtalk '{
  "enabled": true,
  "clientId": "dingXXXX",
  "clientSecret": "your-app-secret",
  "enableAICard": true
}' --json

细化权限:

json 复制代码
{
  "channels": {
    "dingtalk": {
      "dmPolicy": "pairing",
      "groupPolicy": "allowlist",
      "requireMention": true
    }
  }
}

适合有一定 Node / 运维能力的团队。

飞书 / 企业微信:moltbot-china 插件合集

社区项目支持:

  • 飞书
  • 企业微信
  • 钉钉(未来支持 QQ 等)

安装:

bash 复制代码
openclaw plugins install @openclaw-china/channels

飞书配置:

bash 复制代码
openclaw config set channels.feishu '{
  "enabled": true,
  "appId": "cli_xxxxxx",
  "appSecret": "your-app-secret"
}' --json

企业微信配置:

bash 复制代码
openclaw config set channels.wecom '{
  "enabled": true,
  "webhookPath": "/wecom",
  "token": "your-token",
  "encodingAESKey": "43-char-key"
}' --json

在企业微信后台配置回调 URL:

复制代码
https://你的域名/wecom

安全:高权限 Agent 必须有"牢笼"

OpenClaw 可能具备:

  • 执行系统命令
  • 读写文件
  • 控制浏览器
  • 调用敏感 API(云账号 / 支付 / CI/CD)

必做三件事:

① 不暴露公网

  • 使用 bind=loopback
  • 或 内网 + VPN / Tailscale
  • 公网必须配合防火墙 + IP 白名单

② 强制认证

  • gateway.auth.mode=tokenpassword
  • 使用高强度随机值
  • 定期更换

③ 收紧通道策略

  • 私聊:dmPolicy=pairingallowlist
  • 群聊:groupPolicy=allowlist + requireMention=true
  • 限制高危工具的可调用通道

⚠ 不建议在存有重要资料的主力办公电脑上运行。

推荐使用:

  • 独立电脑
  • 云服务器
  • 虚拟机
  • Docker

安装后建议立即运行:

bash 复制代码
openclaw security audit
openclaw security audit --deep
openclaw security audit --fix

可自动检查:

  • 是否缺少认证
  • 凭证目录权限是否过宽
  • 通道策略是否存在风险

OpenClaw 的能力高度依赖 Skills(插件)生态。

社区技能超过 500+,涵盖:

  • DevOps & Cloud(GitHub / GitLab / 部署脚本)
  • 办公协作(日历 / 待办 / Notion)
  • 数据分析(CSV / Excel / 报表)
  • 个人效率(邮件整理 / 计划复盘)

添加 Skills 步骤:

1️⃣ 创建技能文件

每个技能是一个 Markdown 文件,放在:

复制代码
~/openclaw/skills/

2️⃣ 启动自动加载

加载顺序:

  1. <workspace>/skills
  2. ~/.openclaw/skills
  3. Bundled skills

同名技能按优先级覆盖。

3️⃣ 额外目录

在配置文件中增加:

复制代码
skills.load.extraDirs

4️⃣ 使用 ClawHub

默认安装到:

复制代码
./skills
bash 复制代码
npx clawhub@latest install sonoscli

实用建议:

  • 初期只装 1--3 个关键技能
  • 每个逐个调试
  • 涉及生产数据必须审查源码

推荐实践路线

个人 / 家庭用户:

  • 用 nvm 安装 Node 22+
  • npm i -g openclaw
  • openclaw onboard
  • 本机测试
  • 调整为 LAN 模式
  • 运行安全审计
  • 少量添加技能

小团队 / 创业公司:

  • 使用 Docker 部署
  • 内网 + VPN
  • 优先接入钉钉或飞书
  • 创建不同用途的 Agent(独立 workspace)
  • 接入日志系统

安全敏感企业:

  • 部署在隔离网络
  • 开启 Docker/Nix 沙箱
  • 全部通道使用 allowlist
  • 安全部门参与技能审查
  • 定期深度审计

OpenClaw(原 Clawdbot / Moltbot)代表一种新形态:

不是再造一个 ChatGPT 网站,而是:

在你的机器、你的网络、你的规则下,构建一个真正能帮你工作的本地 AI 中枢。

从"浏览器 + 局域网"开始,逐步加入 Skills 与国内通道(钉钉、飞书、企微),你可以把它塑造成深度融入你工作流的"数字同事"。

如果愿意为环境与安全多投入一些耐心,这将是一项长期、值得的技术投资。

目录结构

核心目录

1. src/ - 核心源代码目录

主要源代码目录,包含:

  • src/cli/ - CLI 命令行接口
    • 程序构建、命令注册、依赖注入
    • 使用 Commander.js 框架
  • src/commands/ - CLI 命令实现
    • gatewayagentmessageonboard 等命令
  • src/gateway/ - Gateway 核心
    • WebSocket 控制平面、协议定义、会话管理
    • Gateway 是 OpenClaw 的控制中心
  • src/channels/ - 消息通道实现
    • 内置通道:telegramdiscordslacksignalimessageweb(WhatsApp)
    • 通道路由、消息处理、配对机制
  • src/plugins/ - 插件系统
    • 插件运行时、插件 SDK、插件加载
  • src/config/ - 配置管理
    • 配置文件加载、路径解析、配置验证
  • src/infra/ - 基础设施代码
    • 二进制管理、环境变量、错误处理、端口管理、运行时检查
  • src/media/ - 媒体处理管道
    • 图片/音频/视频处理、转录钩子
  • src/memory/ - 记忆系统
    • 会话记忆、长期记忆存储
  • src/process/ - 进程管理
    • 执行工具、进程监督、沙箱管理
  • src/terminal/ - 终端输出
    • 表格渲染、主题颜色、进度条
  • src/wizard/ - 引导向导
    • 首次设置向导、配置流程

2. extensions/ - 扩展插件目录

可选功能以插件形式存在:

  • extensions/msteams/ - Microsoft Teams 集成
  • extensions/matrix/ - Matrix 协议支持
  • extensions/zalo/extensions/zalouser/ - Zalo 集成
  • extensions/voice-call/ - 语音通话功能
  • extensions/bluebubbles/ - BlueBubbles(iMessage)支持
  • extensions/open-prose/ - Prose 技能系统
  • extensions/nostr/ - Nostr 协议支持
  • extensions/copilot-proxy/ - Copilot 代理
  • 其他扩展...

3. apps/ - 应用程序目录

平台应用:

  • apps/macos/ - macOS 菜单栏应用
    • Swift 代码,提供菜单栏控制、Voice Wake、Talk Mode、WebChat
  • apps/ios/ - iOS 节点应用
    • Canvas、Voice Wake、Talk Mode、相机功能
  • apps/android/ - Android 节点应用
    • Canvas、Talk Mode、相机、屏幕录制
  • apps/shared/ - 共享代码库
    • OpenClawKit - 跨平台共享 Swift 代码

4. skills/ - 技能目录

Agent 可用技能:

  • skills/github/ - GitHub 集成
  • skills/notion/ - Notion 集成
  • skills/slack/ - Slack 技能
  • skills/spotify-player/ - Spotify 播放控制
  • skills/obsidian/ - Obsidian 笔记集成
  • skills/bear-notes/ - Bear 笔记应用
  • skills/apple-reminders/ - Apple 提醒事项
  • 其他技能...

每个技能包含 SKILL.md 文件,描述技能的功能和使用方法。

5. docs/ - 文档目录

项目文档:

  • docs/channels/ - 各通道文档
  • docs/tools/ - 工具文档
  • docs/platforms/ - 平台特定文档
  • docs/zh-CN/ - 中文文档(自动生成)
  • docs/.i18n/ - 国际化配置

6. ui/ - Web UI 前端

Web 控制界面和 WebChat:

  • React/TypeScript 前端代码
  • Control UI 和 WebChat 界面

7. scripts/ - 脚本目录

构建、测试、部署脚本:

  • scripts/bundle-a2ui.sh - A2UI 打包脚本
  • scripts/package-mac-app.sh - macOS 应用打包
  • scripts/test-*.sh - 各种测试脚本
  • scripts/docs-*.mjs - 文档处理脚本

8. test/ - 测试目录

测试文件(通常与源代码文件同目录,使用 *.test.ts 命名)

9. dist/ - 构建输出目录

TypeScript 编译后的 JavaScript 代码(不提交到 Git)

10. Swabble/ - macOS 语音唤醒项目

独立的 macOS 语音唤醒守护进程(Swift),使用 Apple Speech.framework

配置文件

  • package.json - Node.js 项目配置和依赖
  • tsconfig.json - TypeScript 配置
  • vitest.config.ts - 测试配置
  • docker-compose.yml - Docker 配置
  • .github/ - GitHub Actions 工作流和模板

关键概念

  1. Gateway - WebSocket 控制平面,所有客户端通过 Gateway 通信
  2. Channels - 消息通道(WhatsApp、Telegram、Slack 等)
  3. Sessions - Agent 会话,每个会话有独立的上下文
  4. Skills - Agent 可调用的功能模块
  5. Nodes - 设备节点(macOS/iOS/Android),执行本地操作
  6. Plugins - 扩展系统,可选功能以插件形式提供

数据存储位置

运行时数据存储在 ~/.openclaw/

  • ~/.openclaw/openclaw.json - 主配置文件
  • ~/.openclaw/workspace/ - Agent 工作空间
  • ~/.openclaw/sessions/ - 会话数据
  • ~/.openclaw/credentials/ - 凭证存储

核心概念

Gateway 架构

一个长期运行的 Gateway 进程负责管理所有消息通道(WhatsApp 通过 Baileys、Telegram 通过 grammY、Slack、Discord、Signal、iMessage、WebChat)。

控制平面客户端(macOS 应用、CLI、Web UI、自动化程序)通过 WebSocket 连接到 Gateway,使用配置的绑定地址(默认 127.0.0.1:18789)。

节点(macOS / iOS / Android / 无头模式)同样通过 WebSocket 连接,但会声明角色为 node,并显式声明其能力(caps)和可执行命令。

每个主机只运行一个 Gateway;它是唯一会建立 WhatsApp 会话的组件。

Canvas 主机由 Gateway 的 HTTP 服务器提供,路径如下:

  • /__openclaw__/canvas/(可由 Agent 编辑的 HTML / CSS / JS)
  • /__openclaw__/a2ui/(A2UI 主机)

它们与 Gateway 使用相同端口(默认 18789)。

组件与数据流

Gateway(守护进程)
  • 维护各类提供商连接
  • 提供类型化的 WebSocket API(请求、响应、服务器推送事件)
  • 使用 JSON Schema 校验入站帧
  • 发送事件,例如:agent、chat、presence、health、heartbeat、cron
客户端(mac 应用 / CLI / Web 管理界面)
  • 每个客户端建立一个 WebSocket 连接
  • 发送请求(health、status、send、agent、system-presence)
  • 订阅事件(tick、agent、presence、shutdown)
节点(macOS / iOS / Android / 无头模式)
  • 使用 role: node 连接到同一个 WebSocket 服务器

  • 在连接时提供设备身份标识;配对基于设备进行(角色为 node),审批记录存储在设备配对存储中

  • 暴露命令,例如:

    • canvas.*
    • camera.*
    • screen.record
    • location.get

协议细节:

Gateway 协议

WebChat
  • 静态 UI,通过 Gateway 的 WebSocket API 获取聊天历史并发送消息
  • 在远程部署场景中,通过与其他客户端相同的 SSH / Tailscale 隧道连接

连接生命周期(单客户端)

Wire协议(概要)

传输方式: WebSocket,使用文本帧承载 JSON 负载。

首帧必须为 connect

完成握手后:

  • 请求(Requests)
    {type:"req", id, method, params}


    {type:"res", id, ok, payload|error}

  • 事件(Events)
    {type:"event", event, payload, seq?, stateVersion?}

如果设置了 OPENCLAW_GATEWAY_TOKEN(或使用 --token 参数),则
connect.params.auth.token 必须匹配,否则连接会被关闭。

对于具有副作用的方法(如 sendagent),必须提供**幂等键(Idempotency keys)**以支持安全重试;服务器会维护一个短生命周期的去重缓存。

节点必须在 connect 时包含:

  • role: "node"
  • 以及 caps / commands / permissions 信息

配对 + 本地信任机制

所有 WebSocket 客户端(操作端 + 节点)在连接时都必须包含设备身份标识

  • 新设备 ID 需要通过配对审批;
  • Gateway 会为后续连接签发设备令牌(device token)。

本地连接(回环地址或 Gateway 主机自身的 tailnet 地址)可以自动审批,以保证同主机体验流畅。

非本地连接 必须对 connect.challenge 随机数进行签名,并且需要显式审批。

无论本地还是远程连接,gateway.auth.* 认证机制始终适用。

协议类型系统与代码生成

  • 协议由 TypeBox schema 定义
  • 从 TypeBox 生成 JSON Schema
  • 再从 JSON Schema 生成 Swift 模型

远程访问

推荐方式: Tailscale 或 VPN

替代方式: SSH 隧道

bash 复制代码
ssh -N -L 18789:127.0.0.1:18789 user@host

通过隧道连接时,握手与认证 token 机制保持一致。

在远程部署场景中,可以启用:

  • TLS
  • 可选的证书固定(pinning)

运维快照

启动:

bash 复制代码
openclaw gateway

(前台运行,日志输出到 stdout)

健康检查:

通过 WebSocket 的 health 方法(也包含在 hello-ok 响应中)。

进程监管:

使用 launchd / systemd 实现自动重启。

硬性规定

  • 每台主机仅允许一个 Gateway 管理单个 Baileys 会话。
  • 握手是强制性的;任何非 JSON 或首帧非 connect 的连接都会被立即关闭。
  • 事件不会被重放;客户端在检测到事件间隙时必须主动刷新状态。

Gateway 架构通俗解释(基于源码)

一、Gateway 是什么?

Gateway 是一个长期运行的 Node.js 进程,同时提供:

  1. HTTP 服务器(处理网页、API、Canvas)
  2. WebSocket 服务器(处理实时通信)

两者共享同一个端口(默认 18789)。

二、启动流程(基于 server.impl.ts

typescript 复制代码
// 1. 调用 startGatewayServer() (第162行)
startGatewayServer(port = 18789, opts)

// 2. 创建运行时状态 (第353行)
createGatewayRuntimeState({
  // 这会创建 HTTP 服务器和 WebSocket 服务器
})

// 3. 创建通道管理器 (第405行)
channelManager = createChannelManager(...)

// 4. 附加 WebSocket 处理器 (第541行)
attachGatewayWsHandlers({ wss, ... })

三、HTTP 和 WebSocket 的关系

源码位置:src/gateway/server-runtime-state.ts

typescript 复制代码
// 第122行:创建 HTTP 服务器
const httpServer = createGatewayHttpServer({...})

// 第159行:创建 WebSocket 服务器(不直接监听端口)
const wss = new WebSocketServer({
  noServer: true,  // 关键:不自己监听端口
  maxPayload: MAX_PAYLOAD_BYTES,
})

// 第164行:将 WebSocket 附加到 HTTP 服务器
attachGatewayUpgradeHandler({
  httpServer: server,  // 共享同一个 HTTP 服务器
  wss,
  ...
})

要点:

  • HTTP 服务器监听端口(如 18789)
  • WebSocket 服务器不独立监听,通过 HTTP 升级(upgrade)建立连接
  • 两者共用同一端口,WebSocket 连接通过 HTTP 升级请求建立

四、连接建立流程(基于 ws-connection.ts

源码位置:src/gateway/server/ws-connection.ts 第103行

typescript 复制代码
wss.on("connection", (socket, upgradeReq) => {
  // 1. 生成连接ID和挑战随机数
  const connId = randomUUID();
  const connectNonce = randomUUID();
  
  // 2. 立即发送挑战事件(第163-167行)
  send({
    type: "event",
    event: "connect.challenge",
    payload: { nonce: connectNonce, ts: Date.now() },
  });
  
  // 3. 等待客户端发送 connect 请求
  // 处理逻辑在 message-handler.ts
})

五、握手验证(基于 message-handler.ts

源码位置:src/gateway/server/ws-connection/message-handler.ts 第203-299行

typescript 复制代码
// 第206行:验证第一帧必须是 connect 请求
if (!isRequestFrame || parsed.method !== "connect") {
  // 关闭连接
  close(1008, "invalid handshake");
  return;
}

// 第273行:协议版本协商
if (maxProtocol < PROTOCOL_VERSION || minProtocol > PROTOCOL_VERSION) {
  close(1002, "protocol mismatch");
  return;
}

// 第290行:角色验证(只能是 operator 或 node)
const role = roleRaw === "operator" || roleRaw === "node" ? roleRaw : null;
if (!role) {
  close(1008, "invalid role");
  return;
}

// 第342行:认证检查
const authResult = await authorizeGatewayConnect({...})

六、Canvas 路径处理

源码位置:src/gateway/server-http.ts 第90-98行

typescript 复制代码
function isCanvasPath(pathname: string): boolean {
  return (
    pathname === A2UI_PATH ||           // "/__openclaw__/a2ui/"
    pathname.startsWith(`${A2UI_PATH}/`) ||
    pathname === CANVAS_HOST_PATH ||    // "/__openclaw__/canvas/"
    pathname.startsWith(`${CANVAS_HOST_PATH}/`) ||
    pathname === CANVAS_WS_PATH         // Canvas WebSocket
  );
}

Canvas 通过 HTTP 服务器提供,与 Gateway 使用同一端口。

七、消息类型(基于协议定义)

源码位置:src/gateway/protocol/schema/frames.ts

三种消息类型:

  1. 请求(Request)
typescript 复制代码
{
  type: "req",
  id: "唯一ID",
  method: "方法名",
  params: {...}
}
  1. 响应(Response)
typescript 复制代码
{
  type: "res",
  id: "对应请求的ID",
  ok: true/false,
  payload: {...}  // 成功时
  error: {...}    // 失败时
}
  1. 事件(Event)
typescript 复制代码
{
  type: "event",
  event: "事件名",
  payload: {...},
  seq?: number,
  stateVersion?: {...}
}

八、角色系统(基于 message-handler.ts

源码位置:src/gateway/server/ws-connection/message-handler.ts 第290行

typescript 复制代码
// 只允许两种角色
const role = roleRaw === "operator" || roleRaw === "node" ? roleRaw : null;

// Operator(操作端)
// - 可以调用所有 Gateway 方法
// - 需要声明 scopes(权限范围)

// Node(节点)
// - 声明 caps(能力)、commands(命令)、permissions(权限)
// - 只能被调用,不能主动调用 Gateway 方法

九、设备配对机制

源码位置:src/gateway/server/ws-connection/message-handler.ts 第329-450行

typescript 复制代码
// 第329行:获取设备信息
const deviceRaw = connectParams.device;

// 第342行:检查是否有设备令牌
const hasDeviceTokenCandidate = Boolean(
  connectParams.auth?.token && device
);

// 设备配对流程:
// 1. 客户端发送设备身份(device.id + publicKey + signature)
// 2. Gateway 检查是否已配对
// 3. 未配对 → 需要审批
// 4. 已配对 → 颁发设备令牌(deviceToken)

本地连接(127.0.0.1)可自动审批,远程连接需要显式审批。

十、实际工作示例

场景:CLI 连接到 Gateway

复制代码
1. CLI 建立 WebSocket 连接
   ws://127.0.0.1:18789

2. Gateway 立即发送挑战(ws-connection.ts:163)
   Gateway → CLI: {type:"event", event:"connect.challenge", payload:{nonce:"..."}}

3. CLI 发送 connect 请求(message-handler.ts:206)
   CLI → Gateway: {
     type: "req",
     method: "connect",
     params: {
       role: "operator",
       scopes: ["operator.read", "operator.write"],
       device: {id: "...", publicKey: "...", signature: "..."}
     }
   }

4. Gateway 验证并响应(message-handler.ts:成功路径)
   Gateway → CLI: {
     type: "res",
     ok: true,
     payload: {
       type: "hello-ok",
       protocol: 3,
       auth: {deviceToken: "..."}  // 如果已配对
     }
   }

5. 连接建立,可以正常通信

十一、关键代码位置总结

功能 文件位置 关键行数
Gateway 启动入口 src/gateway/server.impl.ts 162
HTTP/WS 服务器创建 src/gateway/server-runtime-state.ts 122, 159
WebSocket 连接处理 src/gateway/server/ws-connection.ts 103
握手验证 src/gateway/server/ws-connection/message-handler.ts 206-299
HTTP 请求处理 src/gateway/server-http.ts 476
协议定义 src/gateway/protocol/schema/frames.ts 全部

十二、重要不变量

  1. 每台主机只有一个 Gateway
    • 源码:src/gateway/server/http-listen.ts 第26行检查 EADDRINUSE
  2. 首帧必须是 connect
    • 源码:message-handler.ts 第206-243行,否则关闭连接
  3. 事件不重放
    • 客户端需主动刷新状态(如通过 health 方法)

总结:

Gateway 是一个 HTTP + WebSocket 服务器:

  • HTTP 服务器处理网页、API、Canvas
  • WebSocket 服务器通过 HTTP 升级建立,处理实时通信
  • 所有客户端通过 WebSocket 连接,首帧必须是 connect
  • 支持两种角色:operator(控制端)和 node(设备节点)
  • 设备需要配对才能连接(本地自动,远程需审批)

Agent 运行时

OpenClaw 运行一个内嵌的 Agent 运行时,源自 pi-mono。

OpenClaw 使用单一的 Agent 工作区目录(agents.defaults.workspace)作为 Agent 的唯一工作目录(cwd),用于工具执行与上下文环境。

推荐做法:

使用 openclaw setup 在缺失时创建 ~/.openclaw/openclaw.json,并初始化工作区文件。

如果启用了 agents.defaults.sandbox,则非主会话 可以在
agents.defaults.sandbox.workspaceRoot 下使用独立的每会话工作区(详见 Gateway 配置)。

Bootstrap 文件(自动注入)

agents.defaults.workspace 目录中,OpenClaw 期望存在以下用户可编辑文件

  • AGENTS.md ------ 操作说明 + "记忆"
  • SOUL.md ------ 人设、边界、语气
  • TOOLS.md ------ 用户维护的工具说明(例如 imsg、sag、使用约定等)
  • BOOTSTRAP.md ------ 首次运行的一次性仪式(完成后会删除)
  • IDENTITY.md ------ Agent 名称 / 氛围 / emoji
  • USER.md ------ 用户档案 + 偏好的称呼方式

新会话的第一轮对话中,OpenClaw 会将这些文件的内容直接注入到 Agent 上下文中。

规则如下:

  • 空文件会被跳过
  • 大文件会被裁剪,并添加截断标记,以保持提示词精简(需要时可读取完整文件)
  • 如果文件缺失,OpenClaw 会注入一行"缺失文件"标记(并且 openclaw setup 会创建安全的默认模板)

BOOTSTRAP.md 仅在全新工作区 (不存在其他 bootstrap 文件)时创建。

如果在完成首次仪式后删除该文件,它不会在后续重启时重新创建。

如果希望完全禁用 bootstrap 文件创建(例如已预置好工作区),可以设置:

json 复制代码
{ agent: { skipBootstrap: true } }

内置工具

核心工具(read / exec / edit / write 以及相关系统工具)始终可用,但受工具策略限制。

apply_patch 是可选工具,受 tools.exec.applyPatch 开关控制。

TOOLS.md 并不决定哪些工具存在;它仅用于说明你希望如何使用这些工具。

Skills(技能)

OpenClaw 会从以下三个位置加载技能(若名称冲突,以工作区中的为准):

  • 内置(Bundled):随安装包提供
  • 本地管理目录(Managed/local)~/.openclaw/skills
  • 工作区目录(Workspace)<workspace>/skills

技能可以通过配置或环境变量进行启用/禁用控制(详见 Gateway 配置中的 skills)。

pi-mono 集成

OpenClaw 复用了 pi-mono 代码库中的部分组件(如模型与工具),但:

  • 会话管理由 OpenClaw 自行实现
  • 服务发现由 OpenClaw 管理
  • 工具绑定由 OpenClaw 控制

不使用:

  • pi-coding agent runtime
  • ~/.pi/agent
  • <workspace>/.pi 配置

会话(Sessions)

会话记录以 JSONL 格式存储在:

复制代码
~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl
  • Session ID 由 OpenClaw 生成并保持稳定
  • 不会读取旧版 Pi/Tau 的会话目录

流式过程中进行引导(Steering while streaming)

当队列模式(queue mode)为 steer 时:

  • 新收到的消息会被注入到当前运行中

  • 每次工具调用完成后都会检查队列

  • 如果存在排队消息:

    • 当前 assistant 消息剩余的工具调用会被跳过
    • 这些被跳过的工具会返回错误结果:
      "Skipped due to queued user message."
    • 然后将排队的用户消息注入
    • 再生成新的 assistant 响应

当队列模式为 followupcollect 时:

  • 新消息会暂存
  • 等当前回合结束后
  • 启动一个新的 agent 回合,并携带排队的消息

详见 Queue 文档(包括模式、去抖动 debounce 与容量上限行为)。

Block Streaming(分块流式):

  • 在一个 assistant 块完成时立即发送

  • 默认关闭(agents.defaults.blockStreamingDefault: "off"

  • 分块边界可通过 agents.defaults.blockStreamingBreak 调整:

    • text_end(默认)
    • message_end
  • 块大小通过 agents.defaults.blockStreamingChunk 控制(默认 800--1200 字符)

    • 优先按段落切分
    • 其次换行
    • 最后按句子
  • 使用 agents.defaults.blockStreamingCoalesce 合并流式块,减少单行刷屏(基于空闲时间合并后发送)

  • 非 Telegram 渠道必须显式设置 *.blockStreaming: true 才会启用分块回复

  • 详细工具摘要在工具启动时立即发送(无 debounce)

  • Control UI 在可用时通过 agent 事件流式展示工具输出

模型引用(Model refs)

配置中的模型引用(例如 agents.defaults.modelagents.defaults.models)会按第一个 / 分割解析。

推荐格式:

复制代码
provider/model

如果模型 ID 本身包含 /(例如 OpenRouter 风格),必须包含 provider 前缀,例如:

复制代码
openrouter/moonshotai/kimi-k2

如果省略 provider:

  • OpenClaw 会将其视为别名
  • 或视为默认 provider 下的模型
  • 仅当模型 ID 本身不包含 / 时可行

最小配置(Configuration)

至少需要设置:

  • agents.defaults.workspace
  • channels.whatsapp.allowFrom(强烈推荐设置)

Memory(记忆)

OpenClaw 的记忆是存放在代理工作区中的纯 Markdown 文件。

这些文件才是唯一的真实来源(source of truth);模型只会"记住"被写入磁盘的内容。

记忆搜索工具由当前启用的记忆插件提供(默认:memory-core)。

可通过以下配置禁用记忆插件:

json 复制代码
plugins.slots.memory = "none"

默认工作区布局包含两层记忆:

memory/YYYY-MM-DD.md

  • 每日日志(仅追加写入)。
  • 在每次会话开始时,建议读取今天 + 昨天的日志。

MEMORY.md(可选)

  • 整理后的长期记忆。
  • 仅在主私有会话中加载(绝不在群组上下文中加载)。

这些文件位于工作区(agents.defaults.workspace,默认 ~/.openclaw/workspace)。

OpenClaw 为这些 Markdown 文件提供两个面向代理的工具:

memory_search

  • 基于语义索引的记忆检索(semantic recall)。

memory_get

  • 读取指定 Markdown 文件或特定行范围。

现在,当文件不存在时(例如当天尚未创建日志),memory_get优雅降级

  • 不再抛出 ENOENT 错误
  • 返回 { text: "", path }

无论是内置管理器还是 QMD 后端都会如此处理。

这样代理可以自然处理"尚未记录任何内容"的情况,而无需用 try/catch 包裹工具调用。

何时写入记忆

  • 决策、偏好、长期有效的事实 → 写入 MEMORY.md
  • 日常笔记、运行中的上下文 → 写入 memory/YYYY-MM-DD.md
  • 如果有人说"记住这个" → 必须写入文件(不要只保存在上下文中)

这一机制仍在演进中。

主动提醒模型存储记忆通常是有帮助的------模型会知道该如何处理。

如果你希望某件事"长期有效",就明确要求机器人写入记忆文件。

自动记忆刷新(压缩前提示)

当会话接近自动上下文压缩(auto-compaction)时,OpenClaw 会触发一个静默的代理回合,提醒模型在压缩前写入长期记忆。

默认提示说明模型可以回复,但通常正确响应是 NO_REPLY,这样用户不会看到该回合。

相关配置项:agents.defaults.compaction.memoryFlush

json 复制代码
{
  "agents": {
    "defaults": {
      "compaction": {
        "reserveTokensFloor": 20000,
        "memoryFlush": {
          "enabled": true,
          "softThresholdTokens": 4000,
          "systemPrompt": "Session nearing compaction. Store durable memories now.",
          "prompt": "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store."
        }
      }
    }
  }
}

机制说明:

  • 软阈值触发条件:

    当会话 token 估算值超过
    contextWindow - reserveTokensFloor - softThresholdTokens 时触发刷新。

  • 默认静默:

    提示中包含 NO_REPLY,因此不会向用户发送任何内容。

  • 双提示机制:

    包括一个 user prompt 和一个 system prompt,共同附加提醒。

  • 每个压缩周期只触发一次:

    sessions.json 中记录。

  • 工作区必须可写:

    若会话在沙箱中运行且 workspaceAccess"ro""none",则不会触发刷新。

向量记忆搜索

OpenClaw 可以为 MEMORY.mdmemory/*.md 构建一个小型向量索引,使语义查询能够找到相关笔记,即便表述不同。

默认行为:

  • 默认启用
  • 监听记忆文件变化(防抖处理)
  • 配置路径:agents.defaults.memorySearch(不是顶层 memorySearch

嵌入模式:

  • 默认使用远程 embeddings

  • 如果未设置 memorySearch.provider,OpenClaw 会自动选择:

    1. local:如果配置了 memorySearch.local.modelPath 且文件存在
    2. openai:如果可解析到 OpenAI Key
    3. gemini:如果可解析到 Gemini Key
    4. voyage:如果可解析到 Voyage Key
  • 如果都未配置,记忆搜索保持禁用,直到手动配置

本地模式:

  • 使用 node-llama-cpp
  • 可能需要 pnpm approve-builds 批准构建
  • 使用 sqlite-vec(如可用)加速 SQLite 内的向量搜索

远程嵌入:

  • 需要嵌入提供商的 API Key

  • OpenClaw 可从以下位置解析 Key:

    • auth 配置文件
    • models.providers.*.apiKey
    • 环境变量
  • 注意事项:

    • Codex OAuth 仅适用于 chat/completions,不可用于记忆搜索 embeddings
    • Gemini:使用 GEMINI_API_KEYmodels.providers.google.apiKey
    • Voyage:使用 VOYAGE_API_KEYmodels.providers.voyage.apiKey
    • 自定义 OpenAI 兼容端点:设置 memorySearch.remote.apiKey(可选 memorySearch.remote.headers
QMD 后端(实验性)

通过设置 memory.backend = "qmd",可以将内置的 SQLite 索引器替换为 QMD:一种本地优先的搜索 sidecar,结合了 BM25、向量检索和重排序。Markdown 文件仍然是唯一的真实来源;OpenClaw 调用 QMD 来进行检索。

前置条件:

  • 默认禁用,需要显式开启:memory.backend = "qmd"

  • 单独安装 QMD CLI:

    bash 复制代码
    bun install -g https://github.com/tobi/qmd
    # 或下载 release 并确保 qmd 可执行文件在网关 PATH 中
  • QMD 需要一个支持扩展的 SQLite 构建(macOS 可通过 brew install sqlite

  • 完全本地运行:Bun + node-llama-cpp,并在首次使用时自动从 HuggingFace 下载 GGUF 模型(无需 Ollama 守护进程)

  • 网关通过设置 XDG_CONFIG_HOMEXDG_CACHE_HOME 将 QMD 运行在自包含的目录:
    ~/.openclaw/agents/<agentId>/qmd/

  • 支持系统:macOS 和 Linux(安装 Bun + SQLite 后即可);Windows 推荐通过 WSL2

Sidecar 的运行方式:

  1. 网关在 ~/.openclaw/agents/<agentId>/qmd/ 创建自包含的 QMD home(存放配置、缓存和 SQLite 数据库)

  2. 通过 qmd collection addmemory.qmd.paths(以及默认工作区记忆文件)加入集合

  3. 启动和间隔更新执行:qmd update + qmd embed(间隔可配置:memory.qmd.update.interval,默认 5 分钟)

  4. 网关启动时初始化 QMD 管理器,即使在首次 memory_search 调用前,周期性更新计时器也已激活

  5. 启动刷新默认后台运行,聊天启动不被阻塞;若希望保持旧的阻塞行为,可设置:

    json 复制代码
    memory.qmd.update.waitForBootSync = true
  6. 搜索通过 memory.qmd.searchMode 运行(默认 qmd search --json,也支持 vsearchquery

    • 若选定模式在 QMD 构建中不支持某些标志,OpenClaw 会自动重试 qmd query
    • 若 QMD 失败或二进制缺失,自动回退到内置 SQLite 管理器,保证记忆工具继续工作
  7. 当前不提供 QMD embed 批量大小调节;批处理行为由 QMD 自身控制

  8. 首次搜索可能较慢:QMD 可能首次下载 GGUF 模型(重排序/查询扩展)

  9. OpenClaw 运行 QMD 时会自动设置 XDG_CONFIG_HOME / XDG_CACHE_HOME

  10. 若希望预下载模型,可在代理的 XDG 目录下执行一次查询以"预热"索引

示例:手动对齐 OpenClaw 使用的状态目录:

bash 复制代码
STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"

export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config"
export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache"

# 可选:强制刷新索引 + embedding
qmd update
qmd embed

# 触发首次模型下载 / 预热索引
qmd query "test" -c memory-root --json >/dev/null 2>&1

配置项(memory.qmd.*):

  • command (默认 qmd):覆盖可执行路径
  • searchMode (默认 search):选择用于 memory_search 的 QMD 命令(search / vsearch / query
  • includeDefaultMemory (默认 true):自动索引 MEMORY.md + memory/**/*.md
  • paths[]:添加额外目录/文件(可选 pattern 与稳定名称)
  • sessions :选择是否将会话 JSONL 索引到 QMD 集合中(enabledretentionDaysexportDir
  • update:控制刷新周期和维护行为(interval, debounceMs, onBoot, waitForBootSync, embedInterval, commandTimeoutMs, updateTimeoutMs, embedTimeoutMs)
  • limits:限制检索结果大小(maxResults, maxSnippetChars, maxInjectedChars, timeoutMs)
  • scope :同 session.sendPolicy 架构,默认仅 DM,可放宽以在群组/通道展示 QMD 命中
  • match.keyPrefix / match.rawKeyPrefix:匹配会话键前缀,用于范围过滤

当 scope 拒绝搜索时,OpenClaw 会记录警告并标记通道/聊天类型,便于调试空结果。

  • 工作区外的片段会显示为 qmd/<collection>/<relative-path>memory_get 会识别前缀并读取对应 QMD collection 根目录
  • memory.qmd.sessions.enabled = true 时,OpenClaw 会将清理后的会话记录 导出到专用 QMD 集合,以便 memory_search 回忆最近对话,而不触及内置 SQLite 索引
  • memory.citations = auto/on 时,memory_search 的 snippet 会附加 Source: <path#line>;若设为 off,路径元数据仅内部使用(memory_get 仍可访问路径,系统提示会告知代理不要引用)

示例配置:

json 复制代码
memory: {
  backend: "qmd",
  citations: "auto",
  qmd: {
    includeDefaultMemory: true,
    update: { interval: "5m", debounceMs: 15000 },
    limits: { maxResults: 6, timeoutMs: 4000 },
    scope: {
      default: "deny",
      rules: [
        { action: "allow", match: { chatType: "direct" } },
        { action: "deny", match: { keyPrefix: "discord:channel:" } },
        { action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } }
      ]
    },
    paths: [
      { name: "docs", path: "~/notes", pattern: "**/*.md" }
    ]
  }
}

引用与回退:

  • memory.citations 在任何后端下均有效(auto/on/off)
  • 当 QMD 运行时,状态标记为 status().backend = "qmd",便于诊断显示使用的引擎
  • 若 QMD 子进程退出或 JSON 输出不可解析,搜索管理器会记录警告并回退至内置 Markdown embedding 以保证记忆工具继续可用
额外的记忆路径

如果你想索引默认工作区布局之外的 Markdown 文件,可以添加显式路径:

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
    }
  }
}

说明:

  • 路径可以是绝对路径或相对于工作区的相对路径。
  • 目录会递归扫描 .md 文件。
  • 仅索引 Markdown 文件。
  • 符号链接(文件或目录)会被忽略。
Gemini 嵌入(原生)

将 provider 设置为 gemini,直接使用 Gemini embeddings API:

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      provider: "gemini",
      model: "gemini-embedding-001",
      remote: {
        apiKey: "YOUR_GEMINI_API_KEY"
      }
    }
  }
}

说明:

  • remote.baseUrl 可选,默认为 Gemini API 基础 URL。
  • remote.headers 可用于添加额外请求头。
  • 默认模型:gemini-embedding-001

如果你想使用自定义 OpenAI 兼容端点(如 OpenRouter、vLLM 或代理),可用 OpenAI provider 的 remote 配置:

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      remote: {
        baseUrl: "https://api.example.com/v1/",
        apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
        headers: { "X-Custom-Header": "value" }
      }
    }
  }
}

如果不想设置 API Key,可以使用:

  • memorySearch.provider = "local"
  • memorySearch.fallback = "none"

回退机制(Fallbacks):

  • memorySearch.fallback 可选值:openaigeminilocalnone
  • 仅在主嵌入提供商失败时才使用回退

批量索引(OpenAI + Gemini + Voyage):

  • 默认关闭

  • 设置 agents.defaults.memorySearch.remote.batch.enabled = true 可启用大规模索引

  • 默认行为会等待批处理完成,可根据需要调整:

    • remote.batch.wait
    • remote.batch.pollIntervalMs
    • remote.batch.timeoutMinutes
  • 设置 remote.batch.concurrency 控制并行批处理作业数量(默认 2)

  • 批量模式适用于 memorySearch.provider = "openai""gemini",并使用对应 API Key

  • Gemini 批量作业使用异步 embeddings 批量端点,需要 Gemini Batch API 支持

OpenAI 批量优势:

  • 支持大量请求异步处理,速度快且成本低
  • 批量 API 提供折扣,通常比同步请求更经济

参考文档:

配置示例:

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      fallback: "openai",
      remote: {
        batch: { enabled: true, concurrency: 2 }
      },
      sync: { watch: true }
    }
  }
}

工具:

  • memory_search --- 返回包含文件路径 + 行号范围的 snippet
  • memory_get --- 按路径读取记忆文件内容

本地模式:

  • 设置 agents.defaults.memorySearch.provider = "local"
  • 提供 agents.defaults.memorySearch.local.modelPath(GGUF 或 hf: URI)
  • 可选:设置 agents.defaults.memorySearch.fallback = "none" 避免远程回退
记忆工具的工作原理
  • memory_search 对 Markdown 块(约 400 token,重叠 80 token)进行语义搜索,来源于 MEMORY.mdmemory/**/*.md。返回信息包括 snippet 文本(约 700 字符上限)、文件路径、行号范围、分数、提供商/模型,以及是否从本地回退到远程 embeddings。不返回完整文件内容
  • memory_get 读取指定的记忆 Markdown 文件(相对于工作区路径),可选从起始行开始读取 N 行。超出 MEMORY.md / memory/ 以外的路径会被拒绝。
  • 两个工具仅在 memorySearch.enabled 对该 agent 返回 true 时启用。
索引内容(以及时机)
  • 文件类型 :仅 Markdown (MEMORY.mdmemory/**/*.md)
  • 索引存储 :每个 agent 对应的 SQLite 文件,路径默认 ~/.openclaw/memory/<agentId>.sqlite,可通过 agents.defaults.memorySearch.store.path 配置
  • 新鲜度MEMORY.md + memory/ 文件变化触发 watcher 标记索引为脏(防抖 1.5 秒)。同步在会话启动、搜索或定时器触发时异步执行。会话记录使用 delta 阈值触发后台同步。
  • 重新索引触发条件:索引存储嵌入提供商/模型 + 端点指纹 + 块分割参数。任何变化都会触发 OpenClaw 自动重置并重新索引整个存储。
混合搜索(BM25 + 向量)

启用时,OpenClaw 将两种信号结合:

  1. 向量相似度(语义匹配,可词汇不同)
  2. BM25 关键字相关性(精确 token,如 ID、环境变量、代码符号)

如果平台不支持全文搜索(FTS5),则降级为向量搜索。

为什么要混合?

  • 向量搜索擅长"意思相同但表达不同":

    • "Mac Studio gateway host" ↔ "the machine running the gateway"
    • "debounce file updates" ↔ "avoid indexing on every write"
  • 但对精确信号 token 较弱:

    • ID(如 a828e60, b3b9895a...)
    • 代码符号(memorySearch.query.hybrid)
    • 错误字符串("sqlite-vec unavailable")
  • BM25 正好相反:精确 token 强,意译弱

  • 混合搜索是折中方案,兼顾自然语言查询与"针眼式"查询

结果合并(当前设计):

实现思路

  1. 从向量和 BM25 两端各取候选池:

    • 向量:按余弦相似度选 top maxResults * candidateMultiplier
    • BM25:按 FTS5 BM25 排名选 top maxResults * candidateMultiplier(排名越低越好)
  2. 将 BM25 排名转换为 0~1 分数:

    text 复制代码
    textScore = 1 / (1 + max(0, bm25Rank))
  3. 以块 ID 联合候选项并计算加权分数:

    text 复制代码
    finalScore = vectorWeight * vectorScore + textWeight * textScore

注意

  • vectorWeight + textWeight 在配置解析时归一化为 1.0
  • 若嵌入不可用或返回零向量,仍运行 BM25
  • FTS5 创建失败时保持向量搜索(不报错)

未来可扩展:如 RRF(Reciprocal Rank Fusion)或分数归一化(min/max、z-score)

后处理管道:

向量 + 关键字得分合并后,可进行两级可选后处理:

复制代码
Vector + Keyword → Weighted Merge → Temporal Decay → Sort → MMR → Top-K Results
  • 默认关闭,可独立启用

MMR 重排序(多样性):

  • 解决重复或高度相似的 snippet 问题
  • 示例:搜索 "home network setup",可能返回五条几乎相同的每日笔记
  • MMR(最大边际相关)重排序平衡相关性与多样性

原理

  1. 根据原始分数(向量 + BM25 加权)评分

  2. 迭代选择最大化公式的结果:

    复制代码
    λ × relevance − (1−λ) × max_similarity_to_selected
  3. 相似度使用 Jaccard 文本相似度

  4. λ 控制权衡:

    • λ = 1 → 仅相关性
    • λ = 0 → 最大多样性
    • 默认 λ = 0.7(轻微偏相关性)

示例("home network setup"):

文件 原文 无 MMR 启用 MMR(λ=0.7)
memory/2026-02-10.md 配置 Omada 路由器,VLAN10 IoT 1 1
memory/2026-02-08.md 配置 Omada 路由器,VLAN10 IoT 2(重复) ---(被剔除)
memory/2026-02-05.md 配置 AdGuard DNS 192.168.10.2 3 3
memory/network.md 路由器:Omada ER605,AdGuard:192.168.10.2 4 2

MMR 确保 agent 获取不同信息,而非重复。

何时启用 :当 memory_search 返回大量重复 snippet 时,尤其每日笔记会重复信息

时间衰减(新近性提升):

  • 每日笔记随时间累积,旧笔记可能覆盖新信息

  • 时间衰减按结果年龄指数衰减得分:

    复制代码
    decayedScore = score × e^(-λ × ageInDays)
    λ = ln(2) / halfLifeDays
  • 默认半衰期 30 天:

时间 得分占比
今天 100%
7 天前 84%
30 天前 50%
90 天前 12.5%
180 天前 1.6%
  • 永久文件不衰减:MEMORY.md、memory/ 下非日期文件
  • 日期文件使用文件名日期,其他源(如会话记录)使用修改时间

示例(今天 2 月 10 日):

文件 内容 原分数 衰减后
memory/2025-09-15.md Rod 周一到周五工作 0.91 0.03
memory/2026-02-10.md Rod 今日 standup 0.82 0.82
memory/2026-02-03.md Rod 新团队 standup 0.80 0.68

旧笔记自动排到后面

何时启用:当 agent 有大量每日笔记且旧信息覆盖新信息时

  • 半衰期 30 天适合每日笔记密集场景
  • 若经常引用旧笔记,可增加半衰期(如 90 天)

配置示例

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      query: {
        hybrid: {
          enabled: true,
          vectorWeight: 0.7,
          textWeight: 0.3,
          candidateMultiplier: 4,
          mmr: {
            enabled: true,   
            lambda: 0.7      
          },
          temporalDecay: {
            enabled: true,   
            halfLifeDays: 30 
          }
        }
      }
    }
  }
}
  • MMR 独立启用:适合大量相似笔记,但年龄不重要
  • 时间衰减独立启用:适合新近性重要,但结果已多样
  • 同时启用:适合长期运行、每日笔记密集的 agent

嵌入缓存(Embedding Cache)

OpenClaw 可以将文本块的嵌入向量缓存到 SQLite,这样在重新索引或频繁更新(尤其是会话记录)时,就不会重复为未改变的文本生成嵌入。

配置示例

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      cache: {
        enabled: true,
        maxEntries: 50000
      }
    }
  }
}

会话记忆搜索(实验性功能)

你可以选择索引会话记录,并通过 memory_search 查询。这是一个实验性功能,需要手动启用。

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      experimental: { sessionMemory: true },
      sources: ["memory", "sessions"]
    }
  }
}

注意事项

  • 会话索引需要手动启用(默认关闭)

  • 会话更新经过防抖处理,异步索引,一旦超过 delta 阈值则触发(尽力而为)

  • memory_search 不会因索引阻塞;结果可能在后台同步完成前略显过时

  • 结果仅包含 snippet,memory_get 仍仅限 memory 文件

  • 会话索引是按 agent 隔离的,仅索引该 agent 的会话日志

  • 会话日志保存在磁盘:~/.openclaw/agents/<agentId>/sessions/*.jsonl

    • 任何拥有文件系统访问权限的进程或用户都可以读取
    • 为更严格的隔离,可在不同 OS 用户或主机上运行 agent

delta 阈值示例

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      sync: {
        sessions: {
          deltaBytes: 100000,   // ~100 KB
          deltaMessages: 50     // JSONL 行数
        }
      }
    }
  }
}

SQLite 向量加速(sqlite-vec)

  • 当可用时,OpenClaw 会将嵌入存储在 SQLite 虚拟表(vec0)中,并在数据库内执行向量距离查询
  • 这样可以在不将所有嵌入加载到 JS 的情况下保持搜索速度

可选配置示例

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      store: {
        vector: {
          enabled: true,
          extensionPath: "/path/to/sqlite-vec"
        }
      }
    }
  }
}

注意事项

  • enabled 默认 true;关闭后搜索回退为 JS 内存余弦相似度
  • 如果 sqlite-vec 缺失或加载失败,OpenClaw 会记录错误并继续使用 JS 回退
  • extensionPath 可覆盖内置路径,用于自定义构建或非标准安装

本地嵌入模型自动下载

  • 默认本地嵌入模型:hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf(约 0.6 GB)

  • memorySearch.provider = "local" 时,node-llama-cpp 会解析 modelPath;如果 GGUF 文件缺失,会自动下载到缓存(或 local.modelCacheDir,如果设置了),然后加载

  • 下载可在重试时恢复

  • 原生构建要求:

    bash 复制代码
    pnpm approve-builds
    # 选择 node-llama-cpp
    pnpm rebuild node-llama-cpp
  • 回退:如果本地嵌入失败且 memorySearch.fallback = "openai",OpenClaw 会自动切换到远程 embeddings(默认 openai/text-embedding-3-small,除非覆盖),并记录原因

自定义 OpenAI 兼容端点示例

json 复制代码
agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      remote: {
        baseUrl: "https://api.example.com/v1/",
        apiKey: "YOUR_REMOTE_API_KEY",
        headers: {
          "X-Organization": "org-id",
          "X-Project": "project-id"
        }
      }
    }
  }
}

注意事项

  • remote.* 优先于 models.providers.openai.*
  • remote.headers 会与 OpenAI 默认头合并,冲突时以 remote 为准
  • 若不设置 remote.headers,则使用 OpenAI 默认 headers

Agent 循环(Agent Loop)

Agent 循环是一个完整的"真实"运行流程:从输入 → 上下文组装 → 模型推理 → 工具执行 → 流式回复 → 持久化。它是将消息转换为操作和最终回复的权威路径,同时保持会话状态的一致性。

在 OpenClaw 中,一个循环是每个会话的单次串行执行,会在模型思考、调用工具和流式输出时发出生命周期和流事件。

入口点:

  • Gateway RPCagentagent.wait
  • CLIagent 命令

工作原理

  1. agent RPC

    • 验证参数
    • 解析会话 (sessionKey / sessionId)
    • 持久化会话元数据
    • 立即返回 { runId, acceptedAt }
  2. agentCommand

    • 运行 agent:

      • 解析模型和默认思考/verbose 配置
      • 加载技能快照
      • 调用 runEmbeddedPiAgent(pi-agent-core 运行时)
      • 如果嵌入循环未发出生命周期结束/错误事件,则手动发出
  3. runEmbeddedPiAgent

    • 通过每会话 + 全局队列进行串行化运行
    • 解析模型和授权配置,构建 pi 会话
    • 订阅 pi 事件并流式传递助手/工具增量
    • 执行超时检测,超时则中止运行
    • 返回 payloads + 使用元数据
  4. subscribeEmbeddedPiSession

    • 将 pi-agent-core 事件桥接到 OpenClaw agent 流:

      • 工具事件 → stream: "tool"
      • 助手增量 → stream: "assistant"
      • 生命周期事件 → stream: "lifecycle"(phase: "start" | "end" | "error"
  5. agent.wait

    • 使用 waitForAgentJob
    • 等待指定 runId 的生命周期结束/错误
    • 返回 { status: ok|error|timeout, startedAt, endedAt, error? }

队列与并发:

  • 每个会话键(session lane)串行化执行,并可选通过全局 lane 控制
  • 防止工具或会话冲突,保持会话历史一致
  • 消息通道可选择队列模式(collect / steer / followup),以供 lane 系统使用

会话与工作区准备:

  • 解析并创建工作区;沙箱运行可能重定向到沙箱工作区根目录
  • 加载技能(或从快照复用),注入到环境和 prompt
  • 解析并注入 bootstrap/context 文件到系统 prompt 报告
  • 获取会话写锁;在流式传递前打开并准备 SessionManager

Prompt 组装 + 系统 Prompt:

  • 系统 Prompt 由 OpenClaw 基础 Prompt、技能 Prompt、bootstrap 上下文和每次运行的覆盖配置组成
  • 强制执行模型特定的限制和压缩保留 token

Hook 点(可拦截的位置)

OpenClaw 提供两类 Hook 系统:

  1. 内部 Hook(Gateway hooks):针对命令和生命周期事件的事件驱动脚本。
  2. 插件 Hook(Plugin hooks):在 agent/工具生命周期和 gateway 流程中的扩展点。

内部 Hook(Gateway hooks):

  • agent:bootstrap:在构建 bootstrap 文件、系统 prompt 最终生成前运行,可用于添加或移除 bootstrap 上下文文件。
  • 命令 Hook :如 /new/reset/stop 等命令事件(详见 Hooks 文档)。
  • 参见 Hooks 获取配置和示例。

插件 Hook(agent + gateway 生命周期):

在 agent 循环或 gateway 流程中运行:

  • before_model_resolve:会话前(无消息)运行,可确定性地覆盖 provider/model。
  • before_prompt_build:会话加载后(含消息)运行,可在提交 prompt 前注入 prependContext/systemPrompt。
  • before_agent_start:旧兼容 Hook,可能在任一阶段运行,推荐使用上面的显式 Hook。
  • agent_end:会话完成后检查最终消息列表和运行元数据。
  • before_compaction / after_compaction:观察或标注压缩周期。
  • before_tool_call / after_tool_call:拦截工具参数和结果。
  • tool_result_persist:在工具结果写入会话记录前同步转换。
  • message_received / message_sending / message_sent:入站和出站消息 Hook。
  • session_start / session_end:会话生命周期边界。
  • gateway_start / gateway_stop:Gateway 生命周期事件。

流式与部分回复

  • 助手增量(assistant deltas)从 pi-agent-core 流式输出,并作为 assistant 事件发出。
  • 块式流(block streaming)可在 text_endmessage_end 发出部分回复。
  • 推理流(reasoning streaming)可以作为独立流或块式回复发出。
  • 详见 Streaming 获取块分割和块式回复行为。

工具执行 + 消息工具

  • 工具开始/更新/结束事件在 tool 流中发出。
  • 工具结果在日志记录/发出前,会对大小和图片负载进行清理。
  • 消息工具发送会被跟踪,以避免重复助手确认。

回复塑形与抑制

最终输出由以下内容组成:

  • 助手文本(可选推理内容)
  • 内联工具摘要(当 verbose + 允许时)
  • 模型报错时的助手错误文本

处理规则:

  • NO_REPLY 被视为静默标记,从输出中过滤。
  • 消息工具重复项会从最终 payload 列表中移除。
  • 如果没有可渲染 payload 且工具报错,则发出回退工具错误回复(除非消息工具已发送用户可见回复)。

压缩 + 重试

  • 自动压缩会发出 compaction 流事件,并可能触发重试。
  • 重试时,会重置内存缓冲和工具摘要以避免重复输出。
  • 详见 Compaction 获取压缩流程。

事件流(当前)

  • lifecycle :由 subscribeEmbeddedPiSession 发出(agentCommand 作为回退)
  • assistant:来自 pi-agent-core 的流式增量
  • tool:来自 pi-agent-core 的流式工具事件

聊天频道处理

  • 助手增量缓冲为聊天增量消息(chat delta messages)。
  • 会话结束/错误时发出聊天最终消息(chat final)。

超时设置

  • agent.wait 默认 30 秒(仅等待),可通过 timeoutMs 参数覆盖。
  • Agent runtimeagents.defaults.timeoutSeconds 默认 600 秒,在 runEmbeddedPiAgent 超时定时器中强制执行。

可能提前终止的情况

  • Agent 超时(Abort)
  • AbortSignal(取消)
  • Gateway 断开或 RPC 超时
  • agent.wait 超时(仅等待,不停止 agent)

流式与块分割(Streaming and Chunking)

OpenClaw 提供两层不同的"流式"机制:

  1. 块式流(Block streaming / channels):随着助手输出生成,按完整块发送消息。这些是正常的频道消息,而不是 token 增量。
  2. 类 token 流(Token-ish streaming / Telegram 专用):在生成过程中,用部分文本更新临时预览消息。

当前没有真正的 token-delta 流到频道消息,Telegram 的预览流是唯一的部分流界面。

块式流(频道消息)

块式流会在文本生成过程中,以粗粒度块发送助手输出。

模型输出流程示意:

复制代码
Model output
  └─ text_delta/events
       ├─ (blockStreamingBreak=text_end)
       │    └─ chunker 随缓冲增长输出块
       └─ (blockStreamingBreak=message_end)
            └─ chunker 在 message_end 时刷新
                   └─ 频道发送(块式回复)

图例:

  • text_delta/events:模型流事件(非流式模型可能稀疏)
  • chunker:EmbeddedBlockChunker,应用最小/最大字符限制 + 分块偏好
  • channel send:实际发出的消息(块式回复)

控制项:

  • agents.defaults.blockStreamingDefault:"on"/"off"(默认 off)
  • 频道覆盖:*.blockStreaming(可对每个频道或账号强制开启/关闭)
  • agents.defaults.blockStreamingBreak:"text_end" 或 "message_end"
  • agents.defaults.blockStreamingChunk:{ minChars, maxChars, breakPreference? }
  • agents.defaults.blockStreamingCoalesce:{ minChars?, maxChars?, idleMs? }(合并流式块后发送)
  • 频道硬上限:*.textChunkLimit(如 channels.whatsapp.textChunkLimit
  • 频道块模式:*.chunkMode(长度优先,换行优先分段,再按长度分块)
  • Discord 软上限:channels.discord.maxLinesPerMessage(默认 17),用于拆分长回复防止 UI 截断

分界语义:

  • text_end:块随 chunker 输出立即流出,每次 text_end 刷新
  • message_end:等待助手消息完成后再刷新缓冲输出
  • 当缓冲文本超出 maxChars 时,message_end 仍会使用 chunker 分块,多块输出

块分割算法(低/高界限)

EmbeddedBlockChunker 实现:

  • 低界限:缓冲区 >= minChars 才输出(除非强制)
  • 高界限:优先在 maxChars 前分块;若强制,maxChars 处分块
  • 分块偏好:段落 → 换行 → 句子 → 空格 → 强制断
  • 代码块:不在代码块内部分割;若 maxChars 强制断,先关闭再重开代码块,保证 Markdown 合法
  • maxChars 会限制在频道 textChunkLimit

合并块(Coalescing)

当块式流启用时,OpenClaw 可以在发送前合并连续块,减少"单行刷屏",仍保持渐进输出。

  • 合并等待空闲间隔(idleMs)
  • 缓冲区超过 maxChars 会强制刷新
  • minChars 防止过小片段发送(最终刷新会发送剩余文本)
  • 合并规则由 blockStreamingChunk.breakPreference 决定(段落 → \n\n,换行 → \n,句子 → 空格)
  • 频道覆盖可通过 *.blockStreamingCoalesce 设置(含 per-account 配置)
  • Signal/Slack/Discord 默认 minChars 提升到 1500(可覆盖)

模拟人类节奏(Human-like pacing)

  • 块式流可在块间增加随机暂停(第一块后)
  • 配置:agents.defaults.humanDelay(可 per-agent 覆盖)
  • 模式:off(默认)、natural(800--2500ms)、custom(minMs/maxMs)
  • 仅适用于块式回复,不影响最终回复或工具摘要

"流块或一次输出"

  • 流块blockStreamingDefault: "on" + blockStreamingBreak: "text_end",按生成流出
  • 一次输出blockStreamingBreak: "message_end",完成后一次刷新(可能多块)
  • 不流式blockStreamingDefault: "off",仅最终回复
  • 注意 :非 Telegram 频道需显式设置 *.blockStreaming: true 才启用;Telegram 可单独开启实时预览(channels.telegram.streamMode

Telegram 预览流(Token-ish)

  • Telegram 是唯一支持实时预览的频道

  • 使用 Bot API sendMessage(首次)+ editMessageText(后续更新)

  • 配置:channels.telegram.streamMode:"partial" | "block" | "off"

    • partial:更新最新流式文本
    • block:按块更新(同块分割规则)
    • off:不预览流
  • 预览块配置(仅 streamMode=block):channels.telegram.draftChunk(默认 minChars: 200, maxChars: 800)

  • 预览流独立于块式流

  • Telegram 块式流显式开启时,跳过预览流避免重复

  • 最终文本仅更新预览消息

  • 非文本或复杂内容则回退到正常最终消息发送

  • /reasoning stream 可写入实时推理(仅 Telegram)

流程示意:

复制代码
Telegram
  └─ sendMessage(临时预览消息)
       ├─ streamMode=partial → 编辑最新文本
       └─ streamMode=block   → chunker + 编辑更新
  └─ 最终文本回复 → 同一消息上最终编辑
  └─ 回退:清理预览 + 正常最终发送(媒体/复杂内容)

图例:

  • preview message:生成中更新的临时 Telegram 消息
  • final edit:在同一预览消息上进行最终文本编辑

多agent(Multi-agent)

目标:在一个运行的 Gateway 中支持多个隔离代理(各自独立的 workspace + agentDir + sessions),同时支持多个频道账户(例如两个 WhatsApp)。入站消息通过绑定路由到特定代理。

什么是"一个代理"?

代理是一个完全独立的智能体,拥有自己的:

  • 工作区(Workspace):文件、AGENTS.md / SOUL.md / USER.md、本地笔记、角色设定规则
  • 状态目录(agentDir):存储认证信息、模型注册和每个代理的配置
  • 会话存储(Session store) :聊天记录和路由状态,位于 ~/.openclaw/agents/<agentId>/sessions
  • 认证信息 :每个代理独立读取 ~/.openclaw/agents/<agentId>/agent/auth-profiles.json,主代理凭证不会自动共享。切勿在多个代理间复用 agentDir(会导致认证/会话冲突)。若要共享凭证,可手动复制 auth-profiles.json 到其他代理的 agentDir
  • 技能(Skills) :每个代理通过 workspace 下的 skills/ 文件夹管理,公共技能位于 ~/.openclaw/skills(参考 "Skills: per-agent vs shared")

Gateway 可以同时托管一个(默认)或多个代理。

工作区注意事项:每个代理的 workspace 是默认当前工作目录(cwd),而非严格沙箱。相对路径解析在 workspace 内,但绝对路径可能访问宿主其他位置,除非启用沙箱。参考 Sandboxing。

路径(快速映射)

类型 默认路径 / 描述
配置 ~/.openclaw/openclaw.json(或 OPENCLAW_CONFIG_PATH
状态目录 ~/.openclaw(或 OPENCLAW_STATE_DIR
工作区 ~/.openclaw/workspace(或 ~/.openclaw/workspace-<agentId>
代理目录 ~/.openclaw/agents/<agentId>/agent(或 agents.list[].agentDir
会话 ~/.openclaw/agents/<agentId>/sessions

单Agent模式(默认)

如果不做任何配置,OpenClaw 运行单代理:

  • agentId 默认 main
  • 会话键值格式:agent:main:<mainKey>
  • 工作区默认 ~/.openclaw/workspace(若设置 OPENCLAW_PROFILE 则为 ~/.openclaw/workspace-<profile>
  • 状态默认 ~/.openclaw/agents/main/agent

代理辅助工具

  • 使用代理向导添加新隔离代理:
bash 复制代码
openclaw agents add work
  • 然后添加绑定(或由向导自动完成)以路由入站消息
  • 验证绑定情况:
bash 复制代码
openclaw agents list --bindings

快速开始

1. 创建每个代理的工作区
  • 使用向导或手动创建工作区:
bash 复制代码
openclaw agents add coding
openclaw agents add social
  • 每个代理拥有自己的 workspace,包括 SOUL.mdAGENTS.md、可选 USER.md,以及专用的 agentDir 和会话存储(~/.openclaw/agents/<agentId>
2. 创建频道账户
  • 每个代理在目标频道创建一个账户:

    • Discord:每个代理一个 bot,启用 Message Content Intent,复制每个 token
    • Telegram:每个代理通过 BotFather 创建 bot,复制 token
    • WhatsApp:每个账号绑定一个手机号
bash 复制代码
openclaw channels login --channel whatsapp --account work
  • 参考频道指南:Discord / Telegram / WhatsApp
3. 添加代理、账户和绑定
  • agents.list 下添加代理
  • channels.<channel>.accounts 下添加频道账户
  • 通过绑定将它们关联(示例略)
4. 重启并验证
bash 复制代码
openclaw gateway restart
openclaw agents list --bindings
openclaw channels status --probe

多代理 = 多用户,多人格

使用多代理时,每个 agentId 都是一个完全隔离的人格:

  • 不同的手机号/账户 (每个频道的 accountId
  • 不同的人格(每个代理 workspace 下的 AGENTS.mdSOUL.md 等文件)
  • 独立认证和会话(除非显式启用,否则不会交叉通信)

这允许多个人共享同一个 Gateway 服务器,同时保持各自的 AI "大脑"和数据隔离。

一个 WhatsApp 号码,多个人(DM 分流)

你可以在同一个 WhatsApp 账号下,将不同的私聊(DM)路由到不同代理。

  • 匹配条件:发送者 E.164 格式(如 +15551234567) + peer.kind: "direct"
  • 回复仍来自同一个 WhatsApp 号码(不支持每个代理单独发信身份)

重要细节:私聊会折叠到代理的主会话键(main session key),要实现真正隔离,每个人最好对应一个代理。

示例配置:

json5 复制代码
{
  agents: {
    list: [
      { id: "alex", workspace: "~/.openclaw/workspace-alex" },
      { id: "mia", workspace: "~/.openclaw/workspace-mia" }
    ]
  },
  bindings: [
    {
      agentId: "alex",
      match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551230001" } }
    },
    {
      agentId: "mia",
      match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551230002" } }
    }
  ],
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551230001", "+15551230002"]
    }
  }
}

注意事项

  • DM 访问控制是全局的(针对 WhatsApp 账号),而非每个代理
  • 对于共享群组,绑定群组到某个代理,或者使用 Broadcast 群组

路由规则(消息如何选择代理)

  • 绑定是确定性的,最具体规则优先:

    1. peer 精确匹配(DM / 群 / 频道 id)
    2. parentPeer 匹配(线程继承)
    3. guildId + roles(Discord 角色路由)
    4. guildId(Discord)
    5. teamId(Slack)
    6. accountId(频道账户匹配)
    7. 频道级别匹配(accountId: "*"
    8. 回退到默认代理(agents.list[].default,否则取列表第一个,默认 main
  • 同一层级匹配多个绑定时,配置中靠前的优先

  • 如果绑定指定多个匹配字段(如 peer + guildId),需同时满足(AND 语义)

多账户 / 多手机号

  • 支持多账户的频道(如 WhatsApp)使用 accountId 区分每个登录实例
  • 每个 accountId 可路由到不同代理,实现一个服务器托管多个手机号而不会混合会话

概念:

  • agentId:一个"脑"(workspace、每代理认证、每代理会话存储)
  • accountId:一个频道账户实例(如 WhatsApp 个人 vs 企业账号)
  • binding:通过 (channel, accountId, peer) 以及可选 guild/team id 路由入站消息到代理
  • 私聊会话折叠agent:<agentId>:<mainKey>(每代理"main"会话,session.mainKey)

平台示例

Discord:每代理一个 bot

  • 每个 Discord bot 对应唯一 accountId
  • 绑定账户到代理,保持每个 bot 的 allowlist
json5 复制代码
{
  agents: {
    list: [
      { id: "main", workspace: "~/.openclaw/workspace-main" },
      { id: "coding", workspace: "~/.openclaw/workspace-coding" }
    ]
  },
  bindings: [
    { agentId: "main", match: { channel: "discord", accountId: "default" } },
    { agentId: "coding", match: { channel: "discord", accountId: "coding" } }
  ],
  channels: {
    discord: {
      groupPolicy: "allowlist",
      accounts: {
        default: { token: "DISCORD_BOT_TOKEN_MAIN", guilds: { ... } },
        coding: { token: "DISCORD_BOT_TOKEN_CODING", guilds: { ... } }
      }
    }
  }
}

注意事项

  • 邀请每个 bot 到对应 guild,并启用 Message Content Intent
  • token 存放在 channels.discord.accounts.<id>.token(默认账户可使用 DISCORD_BOT_TOKEN

Telegram:每代理一个 bot

json5 复制代码
{
  agents: {
    list: [
      { id: "main", workspace: "~/.openclaw/workspace-main" },
      { id: "alerts", workspace: "~/.openclaw/workspace-alerts" }
    ]
  },
  bindings: [
    { agentId: "main", match: { channel: "telegram", accountId: "default" } },
    { agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } }
  ],
  channels: {
    telegram: {
      accounts: {
        default: { botToken: "123456:ABC...", dmPolicy: "pairing" },
        alerts: { botToken: "987654:XYZ...", dmPolicy: "allowlist", allowFrom: ["tg:123456789"] }
      }
    }
  }
}
  • 每个代理在 BotFather 创建一个 bot,并复制 token
  • token 存放在 channels.telegram.accounts.<id>.botToken(默认账户可用 TELEGRAM_BOT_TOKEN

WhatsApp:每代理一个号码

  • 启动 Gateway 前绑定账户:
bash 复制代码
openclaw channels login --channel whatsapp --account personal
openclaw channels login --channel whatsapp --account biz
  • 示例 ~/.openclaw/openclaw.json 配置(JSON5):
json5 复制代码
{
  agents: {
    list: [
      {
        id: "home",
        default: true,
        name: "Home",
        workspace: "~/.openclaw/workspace-home",
        agentDir: "~/.openclaw/agents/home/agent"
      },
      {
        id: "work",
        name: "Work",
        workspace: "~/.openclaw/workspace-work",
        agentDir: "~/.openclaw/agents/work/agent"
      }
    ]
  },

  bindings: [
    { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
    { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
    { agentId: "work", match: { channel: "whatsapp", accountId: "personal", peer: { kind: "group", id: "1203630...@g.us" } } }
  ],

  tools: {
    agentToAgent: { enabled: false, allow: ["home", "work"] }
  },

  channels: {
    whatsapp: {
      accounts: {
        personal: { /* 可选覆盖,默认路径: ~/.openclaw/credentials/whatsapp/personal */ },
        biz: { /* 可选覆盖,默认路径: ~/.openclaw/credentials/whatsapp/biz */ }
      }
    }
  }
}
  • 默认情况下,代理间消息必须显式开启并在 allowlist 中才可发送

示例:WhatsApp 日常聊天 + Telegram 深度工作

按频道拆分:将 WhatsApp 路由到快速的日常代理,将 Telegram 路由到 Opus 代理。

json5 复制代码
{
  agents: {
    list: [
      {
        id: "chat",
        name: "Everyday",
        workspace: "~/.openclaw/workspace-chat",
        model: "anthropic/claude-sonnet-4-5"
      },
      {
        id: "opus",
        name: "Deep Work",
        workspace: "~/.openclaw/workspace-opus",
        model: "anthropic/claude-opus-4-6"
      }
    ]
  },
  bindings: [
    { agentId: "chat", match: { channel: "whatsapp" } },
    { agentId: "opus", match: { channel: "telegram" } }
  ]
}

说明

  • 如果某个频道有多个账户,需要在绑定中添加 accountId(例如 { channel: "whatsapp", accountId: "personal" }
  • 若要将某个单独的 DM/群组路由到 Opus,而其他消息仍走 chat,可添加 match.peer 绑定;peer 绑定总是优先于全频道规则

示例:同一频道,一个 peer 路由到 Opus

保持 WhatsApp 使用快速代理,但将某个 DM 路由到 Opus:

json5 复制代码
{
  agents: {
    list: [
      { id: "chat", name: "Everyday", workspace: "~/.openclaw/workspace-chat", model: "anthropic/claude-sonnet-4-5" },
      { id: "opus", name: "Deep Work", workspace: "~/.openclaw/workspace-opus", model: "anthropic/claude-opus-4-6" }
    ]
  },
  bindings: [
    { agentId: "opus", match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } } },
    { agentId: "chat", match: { channel: "whatsapp" } }
  ]
}

注意:peer 绑定总是优先,因此应放在全频道规则之前。

绑定到 WhatsApp 群组的家庭代理

为单个 WhatsApp 群组绑定一个专用家庭代理,设置 mention 控制和更严格的工具策略:

json5 复制代码
{
  agents: {
    list: [
      {
        id: "family",
        name: "Family",
        workspace: "~/.openclaw/workspace-family",
        identity: { name: "Family Bot" },
        groupChat: { mentionPatterns: ["@family", "@familybot", "@Family Bot"] },
        sandbox: { mode: "all", scope: "agent" },
        tools: {
          allow: ["exec", "read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
          deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"]
        }
      }
    ]
  },
  bindings: [
    { agentId: "family", match: { channel: "whatsapp", peer: { kind: "group", id: "120363999999999999@g.us" } } }
  ]
}

说明

  • 工具允许/禁止列表针对 工具 而非技能;如果技能需要执行二进制程序,确保 exec 被允许,并且二进制文件存在于沙箱中
  • 若要严格控制群组访问,可设置 agents.list[].groupChat.mentionPatterns 并保持频道的群组 allowlist

每代理沙箱和工具配置

从 v2026.1.6 开始,每个代理可以拥有独立的沙箱和工具限制:

json5 复制代码
{
  agents: {
    list: [
      {
        id: "personal",
        workspace: "~/.openclaw/workspace-personal",
        sandbox: { mode: "off" }  // 个人代理无沙箱
        // 工具无限制,所有工具可用
      },
      {
        id: "family",
        workspace: "~/.openclaw/workspace-family",
        sandbox: {
          mode: "all",     // 始终沙箱化
          scope: "agent",  // 每代理一个容器
          docker: { setupCommand: "apt-get update && apt-get install -y git curl" } // 可选一次性容器初始化
        },
        tools: {
          allow: ["read"],                    // 只允许 read 工具
          deny: ["exec", "write", "edit", "apply_patch"] // 禁止其他工具
        }
      }
    ]
  }
}

说明

  • setupCommand 位于 sandbox.docker 下,仅在容器创建时执行一次
  • 当沙箱范围为 "shared" 时,sandbox.docker.* 的每代理覆盖将被忽略

优点

  1. 安全隔离:限制不受信任代理可用的工具
  2. 资源控制:沙箱特定代理,同时其他代理仍可在主机上运行
  3. 灵活策略:每个代理可拥有不同权限

注意

  • tools.elevated 是全局的、基于发送者的权限,不可按代理单独配置
  • 若需每代理边界,请使用 agents.list[].tools 禁止 exec
  • 针对群组,请使用 agents.list[].groupChat.mentionPatterns,确保 @ 提及映射到目标代理

结尾

回顾这次对 OpenClaw 的学习体验,不得不承认,它的文档较为零散,自学曲线陡峭,外部教程也多停留在"能做什么"的层面,很少有人深入讲解背后的实现逻辑。我目前的重点仍然是理解 agent 构建的思路,而实际部署方面,我打算先在 阿里云 用十块钱体验一个月,实战感受才是最直接的学习方式。

阅读源码的过程尤其启发我。例如 Dockerfile.sandbox 的写法让我意识到,Shell 命令可以直接在 Docker 沙箱中执行,这不仅保障了环境隔离,也拓展了 agent 的可操作能力。这种发现的乐趣,恰恰是自学过程中最值得珍视的部分。学习,从来不是一蹴而就的,它是由无数个小积累堆砌而成的。每天多懂一点,每次能解决一点新的疑问,你就已经在稳步前进。或许最终,你会发现,理解 OpenClaw 的过程,不仅是在掌握工具,更是在培养一种解决问题和拆解复杂系统的思维方式。

Little Wins

shell工具

OpenClaw 的 "shell 命令" 是通过 exec 工具跑的,真正执行的位置有三种:

  1. host: "sandbox"(默认):在 Docker 容器里执行,也就是"沙箱里"
  2. host: "gateway":直接在运行 Gateway 的那台机器上执行(本机 shell)
  3. host: "node":转发到配对的节点(macOS / Android 等)上执行

具体用哪个,由 tools.exec.host / exec.host 参数 + 沙箱配置共同决定。

Shell 到底是在哪里跑的?

核心实现在 src/agents/bash-tools.exec.tsbash-tools.exec-runtime.ts

  • createExecTool(...) 决定 host

    ts 复制代码
    // 345-347 行(bash-tools.exec.ts 中)
    const configuredHost = defaults?.host ?? "sandbox";
    const requestedHost = normalizeExecHost(params.host) ?? null;
    let host: ExecHost = requestedHost ?? configuredHost;

    可以看到:

    • 如果没有特别配置,默认 host = "sandbox"
    • 你在调用工具时传 host: "gateway" / "node",才会切到其他宿主
  • 最后真正起子进程的地方在 runExecProcess

    ts 复制代码
    // 388-401 行(bash-tools.exec-runtime.ts)
    if (opts.sandbox) {
      // 在 Docker 里跑
      argv: [
        "docker",
        ...buildDockerExecArgs({ containerName, command, workdir, env, tty }),
      ],
      env: process.env,
    } else {
      // 在本机 shell 里跑
      const { shell, args: shellArgs } = getShellConfig();
      const childArgv = [shell, ...shellArgs, execCommand];
      ...
    }

    也就是说:

    • opts.sandbox → 用 docker ... 去 exec:命令在 Docker 容器内执行
    • 没有 opts.sandbox → 用本机 shell (/bin/bash 等) 跑
  • hostsandbox 的关系:

    ts 复制代码
    // 372 行左右(bash-tools.exec.ts)
    const sandbox = host === "sandbox" ? defaults?.sandbox : undefined;
    • host === "sandbox" 时才会带上 sandbox 配置,进而触发 runExecProcess(..., sandbox: ...)
    • host === "gateway""node" 时,不会给 sandbox,就不会进 Docker 分支

那沙箱是什么?什么时候会用到?

沙箱相关入口在 src/agents/sandbox.ts,它只是把一堆 ./sandbox/* 模块导出来。

比较关键的是 src/agents/sandbox/context.ts

  • resolveSandboxContext(...) 会根据当前会话 + 配置判断要不要为这个 session 准备沙箱:

    ts 复制代码
    const runtime = resolveSandboxRuntimeStatus({ cfg, sessionKey });
    if (!runtime.sandboxed) return null;
    
    const cfg = resolveSandboxConfigForAgent(config, runtime.agentId);
    ...
    const containerName = await ensureSandboxContainer({ ... });
  • resolveSandboxConfigForAgent(在 sandbox/config.ts)从 openclaw.json 里的:

    ts 复制代码
    cfg.agents.defaults.sandbox
    cfg.agents.list[...].sandbox   // 针对某个 agent 覆盖

    推导出:

    • mode: "off" | "non-main" | "all"
    • docker 如何配
    • workspaceAccessnone / ro / rw
    • 等等

因此:

  • 如果配置里 agents.defaults.sandbox.mode"off",那即使你 host: "sandbox",也不会真的有可用沙箱上下文
  • 常见安全配置是:
    • mode: "non-main"主会话在宿主机上跑,其他会话在 Docker 沙箱里跑
    • mode: "all":所有会话都在沙箱里跑

resolveSandboxRuntimeStatus 会根据当前 sessionKey 是不是 main、当前 agent 是谁等,决定 runtime.sandboxed 是 true 还是 false。

沙箱是怎么实现的?(Docker 细节)

真正的 Docker 操作在 src/agents/sandbox/docker.ts

  • 最底层是 execDockerRaw

    ts 复制代码
    // 28-35 行
    const child = spawn("docker", args, {
      stdio: ["pipe", "pipe", "pipe"],
    });

    node:child_process.spawn("docker", [...]) 调 Docker CLI。

  • 创建容器的参数在 buildSandboxCreateArgs 里(237 行起):

    ts 复制代码
    args = ["create", "--name", name, ...]
    args.push("--label", "openclaw.sandbox=1");
    args.push("--label", `openclaw.sessionKey=${scopeKey}`);
    ...
    // 关键安全选项在 resolveSandboxDockerConfig 里
  • resolveSandboxDockerConfigsandbox/config.ts)定义了 默认安全限制

    ts 复制代码
    // 72-81 行
    return {
      image: ... DEFAULT_SANDBOX_IMAGE,
      containerPrefix: DEFAULT_SANDBOX_CONTAINER_PREFIX,
      workdir: DEFAULT_SANDBOX_WORKDIR,
      readOnlyRoot: true,
      tmpfs: ["/tmp", "/var/tmp", "/run"],
      network: "none",              // 默认无网络
      user: ...,
      capDrop: ["ALL"],             // 丢弃所有 Linux capabilities
      env: { ... },
      ...
      binds: ...                     // 只挂你允许的目录
    }

这几个点很关键:

  • network: "none":容器默认没有网络
  • readOnlyRoot: true + tmpfs:根文件系统只读,可写区在内存盘
  • capDrop: ["ALL"]:全部 Capabilities 丢弃,限制对内核敏感操作
  • bindsvalidateSandboxSecurity 校验(sandbox/docker.ts 245 行):
    • 会拒绝危险的挂载、网络模式、seccomp/apparmor 配置

文件系统隔离是怎么做的?

还是在 sandbox/context.tsensureSandboxWorkspaceLayout

  • 决定几个目录:

    ts 复制代码
    agentWorkspaceDir      // 你的真实工作区(~/.openclaw/workspace/...)
    workspaceRoot          // sandbox 根目录
    sandboxWorkspaceDir    // 具体这个 session/agent 的 sandbox 目录
    workspaceDir           // 对 agent 暴露的"当前工作目录"

关键逻辑:

  • workspaceAccess !== "rw"
    • 把 agent 的 workspace 内容 同步 一份到 sandboxWorkspaceDir(只读 / 只在沙箱里改)
  • 实际执行时:
    • 宿主机 workdir 指向 sandbox workspace / agent workspace(二选一)
    • 容器内 workdir 始终指向 cfg.docker.workdir
  • sandboxContext.fsBridge(154 行)则用一个 "FS bridge" 把文件读写映射到正确的沙箱目录

总结一下:沙箱通过"单独的目录 + Docker bind mount"来隔离文件系统,只把你允许的部分映射进去。

host=node 的情况(顺带说一下)

bash-tools.exec.ts 425 行之后:

  • host === "node" 时:
    • 列出已配对节点:listNodes({})
    • 选择一个支持 system.run 的节点
    • 把你的命令转成对应平台的 argv(buildNodeShellCommand
    • 然后通过 Gateway 的 node.invoke 调用节点上的 system.run

也就是说:
host=node 时,命令既不在 Gateway 进程里执行,也不在 Docker 容器里执行,而是在那个"节点进程(比如 macOS app / Android node host)"上执行。

skill 调用

Skill 在 OpenClaw 里不是一个"内建 tool 名字叫 skill"的东西,而是一整套"外部能力插件系统"。核心逻辑是:

  1. 每个 skill 是一个独立目录(通常带 SKILL.md、脚本/二进制等)
  2. Gateway 在启动/刷新时扫描这些目录 ,解析 SKILL.md,形成一个 "技能快照 + 文本提示 + 命令列表"
  3. 这些信息被塞进 Agent 的 system prompt 里的 <available_skills> ... </available_skills> 区域,让大模型自己"决定用哪个技能"
  4. 真正"执行 skill"时,通常是通过已经有的 exec(shell 执行工具)+ node.invoke 等工具 去调用 skill 对应的二进制 / 脚本,而不是有一个通用的 skill.run tool 像 OpenCode 那样

也就是说:OpenClaw 里 Skill 本质上是"通过文件系统 + shell/Docker/node 执行包装起来的外部程序",Agent 通过普通工具(execnode.invoke 等)来调用它们,Skill 系统负责扫描、过滤、拼好 prompt 和 UI 配置。

Skill 是怎么被"发现"和注入给 Agent 的?

src/agents/skills.tssrc/agents/skills/workspace.ts

  • buildWorkspaceSkillSnapshot / loadWorkspaceSkillEntries
ts 复制代码
// workspace.ts (221+)
function loadSkillEntries(workspaceDir: string, opts?: { ... }): SkillEntry[] {
  // 1. 决定 skills 根目录
  // 2. 找到含有 SKILL.md 的子目录
  // 3. 读取/解析 SKILL.md,生成 SkillEntry
}

这段逻辑会:

  1. 从 workspace 根目录、bundled skills 目录等位置找子目录
  2. 只认带 SKILL.md 的目录是一个 skill
  3. 做各种限制(数量、单个 SKILL.md 大小等,防止 prompt 爆炸)
  • 然后 buildWorkspaceSkillsPrompt 把这些 SkillEntry 拼接成一段系统提示(system prompt):
ts 复制代码
// workspace.ts 中(未贴全):
buildWorkspaceSkillsPrompt(...)  // 返回一大段 <available_skills> ... 的文本
  • 最终这段 prompt 会被塞进 system-prompt.ts
ts 复制代码
// src/agents/system-prompt.ts 里
const skillsSection = buildSkillsSection({ skillsPrompt, ... })

也就是你在源码里看到的:

text 复制代码
<available_skills>
  <skill>
    <name>github</name>
    ...
  </skill>
  ...
</available_skills>

这一块和 "OpenCode 里有一个 skill 描述块" 类似,但在 OpenClaw 里,skill 本身不是一个独立的 "tool 名字叫 skill"------它只是通过 prompt 注入,让大模型知道:有哪些技能、叫什么、怎么用。

Skill 什么时候真的"跑起来"?(执行层)

Skill 的真正执行,通常有几种模式(由每个 skill 的 SKILL.md 决定):

  1. 本机脚本/二进制

    • 通过 exec 工具(前面我们分析过的 bash-tools.exec.ts / exec-runtime.ts
    • 可以跑在 host="sandbox" 的 Docker 里,也可以 host="gateway" 在本机,或者 host="node" 在节点
  2. 远程 HTTP / API

    • 有些 skill 会描述"调用某个 HTTP endpoint",Agent 再用已有的 HTTP 工具(OpenClaw 侧是用 Node 内部的 HTTP client,而不是 model tool)去请求
    • 这类通常不需要额外的"skill 工具",只是 skill 的逻辑里写清楚"调用哪儿"
  3. 通过节点(node.invoke)调用设备能力

    • 比如 camera、screen、canvas 等
    • skill 只是把这些命令组合起来用

Skill 系统本身不提供一个统一的 runtime 像 skill.run,而是:

  • skills/*/SKILL.md 描述:这个 skill 是什么、期望输入/输出是什么、一般调用步骤是什么
  • 然后依靠已有的执行通道:exec / node.invoke / 内置 HTTP 客户端等

你可以在 skills/skill-creator/ 里看到官方推荐你写 skill 的方式------那一套脚手架会帮你:

  • 生成 SKILL.md
  • 生成对应的脚本/入口
  • (必要时)帮你准备 Docker / 依赖安装说明

那有没有像 "opencode 的 skill tool" 那种统一调用入口?

严格说,没有一个统一的 skill.run 工具暴露给大模型,而是:

  • 发现/过滤/描述 skill → 由 src/agents/skills/* 完成(生成 prompt + 元数据)
  • 真正执行时,各个 skill 自己约定使用哪些底层工具
    • 绝大多数 "系统外部动作" 还是通过我们刚分析过的 exec(shell + sandbox + node)完成
    • 或者通过 Gateway 提供的其他工具(browser、nodes、sessions_* 等)

换句话说:

在 OpenClaw 里,"Skill 系统 = 文件系统 + 元数据 + prompt 注入 + 可选的安装/健康检查",

执行 依赖的是已经存在的通用工具(尤其是 exec),而不是再造一个通用的 "skill tool"。

对比一下"opencode skill tool"模型

你可能熟悉的是这种设计:

  • 有一个类似 skill / run_skill 的统一工具:

    json 复制代码
    { "tool": "run_skill", "arguments": { "name": "...", "input": "..." } }
  • runtime 里面按名字查 skill,然后帮你执行

OpenClaw 的思路是:

  1. SKILL.md 清晰地把 skill 的用法喂给大模型(在 system prompt 里)
  2. 模型直接"像人一样"调用它描述的命令(通常是 shell / HTTP / node.invoke),而不是通过一个 skill.run 抽象层
  3. Skill 系统重点做的是:
    • 哪些 skill 能用(OS、安装情况、allowlist)
    • 把 skill 说明塞到 prompt 里
    • 在聊天界面里注册 /skill/demo_skill 之类的命令

比如 skills/peekaboo/SKILL.md,它本身就比较复杂,而且是典型的"外部 CLI + 权限 + OS 限制"型 skill,很适合画整条链路。

yaml 复制代码
---
name: peekaboo
description: Capture and automate macOS UI with the Peekaboo CLI.
homepage: https://peekaboo.boo
metadata:
  {
    "openclaw":
      {
        "emoji": "👀",
        "os": ["darwin"],
        "requires": { "bins": ["peekaboo"] },
        "install":
          [
            {
              "id": "brew",
              "kind": "brew",
              "formula": "steipete/tap/peekaboo",
              "bins": ["peekaboo"],
              "label": "Install Peekaboo (brew)",
            },
          ],
      },
  }
---
  1. Gateway 启动 / 刷新时:发现并加载 peekaboo skill

    相关代码: src/agents/skills.ts + src/agents/skills/workspace.ts + src/agents/skills/types.ts

    1. Gateway 启动(startGatewayServer)时,会为每个 agent 准备 skill 快照:

      • 入口在 buildWorkspaceSkillSnapshot / loadWorkspaceSkillEntriesworkspace.ts
    2. loadSkillEntries(...) 做的事情大致是:

      • 在多个 root 下扫描:

        • 用户 workspace 里的 skills/*/SKILL.md
        • bundled skills 目录(仓库里自带的 skills/peekaboo/ 这类)
      • 对每个 SKILL.md

        • 解析 frontmatter(YAML 头)

        • 填一个 SkillEntry

          ts 复制代码
          type SkillEntry = {
            skill: Skill;                 // pi-coding-agent 那边的抽象
            frontmatter: ParsedSkillFrontmatter;
            metadata?: OpenClawSkillMetadata;  // 就是 SKILL.md 里的 openclaw 段
          }
      • 使用 SkillEligibilityContext 过滤:

        ts 复制代码
        type SkillEligibilityContext = {
          remote?: {
            platforms: string[];   // 当前 OS,比如 "darwin"
            hasBin: (bin: string) => boolean;    // 系统里有没有某个命令
            hasAnyBin: (bins: string[]) => boolean;
          };
        };

        结合 OpenClawSkillMetadata.requires

        ts 复制代码
        requires?: {
          bins?: string[];     // 必须存在的二进制(这里是 "peekaboo")
          anyBins?: string[];
          env?: string[];
          config?: string[];
        };

        → 如果当前不是 macOS,或者系统里找不到 peekaboo 这个 bin,就不会把这个 skill 纳入"可用技能列表"。

    3. 通过这一轮过滤后,peekaboo 才会出现在 Agent 的 skill 列表里。

  2. 给大模型看的 system prompt:注入 peekaboo 说明

    相关代码: src/agents/system-prompt.ts + src/agents/skills/workspace.ts

    1. 上一步的 SkillEntry 列表,会被 buildWorkspaceSkillsPrompt(...) 组装成一段文本:

      • 里边包含:
        • name: peekaboo
        • description: ...
        • 一些使用示例(你在 SKILL.md 下半部分看到的那些命令示例)
      • 这段文本会被包在一个 <available_skills> ... </available_skills> 区块里。
    2. 在组 agent 的 system prompt 时(system-prompt.ts):

      • buildSkillsSection({ skillsPrompt }) 把这整块拼进 system prompt
      • 所以 大模型开局就能看到一个"技能总览",里面有 peekaboo 的文档

    这一层很像你在别的框架里看到的 "tools 描述块",但在 OpenClaw 里是走 system prompt 文本,而不是单独的 JSON schema。

  3. 用户发话 → 触发 agent

    举个例子,你在 WhatsApp / WebChat 里说:

    "用 peekaboo 帮我在 Safari 登录某个网站"

    通路(我们只看核心部分):

    1. 消息从通道(WhatsApp/Telegram/...)进来 → Gateway → auto-reply → 路由到了一个 agent session
    2. 这个 session 已经带着上面那份 system prompt(包含 peekaboo 技能说明)
  4. 大模型"决定用 peekaboo"并构造 tool 调用

    这里没有一个统一叫 skill 的 tool。

    大模型是这么用 peekaboo 的:

    1. 它在 prompt 里看到:

      • 有一个 skill 叫 peekaboo
      • 描述:"macOS UI automation CLI,命令行示例......"
    2. 它根据任务(比如网页登录)判断:

      • 需要一个能操纵 macOS UI 的 CLI
      • SKILL.md 已经给了它很多 bash 示例(peekaboo click ... / peekaboo type ...
    3. 然后它会构造一个 exec 工具调用(也就是我们之前看过的 bash 工具):

      • 伪代码形式大概是:
      json 复制代码
      {
        "tool": "exec",
        "arguments": {
          "command": "peekaboo app launch \"Safari\" --open https://example.com && peekaboo see --app Safari ...",
          "workdir": "...",
          "host": "sandbox" | "gateway" | "node",
          "timeout": 600
        }
      }

      这一步是 Pi agent 栈和大模型内部约定的,不在这个仓库里直接写成 JSON,但执行效果就是这么一条 exec 调用。

    关键点:Skill 自己不会"直接变成一个 tool 名字",而是给模型提供足够清晰的 CLI 文档,让模型用现有的 exec / node.invoke 之类的工具去调用它。

  5. Gateway 侧接到 exec 调用:选宿主 + 决定是否进沙箱

    相关代码: src/agents/bash-tools.exec.ts + bash-tools.exec-runtime.ts + agents/sandbox/*

    1. Pi agent 调用到 exec 工具实现,也就是 createExecTool(...).execute(...) 这段:

      ts 复制代码
      // bash-tools.exec.ts 里
      const configuredHost = defaults?.host ?? "sandbox";
      const requestedHost = normalizeExecHost(params.host) ?? null;
      let host: ExecHost = requestedHost ?? configuredHost;  // "sandbox" | "gateway" | "node"
      • 默认是 host="sandbox"(Docker 容器)
      • 也可以配置成 gatewaynode(比如你想在 macOS 节点上直接跑)
    2. 如果 host === "sandbox",会拿 agent 的 sandbox 配置:

      ts 复制代码
      const sandbox = host === "sandbox" ? defaults?.sandbox : undefined;
      ...
      if (sandbox) {
        const resolved = await resolveSandboxWorkdir(...);
        workdir = resolved.hostWorkdir;
        containerWorkdir = resolved.containerWorkdir;
      }

      这些配置来自前面说的 agents.defaults.sandbox.* 和 agent 专用的 sandbox 字段。

  6. 真正跑 shell:runExecProcess + ProcessSupervisor + Docker

相关代码: src/agents/bash-tools.exec-runtime.ts + src/process/supervisor/* + src/agents/sandbox/docker.ts

  1. exec 工具最终会调用 runExecProcess(...)

    ts 复制代码
    // bash-tools.exec-runtime.ts
    const spawnSpec = (() => {
      if (opts.sandbox) {
        return {
          mode: "child",
          argv: [
            "docker",
            ...buildDockerExecArgs({
              containerName: opts.sandbox.containerName,
              command: execCommand,              // 就是那个 "peekaboo ..." 命令
              workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
              env: opts.env,
              tty: opts.usePty,
            }),
          ],
          env: process.env,
        };
      }
      const { shell, args: shellArgs } = getShellConfig();
      const childArgv = [shell, ...shellArgs, execCommand];
      ...
    })();
    • 有 sandbox:
      • 真实执行的是:docker exec ... <execCommand>
      • <execCommand> 里包含 peekaboo ...
    • 没有 sandbox:
      • 真实执行的是:/bin/bash -lc "<execCommand>" 这类本机 shell 命令
  2. 子进程实际由 ProcessSupervisor 管:

    ts 复制代码
    // process/supervisor/supervisor.ts
    const adapter =
      input.mode === "pty"
        ? createPtyAdapter({ shell, args: [...shellArgs, ptyCommand], ... })
        : createChildAdapter({ argv: input.argv, cwd: input.cwd, env: input.env, ... });
    ...
    adapter.onStdout(chunk => input.onStdout?.(chunk));
    ...
    waitPromise = adapter.wait()  // 等进程结束
    • 这一层负责:
      • 起进程
      • 收集 stdout/stderr
      • 超时 / 无输出超时 / 手动取消
      • 把状态回传给上层(exec 工具 → Agent → 大模型)
  3. 如果是 host="node" 的情况(比如你让 exec 绑定到 macOS 节点):

    • bash-tools.exec.ts 里会走 host === "node" 分支:
      • 找到合适的 node(必须支持 system.run
      • 把命令封装成 node.invoke 参数,实际上调用的是节点进程那边的 system.run
    • 这时 peekaboo 命令就不是在 Gateway 机器里执行,而是在那个 macOS 节点上执行
  4. 执行结果如何回到大模型 / 用户这边?

    1. ProcessSupervisor 结束时返回 exit 信息:
      • exitCode、exitSignal、durationMs、stdout/stderr 等
    2. runExecProcess 把这些填进 ExecProcessOutcome 并更新 session 状态:
      • status: "completed" | "failed"
      • aggregated: <合并后的输出>
    3. exec 工具的 onUpdate 回调会把 tail 输出 + 状态通过 Pi agent 的 tool 回调返回给大模型
    4. 大模型看到 peekaboo 命令的输出(例如 --json 下来的 JSON),再继续下一步决策:
      • 比如再发一条 exec 命令点击另外一个按钮
      • 或者总结结果,用自然语言回答你
  5. 整条链路一口气 recap(以 peekaboo 为例)

    • 启动时

      扫描 skills/peekaboo/SKILL.md → 解析 metadata(os、requires、install) → 结合当前 OS 和是否存在 peekaboo 二进制 → 若满足条件,则把它作为一个可用 skill 加入 skill 快照。

    • 构建 prompt

      peekaboo 的说明 + 示例塞进 <available_skills> 区块 → 注入 agent 的 system prompt。

    • 你发一句话

      消息通过通道 → Gateway → agent session → 大模型看到 prompt,知道有 peekaboo 这门"本地 UI 自动化 CLI"。

    • 大模型决策

      判断需要操作 macOS UI → 参考 SKILL.md 里的 CLI 示例 → 生成一个 exec 工具调用,命令形如 peekaboo ...

    • Gateway 执行
      exec 工具根据配置选 host = sandbox/gateway/node

      • sandbox:用 Docker exec,在沙箱容器里跑 peekaboo ...
      • gateway:直接在 Gateway 主机本机 shell 里跑
      • node:转成 node.invoke,在某个 macOS 节点上跑
    • 进程管理与结果
      ProcessSupervisor 管这个子进程的生命周期 → 输出通过 exec 工具回给大模型 → 大模型继续下一步或直接回复你。

相关推荐
秦奈2 小时前
Unity学习复习随笔(12):网络开发基础
网络·笔记·学习·unity
cqbzcsq2 小时前
MC Forge 1.20.1 mod开发学习笔记(战利品、标签、配方)
java·笔记·学习·mod·mc
山岚的运维笔记2 小时前
SQL Server笔记 -- 第70章:临时表的使用
数据库·笔记·sql·microsoft·oracle·sqlserver
量子-Alex2 小时前
【强化学习】强化学习的数学原理课程笔记第三章 最优贝尔曼公式
笔记
马猴烧酒.2 小时前
【JAVA算法|hot100】栈类型题目详解笔记
java·笔记
Rsingstarzengjx2 小时前
【Photoshop从入门到精通】-21 图层进阶 笔记
笔记·ui·photoshop
四谎真好看2 小时前
SSM学习笔记(SpringMVC篇 Day02)
笔记·学习·学习笔记·ssm
蒸蒸yyyyzwd2 小时前
后端学习笔记
笔记
智者知已应修善业2 小时前
【蓝桥杯单词分析最多字母次数并列字典最小输出】2025-4-15
c语言·c++·经验分享·笔记·算法·蓝桥杯