乐于分享
好东西不私藏

OpenClaw工具拆解之canvas+message

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({
actionstringEnum(CANVAS_ACTIONS),
nodeType.Optional(Type.String()),
targetType.Optional(Type.String()),
urlType.Optional(Type.String()),
xType.Optional(Type.Number()),
yType.Optional(Type.Number()),
widthType.Optional(Type.Number()),
heightType.Optional(Type.Number()),
javaScriptType.Optional(Type.String()),
outputFormatType.Optional(Type.String()),
maxWidthType.Optional(Type.Number()),
qualityType.Optional(Type.Number()),
jsonlType.Optional(Type.String()),
jsonlPathType.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.",
parametersCanvasToolSchema,
executeasync (_toolCallId, args) => {
const params = args;

// 1. 解析 action(必填)
const action = readStringParam$1(params, "action", { requiredtrue });
const gatewayOpts = readGatewayCallOptions(params);

// 2. 解析节点 ID
const nodeId = awaitresolveNodeId(gatewayOpts, 
readStringParam$1(params, "node", { trimtrue }), 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 = {
xtypeof params.x === "number" ? params.x : void0,
ytypeof params.y === "number" ? params.y : void0,
widthtypeof params.width === "number" ? params.width : void0,
heighttypeof params.height === "number" ? params.height : void0
                    };

const invokeParams = {};
const presentTarget = readStringParam$1(params, "target", { trimtrue }) ?? 
readStringParam$1(params, "url", { trimtrue });

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({ oktrue });
                }

// === action: hide ===
case"hide":
awaitinvoke("canvas.hide"void0);
returnjsonResult({ oktrue });

// === action: navigate ===
case"navigate":
awaitinvoke("canvas.navigate", { 
urlreadStringParam$1(params, "url", { trimtrue }) ?? 
readStringParam$1(params, "target", {
requiredtrue,
trimtrue,
label"url"
                             }) 
                    });
returnjsonResult({ oktrue });

// === action: eval ===
case"eval": {
const result = (awaitinvoke("canvas.eval", { 
javaScriptreadStringParam$1(params, "javaScript", { requiredtrue }) 
                    }))?.payload?.result;

if (result) {
return {
content: [{
type"text",
text: result
                            }],
details: { result }
                        };
                    }

returnjsonResult({ oktrue });
                }

// === 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",
maxWidthtypeof params.maxWidth === "number" && Number.isFinite(params.maxWidth) ? 
                            params.maxWidth : void0,
qualitytypeof 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({ oktrue });
                }

// === action: a2ui_reset ===
case"a2ui_reset":
awaitinvoke("canvas.a2ui.reset"void0);
returnjsonResult({ oktrue });

// === 未知 action ===
default:
thrownewError(`Unknown action: ${action}`);
            }
        }
    };
}

1.4 支持的 Actions

Action
说明
必需参数
可选参数
present
显示 Canvas
target/url, x, y, width, height
hide
隐藏 Canvas
navigate
导航到 URL
url/target
eval
执行 JavaScript
javaScript
snapshot
截图
outputFormat, maxWidth, quality
a2ui_push
推送 A2UI
jsonl/jsonlPath
a2ui_reset
重置 A2UI

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.",
descriptionbuildMessageToolDescription({
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,
executeasync (_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", { requiredtrue });

// 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({
gatewayUrlreadStringParam$1(params, "gatewayUrl", { trimfalse }),
gatewayTokenreadStringParam$1(params, "gatewayToken", { trimfalse }),
timeoutMsreadNumberParam(params, "timeoutMs")
            });

const gateway = {
url: gatewayResolved.url,
token: gatewayResolved.token,
timeoutMs: gatewayResolved.timeoutMs,
clientNameGATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
clientDisplayName"agent",
modeGATEWAY_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,
skipCrossContextDecorationtrue
            } : 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 支持的消息操作

Action
说明
必需参数
send
发送消息
text/content, target/channel
reply
回复消息
text, replyTo
react
添加表情反应
reaction, target
edit
编辑消息
text, messageId
unsend
撤回消息
messageId
delete
删除消息
messageId
pin
置顶消息
messageId
unpin
取消置顶
messageId
broadcast
广播消息
text, targets
poll
创建投票
question, options
poll-vote
投票
pollId, option
更多操作

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 功能定位

特性
canvas
message
用途
屏幕共享/演示
跨渠道消息
目标
节点 Canvas
消息渠道
媒体支持
截图
不支持

3.2 操作类型

特性
canvas
message
actions 数量
7 个
30+ 个
创建操作
present
send/broadcast
执行代码
eval
不支持

3.3 安全限制

限制类型
canvas
message
所有者限制
不需要
不需要
秘密解析
不需要
resolveSecretRefs
显式目标
不需要
requireExplicitTarget
中止信号
不支持
abortSignal

四、使用示例

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
}