Skip to main content

插件代理工具

OpenClaw 插件可以注册代理工具(JSON-schema 函数),这些工具在代理运行时向 LLM 暴露。工具可以是必需的(始终可用)或可选的(需选择加入)。 代理工具在主配置的 tools 下配置,或在每个代理的 agents.list[].tools 下配置。允许列表/拒绝列表策略控制代理可以调用哪些工具。

基础工具

使用 TypeBox 定义工具参数:
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";

export default function (api: OpenClawPluginApi) {
  api.registerTool({
    name: "my_tool",
    description: "执行一个操作",
    parameters: Type.Object({
      input: Type.String({ description: "要处理的输入" }),
    }),
    async execute(_toolCallId, params) {
      const result = processInput(params.input);
      return {
        content: [
          {
            type: "text",
            text: `处理结果: ${result}`,
          },
        ],
      };
    },
  });
}

function processInput(input: string): string {
  return input.toUpperCase();
}

工具定义

name
string
required
工具的唯一名称。LLM 将使用此名称调用工具。命名规范:
  • 使用 snake_case
  • 清晰描述功能
  • 避免与核心工具冲突
示例: voice_call, search_files, send_email
label
string
工具的显示名称。用于 UI 和日志。示例: “Voice Call”, “Search Files”
description
string
required
工具的详细描述。LLM 使用此信息决定何时调用工具。编写技巧:
  • 清楚说明工具的功能
  • 说明适用场景
  • 提及重要的限制或要求
示例:
description: "发起语音通话并朗读消息。支持通知模式(单向)和对话模式(双向)。需要配置电话服务提供商。"
parameters
TypeBoxSchema | object
required
工具参数的 JSON Schema。推荐使用 TypeBox。
import { Type } from "@sinclair/typebox";

parameters: Type.Object({
  required_field: Type.String({ description: "必需参数" }),
  optional_field: Type.Optional(
    Type.Number({ description: "可选参数", minimum: 0 })
  ),
})
execute
function
required
工具执行函数。接收参数并返回结果。签名:
async execute(
  toolCallId: string,
  params: Record<string, unknown>,
  ctx: OpenClawPluginToolContext
): Promise<ToolResult> | ToolResult
返回值:
{
  content: [
    { type: "text", text: "结果文本" },
    { type: "image", data: "base64..." },
  ],
  details?: any, // 可选的结构化数据
}

可选工具(需选择加入)

可选工具绝不会自动启用。用户必须将它们添加到代理允许列表中。
export default function (api: OpenClawPluginApi) {
  api.registerTool(
    {
      name: "dangerous_operation",
      description: "执行危险操作(需要显式启用)",
      parameters: Type.Object({
        action: Type.String({ description: "操作类型" }),
      }),
      async execute(_toolCallId, params) {
        // 执行危险操作
        return {
          content: [{ type: "text", text: "操作完成" }],
        };
      },
    },
    { optional: true } // 标记为可选
  );
}

启用可选工具

agents.list[].tools.allow 或全局 tools.allow 中启用:
{
  agents: {
    list: [
      {
        id: "main",
        tools: {
          allow: [
            "dangerous_operation", // 具体工具名
            "my-plugin", // 插件 ID(启用该插件下所有工具)
            "group:plugins", // 所有插件工具
          ],
        },
      },
    ],
  },
}

工具组

允许列表支持以下组:
  • group:plugins - 所有插件工具
  • group:core - 所有核心工具
  • <plugin-id> - 特定插件的所有工具
{
  tools: {
    allow: [
      "group:core", // 所有核心工具
      "group:plugins", // 所有插件工具
    ],
  },
}

工具上下文

工具执行时接收上下文对象:
type OpenClawPluginToolContext = {
  config?: OpenClawConfig; // 完整配置
  workspaceDir?: string; // 工作区目录
  agentDir?: string; // 代理目录
  agentId?: string; // 代理 ID
  sessionKey?: string; // 会话键
  sessionId?: string; // 会话 ID(临时)
  messageChannel?: string; // 消息渠道
  agentAccountId?: string; // 代理账户 ID
  requesterSenderId?: string; // 请求者 ID
  senderIsOwner?: boolean; // 是否为所有者
  sandboxed?: boolean; // 是否在沙盒中
};

使用上下文

api.registerTool({
  name: "context_aware_tool",
  description: "使用上下文的工具",
  parameters: Type.Object({
    query: Type.String(),
  }),
  async execute(_toolCallId, params, ctx) {
    // 检查权限
    if (!ctx.senderIsOwner) {
      return {
        content: [
          {
            type: "text",
            text: "此工具仅限所有者使用",
          },
        ],
      };
    }

    // 使用会话信息
    api.logger.info(
      `工具由 ${ctx.requesterSenderId}${ctx.messageChannel} 调用`
    );

    // 访问配置
    const pluginConfig = ctx.config?.plugins?.entries?.[api.id]?.config;

    // 执行操作
    return {
      content: [
        {
          type: "text",
          text: `查询结果: ${params.query}`,
        },
      ],
    };
  },
});

复杂参数

Union 类型

使用 Type.Union 支持多种操作:
const ToolParams = Type.Union([
  Type.Object({
    action: Type.Literal("create"),
    name: Type.String(),
    data: Type.Object({
      title: Type.String(),
      content: Type.String(),
    }),
  }),
  Type.Object({
    action: Type.Literal("delete"),
    id: Type.String(),
  }),
  Type.Object({
    action: Type.Literal("list"),
    limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
  }),
]);

api.registerTool({
  name: "resource_manager",
  description: "管理资源(创建、删除、列表)",
  parameters: ToolParams,
  async execute(_id, params) {
    if ("action" in params) {
      switch (params.action) {
        case "create":
          return { content: [{ type: "text", text: "已创建" }] };
        case "delete":
          return { content: [{ type: "text", text: "已删除" }] };
        case "list":
          return { content: [{ type: "text", text: "列表..." }] };
      }
    }
    return { content: [{ type: "text", text: "未知操作" }] };
  },
});

嵌套对象

const ToolParams = Type.Object({
  filters: Type.Object({
    category: Type.Optional(Type.String()),
    dateRange: Type.Optional(
      Type.Object({
        start: Type.String({ format: "date" }),
        end: Type.String({ format: "date" }),
      })
    ),
    tags: Type.Optional(Type.Array(Type.String())),
  }),
  options: Type.Optional(
    Type.Object({
      limit: Type.Number({ minimum: 1, maximum: 100, default: 10 }),
      offset: Type.Number({ minimum: 0, default: 0 }),
      sortBy: Type.Union([
        Type.Literal("date"),
        Type.Literal("name"),
        Type.Literal("relevance"),
      ]),
    })
  ),
});

数组参数

const ToolParams = Type.Object({
  items: Type.Array(
    Type.Object({
      id: Type.String(),
      quantity: Type.Number({ minimum: 1 }),
    })
  ),
  metadata: Type.Optional(Type.Record(Type.String(), Type.Any())),
});

工具工厂

对于需要访问上下文创建的工具,使用工具工厂:
import type {
  OpenClawPluginApi,
  OpenClawPluginToolFactory,
} from "openclaw/plugin-sdk";

export default function (api: OpenClawPluginApi) {
  const toolFactory: OpenClawPluginToolFactory = (ctx) => {
    // 根据上下文决定是否提供工具
    if (!ctx.config?.myPlugin?.enabled) {
      return null;
    }

    // 返回工具或工具数组
    return {
      name: "context_tool",
      description: "基于上下文的工具",
      parameters: Type.Object({
        input: Type.String(),
      }),
      async execute(_id, params) {
        // 使用工厂时的上下文
        const sessionDir = ctx.agentDir;
        return {
          content: [{ type: "text", text: `处理: ${params.input}` }],
        };
      },
    };
  };

  api.registerTool(toolFactory);
}

返回复杂内容

文本和图片

api.registerTool({
  name: "generate_chart",
  description: "生成图表",
  parameters: Type.Object({
    data: Type.Array(Type.Number()),
  }),
  async execute(_id, params) {
    const chartImage = await generateChart(params.data);

    return {
      content: [
        {
          type: "text",
          text: "图表已生成:",
        },
        {
          type: "image",
          data: chartImage.toString("base64"),
        },
      ],
    };
  },
});

结构化数据

api.registerTool({
  name: "query_database",
  description: "查询数据库",
  parameters: Type.Object({
    query: Type.String(),
  }),
  async execute(_id, params) {
    const results = await db.query(params.query);

    return {
      content: [
        {
          type: "text",
          text: `找到 ${results.length} 条记录`,
        },
      ],
      details: {
        count: results.length,
        rows: results,
        executionTime: 123,
      },
    };
  },
});

错误处理

优雅的错误处理

api.registerTool({
  name: "api_call",
  description: "调用外部 API",
  parameters: Type.Object({
    endpoint: Type.String(),
  }),
  async execute(_id, params) {
    try {
      const response = await fetch(params.endpoint);

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `API 错误: ${response.status} ${response.statusText}`,
            },
          ],
        };
      }

      const data = await response.json();
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(data, null, 2),
          },
        ],
      };
    } catch (err) {
      api.logger.error(`API call failed: ${err}`);
      return {
        content: [
          {
            type: "text",
            text: `请求失败: ${err instanceof Error ? err.message : String(err)}`,
          },
        ],
      };
    }
  },
});

工具配置

按供应商配置

{
  tools: {
    byProvider: {
      anthropic: {
        allow: ["voice_call", "search_files"],
      },
      openai: {
        deny: ["dangerous_operation"],
      },
    },
  },
}

配置文件

{
  tools: {
    profile: "extended", // 预定义的工具集
    allow: [
      // 额外允许的工具
      "my_custom_tool",
    ],
    deny: [
      // 明确拒绝的工具
      "dangerous_tool",
    ],
  },
}
可用的配置文件:
  • minimal - 仅核心工具
  • standard - 标准工具集
  • extended - 包含更多工具

沙盒限制

{
  tools: {
    sandbox: {
      tools: {
        allow: ["safe_tool_1", "safe_tool_2"],
      },
    },
  },
}

最佳实践

1

清晰的描述

编写详细的工具描述,帮助 LLM 正确使用:
description: "搜索文件系统中的文件。支持通配符和正则表达式。返回匹配文件的路径列表。仅搜索工作区目录内的文件。"
2

验证参数

在工具中验证参数,即使 schema 已验证:
async execute(_id, params) {
  if (!params.path || typeof params.path !== 'string') {
    return { content: [{ type: "text", text: "无效的路径参数" }] };
  }
  // ...
}
3

处理错误

捕获并返回有意义的错误消息:
try {
  // 操作
} catch (err) {
  api.logger.error(`Tool error: ${err}`);
  return {
    content: [{
      type: "text",
      text: `操作失败: ${err.message}`
    }]
  };
}
4

记录日志

记录重要的工具调用:
async execute(_id, params, ctx) {
  api.logger.info(`Tool ${name} called by ${ctx.requesterSenderId}`);
  // ...
}
5

可选工具

对于危险或资源密集的工具,使用 optional: true:
api.registerTool(
  { /* tool definition */ },
  { optional: true }
);
6

返回结构化数据

使用 details 字段返回结构化数据供其他工具使用:
return {
  content: [{ type: "text", text: "结果摘要" }],
  details: { full: "完整数据" }
};

示例:完整工具

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";

const FileSearchParams = Type.Object({
  query: Type.String({
    description: "搜索查询(支持通配符)",
    minLength: 1,
  }),
  directory: Type.Optional(
    Type.String({
      description: "要搜索的目录(默认为工作区根目录)",
    })
  ),
  maxResults: Type.Optional(
    Type.Number({
      description: "最大结果数",
      minimum: 1,
      maximum: 100,
      default: 20,
    })
  ),
});

export default function (api: OpenClawPluginApi) {
  api.registerTool({
    name: "search_files",
    label: "Search Files",
    description:
      "搜索工作区中的文件。支持通配符(*、?)和路径过滤。返回匹配文件的路径列表。",
    parameters: FileSearchParams,

    async execute(_toolCallId, params, ctx) {
      const workspaceDir = ctx.workspaceDir || process.cwd();
      const searchDir = params.directory || workspaceDir;
      const maxResults = params.maxResults || 20;

      try {
        // 验证目录在工作区内
        if (!searchDir.startsWith(workspaceDir)) {
          return {
            content: [
              {
                type: "text",
                text: "错误: 只能搜索工作区内的目录",
              },
            ],
          };
        }

        // 执行搜索
        api.logger.info(
          `搜索文件: ${params.query}${searchDir}`
        );
        const results = await searchFiles(
          searchDir,
          params.query,
          maxResults
        );

        // 格式化结果
        const resultText =
          results.length === 0
            ? "未找到匹配的文件"
            : `找到 ${results.length} 个文件:\n${results.map((f) => `- ${f}`).join("\n")}`;

        return {
          content: [
            {
              type: "text",
              text: resultText,
            },
          ],
          details: {
            count: results.length,
            files: results,
            query: params.query,
            directory: searchDir,
          },
        };
      } catch (err) {
        api.logger.error(`文件搜索失败: ${err}`);
        return {
          content: [
            {
              type: "text",
              text: `搜索失败: ${err instanceof Error ? err.message : String(err)}`,
            },
          ],
        };
      }
    },
  });
}

async function searchFiles(
  directory: string,
  query: string,
  maxResults: number
): Promise<string[]> {
  // 实现文件搜索逻辑
  return [];
}

下一步

创建插件

实践构建完整的插件

Plugin SDK

探索完整的 SDK API

插件清单

理解配置验证规则