import { Type, type TSchema } from "typebox"; import { listChannelPlugins } from "../../channels/plugins/index.js"; import {
channelSupportsMessageCapability,
channelSupportsMessageCapabilityForChannel,
type ChannelMessageActionDiscoveryInput,
listCrossChannelSchemaSupportedMessageActions,
resolveChannelMessageToolSchemaProperties,
} from "../../channels/plugins/message-action-discovery.js"; import { CHANNEL_MESSAGE_ACTION_NAMES } from "../../channels/plugins/message-action-names.js"; import type { ChannelMessageCapability } from "../../channels/plugins/message-capabilities.js"; import type { ChannelMessageActionName } from "../../channels/plugins/types.public.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 { loadConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/types.openclaw.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 { normalizeOptionalString } from "../../shared/string-coerce.js"; import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { resolveSessionAgentId } from "../agent-scope.js"; import { listAllChannelSupportedActions, 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 MESSAGE_TOOL_THREAD_READ_HINT = ' Use action="read" with threadId to fetch prior messages in a thread when you need conversation context you do not have yet.'; const EXPLICIT_TARGET_ACTIONS = new Set<ChannelMessageActionName>([ "send", "sendWithEffect", "sendAttachment", "upload-file", "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()),
};
}
function buildSendSchema(options: { includePresentation: boolean; includeDeliveryPin: 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).",
}),
),
asDocument: Type.Optional(
Type.Boolean({
description: "Send image/GIF as document to avoid Telegram compression. Alias for forceDocument (Telegram only).",
}),
),
}; if (options.includePresentation) {
props.presentation = Type.Optional(presentationMessageSchema);
} if (options.includeDeliveryPin) {
props.delivery = Type.Optional(
Type.Object(
{
pin: Type.Optional(
Type.Union([
Type.Boolean(),
Type.Object({
enabled: Type.Boolean(),
notify: Type.Optional(Type.Boolean()),
required: Type.Optional(Type.Boolean()),
}),
]),
),
},
{
description: "Shared delivery preferences. pin requests that the sent message be pinned when the channel supports it.",
},
),
);
} 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 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.",
}),
),
};
}
// If we have a current channel, show its actions and list other configured channels if (currentChannel && messageToolDiscoveryParams) { const channelActions = listChannelSupportedActions(
buildMessageActionDiscoveryInput(messageToolDiscoveryParams, currentChannel),
); if (channelActions.length > 0) { // Always include "send" as a base action const allActions = new Set<ChannelMessageActionName | "send">(["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 = listCrossChannelSchemaSupportedMessageActions(
buildMessageActionDiscoveryInput(messageToolDiscoveryParams, plugin.id),
); if (actions.length > 0) { const all = new Set<ChannelMessageActionName | "send">(["send", ...actions]);
otherChannels.push(`${plugin.id} (${Array.from(all).toSorted().join(", ")})`);
}
} if (otherChannels.length > 0) {
desc += ` Other configured channels: ${otherChannels.join(", ")}.`;
}
return {
label: "Message",
name: "message",
displaySummary: "Send and manage messages across configured channels.",
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]);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.