Files
openclaw/src/agents/tools/message-tool.ts
2026-03-18 02:34:33 +00:00

791 lines
27 KiB
TypeScript

import { Type, type TSchema } from "@sinclair/typebox";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import {
channelSupportsMessageCapability,
channelSupportsMessageCapabilityForChannel,
listChannelMessageActions,
resolveChannelMessageToolSchemaProperties,
} from "../../channels/plugins/message-action-discovery.js";
import type { ChannelMessageCapability } from "../../channels/plugins/message-capabilities.js";
import {
CHANNEL_MESSAGE_ACTION_NAMES,
type ChannelMessageActionName,
} from "../../channels/plugins/types.js";
import { resolveCommandSecretRefsViaGateway } from "../../cli/command-secret-gateway.js";
import { getScopedChannelsCommandSecretTargets } from "../../cli/command-secret-targets.js";
import { resolveMessageSecretScope } from "../../cli/message-secret-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol/client-info.js";
import { getToolResult, runMessageAction } from "../../infra/outbound/message-action-runner.js";
import { POLL_CREATION_PARAM_DEFS, SHARED_POLL_CREATION_PARAM_NAMES } from "../../poll-params.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js";
import { normalizeMessageChannel } from "../../utils/message-channel.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { listChannelSupportedActions } from "../channel-tools.js";
import { channelTargetSchema, channelTargetsSchema, stringEnum } from "../schema/typebox.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
import { resolveGatewayOptions } from "./gateway.js";
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
const EXPLICIT_TARGET_ACTIONS = new Set<ChannelMessageActionName>([
"send",
"sendWithEffect",
"sendAttachment",
"reply",
"thread-reply",
"broadcast",
]);
function actionNeedsExplicitTarget(action: ChannelMessageActionName): boolean {
return EXPLICIT_TARGET_ACTIONS.has(action);
}
function buildRoutingSchema() {
return {
channel: Type.Optional(Type.String()),
target: Type.Optional(channelTargetSchema({ description: "Target channel/user id or name." })),
targets: Type.Optional(channelTargetsSchema()),
accountId: Type.Optional(Type.String()),
dryRun: Type.Optional(Type.Boolean()),
};
}
const interactiveOptionSchema = Type.Object({
label: Type.String(),
value: Type.String(),
});
const interactiveButtonSchema = Type.Object({
label: Type.String(),
value: Type.String(),
style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger"])),
});
const interactiveBlockSchema = Type.Object({
type: stringEnum(["text", "buttons", "select"]),
text: Type.Optional(Type.String()),
buttons: Type.Optional(Type.Array(interactiveButtonSchema)),
placeholder: Type.Optional(Type.String()),
options: Type.Optional(Type.Array(interactiveOptionSchema)),
});
const interactiveMessageSchema = Type.Object(
{
blocks: Type.Array(interactiveBlockSchema),
},
{
description:
"Shared interactive message payload for buttons and selects. Channels render this into their native components when supported.",
},
);
function buildSendSchema(options: { includeInteractive: boolean }) {
const props: Record<string, TSchema> = {
message: Type.Optional(Type.String()),
effectId: Type.Optional(
Type.String({
description: "Message effect name/id for sendWithEffect (e.g., invisible ink).",
}),
),
effect: Type.Optional(
Type.String({ description: "Alias for effectId (e.g., invisible-ink, balloons)." }),
),
media: Type.Optional(
Type.String({
description: "Media URL or local path. data: URLs are not supported here, use buffer.",
}),
),
filename: Type.Optional(Type.String()),
buffer: Type.Optional(
Type.String({
description: "Base64 payload for attachments (optionally a data: URL).",
}),
),
contentType: Type.Optional(Type.String()),
mimeType: Type.Optional(Type.String()),
caption: Type.Optional(Type.String()),
path: Type.Optional(Type.String()),
filePath: Type.Optional(Type.String()),
replyTo: Type.Optional(Type.String()),
threadId: Type.Optional(Type.String()),
asVoice: Type.Optional(Type.Boolean()),
silent: Type.Optional(Type.Boolean()),
quoteText: Type.Optional(
Type.String({ description: "Quote text for Telegram reply_parameters" }),
),
bestEffort: Type.Optional(Type.Boolean()),
gifPlayback: Type.Optional(Type.Boolean()),
forceDocument: Type.Optional(
Type.Boolean({
description: "Send image/GIF as document to avoid Telegram compression (Telegram only).",
}),
),
interactive: Type.Optional(interactiveMessageSchema),
};
if (!options.includeInteractive) {
delete props.interactive;
}
return props;
}
function buildReactionSchema() {
return {
messageId: Type.Optional(
Type.String({
description:
"Target message id for reaction. If omitted, defaults to the current inbound message id when available.",
}),
),
message_id: Type.Optional(
Type.String({
// Intentional duplicate alias for tool-schema discoverability in LLMs.
description:
"snake_case alias of messageId. If omitted, defaults to the current inbound message id when available.",
}),
),
emoji: Type.Optional(Type.String()),
remove: Type.Optional(Type.Boolean()),
targetAuthor: Type.Optional(Type.String()),
targetAuthorUuid: Type.Optional(Type.String()),
groupId: Type.Optional(Type.String()),
};
}
function buildFetchSchema() {
return {
limit: Type.Optional(Type.Number()),
pageSize: Type.Optional(Type.Number()),
pageToken: Type.Optional(Type.String()),
before: Type.Optional(Type.String()),
after: Type.Optional(Type.String()),
around: Type.Optional(Type.String()),
fromMe: Type.Optional(Type.Boolean()),
includeArchived: Type.Optional(Type.Boolean()),
};
}
function buildPollSchema() {
const props: Record<string, TSchema> = {
pollId: Type.Optional(Type.String()),
pollOptionId: Type.Optional(
Type.String({
description: "Poll answer id to vote for. Use when the channel exposes stable answer ids.",
}),
),
pollOptionIds: Type.Optional(
Type.Array(
Type.String({
description:
"Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.",
}),
),
),
pollOptionIndex: Type.Optional(
Type.Number({
description:
"1-based poll option number to vote for, matching the rendered numbered poll choices.",
}),
),
pollOptionIndexes: Type.Optional(
Type.Array(
Type.Number({
description:
"1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.",
}),
),
),
};
for (const name of SHARED_POLL_CREATION_PARAM_NAMES) {
const def = POLL_CREATION_PARAM_DEFS[name];
switch (def.kind) {
case "string":
props[name] = Type.Optional(Type.String());
break;
case "stringArray":
props[name] = Type.Optional(Type.Array(Type.String()));
break;
case "number":
props[name] = Type.Optional(Type.Number());
break;
case "boolean":
props[name] = Type.Optional(Type.Boolean());
break;
}
}
return props;
}
function buildChannelTargetSchema() {
return {
channelId: Type.Optional(
Type.String({ description: "Channel id filter (search/thread list/event create)." }),
),
chatId: Type.Optional(
Type.String({ description: "Chat id for chat-scoped metadata actions." }),
),
channelIds: Type.Optional(
Type.Array(Type.String({ description: "Channel id filter (repeatable)." })),
),
memberId: Type.Optional(Type.String()),
memberIdType: Type.Optional(Type.String()),
guildId: Type.Optional(Type.String()),
userId: Type.Optional(Type.String()),
openId: Type.Optional(Type.String()),
unionId: Type.Optional(Type.String()),
authorId: Type.Optional(Type.String()),
authorIds: Type.Optional(Type.Array(Type.String())),
roleId: Type.Optional(Type.String()),
roleIds: Type.Optional(Type.Array(Type.String())),
participant: Type.Optional(Type.String()),
includeMembers: Type.Optional(Type.Boolean()),
members: Type.Optional(Type.Boolean()),
scope: Type.Optional(Type.String()),
kind: Type.Optional(Type.String()),
};
}
function buildStickerSchema() {
return {
emojiName: Type.Optional(Type.String()),
stickerId: Type.Optional(Type.Array(Type.String())),
stickerName: Type.Optional(Type.String()),
stickerDesc: Type.Optional(Type.String()),
stickerTags: Type.Optional(Type.String()),
};
}
function buildThreadSchema() {
return {
threadName: Type.Optional(Type.String()),
autoArchiveMin: Type.Optional(Type.Number()),
appliedTags: Type.Optional(Type.Array(Type.String())),
};
}
function buildEventSchema() {
return {
query: Type.Optional(Type.String()),
eventName: Type.Optional(Type.String()),
eventType: Type.Optional(Type.String()),
startTime: Type.Optional(Type.String()),
endTime: Type.Optional(Type.String()),
desc: Type.Optional(Type.String()),
location: Type.Optional(Type.String()),
durationMin: Type.Optional(Type.Number()),
until: Type.Optional(Type.String()),
};
}
function buildModerationSchema() {
return {
reason: Type.Optional(Type.String()),
deleteDays: Type.Optional(Type.Number()),
};
}
function buildGatewaySchema() {
return {
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
};
}
function buildPresenceSchema() {
return {
activityType: Type.Optional(
Type.String({
description: "Activity type: playing, streaming, listening, watching, competing, custom.",
}),
),
activityName: Type.Optional(
Type.String({
description: "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.",
}),
),
activityUrl: Type.Optional(
Type.String({
description:
"Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.",
}),
),
activityState: Type.Optional(
Type.String({
description:
"State text. For custom type this is the status text; for others it shows in the flyout.",
}),
),
status: Type.Optional(
Type.String({ description: "Bot status: online, dnd, idle, invisible." }),
),
};
}
function buildChannelManagementSchema() {
return {
name: Type.Optional(Type.String()),
type: Type.Optional(Type.Number()),
parentId: Type.Optional(Type.String()),
topic: Type.Optional(Type.String()),
position: Type.Optional(Type.Number()),
nsfw: Type.Optional(Type.Boolean()),
rateLimitPerUser: Type.Optional(Type.Number()),
categoryId: Type.Optional(Type.String()),
clearParent: Type.Optional(
Type.Boolean({
description: "Clear the parent/category when supported by the provider.",
}),
),
};
}
function buildMessageToolSchemaProps(options: {
includeInteractive: boolean;
extraProperties?: Record<string, TSchema>;
}) {
return {
...buildRoutingSchema(),
...buildSendSchema(options),
...buildReactionSchema(),
...buildFetchSchema(),
...buildPollSchema(),
...buildChannelTargetSchema(),
...buildStickerSchema(),
...buildThreadSchema(),
...buildEventSchema(),
...buildModerationSchema(),
...buildGatewaySchema(),
...buildChannelManagementSchema(),
...buildPresenceSchema(),
...options.extraProperties,
};
}
function buildMessageToolSchemaFromActions(
actions: readonly string[],
options: {
includeInteractive: boolean;
extraProperties?: Record<string, TSchema>;
},
) {
const props = buildMessageToolSchemaProps(options);
return Type.Object({
action: stringEnum(actions),
...props,
});
}
const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
includeInteractive: true,
});
type MessageToolOptions = {
agentAccountId?: string;
agentSessionKey?: string;
sessionId?: string;
config?: OpenClawConfig;
currentChannelId?: string;
currentChannelProvider?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
sandboxRoot?: string;
requireExplicitTarget?: boolean;
requesterSenderId?: string;
};
function resolveMessageToolSchemaActions(params: {
cfg: OpenClawConfig;
currentChannelProvider?: string;
currentChannelId?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
currentAccountId?: string;
sessionKey?: string;
sessionId?: string;
agentId?: string;
requesterSenderId?: string;
}): string[] {
const currentChannel = normalizeMessageChannel(params.currentChannelProvider);
if (currentChannel) {
const scopedActions = listChannelSupportedActions({
cfg: params.cfg,
channel: currentChannel,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
accountId: params.currentAccountId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.agentId,
requesterSenderId: params.requesterSenderId,
});
const allActions = new Set<string>(["send", ...scopedActions]);
// Include actions from other configured channels so isolated/cron agents
// can invoke cross-channel actions without validation errors.
for (const plugin of listChannelPlugins()) {
if (plugin.id === currentChannel) {
continue;
}
for (const action of listChannelSupportedActions({
cfg: params.cfg,
channel: plugin.id,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
accountId: params.currentAccountId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.agentId,
requesterSenderId: params.requesterSenderId,
})) {
allActions.add(action);
}
}
return Array.from(allActions);
}
const actions = listChannelMessageActions(params.cfg);
return actions.length > 0 ? actions : ["send"];
}
function resolveIncludeCapability(
params: {
cfg: OpenClawConfig;
currentChannelProvider?: string;
currentChannelId?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
currentAccountId?: string;
sessionKey?: string;
sessionId?: string;
agentId?: string;
requesterSenderId?: string;
},
capability: ChannelMessageCapability,
): boolean {
const currentChannel = normalizeMessageChannel(params.currentChannelProvider);
if (currentChannel) {
return channelSupportsMessageCapabilityForChannel(
{
cfg: params.cfg,
channel: currentChannel,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
accountId: params.currentAccountId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.agentId,
requesterSenderId: params.requesterSenderId,
},
capability,
);
}
return channelSupportsMessageCapability(params.cfg, capability);
}
function resolveIncludeInteractive(params: {
cfg: OpenClawConfig;
currentChannelProvider?: string;
currentChannelId?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
currentAccountId?: string;
sessionKey?: string;
sessionId?: string;
agentId?: string;
requesterSenderId?: string;
}): boolean {
return resolveIncludeCapability(params, "interactive");
}
function buildMessageToolSchema(params: {
cfg: OpenClawConfig;
currentChannelProvider?: string;
currentChannelId?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
currentAccountId?: string;
sessionKey?: string;
sessionId?: string;
agentId?: string;
requesterSenderId?: string;
}) {
const actions = resolveMessageToolSchemaActions(params);
const includeInteractive = resolveIncludeInteractive(params);
const extraProperties = resolveChannelMessageToolSchemaProperties({
cfg: params.cfg,
channel: normalizeMessageChannel(params.currentChannelProvider),
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
accountId: params.currentAccountId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.agentId,
requesterSenderId: params.requesterSenderId,
});
return buildMessageToolSchemaFromActions(actions.length > 0 ? actions : ["send"], {
includeInteractive,
extraProperties,
});
}
function resolveAgentAccountId(value?: string): string | undefined {
const trimmed = value?.trim();
if (!trimmed) {
return undefined;
}
return normalizeAccountId(trimmed);
}
function buildMessageToolDescription(options?: {
config?: OpenClawConfig;
currentChannel?: string;
currentChannelId?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
currentAccountId?: string;
sessionKey?: string;
sessionId?: string;
agentId?: string;
requesterSenderId?: string;
}): string {
const baseDescription = "Send, delete, and manage messages via channel plugins.";
const resolvedOptions = options ?? {};
const currentChannel = normalizeMessageChannel(resolvedOptions.currentChannel);
// If we have a current channel, show its actions and list other configured channels
if (currentChannel) {
const channelActions = listChannelSupportedActions({
cfg: resolvedOptions.config,
channel: currentChannel,
currentChannelId: resolvedOptions.currentChannelId,
currentThreadTs: resolvedOptions.currentThreadTs,
currentMessageId: resolvedOptions.currentMessageId,
accountId: resolvedOptions.currentAccountId,
sessionKey: resolvedOptions.sessionKey,
sessionId: resolvedOptions.sessionId,
agentId: resolvedOptions.agentId,
requesterSenderId: resolvedOptions.requesterSenderId,
});
if (channelActions.length > 0) {
// Always include "send" as a base action
const allActions = new Set(["send", ...channelActions]);
const actionList = Array.from(allActions).toSorted().join(", ");
let desc = `${baseDescription} Current channel (${currentChannel}) supports: ${actionList}.`;
// Include other configured channels so cron/isolated agents can discover them
const otherChannels: string[] = [];
for (const plugin of listChannelPlugins()) {
if (plugin.id === currentChannel) {
continue;
}
const actions = listChannelSupportedActions({
cfg: resolvedOptions.config,
channel: plugin.id,
currentChannelId: resolvedOptions.currentChannelId,
currentThreadTs: resolvedOptions.currentThreadTs,
currentMessageId: resolvedOptions.currentMessageId,
accountId: resolvedOptions.currentAccountId,
sessionKey: resolvedOptions.sessionKey,
sessionId: resolvedOptions.sessionId,
agentId: resolvedOptions.agentId,
requesterSenderId: resolvedOptions.requesterSenderId,
});
if (actions.length > 0) {
const all = new Set(["send", ...actions]);
otherChannels.push(`${plugin.id} (${Array.from(all).toSorted().join(", ")})`);
}
}
if (otherChannels.length > 0) {
desc += ` Other configured channels: ${otherChannels.join(", ")}.`;
}
return desc;
}
}
// Fallback to generic description with all configured actions
if (resolvedOptions.config) {
const actions = listChannelMessageActions(resolvedOptions.config);
if (actions.length > 0) {
return `${baseDescription} Supports actions: ${actions.join(", ")}.`;
}
}
return `${baseDescription} Supports actions: send, delete, react, poll, pin, threads, and more.`;
}
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
const resolvedAgentId = options?.agentSessionKey
? resolveSessionAgentId({
sessionKey: options.agentSessionKey,
config: options?.config,
})
: undefined;
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;
const 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,
});
return {
label: "Message",
name: "message",
description,
parameters: schema,
execute: async (_toolCallId, args, signal) => {
// Check if already aborted before doing any work
if (signal?.aborted) {
const err = new Error("Message send aborted");
err.name = "AbortError";
throw err;
}
// Shallow-copy so we don't mutate the original event args (used for logging/dedup).
const params = { ...(args as Record<string, unknown>) };
// Strip reasoning tags from text fields — models may include <think>…</think>
// in tool arguments, and the messaging tool send path has no other tag filtering.
for (const field of ["text", "content", "message", "caption"]) {
if (typeof params[field] === "string") {
params[field] = stripReasoningTagsFromText(params[field]);
}
}
const action = readStringParam(params, "action", {
required: true,
}) as ChannelMessageActionName;
let cfg = options?.config;
if (!cfg) {
const loadedRaw = loadConfig();
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 = (
await resolveCommandSecretRefsViaGateway({
config: loadedRaw,
commandName: "tools.message",
targetIds: scopedTargets.targetIds,
...(scopedTargets.allowedPaths ? { allowedPaths: scopedTargets.allowedPaths } : {}),
mode: "enforce_resolved",
})
).resolvedConfig;
}
const requireExplicitTarget = options?.requireExplicitTarget === true;
if (requireExplicitTarget && actionNeedsExplicitTarget(action)) {
const explicitTarget =
(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));
if (!explicitTarget) {
throw new Error(
"Explicit message target required for this run. Provide target/targets (and channel when needed).",
);
}
}
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
if (accountId) {
params.accountId = accountId;
}
const gatewayResolved = resolveGatewayOptions({
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
gatewayToken: readStringParam(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,
};
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,
// Direct tool invocations should not add cross-context decoration.
// The agent is composing a message, not forwarding from another chat.
skipCrossContextDecoration: true,
}
: undefined;
const result = await runMessageAction({
cfg,
action,
params,
defaultAccountId: accountId ?? undefined,
requesterSenderId: options?.requesterSenderId,
gateway,
toolContext,
sessionKey: options?.agentSessionKey,
sessionId: options?.sessionId,
agentId: resolvedAgentId,
sandboxRoot: options?.sandboxRoot,
abortSignal: signal,
});
const toolResult = getToolResult(result);
if (toolResult) {
return toolResult;
}
return jsonResult(result.payload);
},
};
}