主题
OpenClaw 源码解读(十六)Agent 工具系统
本篇深入分析 OpenClaw 的 Agent 工具系统——Agent 对外部世界交互的能力具象化。从通用工具基础设施到 30+ 个具体工具实现,涵盖 Web 搜索、浏览器控制、消息发送、图像/PDF 理解、定时任务、Canvas UI、节点设备控制、子 Agent 编排等全部能力。工具系统是 AI Agent 从"能对话"进化到"能行动"的关键桥梁。
目录
- 一、工具系统全景
- 二、工具基础设施 (common.ts)
- 三、Gateway 工具调用协议
- 四、Web 搜索工具
- 五、Web 抓取工具
- 六、消息工具
- 七、图像理解工具
- 八、PDF 分析工具
- 九、浏览器工具
- 十、Canvas 工具
- 十一、Cron 定时任务工具
- 十二、节点设备工具
- 十三、子 Agent 编排工具
- 十四、会话管理工具
- 十五、TTS 语音合成工具
- 十六、记忆工具
- 十七、设计模式与架构总结
一、工具系统全景
1.1 工具是什么?
在 AI Agent 架构中,工具(Tool)是 Agent 与外部世界交互的接口。Agent(LLM)自身只能处理文本,但通过调用工具,它可以:
- 搜索互联网
- 发送消息
- 拍照 / 录屏
- 控制浏览器
- 设置定时任务
- 管理子 Agent
1.2 工具目录结构
src/agents/tools/
├── common.ts ← 通用工具基础设施(类型、参数读取、结果构建)
├── gateway.ts ← Gateway RPC 调用封装
│
├── web-search.ts ← Web 搜索(Brave/Perplexity/Grok/Gemini/Kimi)
├── web-fetch.ts ← Web 页面抓取(HTML→Markdown)
├── web-tools.ts ← Web 工具聚合注册
├── web-guarded-fetch.ts ← SSRF 安全防护 fetch
├── web-search-citation-redirect.ts ← 搜索结果引用重定向
├── web-shared.ts ← Web 工具共享缓存
│
├── message-tool.ts ← 跨渠道消息发送(send/reply/react/edit/delete/pin...)
├── browser-tool.ts ← 浏览器控制(CDP/Playwright AI)
├── canvas-tool.ts ← Canvas UI 控制(present/navigate/eval/snapshot/A2UI)
├── image-tool.ts ← 图像理解(多模型自动 fallback)
├── pdf-tool.ts ← PDF 分析(原生 PDF + 提取 fallback)
├── cron-tool.ts ← 定时任务管理(CRUD + wake)
├── nodes-tool.ts ← 设备节点控制(camera/screen/location/notify/run)
├── tts-tool.ts ← 文本转语音
├── memory-tool.ts ← 记忆检索(向量搜索 + MMR)
├── subagents-tool.ts ← 子 Agent 编排(list/kill/steer)
│
├── sessions-*.ts ← 会话管理工具集(列表/详情/切换/重置/统计)
├── agents-list-tool.ts ← Agent 列表
├── gateway-tool.ts ← 网关操作
├── session-status-tool.ts ← 会话状态
│
├── discord-actions-*.ts ← Discord 特有操作
├── slack-actions.ts ← Slack 特有操作
├── telegram-actions.ts ← Telegram 特有操作
├── whatsapp-actions.ts ← WhatsApp 特有操作
│
├── media-tool-shared.ts ← 媒体工具共享逻辑
├── model-config.helpers.ts ← 模型配置辅助
├── tool-runtime.helpers.ts ← 工具运行时辅助(沙箱 bridge、模型 fallback)
├── nodes-utils.ts ← 节点解析辅助
└── *.test.ts ← 各工具配套测试1.3 工具数量统计
| 类别 | 工具数 | 说明 |
|---|---|---|
| Web 工具 | 2 | search + fetch |
| 消息工具 | 1 | 20+ 种 action |
| 媒体理解 | 2 | image + pdf |
| 浏览器 | 1 | CDP + Playwright AI |
| Canvas | 1 | 7 种 action |
| 定时任务 | 1 | 8 种 action |
| 设备控制 | 1 | 18 种 action |
| 会话管理 | ~3 | sessions + status + agents-list |
| Agent 编排 | 1 | list / kill / steer |
| 语音 | 1 | TTS |
| 记忆 | 1 | 向量搜索 |
| 渠道操作 | 4 | Discord/Slack/Telegram/WhatsApp |
| 合计 | ~19 主工具 | 70+ 种 action |
Agent 工具系统/01-infographic-tool-system-overview-1775150789999.png)
二、工具基础设施 (common.ts)
2.1 AnyAgentTool 类型
每个工具都必须实现 AnyAgentTool 接口:
typescript
type AnyAgentTool = {
name: string; // 工具唯一标识
label: string; // 人类可读标签
description: string; // 给 LLM 看的描述文本
parameters: TObject; // TypeBox JSON Schema(工具参数定义)
ownerOnly?: boolean; // 是否仅 Owner 可调用
execute: (
toolCallId: string, // 调用 ID(幂等性保证)
args: Record<string, unknown>, // LLM 传入的参数
) => Promise<AgentToolResult>; // 工具执行结果
};2.2 参数读取辅助函数
LLM 传入的参数可能不符合预期类型。OpenClaw 提供了一套防御性读取函数:
typescript
function readStringParam(
params: Record<string, unknown>,
name: string,
opts?: { required?: boolean; trim?: boolean; label?: string }
): string | undefined {
const raw = params[name];
if (raw === null || raw === undefined) {
if (opts?.required) throw new Error(`${opts.label ?? name} is required`);
return undefined;
}
// 数字和布尔值也能接受——转为字符串
if (typeof raw === "number" || typeof raw === "boolean") {
return String(raw);
}
if (typeof raw !== "string") {
throw new Error(`${name} must be a string`);
}
return opts?.trim !== false ? raw.trim() : raw;
}
function readNumberParam(params, name, opts?) {
const raw = params[name];
if (typeof raw === "string") {
const parsed = Number(raw); // 字符串 "42" → 数字 42
if (Number.isFinite(parsed)) return parsed;
}
// ...
}为什么需要这些函数? LLM 有时会把数字传成字符串("42" 而非 42),或者忘记必填参数。这些辅助函数提供了宽容的输入解析 + 严格的类型保证。
2.3 结果构建函数
typescript
function jsonResult(data: unknown): AgentToolResult {
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
details: data as Record<string, unknown>,
};
}
async function imageResult(params: {
label: string;
path: string;
base64: string;
mimeType: string;
details?: Record<string, unknown>;
imageSanitization?: ImageSanitizationLimits;
}): Promise<AgentToolResult> {
// 图像结果会经过尺寸限制和格式清理
const sanitized = await sanitizeToolResultImages(params);
return {
content: [{ type: "image", data: sanitized.base64, mimeType: sanitized.mimeType }],
details: { ...params.details, path: params.path },
};
}2.4 TypeBox Schema 系统
工具参数使用 @sinclair/typebox 定义 JSON Schema,而非手写 JSON:
typescript
import { Type } from "@sinclair/typebox";
const WebSearchSchema = Type.Object({
query: Type.String({ description: "Search query string." }),
count: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
country: Type.Optional(Type.String()),
freshness: Type.Optional(Type.String()),
});为什么用 TypeBox?
- 类型安全:TypeScript 类型推导自动生效
- 避免
anyOf/oneOf/allOf:某些 LLM 提供商不支持复杂 Union schema stringEnum/optionalStringEnum:自定义的枚举类型,避免Type.Union的兼容性问题
2.5 扁平化 Schema 设计
注意工具 schema 的一个重要设计原则——扁平化:
typescript
// ✅ 正确:扁平化 schema,运行时按 action 验证
const NodesToolSchema = Type.Object({
action: stringEnum(NODES_TOOL_ACTIONS), // "status" | "camera_snap" | "location_get" | ...
node: Type.Optional(Type.String()), // 某些 action 需要
facing: Type.Optional(Type.String()), // camera_snap 需要
maxWidth: Type.Optional(Type.Number()), // camera_snap 需要
// ... 所有 action 的参数都在顶层
});
// ❌ 避免:嵌套/Union schema(LLM 处理困难)
// Type.Union([
// Type.Object({ action: "status" }),
// Type.Object({ action: "camera_snap", facing: ..., maxWidth: ... }),
// ])原因: 大多数 LLM 对 anyOf/oneOf 的理解不稳定。扁平化 schema 让 LLM 只需填写一个平坦的对象,运行时再按 action 字段验证哪些参数是必需的。
Agent 工具系统/02-infographic-tool-infrastructure-1775150791029.png)
三、Gateway 工具调用协议
3.1 工具 → Gateway RPC
大多数工具的执行最终都需要调用 Gateway 的 RPC 方法。gateway.ts 封装了这个调用链路:
工具 execute()
│
├─ resolveGatewayOptions() ← 解析 Gateway URL + Token
│ ├─ 显式传入的 gatewayUrl/gatewayToken
│ ├─ 配置文件中的 gateway 设置
│ └─ 默认: ws://127.0.0.1:18789
│
├─ validateGatewayUrlOverrideForAgentTools() ← 安全验证 URL
│ ├─ 仅允许 loopback 地址
│ └─ 或配置中的 gateway.remote.url
│
└─ callGatewayTool(method, opts, params)
├─ 自动附加最小权限 scopes
├─ 设置 clientName = "agent"
└─ 超时默认 30 秒3.2 Gateway URL 安全验证
工具可以指定 gatewayUrl 参数覆盖默认 Gateway 地址,但这个覆盖受到严格限制:
typescript
function validateGatewayUrlOverrideForAgentTools(params) {
const localAllowed = new Set([
`ws://127.0.0.1:${port}`,
`wss://127.0.0.1:${port}`,
`ws://localhost:${port}`,
`wss://localhost:${port}`,
`ws://[::1]:${port}`,
`wss://[::1]:${port}`,
]);
// 只允许本地回环 + 配置中的远程 URL
if (localAllowed.has(parsed.key)) return { url, target: "local" };
if (remoteKey && parsed.key === remoteKey) return { url, target: "remote" };
throw new Error("gatewayUrl override rejected.");
}安全意义: 防止 Agent(可能被 Prompt Injection 操控)将工具调用重定向到恶意的 Gateway 服务器。
3.3 最小权限 Scopes
每个 Gateway RPC 方法都有预定义的最小权限范围:
typescript
const scopes = resolveLeastPrivilegeOperatorScopesForMethod(method);
// "chat.send" → ["chat:write"]
// "node.invoke" → ["node:invoke"]
// "session.list" → ["session:read"]Agent 工具系统/03-infographic-gateway-protocol-1775150792011.png)
四、Web 搜索工具
4.1 五大搜索引擎
typescript
const SEARCH_PROVIDERS = ["brave", "perplexity", "grok", "gemini", "kimi"] as const;| 提供商 | API 端点 | 默认模型 | 特点 |
|---|---|---|---|
| Brave | api.search.brave.com | N/A(传统搜索) | 结构化结果、新鲜度过滤 |
| Perplexity | OpenRouter / 直连 | sonar-pro | AI 总结 + 引用 |
| Grok | api.x.ai/v1/responses | grok-4-1-fast | xAI 搜索 + 注释引用 |
| Gemini | Google Generative AI | gemini-2.5-flash | Grounding 搜索 |
| Kimi | api.moonshot.ai/v1 | moonshot-v1-128k | 月之暗面 Web 搜索 |
4.2 搜索结果缓存
typescript
const SEARCH_CACHE = new Map<string, CacheEntry<Record<string, unknown>>>();
const DEFAULT_CACHE_TTL_MINUTES = 30;
function normalizeCacheKey(query: string, count: number, extras?: Record<string, string>) {
const parts = [query.toLowerCase().trim(), `count=${count}`];
if (extras) {
for (const [k, v] of Object.entries(extras).sort()) {
parts.push(`${k}=${v}`);
}
}
return parts.join("|");
}缓存键由查询 + 参数构成,TTL 30 分钟。避免短时间内重复搜索同一内容产生 API 费用。
4.3 Brave 搜索新鲜度过滤
typescript
const BRAVE_FRESHNESS_SHORTCUTS = new Set(["pd", "pw", "pm", "py"]);
// pd = past day, pw = past week, pm = past month, py = past year
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
// 或自定义日期范围: "2024-01-01to2024-06-30"4.4 引用重定向
搜索结果中的引用链接会经过 resolveCitationRedirectUrl() 处理,确保 Agent 引用的 URL 可以被用户直接点击访问。
五、Web 抓取工具
5.1 HTML → Markdown 转换
web-fetch.ts 的核心能力是将网页内容转换为 LLM 可以理解的 Markdown 格式:
用户请求: "帮我看看这个网页的内容"
│
├─ SSRF 安全检查(通过 web-guarded-fetch.ts)
│
├─ HTTP GET 请求(带超时、重定向跟踪)
│
├─ Content-Type 检测
│ ├─ text/html → HTML → Markdown 转换
│ ├─ application/json → JSON 美化
│ └─ text/plain → 直接返回
│
├─ 内容截断(防止超出 LLM 上下文窗口)
│
└─ 结果返回(Markdown 文本 + 元数据)5.2 安全保护 (web-guarded-fetch.ts)
所有 Web 工具的 HTTP 请求都必须经过 SSRF 防护:
typescript
export async function withTrustedWebToolsEndpoint(
url: string,
fn: (guardedUrl: string) => Promise<Response>,
): Promise<Response> {
// 1. 解析 URL
// 2. SSRF 检查(私有 IP、DNS rebinding 等)
// 3. DNS 钉住
// 4. 执行 fn(guardedUrl)
}Agent 工具系统/04-infographic-web-tools-1775150792917.png)
六、消息工具
6.1 20+ 种消息操作
消息工具是整个工具系统中最复杂的一个,支持 20+ 种跨渠道消息操作:
typescript
const AllMessageActions = [
"send", // 发送消息
"sendWithEffect", // 带特效发送(iMessage bubble effects)
"sendAttachment", // 发送附件
"reply", // 回复消息
"thread-reply", // 线程回复
"broadcast", // 广播消息到多个目标
"react", // 表情反应
"unreact", // 取消表情
"edit", // 编辑消息
"delete", // 删除消息
"forward", // 转发消息
"pin", // 置顶消息
"unpin", // 取消置顶
"read", // 标记已读
"typing", // 发送正在输入状态
"info", // 获取消息信息
"history", // 获取聊天历史
"contacts", // 获取联系人
"groups", // 获取群组列表
"presence", // 在线状态
// ... 渠道特有操作
];6.2 路由 Schema
每个消息操作都需要指定发送目标:
typescript
function buildRoutingSchema() {
return {
channel: Type.Optional(Type.String()), // 目标渠道
target: Type.Optional(channelTargetSchema()), // 单个目标
targets: Type.Optional(channelTargetsSchema()), // 多个目标(broadcast)
accountId: Type.Optional(Type.String()), // 指定账户
dryRun: Type.Optional(Type.Boolean()), // 模拟运行
};
}6.3 Discord Components v2
消息工具对 Discord 有特殊支持——Discord Components v2(按钮、选择菜单、表单模态框):
typescript
const discordComponentButtonSchema = Type.Object({
label: Type.String(),
style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])),
url: Type.Optional(Type.String()),
emoji: Type.Optional(discordComponentEmojiSchema),
disabled: Type.Optional(Type.Boolean()),
allowedUsers: Type.Optional(Type.Array(Type.String())),
});
const discordComponentModalSchema = Type.Object({
title: Type.String(),
triggerLabel: Type.Optional(Type.String()),
fields: Type.Array(discordComponentModalFieldSchema),
});6.4 动态 Schema 裁剪
消息工具的 schema 会根据当前渠道的能力动态裁剪:
typescript
function buildSendSchema(options: {
includeButtons: boolean; // Telegram/Discord 支持
includeCards: boolean; // MS Teams/Slack 支持
includeComponents: boolean; // Discord 支持
}) {
const props = { message, media, buffer, ... };
if (!options.includeButtons) delete props.buttons;
if (!options.includeCards) delete props.card;
if (!options.includeComponents) delete props.components;
return props;
}Agent 工具系统/05-infographic-message-tool-1775150793777.png)
七、图像理解工具
7.1 多模型自动 Fallback
图像工具的核心设计是智能模型选择 + 自动 fallback:
用户: "分析这张图片"
│
├─ 解析图像来源(URL / 本地路径 / Base64 / data: URL)
│
├─ 选择模型(优先级):
│ 1. 显式配置的 imageModel
│ 2. 与主模型同提供商的视觉模型
│ 3. OpenAI gpt-5-mini
│ 4. Anthropic claude-opus-4-6
│ 5. Anthropic claude-opus-4-5 (fallback)
│
├─ 尝试调用 → 失败?→ 尝试下一个 fallback
│
└─ MiniMax 特殊处理(VLM 只支持单张图片)7.2 模型配置解析
typescript
function resolveImageModelConfigForTool(params: {
cfg?: OpenClawConfig;
agentDir: string;
}): ImageModelConfig | null {
// 1. 检查显式配置
const explicit = coerceImageModelConfig(params.cfg);
if (explicit.primary) return explicit;
// 2. 检测可用的 API Key
const openaiOk = hasAuthForProvider({ provider: "openai", agentDir });
const anthropicOk = hasAuthForProvider({ provider: "anthropic", agentDir });
// 3. 按提供商优先级自动选择
if (primary.provider === "minimax") return { primary: "minimax/MiniMax-VL-01" };
if (primary.provider === "openai") return { primary: "openai/gpt-5-mini", fallbacks: [...] };
// ...
}7.3 图像安全限制
typescript
const imageSanitization = resolveImageSanitizationLimits(config);
// 限制图像尺寸、文件大小,防止 OOM
// 对工具返回的图像结果进行清理和压缩八、PDF 分析工具
8.1 双路径处理
PDF 工具有两条处理路径:
PDF 输入
│
├─ Path A: 原生 PDF 支持(Anthropic / Google)
│ └─ 直接将 PDF 二进制传给 API
│
└─ Path B: 提取 Fallback(其他提供商)
├─ 文本提取(pdfjs)
├─ 页面渲染为图片
└─ 构建多模态上下文传给视觉模型typescript
function providerSupportsNativePdf(provider: string): boolean {
return provider === "anthropic" || provider === "google";
}8.2 页面范围过滤
typescript
const pages = parsePageRange("1-5,8,10-12");
// → [1, 2, 3, 4, 5, 8, 10, 11, 12]
// 注意:原生 PDF 提供商不支持页面过滤(会报错)8.3 模型选择(PDF 优先 Anthropic)
PDF 工具的模型选择优先考虑支持原生 PDF 的提供商:
1. 显式 pdfModel 配置
2. 显式 imageModel 配置(降级)
3. Anthropic claude-opus-4-6(原生 PDF)
4. Google gemini-2.5-pro(原生 PDF)
5. OpenAI gpt-5-mini(提取 fallback)Agent 工具系统/06-infographic-media-understanding-1775150794871.png)
九、浏览器工具
9.1 双引擎架构
浏览器工具支持两种底层引擎:
| 引擎 | 说明 | 适用场景 |
|---|---|---|
| CDP | Chrome DevTools Protocol 直连 | 精确控制、自动化 |
| Playwright AI | Playwright + AI 代理 | 智能交互、自然语言操作 |
9.2 操作类型
浏览器工具
├── navigate(url) ← 导航到 URL
├── snapshot() ← 页面快照(截图)
├── click(selector) ← 点击元素
├── fill(selector, value) ← 填写表单
├── evaluate(js) ← 执行 JavaScript
├── scroll(direction) ← 滚动页面
├── upload(filePath) ← 文件上传
└── extract(selector) ← 提取 DOM 内容9.3 导航安全守卫
每次浏览器导航前都会经过 SSRF 检查(复用第十二篇介绍的 SSRF 防护链):
browser.navigate("http://169.254.169.254/latest/meta-data/")
→ NavigationGuard → SsrfBlockedError
→ "Blocked: private network address"Agent 工具系统/07-infographic-browser-tool-1775150795743.png)
十、Canvas 工具
10.1 Canvas 是什么?
Canvas 是 OpenClaw 的"画布"功能——Agent 可以在用户的设备上展示交互式 UI(网页、A2UI 组件)。
10.2 七种 Canvas 操作
typescript
const CANVAS_ACTIONS = [
"present", // 在设备上展示 Canvas(指定 URL + 位置)
"hide", // 隐藏 Canvas
"navigate", // Canvas 内导航到新 URL
"eval", // 在 Canvas 中执行 JavaScript
"snapshot", // 截取 Canvas 当前画面
"a2ui_push", // 推送 A2UI JSONL 数据
"a2ui_reset", // 重置 A2UI 状态
] as const;10.3 执行链路
Canvas 工具的执行需要跨越三层调用:
Agent → Canvas Tool → Gateway RPC → Node Device
│
├─ node.invoke("canvas.present", { url, placement })
├─ node.invoke("canvas.snapshot", { format, maxWidth })
└─ node.invoke("canvas.eval", { javaScript })10.4 JSONL 路径安全
A2UI push 操作可以从文件读取 JSONL 数据,但文件路径受到安全限制:
typescript
async function readJsonlFromPath(jsonlPath: string): Promise<string> {
const resolved = path.resolve(trimmed);
const roots = getDefaultMediaLocalRoots();
// 两次路径检查:resolve 前 + realpath 后
if (!isInboundPathAllowed({ filePath: resolved, roots })) {
throw new Error("jsonlPath outside allowed roots");
}
const canonical = await fs.realpath(resolved).catch(() => resolved);
if (!isInboundPathAllowed({ filePath: canonical, roots })) {
throw new Error("jsonlPath outside allowed roots");
}
return await fs.readFile(canonical, "utf8");
}Agent 工具系统/08-infographic-canvas-tool-1775150796719.png)
十一、Cron 定时任务工具
11.1 八种操作
typescript
const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs", "wake"];11.2 三种调度类型
| 类型 | 格式 | 示例 |
|---|---|---|
at | ISO-8601 一次性 | { "kind": "at", "at": "2025-06-15T09:00:00Z" } |
every | 固定间隔 | { "kind": "every", "everyMs": 3600000 } |
cron | Cron 表达式 | { "kind": "cron", "expr": "0 9 * * *", "tz": "Asia/Shanghai" } |
11.3 上下文消息注入
创建提醒时,工具可以自动将最近的对话上下文注入到提醒文本中:
typescript
async function buildReminderContextLines(params: {
contextMessages: number; // 要附加的最近消息数
agentSessionKey?: string;
}) {
// 从 Gateway 拉取最近 N 条消息
const res = await callGatewayTool("chat.history", opts, {
sessionKey: resolvedKey,
limit: maxMessages, // 最大 10 条
});
// 每条消息最大 220 字符,总计最大 700 字符
const lines = [];
for (const entry of recent) {
const line = `- ${label}: ${truncateText(entry.text, 220)}`;
total += line.length;
if (total > 700) break;
lines.push(line);
}
return lines;
}11.4 从 Session Key 推断投递目标
创建定时任务时,如果用户没有显式指定投递渠道,工具会从当前会话键推断:
typescript
function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | null {
// "agent:main:telegram:direct:12345" → { channel: "telegram", to: "12345" }
// "agent:main:slack:channel:C01234" → { channel: "slack", to: "C01234" }
// "agent:main:main" → null (无法推断)
}十二、节点设备工具
12.1 18 种设备操作
Nodes 工具是工具系统中操作类型最多的,涵盖对配对的 iOS/Android/macOS 设备的全方位控制:
typescript
const NODES_TOOL_ACTIONS = [
"status", // 列出所有已连接节点
"describe", // 节点详细信息
"pending", // 待审批的配对请求
"approve", // 审批配对
"reject", // 拒绝配对
"notify", // 发送通知到设备
"camera_snap", // 拍照(前置/后置/双摄)
"camera_list", // 列出可用相机
"camera_clip", // 录制视频片段
"screen_record", // 录屏
"location_get", // 获取设备 GPS 位置
"notifications_list", // 列出设备通知
"notifications_action", // 通知操作(打开/关闭/回复)
"device_status", // 设备状态(电量/网络/存储)
"device_info", // 设备信息(型号/系统版本)
"device_permissions", // 权限状态
"device_health", // 设备健康状况
"run", // 在设备上执行命令
"invoke", // 调用设备自定义命令
];12.2 拍照 (camera_snap)
Agent: "帮我用手机拍张前置照片"
│
├─ resolveNodeId() → 找到已连接的手机
│
├─ node.invoke("camera.snap", {
│ facing: "front",
│ maxWidth: 1600,
│ quality: 0.92,
│ })
│
├─ 设备返回 Base64 图片数据
│
├─ 写入临时文件
│
└─ 返回 imageResult(经过 sanitization)双摄模式: facing: "both" 会同时拍前后摄像头,返回两张图片。
12.3 设备命令执行 (run)
Agent 可以在配对设备上远程执行命令:
typescript
case "run": {
const command = params.command; // ["ls", "-la", "/tmp"]
const cwd = params.cwd;
const env = parseEnvPairs(params.env);
const timeout = parseTimeoutMs(params.commandTimeoutMs);
const result = await callGatewayTool("node.invoke", gatewayOpts, {
nodeId,
command: "system.run",
params: { command, cwd, env, timeoutMs: timeout },
});
return jsonResult(result);
}Agent 工具系统/09-infographic-cron-nodes-tools-1775150797664.png)
十三、子 Agent 编排工具
13.1 三种编排操作
typescript
const SUBAGENT_ACTIONS = ["list", "kill", "steer"];| 操作 | 作用 | 说明 |
|---|---|---|
list | 列出子 Agent 运行记录 | 按时间排序,显示状态/模型/token 用量 |
kill | 终止运行中的子 Agent | 中止嵌入式 Pi 运行 + 清理队列 |
steer | 转向子 Agent | 发送新指令,让子 Agent 改变方向 |
13.2 Steer 操作(最精巧的设计)
Steer 允许父 Agent 在子 Agent 运行过程中"转向"它——终止当前执行,注入新指令并重启:
父 Agent: "让研究子 Agent 改为搜索 Python 框架"
│
├─ 1. 速率限制检查(每个子 Agent 最短 2 秒间隔)
│
├─ 2. 截断消息(最大 4000 字符)
│
├─ 3. 中止当前运行
│ ├─ abortEmbeddedPiRun(sessionId)
│ └─ clearSessionQueues([childSessionKey])
│
├─ 4. 等待结算(5 秒超时)
│
├─ 5. 注入新指令到子 Agent 的会话
│ ├─ markSubagentRunForSteerRestart(runId, message)
│ └─ replaceSubagentRunAfterSteer(...)
│
└─ 6. 子 Agent 以新指令重启13.3 层级感知
子 Agent 工具有层级感知——它知道调用者是顶级会话还是子 Agent 本身:
typescript
function resolveRequesterKey(params) {
if (!isSubagentSessionKey(callerSessionKey)) {
// 顶级会话 → 看自己直接创建的子 Agent
return { requesterSessionKey: callerSessionKey, callerIsSubagent: false };
}
// 子 Agent 调用 → 检查是否还能再创建子 Agent(编排器模式)
const callerDepth = getSubagentDepthFromSessionStore(callerSessionKey);
if (callerDepth < maxSpawnDepth) {
// 编排器子 Agent → 看自己创建的下级子 Agent
return { requesterSessionKey: callerSessionKey, callerIsSubagent: true };
}
// 叶子子 Agent → 向上回溯到父 Agent,看兄弟运行记录
return { requesterSessionKey: spawnedBy, callerIsSubagent: true };
}Agent 工具系统/10-infographic-subagent-orchestration-1775150798488.png)
十四、会话管理工具
14.1 会话辅助函数 (sessions-helpers.ts)
会话工具的核心辅助模块提供:
- 会话别名解析:
resolveMainSessionAlias()— 将"main"解析为实际的 session key - 内部 session key 解析:统一处理别名、旧版 key、新版 key
- 会话类型分类:
classifySessionKind()— 区分 main / group / cron / hook / node / other - 渠道推导:
deriveChannel()— 从 session key 推导出渠道名 - 文本清理:
sanitizeTextContent()— 去除工具调用标记和思考标签
14.2 会话访问控制 (sessions-access.ts)
会话工具有精细的访问控制——不同角色可以看到不同的会话:
typescript
type SessionToolsVisibility = "full" | "own" | "spawned-tree" | "none";| 可见性 | 可见范围 | 适用场景 |
|---|---|---|
full | 所有会话 | Owner / Main 会话 |
own | 仅自己的会话 | 配对用户 |
spawned-tree | 自己 + 子 Agent 创建的会话 | 编排器子 Agent |
none | 无 | 未授权 |
十五、TTS 语音合成工具
15.1 极简设计
TTS 工具是整个工具系统中最简洁的(61 行):
typescript
const TtsToolSchema = Type.Object({
text: Type.String({ description: "Text to convert to speech." }),
channel: Type.Optional(Type.String()),
});
export function createTtsTool(opts?): AnyAgentTool {
return {
name: "tts",
description: `Convert text to speech. Reply with ${SILENT_REPLY_TOKEN} after a successful call.`,
parameters: TtsToolSchema,
execute: async (_toolCallId, args) => {
const result = await textToSpeech({ text, cfg, channel });
if (result.success && result.audioPath) {
const lines = [];
if (result.voiceCompatible) lines.push("[[audio_as_voice]]"); // Telegram Opus 语音气泡
lines.push(`MEDIA:${result.audioPath}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
return { content: [{ type: "text", text: result.error ?? "TTS conversion failed" }] };
},
};
}SILENT_REPLY_TOKEN:告诉 Agent 在 TTS 工具调用成功后不要再发文字消息(因为音频已经被自动投递了)。
十六、记忆工具
16.1 核心操作
记忆工具允许 Agent 搜索和管理长期记忆:
typescript
const MEMORY_ACTIONS = ["search", "add", "list", "remove"];- search:语义向量搜索 + MMR 多样性重排
- add:添加新的记忆条目
- list:列出所有记忆
- remove:删除指定记忆
16.2 与记忆系统的协作
记忆工具是第八篇介绍的记忆系统的"用户界面"——Agent 通过工具访问底层的 SQLite + 向量索引:
Agent 调用 memory.search("上周讨论的项目方案")
│
├─ 记忆工具 → Gateway RPC → 记忆引擎
│
├─ 文本 → Embedding 向量
│
├─ 向量相似度搜索 (top-K)
│
├─ MMR 多样性重排
│
└─ 返回最相关的记忆条目Agent 工具系统/11-infographic-session-tts-memory-1775150799401.png)
十七、设计模式与架构总结
17.1 统一的工具接口
所有 19 个工具都遵循完全一致的接口:
createXxxTool(options?) → AnyAgentTool → {
name, label, description,
parameters: TypeBox Schema,
execute: (id, args) → Promise<AgentToolResult>
}这种一致性意味着:
- 新工具的添加只需要实现一个工厂函数
- 工具的注册、发现、文档生成全部自动化
- 测试可以用统一的 harness
17.2 扁平化 Action 模式
大多数工具使用单工具多 Action模式:
一个工具(nodes)→ 18 种 action
一个工具(message)→ 20+ 种 action
一个工具(cron)→ 8 种 action优势: 减少 LLM 需要选择的工具数量(从 70+ 降到 ~19),降低工具选择错误率。
17.3 安全边界
┌────────────────────────────────┐
│ LLM (Agent) │
└──────────────┬─────────────────┘
│
┌──────────────▼─────────────────┐
│ 工具参数 Schema 验证 │ ← 第 1 层
└──────────────┬─────────────────┘
│
┌──────────────▼─────────────────┐
│ 防御性参数读取 (common.ts) │ ← 第 2 层
└──────────────┬─────────────────┘
│
┌──────────────▼─────────────────┐
│ Gateway URL 安全验证 │ ← 第 3 层
│ SSRF 防护 (web-guarded-fetch) │
│ 路径安全检查 (canvas/nodes) │
└──────────────┬─────────────────┘
│
┌──────────────▼─────────────────┐
│ 最小权限 Scopes │ ← 第 4 层
│ Gateway 认证 │
└──────────────┬─────────────────┘
│
┌──────────────▼─────────────────┐
│ ownerOnly 权限检查 │ ← 第 5 层
│ 会话访问控制 │
└─────────────────────────────────┘Agent 工具系统/12-infographic-design-patterns-security-1775150800202.png)
17.4 推荐阅读顺序
| 优先级 | 文件 | 行数 | 难度 |
|---|---|---|---|
| 🔴 必读 | common.ts | ~500 | ★★☆☆☆ |
| 🔴 必读 | gateway.ts | ~160 | ★★★☆☆ |
| 🟡 推荐 | web-search.ts | ~700 | ★★★☆☆ |
| 🟡 推荐 | message-tool.ts | ~800 | ★★★★☆ |
| 🟡 推荐 | canvas-tool.ts | ~215 | ★★☆☆☆ |
| 🟡 推荐 | cron-tool.ts | ~500 | ★★★☆☆ |
| 🟡 推荐 | subagents-tool.ts | ~500 | ★★★★☆ |
| 🟢 选读 | image-tool.ts | ~500 | ★★★★☆ |
| 🟢 选读 | pdf-tool.ts | ~500 | ★★★★☆ |
| 🟢 选读 | nodes-tool.ts | ~700 | ★★★☆☆ |
| 🟢 选读 | tts-tool.ts | ~61 | ★☆☆☆☆ |
| 🟢 选读 | sessions-helpers.ts | ~170 | ★★☆☆☆ |
17.5 思考题
为什么工具 schema 要扁平化,而不是用 JSON Schema 的
oneOf为每个 action 定义独立 schema?提示:考虑 LLM 对
oneOf/anyOf的理解能力和不同提供商的兼容性。Gateway URL 覆盖为什么只允许 loopback 地址和配置中的远程 URL?如果允许任意 URL 会有什么风险?
提示:考虑 Prompt Injection 攻击——恶意 prompt 可能诱导 Agent 将工具调用发到攻击者的服务器。
图像工具为什么要自动 fallback 到不同的模型提供商,而不是直接报错?
提示:考虑用户体验——用户不关心用的是哪个模型,只关心图片能不能被分析。
Steer 操作为什么需要 2 秒的速率限制?
提示:考虑中止和重启之间的竞态条件——旧运行可能还没完全结束,新运行就开始了。
📌 小结: Agent 工具系统是 OpenClaw 能力的"手和脚"——通过 19 个主工具和 70+ 种操作,Agent 可以搜索互联网、发送跨渠道消息、理解图像和 PDF、控制浏览器、管理定时任务、操控物理设备、编排子 Agent。工具系统的设计哲学是"宽容的输入 + 严格的安全"——防御性参数读取容忍 LLM 的小错误,而多层安全边界确保 Agent 不会被恶意 prompt 操控做出危险操作。