OpenClaw工具拆解之canvas+message
一、canvas 工具
1.1 工具概述
功能:控制节点 Canvas(屏幕共享/演示)
核心特性:
-
• 支持多种操作(present/hide/navigate/eval/snapshot/A2UI) -
• 支持截图(PNG/JPEG) -
• 支持 JavaScript 执行 -
• 支持 A2UI 推送(JSONL 格式)
1.2 Schema 定义
位置:第 24388 行
constCanvasToolSchema = Type.Object({
action: stringEnum(CANVAS_ACTIONS),
node: Type.Optional(Type.String()),
target: Type.Optional(Type.String()),
url: Type.Optional(Type.String()),
x: Type.Optional(Type.Number()),
y: Type.Optional(Type.Number()),
width: Type.Optional(Type.Number()),
height: Type.Optional(Type.Number()),
javaScript: Type.Optional(Type.String()),
outputFormat: Type.Optional(Type.String()),
maxWidth: Type.Optional(Type.Number()),
quality: Type.Optional(Type.Number()),
jsonl: Type.Optional(Type.String()),
jsonlPath: Type.Optional(Type.String())
});
1.3 完整执行代码
位置:第 24408 行
functioncreateCanvasTool(options) {
const imageSanitization = resolveImageSanitizationLimits(options?.config);
return {
label: "Canvas",
name: "canvas",
description: "Control node canvases (present/hide/navigate/eval/snapshot/A2UI). Use snapshot to capture the rendered UI.",
parameters: CanvasToolSchema,
execute: async (_toolCallId, args) => {
const params = args;
// 1. 解析 action(必填)
const action = readStringParam$1(params, "action", { required: true });
const gatewayOpts = readGatewayCallOptions(params);
// 2. 解析节点 ID
const nodeId = awaitresolveNodeId(gatewayOpts,
readStringParam$1(params, "node", { trim: true }), true);
// 3. 构建调用函数
constinvoke = async (command, invokeParams) => awaitcallGatewayTool("node.invoke", gatewayOpts, {
nodeId,
command,
params: invokeParams,
idempotencyKey: crypto$1.randomUUID()
});
switch (action) {
// === action: present ===
case"present": {
const placement = {
x: typeof params.x === "number" ? params.x : void0,
y: typeof params.y === "number" ? params.y : void0,
width: typeof params.width === "number" ? params.width : void0,
height: typeof params.height === "number" ? params.height : void0
};
const invokeParams = {};
const presentTarget = readStringParam$1(params, "target", { trim: true }) ??
readStringParam$1(params, "url", { trim: true });
if (presentTarget) {
invokeParams.url = presentTarget;
}
if (Number.isFinite(placement.x) || Number.isFinite(placement.y) ||
Number.isFinite(placement.width) || Number.isFinite(placement.height)) {
invokeParams.placement = placement;
}
awaitinvoke("canvas.present", invokeParams);
returnjsonResult({ ok: true });
}
// === action: hide ===
case"hide":
awaitinvoke("canvas.hide", void0);
returnjsonResult({ ok: true });
// === action: navigate ===
case"navigate":
awaitinvoke("canvas.navigate", {
url: readStringParam$1(params, "url", { trim: true }) ??
readStringParam$1(params, "target", {
required: true,
trim: true,
label: "url"
})
});
returnjsonResult({ ok: true });
// === action: eval ===
case"eval": {
const result = (awaitinvoke("canvas.eval", {
javaScript: readStringParam$1(params, "javaScript", { required: true })
}))?.payload?.result;
if (result) {
return {
content: [{
type: "text",
text: result
}],
details: { result }
};
}
returnjsonResult({ ok: true });
}
// === action: snapshot ===
case"snapshot": {
const formatRaw = typeof params.outputFormat === "string" ?
params.outputFormat.toLowerCase() : "png";
// 调用截图
const payload = parseCanvasSnapshotPayload((awaitinvoke("canvas.snapshot", {
format: formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png",
maxWidth: typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth) ?
params.maxWidth : void0,
quality: typeof params.quality === "number" && Number.isFinite(params.quality) ?
params.quality : void0
}))?.payload);
// 保存到临时文件
const filePath = canvasSnapshotTempPath({
ext: payload.format === "jpeg" ? "jpg" : payload.format
});
awaitwriteBase64ToFile(filePath, payload.base64);
const mimeType = imageMimeFromFormat(payload.format) ?? "image/png";
// 返回图片结果
returnawaitimageResult({
label: "canvas:snapshot",
path: filePath,
base64: payload.base64,
mimeType,
details: { format: payload.format },
imageSanitization
});
}
// === action: a2ui_push ===
case"a2ui_push": {
const jsonl = typeof params.jsonl === "string" && params.jsonl.trim() ?
params.jsonl :
typeof params.jsonlPath === "string" && params.jsonlPath.trim() ?
awaitreadJsonlFromPath(params.jsonlPath) : "";
if (!jsonl.trim()) {
thrownewError("jsonl or jsonlPath required");
}
awaitinvoke("canvas.a2ui.pushJSONL", { jsonl });
returnjsonResult({ ok: true });
}
// === action: a2ui_reset ===
case"a2ui_reset":
awaitinvoke("canvas.a2ui.reset", void0);
returnjsonResult({ ok: true });
// === 未知 action ===
default:
thrownewError(`Unknown action: ${action}`);
}
}
};
}
1.4 支持的 Actions
|
|
|
|
|
present |
|
|
|
hide |
|
|
|
navigate |
|
|
|
eval |
|
|
|
snapshot |
|
|
|
a2ui_push |
|
|
|
a2ui_reset |
|
|
|
1.5 执行流程图
canvas 工具调用
↓
1. 解析 action(必填)
↓
2. 解析节点 ID
↓
3. 根据 action 执行
├─ present → 显示 Canvas(URL + 位置)
├─ hide → 隐藏 Canvas
├─ navigate → 导航到新 URL
├─ eval → 执行 JavaScript
│ └─ 返回执行结果
├─ snapshot → 截图
│ ├─ 调用截图命令
│ ├─ 解析 payload
│ ├─ 保存临时文件
│ └─ 返回图片
├─ a2ui_push → 推送 JSONL
└─ a2ui_reset → 重置 A2UI
↓
4. 返回结果
1.6 返回结果格式
present/hide/navigate/a2ui_reset:
{
"ok":true
}
eval 成功:
{
"content":[{
"type":"text",
"text":"JavaScript 执行结果"
}],
"details":{
"result":"JavaScript 执行结果"
}
}
snapshot 成功:
{
"content":[
{"type":"text","text":"canvas:snapshot"},
{
"type":"image",
"data":"base64...",
"mimeType":"image/png"
}
],
"details":{
"format":"png",
"path":"/tmp/canvas-snapshot-xxx.png"
}
}
二、message 工具
2.1 工具概述
功能:跨渠道发送和管理消息
核心特性:
-
• 支持多种消息操作(send/reply/react/edit/unsend 等) -
• 支持秘密解析(resolveSecretRefs) -
• 支持上下文装饰(toolContext) -
• 支持中止信号(abortSignal) -
• 自动剥离推理标签
2.2 Schema 构建
位置:第 102938 行
// Schema 是动态构建的,取决于配置
const schema = options?.config ? buildMessageToolSchema({
cfg: options.config,
currentChannelProvider: options.currentChannelProvider,
currentChannelId: options.currentChannelId,
currentThreadTs: options.currentThreadTs,
currentMessageId: options.currentMessageId,
currentAccountId: agentAccountId,
sessionKey: options.agentSessionKey,
sessionId: options.sessionId,
agentId: resolvedAgentId,
requesterSenderId: options.requesterSenderId
}) : MessageToolSchema;
2.3 完整执行代码
位置:第 103069 行
functioncreateMessageTool(options) {
// 1. 解析依赖函数
const loadConfigForTool = options?.loadConfig ?? loadConfig;
const resolveSecretRefsForTool = options?.resolveCommandSecretRefsViaGateway ?? resolveCommandSecretRefsViaGateway;
const runMessageActionForTool = options?.runMessageAction ?? runMessageAction;
// 2. 解析 Agent 账户 ID
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
const resolvedAgentId = options?.agentSessionKey ? resolveSessionAgentId({
sessionKey: options.agentSessionKey,
config: options?.config
}) : void0;
// 3. 构建 Schema
const schema = options?.config ? buildMessageToolSchema({
cfg: options.config,
currentChannelProvider: options.currentChannelProvider,
currentChannelId: options.currentChannelId,
currentThreadTs: options.currentThreadTs,
currentMessageId: options.currentMessageId,
currentAccountId: agentAccountId,
sessionKey: options.agentSessionKey,
sessionId: options.sessionId,
agentId: resolvedAgentId,
requesterSenderId: options.requesterSenderId
}) : MessageToolSchema;
return {
label: "Message",
name: "message",
displaySummary: "Send and manage messages across configured channels.",
description: buildMessageToolDescription({
config: options?.config,
currentChannel: options?.currentChannelProvider,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
currentAccountId: agentAccountId,
sessionKey: options?.agentSessionKey,
sessionId: options?.sessionId,
agentId: resolvedAgentId,
requesterSenderId: options?.requesterSenderId
}),
parameters: schema,
execute: async (_toolCallId, args, signal) => {
// 4. 检查中止信号
if (signal?.aborted) {
const err = newError("Message send aborted");
err.name = "AbortError";
throw err;
}
const params = { ...args };
// 5. 剥离推理标签
for (const field of ["text", "content", "message", "caption"]) {
if (typeof params[field] === "string") {
params[field] = stripReasoningTagsFromText(params[field]);
}
}
// 6. 解析 action(必填)
const action = readStringParam$1(params, "action", { required: true });
// 7. 解析配置(如果需要秘密解析)
let cfg = options?.config;
if (!cfg) {
const loadedRaw = loadConfigForTool();
const scope = resolveMessageSecretScope({
channel: params.channel,
target: params.target,
targets: params.targets,
fallbackChannel: options?.currentChannelProvider,
accountId: params.accountId,
fallbackAccountId: agentAccountId
});
const scopedTargets = getScopedChannelsCommandSecretTargets({
config: loadedRaw,
channel: scope.channel,
accountId: scope.accountId
});
cfg = (awaitresolveSecretRefsForTool({
config: loadedRaw,
commandName: "tools.message",
targetIds: scopedTargets.targetIds,
...scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {},
mode: "enforce_resolved"
})).resolvedConfig;
}
// 8. 检查显式目标要求
if (options?.requireExplicitTarget === true && actionNeedsExplicitTarget(action)) {
if (!(
typeof params.target === "string" && params.target.trim().length > 0 ||
typeof params.to === "string" && params.to.trim().length > 0 ||
typeof params.channelId === "string" && params.channelId.trim().length > 0 ||
Array.isArray(params.targets) && params.targets.some((value) =>
typeof value === "string" && value.trim().length > 0
)
)) {
thrownewError("Explicit message target required for this run. Provide target/targets (and channel when needed).");
}
}
// 9. 解析账户 ID
const accountId = readStringParam$1(params, "accountId") ?? agentAccountId;
if (accountId) params.accountId = accountId;
// 10. 解析 Gateway 选项
const gatewayResolved = resolveGatewayOptions$1({
gatewayUrl: readStringParam$1(params, "gatewayUrl", { trim: false }),
gatewayToken: readStringParam$1(params, "gatewayToken", { trim: false }),
timeoutMs: readNumberParam(params, "timeoutMs")
});
const gateway = {
url: gatewayResolved.url,
token: gatewayResolved.token,
timeoutMs: gatewayResolved.timeoutMs,
clientName: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
clientDisplayName: "agent",
mode: GATEWAY_CLIENT_MODES.BACKEND
};
// 11. 构建工具上下文
const hasCurrentMessageId = typeof options?.currentMessageId === "number" ||
typeof options?.currentMessageId === "string" &&
options.currentMessageId.trim().length > 0;
const toolContext = options?.currentChannelId || options?.currentChannelProvider ||
options?.currentThreadTs || hasCurrentMessageId ||
options?.replyToMode || options?.hasRepliedRef ? {
currentChannelId: options?.currentChannelId,
currentChannelProvider: options?.currentChannelProvider,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
skipCrossContextDecoration: true
} : void0;
// 12. 执行消息操作
const result = awaitrunMessageActionForTool({
cfg,
action,
params,
defaultAccountId: accountId ?? void0,
requesterSenderId: options?.requesterSenderId,
gateway,
toolContext,
sessionKey: options?.agentSessionKey,
sessionId: options?.sessionId,
agentId: resolvedAgentId,
sandboxRoot: options?.sandboxRoot,
abortSignal: signal
});
// 13. 返回结果
const toolResult = getToolResult(result);
if (toolResult) return toolResult;
returnjsonResult(result.payload);
}
};
}
2.4 支持的消息操作
|
|
|
|
send |
|
|
reply |
|
|
react |
|
|
edit |
|
|
unsend |
|
|
delete |
|
|
pin |
|
|
unpin |
|
|
broadcast |
|
|
poll |
|
|
poll-vote |
|
|
|
|
|
|
2.5 执行流程图
message 工具调用
↓
1. 检查中止信号
↓
2. 剥离推理标签
↓
3. 解析 action(必填)
↓
4. 解析配置(秘密解析)
↓
5. 检查显式目标要求
↓
6. 解析账户 ID
↓
7. 解析 Gateway 选项
↓
8. 构建工具上下文
↓
9. 执行消息操作
↓
10. 返回结果
2.6 秘密解析流程
// 1. 解析秘密作用域
const scope = resolveMessageSecretScope({
channel: params.channel,
target: params.target,
targets: params.targets,
fallbackChannel: options?.currentChannelProvider,
accountId: params.accountId,
fallbackAccountId: agentAccountId
});
// 2. 获取作用域目标
const scopedTargets = getScopedChannelsCommandSecretTargets({
config: loadedRaw,
channel: scope.channel,
accountId: scope.accountId
});
// 3. 解析秘密引用
const cfg = (awaitresolveSecretRefsForTool({
config: loadedRaw,
commandName: "tools.message",
targetIds: scopedTargets.targetIds,
...scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {},
mode: "enforce_resolved"// 强制解析为已解决的秘密
})).resolvedConfig;
2.7 推理标签剥离
// 从消息内容中剥离推理标签
functionstripReasoningTagsFromText(text) {
if (!text) return text;
// 移除 <thinking>...</thinking>
text = text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "");
// 移除 <reasoning>...</reasoning>
text = text.replace(/<reasoning>[\s\S]*?<\/reasoning>/g, "");
return text.trim();
}
三、关键机制对比
3.1 功能定位
|
|
|
|
| 用途 |
|
|
| 目标 |
|
|
| 媒体支持 |
|
|
3.2 操作类型
|
|
|
|
| actions 数量 |
|
|
| 创建操作 |
|
|
| 执行代码 |
|
|
3.3 安全限制
|
|
|
|
| 所有者限制 |
|
|
| 秘密解析 |
|
|
| 显式目标 |
|
|
| 中止信号 |
|
|
四、使用示例
4.1 canvas 工具调用
用户:截取节点屏幕
大模型返回:
{
"tool_call":{
"name":"canvas",
"arguments":{
"action":"snapshot",
"node":"my-phone",
"outputFormat":"png"
}
}
}
执行结果:
{
"content":[
{"type":"text","text":"canvas:snapshot"},
{
"type":"image",
"data":"base64...",
"mimeType":"image/png"
}
],
"details":{
"format":"png",
"path":"/tmp/canvas-snapshot-xxx.png"
}
}
4.2 message 工具调用
用户:发送消息到飞书群
大模型返回:
{
"tool_call":{
"name":"message",
"arguments":{
"action":"send",
"channel":"feishu",
"target":"oc_xxx",
"text":"大家好!"
}
}
}
执行结果:
{
"ok":true,
"messageId":"om_xxx",
"timestamp":1711716000000
}
夜雨聆风