插件代理工具
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();
}
工具定义
工具的唯一名称。LLM 将使用此名称调用工具。命名规范:
- 使用 snake_case
- 清晰描述功能
- 避免与核心工具冲突
voice_call, search_files, send_email工具的显示名称。用于 UI 和日志。示例: “Voice Call”, “Search Files”
工具的详细描述。LLM 使用此信息决定何时调用工具。编写技巧:
- 清楚说明工具的功能
- 说明适用场景
- 提及重要的限制或要求
description: "发起语音通话并朗读消息。支持通知模式(单向)和对话模式(双向)。需要配置电话服务提供商。"
工具参数的 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 })
),
})
工具执行函数。接收参数并返回结果。签名:返回值:
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"],
},
},
},
}
最佳实践
验证参数
在工具中验证参数,即使 schema 已验证:
async execute(_id, params) {
if (!params.path || typeof params.path !== 'string') {
return { content: [{ type: "text", text: "无效的路径参数" }] };
}
// ...
}
处理错误
捕获并返回有意义的错误消息:
try {
// 操作
} catch (err) {
api.logger.error(`Tool error: ${err}`);
return {
content: [{
type: "text",
text: `操作失败: ${err.message}`
}]
};
}
记录日志
记录重要的工具调用:
async execute(_id, params, ctx) {
api.logger.info(`Tool ${name} called by ${ctx.requesterSenderId}`);
// ...
}
可选工具
对于危险或资源密集的工具,使用
optional: true:api.registerTool(
{ /* tool definition */ },
{ optional: true }
);
示例:完整工具
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
插件清单
理解配置验证规则