Skip to content

OpenClaw 源码解读(十六)Agent 工具系统

本篇深入分析 OpenClaw 的 Agent 工具系统——Agent 对外部世界交互的能力具象化。从通用工具基础设施到 30+ 个具体工具实现,涵盖 Web 搜索、浏览器控制、消息发送、图像/PDF 理解、定时任务、Canvas UI、节点设备控制、子 Agent 编排等全部能力。工具系统是 AI Agent 从"能对话"进化到"能行动"的关键桥梁。


目录


一、工具系统全景

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 工具2search + fetch
消息工具120+ 种 action
媒体理解2image + pdf
浏览器1CDP + Playwright AI
Canvas17 种 action
定时任务18 种 action
设备控制118 种 action
会话管理~3sessions + status + agents-list
Agent 编排1list / kill / steer
语音1TTS
记忆1向量搜索
渠道操作4Discord/Slack/Telegram/WhatsApp
合计~19 主工具70+ 种 action

![Agent 工具系统全景:19 个主工具与 70+ 种 action 分类](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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 字段验证哪些参数是必需的。

![工具基础设施 common.ts:Schema 定义、参数读取、执行与结果构建](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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"]

![Gateway 工具调用协议:调用链路与三层安全验证](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)Agent 工具系统/03-infographic-gateway-protocol-1775150792011.png)


四、Web 搜索工具

4.1 五大搜索引擎

typescript
const SEARCH_PROVIDERS = ["brave", "perplexity", "grok", "gemini", "kimi"] as const;
提供商API 端点默认模型特点
Braveapi.search.brave.comN/A(传统搜索)结构化结果、新鲜度过滤
PerplexityOpenRouter / 直连sonar-proAI 总结 + 引用
Grokapi.x.ai/v1/responsesgrok-4-1-fastxAI 搜索 + 注释引用
GeminiGoogle Generative AIgemini-2.5-flashGrounding 搜索
Kimiapi.moonshot.ai/v1moonshot-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)
}

![Web 工具:五大搜索引擎与 HTML→Markdown 抓取管线](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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;
}

![消息工具:20+ 种操作与跨渠道路由机制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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)

![媒体理解工具:图像多模型 Fallback 与 PDF 双路径处理](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)Agent 工具系统/06-infographic-media-understanding-1775150794871.png)


九、浏览器工具

9.1 双引擎架构

浏览器工具支持两种底层引擎:

引擎说明适用场景
CDPChrome DevTools Protocol 直连精确控制、自动化
Playwright AIPlaywright + 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"

![浏览器工具:CDP 与 Playwright AI 双引擎架构](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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");
}

![Canvas 工具:三层调用链路与 7 种操作](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)Agent 工具系统/08-infographic-canvas-tool-1775150796719.png)


十一、Cron 定时任务工具

11.1 八种操作

typescript
const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs", "wake"];

11.2 三种调度类型

类型格式示例
atISO-8601 一次性{ "kind": "at", "at": "2025-06-15T09:00:00Z" }
every固定间隔{ "kind": "every", "everyMs": 3600000 }
cronCron 表达式{ "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);
}

![Cron 定时任务与节点设备工具:调度类型与 18 种设备操作](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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 编排:Steer 操作流程与层级感知机制](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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 多样性重排

    └─ 返回最相关的记忆条目

![会话管理、TTS 语音合成与记忆工具](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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 层
                    │  会话访问控制                     │
                    └─────────────────────────────────┘

![设计模式与安全架构:五层安全边界与三大设计模式](https://qn.huat.xyz/blog/article-Illustration/OpenClaw 源码解读(十六)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 思考题

  1. 为什么工具 schema 要扁平化,而不是用 JSON Schema 的 oneOf 为每个 action 定义独立 schema?

    提示:考虑 LLM 对 oneOf/anyOf 的理解能力和不同提供商的兼容性。

  2. Gateway URL 覆盖为什么只允许 loopback 地址和配置中的远程 URL?如果允许任意 URL 会有什么风险?

    提示:考虑 Prompt Injection 攻击——恶意 prompt 可能诱导 Agent 将工具调用发到攻击者的服务器。

  3. 图像工具为什么要自动 fallback 到不同的模型提供商,而不是直接报错?

    提示:考虑用户体验——用户不关心用的是哪个模型,只关心图片能不能被分析。

  4. Steer 操作为什么需要 2 秒的速率限制?

    提示:考虑中止和重启之间的竞态条件——旧运行可能还没完全结束,新运行就开始了。


📌 小结: Agent 工具系统是 OpenClaw 能力的"手和脚"——通过 19 个主工具和 70+ 种操作,Agent 可以搜索互联网、发送跨渠道消息、理解图像和 PDF、控制浏览器、管理定时任务、操控物理设备、编排子 Agent。工具系统的设计哲学是"宽容的输入 + 严格的安全"——防御性参数读取容忍 LLM 的小错误,而多层安全边界确保 Agent 不会被恶意 prompt 操控做出危险操作。

读文档、看源码、写代码,理解 AI Agent 本质 🤖